Skip to content

Commit 328ac88

Browse files
Claudeclaude
authored andcommitted
feat: add server-side enforcement layer for universal compliance
Advisory enforcement that works in ALL MCP clients, not just Claude Code/Cursor. Prepends warnings to tool responses when agents skip the recall→confirm→act protocol. Three checks: - No active session: warns when session-dependent tools called without session_start - No recall: warns when consequential actions (create_learning, create_decision, etc.) are attempted without first calling recall() - Unconfirmed scars: warns when recalled scars haven't been confirmed via confirm_scars() Design: advisory (warnings), not blocking (errors). Zero overhead on compliant calls. +30 unit tests covering all enforcement paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ca78bf0 commit 328ac88

File tree

3 files changed

+421
-0
lines changed

3 files changed

+421
-0
lines changed

src/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
} from "./services/startup.js";
5656
import { getEffectTracker } from "./services/effect-tracker.js";
5757
import { getProject } from "./services/session-state.js";
58+
import { checkEnforcement } from "./services/enforcement.js";
5859
import {
5960
getTier,
6061
hasSupabase,
@@ -137,6 +138,9 @@ export function createServer(): Server {
137138
};
138139
}
139140

141+
// Server-side enforcement: advisory warnings for protocol violations
142+
const enforcement = checkEnforcement(name);
143+
140144
try {
141145
let result: unknown;
142146

@@ -385,6 +389,11 @@ export function createServer(): Server {
385389
responseText = JSON.stringify(result, null, 2);
386390
}
387391

392+
// Prepend enforcement warning if present (advisory, non-blocking)
393+
if (enforcement.warning) {
394+
responseText = enforcement.warning + "\n\n" + responseText;
395+
}
396+
388397
return {
389398
content: [
390399
{

src/services/enforcement.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Server-Side Enforcement Layer
3+
*
4+
* Advisory warnings that surface in tool responses when the agent
5+
* hasn't followed the recall → confirm → act protocol.
6+
*
7+
* Design principles:
8+
* - Advisory, not blocking: warnings append to responses, never prevent execution
9+
* - Zero overhead on compliant calls: only fires when state is missing
10+
* - Universal: works in ALL MCP clients, no IDE hooks needed
11+
* - Lightweight: pure in-memory checks, no I/O
12+
*/
13+
14+
import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars } from "./session-state.js";
15+
16+
export interface EnforcementResult {
17+
/** Warning text to prepend to tool response (null = clean, no warning) */
18+
warning: string | null;
19+
}
20+
21+
/**
22+
* Tools that require an active session to function properly.
23+
* Read-only/administrative tools are excluded.
24+
*/
25+
const SESSION_REQUIRED_TOOLS = new Set([
26+
"recall", "gitmem-r",
27+
"confirm_scars", "gitmem-cs", "gm-confirm",
28+
"session_close", "gitmem-sc", "gm-close",
29+
"session_refresh", "gitmem-sr", "gm-refresh",
30+
"create_learning", "gitmem-cl", "gm-scar",
31+
"create_decision", "gitmem-cd",
32+
"record_scar_usage", "gitmem-rs",
33+
"record_scar_usage_batch", "gitmem-rsb",
34+
"prepare_context", "gitmem-pc", "gm-pc",
35+
"absorb_observations", "gitmem-ao", "gm-absorb",
36+
"create_thread", "gitmem-ct", "gm-thread-new",
37+
"resolve_thread", "gitmem-rt", "gm-resolve",
38+
"save_transcript", "gitmem-st",
39+
]);
40+
41+
/**
42+
* Tools that represent "consequential actions" — the agent is creating
43+
* or modifying state. These should ideally happen after recall + confirm.
44+
*/
45+
const CONSEQUENTIAL_TOOLS = new Set([
46+
"create_learning", "gitmem-cl", "gm-scar",
47+
"create_decision", "gitmem-cd",
48+
"create_thread", "gitmem-ct", "gm-thread-new",
49+
"session_close", "gitmem-sc", "gm-close",
50+
]);
51+
52+
/**
53+
* Tools that are always safe — no enforcement checks needed.
54+
* Includes session_start (which creates the session), read-only tools,
55+
* and administrative tools.
56+
*/
57+
const EXEMPT_TOOLS = new Set([
58+
"session_start", "gitmem-ss", "gm-open",
59+
"search", "gitmem-search", "gm-search",
60+
"log", "gitmem-log", "gm-log",
61+
"analyze", "gitmem-analyze", "gm-analyze",
62+
"graph_traverse", "gitmem-graph", "gm-graph",
63+
"list_threads", "gitmem-lt", "gm-threads",
64+
"cleanup_threads", "gitmem-cleanup", "gm-cleanup",
65+
"promote_suggestion", "gitmem-ps", "gm-promote",
66+
"dismiss_suggestion", "gitmem-ds", "gm-dismiss",
67+
"archive_learning", "gitmem-al", "gm-archive",
68+
"get_transcript", "gitmem-gt",
69+
"search_transcripts", "gitmem-stx", "gm-stx",
70+
"gitmem-help",
71+
"health", "gitmem-health", "gm-health",
72+
"gitmem-cache-status", "gm-cache-s",
73+
"gitmem-cache-health", "gm-cache-h",
74+
"gitmem-cache-flush", "gm-cache-f",
75+
]);
76+
77+
/**
78+
* Run pre-dispatch enforcement checks for a tool call.
79+
*
80+
* Returns a warning string to prepend to the response, or null if clean.
81+
* Never blocks execution — always advisory.
82+
*/
83+
export function checkEnforcement(toolName: string): EnforcementResult {
84+
// Exempt tools skip all checks
85+
if (EXEMPT_TOOLS.has(toolName)) {
86+
return { warning: null };
87+
}
88+
89+
const session = getCurrentSession();
90+
91+
// Check 1: No active session
92+
if (!session && SESSION_REQUIRED_TOOLS.has(toolName)) {
93+
return {
94+
warning: [
95+
"--- gitmem enforcement ---",
96+
"No active session. Call session_start() first to initialize memory context.",
97+
"Without a session, scars won't be tracked and the closing ceremony can't run.",
98+
"---",
99+
].join("\n"),
100+
};
101+
}
102+
103+
// If no session and not session-required, skip remaining checks
104+
if (!session) {
105+
return { warning: null };
106+
}
107+
108+
// Check 2: Unconfirmed scars before consequential action
109+
if (CONSEQUENTIAL_TOOLS.has(toolName) && hasUnconfirmedScars()) {
110+
const recallScars = getSurfacedScars().filter(s => s.source === "recall");
111+
const confirmedCount = session.confirmations?.length || 0;
112+
const pendingCount = recallScars.length - confirmedCount;
113+
114+
return {
115+
warning: [
116+
"--- gitmem enforcement ---",
117+
`${pendingCount} recalled scar(s) await confirmation.`,
118+
"Call confirm_scars() with APPLYING/N_A/REFUTED for each before proceeding.",
119+
"Unconfirmed scars may contain warnings relevant to what you're about to do.",
120+
"---",
121+
].join("\n"),
122+
};
123+
}
124+
125+
// Check 3: No recall before consequential action
126+
if (CONSEQUENTIAL_TOOLS.has(toolName)) {
127+
const recallScars = getSurfacedScars().filter(s => s.source === "recall");
128+
if (recallScars.length === 0) {
129+
return {
130+
warning: [
131+
"--- gitmem enforcement ---",
132+
"No recall() was run this session before this action.",
133+
"Consider calling recall() first to check for relevant institutional memory.",
134+
"Past mistakes and patterns may prevent repeating known issues.",
135+
"---",
136+
].join("\n"),
137+
};
138+
}
139+
}
140+
141+
return { warning: null };
142+
}

0 commit comments

Comments
 (0)