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:
@@ -550,6 +550,27 @@ Setting the parent object (`agent_skills_security`) directly is not supported; u
|
||||
|
||||
---
|
||||
|
||||
## Capability Trust (`capabilities.*`)
|
||||
|
||||
Policy for installing and updating third-party capabilities (ADR-1244). These keys govern the trust gate; they have no effect if you only ever use the native first-party capabilities shipped with GSD. They are **policy inputs** read by the `gsd capability` command flow, which passes the resulting decision into the capability lifecycle — `strict_known_registries` gates whether a source may be installed at all; `auto_update` is consulted by the `update`/`outdated` flow (which always re-prompts when a new version's executable surface set changes). The full rationale — including why there is no sandbox — is in [The capability trust model](explanation/the-capability-trust-model.md).
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `capabilities.strict_known_registries` | array \| null | `null` | Allowlist gating **which sources** third-party capabilities may be installed from. `null` (default) is permissive: external installs (git / npm / tarball) are allowed and each still passes the consent + integrity gate. `[]` (explicit empty array) is lockdown: **all external installs are blocked** — only local-filesystem installs are permitted (managed/enterprise mode). A non-empty list is a **host-based allowlist**: only sources whose host matches an entry (exact host or a subdomain of it — `github.com` matches `api.github.com` but never `evilgithub.com`) are permitted; add the literal token `npm` to permit the npm source kind. Local installs are never "external" and are always allowed. |
|
||||
| `capabilities.auto_update` | boolean | `false` | Whether installed third-party capabilities may auto-update. **Off by default.** Even when enabled, GSD re-prompts for explicit consent whenever a new version's executable surface set (hooks / command modules / MCP servers) differs from the installed one — the consent you gave was for a specific surface, not a blank cheque. |
|
||||
|
||||
```bash
|
||||
# Lock the machine down to local-only capability installs:
|
||||
gsd config-set capabilities.strict_known_registries '[]'
|
||||
|
||||
# Allow only your org's GitHub + npm:
|
||||
gsd config-set capabilities.strict_known_registries '["github.com", "npm"]'
|
||||
```
|
||||
|
||||
> **Security note:** `strict_known_registries` matching is **host-based, not substring** — a lookalike host like `evilgithub.com` is rejected even when `github.com` is allowed. `integrity` (sha512) pins only the top-level fetched artifact, not an npm package's transitive dependency tree; see the trust-model explanation for that boundary.
|
||||
|
||||
---
|
||||
|
||||
## Feature Flags
|
||||
|
||||
Toggle optional capabilities via the `features.*` config namespace. Feature flags default to `false` (disabled) — enabling a flag opts into new behavior without affecting existing workflows.
|
||||
|
||||
@@ -280,10 +280,12 @@
|
||||
"audit.cjs",
|
||||
"capability-activation.cjs",
|
||||
"capability-ledger.cjs",
|
||||
"capability-lifecycle.cjs",
|
||||
"capability-loader.cjs",
|
||||
"capability-registry.cjs",
|
||||
"capability-source.cjs",
|
||||
"capability-state.cjs",
|
||||
"capability-trust.cjs",
|
||||
"capability-validator.cjs",
|
||||
"capability-writer.cjs",
|
||||
"check-command-router.cjs",
|
||||
|
||||
@@ -391,10 +391,12 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
|
||||
| `capability-activation.cjs` | Capability activation resolver shared by config validation and capability-state consumers — resolves registry-owned config keys from raw runtime config without re-centralizing migrated settings |
|
||||
| `capability-ledger.cjs` | Per-runtime install ledger (ADR-1244 D4) — atomic read/write of `.gsd-capabilities.json` recording `{ id, version, source, integrity, files[], sharedEdits[] }` per installed capability; exports `readLedger`/`writeLedger`/`recordInstall`/`removeEntry`/`reconcile` (orphan detection); atomic commit point and reconciliation basis for Phase-4 upgrade/remove |
|
||||
| `capability-lifecycle.cjs` | Capability lifecycle orchestration (ADR-1244 Phase 4, D5+D6) — composes the source resolver + ledger + trust gate into `installCapability`/`upgradeCapability`/`removeCapability`/`reconcileCapabilities`; ledger write is the commit point; upgrade is atomic stage-then-swap (old set aside, new swapped in, ledger committed, backup dropped) with deterministic crash recovery (`reconcileCapabilities` rolls forward/back to a fully-old-or-fully-new state); remove surgically strips only marker-stamped (`_gsdCapability`) shared-config entries, preserving user hand-edits; never executes capability code |
|
||||
| `capability-loader.cjs` | Runtime Capability Registry overlay (ADR-1244 D2) — `loadRegistry({ includeInstalled })` composes the frozen first-party registry with a validated installed overlay read from `$GSD_HOME/.gsd/capabilities/<id>/` (global) and `<projectRoot>/.gsd/capabilities/<id>/` (project); first-party-wins on id/skill/agent/config collisions, reserved-namespace rejection, load-time `engines.gsd` re-gate (skip-with-warning), and gate-kind fail-closed via `_overlay.blockedGates`; composes through the canonical `buildRegistry` so derived views never drift |
|
||||
| `capability-registry.cjs` | Generated central Capability Registry — role-partitioned index of all co-located capability declarations (`capabilities/<id>/capability.json`); emitted by `scripts/gen-capability-registry.cjs --write` (ADR-894 §5) |
|
||||
| `capability-source.cjs` | Capability source resolver (ADR-1244 D3) — `resolveCapabilitySource(spec, opts)` fetches and stages a capability from local path, git (https/ssh/git transports only), npm pack (no lifecycle scripts), tarball (sha512 integrity verify before extraction), or registry (stub); tar-slip/symlink rejection; atomic staging to `$GSD_HOME/.gsd/capabilities/<id>/`; no capability code executes during install |
|
||||
| `capability-state.cjs` | Unified capability-state resolver (ADR-857 phase 4b/6) — composes install profile, runtime surface, and config activation into one per-capability view consumed by workflow hook rendering; exports pure `resolveCapabilityState`, reusable `resolveCapabilityRuntimeState`, and I/O handler `cmdCapabilityState`; command surface: `gsd-tools capability state [--config-dir <path>]` emitting `{ runtimeConfigDir, capabilities[] }` |
|
||||
| `capability-trust.cjs` | Capability trust gate (ADR-1244 Phase 4, D5 + compatibility half of D6) — PURE policy module: `discloseExecutableSurfaces` (hooks/command modules/mcpServers), `evaluateInstallTrust` (compose source policy + reserved-namespace + engines gate + disclosure → allowed/requiresConsent/blockReasons), `evaluateSourceAllowed` (`strict_known_registries`: permissive/lockdown/host-allowlist), `checkEngines` (engines.gsd hard gate + `compatVersions` graceful-downgrade), `executableSetChanged` (auto-update re-consent trigger); no sandbox — see `docs/explanation/the-capability-trust-model.md` |
|
||||
| `capability-validator.cjs` | Shared runtime-callable capability validator (ADR-1244 D2) — extracted from `scripts/gen-capability-registry.cjs` so the build-time generator and the runtime overlay loader share ONE validation implementation (generative-parity guarded); exports `validateCapability`/`validateCrossCapability`/`validateVersionEnvelope`/`validateConsumesGlobal`/… plus the closed-vocabulary sets and `SEMVER_RE` |
|
||||
| `capability-writer.cjs` | Capability State Writer (ADR-1213) — write-side inverse of the resolver; projects desired per-capability enabled/gates onto surface + config substrates, then re-resolves (assert-and-report); exports `setCapabilityState` and I/O handler `cmdCapabilitySet`; command surface: `gsd-tools capability set <id> [--on\|--off] [--gate <key>=<true\|false>]` |
|
||||
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
|
||||
|
||||
@@ -65,6 +65,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [The phase loop](explanation/the-phase-loop.md) — design rationale for the Discuss → Plan → Execute → Verify → Ship cycle
|
||||
- [Multi-agent orchestration](explanation/multi-agent-orchestration.md) — how subagents are spawned, scoped, and coordinated
|
||||
- [Security model](explanation/security-model.md) — trust boundaries, permissions, and safe automation
|
||||
- [The capability trust model](explanation/the-capability-trust-model.md) — why third-party capabilities are gated by consent + integrity + reversibility, not a sandbox
|
||||
- [Architecture](ARCHITECTURE.md) — system architecture, agent model, and data flow
|
||||
- [Discuss modes](workflow-discuss-mode.md) — assumptions mode vs interview mode for `/gsd-discuss-phase`
|
||||
- [Context monitoring](context-monitor.md) — context window monitoring hook architecture
|
||||
|
||||
89
docs/explanation/the-capability-trust-model.md
Normal file
89
docs/explanation/the-capability-trust-model.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# The capability trust model
|
||||
|
||||
> **Status:** Implemented in ADR-1244 Phase 4 (`gsd-core/bin/lib/capability-trust.cjs`,
|
||||
> `capability-lifecycle.cjs`). This document is the *honest* explanation of why GSD trusts
|
||||
> third-party capabilities the way it does — including what it does **not** protect against.
|
||||
|
||||
GSD capabilities can ship the same executable artifacts GSD itself ships — hook scripts,
|
||||
command modules, and (for third parties) MCP server declarations. That is full first-party
|
||||
parity, chosen deliberately (epic #1244, alternative 2). Parity means a third-party
|
||||
capability is, in the limit, **arbitrary code you have invited into your agent runtime**.
|
||||
This page explains the barrier GSD puts in front of that invitation, and why the barrier is
|
||||
*consent + integrity + reversibility* rather than a sandbox.
|
||||
|
||||
## Why there is no sandbox (Ask #1)
|
||||
|
||||
ADR-857 D8 originally earned "no sandbox" on a *declarative-only* premise: capabilities were
|
||||
data, not code. This feature reverses that premise, so the justification must be re-derived on
|
||||
grounds that survive third-party **code execution**. It is:
|
||||
|
||||
**GSD has no process boundary to interpose.** A capability's executable surfaces all run inside
|
||||
the host agent runtime's own trust context:
|
||||
|
||||
- **hook scripts** run as the runtime's configured hook commands (your shell, your
|
||||
permissions);
|
||||
- **command modules** are `require()`'d directly into the GSD CLI's own Node process;
|
||||
- **MCP servers** are spawned by the host runtime, not by GSD.
|
||||
|
||||
GSD never forks a sandboxed child to run any of this. It *could* not meaningfully do so: the
|
||||
runtime loads hooks and MCP servers itself, and a command module is in-process by definition.
|
||||
A `sandboxTier` enum on capabilities that GSD does not actually enforce would be security
|
||||
theater — worse than nothing, because it implies a boundary that isn't there.
|
||||
|
||||
The runtime descriptor's existing `sandboxTier` axis (`none` | `codex-agent-sandbox`,
|
||||
`runtime-config-adapter-registry.cts`) is the **runtime's own** sandbox over everything it
|
||||
executes — capability code included. That sandbox keeps applying. But it belongs to the
|
||||
runtime (e.g. Codex's `workspace-write`), not to GSD, and GSD does not extend or simulate it.
|
||||
|
||||
So the barrier GSD owns is the same posture the Obsidian and VS Code marketplaces settled on
|
||||
after their own supply-chain incidents:
|
||||
|
||||
1. **No execution at install.** Staging is copy-only. `npm` sources use `--ignore-scripts`;
|
||||
there is no `postinstall`. Installing a capability never runs its code.
|
||||
2. **Integrity before extraction.** Tarball sources verify a `sha512` digest over the fetched
|
||||
bytes *before* anything is written to the install root; a mismatch aborts.
|
||||
3. **Disclosure + explicit consent.** Every executable surface a capability declares is
|
||||
disclosed at install, and installing one that ships *any* executable surface requires
|
||||
explicit consent. Declining aborts cleanly, writing nothing.
|
||||
4. **Namespace reservation.** Third parties cannot claim the `gsd-`, `gsd-core-`, or
|
||||
`anthropic-` id prefixes, so a hostile capability cannot impersonate a first-party one.
|
||||
5. **Reversibility.** The ledger records exactly what each install wrote; `remove` deletes
|
||||
exactly those files and surgically strips exactly the shared-config entries the capability
|
||||
added, touching nothing the user owns.
|
||||
|
||||
Consent is meaningful precisely because (1)–(2) guarantee nothing has run *yet* when you are
|
||||
asked, and (4)–(5) guarantee you can fully undo it. That is the trade GSD makes instead of a
|
||||
sandbox it cannot honestly provide.
|
||||
|
||||
## `strictKnownRegistries` default (Ask #4a)
|
||||
|
||||
`capabilities.strict_known_registries` gates **install itself**, not merely auto-update.
|
||||
|
||||
| Value | Meaning |
|
||||
| --- | --- |
|
||||
| unset / `null` *(default)* | External installs (git / npm / tarball / registry) are allowed, each still passing the consent + integrity gate. Local filesystem installs always allowed. |
|
||||
| `[]` *(explicit empty array)* | **Blocks all external installs** — managed/enterprise lockdown. Local installs only. |
|
||||
| `["github.com", "registry.example.com"]` | Allowlist — only sources whose host matches an entry (exact host or a subdomain of it) are permitted. |
|
||||
|
||||
The default is **permissive-with-consent**, not Obsidian-style restricted-by-default. The epic
|
||||
deliberately chose decentralized URL/git distribution over a curated-registry gate (alternative
|
||||
3), so the consent prompt is the default barrier and full lockdown is one config key away. The
|
||||
allowlist match is **host-based**, not substring: `github.com` does not match
|
||||
`evilgithub.com`.
|
||||
|
||||
## The npm transitive-dependency boundary (Ask #4b)
|
||||
|
||||
`integrity` (sha512) pins **only the top-level fetched artifact** — the tarball, or the output
|
||||
of `npm pack`. It does **not** pin the resolved dependency graph.
|
||||
|
||||
`--ignore-scripts` and copy-only staging stop *install-time* execution (no `postinstall`). But
|
||||
when a capability's command module is later `require()`'d, Node resolves and executes that
|
||||
module's **transitive dependency tree**, and sha512 says nothing about those packages — they
|
||||
can be mutable or compromised independently of the pinned top artifact. This is exactly the
|
||||
Wiz / VS Code supply-chain lesson.
|
||||
|
||||
For the `npm` source kind, therefore: a green integrity check means "the package tarball is the
|
||||
one you pinned," **not** "every line of code that will run is the code you reviewed." Authors
|
||||
who want a stronger guarantee should vendor their dependencies into the capability or ship a
|
||||
lockfile; the consent prompt always discloses when a capability ships command modules, which is
|
||||
the surface through which transitive code reaches your process.
|
||||
@@ -37,7 +37,7 @@ gsd capability remove <id>
|
||||
GSD uses the **ledger** — a per-runtime record written at install time (for example, `~/.claude/.gsd-capabilities.json`) — as the authoritative list of what the install owns. Removal acts precisely on that record:
|
||||
|
||||
- **Owned files** — every file the capability wrote at install (skills, agents, referenced assets) is deleted.
|
||||
- **Shared configuration fragments** — entries the capability injected into shared files such as `settings.json` (hooks) and `hooks.json` (MCP server registrations) are stripped. Only the capability's own entries are removed; no other capability's hooks or MCP server entries are touched.
|
||||
- **Shared configuration fragments** — entries the capability injected into shared files such as `settings.json` (hooks, MCP server registrations) are stripped. Each capability-added entry is stamped at install with a `_gsdCapability` marker naming the owning capability, and removal strips **only** entries carrying that marker. No other capability's entries — and nothing you added by hand — is touched: if you hand-edited `settings.json` between install and remove (added your own hook, your own MCP server, or any other field), those edits are preserved exactly.
|
||||
- **Federated config keys** — configuration keys that belong to the capability's declared config slice are dropped from the merged config.
|
||||
|
||||
### What is NOT removed
|
||||
@@ -108,4 +108,4 @@ gsd capability remove <id> --scope project
|
||||
- [How to version and upgrade a capability](version-a-capability.md)
|
||||
- [Develop a Capability for GSD 1.5+](develop-a-capability.md)
|
||||
- [Turn a capability off (and keep it off)](turn-a-capability-off.md)
|
||||
- [Trust model explanation](../adr/1244-capability-ecosystem.md#d5----trust-model-artifact-parity-is-full-trust-posture-is-tiered)
|
||||
- [The capability trust model](../explanation/the-capability-trust-model.md) — why removal is surgical and reversible
|
||||
|
||||
@@ -44,19 +44,19 @@ When you do raise the lower bound:
|
||||
|
||||
### Maintain `compatVersions`
|
||||
|
||||
`compatVersions` is a capability-version → minimum-GSD-version table that lets GSD offer older consumers a downgrade instead of a hard block:
|
||||
`compatVersions` is a capability-version → GSD-version-**range** table that lets GSD offer older consumers a downgrade instead of a hard block. Each value is a semver range (the same grammar as `engines.gsd`), evaluated against the running GSD version:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"engines": { "gsd": ">=1.7.0 <3.0.0" },
|
||||
"compatVersions": {
|
||||
"1.2.0": "1.6.0"
|
||||
"1.2.0": ">=1.6.0 <1.7.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This entry tells GSD: "version 1.2.0 of this capability requires at least GSD 1.6.0." When a consumer's GSD is older than 1.7.0, GSD uses `compatVersions` to offer them version 1.2.0 instead of failing outright.
|
||||
This entry tells GSD: "version 1.2.0 of this capability is compatible with GSD versions `>=1.6.0 <1.7.0`." When a consumer's GSD is older than the current `engines.gsd` floor (1.7.0), GSD consults `compatVersions`, picks the **newest** capability version whose range the host satisfies, and offers that instead of failing outright.
|
||||
|
||||
Add a new entry **only when you change `engines.gsd`** — that is the only moment an older GSD version and a specific capability version become correlated. A `compatVersions` entry is not meaningful for a capability distributed as a bare tarball URL (a tarball exposes a single version and cannot be auto-selected from a table); it is only actionable for sources that enumerate versions: git tags, a registry, or npm.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user