feat(#1433): capability trust gate + upgrade/compat (ADR-1244 Phase 4) (#1449)

ADR-1244 Phase 4 (D5 trust + D6 upgrade/compat). capability-trust.cjs (disclosure/consent, strict_known_registries, engines+compatVersions, reserved namespace) + capability-lifecycle.cjs (install/upgrade/remove/reconcile; ledger-as-commit-point _pending intent; atomic stage-then-swap; surgical marker-isolated shared-edit strip; owner-token lock) + capability-source promote/skipEnginesGate seams + loader pending-skip + config keys. No sandbox re-derived (consent+integrity+reversibility). 6 Codex adversarial rounds + /security-review (no HIGH) + /code-review; gsd-test green both platforms; CI green. Phase 5 (#1434) wires the CLI dispatch.

Closes #1433.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-18 21:40:37 -04:00
committed by GitHub
parent 1abb0d3427
commit 9219af3360
22 changed files with 2843 additions and 14 deletions

View File

@@ -159,6 +159,27 @@ function overlayRoots(cwd: string, gsdHome?: string): Array<{ dir: string; scope
return roots;
}
/**
* Read the per-scope ledger co-located with an overlay root (the root is `<scope>/.gsd/capabilities`,
* so its ledger is `<scope>/.gsd-capabilities.json`) and return the ids carrying an in-flight
* `_pending` intent — i.e. crashed/uncommitted installs or upgrades that must not be activated until
* reconciliation completes. Never throws: a missing/invalid ledger yields an empty set.
*/
function pendingOverlayIds(rootDir: string): Set<string> {
const out = new Set<string>();
try {
const ledgerPath = path.join(rootDir, '..', '..', '.gsd-capabilities.json');
const parsed: unknown = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
if (!parsed || typeof parsed !== 'object') return out;
const entries = (parsed as Record<string, unknown>)['entries'];
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) return out;
for (const [id, entry] of Object.entries(entries as Record<string, unknown>)) {
if (entry && typeof entry === 'object' && (entry as Record<string, unknown>)['_pending']) out.add(id);
}
} catch { /* missing/invalid ledger — nothing pending */ }
return out;
}
/** Shallow-attach overlay diagnostics WITHOUT mutating the frozen registry module. */
function withOverlayMeta(reg: Registry, meta: OverlayMeta): Registry {
return Object.assign({}, reg, { _overlay: meta });
@@ -240,12 +261,22 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry {
} catch {
continue; // no overlay dir at this scope — normal
}
// Ids whose ledger entry carries an in-flight `_pending` intent (a crashed/uncommitted
// install or upgrade). They are NOT yet committed, so they must not be activated — reconcile
// will roll them forward or back. Fail OPEN (skip without a gate block): an uncommitted gate
// is not a real installed gate. See capability-lifecycle.cts (ADR-1244 Phase 4).
const pendingIds = pendingOverlayIds(root.dir);
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const id = ent.name;
const capDir = path.join(root.dir, id);
const manifestPath = path.join(capDir, 'capability.json');
if (pendingIds.has(id)) {
warnings.push({ id, scope: root.scope, reason: 'install/upgrade in progress (uncommitted) — deferred until reconciliation' });
continue;
}
let cap: CapManifest;
try {
cap = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as CapManifest;