shieldcortex 2.19.1 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +14 -10
  2. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  3. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
  5. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  26. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  27. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  28. package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +3 -3
  29. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_25b1b286._.js +1 -1
  31. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  32. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  33. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
  34. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
  35. package/dashboard/.next/standalone/dashboard/.next/static/chunks/26118d592a545e00.js +3 -0
  36. package/dashboard/.next/standalone/dashboard/.next/static/chunks/6a6ccfb7834de00a.js +9 -0
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/6b11a7d29e9abffd.js +1 -0
  38. package/dashboard/.next/standalone/dashboard/.next/static/chunks/ab13d81ce0e121f2.css +3 -0
  39. package/dashboard/.next/standalone/dashboard/.next/static/chunks/cf05262adfab5818.js +1 -0
  40. package/dist/api/control.d.ts +28 -11
  41. package/dist/api/control.d.ts.map +1 -1
  42. package/dist/api/control.js +120 -19
  43. package/dist/api/control.js.map +1 -1
  44. package/dist/api/events.d.ts +12 -1
  45. package/dist/api/events.d.ts.map +1 -1
  46. package/dist/api/events.js +9 -0
  47. package/dist/api/events.js.map +1 -1
  48. package/dist/api/visualization-server.d.ts.map +1 -1
  49. package/dist/api/visualization-server.js +306 -39
  50. package/dist/api/visualization-server.js.map +1 -1
  51. package/dist/cli/doctor.d.ts +6 -0
  52. package/dist/cli/doctor.d.ts.map +1 -0
  53. package/dist/cli/doctor.js +469 -0
  54. package/dist/cli/doctor.js.map +1 -0
  55. package/dist/cloud/quarantine-sync.d.ts +3 -0
  56. package/dist/cloud/quarantine-sync.d.ts.map +1 -1
  57. package/dist/cloud/quarantine-sync.js +7 -0
  58. package/dist/cloud/quarantine-sync.js.map +1 -1
  59. package/dist/cloud/sync.d.ts +11 -0
  60. package/dist/cloud/sync.d.ts.map +1 -1
  61. package/dist/cloud/sync.js +37 -0
  62. package/dist/cloud/sync.js.map +1 -1
  63. package/dist/database/init.d.ts +8 -0
  64. package/dist/database/init.d.ts.map +1 -1
  65. package/dist/database/init.js +208 -4
  66. package/dist/database/init.js.map +1 -1
  67. package/dist/defence/index.d.ts +2 -0
  68. package/dist/defence/index.d.ts.map +1 -1
  69. package/dist/defence/index.js +2 -0
  70. package/dist/defence/index.js.map +1 -1
  71. package/dist/defence/input-sanitisation/index.d.ts +28 -0
  72. package/dist/defence/input-sanitisation/index.d.ts.map +1 -0
  73. package/dist/defence/input-sanitisation/index.js +71 -0
  74. package/dist/defence/input-sanitisation/index.js.map +1 -0
  75. package/dist/defence/iron-dome/config.js +5 -5
  76. package/dist/defence/iron-dome/config.js.map +1 -1
  77. package/dist/defence/iron-dome/index.d.ts +6 -0
  78. package/dist/defence/iron-dome/index.d.ts.map +1 -1
  79. package/dist/defence/iron-dome/index.js +19 -0
  80. package/dist/defence/iron-dome/index.js.map +1 -1
  81. package/dist/defence/pipeline.d.ts.map +1 -1
  82. package/dist/defence/pipeline.js +21 -11
  83. package/dist/defence/pipeline.js.map +1 -1
  84. package/dist/events/webhooks.d.ts +21 -0
  85. package/dist/events/webhooks.d.ts.map +1 -0
  86. package/dist/events/webhooks.js +61 -0
  87. package/dist/events/webhooks.js.map +1 -0
  88. package/dist/graph/backfill.d.ts +6 -2
  89. package/dist/graph/backfill.d.ts.map +1 -1
  90. package/dist/graph/backfill.js +32 -4
  91. package/dist/graph/backfill.js.map +1 -1
  92. package/dist/graph/extract.d.ts.map +1 -1
  93. package/dist/graph/extract.js +105 -37
  94. package/dist/graph/extract.js.map +1 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +67 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/lib.d.ts +2 -0
  99. package/dist/lib.d.ts.map +1 -1
  100. package/dist/lib.js +2 -0
  101. package/dist/lib.js.map +1 -1
  102. package/dist/memory/consolidate.d.ts +23 -0
  103. package/dist/memory/consolidate.d.ts.map +1 -1
  104. package/dist/memory/consolidate.js +239 -2
  105. package/dist/memory/consolidate.js.map +1 -1
  106. package/dist/memory/decay.d.ts.map +1 -1
  107. package/dist/memory/decay.js +9 -0
  108. package/dist/memory/decay.js.map +1 -1
  109. package/dist/memory/embedding-cache.d.ts +21 -0
  110. package/dist/memory/embedding-cache.d.ts.map +1 -0
  111. package/dist/memory/embedding-cache.js +92 -0
  112. package/dist/memory/embedding-cache.js.map +1 -0
  113. package/dist/memory/embedding.d.ts +37 -0
  114. package/dist/memory/embedding.d.ts.map +1 -0
  115. package/dist/memory/embedding.js +86 -0
  116. package/dist/memory/embedding.js.map +1 -0
  117. package/dist/memory/expiry.d.ts +26 -0
  118. package/dist/memory/expiry.d.ts.map +1 -0
  119. package/dist/memory/expiry.js +109 -0
  120. package/dist/memory/expiry.js.map +1 -0
  121. package/dist/memory/store.d.ts +14 -0
  122. package/dist/memory/store.d.ts.map +1 -1
  123. package/dist/memory/store.js +82 -0
  124. package/dist/memory/store.js.map +1 -1
  125. package/dist/memory/types.d.ts +1 -0
  126. package/dist/memory/types.d.ts.map +1 -1
  127. package/dist/memory/types.js.map +1 -1
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +193 -45
  130. package/dist/server.js.map +1 -1
  131. package/dist/tools/context.d.ts +4 -4
  132. package/dist/tools/forget.d.ts +4 -4
  133. package/dist/tools/recall.d.ts +9 -9
  134. package/dist/tools/recall.d.ts.map +1 -1
  135. package/dist/tools/recall.js +25 -1
  136. package/dist/tools/recall.js.map +1 -1
  137. package/dist/tools/remember.d.ts +6 -6
  138. package/hooks/openclaw/cortex-memory/handler.ts +8 -18
  139. package/package.json +1 -1
  140. package/dashboard/.next/standalone/dashboard/.next/static/chunks/1a71c9a52f0c9b16.css +0 -3
  141. package/dashboard/.next/standalone/dashboard/.next/static/chunks/6bf7d89d34068ecb.js +0 -9
  142. package/dashboard/.next/standalone/dashboard/.next/static/chunks/a3989d0e6629bcf8.js +0 -3
  143. package/dashboard/.next/standalone/dashboard/.next/static/chunks/d0dcb5e0e04ae015.js +0 -1
  144. package/dashboard/.next/standalone/dashboard/.next/static/chunks/fc2dbf641aad1448.js +0 -1
  145. /package/dashboard/.next/standalone/dashboard/.next/static/{vxPliVFK4FIBIPl1JPL0U → aFo1BShJENvQZgqpWRJaw}/_buildManifest.js +0 -0
  146. /package/dashboard/.next/standalone/dashboard/.next/static/{vxPliVFK4FIBIPl1JPL0U → aFo1BShJENvQZgqpWRJaw}/_clientMiddlewareManifest.json +0 -0
  147. /package/dashboard/.next/standalone/dashboard/.next/static/{vxPliVFK4FIBIPl1JPL0U → aFo1BShJENvQZgqpWRJaw}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -26,6 +26,7 @@ import { resolveSource } from './defence/trust/env-detector.js';
