Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
5.4 KiB
5.4 KiB
ADR-1577: Untrusted-input boundary + opt-in injection blocking
- Status: Accepted — ratified 2026-07-17 (originally Proposed 2026-06-25); see "Ratification" below
- Issue: #1577
- Part of: #1573 (harden the agent layer against documented LLM failure modes)
Ratification (2026-07-17): Proposed → Accepted
Ratified by explicit maintainer directive; the Proposed status had gone unconfirmed for 22 days since the ADR landed on 2026-06-25.
Evidence the decision shipped:
- Issue #1577 is closed (
state=CLOSED,stateReason=COMPLETED, closed 2026-06-24T21:07:24Z) as split A of the umbrella #1573, scoped exactly to this ADR's decision. hooks/msd-read-injection-scanner.js:118extends the scanner toSCANNED_TOOLS = new Set(['Read', 'WebFetch', 'WebSearch']), wired viahooks/hooks.json:34's"Read|WebFetch|WebSearch"matcher — closing the WebFetch/WebSearch gap named in Context.hooks/msd-read-injection-scanner.js:212gates blocking oncfg.security?.injection_blocking === true, read directly viafs.readFileSync/JSON.parse(independent ofsrc/configuration.cts's key whitelist, so no drop risk).security.injection_blockingis a registered config key end-to-end:msd-core/bin/shared/config-schema.manifest.json:109lists it andmsd-core/bin/shared/config-defaults.manifest.json:103defaults itfalse;src/configuration.cts:47buildsVALID_CONFIG_KEYSfrom that manifest andsrc/config-schema.cts:61(isValidConfigKey) consults it.msd-core/references/untrusted-input-boundary.mdexists and is@-included by exactly the 10 ingest agents named in the Decision:msd-advisor-researcher,msd-ai-researcher,msd-assumptions-analyzer,msd-doc-classifier,msd-doc-synthesizer,msd-domain-researcher,msd-phase-researcher,msd-project-researcher,msd-research-synthesizer,msd-ui-researcher.docs/explanation/security-model.md:150-165documents the PostToolUse pre-filter framing and names all 10 agents;docs/CONFIGURATION.md:910documentssecurity.injection_blockingwith a direct link to ADR-1577.tests/read-injection-scanner.security.test.cjsrunsSCAN-WF-01,SCAN-WF-02,SCAN-WF-03, andSCAN-WS-01against the real hook subprocess for WebFetch/WebSearch payloads and asserts real detections.tests/injection-blocking-config.test.cjsassertsisValidConfigKey('security.injection_blocking')is true,isValidConfigKey('security')is false, andCONFIG_DEFAULTS.security.injection_blocking === false.
Governance state: owning issue #1577 — CLOSED, stateReason COMPLETED, closed 2026-06-24T21:07:24Z.
Context
The research/doc-ingest agents concatenate text returned by WebFetch / WebSearch / Read into their context with no data/instruction separation, and the msd-read-injection-scanner hook only scanned the Read tool — leaving WebFetch/WebSearch (the largest untrusted channel) unscanned. Prompt injection via fetched content is a documented LLM failure mode (arXiv 2506.05739, 2507.15219, 2504.20472).
Two mechanisms were considered for the hook-level control:
- Redaction — strip the detected content before it reaches the model. This requires
hookSpecificOutput.updatedToolOutput, which is unused anywhere in this repo and not verifiable in CI for a PostToolUse hook. Claiming redaction the code can't reliably perform would re-introduce exactly the overclaim this work set out to remove. - Circuit-breaker — a PostToolUse hook that, after the fetch has executed and the content is already in the transcript, emits
decision: "block"to halt the agent's next step. It does not redact content already in context.
Decision
- Extend the scanner to match
Read | WebFetch | WebSearch, documented honestly as a pattern-based pre-filter, not a model-level guard. - Make the prompt-level boundary the primary control: a shared
msd-core/references/untrusted-input-boundary.md,@-included by the 10 ingest agents, instructs treat-fetched-text-as-data, self-scan before use, task-anchoring, and a fresh random delimiter per quoted wrap. This is the layer that keeps an injection from being followed even while it sits in context. - Ship hook-level blocking as an opt-in circuit-breaker:
security.injection_blocking(a registered config key; default advisory). Documentation states plainly that enabling it halts further processing on a HIGH detection — it does not retroactively redact the already-fetched content. Redaction viaupdatedToolOutputis deferred until that field's behavior is verifiable in this runtime.
Consequences
- Non-breaking. The default posture is advisory; no existing default changes. Blocking is reached only by an explicit opt-in.
- The strongest guarantee is prompt-level (data/instruction separation), which is unenforced at runtime — this is defense-in-depth (arXiv 2503.00061), not a hard sandbox. A determined adaptive attacker or a weaker model may still be influenced.
- Localized docs are managed separately; only the canonical English
docs/explanation/security-model.mdis updated here. - Follow-up: if/when
updatedToolOutputredaction is confirmed supported, the circuit-breaker can be upgraded to an actual redactor without changing the opt-in surface.