Skip to content

feat(scope): exact raw-name tool identity on every dispatch path (Spec 105 PR A, FR-009) - #1279

Merged
Dumbris merged 26 commits into
mainfrom
105-agent-scope-hardening
Sep 16, 2026
Merged

Dumbris merged 26 commits into
mainfrom
105-agent-scope-hardening

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Spec 105 PR A — roadmap task scope-target-identity-producers (FR-009). Every approval record, search-index entry and policy gate is now keyed by the exact raw tool name its server reports (ns:erase is ns:erase, never erase), on all three dispatch paths (call_tool_*, /mcp/all direct, nested code_execution), preflight and describe_tool. An unresolved or stale identity on a connected, discovered server is refused before any upstream call — for every caller, administrators included (spec Edge Case, research D4). A discovered tool with no approval record under an active quarantine gate is pending, never ready.

Also lands the Spec 105 planning artifacts (plan / research / data-model / contracts / tasks / gap-map, 6 astra plan-review rounds → CLEAN) and the six spec-text amendments from that round.

Gaps closed (gap-map §1)

FR009-G1 ns:erase inherited erase's approval · G2 index docID collapse + exact-name record deleted on rediscovery · G3 direct/preflight readers used the exact key only · G4 unresolved identity granted the destructive tier · G5 no-record classified ready · G6 acceptance tables (retrieve 54 cells + direct + nested) · G7 docs (exact-match permissions).

What changed

  • Producers (internal/runtime/tool_quarantine.go, lifecycle.go): checkToolApprovals and applyDifferentialToolUpdate key by RawName; an old collapsed docID whose raw name is a suffix of a new namespaced key is a migration alias, not a removal; IdentityKeyed stamp on every post-105 write; legacy collapsed records lend a user block / pending / changed lock to the namespaced tool once per identity until the operator acts on them; never-baselined (toggle-synthesized) exact records adopt the legacy lock or baseline only against a matching contract; the changed-record revert predicate compares the whole contract (closes a latent main bug: a schema-only rug-pull auto-approved on the second pass).
  • Readers (internal/server/tool_gate.go, mcp_direct_callability.go, preflight_glue.go, internal/preflight): exact record wins; no record for a discovered tool under an active gate ⇒ pending; the direct catalog is the direct discovery snapshot; preflight check mode recommends rediscovery for record-less tools.
  • Identity gate (internal/server/mcp.go, mcp_code_execution.go, jsruntime): toolIdentity{ServerKnown, SnapshotHydrated, DiscoveryDone, Found}; refusal fires only for a connected, discovered server (live client, not a stale StateView flag); ToolsDiscovered is per connection generation (stamped by discovery incl. zero-tool results, cleared on disconnect and by reconcile, stale generations dropped at publish).
  • Index (internal/index/bleve.go): docID = server:rawName; full_tool_name stays the raw name so admin BM25 scores are byte-identical to main (pinned by TestBleveIndex_RawShapedExactNameScoresUnchanged); identity derived from hit.ID on read; no global index rebuild — the differential self-heals pre-upgrade docs.
  • Docs: docs/features/agent-tokens.md (Target tool tier), docs/features/security-quarantine.md (Namespaced tool names), .github/RELEASE_NOTICE.md (one-time pending after upgrade).

Tests inverted (never deleted)

TestLookupToolPermission_UnresolvedMetadataIsDestructive → …UndiscoveredOnKnownServerIsUnresolved; TestCallToolRead_UndiscoveredTool_RequiresDestructiveTier → …RefusedForEveryCaller; TestToolGate_StaleExactApprovalCannotShadowCollapsedChange → …RugPullOnExactRecord_StaleLegacyApprovalCannotShadowIt; merge-reader cells in TestToolGate_LegacyCollapsedApprovalRecord_StillGates, …MergedApprovalRecordsKeepIndependentLocks, TestLookupToolApproval_ReadsBothKeysFromOneSnapshot; TestSetToolEnabled_CreatesRecordWhenMissing (active gate ⇒ pending). Admin controls kept green.

SC-005 admin changes (all named exceptions)

Config-denied / unapproved a:ns:erase refused for admins; erase and ns:erase distinct index entries; unresolved identity on a connected discovered server refused; container-housekeeping records unchanged in this PR. Four retrieve_full_* goldens and toolslist_goldens byte-identical.