26
26
  import { logAudit } from './defence/audit/logger.js';
27
27
  import { scanToolResponse, shouldScanToolResponse } from './defence/tool-response-scanner.js';
28
28
  import { getToolResponseScanConfig } from './cloud/config.js';
29
+ import { isKillSwitchActive, getKillSwitchMeta, assertOperationAllowed, activateKillSwitch, deactivateKillSwitch, KillSwitchError, } from './api/control.js';
29
30
  // Shared source schema for access control on MCP tools
30
31
  const sourceParam = z.object({
31
32
  type: z.enum(['user', 'cli', 'hook', 'email', 'web', 'agent', 'file', 'api', 'tool_response']),
@@ -96,6 +97,55 @@ function resolveToolSource(declaredSource, toolName) {
96
97
  }
97
98
  return source;
98
99
  }
100
+ /**
101
+ * Wrap an MCP tool handler to enforce kill switch lockdown.
102
+ * If the kill switch is active and the operation kind is blocked,
103
+ * returns a lockdown error instead of executing the handler.
104
+ */
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ function withKillSwitchGuard(kind, handler) {
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ return async (...handlerArgs) => {
109
+ try {
110
+ assertOperationAllowed(kind);
111
+ }
112
+ catch (e) {
113
+ if (e instanceof KillSwitchError) {
114
+ const meta = e.meta;
115
+ return {
116
+ content: [{
117
+ type: 'text',
118
+ text: `## KILL SWITCH ACTIVE\n\nAll operations are locked down.\n\n` +
119
+ `**Triggered:** ${meta?.triggeredAt ?? 'unknown'}\n` +
120
+ `**Source:** ${meta?.source ?? 'unknown'}\n` +
121
+ (meta?.phrase ? `**Phrase:** "${meta.phrase}"\n` : '') +
122
+ `\nUse \`iron_dome_resume\` to resume after investigation.`,
123
+ }],
124
+ isError: true,
125
+ };
126
+ }
127
+ throw e;
128
+ }
129
+ return handler(...handlerArgs);
130
+ };
131
+ }
132
+ /**
133
+ * Check text for kill phrase and trigger kill switch if detected.
134
+ * Returns true if kill switch was activated.
135
+ */
136
+ function checkAndTriggerKillSwitch(text, source) {
137
+ if (isKillSwitchActive())
138
+ return false; // already active
139
+ try {
140
+ // Lazy import to avoid circular deps
141
+ const ironDome = require('./defence/iron-dome/index.js');
142
+ const result = ironDome.checkKillPhrase(text);
143
+ return result.triggered;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
99
149
  /**
100
150
  * Create and configure the MCP server
101
151
  */
@@ -151,13 +201,22 @@ Content is scanned through the defence pipeline before storage. Suspicious conte
151
201
  transferable: z.boolean().optional()
152
202
  .describe('Whether this memory can be transferred to other projects'),
153
203
  source: sourceParam,
154
- }, { title: 'Store Memory', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, async (args) => {
204
+ }, { title: 'Store Memory', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, withKillSwitchGuard('memory_write', async (args) => {
205
+ // Check for kill phrase in content before storing
206
+ const textToCheck = `${args.title ?? ''} ${args.content ?? ''}`;
207
+ if (checkAndTriggerKillSwitch(textToCheck, 'remember')) {
208
+ const meta = getKillSwitchMeta();
209
+ return {
210
+ content: [{ type: 'text', text: `## KILL SWITCH ACTIVATED\n\nKill phrase detected in memory content. All operations locked down.\n\nTriggered: ${meta?.triggeredAt}\nUse \`iron_dome_resume\` to resume after investigation.` }],
211
+ isError: true,
212
+ };
213
+ }
155
214
  const source = resolveToolSource(args.source, 'remember');
156
215
  const result = await executeRemember({ ...args, source });
157
216
  return {
158
217
  content: [{ type: 'text', text: formatRememberResult(result) }],
159
218
  };
160
- });
219
+ }));
161
220
  // Recall - Search and retrieve memories
162
221
  server.tool('recall', `Search and retrieve memories. Use this to:
163
222
  - Find relevant context ("What do I know about auth?")
@@ -183,13 +242,13 @@ Modes: search (query-based), recent (by time), important (by salience)`, {
183
242
  mode: z.enum(['search', 'recent', 'important']).optional().default('search')
184
243
  .describe('Recall mode'),
185
244
  source: sourceParam,
186
- }, { title: 'Search Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('recall', async (args) => {
245
+ }, { title: 'Search Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_read', withResponseScan('recall', async (args) => {
187
246
  const source = resolveToolSource(args.source, 'recall');
188
247
  const result = await executeRecall({ ...args, source });
189
248
  return {
190
249
  content: [{ type: 'text', text: formatRecallResult(result, true) }],
191
250
  };
192
- }));
251
+ })));
193
252
  // Forget - Delete memories
