Skip to content

Host & provider consistency: Phases 0–3 (registry dispatch, provider axis, extension point) - #151

Merged
pacphi merged 9 commits into
mainfrom
develop
Aug 16, 2026
Merged

Host & provider consistency: Phases 0–3 (registry dispatch, provider axis, extension point)#151
pacphi merged 9 commits into
mainfrom
develop

Conversation

@pacphi

@pacphi pacphi commented Aug 15, 2026

Copy link
Copy Markdown
Owner

This is the consolidated host/provider consistency program — every phase built, reviewed, and CI-verified as its own PR into develop, assembled here for a single human review. It is left open deliberately; nothing here has been merged to main.

Origin: this began as an evaluation of @adrianco's PR #131 (proposed ADRs 0028/0029/0030 for Hermes/host-adapter support). We took ownership of the design, grounded it in a four-sweep research pass against live upstream source (ruflo, the three hosts, Hermes at HEAD, agentic-qe), and executed it in dependency order.

The eight increments

PR Phase What
#143 1 Per-entry provider records + the generic local-openai provider row; ADR-0028 accepted in-tree with corrections (F-28/F-29/F-30)
#144 3a Quota labels hosts with no quota surface; usage-docs current-state verification (F-10)
#146 2·W1 Dispatch by registry lookup: the import-time brick softened, lifecycle registry, uninstall-through-undo, host-keyed permissions, registry-derived guidance, unknown-key warnings (F-01/02/03/04/17/14)
#145+#147 2·W2 ak status renders host detail through a host-neutral dispatch loop (F-05)
#148 2·W3 Test expectations derive from the registry; directory parity built-in-scoped (F-21/F-06)
#149 2·W4 The host-adapter extension point — validated manifest + consented subprocess hooks, no in-process third-party code, behind an experimental flag; ADR-0029 accepted, superseding ADR-0016's closed-registry clause
#150 3b Capability-derived host tiers and asymmetry notes; docs stop claiming a closed host set (D-2/F-25/F-26/F-27)

How it was built

Each wave ran as a swarm of TDD builders with disjoint file ownership (one git author, zero merge conflicts), followed by effort/impact-scaled review — deep adversarial review on the load-bearing and security-critical changes, light review on mechanical ones, with a second verify pass on every major finding. Independent parallel tracks ran in git worktrees where file footprints didn't overlap.

The review layer caught and fixed real defects before merge, not after: a fail-open inversion of the qe-court collusion check; an actor-host misattribution in live events; an uninstall save-gate that stranded ownership markers; a latent path-traversal in the guidance fallback; and — in the extension point — a consent-forgery gap and a checklist-not-allowlist weakness that together could have reached an npm install postinstall. The security review (approve-with-nits, no RCE) ran seven named attacks; six held, the seventh was closed and pinned as a regression corpus.

What the reviewer should know

  • The extension point ships behind AK_EXPERIMENTAL_HOST_ADAPTERS=1 with byte-zero default behavior — effectiveHostRegistry() is HOST_REGISTRY by reference until an adapter is admitted. ADR-0029 carries a graded Working/Demo/TBD table; external lifecycle execution, a trust CLI verb, and npm/URL manifest sources are explicitly TBD for a later graduation.
  • No Hermes-specific code is in-tree. A synthetic fixture adapter under tests/fixtures/adapters/acme proves the contract; a real Hermes adapter waits until the contract graduates (owned by its proposer).
  • Docs follow the current-state-only policy: history lives in ADR update notes and each doc's fix-history appendix; ADR-0016's closed-registry clause is formally superseded by ADR-0029.

59 files, +4,716/−280. Full gate green on every merged commit (typecheck, lint, markdownlint, build, ~1,682 tests across macOS/Ubuntu/Windows × Node 22/24/26).

🤖 Generated with Claude Code

