diff --git a/docs/AUTHORING-HOST-ADAPTERS.md b/docs/AUTHORING-HOST-ADAPTERS.md new file mode 100644 index 0000000..9918adb --- /dev/null +++ b/docs/AUTHORING-HOST-ADAPTERS.md @@ -0,0 +1,393 @@ +# Authoring a host adapter + +A walkthrough for adding a new agent CLI to `ak` without touching `ak`. The running example is +**Hermes** — the fourth-host candidate this contract was designed against, and the adapter that +would graduate it. Substitute your own host throughout. + +Read this alongside [ADR-0029](adr/0029-host-adapter-extension-point.md) (the contract) and +[ADR-0031](adr/0031-capability-graduation-and-upstream-requests.md) (how a capability is earned). +For what the kit does with a host once it knows about one, see +[HOST-SUPPORT.md](HOST-SUPPORT.md) and [PROVIDERS.md](PROVIDERS.md). + +> **Experimental.** The whole surface is inert unless `AK_EXPERIMENTAL_HOST_ADAPTERS=1` is set, and +> `contract: 1` can still change between alpha releases. See [The freeze](#9-the-freeze-and-why-you-matter). + +## 1. What you're actually building + +Two things: + +- **One JSON manifest** — a description of your host. Every value is JSON-serializable. A manifest + that cannot express a closure cannot smuggle one in. +- **A handful of small hook scripts** — the only part that ever executes. + +**Nothing you write runs inside the `ak` process.** Every hook is a subprocess `ak` spawns, +supervises, and owns the termination of. `ak` never calls `import()` or `require()` on a path you +supply, and registering your adapter adds no npm dependency to `@pacphi/agentic-kit`. + +That constraint is also your leverage. ADR-0029 records what adding a host used to cost: a dedicated +owner module, native surfaces reverse-engineered from scratch, and edits across roughly a dozen +files and eight test suites — plus a permanent maintenance obligation on a maintainer who may not +run your CLI. That collapses to a manifest and some hooks. You write no `ak` code, own no module in +this repository, and reverse-engineer no internals. + +What you give up in exchange is real and is covered in [section 8](#8-what-earning-actually-gets-you-today). + +## 2. Write the manifest + +Name it whatever you like as a file; name it exactly `ak-adapter.json` at the package root if you +publish it on npm. Here is a complete, valid one — this shape validates against `ak`'s real +validator: + +```json +{ + "name": "hermes", + "version": "0.1.0", + "contract": 1, + "host": { + "id": "hermes", + "label": "Hermes", + "install": { "bin": "hermes", "externalInstallPolicy": "detect-never-overwrite" }, + "capabilities": { + "canDriveSession": false, + "canBePrimary": false, + "canRouteActivities": true, + "commandStatusline": false, + "transcripts": false, + "usage": false, + "nativeMcpConfig": false, + "nativeGuidance": false + }, + "trust": { "approvalPolicy": "unchanged", "changes": [] }, + "enabledByDefault": false, + "configProjection": "ruflo", + "observability": [] + }, + "detection": { + "bin": "hermes", + "versionArgs": ["--version"], + "versionPattern": "\\d+\\.\\d+\\.\\d+" + }, + "driving": { "surfaces": ["cli-subprocess"] }, + "lifecycle": { + "detect": { "hook": { "command": ["node", "detect-hook.mjs"], "timeoutMs": 5000 } } + }, + "execution": { + "run": { "hook": { "command": ["node", "run-hook.mjs"], "timeoutMs": 120000 } } + }, + "trust": { + "changes": [ + { + "id": "hermes-subprocess-hooks", + "kind": "third-party-adapter", + "scope": "project", + "owner": "hermes", + "value": "subprocess hooks", + "effect": "run consented lifecycle and execution hooks for hermes" + } + ] + } +} +``` + +Field by field: + +| Field | What it's for | +| --- | --- | +| `name` / `host.id` | Your host's id. They must agree, and must not collide with a built-in. | +| `host.label` | Human-readable name shown in status output. | +| `host.install.bin` | Your CLI's binary name. `externalInstallPolicy: "detect-never-overwrite"` is the honest posture — `ak` finds your CLI, never installs or upgrades it. | +| `host.capabilities` | All eight keys are **required**. Declare what your host legitimately does. | +| `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. | + +### Driving surfaces + +The vocabulary has three names — `cli-subprocess`, `acp`, `mcp` — but **`cli-subprocess` is the only +one with a working implementation.** Declare an `execution` block without `cli-subprocess` in +`driving.surfaces` and you get no execution adapter at all — the registration is refused +`surface-unsupported`, never silently downgraded to a surface you didn't test against. `acp` and +`mcp` are reserved for forward compatibility, and nothing drives them today. + +### The three capabilities you cannot claim + +`canBePrimary`, `commandStatusline`, and `aqeProvider` are **not yours to assert**: + +- `canBePrimary` and `commandStatusline` are required keys, and the schema accepts them **only at + `false`**. Writing `true` is rejected before anything runs (`cap-can-be-primary`, + `cap-command-statusline`). You cannot write down the claim; you **earn** the capability through a + conformance tier plus an explicit maintainer grant, recorded outside your manifest entirely + (ADR-0031 §1). +- `host.legacy.aqeProvider` must be absent — `cap-aqe-provider` refuses any value. This one is not + earnable at all through `ak`: agentic-qe's provider set is a closed upstream enumeration, so being + an AQE provider type is upstream's to grant. See [section 9](#9-the-freeze-and-why-you-matter). + +Self-declaration is the attack surface, so it stays closed permanently. The *capability* is a ladder; +the *declaration* is a wall. + +### One structural coupling worth knowing + +Declaring `execution` while `canRouteActivities` is `false` is a contradiction the schema refuses +outright (`execution-not-routable`). The converse is fine: a routable host with no `execution` block +is legal and degrades honestly at run time as `cli_unavailable`. + +## 3. Write the hooks + +Hooks are ordinary executables. They read stdin, read a few environment variables, print to stdout, +and exit with a code. No SDK, no imports from `ak`. + +### The `detect` hook (lifecycle) + +Reports that your host is present and describes what it found. It prints a JSON object on stdout; +`ak` parses it and returns it verbatim as the `detect` result. + +```js +// detect-hook.mjs +process.stdout.write(JSON.stringify({ + observed: { host: 'hermes', bin: 'hermes', version: '1.4.2' }, +})); +``` + +`detect`, `plan`, `apply`, `verify`, and `undo` are the five lifecycle verbs. Declare only the ones +you need — an undeclared verb is an honest no-op, not a fabricated success. `apply` and `undo` are +what wire (and unwire) your host's own configuration when the user runs `ak setup` / `ak uninstall`. + +### The `execution.run` hook + +This is the one that actually drives your host as a worker under `ak run`. + +- **stdin** carries the worker prompt. +- **the environment** carries `AK_WORKER_ID`, `AK_WORKER_ACTIVITY`, `AK_WORKER_ROLE`, + `AK_WORKER_MODEL`, and `AK_WORKER_CWD` (the repository being worked on — your hook does *not* + spawn there, see below). +- **stdout** carries either a JSON object — `{summary, observedModel, provider, usage}`, all + optional — or plain text, which is taken as the summary. +- **the exit code** is the sole authority for success. + +```js +// run-hook.mjs +let prompt = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (chunk) => { prompt += chunk; }); +process.stdin.on('end', async () => { + const outcome = await driveHermes(prompt, { + cwd: process.env.AK_WORKER_CWD, + model: process.env.AK_WORKER_MODEL || undefined, + }); + if (outcome.needsLogin) process.exit(78); + process.stdout.write(JSON.stringify({ + summary: outcome.text, + observedModel: outcome.model, + provider: 'hermes', + })); +}); +``` + +### Exit codes + +| Code | Meaning | What the runner does | +| --- | --- | --- | +| `0` | Success | Reads your stdout payload. | +| `77` | Permission refused — you need consent you don't have | Records `blocked` / `permission_required` and **never escalates**. Escalating around a consent boundary is the safety violation ADR-0019 forbids. | +| `78` | Authentication required | Records `failed` / `auth_required`; the runner may retry on another host. | +| other non-zero | Ordinary failure | Records `failed` / `worker_error`; may be escalated. | + +Codes `77` and `78` exist so you can say "I refused" or "I'm not logged in" honestly, instead of a +bare non-zero exit that reads as a generic failure. + +### Rules `ak` enforces on every hook + +These aren't suggestions — they're the supervision that makes running your code safe, and they shape +how you write it: + +- **Anchored working directory.** A hook spawns with its cwd pinned to *your adapter's own resolved + directory*, never `ak`'s. So `["node", "run-hook.mjs"]` resolves to **your** `run-hook.mjs`, and a + 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. +- **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. +- **Bounded time.** Your declared `timeoutMs` and `ak`'s own budget both apply, and the tighter one + wins; with neither, a hook gets 30 seconds. On timeout the whole process group is killed — there is + no grace period, because the budget is already spent. +- **No shell.** `command` is an argv array, spawned directly. There is no shell to quote against. +- **stderr is never promoted.** Diagnostics you write to stderr are captured for the operator but + never folded into a downstream worker's prompt. Practical consequence: **write your JSON payload + to stdout only** — stderr chatter won't corrupt a valid JSON parse, but a plain-text summary is + discarded if you also wrote to stderr. +- **Results never launder trust.** A `provider` you declare in your payload is stamped `inferred`, + never `observed` — `ak` didn't verify it against anything. + +## 4. Publish it, and let a user opt in + +Ship the manifest any of three ways: + +| Source form | Example | Note | +| --- | --- | --- | +| File | `~/.config/ak/adapters/hermes.json` | Fully anchored; relative hook commands work. | +| npm | `npm:@you/ak-adapter-hermes` | Must ship `ak-adapter.json` at the package root. `ak` reads it from the tarball to stdout — nothing is extracted to disk and your package scripts never run. | +| URL | `https://example.com/hermes.json` | No redirects, bounded time and bytes, no credentials attached. | + +A user then registers it in their own `kit.json`: + +```json +{ + "hostAdapters": [ + { "name": "hermes", "source": "~/.config/ak/adapters/hermes.json", "contract": 1 } + ] +} +``` + +and opts in: + +```bash +export AK_EXPERIMENTAL_HOST_ADAPTERS=1 +ak host adapters trust hermes +``` + +`trust` prints the full validated manifest — every 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. + +Three more notes for your install docs: + +- Unattended consent to a **remote** source (`npm:` / `https://`) requires `--yes --expect-hash + `. `--yes` alone is refused for a non-file origin, so a CI run can never blanket-consent to + whatever the remote happens to serve. Publish the hash alongside your adapter. +- With the flag unset, a `hostAdapters` entry is parsed and preserved but never admitted. It is + reported present-but-inactive, not silently dropped. +- Your **lifecycle** hooks only run during `ak setup` / `ak sync` / `ak uninstall` when the user has + *also* explicitly enabled your host under `integrations.hosts` in `kit.json`. There is no pick-UI + for external hosts yet. + +## 5. Self-test with the conformance kit + +```bash +ak host adapters conformance hermes +``` + +This runs the tiered black-box harness against your **real** host — spawning your actual hooks — and +prints an honest per-tier verdict. It warns first, listing every hook command it is about to run as +a real subprocess. Here is a real run against the repository's conformance fixture +(an adapter shaped exactly like the manifest in section 2): + +```text +host adapter conformance — acme (56fa107674d2) +admission passed host id 'acme', contract 1 +session-driving skipped not declared — nothing to prove +activity-routing passed registered for 'acme' +primary-eligible passed 'acme' completed a direct (non-escalated) run to a succeeded result, and… +statusline gated ak-local: awaiting maintainer grant for 'commandStatusline' (not an… +``` + +(Detail columns elided at the right margin; the real ones run longer.) + +What each tier means for you: + +| Tier | What it proves | What to expect | +| --- | --- | --- | +| `admission` | Your manifest validates, admits through the fail-closed gate, joins the registry, and your `detect` hook runs as a real subprocess. | **Can genuinely pass.** A failure here short-circuits every downstream tier, so nothing gets laundered. | +| `session-driving` | Gates `canDriveSession`. | `skipped` if you don't declare it. **`gated` if you do** — `ak` has no external session-driving path, and being a native ruflo backend is upstream-owned. | +| `activity-routing` | Gates `canRouteActivities`. A real one-worker `ak run` routed to your host returns a succeeded `WorkerResult`. | **Can genuinely pass.** | +| `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. | + +**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 +live capability are auto-voided. + +## 6. Propose it for graduation + +You don't need a maintainer to ship. Publish, and any user can opt in behind the flag with pinned +consent — that path is entirely yours. + +To go further, hand a maintainer your conformance report. They: + +1. **Re-run the conformance kit** themselves — conformance is objective, and trust rests on + reproduced evidence rather than your report. +2. **Read your hook scripts** — the only part that executes. +3. **Decide** the tier and the destination. + +Two destinations, the maintainer's call: + +- **Blessed external adapter** — `ak host adapters bless hermes ` (`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. +- **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 + means. + +A tier you can't clear because the capability lives upstream is recorded as +**`gated: #NNN`**, not failed: + +```bash +ak host adapters gate hermes session-driving ruvnet/ruflo#1234 +ak host adapters status hermes +``` + +The maintainer champions that request upstream with a named extension point, and `status` shows +exactly what your adapter is waiting on. + +## 7. The commands, in one place + +```bash +ak host adapters list # configured adapters and their consent state +ak host adapters trust # disclose the manifest, confirm, pin the hash + # (--yes --expect-hash for unattended/remote) +ak host adapters revoke # withdraw consent (works with the flag off) +ak host adapters conformance # run the tiered harness +ak host adapters status # per-tier state + granted capabilities +ak host adapters grant # maintainer: confer an earned capability (alias: bless) +ak host adapters revoke-grant [cap] # withdraw a granted capability +ak host adapters gate # record an upstream blocker against a tier +``` + +## 8. What earning actually gets you today + +Be clear-eyed about this, because the machinery is ahead of its consumers: + +- **A granted `canBePrimary`** raises the capability on your host's effective registry entry, so + `hostTierLabel` and the primary-eligible host set reflect it. But **no path yet *selects* an + external host as primary** — `ak host pick` is still built-in-scoped. You show as eligible; nothing + acts on it. +- **A granted `commandStatusline` is currently inert.** There is no runtime reader for an + admitted host's command-backed footer. The grant records what you earned; nothing renders it yet. + +Both gaps are disclosed at grant time, and both are `ak`-local work rather than upstream ceilings — +they will light up without needing anything from you. What works end-to-end today is +`activity-routing`: a real `ak run` worker on your host, supervised, with a structured result. + +## 9. The freeze, and why you matter + +Two ceilings are genuinely not `ak`'s to lift: + +- **Native ruflo backend** (`session-driving`). ruflo's backend enablement is per-host and defined + inside ruflo, not through an outside registration surface. *Interim:* your host runs through `ak`'s + own supervised execution — just not as a ruflo-native backend. +- **agentic-qe provider type** (`aqeProvider`). agentic-qe's provider set is a closed upstream + enumeration, extended only by an upstream code change. *Interim:* quality still runs through the + model provider underneath your host, so QE isn't blocked — only your host's own AQE identity is. + +Neither is faked, hidden, or shimmed. Each gets a tracked capability request and an honest interim +behaviour ([ADR-0031 §4](adr/0031-capability-graduation-and-upstream-requests.md)). + +And the contract itself stays experimental until a **real external adapter clears the full +conformance kit and survives one release of soak in the field with no contract-shape change**. That +adapter could be yours. Until then, `contract: 1` may change between alpha releases — bounded +instability that exists precisely so the first real adapter's conformance run can surface shape +problems cheaply. + +So a real Hermes isn't just a beneficiary of this machinery. It's the test that graduates the +contract from experimental to frozen. diff --git a/docs/HOST-ADAPTER-FREEZE-CHECKLIST.md b/docs/HOST-ADAPTER-FREEZE-CHECKLIST.md new file mode 100644 index 0000000..5004312 --- /dev/null +++ b/docs/HOST-ADAPTER-FREEZE-CHECKLIST.md @@ -0,0 +1,76 @@ +# Host-adapter contract freeze checklist (ADR-0029 §graduation, ADR-0031 §6) + +The external host-adapter contract (`contract: 1`) is **experimental** and behind +`AK_EXPERIMENTAL_HOST_ADAPTERS=1`. This is the falsifiable checklist for freezing it — +turning `contract: 1` into a guaranteed-stable shape and dropping the flag. **Do not claim +the freeze until every box is genuinely checked against a real adapter.** The freeze is not +a maintainer's assertion; it is an earned, evidenced event. + +## The freeze criterion (ADR-0031 §6, ADR-0029 graduation gate) + +Freeze requires **all** of: + +1. A **real external adapter** — maintained *outside this repository, by someone other than + an agentic-kit maintainer* (Hermes is the expected first) — passes the full conformance + kit against its real host. +2. That adapter completes **one full release's worth of soak** in the field with **no + contract-shape change** required to keep it working. +3. Only then does the manifest **contract integer** freeze: `contract: 1` becomes stable, + and any later breaking change ships as `contract: 2`, admitted alongside `contract: 1`. + +Tier graduation and the contract freeze are distinct: an adapter can earn capability tiers +while the contract is still experimental. + +## Readiness — machinery the freeze rests on (all shipped; verify before soak) + +- [ ] Admission gate, hash-pinned consent, subprocess hook-runner, `admission` tier + (ADR-0029). +- [ ] `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, + reserved 77/78, no trust laundering). +- [ ] Admitted lifecycle execution through setup / sync / uninstall. +- [ ] Tiered conformance kit: `admission`, `activity-routing`, `primary-eligible` pass + against a real fixture; `session-driving` / `statusline` honestly gated. +- [ ] Capability-grant store + `grant`/`bless`, `gate`, `status`, `revoke-grant`; earned + capability enforced at read time; granted caps live in the effective registry. + +## Freeze gate — the real-adapter run (fill in with evidence, not intent) + +- [ ] **Real external adapter identified**, maintained outside this repo by a non-maintainer. + Adapter: `__________` Maintainer: `__________` Source: `__________` +- [ ] **Conformance report captured** from `ak host adapters conformance ` against the + real host (attach or link the per-tier verdict). + - [ ] `admission` passed + - [ ] `activity-routing` passed + - [ ] `primary-eligible` passed *(or consciously out of scope for this adapter)* + - [ ] `session-driving` — passed **iff** the upstream ruflo backend-registration request + (`docs/upstream-requests/ruflo-backend-registration.md`) has shipped; otherwise + legitimately `gated` and recorded via `ak host adapters gate`. + - [ ] `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). +- [ ] **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: `__________`. +- [ ] **No `contract: 1` shape change** was needed during soak (if one was, the clock resets). + +## Only when every box above is genuinely checked + +- [ ] Set the manifest `contract` shape to frozen; document that a breaking change now ships + as `contract: 2` admitted alongside `contract: 1`. +- [ ] Drop `AK_EXPERIMENTAL_HOST_ADAPTERS` gating for the frozen surface (or flip its default), + per the ADR-0029/0031 freeze decision. +- [ ] Update ADR-0029 (graduation gate) and ADR-0031 §6 status to **frozen**, dated, with the + adapter + release that earned it. + +## Honest ceilings that do NOT block the freeze + +The freeze is of the **contract shape**, not of universal tier passage. These stay open by +design and are recorded as gated, not as failures: + +- `session-driving` until ruflo ships a backend-registration surface (upstream request drafted). +- An `aqeProvider` identity until agentic-qe ships a provider-plugin API (upstream request drafted). +- `statusline` runtime rendering (no `ak` render surface for a third-party TUI yet). +- Grant *consumption* last-mile: selecting an external host as primary, and a `commandStatusline` + runtime reader. diff --git a/docs/HOST-EXTENSIBILITY-EXPLAINER.html b/docs/HOST-EXTENSIBILITY-EXPLAINER.html index 5c4fd3a..f079c33 100644 --- a/docs/HOST-EXTENSIBILITY-EXPLAINER.html +++ b/docs/HOST-EXTENSIBILITY-EXPLAINER.html @@ -199,7 +199,7 @@