194
253
  server.tool('forget', `Delete memories. Use dryRun: true to preview, confirm: true for bulk.`, {
195
254
  id: z.number().optional().describe('Memory ID to delete'),
@@ -207,13 +266,13 @@ Modes: search (query-based), recent (by time), important (by salience)`, {
207
266
  confirm: z.boolean().optional().default(false)
208
267
  .describe('Confirm bulk delete'),
209
268
  source: sourceParam,
210
- }, { title: 'Delete Memories', readOnlyHint: false, destructiveHint: true, idempotentHint: false }, async (args) => {
269
+ }, { title: 'Delete Memories', readOnlyHint: false, destructiveHint: true, idempotentHint: false }, withKillSwitchGuard('memory_write', async (args) => {
211
270
  const source = resolveToolSource(args.source, 'forget');
212
271
  const result = await executeForget({ ...args, source });
213
272
  return {
214
273
  content: [{ type: 'text', text: formatForgetResult(result) }],
215
274
  };
216
- });
275
+ }));
217
276
  // Get Context - THE KEY TOOL
218
277
  server.tool('get_context', `Get relevant context from memory. THE KEY TOOL for maintaining context.
219
278
 
@@ -224,7 +283,7 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
224
283
  format: z.enum(['summary', 'detailed', 'raw']).optional().default('summary')
225
284
  .describe('Output format'),
226
285
  source: sourceParam,
227
- }, { title: 'Get Project Context', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('get_context', async (args) => {
286
+ }, { title: 'Get Project Context', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_read', withResponseScan('get_context', async (args) => {
228
287
  const source = resolveToolSource(args.source, 'get_context');
229
288
  const result = await executeGetContext({ ...args, source });
230
289
  return {
@@ -233,11 +292,11 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
233
292
  text: result.success ? result.context : `Error: ${result.error}`
234
293
  }],
235
294
  };
236
- }));
295
+ })));
237
296
  // Start Session