pacphi added 8 commits August 14, 2026 23:35
…28 (F-28/F-29/F-30) (#143)

* feat: per-entry provider records and the generic local-openai provider row

Phase 1 provider axis (ADR-0028, F-28): providerEntries is six explicit
records — capabilities live on each entry instead of being derived from
identity comparisons, matching hostEntries; the five pre-existing providers
are pinned deep-equal to the old construction. local-openai names any
OpenAI-compatible server the user runs: billing local, no credentials, the
openai-compatible transport only, projections ruflo/codex/opencode (no aqe —
AQE's provider set is upstream's; no claude — that projection expects an
anthropic-compatible surface), no discovery claims, no builtin bindings.
Remote-endpoint policy pinned: local is a billing claim, not topology —
https off-box is legal, plain http stays loopback-only.

* feat: ak status names local non-AQE bindings plainly

Phase 1 provider axis (F-29): when kit.json declares a binding whose provider
is local and not AQE-projected, status prints one info row naming provider,
host, and endpoint with the not-an-AQE-provider-type fact. Registry-driven
(billing + projections), so ollama never triggers it and a future provider of
the same shape gets the same treatment; no bindings means no new output.

* docs: accept ADR-0028 with corrections; PROVIDERS.md gains the local-openai recipe

Proposed by adrianco in PR #131; accepted with the api_mode citation
annotated as invalid per Hermes v0.20.0 source and the ollama/local-openai
AQE-projection asymmetry stated as an intentional decision. PROVIDERS.md adds
a brief current-state section on declaring a local-openai binding.
…erification (#144)

* docs: verify the current-state-only policy across the usage docs; fix the one leftover

Sweep of USAGE-SCORECARD-METRICS, TRANSCRIPTS, and TROUBLESHOOTING against the
house policy (main bodies describe current behavior; history lives in each
doc's fix-history appendix). The corpus already complied except one inline
before/after aside in TRANSCRIPTS §4.2, now a current-state sentence with the
history moved to Appendix A. Doc-citation, markdownlint, and ga-surface gates
green.

* feat: quota labels hosts that have no quota surface instead of omitting them

Phase 3 (F-10): readLimits gains an opt-in enabledHosts parameter and an
'others' list — every additional enabled registry-managed host appears as
{supported: false, reason: 'no quota surface for this host'} so users can
tell absent-by-design from broken. The two ADR-0010-sanctioned channels are
unchanged; no new channel, no probe. The dashboard /api/limits payload now
carries the labels (browser UI rendering is a noted follow-up).
…04/F-17/F-14) (#146)

* feat: per-entry provider records and the generic local-openai provider row

Phase 1 provider axis (ADR-0028, F-28): providerEntries is six explicit
records — capabilities live on each entry instead of being derived from
identity comparisons, matching hostEntries; the five pre-existing providers
are pinned deep-equal to the old construction. local-openai names any
OpenAI-compatible server the user runs: billing local, no credentials, the
openai-compatible transport only, projections ruflo/codex/opencode (no aqe —
AQE's provider set is upstream's; no claude — that projection expects an
anthropic-compatible surface), no discovery claims, no builtin bindings.
Remote-endpoint policy pinned: local is a billing claim, not topology —
https off-box is legal, plain http stays loopback-only.

* feat: ak status names local non-AQE bindings plainly

Phase 1 provider axis (F-29): when kit.json declares a binding whose provider
is local and not AQE-projected, status prints one info row naming provider,
host, and endpoint with the not-an-AQE-provider-type fact. Registry-driven
(billing + projections), so ollama never triggers it and a future provider of
the same shape gets the same treatment; no bindings means no new output.

* docs: accept ADR-0028 with corrections; PROVIDERS.md gains the local-openai recipe

Proposed by adrianco in PR #131; accepted with the api_mode citation
annotated as invalid per Hermes v0.20.0 source and the ollama/local-openai
AQE-projection asymmetry stated as an intentional decision. PROVIDERS.md adds
a brief current-state section on declaring a local-openai binding.

* fix: the execution-adapter invariant no longer bricks ak at import for unwired routable hosts

Phase 2 Wave 1 (F-01): the frozen adapter map's bidirectional import-time
invariant becomes built-ins-one-directional (a built-in adapter wired to a
non-routable or absent host still throws — the in-tree mistake), and
executionAdapterFor() is the lookup seam externally-admitted adapters will
join. A routable host with no adapter imports cleanly and degrades per worker
via the pre-existing cli_unavailable path, which escalation ladders already
step past. Built-in behavior byte-identical (same object references).

* feat: host lifecycle is reached by registry lookup, uninstall runs the contract, permissions key by host

Phase 2 Wave 1 (F-02, F-03, F-04): a lifecycle registry (construction-
validated, one-way import) replaces the five OPENCODE_LIFECYCLE_ADAPTER named
imports; source-text guards pin that no command names the adapter again.
Uninstall tears down every lifecycle host through runLifecycle undo honoring
ownership receipts — and persists nulled markers unconditionally like
x/host.mjs always has (review-caught: gating the save on file changes
stranded a stale mcp:ak receipt forever on the quiet-success path; regression
test added). Setup's permission manifest unions auto-approve trust changes
across ALL enabled hosts instead of claude's alone — byte-identical at the
claude-only default; on opencode-enabled machines its four MCP tool-name
rules join the disclosed/authorized set (stated tradeoff, disclosed pre-write
by the trust manifest).

* refactor: guidance targets derive from the host registry

Phase 2 Wave 1 (F-17): guidanceTargets() loops nativeGuidance hosts and their
legacy.guidanceFile instead of a closed four-name literal; output pinned
byte-identical across every enablement combination. retiredForTarget stops
force-stripping targets outside the derived universe. The generic fallback
accepts id-shaped names only — a traversal-shaped guidanceFile contributes no
target at all (review-caught latent escape; schema-level validation is the
wave-4 admission gate's job).

* feat: kit.json names its unknown top-level keys instead of ignoring them silently

Phase 2 Wave 1 (F-14): loadKitConfig warns once per distinct unknown key-set
on stderr — 'preserved, ignored' — so a future hostAdapters key on an older
ak is a named no-op, not a silent one. Keys still round-trip untouched; clean
configs warn nothing; legacy pre-migration shapes live nested inside
recognized envelopes and never trip it.
…loop (#145)

Phase 2 Wave 2 (F-05): the ~94-line inline opencode block becomes a per-host
detail renderer registered in a dispatch table and invoked by an exported
host-neutral loop — output byte-identical (whitespace-normalized diff clean),
row order unchanged, the duplicate have('opencode') probe replaced by the
integration-facts snapshot the surrounding loops already consume. Renderers
carry their own error isolation by contract. The #129/#133 AQE-drift
projection sharing and Phase 1's local-binding row are untouched, verified by
their unchanged suites. A synthetic fourth host renders through the same loop
with zero loop changes.
…h loop passes (#147)

The wave-2 quality gate caught a JSDoc mismatch: renderHostDetailRows calls
every renderer with hostId, which opencodeDetailRows' destructured signature
did not declare. Typecheck green; suite unchanged.
…t-in-scoped (#148)

Phase 2 Wave 3 (F-21, F-06): host-set and default-map pins across nine test
files now derive from managedHostIds()/routableHostIds()/primaryHostIds()/
defaultHostMap() — scoped to built-ins where the assertion is about built-ins
— so registering a host updates expectations exactly once. Deliberate
literals that survive are policy pins (DEFAULT_PRIMARY_HOST) or fixture
inputs, each commented. The component-directory parity invariant is stated
built-in-scoped; externally-admitted adapters are exempt until the wave-4
contract graduates them. No production code changed; suite counts identical.
…mental) (#149)

* feat: the host-adapter extension point — data plus consented subprocess hooks (ADR-0029)

Phase 2 Wave 4: an external host adapter is a validated JSON manifest plus
subprocess hooks; no third-party code ever runs in-process. Behind
AK_EXPERIMENTAL_HOST_ADAPTERS=1 with byte-zero default behavior.

- manifest.mjs: a STRICT allowlist at every level — top-level, host,
  install, legacy, capabilities, detection, driving, lifecycle, and trust
  reject any unknown key, so the structural caps (canBePrimary, aqeProvider,
  commandStatusline) and a path-traversal guidanceFile are inexpressible, not
  merely refused; an external adapter may not name an npm package ak would
  install, and its detection bin must be id-shaped.
- admission.mjs: fail-closed, per-adapter isolated admission — validate,
  cap-check, contract match, builtin-shadow refusal, then a locale-independent
  content hash checked against hash-pinned consent (edit-invalidated). One bad
  entry never affects built-ins or siblings.
- hook-runner.mjs: the only path external code executes — a supervised,
  shell-free subprocess with an env allowlist, process-group kill on timeout,
  and bounded capture.
- consent.mjs: the 0600 hash-pinned trust store.
- admitted.mjs: the frozen built-ins+admitted overlay; effectiveHostRegistry
  is HOST_REGISTRY by reference until something is admitted.
- Built-in lifecycle loops iterate builtinHostsWithLifecycle() so an admitted
  host can never enter an opencode-shaped teardown before external lifecycle
  execution graduates.

Security-reviewed (approve-with-nits, no RCE/escape/pollution): six of seven
attacks held; the consent-forgery gap and the checklist-not-allowlist finding
are closed by the strict allowlist above, verified against the reviewer's
exploit probes.

* test: adapter-door conformance harness, fixture adapter, and security regression corpus

The graduation artifact ADR-0029 names: a real fixture adapter under
tests/fixtures/adapters/acme with committed subprocess hooks, plus a
black-box conformance harness that admits it through the real consent store,
runs its declared hooks as real subprocesses against a marker file, and
proves each negative-corpus manifest is refused with its exact named reason.
Includes the allowlist, locale-hash, and consent-edit-invalidation regression
tests that pin the closed security findings.

* docs: ADR-0029 accepts the extension point; supersede ADR-0016's closed-registry clause

ADR-0029 (Accepted, experimental contract) records the data-plus-hooks
mechanism, credits @adrianco's PR #131 proposal and states what changed and
why (grounded in the four-sweep research), carries a graded Working/Demo/TBD
table, and formally supersedes ADR-0016's closed-registry clause — narrowly:
kit.json may name a manifest, but nothing under it is ever imported in-process,
so the clause's intent survives. PROVIDERS.md gains a brief current-state note.
kit.json entry shape corrected to {name, source, contract} to match the code.

* test: skip the POSIX-mode consent-file assertion on Windows

NTFS carries no Unix permission bits, so mode & 0o777 never reflects the 0600
the consent store writes; the write still requests 0600. Guarded with the
repo's { skip: process.platform === 'win32' } idiom (windows-only CI failure).
…set docs (D-2/F-25/F-26/F-27) (#150)

* feat: host tiers and asymmetries render from capabilities, not host names

Phase 3 (D-2, F-25, F-26): hostTierLabel() derives a plain phrase from a
host's capabilities — 'drives sessions · can lead' for a primary host,
'routing only · supervised · not AQE' for a supervised one, 'routing only ·
external adapter' for an admitted external — replacing x/host.mjs's
h.id === 'opencode' tier ternary. hostAsymmetryNote() states the cross-host
MCP delegation bridge (F-25) and the permission consent boundary / absent
backend env flag (F-26) from each host's own trust manifest and capability
set. Both are true by construction for any host, built-in or admitted; no new
tier jargon.

* docs: the host set is the built-in set, not a closed universe

Phase 3 (F-27): HOST-SUPPORT.md presents claude/codex/opencode as the built-in
execution hosts and notes external adapters can extend the set behind the
experimental flag (ADR-0029), rather than 'the three execution hosts';
ubiquitous-language defines host by registry capability with the three as
built-in examples. Current-state voice; every factual row about the built-ins
unchanged.
* docs: ADR-0031 ratifies the capability-graduation model and the upstream request path

Accepts, as a governance decision, that external host adapters reach full
parity by EARNING capabilities through tiered conformance plus an explicit
maintainer grant — never by self-declaring them (that block stays permanent,
the safety invariant from ADR-0029, which this amends). Records the two
graduation destinations (blessed external, promoted built-in), the
contributor-to-built-in lifecycle, and the upstream capability-request path:
some ceilings aren't ak's to lift (AQE provider-type identity is agentic-qe's
closed enum; native ruflo backend is ruflo's ENABLE_* model), so those become
tracked requests with honest interim behavior. Carries a dated Working/Proposed
self-grade — the machinery (trust CLI, external execution, tiered conformance,
grant store) is staged, not built. ADR-0029 gets a matching amendment note.

* docs: local snapshots of the three companion artifacts, each linking its origin

The host-extensibility explainer (consumer + implementer walkthrough), the
adapter-contract dossier (the research synthesis behind ADR-0029/0031), and the
host/provider consistency master document now live in docs/ as self-contained
HTML, each carrying an origin banner back to its live Claude artifact so the
canonical source is one click away.
@pacphi
pacphi merged commit 46c3f79 into main Aug 16, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant