test(mcp): harden RecallMemory_GlobalScope_Blocked — add OFFSEC-001 contract assertions #725
Reference in New Issue
Block a user
Delete Branch "fix/681-recallmemory-offsec-contract"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Hardens
TestMCPHandler_RecallMemory_GlobalScope_Blockedto verify the OFFSEC-001 scrub contract on the recall-memory path — mirrors PR#680s hardening from the commit-memory path (issue #681).Root cause / what was missing
Before: only asserted
resp.Error != nil. A future OFFSEC-001 regression that returned the rawerr.Error()verbatim would still pass the test silently.What the test now asserts
resp.Error.Code == -32000resp.Error.Message == "GLOBAL scope is not permitted via the MCP bridge — use LOCAL, TEAM, or empty"(the constant defined intoolRecallMemory, not the raw error)xK8mPqRwT,zN7vLsJhYw) planted in thequeryargument: truly arbitrary strings chosen to NOT overlap with the legitimate error message text. A future OFFSEC-001 regression whereerr.Error()is returned directly would expose these verbatim in the responsestrings.Containsloop catches the canary leak even if the exact-message assertion is accidentally deleted in a future refactormock.ExpectationsWereMet()— handler aborts before touching the DBFiles
workspace-server/internal/handlers/mcp_test.goTest plan
CGO_ENABLED=0 go test ./internal/handlers/...— all handlers tests pass (7.6s)Closes #681.
🤖 Generated with Claude Code
[core-qa-agent] QA APPROVED — MR !725 (test(mcp): harden RecallMemory_GlobalScope_Blocked — add OFFSEC-001 contract assertions)
Summary
Test-only PR (+40/-1 lines) on staging base. Adds OFFSEC-001 regression defense to the
RecallMemorytool path, mirroring the same contract hardening added in PR #680 for theCommitMemorypath.Quality
xK8mPqRwT zN7vLsJhYwas a defense-in-depth guard — if OFFSEC-001 regresses and rawerr.Error()leaks into the JSON-RPC message, the canary will appear verbatim in the response and the test will fail ✓Verdict
[core-qa-agent] APPROVED — e2e: N/A (Go backend test only)
SRE Review (infra-sre)
LGTM ✅ — solid OFFSEC-001 regression test for the recall-memory path.
Verified:
xK8mPqRwT,zN7vLsJhYw) planted in query argument — any raw error leak would expose them verbatimstrings.Containsloop in case the exact-message assertion is accidentally removedmock.ExpectationsWereMet()confirms no DB calls made (handler aborts before DB)This is the right pattern for OFFSEC regression detection. The canary approach is more robust than just checking for
err != nil.Tier: tier:low — test hardening only.
[core-security-agent] N/A — test-only. mcp_test.go +40/-1 lines: hardens RecallMemory_GlobalScope_Blocked to verify OFFSEC-001 scrub contract on recall-memory path. Follows PR #680/#693/#696 pattern. Targets staging. No production code changes.
[APPROVE-rec] OFFSEC-001 Contract Review — core-security
Verdict: APPROVE — no blocking issues found.
What I checked
1. Exact error code assertion (
resp.Error.Code != -32000) — present.mcp.go:433setsCode: -32000for all non-unknown-tool errors, so this assertion binds directly to the production constant.2. Exact error message assertion — present and correct.
wantMsgis set verbatim to"GLOBAL scope is not permitted via the MCP bridge — use LOCAL, TEAM, or empty", which is the exact string returned bytoolRecallMemoryinmcp_tools.go:463. Assertion uses!=(exact equality, not substring). Any regression where the constant changes orerr.Error()is swapped in would break this test immediately.3. Canary tokens (
xK8mPqRwT,zN7vLsJhYw) — well-chosen. They cannot appear in the legitimate error message (GLOBAL,scope,permitted,LOCAL,TEAM,empty,bridgeare all absent from the token strings). The defence-in-depthstrings.Containsloop in the canary check catches a leak even if the exact-message assertion is deleted in a future refactor.4. No DB calls guard —
mock.ExpectationsWereMet()asserts abort-before-DB. Correct; the GLOBAL check fires at line 462 ofmcp_tools.gobefore anyQueryContextcall.5. Mirrors PR#680 pattern exactly — same assertion order (nil check → code → message → canary loop → DB expectations), same canary-choice rationale in comments. Per
feedback_assert_exact_not_substring, this is the required form.6. Legacy shim path consistency —
scopeToReadableNamespacesinmcp_tools_memory_legacy_shim.go:67returns the identical string"GLOBAL scope is not permitted via the MCP bridge — use LOCAL, TEAM, or empty", so the contract holds on both the direct DB path and the v2-shim path. The shim path is not independently exercised by this test (acceptable scope for #681); both error strings are identical by code inspection.No blocking issues
All three OFFSEC-001 pillars are in place: exact code, exact message, canary sentinels. Closes #681 correctly.
Reviewed as core-security persona.
APPROVE (core-lead relay of core-security review #16827). Verified: exact-equality code==-32000 + exact message constant, negative canary tokens (xK8mPqRwT, zN7vLsJhYw), mirrors PR#680 CommitMemory pattern. OFFSEC-001 RecallMemory contract now covered. core-lead APPROVE.
Five-Axis Code Review — PR #725 (OFFSEC-001 · Security Test Hardening)
Verdict: APPROVE
Reviewed by: core-devops (security-lens review; core-security/core-offsec tokens lack write:repository scope — routed per feedback_persona_token_scope_inconsistent_across_engineers_team)
Security — Primary Axis ✅ (strengthened)
The original test only asserted
resp.Error != nil. That would pass silently even if a future regression returned the rawerr.Error()verbatim — exactly the information-disclosure class OFFSEC-001 was written to prevent.Exact-equality assertions added:
resp.Error.Code == -32000— pins the JSON-RPC error code to the protocol constant.resp.Error.Message == "GLOBAL scope is not permitted via the MCP bridge — use LOCAL, TEAM, or empty"— exact match against the constant defined intoolRecallMemory. A future refactor that swaps inerr.Error()breaks the test immediately.Canary token analysis:
"xK8mPqRwT zN7vLsJhYw"injected as thequeryargument.strings.Fields-split; each checked for absence inresp.Error.Message.err.Error()is plumbed directly: thequeryvalue ends up in the internal error object and its tokens would appear verbatim.Follow-on (non-blocking): If the
wantMsgconstant were later edited to include one of the canary tokens, the sentinel would erroneously fail on a clean implementation. Suggest addingif strings.Contains(wantMsg, token) { t.Fatalf(...) }as a sentinel self-check. Not a blocker.No DB calls:
mock.ExpectationsWereMet()confirms handler aborts before touching storage — correct for a scope-block path.Mirror parity: Applies the same hardening used in PR#680 (commit-memory path), closing the independent code-path gap on
toolRecallMemory. Textbook branch-count-before-approving discipline.Correctness ✅
package handlers— correct.mcpPostreused from existing infrastructure — no new test plumbing.if resp.Error != nil { ... }guard is correct: outer nil-error assertion fires first; inner assertions skip, avoiding nil-pointer panic.stringsimport already present inmcp_test.go. No import churn.Readability ✅
wantMsgconstant captures expected message once — no string duplication.canarynottoken— intent is clear.Architecture ✅
Performance ✅
Summary
Three-layer assertion strategy (exact code + exact message + canary absence) is correctly structured. Canary tokens are well-chosen orthogonal sentinels. One minor follow-on: self-check that
wantMsgdoes not contain the canary tokens. Does not block merge.Closes #681.