238
297
  server.tool('start_session', 'Start a new coding session. Returns relevant context.', {
239
298
  project: z.string().optional().describe('Project scope. Auto-detected if not provided. Use "*" for global.'),
240
- }, { title: 'Start Session', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, async (args) => {
299
+ }, { title: 'Start Session', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, withKillSwitchGuard('memory_write', async (args) => {
241
300
  const result = await executeStartSession(args);
242
301
  return {
243
302
  content: [{
@@ -247,12 +306,12 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
247
306
  : `Error: ${result.error}`
248
307
  }],
249
308
  };
250
- });
309
+ }));
251
310
  // End Session
252
311
  server.tool('end_session', 'End session and trigger consolidation.', {
253
312
  sessionId: z.number().describe('Session ID'),
254
313
  summary: z.string().optional().describe('Session summary'),
255
- }, { title: 'End Session', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
314
+ }, { title: 'End Session', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_write', async (args) => {
256
315
  const result = executeEndSession(args);
257
316
  if (!result.success) {
258
317
  return { content: [{ type: 'text', text: `Error: ${result.error}` }] };
@@ -264,12 +323,12 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
264
323
  text: `Session ended. Consolidation: ${r.consolidated} promoted, ${r.decayed} decayed, ${r.deleted} deleted.`
265
324
  }],
266
325
  };
267
- });
326
+ }));
268
327
  // Consolidate
269
328
  server.tool('consolidate', 'Run memory consolidation (like brain sleep). Promotes STM to LTM, decays old memories. Use dryRun to preview.', {
270
329
  force: z.boolean().optional().default(false).describe('Force consolidation'),
271
330
  dryRun: z.boolean().optional().default(false).describe('Preview what would happen without doing it'),
272
- }, { title: 'Run Memory Consolidation', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, async (args) => {
331
+ }, { title: 'Run Memory Consolidation', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, withKillSwitchGuard('consolidation', async (args) => {
273
332
  const result = executeConsolidate(args);
274
333
  if (!result.success) {
275
334
  return { content: [{ type: 'text', text: `Error: ${result.error}` }] };
@@ -301,11 +360,11 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
301
360
  text: `Consolidation: ${r.consolidated} promoted, ${r.decayed} updated, ${r.deleted} deleted.`
302
361
  }],
303
362
  };
304
- });
363
+ }));
305
364
  // Stats
306
365
  server.tool('memory_stats', 'Get memory statistics.', {
307
366
  project: z.string().optional().describe('Project scope. Auto-detected if not provided. Use "*" for all projects.'),
308
- }, { title: 'Memory Statistics', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async (args) => {
367
+ }, { title: 'Memory Statistics', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('status', async (args) => {
309
368
  const result = executeStats(args);
310
369
  return {
311
370
  content: [{
@@ -313,12 +372,12 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
313
372
  text: result.success ? formatStats(result.stats) : `Error: ${result.error}`
314
373
  }],
315
374
  };
316
- });
375
+ }));
317
376
  // Get Memory by ID
318
377
  server.tool('get_memory', 'Get a specific memory by ID.', {
319
378
  id: z.number().describe('Memory ID'),
320
379
  source: sourceParam,
321
- }, { title: 'Get Memory by ID', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('get_memory', async (args) => {
380
+ }, { title: 'Get Memory by ID', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_read', withResponseScan('get_memory', async (args) => {
322
381
  const source = resolveToolSource(args.source, 'get_memory');
323
382
  const result = executeGetMemory({ ...args, source });
324
383
  return {
@@ -327,11 +386,11 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
327
386
  text: result.success ? formatMemory(result.memory, true) : `Error: ${result.error}`
328
387
  }],
329
388
  };
330
- }));
389
+ })));
331
390
  // Export memories
