From 9b15307db5ec2c94f0359e8bac8b3d1ed16ec99c Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Sun, 16 Aug 2026 12:19:39 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(adapters):=20grant-augmented=20host=20?= =?UTF-8?q?overlay=20=E2=80=94=20earned=20capabilities=20go=20live=20in=20?= =?UTF-8?q?the=20effective=20registry=20(ADR-0031=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keystone that fulfils ADR-0031 §1: at bootstrap the admitted-host overlay reads grantedCapabilitiesFor(name, ) and raises canBePrimary/commandStatusline on the effective-registry entry, so hostTierLabel and the new effectivePrimaryHostIds() reflect an earned capability with no call-site special-casing. A local allow-list means the overlay can raise ONLY those two caps — never aqeProvider, canRouteActivities, or any other key, even under an adversarial grant map — and can only raise, never lower the manifest floor. A manifest edited since the grant re-hashes, grantedCapabilitiesFor returns {}, and the cap drops: edit-invalidation flows end to end. Flag-off / nothing-granted is byte-identical (effectiveHostRegistry() === HOST_REGISTRY by identity). grantsByName is a null-prototype map read through Object.hasOwn. drivingHost() falls back to a resolvable built-in for an eligible-but- unresolvable host; validatePrimaryHost and PRIMARY_HOSTS stay built-in-only by design (the external-primary selection UX is a later wave) and say so. --- src/lib/adapters/admission.mjs | 43 ++++++- src/lib/adapters/admitted.mjs | 101 ++++++++++++++- src/lib/adapters/registries.mjs | 14 +++ src/lib/hosts.mjs | 30 ++++- src/lib/routing.mjs | 14 +++ tests/kit/adapter-admission.test.mjs | 88 +++++++++++++ tests/kit/admitted-grants.test.mjs | 180 +++++++++++++++++++++++++++ tests/kit/hosts.test.mjs | 55 ++++++++ tests/kit/routing-primary.test.mjs | 34 ++++- 9 files changed, 552 insertions(+), 7 deletions(-) create mode 100644 tests/kit/admitted-grants.test.mjs diff --git a/src/lib/adapters/admission.mjs b/src/lib/adapters/admission.mjs index 0702ab6..84bbe7a 100644 --- a/src/lib/adapters/admission.mjs +++ b/src/lib/adapters/admission.mjs @@ -217,7 +217,48 @@ export async function bootstrapHostAdapters({ if (admitted.length) { const { applyAdmitted } = await import('./admitted.mjs'); - applyAdmitted(admitted); + + // Keystone (ADR-0031 §1): build the per-host granted-capability lookup + // BEFORE applying the overlay, so an earned canBePrimary/commandStatusline + // is live in effectiveHostRegistry() from process start. Guarded and + // non-fatal, lazy import — the same posture as the execution/lifecycle + // registration blocks below, and a NEW sibling concern to them (it does + // not touch either). One host's grant lookup failing must never block + // another host's, or the admission result itself. + // + // CRITICAL: the hash passed to grantedCapabilitiesFor is computed FRESH + // here, via hashManifest(result.manifest) — never read from a cache or + // carried over from admitOne's own internal hash — because a stale hash + // would silently defeat grants.mjs's edit-invalidation pin (a manifest + // edited since the grant must re-hash to a different value and come back + // {}, dropping the capability). grantedCapabilitiesFor is the ONLY + // sanctioned reader for this (see grants.mjs's module-header invariant); + // reading grantsFor(name).capabilities directly would return the + // unfiltered set and is exactly the bug that invariant exists to prevent. + let grantsByName; + try { + const { grantedCapabilitiesFor } = await import('./grants.mjs'); + // Object.create(null), not {} (F-6): admitted host ids come from + // consented adapter names, which are attacker-influenceable in + // principle — a plain object literal's prototype chain would make + // 'constructor' a live (if inert) key collision. No inherited + // properties at all closes that off entirely; admitted.mjs's own + // Object.hasOwn guard on the read side is the belt to this suspenders. + grantsByName = Object.create(null); + for (const result of admitted) { + try { + grantsByName[result.name] = grantedCapabilitiesFor(result.name, hashManifest(result.manifest)); + } catch (error) { + warnings.push({ name: result.name, reason: 'grant-lookup-failed', detail: error?.message ?? String(error) }); + } + } + } catch { + // grants.mjs unavailable — proceed ungranted (manifest-floor + // capabilities only), exactly like the flag-off path. Never fatal. + grantsByName = undefined; + } + + applyAdmitted(admitted, { grantsByName }); // name -> the cfg entry's own declared source, for F-1's baseDir // derivation below (admitted results carry the validated manifest, not diff --git a/src/lib/adapters/admitted.mjs b/src/lib/adapters/admitted.mjs index 5f42e25..cad384b 100644 --- a/src/lib/adapters/admitted.mjs +++ b/src/lib/adapters/admitted.mjs @@ -30,6 +30,54 @@ function conformOne(item) { return validateHostAdapter(item?.entry ?? item); } +// The ONLY two capabilities a grant may ever flip true (ADR-0031 §1: the +// permanent self-declaration ban — canBePrimary/commandStatusline can never +// come from the manifest itself, see manifest.mjs's cap-can-be-primary / +// cap-command-statusline refusals — so the sole other path to `true` is an +// explicit maintainer grant, applied here). Kept local to this module (not +// imported from grants.mjs's TIER_GRANTS) so this file's own guarantee does +// not depend on grants.mjs staying correct — belt-and-suspenders, per the +// keystone security review. +const GRANTABLE_CAPABILITIES = Object.freeze(['canBePrimary', 'commandStatusline']); + +/** + * Overlay ONE validated, admitted host entry with its earned capability + * grants. `granted` is the caller-supplied {capability: true, ...} map for + * THIS host — bootstrapHostAdapters (admission.mjs) is the sole production + * caller, and it sources `granted` exclusively from grants.mjs's + * grantedCapabilitiesFor() (hash-pinned to the CURRENT manifest, already + * intersected with TIER_GRANTS at read time — see that module's header + * invariant). This function does not simply trust that upstream filtering: + * it re-intersects with its own local GRANTABLE_CAPABILITIES allow-list + * below, so even a caller that got it wrong (or a future misuse of + * applyAdmitted) can never smuggle a capability other than + * canBePrimary/commandStatusline into the overlay — e.g. aqeProvider, or + * flipping canRouteActivities, can NEVER reach an entry this way, no matter + * what `granted` contains. + * + * Grants may only flip a capped capability TRUE, never back to false: the + * validated manifest capabilities are the floor. With no grant (or a grant + * that changes nothing — every relevant flag already true, or absent), the + * SAME `entry` reference is returned unchanged, so the byte-identity + * guarantee (no grants ⇒ effectiveHostRegistry() entries equal the validated + * manifest entries) holds without a caller having to special-case it. + * @param {any} entry — an already-validated, already-frozen host entry + * @param {Record|undefined} granted + */ +function withGrantedCapabilities(entry, granted) { + if (!granted) return entry; + let changed = false; + const capabilities = { ...entry.capabilities }; + for (const capability of GRANTABLE_CAPABILITIES) { + if (granted[capability] === true && capabilities[capability] !== true) { + capabilities[capability] = true; + changed = true; + } + } + if (!changed) return entry; + return Object.freeze({ ...entry, capabilities: Object.freeze(capabilities) }); +} + /** * Set the admitted overlay. Accepts either raw host entries or admission * results ({name, admitted, entry, manifest}) — whichever admitAdapters @@ -37,10 +85,28 @@ function conformOne(item) { * a non-conforming entry), and stores the result. A second call replaces the * overlay outright (useful for tests via resetAdmitted()); production calls * this at most once per process since the env flag gates it. + * + * `grantsByName` (optional) is a `{ [hostId]: { canBePrimary?: true, + * commandStatusline?: true } }` lookup — the keystone that makes an earned + * capability grant LIVE (ADR-0031 §1). Omitted (or a host absent from it), + * the stored entry is exactly the validated manifest entry — the flag-off / + * nothing-granted byte-identity this module has always guaranteed. * @param {ReadonlyArray} entries + * @param {{ grantsByName?: Record> }} [options] */ -export function applyAdmitted(entries) { - admittedEntries = Object.freeze((entries ?? []).map(conformOne)); +export function applyAdmitted(entries, { grantsByName } = {}) { + admittedEntries = Object.freeze((entries ?? []).map((item) => { + const entry = conformOne(item); + // Object.hasOwn, never bare `grantsByName[entry.id]`/`in` (F-6): + // entry.id is a consented-but-still-adapter-supplied string, so an id + // like 'constructor' must never resolve via the prototype chain to + // Object.prototype.constructor and be misread as a truthy grant record. + // Belt-and-suspenders alongside admission.mjs building grantsByName as + // Object.create(null) — this holds even for a caller (a test, a future + // refactor) that passes a plain object literal instead. + const granted = grantsByName && Object.hasOwn(grantsByName, entry.id) ? grantsByName[entry.id] : undefined; + return withGrantedCapabilities(entry, granted); + })); applied = true; return admittedEntries; } @@ -74,3 +140,34 @@ export function effectiveRoutableHostIds() { .filter((host) => host.capabilities.canRouteActivities === true) .map((host) => host.id); } + +/** Built-ins ∪ admitted hosts whose EFFECTIVE capabilities (validated + * manifest floor + any live grant overlay from applyAdmitted) include + * canBePrimary — the primary-eligibility sibling to effectiveRoutableHostIds + * above (ADR-0031 §1 keystone: this is what makes a granted canBePrimary + * actually count somewhere). hosts.mjs's drivingHost() consults this for its + * eligibility check — the primitive is live, though no production path + * currently drives kit.json's routing.primaryHost to an admitted external + * id (the picker that writes it stays built-in-only), so today this has no + * live privileged caller. + * + * Deliberately NOT wired into routing.mjs's PRIMARY_HOSTS constant, which + * stays frozen at import time and built-ins-only: PRIMARY_HOSTS backs the + * primary-host SELECTION UX (`ak setup --primary-host`, `ak host pick`), + * and extending that picker to external hosts is out of scope this wave + * (deferred, same as HOSTS staying display-only in routing.mjs) — an + * eligibility reader and a selection-menu source are different concerns + * even though both currently trace back to the same capability. + * + * Homed here (next to effectiveHostRegistry/effectiveRoutableHostIds), not + * in registries.mjs where the built-in-only primaryHostIds() lives: this + * file already imports registries.mjs for HOST_REGISTRY, so an + * effective-registry-aware selector living in registries.mjs would need the + * reverse import and create a real module cycle; lifecycle-registry.mjs's + * own effectiveHostRegistry import mirrors this same choice. Fresh on every + * call, like effectiveRoutableHostIds() above. */ +export function effectivePrimaryHostIds() { + return effectiveHostRegistry() + .filter((host) => host.capabilities.canBePrimary === true) + .map((host) => host.id); +} diff --git a/src/lib/adapters/registries.mjs b/src/lib/adapters/registries.mjs index 29fb535..6a2e67f 100644 --- a/src/lib/adapters/registries.mjs +++ b/src/lib/adapters/registries.mjs @@ -371,6 +371,20 @@ export function providersWithCapability(entriesOrCapability, maybeCapability) { return entries.filter((provider) => provider.capabilities?.[capability] === true); } +// Built-in-only BY DESIGN, not by oversight (F-4, D2 keystone security +// review): this defaults `hosts` to HOST_REGISTRY, so a caller invoking it +// with no second argument will refuse an admitted-and-granted external host +// even though it has earned canBePrimary. registries.mjs has no knowledge of +// the admitted-overlay/grant machinery (admitted.mjs imports FROM this file, +// not the reverse — see admitted.mjs's effectivePrimaryHostIds() comment), +// so it cannot default to the grant-aware set itself. There is no production +// caller of this function today (fail-closed by omission, not a leak) — but +// if one is ever wired up for an admitted host's eligibility, it MUST pass +// `hosts: effectiveHostRegistry()` explicitly (mirroring how +// materializeRunPlan calls the routing sibling, validateActivityHost, in +// routing.mjs), or use admitted.mjs's effectivePrimaryHostIds() ids-only +// check instead. Do NOT wire this function into a granted-host path using +// its bare default. export function validatePrimaryHost(id, hosts = HOST_REGISTRY) { const host = hosts.find((entry) => entry.id === id); if (!host) return { ok: false, reason: 'unknown-host' }; diff --git a/src/lib/hosts.mjs b/src/lib/hosts.mjs index ee86490..8d6bc5c 100644 --- a/src/lib/hosts.mjs +++ b/src/lib/hosts.mjs @@ -23,7 +23,7 @@ // ~/.codex/auth.json (key overrides login). claude auth on macOS lives in the // Keychain (no readable file); ANTHROPIC_API_KEY, when used, is not a simple // override of a subscription login, so we label it conservatively. -import { HOST_REGISTRY, effectiveHostRegistry } from './adapters/index.mjs'; +import { HOST_REGISTRY, effectiveHostRegistry, effectivePrimaryHostIds } from './adapters/index.mjs'; /** Per-host adapter descriptors. Logical names (`guidanceFile`, `loginFile` * segments) are resolved to real paths by callers so this stays pure. */ @@ -53,6 +53,11 @@ export function adapterFor(id) { * (heuristic) → configured primary (kit.json routing.primaryHost) → 'claude'. * Pure: reads only env + the passed cfg; never spawns. * + * Always returns a host id HOST_ADAPTERS/adapterFor can resolve (a built-in, + * canDriveSession host) — never an id that is merely eligible per + * effectivePrimaryHostIds() (below) but that this module has no session- + * driving descriptor for. + * * @param {NodeJS.ProcessEnv} [env] * @param {any} [cfg] kit.json config (for routing.primaryHost) * @returns {'claude'|'codex'|'opencode'} @@ -65,8 +70,27 @@ export function drivingHost(env = process.env, cfg = null) { // heuristic because the fallback below covers the miss. if (Object.keys(env).some((k) => k.startsWith('CODEX_'))) return 'codex'; const primary = cfg?.routing?.primaryHost; - const primaryCapable = HOST_REGISTRY.some((host) => host.id === primary && host.capabilities.canBePrimary); - if (primary && primaryCapable) return /** @type {'claude'|'codex'} */ (primary); + // effectivePrimaryHostIds() reads the EFFECTIVE (built-in + grant-overlaid + // admitted) set for ELIGIBILITY, not HOST_REGISTRY directly (ADR-0031 §1) — + // that primitive is live, so a hand-edited kit.json pointing + // routing.primaryHost at a host that has since earned a canBePrimary grant + // is recognized as eligible here. (No production path drives kit.json's + // primaryHost to an admitted external id today — `ak host pick`'s + // selection menu stays built-in-scoped this wave — so this is a live + // primitive with no live caller yet, not an active privilege boundary.) + // + // Eligibility alone is not enough to RETURN the host, though: HOST_ADAPTERS + // (built from HOST_REGISTRY, filtered by canDriveSession) is what this + // module actually knows how to drive a session for — guidanceFile, + // statusline, envMarkers, auth — and stays built-in-only; no admitted + // external host has that wiring registered. Without the adapterFor(primary) + // guard, an eligible-but-unresolvable host would be returned here and a + // caller doing `adapterFor(drivingHost(...)).guidanceFile` would crash on + // null. Both checks must agree, so this function's contract (always a + // HOST_ADAPTERS-resolvable id) holds even once effectivePrimaryHostIds() + // can name a host HOST_ADAPTERS doesn't. + const primaryCapable = effectivePrimaryHostIds().includes(primary); + if (primary && primaryCapable && adapterFor(primary)) return /** @type {'claude'|'codex'} */ (primary); return 'claude'; } diff --git a/src/lib/routing.mjs b/src/lib/routing.mjs index 86efc2e..6b91a5b 100644 --- a/src/lib/routing.mjs +++ b/src/lib/routing.mjs @@ -172,6 +172,20 @@ export function formatModelHelp() { // reasoning roles). When codex is chosen as PRIMARY, we mirror each default route // to the opposite host so codex takes the lead and claude becomes the alternate — // a defaults/policy change only (DualModeOrchestrator workers are symmetric). +// Frozen at import time — built-ins only, deliberately (audited for the D2 +// keystone wave, ADR-0031 §1). Every current reader of PRIMARY_HOSTS +// (providers.mjs's applySetupHostFlags `--primary-host` validation, and +// x/host.mjs's `ak host pick` primary-host flag + selection menu) is the +// primary-host SELECTION UX, not an eligibility-VALIDATION path — extending +// that picker surface to an admitted external host is explicitly out of +// scope this wave (the deferred pick surface), mirroring how HOSTS above +// stays display-only. hosts.mjs's drivingHost() does NOT use this constant — +// it consults admitted.mjs's effectivePrimaryHostIds() (fresh per call) +// instead, so the eligibility PRIMITIVE is live there. That said, no +// production path drives kit.json's routing.primaryHost to an admitted +// external id today (the picker that writes it stays built-in-only, as +// above), so this is a live primitive with no live privileged caller yet — +// not an active validation gate anything currently depends on. export const PRIMARY_HOSTS = primaryHostIds(); export const DEFAULT_PRIMARY_HOST = 'claude'; diff --git a/tests/kit/adapter-admission.test.mjs b/tests/kit/adapter-admission.test.mjs index 089c603..99c8fd1 100644 --- a/tests/kit/adapter-admission.test.mjs +++ b/tests/kit/adapter-admission.test.mjs @@ -16,6 +16,7 @@ import { import { applyAdmitted, resetAdmitted, admittedHostIds, effectiveHostRegistry, } from '../../src/lib/adapters/admitted.mjs'; +import { recordTierResult, grantCapability } from '../../src/lib/adapters/grants.mjs'; import { validateAdapterManifest } from '../../src/lib/adapters/manifest.mjs'; import { validateLifecycleAdapter } from '../../src/lib/adapters/lifecycle.mjs'; import { @@ -239,6 +240,93 @@ test('flag-on with a trusted entry admits it and applies the overlay', async () assert.equal(effectiveHostRegistry().length, HOST_REGISTRY.length + 1); }); +// ── D2 keystone: bootstrapHostAdapters wires REAL grants.mjs grants into the +// overlay, live (ADR-0031 §1). Unlike admitted-grants.test.mjs (which injects +// grantsByName directly to test applyAdmitted's own guarantee in isolation), +// these two exercise the actual bootstrap -> grantedCapabilitiesFor(name, +// freshly-computed hash) -> applyAdmitted wiring end to end, against the +// REAL grants.mjs file store — redirected via XDG_CONFIG_HOME to a throwaway +// directory so they never touch the developer's real +// ~/.config/agentic-kit/adapter-grants.json. + +test('bootstrapHostAdapters: a real, currently-hashed grant for primary-eligible makes canBePrimary live in effectiveHostRegistry()', async (t) => { + const prevXdg = process.env.XDG_CONFIG_HOME; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-d2-grant-live-')); + process.env.XDG_CONFIG_HOME = dir; + t.after(() => { process.env.XDG_CONFIG_HOME = prevXdg; fs.rmSync(dir, { recursive: true, force: true }); }); + + const name = 'hermes-grant-live'; + const manifest = validateAdapterManifest(validManifest({ name, host: validHost({ id: name }) })); + const hash = hashManifest(manifest); + recordTierResult(name, 'primary-eligible', { hash, evidence: 'harness-driven lead + escalation, real WorkerResult trail' }); + grantCapability(name, 'canBePrimary', { hash }); + + const result = await bootstrapHostAdapters({ + cfg: { hostAdapters: [{ name, source: 'mem://hermes-grant-live' }] }, + env: { AK_EXPERIMENTAL_HOST_ADAPTERS: '1' }, + readManifest: async () => manifest, + consent: trustingConsent({ [name]: hash }), + }); + assert.equal(result.admitted.length, 1); + const entry = effectiveHostRegistry().find((h) => h.id === name); + assert.ok(entry, 'the admitted host must be in the effective registry'); + assert.equal(entry.capabilities.canBePrimary, true, 'the real, currently-hashed grant must be live'); +}); + +test('bootstrapHostAdapters: a grant recorded against a STALE manifest hash never lights up (edit-invalidation)', async (t) => { + const prevXdg = process.env.XDG_CONFIG_HOME; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-d2-grant-stale-')); + process.env.XDG_CONFIG_HOME = dir; + t.after(() => { process.env.XDG_CONFIG_HOME = prevXdg; fs.rmSync(dir, { recursive: true, force: true }); }); + + const name = 'hermes-grant-stale'; + const oldManifest = validateAdapterManifest(validManifest({ name, host: validHost({ id: name }) })); + const oldHash = hashManifest(oldManifest); + recordTierResult(name, 'primary-eligible', { hash: oldHash, evidence: 'led a run, escalated to' }); + grantCapability(name, 'canBePrimary', { hash: oldHash }); + + // The manifest changes underneath the grant (a real edit — a bumped + // version string) WITHOUT the tier being re-earned/re-granted at the new + // hash. Bootstrap must compute the hash of the manifest it just admitted + // (newHash), never reuse oldHash — so grantedCapabilitiesFor sees a + // mismatch and returns {}. + const newManifest = validateAdapterManifest(validManifest({ name, host: validHost({ id: name }), version: '1.0.1' })); + const newHash = hashManifest(newManifest); + assert.notEqual(oldHash, newHash, 'sanity: the edit must actually change the hash'); + + const result = await bootstrapHostAdapters({ + cfg: { hostAdapters: [{ name, source: 'mem://hermes-grant-stale' }] }, + env: { AK_EXPERIMENTAL_HOST_ADAPTERS: '1' }, + readManifest: async () => newManifest, + consent: trustingConsent({ [name]: newHash }), + }); + assert.equal(result.admitted.length, 1); + const entry = effectiveHostRegistry().find((h) => h.id === name); + assert.equal(entry.capabilities.canBePrimary, false, 'a grant pinned to the OLD hash must not apply to the new manifest'); +}); + +test('bootstrapHostAdapters: with no grant recorded at all, an admitted host stays at the manifest floor (canBePrimary false)', async (t) => { + const prevXdg = process.env.XDG_CONFIG_HOME; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-d2-grant-none-')); + process.env.XDG_CONFIG_HOME = dir; + t.after(() => { process.env.XDG_CONFIG_HOME = prevXdg; fs.rmSync(dir, { recursive: true, force: true }); }); + + const name = 'hermes-grant-none'; + const manifest = validateAdapterManifest(validManifest({ name, host: validHost({ id: name }) })); + const hash = hashManifest(manifest); + + const result = await bootstrapHostAdapters({ + cfg: { hostAdapters: [{ name, source: 'mem://hermes-grant-none' }] }, + env: { AK_EXPERIMENTAL_HOST_ADAPTERS: '1' }, + readManifest: async () => manifest, + consent: trustingConsent({ [name]: hash }), + }); + assert.equal(result.admitted.length, 1); + assert.deepEqual(result.warnings, []); + const entry = effectiveHostRegistry().find((h) => h.id === name); + assert.equal(entry.capabilities.canBePrimary, false); +}); + // ── overlay ────────────────────────────────────────────────────────────── test('effectiveHostRegistry returns the built-in registry (same reference) when nothing is admitted', () => { diff --git a/tests/kit/admitted-grants.test.mjs b/tests/kit/admitted-grants.test.mjs new file mode 100644 index 0000000..a0123b0 --- /dev/null +++ b/tests/kit/admitted-grants.test.mjs @@ -0,0 +1,180 @@ +// The D2 keystone (ADR-0031 §1): a maintainer-granted capability +// (canBePrimary/commandStatusline) must become LIVE in effectiveHostRegistry() +// once applyAdmitted is handed a per-host grantsByName lookup — and must be +// impossible to smuggle anything else through that same seam. This file tests +// admitted.mjs's own overlay guarantee in isolation, with an injected +// grantsByName object (no fs, no grants.mjs) — the full bootstrap -> +// grants.mjs -> applyAdmitted wiring (including the manifest-hash-mismatch +// edit-invalidation case) is covered in adapter-admission.test.mjs instead. +import { test, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { + applyAdmitted, resetAdmitted, effectiveHostRegistry, effectivePrimaryHostIds, +} from '../../src/lib/adapters/admitted.mjs'; +import { HOST_REGISTRY, primaryHostIds } from '../../src/lib/adapters/registries.mjs'; +import { hostTierLabel } from '../../src/lib/hosts.mjs'; + +beforeEach(() => resetAdmitted()); + +/** A validated, admittable host entry — canBePrimary/commandStatusline start + * false (the manifest floor: external adapters may never self-declare + * either, see manifest.mjs's cap-can-be-primary/cap-command-statusline + * refusals), so every capability these tests observe as true came from the + * grant overlay, never the manifest. */ +function admittedHost(overrides = {}) { + return { + id: 'hermes', + label: 'Hermes', + install: { bin: 'hermes', externalInstallPolicy: 'detect-never-overwrite' }, + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: true, + commandStatusline: false, transcripts: true, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + trust: { approvalPolicy: 'unchanged', changes: [] }, + enabledByDefault: false, + configProjection: 'ruflo', + observability: [], + ...overrides, + }; +} + +// ── flag-off / nothing-admitted byte-identity ─────────────────────────────── + +test('nothing admitted: effectiveHostRegistry() is HOST_REGISTRY by identity, effectivePrimaryHostIds() == primaryHostIds()', () => { + assert.equal(effectiveHostRegistry(), HOST_REGISTRY); + assert.deepEqual(effectivePrimaryHostIds(), primaryHostIds()); +}); + +test('applyAdmitted with no grantsByName leaves capabilities at the manifest floor (byte-identical entry)', () => { + applyAdmitted([{ entry: admittedHost() }]); + const entry = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(entry.capabilities.canBePrimary, false); + assert.equal(entry.capabilities.commandStatusline, false); + assert.equal(effectivePrimaryHostIds().includes('hermes'), false); + assert.deepEqual([...effectivePrimaryHostIds()].sort(), [...primaryHostIds()].sort()); +}); + +test('applyAdmitted({grantsByName: {}}) — no entry for this host — is identical to no grantsByName at all', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: {} }); + const entry = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(entry.capabilities.canBePrimary, false); +}); + +// ── the keystone: a granted capability lights up ──────────────────────────── + +test('a granted canBePrimary makes effectiveHostRegistry() show it true, and effectivePrimaryHostIds() include the host', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: { hermes: { canBePrimary: true } } }); + const entry = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(entry.capabilities.canBePrimary, true); + assert.ok(effectivePrimaryHostIds().includes('hermes')); +}); + +test('a granted canBePrimary lights up hostTierLabel — "drives sessions · can lead"', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: { hermes: { canBePrimary: true } } }); + assert.equal(hostTierLabel('hermes'), 'drives sessions · can lead'); +}); + +test('without the grant, hostTierLabel reflects the ungranted (routing-only external) tier', () => { + applyAdmitted([{ entry: admittedHost() }]); + assert.equal(hostTierLabel('hermes'), 'routing only · external adapter · not AQE'); +}); + +test('a granted commandStatusline flips only that flag, leaving canBePrimary at the manifest floor', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: { hermes: { commandStatusline: true } } }); + const entry = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(entry.capabilities.commandStatusline, true); + assert.equal(entry.capabilities.canBePrimary, false); + assert.equal(effectivePrimaryHostIds().includes('hermes'), false); +}); + +test('a host absent from grantsByName stays at its manifest floor while a sibling host in the same call is granted', () => { + applyAdmitted([ + { entry: admittedHost({ id: 'hermes' }) }, + { entry: admittedHost({ id: 'iris' }) }, + ], { grantsByName: { hermes: { canBePrimary: true } } }); + const hosts = effectiveHostRegistry(); + assert.equal(hosts.find((h) => h.id === 'hermes').capabilities.canBePrimary, true); + assert.equal(hosts.find((h) => h.id === 'iris').capabilities.canBePrimary, false); +}); + +// ── defensive allow-list: the overlay can NEVER carry anything else ───────── + +test('a grantsByName entry cannot flip canRouteActivities, transcripts, or any non-grantable capability', () => { + applyAdmitted([{ entry: admittedHost({ + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: false, + commandStatusline: false, transcripts: false, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + }) }], { + grantsByName: { + hermes: { + canBePrimary: true, canRouteActivities: true, transcripts: true, + usage: true, nativeMcpConfig: true, nativeGuidance: true, canDriveSession: true, + }, + }, + }); + const { capabilities } = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(capabilities.canBePrimary, true, 'the one grantable flag actually granted must flip'); + assert.equal(capabilities.canRouteActivities, false, 'not grantable — must stay at the manifest floor'); + assert.equal(capabilities.transcripts, false, 'not grantable — must stay at the manifest floor'); + assert.equal(capabilities.usage, false, 'not grantable — must stay at the manifest floor'); + assert.equal(capabilities.nativeMcpConfig, false, 'not grantable — must stay at the manifest floor'); + assert.equal(capabilities.nativeGuidance, false, 'not grantable — must stay at the manifest floor'); +}); + +test('a grantsByName entry can never introduce a key outside the eight schema-defined capabilities (e.g. aqeProvider)', () => { + applyAdmitted([{ entry: admittedHost() }], { + grantsByName: { hermes: { canBePrimary: true, aqeProvider: 'claude-code' } }, + }); + const { capabilities } = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(capabilities.aqeProvider, undefined, 'aqeProvider must never reach a host capabilities object via a grant'); + assert.deepEqual(Object.keys(capabilities).sort(), [ + 'canBePrimary', 'canDriveSession', 'canRouteActivities', 'commandStatusline', + 'nativeGuidance', 'nativeMcpConfig', 'transcripts', 'usage', + ]); +}); + +test('a grant can never flip a capability back to false — it can only raise, never lower', () => { + applyAdmitted([{ entry: admittedHost({ + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: true, + commandStatusline: false, transcripts: true, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + }) }], { grantsByName: { hermes: { canBePrimary: false } } }); + const { capabilities } = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.equal(capabilities.canBePrimary, false); +}); + +// ── deep-freeze holds on the grant-augmented entry too ────────────────────── + +test('the grant-augmented entry (and its capabilities) is still deep-frozen', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: { hermes: { canBePrimary: true } } }); + const entry = effectiveHostRegistry().find((h) => h.id === 'hermes'); + assert.ok(Object.isFrozen(entry)); + assert.ok(Object.isFrozen(entry.capabilities)); + assert.throws(() => { 'use strict'; entry.capabilities.canBePrimary = false; }, TypeError); + assert.equal(entry.capabilities.canBePrimary, true, 'the mutation attempt must not have taken effect'); +}); + +// ── F-6 (LOW hardening): prototype-chain safety on the grantsByName lookup ── +// entry.id is a consented-but-adapter-supplied string; a host literally named +// 'constructor' must never resolve to Object.prototype.constructor via `[]` +// lookup on a plain object literal (grantsByName here is deliberately `{}`, +// NOT Object.create(null), to prove the READ side's own Object.hasOwn guard +// holds independently of admission.mjs building it null-prototype). + +test('a host id of "constructor" is never misread as granted via the prototype chain (plain-object grantsByName)', () => { + applyAdmitted([{ entry: admittedHost({ id: 'constructor' }) }], { grantsByName: {} }); + const entry = effectiveHostRegistry().find((h) => h.id === 'constructor'); + assert.equal(entry.capabilities.canBePrimary, false); + assert.equal(entry.capabilities.commandStatusline, false); +}); + +test('effectiveHostRegistry() still returns HOST_REGISTRY entries by identity alongside a granted admitted entry', () => { + applyAdmitted([{ entry: admittedHost() }], { grantsByName: { hermes: { canBePrimary: true } } }); + const combined = effectiveHostRegistry(); + assert.ok(HOST_REGISTRY.every((host) => combined.includes(host)), 'built-in entries must be unchanged references'); +}); diff --git a/tests/kit/hosts.test.mjs b/tests/kit/hosts.test.mjs index 666564f..7a95f18 100644 --- a/tests/kit/hosts.test.mjs +++ b/tests/kit/hosts.test.mjs @@ -6,6 +6,7 @@ import path from 'node:path'; import { HOST_IDS, adapterFor, drivingHost, hostTierLabel, hostAsymmetryNote } from '../../src/lib/hosts.mjs'; import { hostAuthState } from '../../src/lib/providers.mjs'; import { managedHostIds } from '../../src/lib/adapters/registries.mjs'; +import { applyAdmitted, resetAdmitted, effectivePrimaryHostIds } from '../../src/lib/adapters/admitted.mjs'; // ── HOST_ADAPTERS descriptors ──────────────────────────────────────────────── // HOST_IDS is HOST_ADAPTERS' key order (hosts.mjs filters HOST_REGISTRY by @@ -72,6 +73,60 @@ test('drivingHost defaults to claude with no signal', () => { assert.equal(drivingHost({}), 'claude'); }); +// ── D2 keystone (ADR-0031 §1), F-3-hardened: drivingHost's ELIGIBILITY check +// reads the EFFECTIVE (grant-overlaid) registry, not HOST_REGISTRY directly — +// that primitive is live even for an admitted external host. But eligibility +// alone is not enough to RETURN the host: HOST_ADAPTERS (this module's own +// session-driving descriptors — guidanceFile, statusline, envMarkers) stays +// built-in-only, so an admitted external host — however eligible, however +// granted — has nothing here to resolve adapterFor() against. drivingHost() +// must fall back to 'claude' rather than return an id a caller's +// `adapterFor(drivingHost(...)).guidanceFile` would crash on. +test('drivingHost recognizes a granted external host as eligible, but still falls back to claude (no HOST_ADAPTERS descriptor to resolve it)', (t) => { + t.after(() => resetAdmitted()); + applyAdmitted([{ + entry: { + id: 'hermes', + label: 'Hermes', + install: { bin: 'hermes', externalInstallPolicy: 'detect-never-overwrite' }, + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: true, + commandStatusline: false, transcripts: true, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + trust: { approvalPolicy: 'unchanged', changes: [] }, + enabledByDefault: false, + configProjection: 'ruflo', + observability: [], + }, + }], { grantsByName: { hermes: { canBePrimary: true } } }); + assert.ok(effectivePrimaryHostIds().includes('hermes'), 'sanity: the eligibility primitive DOES see the grant'); + assert.equal(adapterFor('hermes'), null, 'sanity: this module has no session-driving descriptor for it'); + assert.equal(drivingHost({}, { routing: { primaryHost: 'hermes' } }), 'claude', + 'eligible-but-unresolvable must never be returned — falls back to claude, never crashes a caller downstream'); +}); + +test('drivingHost also falls back to claude for the same external host before it is granted canBePrimary', (t) => { + t.after(() => resetAdmitted()); + applyAdmitted([{ + entry: { + id: 'hermes', + label: 'Hermes', + install: { bin: 'hermes', externalInstallPolicy: 'detect-never-overwrite' }, + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: true, + commandStatusline: false, transcripts: true, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + trust: { approvalPolicy: 'unchanged', changes: [] }, + enabledByDefault: false, + configProjection: 'ruflo', + observability: [], + }, + }]); // no grantsByName + assert.equal(drivingHost({}, { routing: { primaryHost: 'hermes' } }), 'claude'); +}); + // ── hostAuthState (billing axis) ───────────────────────────────────────────── test('hostAuthState reports api-key/metered when the key env is set (codex)', () => { const a = hostAuthState('codex', { env: { OPENAI_API_KEY: 'sk-x' }, present: true }); diff --git a/tests/kit/routing-primary.test.mjs b/tests/kit/routing-primary.test.mjs index a8cf755..7b48933 100644 --- a/tests/kit/routing-primary.test.mjs +++ b/tests/kit/routing-primary.test.mjs @@ -1,7 +1,8 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { swapHostModel, swapRoute, seedActivityRoutes, DEFAULT_ROUTES, DEFAULT_PRIMARY_HOST, PRIMARY_HOSTS } from '../../src/lib/routing.mjs'; -import { primaryHostIds } from '../../src/lib/adapters/index.mjs'; +import { primaryHostIds, effectivePrimaryHostIds } from '../../src/lib/adapters/index.mjs'; +import { applyAdmitted, resetAdmitted } from '../../src/lib/adapters/admitted.mjs'; // ── swapHostModel ──────────────────────────────────────────────────────────── test('swapHostModel maps a claude model to the codex host', () => { @@ -50,3 +51,34 @@ test('DEFAULT_PRIMARY_HOST is claude and PRIMARY_HOSTS mirrors the registry\'s c // instead of a hand-typed host list. assert.deepEqual([...PRIMARY_HOSTS].sort(), [...primaryHostIds()].sort()); }); + +// D2 keystone (ADR-0031 §1): PRIMARY_HOSTS is frozen at import time and stays +// built-in-only ON PURPOSE — it backs the primary-host SELECTION UX +// (`ak setup --primary-host`, `ak host pick`), which this wave deliberately +// does not extend to admitted external hosts (the deferred pick surface; see +// routing.mjs's own comment above PRIMARY_HOSTS). A granted external host +// must NOT appear here even though it DOES appear in the eligibility- +// VALIDATION sibling, effectivePrimaryHostIds() (proven live by +// hosts.test.mjs's drivingHost tests) — these are two different concerns +// that happen to share a capability. +test('PRIMARY_HOSTS stays built-in-only even once an external host is admitted and granted canBePrimary', (t) => { + t.after(() => resetAdmitted()); + applyAdmitted([{ + entry: { + id: 'hermes', + label: 'Hermes', + install: { bin: 'hermes', externalInstallPolicy: 'detect-never-overwrite' }, + capabilities: { + canDriveSession: true, canBePrimary: false, canRouteActivities: true, + commandStatusline: false, transcripts: true, usage: false, + nativeMcpConfig: false, nativeGuidance: false, + }, + trust: { approvalPolicy: 'unchanged', changes: [] }, + enabledByDefault: false, + configProjection: 'ruflo', + observability: [], + }, + }], { grantsByName: { hermes: { canBePrimary: true } } }); + assert.equal(PRIMARY_HOSTS.includes('hermes'), false, 'the frozen, import-time SELECTION constant must not grow'); + assert.ok(effectivePrimaryHostIds().includes('hermes'), 'sanity: the eligibility-VALIDATION sibling DOES see it'); +}); From c60f0277651151e92a9caabf1bbdb2ba3261ac2a Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Sun, 16 Aug 2026 12:19:39 -0700 Subject: [PATCH 2/4] =?UTF-8?q?feat(conformance):=20primary-eligible=20tie?= =?UTF-8?q?r=20genuinely=20passes=20via=20a=20real=20escalation=20exercise?= =?UTF-8?q?=20(ADR-0031=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder with a real, unconditional exercise: the harness drives an actual executeRunPlan where the host anchors a run and, separately, receives a genuine ADR-0019 escalation onto itself after an unrouted host's real cli_unavailable failure — both legs run the adapter's own execution.run hook as real subprocesses. Evidence is derived from the real WorkerResult/attempts trail, recorded with NO pre-existing grant, which breaks the earn/grant deadlock the first cut had: passing records evidence, the maintainer's grant turns it into capability (§2), in that order. There is no injection seam — a caller cannot substitute a fabricated pass. primary-eligible short-circuits to skipped unless activity-routing genuinely passed this run; statusline stays honestly gated. --- src/lib/adapters/conformance.mjs | 223 +++++++++++++++++++++++---- tests/kit/conformance-tiers.test.mjs | 158 +++++++++++++------ 2 files changed, 311 insertions(+), 70 deletions(-) diff --git a/src/lib/adapters/conformance.mjs b/src/lib/adapters/conformance.mjs index 26c0f39..f05f813 100644 --- a/src/lib/adapters/conformance.mjs +++ b/src/lib/adapters/conformance.mjs @@ -249,19 +249,153 @@ async function checkActivityRouting({ }; } -// ── primary-eligible / statusline tiers ───────────────────────────────── -// Both gate a capability the manifest can NEVER declare (canBePrimary, -// commandStatusline are inexpressible in the schema — ADR-0029/0031). Neither -// can be exercised end-to-end today: there is no lead-a-run/receive-an- -// escalation runtime path (ADR-0019) and no admitted-host statusline -// footer-render path anywhere in src/ yet (grep-verified against this wave) — -// so even a granted capability has nothing real to drive through. `exercise` -// is the injection point for the day one of those paths lands: the honest -// default reports 'gated' rather than fabricate a pass, per ADR-0031's "do -// not fake a pass" discipline. Ungranted is reported 'gated' too (an -// ak-local wait on the maintainer's promotion-command grant, Wave D — never -// an upstream ceiling, so it is never persisted via grants.mjs's -// recordTierGate, which is reserved for genuinely upstream gates). +// ── primary-eligible tier: real exercise (ADR-0031 §2, ADR-0019) ─────────── +// The chicken-and-egg this tier exists to break: earning canBePrimary needs +// evidence of leading a run and receiving an escalation, but ak's real +// primary-host machinery (routing.mjs) only ever selects a host that has +// ALREADY got canBePrimary — proving it through that live path would need +// the grant this tier exists to justify. The harness sidesteps that by +// building the lead/escalation plan directly in its OWN sandbox and driving +// it through the real executeRunPlan (ADR-0018/0019) — genuine subprocess +// behaviour, not the live primary-host registry. +// +// F-1 (security review, HIGH, fixed): this exercise runs UNCONDITIONALLY — +// never gated on an existing canBePrimary grant. ADR-0031 §2's own sequence +// is evidence FIRST, grant SECOND ("passing a tier records evidence; the +// maintainer's grant turns evidence into capability"): grantCapability +// (grants.mjs) refuses unless a 'passed' primary-eligible tier is ALREADY +// recorded at this hash, so gating the exercise on the grant it is meant to +// justify is a deadlock — the only way out would be hand-seeding a 'passed' +// tier before ever running the exercise, which defeats the entire earned- +// evidence point. checkPrimaryEligible below records 'passed' straight off a +// genuine exercise result; a maintainer's subsequent `ak host adapters grant +// canBePrimary` is the ONLY thing that turns that recorded evidence +// into a live capability via the keystone overlay (admitted.mjs) — that +// separation, not a pre-exercise grant check, is what ADR-0031 §1 actually +// asks for. (The grant check this replaced existed to stop a caller-supplied +// exercise result from being self-evidence — moot now: there is no +// caller-injectable exercise at all, see below.) +// +// Two things the exercise proves, and what it does NOT claim (F-7): +// - A direct, non-escalated worker completes on the admitted host — the same +// worker-execution path activity-routing already proved, run again here as +// this tier's own independent evidence. +// - A SEPARATE worker starts on a host id this harness invents on the spot — +// never a built-in, never admitted, never registered in ANY adapter +// overlay — which the real runner genuinely cannot route: a real +// `cli_unavailable` WorkerResult, not a fabricated one (no hook trickery, +// no fixture changes needed: the failure is architectural — no adapter +// exists for that host id at all). That worker's one-rung escalation +// ladder points at the admitted host, so a real escalatable failure +// (runner.mjs's ESCALATABLE_STATUSES/BLOCKING_CATEGORIES) genuinely +// advances onto it (ADR-0019) — the `attempts` trail proves more than one +// attempt ran and the admitted host is the LAST rung, succeeded. +// Both completions are the SAME execution.run hook exiting 0 twice, driven +// through the real derived execution adapter — this demonstrates the +// escalation ladder and execution-adapter contract plumbing over an +// already-proven activity-routing path. It is NOT evidence the host can +// "lead agentic work" in any qualitative sense — the evidence string below +// is worded to that precision, not more. +// +// This needs the host's activity-routing to actually work (it runs real +// workers through the same derived execution adapter), so this tier depends +// on activity-routing genuinely passing THIS run — enforced by the caller +// (runTieredConformance) before this function is ever reached. +// +// No caller-injectable seam: unlike the placeholder this replaces, there is +// no `exercisePrimaryEligible` option anymore — the ONLY exercise that can +// ever run is this real one, so a caller cannot launder a fabricated +// {ok:true} into a pass (Wave C's off-the-CLI constraint, hardened: not just +// unreachable from the CLI, structurally unreachable from ANY caller). +const PRIMARY_ELIGIBLE_UNROUTED_HOST_SUFFIX = 'conformance-unrouted-rung'; + +async function runPrimaryEligibleExercise({ + manifest, name, baseDir, haveFn, clock, +}) { + const unroutedHost = `${PRIMARY_ELIGIBLE_UNROUTED_HOST_SUFFIX}-${name}`; + resetAdmittedExecution(); + try { + registerAdmittedExecution(manifest, { haveFn, baseDir }); + const plan = { + workers: [ + { + id: 'primary-eligible-direct', activity: 'implementation', role: 'coder', host: name, + prompt: 'conformance harness probe: primary-eligible direct run (non-escalated)', + }, + { + id: 'primary-eligible-escalation', activity: 'implementation', role: 'coder', host: unroutedHost, + prompt: 'conformance harness probe: primary-eligible escalation (receives an escalation)', + escalate: [{ host: name, model: null }], + }, + ], + }; + const [direct, escalation] = await executeRunPlan(plan, { clock, escalate: true }); + + if (direct?.status !== 'succeeded' || direct.host !== name) { + return { ok: false, detail: `direct (non-escalated) worker did not complete via '${name}' (status=${direct?.status})` }; + } + const attempts = escalation?.attempts ?? []; + const lastAttempt = attempts[attempts.length - 1]; + const genuinelyEscalated = attempts.length > 1 + && lastAttempt?.host === name && lastAttempt?.status === 'succeeded' + && attempts.slice(0, -1).every((a) => a.host !== name); + if (escalation?.status !== 'succeeded' || escalation.host !== name || !genuinelyEscalated) { + return { + ok: false, + detail: `escalation worker did not genuinely advance onto '${name}' via ADR-0019 (status=${escalation?.status}, attempts=${attempts.length})`, + }; + } + return { + ok: true, + detail: `'${name}' completed a direct (non-escalated) run to a succeeded result, and separately received a ` + + `genuine ADR-0019 escalation onto itself after an unrouted host's real cli_unavailable failure ` + + `(${attempts.length} attempts: ${attempts.map((a) => `${a.host}=${a.status}`).join(' -> ')}) — both runs ` + + "completed via the adapter's real execution.run hook (escalation-ladder and execution-contract plumbing " + + 'over the already-proven activity-routing path, not a claim about agentic task quality)', + }; + } catch (error) { + return { ok: false, detail: `primary-eligible exercise threw: ${error?.message ?? String(error)}` }; + } finally { + resetAdmittedExecution(); + } +} + +/** + * primary-eligible's own check (ADR-0031 §2, ADR-0019) — deliberately NOT + * checkGrantGatedTier: F-1 (security review) found that gating this + * exercise on an EXISTING canBePrimary grant deadlocks against + * grantCapability's own requirement of an already-'passed' tier. This runs + * the real exercise unconditionally (once admission and activity-routing + * have genuinely passed) and reports 'passed' straight off a genuine result + * — recordTierResult (the caller's shared persist step) writes that evidence + * regardless of whether anything is granted yet, which is precisely what + * lets a maintainer's later grantCapability succeed at all. + */ +async function checkPrimaryEligible({ + manifest, name, baseDir, haveFn, clock, +}) { + const exerciseLabel = 'leads a run and receives an escalation (ADR-0019)'; + const outcome = await runPrimaryEligibleExercise({ + manifest, name, baseDir, haveFn, clock, + }); + if (outcome.ok) { + return { status: 'passed', checks: [{ name: exerciseLabel, ok: true, detail: outcome.detail }], evidence: outcome.detail }; + } + return { status: 'failed', checks: [{ name: exerciseLabel, ok: false, detail: outcome.detail }] }; +} + +// ── statusline tier ───────────────────────────────────────────────────── +// Gates a capability the manifest can NEVER declare (commandStatusline is +// inexpressible in the schema — ADR-0029/0031). Cannot be exercised +// end-to-end today: there is no admitted-host statusline footer-render path +// anywhere in src/ yet (grep-verified against this wave) — so even a granted +// capability has nothing real to drive through. `exercise` is the injection +// point for the day that path lands: the honest default reports 'gated' +// rather than fabricate a pass, per ADR-0031's "do not fake a pass" +// discipline. Ungranted is reported 'gated' too (an ak-local wait on the +// maintainer's promotion-command grant, Wave D — never an upstream ceiling, +// so it is never persisted via grants.mjs's recordTierGate, which is +// reserved for genuinely upstream gates). async function checkGrantGatedTier({ capability, manifest, name, hash, grantsFile, exercise, exerciseLabel, }) { @@ -312,13 +446,25 @@ async function checkGrantGatedTier({ * conformance run to touch the real store (tests, dry runs) must pass an * explicit path. Pass `persist: false` to skip recording altogether. * + * primary-eligible has no caller-injectable exercise (unlike statusline, + * still a placeholder this wave): it always runs runPrimaryEligibleExercise, + * a real escalation-plus-direct-run probe driven through executeRunPlan + * (ADR-0018/0019), UNCONDITIONALLY — never gated on an existing canBePrimary + * grant (F-1, security review: gating the exercise on the grant it exists to + * justify is a deadlock against grantCapability's own "already passed" + * requirement). It depends on activity-routing genuinely passing in THIS run + * (computed regardless of whether 'activity-routing' is in `tiers`) — + * 'skipped' with "activity-routing did not pass" otherwise, mirroring the + * admission short-circuit above it. A genuine pass is recorded via + * recordTierResult like any other tier; a maintainer's subsequent + * grantCapability call is what turns that evidence into a live capability. + * * @param {{ * fixtureRoot?: string, manifestSource?: string, name?: string, * tiers?: readonly string[], readManifest?: (source: string) => Promise, * consentFile?: string, grantsFile?: string, persist?: boolean, * haveFn?: (cmd: string, opts?: any) => Promise, baseDir?: string|null, * sessionDrivingUpstreamRef?: string, - * exercisePrimaryEligible?: (ctx: {manifest:any,name:string,hash:string}) => Promise<{ok:boolean,detail?:string}>, * exerciseStatusline?: (ctx: {manifest:any,name:string,hash:string}) => Promise<{ok:boolean,detail?:string}>, * clock?: () => string, * }} [options] @@ -339,7 +485,6 @@ export async function runTieredConformance({ haveFn = have, baseDir, sessionDrivingUpstreamRef, - exercisePrimaryEligible, exerciseStatusline, clock = nowIso, } = {}) { @@ -393,23 +538,47 @@ export async function runTieredConformance({ tierResults.push({ tier: 'session-driving', ...checkSessionDriving({ manifest: effectiveManifest, upstreamRef: sessionDrivingUpstreamRef }) }); } - if (wantTier('activity-routing')) { - const result = await checkActivityRouting({ + // primary-eligible's real exercise runs actual workers through the + // admitted host's derived execution adapter, so it needs activity-routing + // to have genuinely passed THIS run — not merely to have been requested. + // Computed here, once, whenever either tier needs it, so a caller asking + // for `tiers: ['primary-eligible']` alone still gets the real dependency + // check rather than an unconditioned pass/skip. + let activityRoutingResult = null; + if (wantTier('activity-routing') || wantTier('primary-eligible')) { + activityRoutingResult = await checkActivityRouting({ manifest: effectiveManifest, name: resolvedName, baseDir: derivedBaseDir, haveFn, clock, }); - tierResults.push({ tier: 'activity-routing', ...result }); + if (wantTier('activity-routing')) { + tierResults.push({ tier: 'activity-routing', ...activityRoutingResult }); + } } if (wantTier('primary-eligible')) { - const result = await checkGrantGatedTier({ - capability: 'canBePrimary', - manifest: effectiveManifest, - name: resolvedName, - hash, - grantsFile, - exercise: exercisePrimaryEligible, - exerciseLabel: 'leads a run and receives an escalation (ADR-0019)', - }); + // F-1 (security review): NOT checkGrantGatedTier — that gates the + // exercise on an already-existing canBePrimary grant, which deadlocks + // against grantCapability's own requirement of an already-'passed' + // tier (see checkPrimaryEligible's header comment). checkPrimaryEligible + // runs the real exercise unconditionally once its own prerequisites + // (admission, activity-routing) are met — evidence first, grant second, + // per ADR-0031 §1/§2. + let result; + if (!effectiveManifest) { + result = { status: 'skipped', checks: [{ name: 'admission prerequisite', ok: false, detail: 'admission tier did not pass — cannot evaluate' }] }; + } else if (activityRoutingResult?.status !== 'passed') { + // Mirrors the admission short-circuit above: when admission passed + // but activity-routing (this tier's own real prerequisite) did not, + // report the SAME 'skipped' shape with a distinct reason, rather + // than attempting an exercise the host cannot actually support yet. + result = { + status: 'skipped', + checks: [{ name: 'activity-routing prerequisite', ok: false, detail: 'activity-routing did not pass — cannot evaluate' }], + }; + } else { + result = await checkPrimaryEligible({ + manifest: effectiveManifest, name: resolvedName, baseDir: derivedBaseDir, haveFn, clock, + }); + } tierResults.push({ tier: 'primary-eligible', ...result }); } diff --git a/tests/kit/conformance-tiers.test.mjs b/tests/kit/conformance-tiers.test.mjs index f853769..e68e9a1 100644 --- a/tests/kit/conformance-tiers.test.mjs +++ b/tests/kit/conformance-tiers.test.mjs @@ -12,7 +12,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { runTieredConformance, CONFORMANCE_TIERS } from '../../src/lib/adapters/conformance.mjs'; -import { grantsFor, recordTierResult, grantCapability } from '../../src/lib/adapters/grants.mjs'; +import { grantsFor, grantCapability, grantedCapabilitiesFor } from '../../src/lib/adapters/grants.mjs'; import { lifecycleAdapterFor } from '../../src/lib/adapters/lifecycle-registry.mjs'; const FIXTURE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../fixtures/adapters/acme'); @@ -259,54 +259,62 @@ test('session-driving tier persists via recordTierGate when a real upstream ref assert.equal(record.tiers['session-driving'].gatedBy, 'ruvnet/ruflo#9001'); }); -// ── primary-eligible / statusline: honest gated, ungranted or unbuilt ────── +// ── statusline: honest gated, ungranted (untouched this wave) ────────────── -test('primary-eligible and statusline tiers are gated (never passed) against acme with no grant recorded', async () => { +test('statusline tier is gated (never passed) against acme with no grant recorded', async () => { const grantsFile = tempGrantsFile(); const report = await runTieredConformance({ manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, - tiers: ['primary-eligible', 'statusline'], + tiers: ['statusline'], grantsFile, }); - for (const tier of report.tiers) { - assert.equal(tier.status, 'gated', `${tier.tier}: ${JSON.stringify(tier.checks)}`); - assert.notEqual(tier.status, 'passed'); - assert.match(tier.detail, /ak-local/); - } + const [tier] = report.tiers; + assert.equal(tier.status, 'gated', JSON.stringify(tier.checks)); + assert.notEqual(tier.status, 'passed'); + assert.match(tier.detail, /ak-local/); assert.equal(grantsFor('acme', { file: grantsFile }), null); }); -// F-6 (security review): the property that actually holds the "no exercise -// confers a grant" invariant is checkGrantGatedTier evaluating `granted` -// BEFORE ever calling `exercise` (conformance.mjs) — a caller-supplied -// exercise result can never confer the FIRST grant, because exercise is -// simply never reached without one already recorded. Pin that ordering -// directly: inject a real exercisePrimaryEligible with NO existing grant. -test('primary-eligible tier stays gated (never passed) when an exercise callback is injected but no grant exists — granted is checked BEFORE exercise runs', async () => { +// ── primary-eligible: honest activity-routing dependency ─────────────────── + +test('primary-eligible tier is skipped when activity-routing did not pass this run', async () => { const grantsFile = tempGrantsFile(); + // No haveFn override: acme's detection.bin will never be on a test + // machine's real PATH, so activity-routing genuinely fails this run — + // primary-eligible must short-circuit on that. const report = await runTieredConformance({ manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, tiers: ['primary-eligible'], grantsFile, - exercisePrimaryEligible: async () => ({ ok: true }), }); const [tier] = report.tiers; - assert.equal(tier.status, 'gated'); + assert.equal(tier.status, 'skipped', JSON.stringify(tier.checks)); assert.notEqual(tier.status, 'passed'); - assert.equal(grantsFor('acme', { file: grantsFile }), null, 'a caller-supplied exercise result must never confer the FIRST grant'); + assert.match(tier.checks[0].detail, /activity-routing did not pass/); + assert.equal(grantsFor('acme', { file: grantsFile }), null); }); -test('primary-eligible tier stays honestly gated even when GRANTED, because no lead/escalation runtime path is built yet', async () => { +test('primary-eligible tier is skipped on a missing activity-routing prerequisite even when a real grant was earned earlier through the sanctioned flow', async () => { const grantsFile = tempGrantsFile(); - // Seed a real grant the way the (future) promotion command would. - const hash = (await runTieredConformance({ - manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, tiers: ['admission'], grantsFile, - })).hash; - recordTierResult('acme', 'primary-eligible', { hash, evidence: 'seeded for test' }, { file: grantsFile }); - grantCapability('acme', 'canBePrimary', { hash }, { file: grantsFile }); + // Earn the grant honestly first, via the real sanctioned sequence (ADR-0031 + // §2): a run with activity-routing genuinely passing records a real + // 'passed' primary-eligible tier, which grantCapability (grants.mjs) + // requires before it will confer canBePrimary — never hand-seeded. + const earned = await runTieredConformance({ + manifestSource: VALID_MANIFEST_PATH, + readManifest: readManifestFromFile, + tiers: ['primary-eligible'], + grantsFile, + haveFn: async () => true, + }); + assert.equal(earned.tiers[0].status, 'passed', JSON.stringify(earned.tiers[0].checks)); + grantCapability('acme', 'canBePrimary', { hash: earned.hash }, { file: grantsFile }); + // Re-run WITHOUT the haveFn override — activity-routing genuinely fails + // this run, so primary-eligible must short-circuit regardless of the (now + // real, honestly earned) grant already sitting in the store. const report = await runTieredConformance({ manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, @@ -314,33 +322,93 @@ test('primary-eligible tier stays honestly gated even when GRANTED, because no l grantsFile, }); const [tier] = report.tiers; - assert.equal(tier.status, 'gated'); - assert.notEqual(tier.status, 'passed'); - assert.match(tier.detail, /granted/); - assert.match(tier.detail, /not built/); + assert.equal(tier.status, 'skipped', JSON.stringify(tier.checks)); + assert.match(tier.checks[0].detail, /activity-routing did not pass/); }); -test('primary-eligible tier PASSES when granted AND a real exercise path is injected (proves the injection seam, not a built-in fake)', async () => { +// ── primary-eligible: the real exercise, evidence-first (F-1 fix) ────────── +// F-1 (security review, HIGH, fixed): the exercise runs UNCONDITIONALLY — +// never gated on a pre-existing canBePrimary grant. Gating it on the grant +// it exists to justify was a deadlock: grantCapability (grants.mjs) refuses +// unless a 'passed' primary-eligible tier is ALREADY recorded at this hash, +// so the only way it could ever pass before this fix was hand-seeding a fake +// tier result first — defeating the whole earned-evidence point of +// ADR-0031 §2 ("passing a tier records evidence; the maintainer's grant +// turns evidence into capability" — evidence comes FIRST). These tests prove +// the corrected, sanctioned sequence with NO hand-seeding anywhere. + +test('primary-eligible tier GENUINELY PASSES via the real exercise with NO pre-existing grant — evidence records first, per ADR-0031 §2', async () => { const grantsFile = tempGrantsFile(); - const hash = (await runTieredConformance({ - manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, tiers: ['admission'], grantsFile, - })).hash; - recordTierResult('acme', 'primary-eligible', { hash, evidence: 'seeded for test' }, { file: grantsFile }); - grantCapability('acme', 'canBePrimary', { hash }, { file: grantsFile }); + assert.equal(grantsFor('acme', { file: grantsFile }), null, 'sanity: nothing recorded before this run'); const report = await runTieredConformance({ manifestSource: VALID_MANIFEST_PATH, readManifest: readManifestFromFile, tiers: ['primary-eligible'], grantsFile, - exercisePrimaryEligible: async () => ({ ok: true, detail: 'led a run and received a simulated escalation' }), + haveFn: async () => true, }); const [tier] = report.tiers; - assert.equal(tier.status, 'passed'); - assert.equal(tier.evidence, 'led a run and received a simulated escalation'); + assert.equal(tier.status, 'passed', JSON.stringify(tier.checks, null, 2)); + // Real evidence, not a fabricated string — worded to what was actually + // demonstrated (a direct run plus a genuine ADR-0019 escalation onto the + // same host: ladder + execution-contract plumbing), not an overclaim about + // agentic task quality (F-7). + assert.match(tier.evidence, /'acme' completed a direct \(non-escalated\) run/); + assert.match(tier.evidence, /genuine ADR-0019 escalation onto itself/); + assert.match(tier.evidence, /2 attempts:/); + assert.match(tier.evidence, /=failed -> acme=succeeded/); + const record = grantsFor('acme', { file: grantsFile }); assert.equal(record.tiers['primary-eligible'].status, 'passed'); - assert.equal(record.tiers['primary-eligible'].evidence, 'led a run and received a simulated escalation'); + assert.match(record.tiers['primary-eligible'].evidence, /genuine ADR-0019 escalation/); +}); + +test('the sanctioned §2 sequence end to end: a freshly-passed tier with NO prior grant lets grantCapability confer a LIVE canBePrimary', async () => { + const grantsFile = tempGrantsFile(); + assert.equal(grantsFor('acme', { file: grantsFile }), null, 'sanity: nothing recorded before this run'); + + // Step 1: the real exercise earns the evidence (no grant exists yet). + const report = await runTieredConformance({ + manifestSource: VALID_MANIFEST_PATH, + readManifest: readManifestFromFile, + tiers: ['primary-eligible'], + grantsFile, + haveFn: async () => true, + }); + assert.equal(report.tiers[0].status, 'passed', JSON.stringify(report.tiers[0].checks)); + assert.equal( + grantedCapabilitiesFor('acme', report.hash, { file: grantsFile }).canBePrimary, + undefined, + 'passing the tier alone must never itself grant the capability', + ); + + // Step 2: the maintainer's explicit act (ADR-0031 §1) — only possible now + // because the tier is genuinely 'passed' at this hash, not because + // anything above hand-seeded it. + grantCapability('acme', 'canBePrimary', { hash: report.hash }, { file: grantsFile }); + assert.equal(grantedCapabilitiesFor('acme', report.hash, { file: grantsFile }).canBePrimary, true); +}); + +test('primary-eligible tier: a caller-supplied legacy exercisePrimaryEligible option has no effect — there is no injection seam, only the real exercise ever runs', async () => { + const grantsFile = tempGrantsFile(); + const report = await runTieredConformance({ + manifestSource: VALID_MANIFEST_PATH, + readManifest: readManifestFromFile, + tiers: ['primary-eligible'], + grantsFile, + haveFn: async () => true, + // Not a real option on runTieredConformance anymore (silently ignored) — + // proves a caller cannot launder a fabricated pass through what used to + // be the injection point. + exercisePrimaryEligible: async () => ({ ok: true, detail: 'FAKE-LAUNDERED-PASS' }), + }); + const [tier] = report.tiers; + assert.equal(tier.status, 'passed'); + assert.notEqual(tier.evidence, 'FAKE-LAUNDERED-PASS'); + assert.match(tier.evidence, /genuine ADR-0019 escalation onto itself/); + const record = grantsFor('acme', { file: grantsFile }); + assert.notEqual(record.tiers['primary-eligible'].evidence, 'FAKE-LAUNDERED-PASS'); }); test('statusline tier is skipped/gated (never passed) when the admission prerequisite tier is not part of this run and the manifest never admitted', async () => { @@ -358,7 +426,7 @@ test('statusline tier is skipped/gated (never passed) when the admission prerequ // ── full sequence + persistence summary ───────────────────────────────── -test('running all five tiers together against acme yields exactly one passed-non-admission tier (activity-routing) plus admission, with nothing faked', async () => { +test('running all five tiers together against acme yields three genuinely-passed tiers (admission, activity-routing, primary-eligible) plus an honestly gated statusline, with nothing faked', async () => { const grantsFile = tempGrantsFile(); const report = await runTieredConformance({ manifestSource: VALID_MANIFEST_PATH, @@ -371,13 +439,17 @@ test('running all five tiers together against acme yields exactly one passed-non assert.equal(byTier.admission, 'passed'); assert.equal(byTier['session-driving'], 'skipped'); // acme declares canDriveSession:false assert.equal(byTier['activity-routing'], 'passed'); - assert.equal(byTier['primary-eligible'], 'gated'); + // primary-eligible now genuinely passes in the same run (F-1 fix): its + // real exercise is unconditional, not gated on a pre-existing grant — no + // grant was seeded anywhere in this test. + assert.equal(byTier['primary-eligible'], 'passed'); assert.equal(byTier.statusline, 'gated'); const record = grantsFor('acme', { file: grantsFile }); assert.equal(record.tiers.admission.status, 'passed'); assert.equal(record.tiers['activity-routing'].status, 'passed'); - assert.equal(record.tiers['primary-eligible'], undefined, 'an ak-local gate with no upstream ref persists nothing'); + assert.equal(record.tiers['primary-eligible'].status, 'passed'); + assert.ok(record.tiers['primary-eligible'].evidence.length > 0); assert.equal(record.tiers.statusline, undefined); }); From ecb89718ef5813c831476d08a51def3699ec8270 Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Sun, 16 Aug 2026 12:19:39 -0700 Subject: [PATCH 3/4] fix(cli): grant disclosure states what a grant actually does now that it is live (ADR-0031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the keystone landed, a grant is no longer inert: the confirmation and success messages now say it takes effect in the effective host registry from the next ak invocation (tier label + effectivePrimaryHostIds), while honestly bounding the two consumption gaps — no path yet selects an external host as primary, and commandStatusline has no runtime reader — so the maintainer sees exactly which grantable capability does something today. --- src/commands/x/host-adapters-grants.mjs | 40 +++++++++++++++++++---- tests/kit/host-adapters-cli.test.mjs | 43 +++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/commands/x/host-adapters-grants.mjs b/src/commands/x/host-adapters-grants.mjs index f905531..da9ced9 100644 --- a/src/commands/x/host-adapters-grants.mjs +++ b/src/commands/x/host-adapters-grants.mjs @@ -39,6 +39,28 @@ function gatingTierFor(capability) { return Object.entries(TIER_GRANTS).find(([, cap]) => cap === capability)?.[0]; } +/** F-2 (security re-review, HIGH): what a grant of `capability` ACTUALLY + * does today — precisely bounded, not overclaimed and not underclaimed. The + * D2 keystone (admission.mjs) makes grantedCapabilitiesFor overlay into + * effectiveHostRegistry() on every AK_EXPERIMENTAL_HOST_ADAPTERS=1 + * invocation, so a granted canBePrimary is genuinely live from the next + * invocation: hostTierLabel() shows 'drives sessions · can lead' and the + * host joins effectivePrimaryHostIds(). What is still deferred: no + * production path SELECTS an external host as primary today (`ak host pick + * --primary-host` only accepts claude|codex — built-in-scoped), so a + * granted canBePrimary is visible/eligible but not yet auto-consumed. + * commandStatusline reaches the same overlay but has NO runtime reader + * anywhere in src/ (grep-verified) — its statusline render path is a later + * wave, so it is currently inert. F-5: this is also the one-sentence + * distinction between the two grantable caps the disclosure owes the + * maintainer, so both call sites (pre-confirm disclosure, post-grant + * success) share this single source of truth rather than drifting. */ +function capabilityStatusNote(capability) { + return capability === 'canBePrimary' + ? "canBePrimary is live: from the next ak invocation this host's tier label shows 'can lead' and it joins effectivePrimaryHostIds() — but no production path yet SELECTS an external host as primary ('ak host pick' stays built-in-scoped), so this is visible/eligible, not yet auto-consumed." + : 'commandStatusline is currently inert: it reaches the effective host registry, but no runtime path reads it anywhere yet — the statusline render path is a later wave.'; +} + export async function grant({ name, capability, cfg, consent, reader, ask, isTTY, yes, grantsFile, }) { @@ -98,7 +120,8 @@ export async function grant({ } console.log(` manifest trust state: ${trustState}`); info('this grant pins the MANIFEST content (its hash), not the hook script bytes it references — see ADR-0031 §2 for that boundary.'); - info("granting a capability is a trust act, same posture as 'trust': it is recorded now, but nothing in ak reads granted capabilities into runtime behaviour yet — see the note after recording."); + info("granting a capability is a trust act, same posture as 'trust': it takes effect in the effective host registry from the next ak invocation."); + info(capabilityStatusNote(capability)); if (!yes) { if (!isTTY) { @@ -118,12 +141,15 @@ export async function grant({ ok(`granted '${safeCapability}' to '${safeName}' at ${hash}`); info('an edit to the manifest voids this grant until the tier is re-earned and re-granted'); - // F-7 (honesty, ADR-0023): no runtime code reads granted capabilities - // today — grantedCapabilitiesFor's only call sites are the conformance - // harness's own gate and `ak host adapters status`. The grant is recorded, - // not yet live; it lights up once a later wave wires granted capabilities - // into actual runtime behaviour. - info("this grant is recorded, not yet live: nothing in ak reads granted capabilities into runtime behaviour today — it will take effect once a later wave wires that in"); + // F-2 (security re-review, HIGH — corrects the prior F-7 wording, which + // was accurate when written but was made FALSE by the D2 keystone + // (admission.mjs) that landed since: grantedCapabilitiesFor is now read on + // every flagged ak invocation and overlaid into effectiveHostRegistry(), + // so a granted capability is NOT inert in general — see + // capabilityStatusNote's header comment for exactly what is and isn't + // live yet, per capability. + info("this grant takes effect from the next ak invocation (with AK_EXPERIMENTAL_HOST_ADAPTERS=1 set): it is reflected in the effective host registry and this host's tier label."); + info(capabilityStatusNote(capability)); return 0; } diff --git a/tests/kit/host-adapters-cli.test.mjs b/tests/kit/host-adapters-cli.test.mjs index cb4cd7f..2edc5e8 100644 --- a/tests/kit/host-adapters-cli.test.mjs +++ b/tests/kit/host-adapters-cli.test.mjs @@ -824,12 +824,17 @@ test('conformance: happy path against the real acme fixture prints a per-tier ta assert.match(text, /admission\s+passed/); assert.match(text, /activity-routing\s+passed/); assert.match(text, /session-driving\s+skipped/); - assert.match(text, /primary-eligible\s+gated/); + // primary-eligible now runs its real exercise unconditionally once + // admission and activity-routing have genuinely passed (conformance.mjs's + // checkPrimaryEligible, landed concurrently on this branch) — it is no + // longer gated on a pre-existing grant, so it genuinely passes here. + assert.match(text, /primary-eligible\s+passed/); assert.match(text, /statusline\s+gated/); const record = grantsFor('acme', { file: grantsFile }); assert.equal(record.tiers.admission.status, 'passed'); assert.equal(record.tiers['activity-routing'].status, 'passed'); + assert.equal(record.tiers['primary-eligible'].status, 'passed'); }); test('conformance: nothing is recorded when the flag is off, even with a real cfg entry and grantsFile supplied', async () => { @@ -958,12 +963,44 @@ test('grant: happy path — capability granted once the gating tier is force-rec assert.equal(code, 0, cap.text()); assert.match(cap.text(), /granted 'canBePrimary'/); // F-3: the disclosure prints the actual evidence backing the tier, not - // just a hash, and the F-7 honesty note that the grant is not yet live. + // just a hash. F-2 (security re-review, HIGH): the D2 keystone + // (admission.mjs) makes a granted canBePrimary genuinely live in the + // effective host registry / tier label / primary-eligibility set from the + // next invocation — the message must say so, not claim the grant is + // inert. It must also disclose the honestly-deferred last mile: no + // production path yet SELECTS an external host as primary. assert.match(cap.text(), /tier evidence:\s+leads a run, receives escalation/); - assert.match(cap.text(), /not yet live/); + assert.match(cap.text(), /effective host registry/); + assert.match(cap.text(), /canBePrimary is live/); + assert.match(cap.text(), /effectivePrimaryHostIds/); + assert.match(cap.text(), /no production path yet SELECTS an external host as primary/); + assert.doesNotMatch(cap.text(), /not yet live/, 'must never claim the grant is inert now that D2 makes it live'); assert.deepEqual(grantedCapabilitiesFor('hermes', hash, { file: grantsFile }), { canBePrimary: true }); }); +test('grant: commandStatusline is honestly disclosed as currently inert (no runtime reader yet), unlike canBePrimary', async () => { + const grantsFile = tmpGrantsFile(); + const raw = validManifest(); + const hash = hashManifest(validateAdapterManifest(raw)); + recordTierResult('hermes', 'statusline', { hash, evidence: 'footer renders' }, { file: grantsFile }); + const cfg = cfgWith([{ name: 'hermes', source: 'mem://hermes' }]); + + const cap = capture(); + let code; + try { + code = await run({ + positionals: ['grant', 'hermes', 'commandStatusline'], env: ON_ENV, cfg, + reader: async () => raw, ask: async () => true, isTTY: true, grantsFile, flags: {}, + consent: fileConsent(tmpConsentFile()), + }); + } finally { cap.restore(); } + + assert.equal(code, 0, cap.text()); + assert.match(cap.text(), /commandStatusline is currently inert/); + assert.match(cap.text(), /no runtime path reads it/); + assert.deepEqual(grantedCapabilitiesFor('hermes', hash, { file: grantsFile }), { commandStatusline: true }); +}); + // N-1 (security re-review): grant must resolve the manifest source EXACTLY // ONCE. Previously the disclosure's trust-state line called stateFor, which // ran its own independent loadAndHash — on a mutable remote source a second From 4cead35363ee9a37a85327ecb087a2416626e592 Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Sun, 16 Aug 2026 12:19:40 -0700 Subject: [PATCH 4/4] =?UTF-8?q?docs(adr):=20ADR-0031=20=E2=80=94=20primary?= =?UTF-8?q?-eligible=20now=20passes,=20grants=20go=20live=20in=20the=20reg?= =?UTF-8?q?istry;=20track=20N-1=20un-earn=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/adr/0031-capability-graduation-and-upstream-requests.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0031-capability-graduation-and-upstream-requests.md b/docs/adr/0031-capability-graduation-and-upstream-requests.md index 3659feb..d741c7c 100644 --- a/docs/adr/0031-capability-graduation-and-upstream-requests.md +++ b/docs/adr/0031-capability-graduation-and-upstream-requests.md @@ -165,8 +165,8 @@ unbuilt. This table is the source of truth for what is real. | `ak host adapters trust` CLI (records consent/grants) | **Working** (2026-08-16, wave A) | `list`/`trust`/`revoke` + `--expect-hash` pinning; disclosure prints the full validated manifest (control-char-safe); mirrors every pre-hash admission refusal; `revoke` works with the flag off (fail-safe) | | External execution (`ak run` drives an admitted host) | **Working** (2026-08-16, wave B) | Manifest `execution.run` hook (coupled to `canRouteActivities`, else refused `execution-not-routable`); derived subprocess adapter behind `executionAdapterFor`; routing is overlay-aware via a lazy `effectiveRoutableHostIds()`. Security-hardened (adversarial review): hooks spawn with `cwd` pinned to the adapter's own resolved directory (never the operator's cwd — a relative hook on a remote source is refused `execution-unanchored`); an unresolved-launch cancellation reports `orphaned` (non-escalating), never an escalatable `timed_out`; handoff data is redacted from public results; stderr is never promoted into a downstream prompt; reserved hook exit codes `77`/`78` express `permission_required`/`auth_required` boundaries; a self-declared `provider` is stamped `inferred`, never `observed` | | External lifecycle execution wired into setup/sync/uninstall | **Working** (2026-08-16, wave C) | The loops iterate `hostsWithLifecycle()` (built-ins + admitted) through a shape-agnostic renderer; an admitted host's lifecycle runs only when explicitly enabled in `kit.json` **and** the flag is set. Admitted lifecycle hooks are cwd-anchored to the adapter's own directory (per-verb `lifecycle-unanchored` refusal for a relative hook on a remote source), the same F-1 protection as execution. *Known limitation:* the `sync` path is wired but not yet reachable through a real `ak sync` — `status.mjs`'s subsystem derivation is still opencode-scoped; setup and uninstall are fully live. Generalizing `status.mjs` is a tracked follow-up | -| Tiered conformance harness (`session-driving` … `statusline`) | **Working** (2026-08-16, wave C) | `runTieredConformance` + `ak host adapters conformance`: `admission` and `activity-routing` genuinely pass black-box against a real fixture (real subprocess worker); `session-driving`/`primary-eligible`/`statusline` report `gated`/`skipped` honestly because their runtime paths (external session driving, lead/escalation, statusline render) are not built — the harness never fabricates a pass. Passed/gated tiers are recorded into the grant store against the manifest hash; a failed `admission` tier short-circuits every downstream tier so no evidence is laundered | -| Capability-grant store + promotion command | **Working** (2026-08-16, wave D) | `grants.mjs` (hash-pinned, evidence-gated, edit-invalidated like consent — the earned capability is enforced at **read** time, not only at grant time) plus `ak host adapters grant`/`bless`: the maintainer's explicit grant of a tier-earned capability. It refuses any capability whose gating tier is not recorded `passed` at the current manifest hash — so `canBePrimary`/`commandStatusline` correctly refuse today (their tiers can't yet pass, §2), which is the safety invariant working. A granted capability is recorded but currently **inert** — no runtime consumer reads grants yet (the wave that wires granted capabilities into behaviour is still ahead) | +| Tiered conformance harness (`session-driving` … `statusline`) | **Working** (2026-08-16, waves C+D2) | `runTieredConformance` + `ak host adapters conformance`: `admission`, `activity-routing`, and now `primary-eligible` genuinely pass black-box against a real fixture — `primary-eligible` drives a real `executeRunPlan` where the host anchors a run and receives a genuine ADR-0019 escalation onto itself (a real second subprocess), recorded with no pre-existing grant. `session-driving`/`statusline` stay honestly `gated`/`skipped` (external session driving and the statusline render path are not built) — the harness never fabricates a pass, and there is no injection seam through which a caller could substitute one. A failed `admission` tier short-circuits every downstream tier so no evidence is laundered. *Known gap (N-1, tracked for §6):* a grant-bearing tier that later re-runs `failed` under an unchanged manifest hash does not yet auto-void the granted capability — operator-visible in `status`, remediable with `revoke-grant`; the auto-downgrade lands with the hook-bytes-pinning work | +| Capability-grant store + promotion command | **Working** (2026-08-16, waves D+D2) | `grants.mjs` (hash-pinned, evidence-gated, edit-invalidated like consent — the earned capability is enforced at **read** time, not only at grant time) plus `ak host adapters grant`/`bless`: the maintainer's explicit grant of a tier-earned capability, refused unless the gating tier is recorded `passed` at the current manifest hash. **Wave D2 makes a grant live:** at bootstrap the admitted-host overlay reads `grantedCapabilitiesFor` at the fresh current hash and raises `canBePrimary`/`commandStatusline` on the effective-registry entry (through a local allow-list that can raise only those two, never `aqeProvider` or any other key), so `hostTierLabel` and `effectivePrimaryHostIds()` reflect it. Two consumption gaps remain, honestly disclosed at grant time: no path yet *selects* an external host as primary (`ak host pick` stays built-in-scoped), and `commandStatusline` has no runtime reader yet (its render path is a later wave) | | Remote manifest sources (npm / URL) + resolve→hash ordering | **Working** (2026-08-16, wave A) | file / https (no redirects, bounded time+bytes) / `npm:` (`npm pack --ignore-scripts` + `tar -xzOf` stdout-only — nothing extracted to disk, package scripts never run); resolver runs before hashing, so a mutated remote surfaces as `consent-stale`. The https fetch is host-unrestricted by design (the source is operator-authored in user-scope `kit.json`; redirects refused, no credentials attached) | | Upstream request tracking (`gated: #NNN` against a tier) | **Working** (2026-08-16, wave D) | `ak host adapters gate ` records a ref-format-validated upstream gate; `ak host adapters status` surfaces per-tier passed/gated state (stale-marked on a manifest edit) and the granted capabilities | | A real external adapter (Hermes) clearing the kit → contract freeze | **Not started** | Freeze criterion (§6) |