From d96eb28cc2ab1f542414e50b3cd398ec325cdc34 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 21 Sep 2026 05:25:16 +0000 Subject: [PATCH 1/2] chore(specs): gardener checkbox sync --- specs/105-agent-scope-hardening/tasks.md | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/specs/105-agent-scope-hardening/tasks.md b/specs/105-agent-scope-hardening/tasks.md index d4fa88745..85a83e914 100644 --- a/specs/105-agent-scope-hardening/tasks.md +++ b/specs/105-agent-scope-hardening/tasks.md @@ -11,9 +11,9 @@ ## Phase 1: Setup (shared across PRs) -- [ ] T001 Add `agentCtx(allowed []string, perms []string, pin string) context.Context` and `adminCtx()` helpers with the `["*"]`-means-unrestricted rule documented, in `internal/server/scope_fixture_test.go` (new; consumed by every PR's tests; empty `AllowedServers` = deny-all) -- [ ] T002 [P] Add `startCountingUpstream(t, tools ...toolSpec) (*countingUpstream)` generalising `startCountingTargetTierUpstream` (`internal/server/mcp_call_tool_target_tier_test.go:335-411`) so any test can assert zero upstream calls, in `internal/server/scope_fixture_test.go` -- [ ] T003 [P] Amend spec text per plan §"Spec text amendments" (FR-008 empty-raw-name analogue; FR-003/004 D1 pinned zero-reach decision; SC-007 restated as the gate on FR-005/008/010 + inverted-assertion list; FR-009 unresolved identity refused for every caller per Edge Case `spec.md:107`; FR-002 anonymous callers; SC-005 two new named administrator exceptions — FR-005 profile-scoped admin on shared-index fallback (D3) and FR-001 caller-kind-first (D5)) in `specs/105-agent-scope-hardening/spec.md` +- [x] T001 Add `agentCtx(allowed []string, perms []string, pin string) context.Context` and `adminCtx()` helpers with the `["*"]`-means-unrestricted rule documented, in `internal/server/scope_fixture_test.go` (new; consumed by every PR's tests; empty `AllowedServers` = deny-all) +- [x] T002 [P] Add `startCountingUpstream(t, tools ...toolSpec) (*countingUpstream)` generalising `startCountingTargetTierUpstream` (`internal/server/mcp_call_tool_target_tier_test.go:335-411`) so any test can assert zero upstream calls, in `internal/server/scope_fixture_test.go` +- [x] T003 [P] Amend spec text per plan §"Spec text amendments" (FR-008 empty-raw-name analogue; FR-003/004 D1 pinned zero-reach decision; SC-007 restated as the gate on FR-005/008/010 + inverted-assertion list; FR-009 unresolved identity refused for every caller per Edge Case `spec.md:107`; FR-002 anonymous callers; SC-005 two new named administrator exceptions — FR-005 profile-scoped admin on shared-index fallback (D3) and FR-001 caller-kind-first (D5)) in `specs/105-agent-scope-hardening/spec.md` --- @@ -24,26 +24,26 @@ ### Failing tests (write first, confirm RED against the merge base) -- [ ] T004 [US2] FR009-G1: `setupQuarantineRuntime` manual trust, baseline `erase` approved, discover `[erase, ns:erase]` → expect `BlockedTools["ns:erase"]` and a pending record keyed `ns:erase`; full-tier `a`-only `call_tool_read a:ns:erase` → `TOOL_QUARANTINED`, zero upstream — `internal/runtime/tool_quarantine_identity_test.go` (new) + `internal/server/mcp_call_tool_target_tier_test.go` -- [ ] T005 [P] [US2] FR009-G2: `applyDifferentialToolUpdate` with `[erase, ns:erase]` → `GetToolsByServer` length 2; `SaveToolApproval(ns:erase, Disabled)` survives a rerun (HEAD deletes it) — `internal/runtime/lifecycle_identity_test.go` (new) + `internal/index/bleve_rawname_test.go` (new: distinct docIDs) -- [ ] T006 [P] [US2] FR009-G3: seed `{a, erase, pending}`, delete `ns:erase` record; full-tier ctx: `makeDirectModeHandler` entry `ns:erase` → IsError + zero upstream; `preflightApprovalReader.ToolApproval` → non-nil pending — `internal/server/mcp_direct_callability_test.go` + `internal/server/preflight_glue_test.go` -- [ ] T007 [P] [US2] FR009-G4: counting upstream `[erase]`; full-tier `a`-only AND admin `call_tool_read a:ghost` → `Permission denied`, zero upstream (D4: every caller; SC-005 named exception); sandbox `call_tool('a','ghost')` → `PERMISSION_DENIED` envelope; unknown-server branch unchanged (control) — `internal/server/mcp_call_tool_target_tier_test.go` + `internal/server/mcp_code_execution_scope_test.go` (new) -- [ ] T008 [P] [US2] FR009-G5: manual-trust server, quarantine on, StateView has `ns:erase`, storage has no record → refused; only `{a, erase, approved}` seeded → `a:ns:erase` refused; `quarantine_enabled=false` → unchanged — `internal/server/tool_gate_test.go` -- [ ] T009 [P] [US2] FR009-G6: generated tables at the spec shape (`spec.md:132`): retrieve **54 cells** = 3 permission sets × 3 target tiers × 3 `call_tool_*` variants × strict-intent on/off, pre-classified allowed / insufficient-permission / intent-mismatch; plus direct (permission set × target tier, driven through the registered handler in A — G's T104a re-drives the same table through `HandleMessage`, D15) and nested (via real sandbox, envelope asserted) tables; paired-name rows (`erase` approved, `ns:erase` config-denied via `disabled_tools` / unapproved); `{read,destructive}` rows; `auth.AdminContext()` control rows; counting oracle on every cell — `internal/server/scope_target_tier_matrix_test.go` (new) -- [ ] T010 [P] [US2] FR009-G7: regression cell `{read,destructive}` × write target refused on all three paths (passes today; pins the exact-match rule) — `internal/server/scope_target_tier_matrix_test.go` +- [x] T004 [US2] FR009-G1: `setupQuarantineRuntime` manual trust, baseline `erase` approved, discover `[erase, ns:erase]` → expect `BlockedTools["ns:erase"]` and a pending record keyed `ns:erase`; full-tier `a`-only `call_tool_read a:ns:erase` → `TOOL_QUARANTINED`, zero upstream — `internal/runtime/tool_quarantine_identity_test.go` (new) + `internal/server/mcp_call_tool_target_tier_test.go` +- [x] T005 [P] [US2] FR009-G2: `applyDifferentialToolUpdate` with `[erase, ns:erase]` → `GetToolsByServer` length 2; `SaveToolApproval(ns:erase, Disabled)` survives a rerun (HEAD deletes it) — `internal/runtime/lifecycle_identity_test.go` (new) + `internal/index/bleve_rawname_test.go` (new: distinct docIDs) +- [x] T006 [P] [US2] FR009-G3: seed `{a, erase, pending}`, delete `ns:erase` record; full-tier ctx: `makeDirectModeHandler` entry `ns:erase` → IsError + zero upstream; `preflightApprovalReader.ToolApproval` → non-nil pending — `internal/server/mcp_direct_callability_test.go` + `internal/server/preflight_glue_test.go` +- [x] T007 [P] [US2] FR009-G4: counting upstream `[erase]`; full-tier `a`-only AND admin `call_tool_read a:ghost` → `Permission denied`, zero upstream (D4: every caller; SC-005 named exception); sandbox `call_tool('a','ghost')` → `PERMISSION_DENIED` envelope; unknown-server branch unchanged (control) — `internal/server/mcp_call_tool_target_tier_test.go` + `internal/server/mcp_code_execution_scope_test.go` (new) +- [x] T008 [P] [US2] FR009-G5: manual-trust server, quarantine on, StateView has `ns:erase`, storage has no record → refused; only `{a, erase, approved}` seeded → `a:ns:erase` refused; `quarantine_enabled=false` → unchanged — `internal/server/tool_gate_test.go` +- [x] T009 [P] [US2] FR009-G6: generated tables at the spec shape (`spec.md:132`): retrieve **54 cells** = 3 permission sets × 3 target tiers × 3 `call_tool_*` variants × strict-intent on/off, pre-classified allowed / insufficient-permission / intent-mismatch; plus direct (permission set × target tier, driven through the registered handler in A — G's T104a re-drives the same table through `HandleMessage`, D15) and nested (via real sandbox, envelope asserted) tables; paired-name rows (`erase` approved, `ns:erase` config-denied via `disabled_tools` / unapproved); `{read,destructive}` rows; `auth.AdminContext()` control rows; counting oracle on every cell — `internal/server/scope_target_tier_matrix_test.go` (new) +- [x] T010 [P] [US2] FR009-G7: regression cell `{read,destructive}` × write target refused on all three paths (passes today; pins the exact-match rule) — `internal/server/scope_target_tier_matrix_test.go` ### Implementation -- [ ] T011 [US2] Add `RawName` to `config.ToolMetadata` with `CanonicalToolName`/`RawToolName` helpers in `internal/config/tool_identity.go` (new) and populate it from upstream tool names in `internal/upstream/core/client.go:394-397` -- [ ] T012 [US2] Producers use `RawName`: `checkToolApprovals` keys records by raw name (`internal/runtime/tool_quarantine.go:443-616,1216-1229`), `applyDifferentialToolUpdate`/`newToolsMap` stop collapsing and stop deleting exact-name records (`internal/runtime/lifecycle.go:673-790,975-980`); legacy collapsed record approves only its own raw name (one-shot, documented in code comment) -- [ ] T013 [US2] Bleve docID = `server:` + raw name (`internal/index/bleve.go:158-183,356-372,433`), `RawName` derived on read from the docID (not the stored `tool_name` / `full_tool_name`, which stay byte-identical scored fields — SC-005). **No index rebuild trigger** (adversarial review, 2026-09-14): the raw-keyed differential update self-heals a pre-upgrade collapsed doc on the first discovery (`TestApplyDifferentialToolUpdate_HealsPreUpgradeCollapsedDoc`), so a global wipe gated on the storage schema counter was pure cost and fired on every start -- [ ] T014 [US2] Reader: `lookupToolApproval` exact-wins + no-record-under-active-gate ⇒ pending (`internal/server/tool_gate.go:96-227`); direct callability and preflight read through it (`internal/server/mcp_direct_callability.go:136,192-201,253`, `internal/server/preflight_glue.go:400-411`); `ClassifyTool` consults the gate before returning Ready on nil approval (`internal/preflight/classify.go:98-100`) -- [ ] T015 [US2] Unresolved identity on a known server refuses **every** caller on retrieve (`internal/server/mcp.go:2283-2323`) and nested (`internal/server/mcp_code_execution.go:1251-1268` → explicit unresolved sentinel in the `ToolAnnotationFunc` contract, `internal/jsruntime/runtime.go:401-409` refuses on it); the unknown-server branch (`mcp_code_execution.go:987-997`) is untouched (D4); seed StateView in fixtures that register an upstream without one (`mcp_call_tool_trim_test.go:57`) -- [ ] T016 [US2] Docs: "Target tool tier" paragraph (exact-match permissions, annotation-less → read, unresolved → refused for scoped callers) replacing the "destructive implies both" claim in `docs/features/agent-tokens.md:140-150` +- [x] T011 [US2] Add `RawName` to `config.ToolMetadata` with `CanonicalToolName`/`RawToolName` helpers in `internal/config/tool_identity.go` (new) and populate it from upstream tool names in `internal/upstream/core/client.go:394-397` +- [x] T012 [US2] Producers use `RawName`: `checkToolApprovals` keys records by raw name (`internal/runtime/tool_quarantine.go:443-616,1216-1229`), `applyDifferentialToolUpdate`/`newToolsMap` stop collapsing and stop deleting exact-name records (`internal/runtime/lifecycle.go:673-790,975-980`); legacy collapsed record approves only its own raw name (one-shot, documented in code comment) +- [x] T013 [US2] Bleve docID = `server:` + raw name (`internal/index/bleve.go:158-183,356-372,433`), `RawName` derived on read from the docID (not the stored `tool_name` / `full_tool_name`, which stay byte-identical scored fields — SC-005). **No index rebuild trigger** (adversarial review, 2026-09-14): the raw-keyed differential update self-heals a pre-upgrade collapsed doc on the first discovery (`TestApplyDifferentialToolUpdate_HealsPreUpgradeCollapsedDoc`), so a global wipe gated on the storage schema counter was pure cost and fired on every start +- [x] T014 [US2] Reader: `lookupToolApproval` exact-wins + no-record-under-active-gate ⇒ pending (`internal/server/tool_gate.go:96-227`); direct callability and preflight read through it (`internal/server/mcp_direct_callability.go:136,192-201,253`, `internal/server/preflight_glue.go:400-411`); `ClassifyTool` consults the gate before returning Ready on nil approval (`internal/preflight/classify.go:98-100`) +- [x] T015 [US2] Unresolved identity on a known server refuses **every** caller on retrieve (`internal/server/mcp.go:2283-2323`) and nested (`internal/server/mcp_code_execution.go:1251-1268` → explicit unresolved sentinel in the `ToolAnnotationFunc` contract, `internal/jsruntime/runtime.go:401-409` refuses on it); the unknown-server branch (`mcp_code_execution.go:987-997`) is untouched (D4); seed StateView in fixtures that register an upstream without one (`mcp_call_tool_trim_test.go:57`) +- [x] T016 [US2] Docs: "Target tool tier" paragraph (exact-match permissions, annotation-less → read, unresolved → refused for scoped callers) replacing the "destructive implies both" claim in `docs/features/agent-tokens.md:140-150` ### Inverted pinned tests (gap-map §7) -- [ ] T017 [US2] Invert `internal/server/mcp_call_tool_target_tier_test.go:210-223,244-253,474` (ghost tool now refused for scoped callers); rebuild `internal/server/mcp_routing_test.go:306-447` direct cells on the real fixture with the counting oracle; resolve `extractToolName` callers in the three test files that still use it +- [x] T017 [US2] Invert `internal/server/mcp_call_tool_target_tier_test.go:210-223,244-253,474` (ghost tool now refused for scoped callers); rebuild `internal/server/mcp_routing_test.go:306-447` direct cells on the real fixture with the counting oracle; resolve `extractToolName` callers in the three test files that still use it ### Verification From 77c5a5eef4ae02708650415be85d7371c185a9b1 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 21 Sep 2026 05:25:22 +0000 Subject: [PATCH 2/2] chore(roadmap): regenerate ROADMAP.md after gardener checkbox sync --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d828f8d7a..182daf335 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -888,7 +888,7 @@ graph LR | Web UI + macOS app UX audit | In progress | P0 | — | | | | Release qualification gate (auto-QA matrix blocks the tag) | In progress | P0 | — | [081-release-qa-gate](./specs/081-release-qa-gate/) | | | Action log / transparency — info at a glance | In progress | P1 | — | | | -| Agent-token scope hardening: every MCP request authorized by its own scope (spec 105) | In progress | P1 | 82/112 (73%) | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | | +| Agent-token scope hardening: every MCP request authorized by its own scope (spec 105) | In progress | P1 | 99/112 (88%) | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | | | Token-efficiency benchmark: measured savings, published results | In progress | P1 | 62/64 (97%) | [103-token-bench](./specs/103-token-bench/) | | | Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | P1 | — | | | | Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | | @@ -1034,6 +1034,6 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [102-schema-deferred](./specs/102-schema-deferred/) | `shipped` | 89/89 (100%) | | [103-token-bench](./specs/103-token-bench/) | `shipped` | 62/64 (97%) | | [104-auto-routing-mode](./specs/104-auto-routing-mode/) | — | — | -| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 82/112 (73%) | +| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 99/112 (88%) | | [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) | | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `shipped` | 126/126 (100%) |