Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions docs/AUTHORING-HOST-ADAPTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ validator:
},
"driving": { "surfaces": ["cli-subprocess"] },
"lifecycle": {
"detect": { "hook": { "command": ["node", "detect-hook.mjs"], "timeoutMs": 5000 } }
"detect": { "hook": { "command": ["node", "detect-hook.mjs"], "files": ["detect-hook.mjs"], "timeoutMs": 5000 } }
},
"execution": {
"run": { "hook": { "command": ["node", "run-hook.mjs"], "timeoutMs": 120000 } }
"run": { "hook": { "command": ["node", "run-hook.mjs"], "files": ["run-hook.mjs"], "timeoutMs": 120000 } }
},
"trust": {
"changes": [
Expand Down Expand Up @@ -100,7 +100,7 @@ Field by field:
| `host.trust` / `trust.changes` | Up-front disclosure of what your adapter touches. `trust.changes` is what the user reads before consenting. |
| `detection` | How `ak` proves your CLI is present: the binary, the version arguments, and a regular-expression source for the version. |
| `driving.surfaces` | Declare `cli-subprocess`. See below. |
| `lifecycle` / `execution` | Your hooks ([section 3](#3-write-the-hooks)). Both are optional; a manifest with neither is a pure description. |
| `lifecycle` / `execution` | Your hooks ([section 3](#3-write-the-hooks)). Both are optional; a manifest with neither is a pure description. A file-backed hook must list its adapter-owned files in `hook.files`. |

> **Capabilities describe what the adapter *delivers through `ak`*, not what your host can do in
> principle.** A real Hermes adapter's first draft declared `nativeMcpConfig: true` and
Expand Down Expand Up @@ -217,8 +217,17 @@ how you write it:
file planted in the operator's cwd is unreachable. This is why `AK_WORKER_CWD` exists: it's how
you learn which repository to work on. A *remote*-sourced manifest (`npm:` / `https://`) has no
local directory to anchor to, so a relative command from such a source is refused
(`execution-unanchored` / `lifecycle-unanchored`) — publish remotely and you must use absolute
paths or bare PATH binaries.
(`execution-unanchored` / `lifecycle-unanchored`) — publish remotely and contract v1 requires
path-independent PATH binaries or inline evaluator commands.
- **Declared hook-file integrity.** A relative/script-like hook argument must be covered by that
hook's `files` array, with a path relative to the manifest directory. `ak` reads each declared
regular file, records its SHA-256 digest alongside the manifest identity, discloses the digest during
`trust`, and rechecks it immediately before every spawn. Edit, remove, or replace a declared file
and admission/grants go stale; an edit after admission is refused at spawn time. The inventory is
explicit, not a transitive import scanner: list every adapter-owned file your hook executes.
- **Remote path restriction.** npm/URL manifests are read and discarded rather than retained as a
local bundle. Contract v1 therefore refuses script-like hook paths from those sources; use a PATH
binary or inline evaluator command. A future immutable bundle/signature contract may widen this.
- **Minimal environment.** Your hook gets `PATH`, `HOME`, and whatever `ak` injects for that verb —
never `ak`'s full environment. Don't expect to inherit the operator's secrets.
- **Bounded output.** Captured output is capped at 256 KB and truncated with a marker beyond that.
Expand Down Expand Up @@ -260,11 +269,11 @@ export AK_EXPERIMENTAL_HOST_ADAPTERS=1
ak host adapters trust hermes
```

`trust` prints the full validated manifestevery hook command that will spawn is right there in it
— then asks for confirmation before pinning a hash of that content. **Edit the manifest afterwards
and consent invalidates**: the adapter is not admitted again until the user re-confirms the new
content. Consent lives outside your code, attached to a specific byte sequence, never to a name your
content could drift underneath.
`trust` prints the full validated manifest, every declared hook-file digest, and every hook command
that will spawn — then asks for confirmation before pinning a hash of that combined content. **Edit
the manifest or a declared hook file afterwards and consent invalidates**: the adapter is not admitted
again until the user re-confirms the new content. Consent lives outside your code, attached to a
specific byte sequence, never to a name your content could drift underneath.

Three more notes for your install docs:

Expand Down Expand Up @@ -316,12 +325,17 @@ What each tier means for you:
| `primary-eligible` | Earns `canBePrimary`. Your host anchors a real run *and* receives a genuine ADR-0019 escalation onto itself — a second real subprocess. | **Can genuinely pass**, with no pre-existing grant. |
| `statusline` | Earns `commandStatusline`. | **`gated`.** There is no admitted-host footer-render path yet, so even a granted capability has nothing real to drive. |

Use `ak host adapters conformance <name> --dev` while iterating. It runs the same real subprocess
checks but loudly persists no consent, tier evidence, or capability grant; a dev run cannot graduate
the adapter. Use the default command when you want the reproduced evidence that a maintainer may
review.

**A `gated` or `skipped` result on `session-driving` and `statusline` is expected, not your adapter
failing.** The harness never fabricates a pass, and there is no injection seam through which a caller
could substitute one. Only `failed` means something is wrong with your adapter.

Evidence is hash-pinned to your manifest, so any edit voids it. And it's a two-way street: if a
grant-bearing tier later re-runs `failed` at the same manifest hash, the stored evidence *and* the
Evidence is hash-pinned to your combined manifest/file identity, so any declared edit voids it. And it's a two-way street: if a
grant-bearing tier later re-runs `failed` at the same adapter-content hash, the stored evidence *and* the
live capability are auto-voided.

## 6. Propose it for graduation
Expand All @@ -341,7 +355,7 @@ Two destinations, the maintainer's call:
- **Blessed external adapter** — `ak host adapters bless hermes <capability>` (`grant` is the same
command). Your adapter stays out-of-tree and experimental, holding exactly the capabilities its
tiers earned. A grant is refused unless the gating tier is recorded `passed` at the current
manifest hash, and it's re-checked at read time, not just at write time.
adapter-content hash, and it's re-checked at read time, not just at write time.
- **Promoted built-in** — your host descriptor is adopted into the first-party registry. This is now
an ordinary PR: a registry entry, a lifecycle adapter, an About card. Once built-in, the caps no
longer apply, because it is first-party code the maintainer vouches for. That's what promotion
Expand All @@ -366,6 +380,7 @@ ak host adapters trust <name> # disclose the manifest, confirm, pi
# (--yes --expect-hash <sha256> for unattended/remote)
ak host adapters revoke <name> # withdraw consent (works with the flag off)
ak host adapters conformance <name> # run the tiered harness
ak host adapters conformance <name> --dev # real self-test; persist no evidence or grants
ak host adapters status <name> # per-tier state + granted capabilities
ak host adapters grant <name> <capability> # maintainer: confer an earned capability (alias: bless)
ak host adapters revoke-grant <name> [cap] # withdraw a granted capability
Expand Down
8 changes: 8 additions & 0 deletions docs/HOST-ADAPTER-FREEZE-CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ while the contract is still experimental.

- [ ] Admission gate, hash-pinned consent, subprocess hook-runner, `admission` tier
(ADR-0029).
- [ ] File-backed hooks declare a relative `hook.files` inventory; each path is digest-pinned,
rechecked immediately before spawn, and a changed/missing/non-regular file fails closed.
- [ ] Remote (`npm:` / URL) adapters use only path-independent hooks, or move to a retained,
immutable bundle contract before freeze; no remote hook bytes are assumed immutable.
- [ ] `ak host adapters trust` / `list` / `revoke` (+ `--expect-hash`).
- [ ] Remote manifest sources (file / `https` / `npm:`), resolve-before-hash.
- [ ] `ak run` drives an admitted routable host (cwd-anchored, exit-code authority,
Expand All @@ -50,6 +54,10 @@ while the contract is still experimental.
- [ ] `statusline` — `gated` remains acceptable at freeze (its render path is a later wave);
the freeze is of the **contract shape**, not of every tier passing.
- [ ] **Hooks read** by a maintainer (the only executing part).
- [ ] **Hook-byte evidence reproduced** by the maintainer: the disclosed per-path digests match the
files reviewed, and an edit between admission and spawn is observed to fail closed.
- [ ] **Development conformance evidence kept separate**: any `ak host adapters conformance <name>
--dev` run is explicitly non-persistent and is not used for graduation.
- [ ] **Grant/bless decision** recorded (blessed external adapter, or promoted built-in).
- [ ] **Soak: one full release** elapsed with the adapter in the field and **no
contract-shape change** required. Release soaked through: `__________`.
Expand Down
74 changes: 31 additions & 43 deletions docs/adr/0029-host-adapter-extension-point.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

- **Status:** Accepted (experimental contract)
- **Date:** 2026-08-15
- **Updated:** 2026-08-16
- **Updated:** 2026-08-24
- **Update note:** [ADR-0031](0031-capability-graduation-and-upstream-requests.md) amends this ADR's
"permanent caps" framing. The block on *self-declaring* `canBePrimary` / `aqeProvider` /
`commandStatusline` in the manifest is permanent (the safety invariant here), but the *capability*
is earnable through a conformance tier plus a maintainer grant recorded outside the manifest — up
to promotion to a first-party built-in. The schema, admission gate, consent model, and hook runner
in this ADR are unchanged.
now also pin declared hook-file bytes as described in §6.
- **Deciders:** agentic-kit maintainers
- **Related:** [ADR-0016](0016-capability-driven-integration-adapters.md) (closed-registry clause
superseded — see [Supersession](#supersession-of-adr-0016s-closed-registry-clause)),
Expand Down Expand Up @@ -155,23 +155,15 @@ external-execution row, after an adversarial review of the surface):
operator's cwd is unreachable. A remote-sourced manifest (`npm:`/`https://`) has no persistent
local directory, so a *relative* hook command from such a source is refused
(`execution-unanchored`) rather than resolved against an ambient path; a bare PATH binary
(`node`, `hermes`) stays legal. The consent hash still pins the manifest text verbatim; the
resolution is a pure function of that text plus the (already-pinned) source, so it cannot drift
without the hash changing.
- *Boundary of the anchorability check for remote sources.* When a remote-sourced adapter has no
local directory to anchor to, its hook command spawns in the repository `ak run` was invoked in
(which the operator already runs at full trust, per ADR-0018), and the `execution-unanchored`
refusal is a **best-effort** screen for path-shaped tokens (separators, script extensions, flag
values), not a complete one: an *extensionless, separator-free* relative token
(`["node", "runhook"]`) is indistinguishable by inspection from an ordinary positional argument
(`["hermes-run", "build"]`), so it is not refused and would resolve against the repo. A complete
rule would have to reject every non-absolute, non-flag argument, which would also reject
legitimate positional arguments — a false-positive cost this contract does not pay by default.
The exposure is bounded on every axis that matters: it requires a remote (`npm:`/`https://`)
source, a consented manifest the operator hash-pinned with that exact relative token, and write
access to the operator's repo. A **file-sourced** adapter — the fixture, and every adapter that
ships a bundle — is fully anchored and unaffected. A remote-sourced adapter should declare
absolute paths or PATH binaries; a future contract revision may make that a hard requirement.
(`node`, `hermes`) stays legal. The combined content hash pins the manifest text and any declared
local hook bytes; the resolution is anchored to the source's real directory, so those bytes cannot
drift without the hash changing.
- *Remote sources are path-independent in contract v1.* A remote (`npm:`/`https://`) resolver does
not retain a bundle after extracting the manifest, so admission refuses script-like hook paths
and any declared `hook.files` with `hook-files-unavailable`. Remote adapters must use PATH
binaries or inline evaluator commands. A retained file source may use relative hook paths, but
every adapter-owned file must be listed in that hook's `files` inventory and is hashed before
consent.
- **Reserved exit codes carry consent/auth boundaries.** Hook exit `77` maps to
`permission_required` (a blocked, never-escalated result — escalating around a consent boundary
is the safety violation ADR-0019 already forbids) and `78` to `auth_required`. This gives an
Expand Down Expand Up @@ -224,14 +216,15 @@ amended gate list below.

### 6. Consent: hash-pinned, edit-invalidated

Registering an adapter computes a content hash over the manifest and every declared subprocess hook
command, discloses the full manifest through the same trust-manifest surface ADR-0018/ADR-0023
already use before any other mutation, and requires explicit confirmation before persisting
`trust.hash` and `trust.consentedAt`. Every subsequent load re-hashes the manifest and compares: a
mismatch means the manifest changed since consent, and the adapter is **not admitted** until
re-consented. This is Codex's pin-and-invalidate model, applied to `ak`'s own adapter manifests
instead of Codex's MCP servers — consent lives outside the trusted boundary, attached to a specific
byte sequence, never to an identity that content can silently drift underneath.
Registering an adapter computes a content hash over the validated manifest and every declared
subprocess hook command. For a file-sourced adapter, each hook's explicit relative `files` inventory
also contributes a per-path SHA-256 digest; the combined identity is disclosed before confirmation
and stored in `trust.hash`/`trust.consentedAt`. Every subsequent load re-hashes the manifest and
declared files and compares: a mismatch means the adapter is **not admitted** until re-consented.
Every admitted spawn repeats the file check immediately before execution, so a file edited after
admission cannot run under the old consent or grant. This is Codex's pin-and-invalidate model,
extended to the bytes the manifest names. It is not a race-free immutable snapshot; a future retained
bundle/signature design can provide that stronger TOCTOU guarantee.

### 7. No in-process third-party code, ever

Expand Down Expand Up @@ -333,24 +326,19 @@ A matching one-line update-note has been added to ADR-0016 itself, pointing here

## Self-graded implementation status

Dated 2026-08-15, at Wave 4 doc-authoring time. Rows already covered by the amended gate list are
not repeated; this table grades the mechanism this ADR newly decides — the manifest, admission,
overlay, hook-runner, and consent — none of which existed as code prior to this wave. Grades:
**Working** (implemented and tested in this worktree), **Demo** (implemented, not yet under test),
**TBD** (not yet implemented as of this dating). Per the model set by ruflo's own ADR-015-v2
practice — a self-graded status table an ADR carries at acceptance time, filled in with real
evidence as implementation lands rather than promised in prose — the lead fills in test counts and
flips remaining TBD cells at Wave 4 integration.
Dated 2026-08-24, after the PR #131 follow-up implementation. Rows already covered by the amended
gate list are not repeated; this table grades the mechanism this ADR newly decides. **Working** means
implemented and tested in this worktree.

| Mechanism | Grade (2026-08-15) | Evidence |
| Mechanism | Grade (2026-08-24) | Evidence |
|---|---|---|
| Manifest schema (contract: 1) | TBD | Owned by the sibling contract work package; no schema module present in this worktree as of this dating. |
| Admission gate (fail-closed, per-adapter isolated) | TBD | Owned by the sibling contract work package; not present in this worktree as of this dating. |
| `AK_EXPERIMENTAL_HOST_ADAPTERS` flag gating | TBD | Not present in this worktree as of this dating; no occurrences found under `src/`. |
| Admitted-host overlay (registry-adjacent, non-mutating) | TBD | Depends on the admission gate landing first. |
| Subprocess hook-runner (`cli-subprocess` surface) | TBD | Owned by the sibling hook work package; not present in this worktree as of this dating. |
| Hash-pinned consent + edit-invalidation | TBD | Owned by the sibling hook work package; not present in this worktree as of this dating. |
| Capability-cap schema absence (§3) | TBD | Depends on the manifest schema landing first. |
| Manifest schema (contract: 1) | **Working** | `src/lib/adapters/manifest.mjs`; strict hook `files` inventory validation; manifest tests. |
| Admission gate (fail-closed, per-adapter isolated) | **Working** | `src/lib/adapters/admission.mjs`; admission and integrity tests. |
| `AK_EXPERIMENTAL_HOST_ADAPTERS` flag gating | **Working** | Bootstrap and CLI flag-off tests. |
| Admitted-host overlay (registry-adjacent, non-mutating) | **Working** | `src/lib/adapters/admitted.mjs`; overlay/grant tests. |
| Subprocess hook-runner (`cli-subprocess` surface) | **Working** | `src/lib/adapters/hook-runner.mjs`; bounded real-subprocess tests. |
| Hash-pinned consent + edit-invalidation | **Working** | `src/lib/adapters/integrity.mjs`; manifest + declared hook-file digests, pre-spawn recheck, integrity tests. |
| Capability-cap schema absence (§3) | **Working** | Schema refusal tests and maintainer-only grant allow-list. |
| Gate item 1 — import-time invariant | **Working** | `assertBuiltinAdaptersRoutable`, one-directional since W1-B (`src/lib/execution/adapters.mjs`). |
| Gate item 2 — uninstall-through-undo | **Working** | Registry-driven `hostsWithLifecycle()` teardown loop (`src/commands/uninstall.mjs`). |
| Gate item 3 — permission authorization by host | **Working** | `projectPermissionManifest` union-across-enabled-hosts, F-04 (`src/commands/setup.mjs`). |
Expand Down
Loading
Loading