* feat(#1435): capability matrix (generated + drift-guarded) + trust-model doc consolidation ADR-1244 Phase 6. Adds the capability matrix reference, generated FROM the committed registry so it can never drift from the actual capability set: - scripts/gen-capability-matrix.cjs (--write / --check); --check is a CI drift guard. - tests/capability-matrix-sync.test.cjs (4 tests): drift guard, buildMatrix==committed, every cap present, no placeholders. - docs/reference/capability-matrix.md regenerated from the registry (release-stable: shows engines.gsd, omits the lockstep per-cap version that would churn the file every release). - Consolidated the duplicate trust-model doc: deleted docs/explanation/the-capability-trust-model.md, merged its content into capability-trust-model.md, redirected ~10 references; no stale links remain. - Diataxis verification (now that gsd capability is a real command): corrected the matrix's third-party section — the matrix is the first-party catalogue; the overlay-aware view of installed third-party capabilities is 'gsd capability list', not this generated file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1435): Added changeset for the capability matrix reference Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1435): address code-review — non-vacuous matrix test + generator polish - capability-matrix-sync.test.cjs: assert the 'security registers a ship:pre gate' precondition unconditionally so the extension-point check can never degrade to a vacuous pass on registry drift. - gen-capability-matrix.cjs: warn (stderr) on an unknown loop point at generation time; rename enginesOf -> fmtEngines for consistency with the other fmt* helpers (output unchanged). - capability-trust-model.md: point the two how-to links at the real files (import-a-capability-from-a-url.md, version-a-capability.md) instead of the bare directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1435): backfill changeset PR number → #1458 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
> **Explanation** — This document describes *why* GSD draws its trust
|
||||
> boundaries where it does, and *what the trade-offs are*. It is not a
|
||||
> step-by-step guide to installing capabilities; for that, see the how-to
|
||||
> guides for [importing a capability](../how-to/) and
|
||||
> [version management](../how-to/). For the decision record, see
|
||||
> guides for [importing a capability](../how-to/import-a-capability-from-a-url.md) and
|
||||
> [version management](../how-to/version-a-capability.md). For the decision record, see
|
||||
> [ADR-1244 D5](../adr/1244-capability-ecosystem.md#d5--trust-model-artifact-parity-is-full-trust-posture-is-tiered).
|
||||
> For the capability field reference, see the
|
||||
> [capability matrix](../reference/capability-matrix.md).
|
||||
@@ -172,6 +172,15 @@ What it does not defend against: a malicious capability where the author
|
||||
themselves publishes a bad bundle. The SHA is honest about what you are
|
||||
installing; it says nothing about whether what you are installing is safe.
|
||||
|
||||
It also pins **only the top-level bundle**, not an `npm`-sourced capability's
|
||||
resolved dependency graph. `--ignore-scripts` and copy-only staging stop
|
||||
install-time execution, but when a command module is later `require()`'d, Node
|
||||
resolves and runs its transitive dependencies — which the bundle SHA does not
|
||||
cover (the Wiz / VS Code lesson). For the `npm` source kind, 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 or ship a lockfile.
|
||||
|
||||
### Auto-update off by default, re-consent on executable-set change
|
||||
|
||||
When auto-update is enabled for a third-party capability, each update is
|
||||
@@ -200,13 +209,54 @@ rejected at the conformance gate. This prevents impersonation: a malicious
|
||||
actor cannot publish a capability called `gsd-security` and exploit a user's
|
||||
implicit trust in the GSD namespace.
|
||||
|
||||
### `strictKnownRegistries` for managed environments
|
||||
### `capabilities.strict_known_registries` for managed environments
|
||||
|
||||
Teams or enterprises that want to constrain which capability sources are
|
||||
permissible can set `strictKnownRegistries` in managed or project config to an
|
||||
explicit allowlist of URLs or registry names. Setting it to `[]` blocks all
|
||||
external installs. This gives an administrator a policy lever that operates
|
||||
before the user even sees a consent prompt.
|
||||
permissible set `capabilities.strict_known_registries` in config. Its semantics:
|
||||
|
||||
- **unset / `null`** *(default)* — permissive: external installs (git / npm /
|
||||
tarball) are allowed, each still passing the consent + integrity gate. Local
|
||||
filesystem installs are always allowed.
|
||||
- **`[]`** *(explicit empty array)* — lockdown: **all external installs are
|
||||
blocked**; local-only.
|
||||
- **non-empty list** — 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`; the literal token `npm` permits
|
||||
the npm source kind). A malformed (non-array) value **fails closed**.
|
||||
|
||||
This gives an administrator a policy lever that operates before the user even
|
||||
sees a consent prompt. The default is permissive-with-consent (not Obsidian-style
|
||||
restricted-by-default), because the epic deliberately chose decentralised import
|
||||
with the consent prompt as the default barrier and lockdown one config key away.
|
||||
|
||||
### Command dispatch: where third-party code runs (1.6.0)
|
||||
|
||||
A capability may declare a **command family** (`commands: [{ family, module,
|
||||
router }]`); `gsd-tools <family>` dispatches it by `require()`-ing the router.
|
||||
This is the one place a third-party capability's own code executes, so it is
|
||||
gated twice. **Consent:** a third-party family is dispatchable only if its
|
||||
capability has a **committed ledger entry** — i.e. you installed it through the
|
||||
lifecycle and consented. A bundle merely present on disk with no install record
|
||||
still contributes its declarative surfaces but is **not** command-dispatchable.
|
||||
**Confinement:** the router module loads only from the capability's own install
|
||||
root (bare-`.cjs` basename, `realpath`-confined, rejecting `..` traversal and
|
||||
symlink escape); a first-party command can never be shadowed by a third-party one.
|
||||
|
||||
#### The project-scope trust boundary
|
||||
|
||||
Capabilities install **globally** (`$GSD_HOME/.gsd/capabilities/`) or
|
||||
**project-scoped** (`<projectRoot>/.gsd/capabilities/`). The consent record is
|
||||
the ledger — and a project-scope ledger lives **inside the repository**. A repo
|
||||
you check out can therefore ship a capability bundle *and* a ledger that marks
|
||||
it committed; running `gsd-tools <its-family>` in that repo executes its code.
|
||||
This is the same boundary that already applies to a repo's build scripts or a
|
||||
project-scoped capability's hooks, and it is **narrower** — a command never fires
|
||||
on its own; you have to type it. GSD cannot cryptographically distinguish a
|
||||
genuine project-local install from a forged project ledger, so: a **global**
|
||||
install's consent record lives outside any repo and is trustworthy; a
|
||||
**project-scoped** capability is only as trustworthy as the repository it ships
|
||||
in. Review repos before running `gsd` commands in them, and prefer global
|
||||
installs for capabilities you want to trust across projects.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Where third-party code runs: command dispatch (Phase 5 / D7)
|
||||
|
||||
A capability may declare a **command family** (`commands: [{ family, module, router }]`). When you
|
||||
run `gsd-tools <family> …`, GSD dispatches it by `require()`-ing the named module's router function.
|
||||
This is the one place a third-party capability's own code executes in the GSD CLI process, so it is
|
||||
gated twice:
|
||||
|
||||
1. **Consent.** A third-party family is dispatchable only if its capability has a **committed
|
||||
ledger entry** — i.e. you installed it through the lifecycle (and, for executable surfaces,
|
||||
consented). The loader records a dispatchable "command root" only for committed (non-`_pending`)
|
||||
capabilities; a bundle merely *present* on disk with no ledger entry still contributes its
|
||||
declarative surfaces (skills/agents/config) but is **not** command-dispatchable.
|
||||
2. **Confinement.** The router module is loaded **from the capability's own install root** — a bare
|
||||
`.cjs` basename, `realpath`-confined to that directory, rejecting `..` traversal and symlink
|
||||
escape. A capability can never reach code outside its own bundle, and a first-party command
|
||||
(`graphify`/`intel`/`audit`, which ship in `bin/lib/`) can never be shadowed by a third-party one.
|
||||
|
||||
### The project-scope trust boundary (be honest about it)
|
||||
|
||||
Capabilities can be installed **globally** (under your home, `$GSD_HOME/.gsd/capabilities/`) or
|
||||
**project-scoped** (under a repository, `<projectRoot>/.gsd/capabilities/`). The consent signal is
|
||||
the ledger, and the project-scope ledger (`<projectRoot>/.gsd-capabilities.json`) lives **inside the
|
||||
repository**. A repository you check out can therefore ship both a capability bundle *and* a
|
||||
ledger that marks it "committed." Running `gsd-tools <that-family>` inside such a repo will execute
|
||||
its code.
|
||||
|
||||
This is the same trust boundary that already applies to a repository's build scripts, npm
|
||||
`postinstall`, or a project-scoped capability's hooks (which GSD has loaded since Phase 2) — and it
|
||||
is **narrower** than those, because a command never fires on its own: you have to type it. GSD does
|
||||
not (and with plain files cannot) cryptographically distinguish a genuine project-local install from
|
||||
a forged project ledger. The honest guidance:
|
||||
|
||||
- A **global** install's consent record lives outside any repo and is trustworthy.
|
||||
- A **project-scoped** capability is only as trustworthy as the repository it ships in — treat
|
||||
running its commands like running that repo's other code. Review repos before running `gsd`
|
||||
commands in them, and prefer global installs for capabilities you want to trust across projects.
|
||||
Reference in New Issue
Block a user