332
391
  server.tool('export_memories', 'Export memories as JSON for backup.', {
333
392
  project: z.string().optional().describe('Project scope. Auto-detected if not provided. Use "*" for all projects.'),
334
- }, { title: 'Export Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('export_memories', async (args) => {
393
+ }, { title: 'Export Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_read', withResponseScan('export_memories', async (args) => {
335
394
  const result = executeExport(args);
336
395
  return {
337
396
  content: [{
@@ -341,11 +400,29 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
341
400
  : `Error: ${result.error}`
342
401
  }],
343
402
  };
344
- }));
403
+ })));
345
404
  // Import memories
346
405
  server.tool('import_memories', 'Import memories from JSON.', {
347
406
  data: z.string().describe('JSON data'),
348
- }, { title: 'Import Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, async (args) => {
407
+ }, { title: 'Import Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, withKillSwitchGuard('memory_write', async (args) => {
408
+ // Check imported content for kill phrase
409
+ try {
410
+ const parsed = JSON.parse(args.data);
411
+ const entries = Array.isArray(parsed) ? parsed : (parsed.memories ?? []);
412
+ for (const entry of entries) {
413
+ const text = `${entry.title ?? ''} ${entry.content ?? ''}`;
414
+ if (checkAndTriggerKillSwitch(text, 'import_memories')) {
415
+ const meta = getKillSwitchMeta();
416
+ return {
417
+ content: [{ type: 'text', text: `## KILL SWITCH ACTIVATED\n\nKill phrase detected in imported content. All operations locked down.\n\nTriggered: ${meta?.triggeredAt}\nUse \`iron_dome_resume\` to resume after investigation.` }],
418
+ isError: true,
419
+ };
420
+ }
421
+ }
422
+ }
423
+ catch {
424
+ // JSON parse failures will be caught by executeImport
425
+ }
349
426
  const result = executeImport(args);
350
427
  return {
351
428
  content: [{
@@ -355,11 +432,11 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
355
432
  : `Error: ${result.error}`
356
433
  }],
357
434
  };
358
- });
435
+ }));
359
436
  // Get Related Memories
360
437
  server.tool('get_related', 'Get memories related to a specific memory. Shows connections and relationships.', {
361
438
  id: z.number().describe('Memory ID to find relationships for'),
362
- }, { title: 'Get Related Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('get_related', async (args) => {
439
+ }, { title: 'Get Related Memories', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_read', withResponseScan('get_related', async (args) => {
363
440
  const related = getRelatedMemories(args.id);
364
441
  if (related.length === 0) {
365
442
  return { content: [{ type: 'text', text: 'No related memories found.' }] };
@@ -371,7 +448,7 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
371
448
  lines.push(` ID: ${r.memory.id} | ${r.memory.category} | ${(r.memory.salience * 100).toFixed(0)}% salience`);
372
449
  }
373
450
  return { content: [{ type: 'text', text: lines.join('\n') }] };
374
- }));
451
+ })));
375
452
  // Link Memories
376
453
  server.tool('link_memories', 'Create a relationship link between two memories.', {
377
454
  sourceId: z.number().describe('Source memory ID'),
@@ -380,17 +457,17 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
380
457
  .describe('Type of relationship'),
381
458
  strength: z.number().min(0).max(1).optional().default(0.5)
382
459
  .describe('Relationship strength (0-1)'),
383
- }, { title: 'Link Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
460
+ }, { title: 'Link Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('memory_write', async (args) => {
384
461
  const link = createMemoryLink(args.sourceId, args.targetId, args.relationship, args.strength);
385
462
  if (!link) {
386
463
  return { content: [{ type: 'text', text: 'Failed to create link. Memories may not exist or link already exists.' }] };
387
464
  }
388
465
  return { content: [{ type: 'text', text: `✓ Linked memory ${args.sourceId} → ${args.targetId} (${args.relationship})` }] };
389
- });
466
+ }));
390
467
  // Set Project - Switch active project context