Verification

  • go test -race on runtime/index/preflight/jsruntime/config/upstream/storage/security, internal/server (CI skip regex), -tags server serveredition — green.
  • ./scripts/test-api-e2e.sh — 65/65.
  • go vet clean; goldens unchanged. golangci-lint v2 could not run locally (binary built with go1.25 refuses the go 1.26 target) — relying on CI.
  • Cross-model review, 10 rounds (the CLAUDE.md cap), every finding verified by probe before fixing:
    • Round 1 — opencode github-copilot/gpt-6-astra, four file-named chunks: index CLEAN; producers 4, identity gate 4, readers 1 (all confirmed; one was a latent main bug: a schema-only rug-pull auto-approved on the second pass).
    • Rounds 2–10 — codex gpt-5.6-sol (Copilot monthly quota exhausted after round 1; switched on maintainer direction). Producers CLEAN at r3, readers CLEAN at r4, identity gate CLEAN at r10. Rounds 3/5/6/7/9 were one defect class — a cache-derived identity refusal pre-empting an authoritative server-level verdict during StateView lag — closed structurally: one persisted read per dispatch (gate → identity → live certification → epoch-pinned upstream call), a live client overrules the snapshot's connection flag, and process-wide connection epochs.
    • Preceded by six internal adversarial rounds (security / SC-005 / migration / test-quality / upgrade-matrix lenses) that reached no findings before astra ran.
  • CI: the one red run was TestSupervisor_ToolsDiscoveredMarker reading the mock before an async connect on slow runners (fixed test-side), plus a pre-existing scanner SetDeepScan data race the Integration job caught (fixed with the engine lock); a hdiutil: Resource busy DMG flake was re-run.

