diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..59a2425 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/contracts/scp_antigen_mcp_v1.md text eol=lf diff --git a/docs/contracts/scp_antigen_mcp_v1.md b/docs/contracts/scp_antigen_mcp_v1.md new file mode 100644 index 0000000..01a8396 --- /dev/null +++ b/docs/contracts/scp_antigen_mcp_v1.md @@ -0,0 +1,99 @@ +# SCP Antigen MCP contract v1 + +**Version:** 1.3 +**Purpose:** Public specification for the SCP mesh extension MCP server (`antigen_mcp.py`) — antigen transport, shared registry fetch/contribute/apply. Separate from core [scp_mcp_v1.md](scp_mcp_v1.md) to preserve frozen v1.0 core contract. + +**Normative:** Tool names and human-gate semantics below. Network I/O tools live here only — not on core `scp_mcp`. + +--- + +## Transport + +- MCP over **stdio** (typical `mcp.json` entry `scp-antigen`). +- Server name identifier: `SCP-Antigen` (FastMCP) or equivalent. + +--- + +## Tool groups + +### Antigen bundle (P0) + +| Tool | Parameters | Human gate | Notes | +|------|------------|------------|-------| +| `scp_antigen_export` | `patterns_json`, `antigen_id`, optional metadata; **no MCP `seckey_hex`** | — | Unsigned build on MCP; sign via CLI | +| `scp_antigen_verify` | `bundle_json` (**JSON object** only), `allowlist?` (issuer pubkeys); `require_signature` ignored (always true) | — | No side effects; reject JSON string paths | +| `scp_antigen_import` | `bundle_json` (**JSON object** only), `allowlist?` (issuer pubkeys); `require_signature` ignored (always true) | Never auto-merges | Quarantine only; filesystem paths are CLI-only | +| `scp_antigen_merge` | `bundle_json` (**JSON object** only), `approve?` default **false**, `allowlist?` | **`approve=true` + `SCP_REGISTRY_MERGE_CONSENT=1`**; signature always required | Registry merge; no path-as-JSON | +| `scp_antigen_publish` | `bundle_json` (**JSON object** only), `relays?`, `dry_run?`, `approve?` | **`approve=true` + `SCP_ANTIGEN_PUBLISH_CONSENT=1`**; no MCP `seckey_hex`; `dry_run` never signs | Nostr kind 30078 | +| `scp_antigen_discover` | `allowlist?` (issuer pubkeys), `relays?`, filters | Empty pubkey allowlist fails closed; relays ⊆ `SCP_ANTIGEN_RELAY_ALLOWLIST` | Metadata only | +| `scp_antigen_fetch` | `url`, `expected_hash`, `allowlist?` (ignored for hosts), `l402_token?` | No auto-pay on 402; **hosts env-only** (`SCP_ANTIGEN_FETCH_HOST_ALLOWLIST`); **never auto-loads** `SCP_ANTIGEN_L402_TOKEN` | HTTPS fetch + verify; body hard-capped (`SCP_ANTIGEN_MAX_PAYLOAD_BYTES`) before JSON parse | + +`bundle_json` must be a JSON **object** (the antigen bundle). A JSON string value (including a filesystem path) is rejected. Loading bundles from disk is CLI-only (`pathlib.Path`). + +### Registry mycelium (R3/R4) + +| Tool | Parameters | Human gate | Notes | +|------|------------|------------|-------| +| `scp_fetch_registry` | `source`, `allowlist` (issuer pubkeys for nostr), `if_none_match?`, `relays?` | Stages to quarantine only; **`merged` always false**; HTTPS hosts from env; TLS via **`SCP_REGISTRY_TLS_VERIFY`** (no MCP `tls_verify`) | HTTPS or nostr; HTTPS body capped to quarantine content limit; writes under `registry_fetch/` | +| `scp_contribute_pattern` | `transport`, `patterns_json?`, `raw_content?`, `approve?` default **false**, … | **`approve=true`**; `SCP_CONTRIBUTE_CONSENT=1`; nostr/both also **`SCP_ANTIGEN_PUBLISH_CONSENT=1`** under MCP; **`SCP_CONTRIBUTE_HOST_ALLOWLIST`** gates POST; MCP rejects `seckey_hex`; TLS via **`SCP_REGISTRY_TLS_VERIFY`** | R3 contribute | +| `scp_apply_registry_quarantine` | `quarantine_path`, `approve?` default **false** | **`approve=true` + `SCP_REGISTRY_MERGE_CONSENT=1`**; `SCP_REGISTRY_MERGE_DEV_AUTO` disabled under MCP | Path must be under `{SCP_QUARANTINE_DIR}/registry_fetch/` from `scp_fetch_registry` (envelope + sidecar `reason=registry_fetch`); core `scp_quarantine` paths rejected | + +--- + +## End-to-end mycelium flow (informative) + +```text +scp_fetch_registry (antigen_mcp) → quarantine_path +scp_apply_registry_quarantine approve=true + SCP_REGISTRY_MERGE_CONSENT=1 → SSOT + projection +scp_registry_summary / scp_inspect (scp_mcp v1.0 + v1.1) → uses updated projection per load order +``` + +Core inspect load order documented in [scp_mcp_v1.1.md](scp_mcp_v1.1.md). + +--- + +## Environment variables (informative) + +| Variable | Purpose | +|----------|---------| +| `SCP_PATTERN_SSOT_PATH` | pattern_record SSOT store | +| `SCP_THREAT_REGISTRY_PATH` | Override registry/projection path | +| `SCP_REGISTRY_MERGE_CONSENT` | Hard attestation for approve=true merge/apply (`==1`) | +| `SCP_REGISTRY_MERGE_DEV_AUTO` | Dev-only auto-merge low-risk (CLI only; ignored under MCP) | +| `SCP_ANTIGEN_FETCH_HOST_ALLOWLIST` | Env-only HTTPS destinations for antigen fetch | +| `SCP_REGISTRY_FETCH_HOST_ALLOWLIST` | Optional HTTPS hosts for registry fetch (else fetch host allowlist) | +| `SCP_REGISTRY_TLS_VERIFY` | Registry HTTPS TLS verify (default on; `0`/`false`/`no` disables). MCP cannot pass `tls_verify` | +| `SCP_ANTIGEN_TLS_VERIFY` | Antigen fetch HTTPS TLS verify (default on; same disable tokens) | +| `SCP_ANTIGEN_RELAY_ALLOWLIST` | Fail-closed WSS relay allowlist for MCP | +| `SCP_ANTIGEN_RELAYS` | Default nostr relays (CLI; filtered by relay allowlist when set) | +| `SCP_ANTIGEN_PUBLISH_CONSENT` | Hard attestation for live antigen nostr publish (also required for MCP contribute nostr/both) | +| `SCP_ANTIGEN_L402_TOKEN` | Operator L402 token (CLI/library only; not auto-attached on MCP) | +| `NOSTR_SECKEY` | Publish credentials after consent (MCP never accepts seckey tool arg) | + +--- + +## Security invariants + +- Fetch stages only; merge is separate apply step with env consent +- Contribute two-phase consent: proposal (`approve=false`) then publish (`approve=true` + env) +- No network tools on core `scp_mcp` v1.0 required set +- HTTPS destination allowlists are **operator env only** (MCP cannot expand) +- Registry/antigen TLS verify is **operator env only** (`SCP_REGISTRY_TLS_VERIFY` / `SCP_ANTIGEN_TLS_VERIFY`); MCP tools must not expose `tls_verify` +- Relays fail closed under MCP without `SCP_ANTIGEN_RELAY_ALLOWLIST`; loopback/link-local/metadata blocked +- Encounter auto-log redacts secrets before durable write + +--- + +## Verification + +- **CONTRACT_HASH:** SHA-256 of this file (UTF-8, LF). Vendored in SCP `docs/contracts/`. +- Implementations SHOULD document tool list parity in release notes. + +--- + +## Changelog + +- **1.3** — AppSec: apply quarantine limited to egistry_fetch/\ paths from \scp_fetch_registry\ (core quarantine rejected); HTTPS body caps before parse/quarantine write. +- **1.2** — AppSec: remove MCP `tls_verify`; registry TLS via `SCP_REGISTRY_TLS_VERIFY` only (CLI `--no-tls-verify` retained). +- **1.1** — AppSec hardening: env-only hosts, publish/merge consent, MCP L402/seckey refusal, relay allowlist. +- **1.0** — Initial antigen + mycelium MCP contract. diff --git a/docs/plans/2026-08-12-001-fix-antigen-mcp-contract-align-plan.md b/docs/plans/2026-08-12-001-fix-antigen-mcp-contract-align-plan.md new file mode 100644 index 0000000..a794886 --- /dev/null +++ b/docs/plans/2026-08-12-001-fix-antigen-mcp-contract-align-plan.md @@ -0,0 +1,233 @@ +--- +title: Align antigen MCP contract with hardened SCP +date: 2026-08-12 +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-plan-bootstrap +execution: code +topic: scp-antigen-mcp-contract-align +target_repos: OpenHarness, SCP +--- + +# Align antigen MCP contract with hardened SCP + +## Goal Capsule + +**Objective:** Make OpenHarness `docs/contracts/scp_antigen_mcp_v1.md` normative again by promoting the hardened SCP vendor text (already at Version 1.3), closing the three audit gaps (MCP DEV_AUTO, MCP `tls_verify`, dual-gate consent), then re-sync SCP vendor + hash pin so both repos agree. + +**Authority:** SCP `antigen_mcp.py` + AppSec tests are behavioral source of truth; OpenHarness contract is public normative home per `SCP/docs/OPENHARNESS_CONTRACT.md`. + +**Stop when:** OH and SCP antigen contract files are byte-identical (UTF-8, LF), hash test pins match, and grep gates for the three findings pass on the OH file. + +--- + +## Product Contract + +### Summary + +Operators and downstream MCP implementers must not be told that agents can disable TLS verify, use DEV_AUTO under MCP, or merge/publish with `approve=true` alone. OpenHarness currently ships stale antigen v1.0; SCP already vendors corrected v1.3. This work restores OH→SCP sync discipline without softening SCP runtime. + +### Requirements + +- R1. Antigen contract documents that `SCP_REGISTRY_MERGE_DEV_AUTO` does **not** apply under MCP (CLI-only / ignored when MCP transport active). +- R2. Antigen contract lists **no** MCP `tls_verify` parameter; registry TLS is env-only via `SCP_REGISTRY_TLS_VERIFY` (and related antigen TLS env as already in vendor). +- R3. Antigen contract documents dual-gate merge/contribute/publish: tool `approve=true` **plus** required operator consent env vars (`SCP_REGISTRY_MERGE_CONSENT`, `SCP_CONTRIBUTE_CONSENT`, `SCP_ANTIGEN_PUBLISH_CONSENT` as applicable). +- R4. OpenHarness and SCP vendored `docs/contracts/scp_antigen_mcp_v1.md` are byte-identical after sync; SCP `EXPECTED_SCP_ANTIGEN_MCP_V1_SHA256` matches that content. +- R5. In-file Version / Changelog honestly reflect the promoted revision (Version **1.3** with a **1.3** changelog bullet — vendor currently headers 1.3 but changelog tops at 1.2). + +### Actors + +- A1. Operator configuring MCP / consent env +- A2. Implementer vendoring the OpenHarness contract hash +- A3. Reviewer verifying AppSec parity before merge + +### Key Flows + +- F1. Promote hardened contract text into OpenHarness, then copy OH → SCP per existing sync SOP. +- F2. Recompute SHA-256; update SCP hash pin; run contract hash tests. + +### Acceptance Examples + +- AE1. OH antigen contract has no `tls_verify` in the `scp_fetch_registry` parameter column; mentions `SCP_REGISTRY_TLS_VERIFY`. +- AE2. `scp_apply_registry_quarantine` human-gate text states DEV_AUTO disabled under MCP. +- AE3. Merge/apply and publish rows require consent env vars alongside `approve=true`. +- AE4. `pytest tests/test_contract_document_hash.py` passes in SCP after sync. + +### Scope Boundaries + +**In scope:** Antigen contract text in OpenHarness + SCP vendor copy + hash pin + missing 1.3 changelog bullet; minimal note in sync docs if direction-of-travel needs a one-line historical caveat. + +**Deferred to Follow-Up Work:** Refresh stale narrative in `SCP/docs/SCP_R5_MCP_INTEGRATION.md`; add OH `scp_antigen_mcp_v1.sha256` / releases-table row if product wants public antigen hash discipline like core; expand OH `verify_contract_hash.py` to antigen. + +**Outside this product's identity:** Changing SCP runtime to restore MCP `tls_verify` or MCP DEV_AUTO; core `scp_mcp_v1` / `v1.1` (already hash-matched). + +### Key Decisions + +- KD1. Align contract **to** hardened SCP behavior (session-settled) — Governs R1–R3. +- KD2. Deliver OpenHarness **and** SCP vendor together — Governs R4. +- KD3. Adopt Version **1.3** (not silent v1.0 edit) — Governs R5. + +--- + +## Planning Contract + +### Key Technical Decisions + +- KTD1. **Promote SCP vendor body into OpenHarness, then restore OH→SCP SOP** `(session-settled: user-approved — chosen over OpenHarness-only edit that leaves SCP hash/docs drift)`. SCP vendor already encodes R1–R3 plus related AppSec (no MCP seckey, forced signature, host/relay allowlists). Do not hand-patch OH from the three findings alone and invent divergent prose. +- KTD2. **Add missing Changelog 1.3 bullet before final hash pin** — header already says 1.3; changelog must name quarantine-path / body-cap (or whatever landed as 1.3) so Version and history agree. Prefer editing the canonical OH file then copying to SCP so only one authorship pass. +- KTD3. **Do not soften SCP tests or implementation** — verification is hash + existing AppSec tests remaining green; no production behavior change expected if OH simply catches up to vendor text. + +### Assumptions + +- Byte-identical sync uses UTF-8 with LF line endings as stated in `OPENHARNESS_CONTRACT.md`. +- Core `scp_mcp_v1` / `v1.1` need no edits in this plan. +- Institutional `docs/solutions/` has no prior learning on this drift; SCP AppSec tests are the proof corpus. + +### High-Level Technical Design + +```mermaid +flowchart LR + SCP_impl[SCP antigen_mcp + AppSec tests] + SCP_vendor[SCP docs/contracts antigen v1.3] + OH[OpenHarness docs/contracts antigen] + pin[SCP EXPECTED_SCP_ANTIGEN_MCP_V1_SHA256] + + SCP_impl -->|already matches| SCP_vendor + SCP_vendor -->|U1 promote + R5 changelog| OH + OH -->|U2 copy per SOP| SCP_vendor + SCP_vendor -->|U2 rehash| pin +``` + +### Sequencing + +U1 → U2. U3 optional/deferred narrative only if time remains; not required for Definition of Done. + +--- + +## Implementation Units + +### U1. Promote hardened antigen contract into OpenHarness + +**Goal:** Replace stale OpenHarness antigen v1.0 with hardened v1.3 text that satisfies R1–R3 and R5. + +**Requirements:** R1, R2, R3, R5 + +**Dependencies:** None + +**Files:** +- Modify: `docs/contracts/scp_antigen_mcp_v1.md` (OpenHarness) +- Reference (read-only source): sibling SCP `docs/contracts/scp_antigen_mcp_v1.md` +- Test expectation: none in OpenHarness repo — verification via content gates in Verification Contract (no existing antigen hash script in OH) + +**Approach:** +1. Start from SCP vendor file content as the baseline. +2. Ensure Version **1.3** and add a **1.3** changelog bullet describing the revision that the header already claims (quarantine_path under `registry_fetch/` + body caps / related AppSec — match reality in vendor tables). +3. Confirm the three audit strings: no MCP `tls_verify`; DEV_AUTO CLI-only / disabled under MCP; consent envs on merge/contribute/publish. +4. Preserve LF / UTF-8; do not reintroduce MCP `seckey_hex` or optional signature-off. + +**Patterns to follow:** Existing SCP vendor contract tables; in-place antigen versioning (`1.0 → 1.1 → 1.2 → 1.3`) rather than a new filename. + +**Test scenarios:** +- Happy path: After write, `scp_fetch_registry` parameter list has no `tls_verify`. +- Happy path: `scp_apply_registry_quarantine` states DEV_AUTO disabled under MCP. +- Happy path: Env table includes `SCP_REGISTRY_MERGE_CONSENT`, `SCP_CONTRIBUTE_CONSENT` (or contribute gate text), `SCP_ANTIGEN_PUBLISH_CONSENT`, `SCP_REGISTRY_TLS_VERIFY`. +- Edge: Changelog contains **1.3** bullet and Version field is 1.3. +- Error/regression: File must not regain `tls_verify?` on MCP tool rows. + +**Verification:** Content gates for AE1–AE3 pass on the OpenHarness path. + +--- + +### U2. Re-sync SCP vendor + hash pin + +**Goal:** Restore OpenHarness-as-upstream discipline: SCP vendor copy and `EXPECTED_SCP_ANTIGEN_MCP_V1_SHA256` match the OH file from U1. + +**Requirements:** R4, R5 + +**Dependencies:** U1 + +**Files:** +- Modify: `docs/contracts/scp_antigen_mcp_v1.md` (SCP) +- Modify: `tests/test_contract_document_hash.py` (SCP) — only if hash changes +- Optionally touch: `docs/OPENHARNESS_CONTRACT.md` (SCP) — one-line note that antigen briefly led from SCP AppSec then re-synced +- Tests: `tests/test_contract_document_hash.py`; optionally `tests/test_appsec_antigen_mcp_gates.py` (no expected failures) + +**Approach:** +1. Copy OH antigen contract over SCP vendor (UTF-8, LF). +2. Compute SHA-256; update `EXPECTED_SCP_ANTIGEN_MCP_V1_SHA256` if bytes differ from prior vendor. +3. Run contract hash pytest; spot-run AppSec MCP gate tests if any doubt about prose/impl drift (should already be green). + +**Execution note:** Prefer install/runtime smoke via existing pytest hash suite over inventing new OH tests. + +**Patterns to follow:** `SCP/docs/OPENHARNESS_CONTRACT.md` sync procedure steps 1–4. + +**Test scenarios:** +- Covers AE4. Hash test passes after pin update. +- Integration: Byte compare OH vs SCP antigen files succeeds. +- Edge: If U1 only added a changelog bullet, pin **must** change; do not leave stale EXPECTED. +- Regression: `test_mcp_registry_tools_have_no_tls_verify_param` and `test_mcp_dev_auto_disabled` still pass (behavior unchanged). + +**Verification:** SCP hash test green; OH and SCP files identical. + +--- + +### U3. Deferred narrative hygiene (optional) + +**Goal:** Avoid operators reading soft DEV_AUTO language in R5 integration prose. + +**Requirements:** none required for DoD + +**Dependencies:** U2 + +**Files:** +- Modify (optional): `docs/SCP_R5_MCP_INTEGRATION.md` (SCP) + +**Approach:** If touched, replace soft DEV_AUTO / “to be authored” antigen language with pointers to the v1.3 contract. Otherwise leave under Deferred. + +**Test expectation:** none -- documentation pointer only + +**Verification:** N/A if skipped. + +--- + +## Verification Contract + +| Gate | Where | Pass criteria | +|------|--------|----------------| +| Content gates AE1–AE3 | OpenHarness `docs/contracts/scp_antigen_mcp_v1.md` | (1) `scp_fetch_registry` parameter cell has no `tls_verify`; (2) apply-quarantine human-gate states DEV_AUTO disabled under MCP; (3) env/table text includes merge/publish consent vars and `SCP_REGISTRY_TLS_VERIFY` | +| Byte identity | OH vs SCP antigen files | Same SHA-256 (UTF-8, LF) via SOP one-liners in `OPENHARNESS_CONTRACT.md`, or binary compare of the two paths | +| Hash pin | SCP `tests/test_contract_document_hash.py` | Pass; `EXPECTED_SCP_ANTIGEN_MCP_V1_SHA256` equals that SHA-256 | +| AppSec spot | SCP `tests/test_appsec_antigen_mcp_gates.py` | Pass if run (no behavior change expected) | + +Do not treat OpenHarness CI as covering antigen hash (script is core-only today). + +--- + +## Definition of Done + +- R1–R5 satisfied; AE1–AE4 evidenced. +- U1 and U2 landed; U3 optional. +- No SCP runtime softening. +- Plan’s three original audit findings cannot be re-derived from OH contract prose. + +--- + +## Risks & Dependencies + +| Risk | Mitigation | +|------|------------| +| Dual authorship drifts again | Finish with OH→SCP copy + hash pin in same change set | +| Changelog 1.3 vague | Tie bullet to observable table rows (quarantine path + caps) | +| Reviewers assume OH hash tooling covers antigen | Call out core-only hash script in PR | + +**Dependency:** Local checkouts of OpenHarness and SCP; write access to both for a complete R4. + +--- + +## Sources & Research + +- Security review of `chore/ci-workflow-dispatch` (medium findings on OH v1.0 antigen contract). +- SCP vendor `docs/contracts/scp_antigen_mcp_v1.md` Version 1.3 vs OH Version 1.0. +- SCP `src/scp/antigen_mcp.py`, `operator_consent.py`, `registry_ssot.py`, `tests/test_appsec_antigen_mcp_gates.py`. +- `SCP/docs/OPENHARNESS_CONTRACT.md` sync SOP. +- No institutional hits under `docs/solutions/` for this topic. diff --git a/state/README.md b/state/README.md index eed5266..394eb8a 100644 --- a/state/README.md +++ b/state/README.md @@ -77,6 +77,7 @@ See [docs/HANDOFF_FLOW.md](../docs/HANDOFF_FLOW.md) and [docs/INTENT_ENGINEERING - **Section per date:** `## YYYY-MM-DD` - **Each entry:** `- **[Area]** Decision: . Rationale: .` Optional: `(plan: )` +- **Decision graph projection (optional):** Typed nodes/edges for MCP + OpenGrimoire live in a local SQLite projection (`decision-graph.sqlite`), not in this markdown file. Humans still append here only. Optional causal edges that prose cannot express: `decision-graph-edges.jsonl` beside the log (see OpenGrimoire `docs/DECISION_GRAPH_SCHEMA.md`). Rebuild: `python MiscRepos/.cursor/scripts/project_decision_graph.py` with `DECISION_LOG_PATH` / `DECISION_GRAPH_DB`. ## known-issues.md