391
468
  server.tool('set_project', `Switch active project context. Use "${GLOBAL_PROJECT_SENTINEL}" for global/all projects.`, {
392
469
  project: z.string().describe(`Project name, or "${GLOBAL_PROJECT_SENTINEL}" for global scope`),
393
- }, { title: 'Switch Project', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
470
+ }, { title: 'Switch Project', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('config', async (args) => {
394
471
  const oldProject = getActiveProject();
395
472
  setActiveProject(args.project === GLOBAL_PROJECT_SENTINEL ? null : args.project);
396
473
  const newProject = getActiveProject();
@@ -400,8 +477,8 @@ Returns: architecture decisions, patterns, pending items, recent activity.`, {
400
477
  text: `Project context changed: ${oldProject || 'global'} → ${newProject || 'global'}`
401
478
  }]
402
479
  };
403
- });
404
- // Get Project - Show current project scope
480
+ }));
481
+ // Get Project - Show current project scope (status — allowed during lockdown)
405
482
  server.tool('get_project', 'Show current project scope and detection info.', {}, { title: 'Show Current Project', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async () => {
406
483
  const info = getProjectContextInfo();
407
484
  const lines = [
@@ -432,7 +509,7 @@ but you can use this tool to check for new contradictions at any time.`, {
432
509
  .describe('Minimum contradiction score (0-1, default 0.4)'),
433
510
  limit: z.number().min(1).max(50).optional().default(10)
434
511
  .describe('Maximum results to return'),
435
- }, { title: 'Detect Contradictions', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('detect_contradictions', async (args) => {
512
+ }, { title: 'Detect Contradictions', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('consolidation', withResponseScan('detect_contradictions', async (args) => {
436
513
  const project = args.project ?? getActiveProject() ?? undefined;
437
514
  const contradictions = detectContradictions({
438
515
  project,
@@ -455,7 +532,7 @@ but you can use this tool to check for new contradictions at any time.`, {
455
532
  }
456
533
  lines.push(`\n*Found ${contradictions.length} potential contradiction(s). Use \`get_related\` to see linked contradictions.*`);
457
534
  return { content: [{ type: 'text', text: lines.join('\n') }] };
458
- }));
535
+ })));
459
536
  // ============================================
460
537
  // KNOWLEDGE GRAPH TOOLS
461
538
  // ============================================
@@ -464,19 +541,19 @@ but you can use this tool to check for new contradictions at any time.`, {
464
541
  entity: z.string().describe('Entity name to start from'),
465
542
  depth: z.number().optional().describe('Max traversal depth (default 2)'),
466
543
  predicates: z.array(z.string()).optional().describe('Filter by predicate types'),
467
- }, { title: 'Knowledge Graph Query', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('graph_query', async (args) => handleGraphQuery(args)));
544
+ }, { title: 'Knowledge Graph Query', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('graph', withResponseScan('graph_query', async (args) => handleGraphQuery(args))));
468
545
  // Graph Entities - List known entities
469
546
  server.tool('graph_entities', 'List known entities in the knowledge graph, optionally filtered by type.', {
470
547
  type: z.string().optional().describe('Filter by entity type (person, tool, concept, file, language, service, pattern)'),
471
548
  minMentions: z.number().optional().describe('Minimum memory references (default 1)'),
472
549
  limit: z.number().optional().describe('Max results (default 50)'),
473
- }, { title: 'List Graph Entities', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('graph_entities', async (args) => handleGraphEntities(args)));
550
+ }, { title: 'List Graph Entities', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('graph', withResponseScan('graph_entities', async (args) => handleGraphEntities(args))));
474
551
  // Graph Explain - Find paths between entities
475
552
  server.tool('graph_explain', 'Explain the relationship between two entities by finding paths connecting them in the knowledge graph.', {
476
553
  from: z.string().describe('Source entity name'),
477
554
  to: z.string().describe('Target entity name'),
478
555
  maxDepth: z.number().optional().describe('Max path length (default 4)'),
479
- }, { title: 'Explain Entity Relationship', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withResponseScan('graph_explain', async (args) => handleGraphExplain(args)));
556
+ }, { title: 'Explain Entity Relationship', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('graph', withResponseScan('graph_explain', async (args) => handleGraphExplain(args))));
480
557
  // ============================================
481
558
  // DEFENCE TOOLS
482
559
  // ============================================
@@ -499,7 +576,7 @@ but you can use this tool to check for new contradictions at any time.`, {
499
576
  quarantineId: z.number().optional().describe('ID for approve/reject'),
500
577
  sourceIdentifier: z.string().optional().describe('Source identifier for batch approve (e.g. "user-spawned>task-1")'),
501
578
  notes: z.string().optional(),
502
- }, { title: 'Review Quarantined Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, async (args) => {
579
+ }, { title: 'Review Quarantined Memories', readOnlyHint: false, destructiveHint: false, idempotentHint: false }, withKillSwitchGuard('memory_write', async (args) => {
503
580
  const db = (await import('./database/init.js')).getDatabase();
504
581
  if (args.action === 'list') {
505
582
  const items = db.prepare('SELECT * FROM quarantine WHERE status = ? ORDER BY created_at DESC LIMIT 50').all('pending');
@@ -545,8 +622,8 @@ but you can use this tool to check for new contradictions at any time.`, {
545
622
  return { content: [{ type: 'text', text: `Batch approved ${items.length} items from "${args.sourceIdentifier}" (${promoted} promoted to memory).` }] };
546
623
  }
547
624
  return { content: [{ type: 'text', text: 'Invalid action or missing required parameters.' }] };
548
- });
549
- // Defence Stats
625
+ }));
626
+ // Defence Stats (allowed during lockdown)
550
627
  server.tool('defence_stats', 'Get defence system statistics', {
551
628
  timeRange: z.enum(['24h', '7d', '30d']).default('24h'),
552
629
  }, { title: 'Defence Statistics', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async (args) => {
@@ -573,6 +650,14 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
573
650
  mode: z.enum(['advisory', 'enforce']).optional()
574
651
  .describe('Scan mode: advisory (log only) or enforce (can redact). Default: advisory'),
575
652
  }, { title: 'Scan Tool Response', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async (args) => {
653
+ // Check for kill phrase in scanned content
654
+ if (checkAndTriggerKillSwitch(args.content, 'scan_tool_response')) {
655
+ const meta = getKillSwitchMeta();
656
+ return {
657
+ content: [{ type: 'text', text: `## KILL SWITCH ACTIVATED\n\nKill phrase detected in tool response. All operations locked down.\n\nTriggered: ${meta?.triggeredAt}\nUse \`iron_dome_resume\` to resume after investigation.` }],
658
+ isError: true,
659
+ };
660
+ }
576
661
  const scan = scanToolResponse(args.toolName, args.content, args.mode);
577
662
  const lines = [
578
663
  `## Tool Response Scan: ${args.toolName}`,
@@ -664,15 +749,29 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
664
749
  // ============================================
665
750
  // IRON DOME TOOLS
666
751
  // ============================================
667
- // Iron Dome Status
668
- server.tool('iron_dome_status', 'Check if Iron Dome is active, show config summary including profile, trusted channels, and approval rules.', {}, { title: 'Iron Dome Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async () => {
752
+ // Iron Dome Status (allowed during lockdown — forensic access)
753
+ server.tool('iron_dome_status', 'Check if Iron Dome is active and show kill switch state. Shows config summary including profile, trusted channels, and approval rules.', {}, { title: 'Iron Dome Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true }, async () => {
669
754
  const { getIronDomeStatus } = await import('./defence/iron-dome/index.js');
670
755
  const status = getIronDomeStatus();
756
+ const killMeta = getKillSwitchMeta();
671
757
  const lines = [
672
758
  `## Iron Dome Status`,
673
759
  '',
674
- `**Active:** ${status.enabled ? 'Yes' : 'No'}`,
675
760
  ];
761
+ // Kill switch state (most important — show first)
762
+ if (isKillSwitchActive() && killMeta) {
763
+ lines.push(`**KILL SWITCH: ACTIVE**`);
764
+ lines.push(`**Triggered:** ${killMeta.triggeredAt}`);
765
+ lines.push(`**Source:** ${killMeta.source}`);
766
+ if (killMeta.phrase)
767
+ lines.push(`**Phrase:** "${killMeta.phrase}"`);
768
+ if (killMeta.memoryCountAtTrigger != null)
769
+ lines.push(`**Memories at trigger:** ${killMeta.memoryCountAtTrigger}`);
770
+ lines.push('');
771
+ lines.push('> All operations are locked down. Use `iron_dome_resume` to resume.');
772
+ lines.push('');
773
+ }
774
+ lines.push(`**Iron Dome Active:** ${status.enabled ? 'Yes' : 'No'}`);
676
775
  if (status.enabled) {
677
776
  const c = status.config;
678
777
  lines.push(`**Profile:** ${c.profile ?? 'custom'}`);
@@ -740,7 +839,7 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
740
839
  server.tool('iron_dome_activate', 'Activate Iron Dome with a profile. Profiles: school (GDPR strict), enterprise (financial protection), personal (lighter touch), paranoid (everything requires approval).', {
741
840
  profile: z.enum(['school', 'enterprise', 'personal', 'paranoid']).optional()
742
841
  .describe('Security profile to activate'),
743
- }, { title: 'Activate Iron Dome', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
842
+ }, { title: 'Activate Iron Dome', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, withKillSwitchGuard('config', async (args) => {
744
843
  const { activateIronDome } = await import('./defence/iron-dome/index.js');
745
844
  const config = activateIronDome(args.profile);
746
845
  const lines = [
@@ -753,12 +852,50 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
753
852
  `**Auto-Approve:** ${config.autoApprove.join(', ')}`,
754
853
  ];
755
854
  return { content: [{ type: 'text', text: lines.join('\n') }] };
855
+ }));
856
+ // ============================================
857
+ // IRON DOME EMERGENCY TOOLS
858
+ // ============================================
859
+ // Emergency Stop — NOT guarded (must always work)
860
+ server.tool('iron_dome_emergency_stop', 'Emergency kill switch — immediately locks down ALL agent operations. Use when you detect suspicious activity. Reversible via iron_dome_resume.', {}, { title: 'Emergency Stop', readOnlyHint: false, destructiveHint: true, idempotentHint: true }, async () => {
861
+ activateKillSwitch({ source: 'mcp_tool' });
862
+ const meta = getKillSwitchMeta();
863
+ return {
864
+ content: [{
865
+ type: 'text',
866
+ text: `## KILL SWITCH ACTIVATED\n\nAll operations locked down.\n\n` +
867
+ `**Triggered:** ${meta?.triggeredAt}\n` +
868
+ `**Source:** MCP tool\n\n` +
869
+ `Memory creation, recall, graph queries, and all other operations are blocked.\n` +
870
+ `Use \`iron_dome_resume\` with a reason to resume after investigation.`,
871
+ }],
872
+ };
873
+ });
874
+ // Resume — NOT guarded (this IS the unlock)
875
+ server.tool('iron_dome_resume', 'Resume agent operations after kill switch investigation. Only use after confirming the threat has been addressed.', {
876
+ reason: z.string().describe('Why operations are being resumed (required for audit trail)'),
877
+ }, { title: 'Resume Operations', readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
878
+ if (!isKillSwitchActive()) {
879
+ return {
880
+ content: [{ type: 'text', text: 'Kill switch is not active. No action needed.' }],
881
+ };
882
+ }
883
+ deactivateKillSwitch(args.reason);
884
+ return {
885
+ content: [{
886
+ type: 'text',
887
+ text: `## Operations Resumed\n\nKill switch deactivated.\n**Reason:** ${args.reason}\n\nAll tools are now operational. Iron Dome continues protecting.`,
888
+ }],
889
+ };
756
890
  });
757
891
  // ============================================
758
892
  // RESOURCES
759
893
  // ============================================
760
- // Project context resource
894
+ // Project context resource (blocked during kill switch)
761
895
  server.resource('memory://context', 'memory://context', async () => {
896
+ if (isKillSwitchActive()) {
897
+ return { contents: [{ uri: 'memory://context', mimeType: 'text/plain', text: '[KILL SWITCH ACTIVE] Memory access blocked.' }] };
898
+ }
762
899
  const summary = await generateContextSummary();
763
900
  return {
764
901
  contents: [{
@@ -768,8 +905,11 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
768
905
  }],
769
906
  };
770
907
  });
771
- // Important memories resource
908
+ // Important memories resource (blocked during kill switch)
772
909
  server.resource('memory://important', 'memory://important', async () => {
910
+ if (isKillSwitchActive()) {
911
+ return { contents: [{ uri: 'memory://important', mimeType: 'text/plain', text: '[KILL SWITCH ACTIVE] Memory access blocked.' }] };
912
+ }
773
913
  const memories = getHighPriorityMemories(20);
774
914
  const text = memories.map(m => `## ${m.title}\n${m.content}\n*${m.category} | ${(m.salience * 100).toFixed(0)}% salience*\n`).join('\n');
775
915
  return {
@@ -780,8 +920,11 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
780
920
  }],
781
921
  };
782
922
  });
783
- // Recent memories resource
923
+ // Recent memories resource (blocked during kill switch)
784
924
  server.resource('memory://recent', 'memory://recent', async () => {
925
+ if (isKillSwitchActive()) {
926
+ return { contents: [{ uri: 'memory://recent', mimeType: 'text/plain', text: '[KILL SWITCH ACTIVE] Memory access blocked.' }] };
927
+ }
785
928
  const memories = getRecentMemories(15);
786
929
  const text = memories.map(m => `- **${m.title}** (${m.category}): ${m.content.slice(0, 100)}...`).join('\n');
787
930
  return {
@@ -795,8 +938,13 @@ Runs injection detection (40+ patterns) and credential leak scanning (25+ provid
795
938
  // ============================================
796
939
  // PROMPTS
797
940
  // ============================================
798
- // Context restoration prompt
941
+ // Context restoration prompt (blocked during kill switch)
799
942
  server.prompt('restore_context', 'Restore context after compaction or at session start', async () => {
943
+ if (isKillSwitchActive()) {
944
+ return {
945
+ messages: [{ role: 'user', content: { type: 'text', text: '[KILL SWITCH ACTIVE] Context restoration blocked. Use iron_dome_resume to resume.' } }],
946
+ };
947
+ }
800
948
  const summary = await generateContextSummary();
801
949
  const context = formatContextSummary(summary);
802
950
  return {