Room for more hosts

  • Two kinds of host — built-in today vs. the external door
  • Why this was worth doing — the end of open-heart surgery
  • What an external adapter actually is — data plus arm's-length hooks
  • -
  • Where we are — and what's honestly next — the rollout, and the limits
  • +
  • Where we are today — what's real, and the honest limits
  • For implementers: the shape of an adapter — the manifest
  • From a contributor's idea to a built-in host — the exact sequence
  • Some ceilings aren't ours to lift — the upstream path to ruflo & agentic-qe
  • @@ -214,12 +214,18 @@

    1 · The one-sentence version

    Is this what lets us use Gemini CLI, Hermes, and others?
    -

    Yes — this is the foundation that makes those possible. - It is not yet a finished on-ramp an external assistant can be driven across today. What exists now: - the kit can recognize and register an external host from a manifest, safely and - reversibly, behind an experimental switch. What comes next is turning that recognition into - running work through it. The staging is spelled out in - section 5 — honestly, so nobody expects more than is there.

    +

    Yes — and the on-ramp is open. Behind an experimental switch, an + external host described by a manifest can be approved + (ak host adapters trust discloses it and pins its exact content), run + (ak run drives it as a supervised worker, as a real subprocess), and + certified (a tiered conformance kit exercises it against your actual CLI). What's + still genuinely ahead is narrower than it used to be: two conformance tiers remain gated + — session-driving waits on upstream, statusline on a render surface that isn't + built — and an earned capability is only partly consumed, since nothing yet selects an external + host as primary. The one thing the kit can't supply is the adapter itself: no Hermes or Gemini CLI + manifest exists yet, so using one of those still starts with someone writing it + (the authoring guide is the walkthrough). + Section 5 has the honest limits, stated plainly.

    2 · Two kinds of host

    @@ -308,13 +314,14 @@

    Built-in hosts here now

    -

    External adapters the foundation

    +

    External adapters experimental

    Hermes · Gemini CLI · anything CLI-shaped

    • Described by a manifest the adapter author publishes — not bundled in the kit.
    • Enter only through the admission gate, only behind an experimental switch.
    • Cannot self-declare that they lead, bill, or own a status line.
    • -
    • Today: can be recognized and registered. Running work through them is the next step.
    • +
    • Today: trusted, run via ak run, and able to earn capabilities through + conformance — behind the experimental flag.
    @@ -476,8 +483,8 @@

    What the kit guarantees about an external host

    status line; OpenCode: none). "Primary" means leading the session — a separate axis from taking part in ak run, which OpenCode does as a supervised worker (unpacked in "Leading vs. being supervised" just below). The safety point stands: -an external adapter cannot self-declare any of these — the schema has no field to say so — -but a capability can be earned via conformance +an external adapter cannot self-declare any of these — the schema refuses the claim outright, +before anything runs — but a capability can be earned via conformance (section 7).

    Leading vs. being supervised

    @@ -572,36 +579,39 @@

    Leading vs. being supervised

    do is lead one — be the default, own the reasoning roles, or be the host escalation resolves toward. That authority stays with a host that can be primary.

    -

    5 · Where we are — and what's honestly next

    -

    The door is built and its safety is proven. But a door a host can be registered through is -not yet a door work can be run through. Here is the truthful staging.

    +

    5 · Where we are today

    +

    The door is built, its safety is proven, and work now runs through it. What remains is not +plumbing — it's evidence: a real outside adapter has to walk the whole path before the +contract can stop being experimental. Here is the truthful staging.

    + aria-label="Rollout stages: admitting a host, approving it with the trust command, and running work through it are all shipped behind the experimental flag. Still ahead: a real adapter passing the conformance kit, then freezing the contract and dropping the flag."> - HERE NOW + SHIPPED — behind the flag + STILL AHEAD Admit register an external host from a manifest - - - Trust - a command to - approve an adapter - (smallest next step) + + Trust + disclose it, then + pin its exact + content by hash - - Run - actually drive - work through it + + Run + drive work through + it as a supervised + subprocess + Prove a real adapter @@ -620,9 +630,11 @@

    5 · Where we are — and what's honestly next

    the experimental switch stays ON until the last step -
    Foundation laid, on-ramp staged. Only the first box is done. Each later -box is a small, self-contained increment behind the same experimental switch — so nobody is exposed -to a half-built external host by accident.
    +
    On-ramp open; the proof is what's left. Admitting, approving, and running +an external host all work today. The last two boxes can't be built — they have to be +earned, by a real outside adapter clearing the conformance kit and then surviving a release +in the field. The experimental switch stays on until that happens, so nobody meets a half-proven +external host by accident.

    What can be expected today

    @@ -631,36 +643,42 @@

    What can be expected today

    shipped payoff, and it needs no flag.
  • The internals are ready for more — a new host is a registry entry or a manifest, not a rewrite.
  • -
  • An external host can be recognized and registered from a manifest, safely and - reversibly, behind AK_EXPERIMENTAL_HOST_ADAPTERS=1.
  • +
  • An external host can be trusted, run, and can earn capabilities — from a + manifest, safely and reversibly, behind AK_EXPERIMENTAL_HOST_ADAPTERS=1: + ak host adapters trust records hash-pinned consent, ak run drives it as a + supervised subprocess, and the conformance kit certifies it tier by tier.
  • Limitations — read before expecting an external host to "just work"
      -
    • Work can't be run through an external host yet. The kit can admit one into - its registry, but driving a task through it (the ak run path) is not built — an - admitted external host currently reports "no execution available" rather than running.
    • -
    • There's no command to approve one yet. Approval is checked at admission, but - the ak host adapters trust command that would record it isn't built — so in practice - admission refuses until that lands. This is the intended next step.
    • -
    • Manifests are local files only. Fetching an adapter from npm or a URL isn't - built; a manifest is a file path today.
    • An external host can't self-declare that it's primary, an AQE provider, or a status-line owner. That block on self-declaration is permanent — it's the safety invariant. But the capability itself is earnable: pass the matching conformance tier and - get a maintainer's grant, and an adapter reaches full parity (the graduation ladder in - section 7). One honest exception — being an AQE provider - type isn't the kit's to grant; agentic-qe's provider list is defined upstream - (section 8).
    • -
    • It's experimental. The contract version can still change; nothing about the - external-adapter surface is promised stable until the final "Open" stage above.
    • + get a maintainer's grant (the graduation ladder in section 7). One + honest exception — being an AQE provider type isn't the kit's to grant; agentic-qe's + provider list is defined upstream (section 8). +
    • Two conformance tiers can't be cleared yet, by design. session-driving + is gated on an upstream ruflo backend, and statusline is gated + because the kit has no footer-render surface for a third-party host. They report + gated/skipped honestly — never a fabricated pass.
    • +
    • An earned capability isn't fully consumed yet. A granted + canBePrimary makes a host show as primary-eligible, but no path yet selects + an external host as primary (ak host pick stays built-in-scoped); a granted + commandStatusline is currently inert (nothing reads it at runtime).
    • +
    • It's experimental. contract: 1 can still change between alpha + releases; nothing about the external-adapter surface is promised stable until a real external + adapter clears the full kit and soaks — the final "Open" stage above.

    6 · For implementers: the shape of an adapter

    -

    When the on-ramp is complete, authoring a host adapter will mean shipping one manifest and a few -small hook scripts. The manifest looks like this (illustrative):

    +

    Authoring a host adapter means shipping one JSON manifest and a few small +hook scripts — nothing else, and no code of yours ever runs inside the kit's own process. +The step-by-step walkthrough lives in +docs/AUTHORING-HOST-ADAPTERS.md; this is the +shape of it.

    +

    The manifest (abridged — the guide has a complete, validating one):

    {
       "name": "hermes",
       "version": "0.1.0",
    @@ -668,35 +686,73 @@ 

    6 · For implementers: the shape of an adapter

    "host": { "id": "hermes", "label": "Hermes", + "install": { "bin": "hermes", "externalInstallPolicy": "detect-never-overwrite" }, "capabilities": { - "canDriveSession": true, - "canRouteActivities": true - // canBePrimary / commandStatusline: not allowed — omitted by force + "canDriveSession": false, + "canBePrimary": false, + "canRouteActivities": true, + "commandStatusline": false, + "transcripts": false, "usage": false, + "nativeMcpConfig": false, "nativeGuidance": false }, - "install": { "bin": "hermes", "externalInstallPolicy": "detect-never-overwrite" } + "trust": { "approvalPolicy": "unchanged", "changes": [] }, + "enabledByDefault": false, "configProjection": "ruflo", "observability": [] }, "detection": { "bin": "hermes", "versionArgs": ["--version"] }, "driving": { "surfaces": ["cli-subprocess"] }, "lifecycle": { - "detect": { "hook": { "command": ["hermes", "doctor", "--json"] } } + "detect": { "hook": { "command": ["node", "detect-hook.mjs"] } } }, - "trust": { "changes": [ /* what the adapter will touch, disclosed up front */ ] } + "execution": { + "run": { "hook": { "command": ["node", "run-hook.mjs"] } } + }, + "trust": { "changes": [ ... what the adapter will touch, disclosed up front ... ] } }
    -

    It is registered in the user's own kit.json, and its exact bytes are approved:

    +

    A user registers it in their own kit.json and approves its exact bytes:

    // kit.json
     "hostAdapters": [
       { "name": "hermes", "source": "~/.config/ak/adapters/hermes.json", "contract": 1 }
    -]
    +] + +$ export AK_EXPERIMENTAL_HOST_ADAPTERS=1 +$ ak host adapters trust hermes # discloses the manifest, then pins its hash +

    A manifest can also be published as an npm package shipping ak-adapter.json at its +root, or as an https URL.

      -
    • The conformance kit is the contract. A committed test harness admits a real - fixture adapter, runs its hooks as real subprocesses, and refuses a corpus of bad manifests with - exact reasons — the same bar a real adapter (Hermes first) must clear to graduate.
    • +
    • The hooks are the only code that runs — as supervised subprocesses. A hook + reads the worker prompt on stdin, gets the worker's identity in its environment, prints a JSON + result, and exits: 0 for success, 77 to refuse on a consent boundary + (never escalated), 78 for "not authenticated". The kit pins each hook's working + directory to the adapter's own bundle, hands it a minimal environment, bounds its output and its + time, and never promotes its stderr into another worker's prompt.
    • Approval is pinned to the bytes. Consent is a hash of the exact manifest; edit one character and the kit asks again. Content is never approved unseen.
    • Refusals are specific. An unknown field, a forbidden capability, a bad contract version, a name that collides with a built-in — each is refused by name, and one bad adapter never disturbs the others or the built-ins.
    • +
    • Self-test before you ask anyone for anything. + ak host adapters conformance hermes runs the tiered harness against your real host and + reports each tier honestly. admission, activity-routing and + primary-eligible can genuinely pass — the last one by driving a real run and receiving + a real escalation onto your host. session-driving and statusline come back + gated (or skipped, if you never declared the capability) because those paths are + not built; that is expected, not your adapter failing. The harness never fabricates a pass.
    +
    +
    What an author should not expect yet
    +
      +
    • cli-subprocess is the only working driving surface. + acp and mcp are named in the vocabulary for forward compatibility; + declaring one is refused, never silently downgraded.
    • +
    • An earned canBePrimary is only partly consumed. A grant does + raise the capability, so the host shows as primary-eligible — but no path yet selects an + external host as primary. An earned commandStatusline is currently inert: there is no + runtime reader for an admitted host's footer.
    • +
    • The contract is still experimental. contract: 1 can change + between alpha releases, and freezes only once a real external adapter clears the full kit and + survives a release of soak.
    • +
    +

    7 · From a contributor's idea to a built-in host

    This is the part that ties it together: the exact sequence from "someone wants to add Hermes" to @@ -1010,8 +1066,8 @@

    What it doesn't — and who owns it

    point and its safety model are ADR-0029 (which supersedes ADR-0016's original "closed registry" stance, narrowly — nothing under a manifest is ever loaded into the kit's process). The graduation ladder — experimental external adapter, earned capabilities via conformance tiers, promotion to -built-in, and the upstream capability-request path to ruflo and agentic-qe — is the proposed next -decision (a prospective ADR-0031), not yet ratified. Upstream facts (agentic-qe's closed provider +built-in, and the upstream capability-request path to ruflo and agentic-qe — is ADR-0031, accepted as +a governance decision with its machinery staged and its own implementation-status table. Upstream facts (agentic-qe's closed provider enum; ruflo's ENABLE_* backend model) are grounded in a source-cited research sweep of agentic-qe@3.13.10 and ruvnet/ruflo@45e65b5. Hermes and Gemini CLI are named here as illustrative candidates for the external door, not as hosts the kit supports today.

    diff --git a/docs/HOST-SUPPORT.md b/docs/HOST-SUPPORT.md index 2c9cdb1..46c5cdb 100644 --- a/docs/HOST-SUPPORT.md +++ b/docs/HOST-SUPPORT.md @@ -6,11 +6,18 @@ RuvNet Brain without treating those independent layers as interchangeable. Behind an experimental flag, agentic-kit can also admit **external host adapters** that extend this set with a host not shipped in-tree — see -[External host adapters](PROVIDERS.md#external-host-adapters-experimental) and -[ADR-0029](adr/0029-host-adapter-extension-point.md). An admitted external host -picks up the same capability-driven treatment described here, but it is not one -of the three built-ins this reference compares, and it can never claim -primary-host, AQE-provider, or status-line status. +[External host adapters](PROVIDERS.md#external-host-adapters-experimental), +[AUTHORING-HOST-ADAPTERS.md](AUTHORING-HOST-ADAPTERS.md), +[ADR-0029](adr/0029-host-adapter-extension-point.md), and +[ADR-0031](adr/0031-capability-graduation-and-upstream-requests.md). An admitted +external host picks up the same capability-driven treatment described here, but it +is not one of the three built-ins this reference compares. It can never +**self-declare** primary-host, AQE-provider, or status-line status — that ban is +permanent — while `canBePrimary` and `commandStatusline` are **earnable** through +a passed conformance tier plus an explicit maintainer grant. `aqeProvider` stays +upstream-owned and is never `ak`-grantable. See +[External host adapters](#external-host-adapters) below for what a grant does and +does not buy today. Evidence cutoff: **2026-08-04**. The comparison was checked against agentic-kit `4.0.0-alpha.36`, Ruflo `3.34.0`, agentic-qe `3.13.x`, RuvNet Brain `4.0.7`, @@ -218,6 +225,42 @@ subagent permission rules being ignored `AGENTS.md` guidance being forgotten ([#40348](https://github.com/anomalyco/opencode/issues/40348)). +## External host adapters + +An external adapter is data, not code: a hash-pinned manifest plus subprocess +hooks, admitted only behind `AK_EXPERIMENTAL_HOST_ADAPTERS=1` and only after +`ak host adapters trust ` discloses the full validated manifest and records +your consent. `contract: 1` is still experimental and **not frozen** — the freeze +waits on a real external adapter clearing the conformance kit and soaking. + +`ak host adapters conformance ` reports each graduation tier honestly: + +| Tier | Status today | Gates | Why | +| --- | --- | --- | --- | +| `admission` | Genuinely passes | — | Manifest validation and consent are built | +| `activity-routing` | Genuinely passes | — | Real supervised subprocess worker via `ak run` | +| `primary-eligible` | Genuinely passes | `canBePrimary` | Observes a real escalation | +| `session-driving` | **Gated** | — | Being a native Ruflo backend is upstream's to grant | +| `statusline` | **Gated** | `commandStatusline` | `ak` has no render surface for it yet | + +The two gated tiers are honest ceilings, not failures — they report `gated` or +`skipped` and never `passed`, with `ak host adapters gate #NNN` +recording the upstream issue each waits on. + +What a maintainer grant (`ak host adapters grant`, alias `bless`) buys today, stated +narrowly: the capability goes live in the effective host registry from the next +flagged invocation, so the host's tier label reflects it and it joins +primary-eligibility. No path yet **selects** an external host as primary — `ak host +pick` stays built-in-scoped — and `commandStatusline` has no runtime reader, so a +granted `commandStatusline` is currently inert. Grants are withdrawable with +`revoke-grant`, and every tier result is stale-marked the moment the manifest +changes. + +A graduated adapter ends in one of two places: a **blessed external adapter** that +stays out-of-tree holding exactly the capabilities its tiers earned, or a +**promoted built-in** whose descriptor a maintainer adopts as a first-party registry +entry — an ordinary pull request, not a command. + ## Known contract discrepancies These are intentionally visible rather than hidden behind an over-broad “supported” @@ -239,7 +282,12 @@ ADR-0020 is **Implemented** as of 2026-07-30; ADR-0021 is **Accepted** and was updated 2026-08-03. See [ADR-0017](adr/0017-opencode-host.md), [ADR-0018](adr/0018-generalized-host-worker-execution.md), [ADR-0020](adr/0020-ga-stable-surfaces.md), and -[ADR-0021](adr/0021-inference-provider-provenance.md). +[ADR-0021](adr/0021-inference-provider-provenance.md). For the external-adapter +section above, [ADR-0029](adr/0029-host-adapter-extension-point.md) is the +extension point and [ADR-0031](adr/0031-capability-graduation-and-upstream-requests.md) +amends it with capability graduation — replacing ADR-0029's permanent +capability caps with the earn-then-grant model, except for the permanent ban on +self-declaring them. ## Operational guidance diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 3a22058..11ff5c9 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -88,14 +88,58 @@ like Hermes, say? Set `AK_EXPERIMENTAL_HOST_ADAPTERS=1` and declare it as **data ``` An adapter is a manifest plus a handful of subprocess hooks — nothing an adapter declares ever -runs inside the `ak` process itself. Registering one asks you to confirm a content hash of the -manifest; edit the manifest afterward and that consent is invalidated until you confirm again. A -broken adapter is reported and skipped — it never takes down the hosts that already work. - -An external adapter can never claim to be the primary host, an AQE provider, or the status-line -owner; those stay first-party. Nothing here installs itself: you declare the adapter, you consent -to it, and teardown remains reversible. See -[ADR-0029](adr/0029-host-adapter-extension-point.md). +runs inside the `ak` process itself. `source` may be a local file path, an `https://` URL (HTTPS +only, no redirects, bounded), or `npm:[@version]`, fetched with `npm pack --ignore-scripts` +and extracted to stdout — nothing runs and nothing lands on disk. The source is resolved *before* +hashing, so a mutated remote surfaces as `consent-stale` rather than sliding in quietly. + +Consent is explicit and hash-pinned. `ak host adapters trust ` discloses the full validated +manifest and records your consent against its content hash (`--expect-hash` pins it +non-interactively); `list` shows trust state, and `revoke` works even with the flag off. Once a +host is admitted *and* explicitly enabled in `kit.json`, its lifecycle hooks run through +`ak setup`, `ak sync`, and uninstall — a failed detect or plan aborts the apply. A broken adapter +is reported and skipped; it never takes down the hosts that already work. + +### What an external adapter can earn + +An external adapter can never **self-declare** that it is the primary host, an AQE provider, or +the status-line owner. That ban is permanent — it is the safety invariant the whole extension +point rests on. But `canBePrimary` and `commandStatusline` are **earnable**: passing the gating +conformance tier is evidence, and `ak host adapters grant ` (alias `bless`) is +a maintainer's explicit grant, refused unless that tier is recorded passed at the adapter's +current manifest hash. `aqeProvider` stays upstream-owned and is never `ak`-grantable. + +`ak host adapters conformance ` runs the tiered black-box kit: + +| Tier | Status today | Gates | +| --- | --- | --- | +| `admission` | Genuinely passes against a real adapter | — | +| `activity-routing` | Genuinely passes — real supervised subprocess worker | — | +| `primary-eligible` | Genuinely passes — observes a real escalation | `canBePrimary` | +| `session-driving` | **Gated** — a native Ruflo backend is upstream's to grant | — | +| `statusline` | **Gated** — `ak` has no render surface for it yet | `commandStatusline` | + +The two gated tiers are honest ceilings, not failures: they report `gated`/`skipped` and never +`passed`. `ak host adapters gate #NNN` records the upstream issue the ceiling +is waiting on, `status [name]` shows per-tier state (stale-marked the moment the manifest changes) +alongside granted capabilities, and `revoke-grant [capability]` withdraws one or all. + +Be precise about what a grant buys **today**. A granted capability goes live in the effective host +registry from the next flagged invocation — the host's tier label reflects it and it joins +primary-eligibility. But no path yet *selects* an external host as primary (`ak host pick` stays +built-in-scoped), and `commandStatusline` has no runtime reader. So a granted `canBePrimary` is +visible and eligible, while a granted `commandStatusline` is currently inert. + +Graduation has two destinations: a **blessed external adapter** stays out-of-tree holding exactly +the capabilities its tiers earned, or a maintainer **promotes it to a built-in** by adopting its +descriptor as a first-party registry entry — an ordinary PR, not a command. + +Nothing here installs itself: you declare the adapter, you consent to it, and teardown remains +reversible. `contract: 1` is still experimental and **not frozen** — freezing waits on a real +external adapter clearing the conformance kit and soaking. Writing one? See +[AUTHORING-HOST-ADAPTERS.md](AUTHORING-HOST-ADAPTERS.md). The governing decisions are +[ADR-0029](adr/0029-host-adapter-extension-point.md) and +[ADR-0031](adr/0031-capability-graduation-and-upstream-requests.md). --- @@ -391,6 +435,8 @@ just makes the good default automatic and the customization reversible. - Capability-driven integration axes, bindings, and provenance: [ADR-0016](adr/0016-capability-driven-integration-adapters.md). - The generic local OpenAI-compatible provider: [ADR-0028](adr/0028-local-openai-compatible-providers.md). -- External host adapters (experimental): [ADR-0029](adr/0029-host-adapter-extension-point.md). +- External host adapters (experimental): [ADR-0029](adr/0029-host-adapter-extension-point.md), + amended by [ADR-0031](adr/0031-capability-graduation-and-upstream-requests.md) — capability + graduation. Authoring guide: [AUTHORING-HOST-ADAPTERS.md](AUTHORING-HOST-ADAPTERS.md). - Host env flags (`ENABLE_CLAUDE_CODE` / `ENABLE_CODEX`): upstream ruflo ADR-034, "Optional MCP Backends". diff --git a/docs/upstream-requests/agentic-qe-provider-plugin.md b/docs/upstream-requests/agentic-qe-provider-plugin.md new file mode 100644 index 0000000..02c2ac2 --- /dev/null +++ b/docs/upstream-requests/agentic-qe-provider-plugin.md @@ -0,0 +1,72 @@ + + +# Feature request: a provider-plugin registration API for `AQE_LLM_PROVIDER` + +## Summary + +agentic-qe's LLM provider set is a closed, upstream-defined enumeration. A downstream +integrator (agentic-kit) can select any *existing* provider via `AQE_LLM_PROVIDER`, but +cannot introduce a *new* provider type without an upstream code change. We'd like a +documented registration surface so a host that a downstream tool supervises can be +recognized as its own provider identity, rather than only borrowing the model provider +underneath it. + +## Where this stands today (please re-verify against HEAD) + +Grounded against `agentic-qe@3.13.10` (as cited in agentic-kit's ADR-0031): + +- `ALL_PROVIDER_TYPES` is a fixed union of provider type strings. +- `createProvider` is a `switch` over those types; an unrecognized type has no arm. +- So the provider set is extended only by editing that enum + switch — there is no + runtime/plugin registration path. + +If any of this has changed on HEAD (a registration hook, an open provider map, a plugin +entrypoint), this request may already be satisfied — please close it as such. + +## The concrete ask + +A minimal, documented way for a downstream tool to register an additional provider type at +runtime, e.g.: + +- a `registerProvider(type, factory)` entrypoint (factory conforming to the same interface + `createProvider` returns), and inclusion of registered types in `ALL_PROVIDER_TYPES` + for validation; **or** +- a documented "external provider" adapter interface a downstream package can implement + and hand to the `HybridRouter` / `ProviderManager`. + +We are not asking to widen the *default* provider set — only for a sanctioned extension +point so `AQE_LLM_PROVIDER=` can resolve to a provider we supply, instead of us +either forking the enum or projecting our host onto an unrelated provider identity (which +would misrepresent which vendor served the work). + +## Why (the downstream context) + +agentic-kit admits external host adapters (declarative manifest + consented subprocess +hooks) and runs a tiered conformance kit against them. Quality still runs through the +*model provider underneath* a host, so QE is never blocked — but a host cannot earn an +**AQE-provider identity** of its own, because that identity is yours to define, not ours to +extend. agentic-kit deliberately refuses to fabricate one (projecting an unknown host into +your config would assert an identity you never declared you understood). This request is +the honest alternative: a real extension point, so the capability can light up when you +release it. + +## Non-goals + +- Not asking agentic-kit's hosts to be bundled into agentic-qe. +- Not asking to bypass any provider validation — registered providers should be validated + exactly like built-in ones. + +## References + +- agentic-kit ADR-0031 §4 (the upstream-request path) and ADR-0029 (the host-adapter + contract): the design that motivates this. +- `AQE_LLM_PROVIDER` / `HybridRouter` / `createProvider` / `ALL_PROVIDER_TYPES` in + agentic-qe (re-verify paths against HEAD). diff --git a/docs/upstream-requests/ruflo-backend-registration.md b/docs/upstream-requests/ruflo-backend-registration.md new file mode 100644 index 0000000..c9f46e2 --- /dev/null +++ b/docs/upstream-requests/ruflo-backend-registration.md @@ -0,0 +1,65 @@ + + +# Feature request: a documented backend-registration surface for host CLIs + +## Summary + +ruflo's set of agent **backends** (the host CLIs that drive the loop) is enabled per-host +through fixed `ENABLE_*` flags. There is no documented way for a downstream tool to +register an *additional* backend, so a host CLI that a downstream integrator supervises +cannot become a ruflo-native backend without an upstream code change. We'd like a +documented registration surface for that. + +## Where this stands today (please re-verify against HEAD) + +Grounded against `ruvnet/ruflo@45e65b5` (as cited in agentic-kit's ADR-0031): + +- Backend enablement is per-host and fixed: `ENABLE_CLAUDE_CODE`, `ENABLE_CODEX`, + `ENABLE_GEMINI_MCP` (ADR-034 "Optional MCP Backends"). +- A new backend is added by defining a new `ENABLE_*` target inside ruflo — there is no + outside/plugin registration path. + +If HEAD already exposes a backend-registration API or a documented plugin entrypoint, this +request may already be satisfied — please close it as such. + +## The concrete ask + +A documented way to register an additional agent backend from outside the ruflo tree, e.g.: + +- a backend descriptor interface (how ruflo detects the CLI, launches an interactive / + oneshot session, and observes completion) that a downstream package can implement and + register, honored by the same loop the `ENABLE_*` backends drive; **or** +- a documented convention for an external backend entrypoint ruflo will discover and drive. + +The goal is that a host CLI can *drive a ruflo session* as a first-class backend, not only +run under a downstream tool's own supervision. + +## Why (the downstream context) + +agentic-kit admits external host adapters and certifies them with a tiered conformance kit. +The `session-driving` tier — "the host actually drives an interactive/oneshot session" — +is the one an external adapter cannot clear on its own, because being a ruflo-native +backend is defined inside ruflo, not registered from outside. agentic-kit's honest interim +is to run such a host through its *own* supervised execution (`ak run`), and to mark the +tier `gated` on this request rather than fake a pass. A documented backend-registration +surface upstream is what lets that tier genuinely light up. + +## Non-goals + +- Not asking to bundle agentic-kit's hosts into ruflo. +- Not asking to loosen the trust model — a registered backend should be subject to the same + enablement/consent posture as the built-in `ENABLE_*` backends. + +## References + +- agentic-kit ADR-0031 §4 (the upstream-request path) and ADR-0029 (the host-adapter + contract). +- ruflo ADR-034 "Optional MCP Backends" and the `ENABLE_CLAUDE_CODE` / `ENABLE_CODEX` / + `ENABLE_GEMINI_MCP` backend model (re-verify paths against HEAD).