Follow-ups / Spec 105 gaps

  • POST /api/v1/tool-calls/{id}/replay dispatches with no tool gate (pre-existing, REST is outside Spec 105's stated scope) — recorded in gap-map §8.
  • The REST/UI "active profile" (PUT /api/v1/profiles/active) bounds no MCP session today — noted for the scope-management spec.
  • Remaining Spec 105 PRs per plan: B ∥ D ∥ E ∥ H0 → C → F → G → H1.

🤖 Generated with Claude Code

Dumbris and others added 16 commits September 14, 2026 15:20
…p map

Phase 0 evidence (gap-map.md: 54 gaps adversarially verified against
b39800a; prior-diff-assessment.md) and Phase 1 design: nine PRs in a
fixed merge order, one effective-scope predicate, identity stamped at
production, single refusal constructor per surface, two-fixture
differential oracle. research.md records the eleven open decisions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fied gap

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h/contracts/tasks

14 findings, all verified against spec.md and applied: D3 admins keep
today's retrieve path (SC-005), D4 unresolved identity refuses every
caller (Edge Case spec.md:107), D5 caller-kind-first per FR-001, D6
score neutrality asserted not assumed with exhaustive as the only
fallback, D7 terminal strip filter + protocol-level proof, D8 reuse the
existing server field (no admin record change), D10 four operations +
merge-base CI gate, D12 whole-envelope refusal parity, D13 only tier
leaves the call-time filter, D14 standalone tests before H1; FR-003 URL
precedence, FR-005 approval-aware usage ranking, FR-006 unstamped
prompts withheld for everyone, FR-009 tables at spec shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ifacts

Round 2 (20 items: 6 stale propagations from round 1, 6 new). Applied:
D3 follows spec.md:113 and names the profiled-admin fallback in SC-005;
D5 named in SC-005 and data-model order fixed; D4 stale fail-open text
removed from plan/contract; D13 tier-first over callability inside one
call-time filter; D8 boundary-aware fields parsing (left-to-right first
complete JSON object) + producer audit; D9 image-name cleanup fallback
covered; D10 merge-base harness supplied from head, missing baseline
fails; D15 stages the FR-009 direct table across A and G; T065 env var
is a directory; T084 compares the same requested name across fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ifacts

Retained-effect assertion mode + fixtures (spec.md:114,161) and the
operator-published-content positive controls (spec.md:116) assigned;
usage ranking requires population membership AND approval (stale
approved record fixture); golden gate narrowed to the frozen files so
H1's latency fixtures can land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ifacts

Historical subject-conflict log records (foreign container names,
ID-only container records, mis-routed callback records) withheld from
scoped readers per FR-007; usage-ranking membership rule and the
golden-only gate propagated into plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ifacts

Container log records need canonical subject evidence (container_owner
from the label) — sanitised names collide (a/b ≡ a-b), so every
pre-upgrade container record is withheld from scoped readers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d fixtures

T001-T003: scope_fixture_test.go (agentCtx/adminCtx/startCountingUpstream),
spec.md amendments (6 items from the plan round). T004-T010: 21 tests,
16 red on HEAD by assertion (ns:erase inherits erase's approval, index
docID collapse, exact-name record deleted on rediscovery, unresolved
identity granted destructive tier, no-record classified ready), 5
regression pins (54-cell retrieve table, direct/nested tables, exact-
match permissions, quarantine-disabled control).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (FR-009)

Producers key approval records and index docIDs by the exact raw name;
lookupToolApproval: exact record wins, legacy collapsed record only
restricts, discovered-but-unrecorded tool under an active quarantine
gate is pending; unresolved identity on a known server refused for
every caller (D4); pinned pre-105 tests inverted. WIP: critique fixes
follow in the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…napshot, keep full_tool_name raw, drop the index wipe, carry legacy blocks

F1 direct evaluator Discovered=true; F2 unresolved refusal only for a
hydrated snapshot; F3 full_tool_name stays the raw name (admin BM25
scores byte-identical to main, pinned); F4 remove the global index
rebuild + schema bump (differential self-heals, migration test); F5
legacy collapsed restriction carried onto the new exact record; F6
drop the raw-name alias clause; F7 distinct no-record body; F8 direct
reader test; F9 docs + RELEASE_NOTICE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dentityKeyed stamp, discovery marker, exact describe gates

R1 adopt a legacy lock onto a toggle-synthesized exact record; R2 carry
only a real user block from unstamped siblings (IdentityKeyed on every
post-105 write); R3 ToolsDiscovered marker so connected-but-undiscovered
servers refuse unresolved names; R4 describe/count gates use the exact
pair; R5 distinct no-record body on describe, distinct block reason,
nested check without AuthContext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tityKeyed backfill, disconnected pending, discovery window body

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce, restricting orphans wait, toggle-minted records pending, supervisor marker cleared, bulk toggle raw names

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…output-schema backfill; restricting legacy records stay consultable across operator writes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s the legacy-consult seam; race-free supervisor marker test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ration-alias diff keys, per-identity legacy consult, atomic stamp sweep, connection-generation discovery marker

P1 schema-only change can no longer auto-revert (also a latent main
bug); P2 an old collapsed docID whose raw name is a suffix of a new
namespaced key is an alias, not a removal; P3 restricting collapsed
records lend their lock per namespaced identity until the operator
acts on them; P4 stamp sweep reads and writes inside one bbolt Update;
R1 preflight check mode recommends rediscovery for no-record tools;
I1 empty rediscovery never certifies a retained tool set; I2 discovery
results carry the connection generation and are dropped when stale;
I3 the identity gate keys on the live client, not StateView.Connected;
I4 reconcile invalidates ToolsDiscovered whenever it observes a
disconnect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: f60c688
Status: ✅  Deploy successful!
Preview URL: https://827b1c47.mcpproxy-docs.pages.dev
Branch Preview URL: https://105-agent-scope-hardening.mcpproxy-docs.pages.dev

View logs

…concile in the discovery-marker test

Reconcile reads Connected from the adapter, whose ConnectServer the
earlier passes dispatched asynchronously; on slow CI runners the
goroutine had not run yet and reconcile correctly treated the server as
disconnected, clearing the marker the test expected to survive. All
four failing CI jobs were this one test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Sep 15, 2026 •

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 105-agent-scope-hardening

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (21 MB)
  • smart-mcp-proxymcpproxy-goM9ZUEQ.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 34945538835 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

Dumbris and others added 9 commits September 15, 2026 07:08
…witch

ApplySecurityConfig on the routing-mode-refresh goroutine wrote
engine.deepScanEnabled/deepScanScanners while StartScan read them under
e.mu in resolveScanners — a pre-existing data race the Integration
Tests job caught on this branch (TestE2E_DirectModeLegacyAliasesFollow-
SerializationMode). Writer and out-of-section reader now go through
e.mu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… identity-checked describe/preflight, live-client connection epoch

C1 stampConsultedLegacySibling uses the conditional in-tx stamp; C2
describe_tool and preflight/check mode reject a known-server unresolved
identity like dispatch (search listing untouched); C3 the managed client
carries a connection epoch that discovery captures and both identity
checks compare against, so a stale generation never passes when the
disconnect/reconnect events are delayed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…overed identity; epoch-pinned upstream dispatch

D1 a disconnected server keeps its not-connected / reconnect_on_use
verdict (the S3 clause protected nothing: no gated path reconnects);
D2 the validated connection epoch travels into the managed-client call
and is rechecked under the client lock right before the transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…certified identity; release notice matches the disconnected verdict

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e Connected snapshot for the identity gate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…vel verdicts derive from one persisted read

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…spatch gate's persisted record

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…de so a replacement client never reuses one

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… from the sandbox preflight through dispatch

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Dumbris
Dumbris marked this pull request as ready for review September 15, 2026 08:13
@Dumbris
Dumbris merged commit bac8f6b into main Sep 16, 2026
57 checks passed
@Dumbris
Dumbris deleted the 105-agent-scope-hardening branch September 16, 2026 04:29
Dumbris added a commit that referenced this pull request Sep 20, 2026
…main (#1324)

CHANGELOG.md's Unreleased section was missing entries for 9 recent
behavior-changing commits, most notably the Spec 105 scope-hardening
security fixes (#1283/#1284/#1285) and Spec 107 SSO features
(#1293/#1296).

roadmap.yaml had drifted stale in both directions for two in-flight
specs:
- Spec 105 (agent-scope-hardening): 4 merged PRs (A #1279, B #1282,
  D #1283, E #1284) still showed status: todo, and PR H0 (#1285) had
  no roadmap task entry at all. Flipped to done; genuinely outstanding
  work (PR C/F/G/H1) is untouched.
- Spec 107 (sso): PR-D (#1296) is merged but the epic and its task
  still read in_review. Verified all 87 files PR-D touched exist on
  disk (internal/audit/*, docs/features/audit-log.md, config, docs,
  frontend) and that verification.md records a completed multi-round
  cross-model review and a passing SC-009 benchmark, then ticked the
  20 checkbox-lag tasks (T096-T115, T118-T119) in tasks.md and flipped
  both the task and epic status to done.

Regenerated ROADMAP.md via scripts/gen-roadmap.py.
Dumbris added a commit that referenced this pull request Sep 21, 2026
…ng holds (Spec 105 PR H1) (#1332)

Spec 105 PR H1 — `scope-regression-suite` part 2 (FR-011/013/014, SC-007,
gaps FR01x-G4..G7). Closes tasks T107-T116. This is the FINAL phase of the
nine-PR agent-scope-hardening epic (A, B, D, E, H0, C, F, G already merged).

## Changes

- **Two-fixture differential oracle** (`internal/server/scope_differential_test.go`,
  new): `newScopeFixture`/`runScopeScenario`/`normalizeScopeResponse` per
  contracts/differential-oracle.md, plus `TestScopeCoverage_EveryUserStoryScenario`
  registering every US1.1-1.8/US2.1-2.6/US3.1-3.4 scenario by id — some
  directly via the general three-server harness, most by re-running each
  prior PR's own dedicated differential fixture (plan.md: "H1 re-registers
  them by User Story id").
- **Pinned-reversal grep guard** (`internal/server/scope_pinned_reversal_guard_test.go`,
  new): `TestScopePinnedReversalsStayInverted` proves none of the four
  pre-105 insecure assertions gap-map.md's FR01x-G7 entry names have crept
  back to their original form, traced to their exact pre/post-105 diffs.
- **Retained-effect fixtures** (`internal/server/scope_retained_effects_test.go`,
  new): real reproductions of 5 of 7 SC-001-excluded effects (shared-limiter
  contention, cross-server scan admission via the actual shadowing.cross_server
  impersonation-clone mechanism, prompt-name collision, global prompt cap,
  direct display-name collision), 1 via a companion package
  (`internal/upstream/manager_prompts_deadline_test.go`, new — shared vs
  independent prompt-refresh deadline, two discriminating regimes), 1 cited
  cross-package (log rotation).
- **HTTP credential matrix** (`internal/server/scope_http_matrix_test.go`,
  new): real minted tokens over real loopback HTTP through mcpAuthMiddleware,
  covering every FR-014 applicability-matrix row at least once plus a
  content-level (raw wire-text) non-disclosure proof for retrieve_tools.
- **Latency harness finalized** (`internal/server/scope_latency_test.go`):
  all four FR-011 operations (retrieve_tools, read_cache, prompts/list,
  tools/list) with real workloads, plus `.github/workflows/scope-latency.yml`
  (new) and `cmd/scope-latency-compare` (new, stdlib-only) for the
  merge-base p95 regression gate.
- **Bug fix found via this suite**: `internal/logs.ReadUpstreamServerLogTail`/
  `ReadUpstreamServerLogTailAttributed` dereferenced a nil `*config.LogConfig`
  — fixed with a TDD red/green pair (`internal/logs/nil_log_config_test.go`).
- Docs and roadmap sync: `docs/features/agent-tokens.md` invariant wording
  finalized; `roadmap.yaml`'s `scope-refusal-shapes` task (stale `todo`
  despite PR G #1328 already merged) flipped to `done`. `scope-regression-suite`
  and the epic-level status are deliberately left for the maintainer to flip
  on merge.

## Testing

- `go build`/`go vet` (bare + `-tags server`): clean
- Full `-race` suite across internal/server, internal/upstream,
  internal/cache, internal/logs, cmd/scope-latency-compare: green
- `golangci-lint --new-from-rev=origin/main` (bare + `--build-tags server`):
  0 new issues
- `./scripts/test-api-e2e.sh`: 70/70 passed
- Cross-model review: opencode terra/sol/astra confirmed quota-exhausted;
  fell back to `codex exec --model gpt-5.6-sol`. 4 rounds, every finding
  verified genuine before fixing (non-vacuous confirmed by temporarily
  reverting the underlying guard and observing red). `VERDICT: clean`
  (round 4) — full history in tasks.md T116.

Related #1223 #1224 #1225 #1226 #1227 #1279 #1282 #1283 #1284 #1285 #1325 #1326 #1328
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants