@drakon-systems/shieldcortex-realtime 4.50.0 → 4.51.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.
package/README.md CHANGED
@@ -32,7 +32,8 @@ The defensive root `openclaw.plugin.json` is kept for one release on the main pa
32
32
  | `before_agent_run` | **The conversation firewall's enforcement point.** The documented input gate — it is awaited and its result decides whether the run proceeds. Behaviour is set by `interceptor.conversation.posture` (see [Conversation firewall](#conversation-firewall)). |
33
33
  | `llm_output` | Extracts high-signal memories from assistant replies and writes them into ShieldCortex with novelty filtering and dedupe. |
34
34
  | `before_tool_call` | Runs the Action Guard before tools execute. Catastrophic shell/file/network/git actions are always blocked. Recognised-dangerous actions are **enforced by default**: attended sessions get an approval prompt, unattended sessions fail closed per `failurePolicy`. Set `actionGuard.enforce: false` to opt down to warn-and-allow, or pre-approve specific operations with `actionGuard.autoApprove`. |
35
- | `session_end` | Resets the interceptor's per-session caches and releases that session's scan-unavailable alert window. Registered even when `interceptor.enabled` is `false`, because the conversation gate keeps per-session state regardless. |
35
+ | `session_end` | Resets the interceptor's per-session caches, releases that session's scan-unavailable alert window, and (with `agent_end`) writes `action_guard_degraded` when the Action Guard denied or warned during the session. Registered even when `interceptor.enabled` is `false`, because the conversation gate keeps per-session state regardless. Neither hook can block, approve, or delay a turn. |
36
+ | `agent_end` | Same degraded-run summariser as `session_end`, idempotent with it. Present on OpenClaw 2026.5.7+; an older host warns-and-returns and `session_end` still summarises. |
36
37
  | `/shieldcortex-status` | Slash command reporting the plugin's runtime state. |
37
38
 
38
39
  The scanning and memory paths are fire-and-forget: they do not stall the OpenClaw turn loop if ShieldCortex is unavailable. The Action Guard is the deliberate exception — it gates tool calls inline, and since 4.47.5 a guard that fails to load falls back to a dependency-free scanner that still denies unambiguous catastrophic operations (fail-closed) rather than allowing everything.
package/dist/index.js CHANGED
@@ -239,6 +239,24 @@ export function __setDefenceModuleForTest(mod) {
239
239
  _defenceModOverride = mod;
240
240
  _defenceModPromise = null;
241
241
  }
242
+ /** #260 — emit action_guard_degraded for this session. Never throws, never
243
+ * waits when the defence module is already injected (the test / in-process
244
+ * path). A missing module is a silent no-op: session_end cannot block (#112). */
245
+ function summariseGuardSession(sessionId, origin) {
246
+ if (!sessionId)
247
+ return;
248
+ const run = (mod) => {
249
+ try {
250
+ mod?.recordActionGuardDegraded?.(sessionId, { origin });
251
+ }
252
+ catch { /* never wedge */ }
253
+ };
254
+ if (_defenceModOverride !== undefined) {
255
+ run(_defenceModOverride);
256
+ return;
257
+ }
258
+ void getDefenceModule().then(run).catch(() => { });
259
+ }
242
260
  export function __setRuntimeForTest(runtime) {
243
261
  _runtimeOverride = runtime;
244
262
  if (runtime)
@@ -1690,8 +1708,23 @@ const MIN_NOVELTY_CHARS = 40;
1690
1708
  async function auditLog(entry) {
1691
1709
  const dir = auditDir();
1692
1710
  try {
1711
+ const hookName = typeof entry.hook === 'string' && entry.hook ? entry.hook : 'llm_input';
1712
+ const plane = hookName === 'before_tool_call' ? 'action_guard' : 'conversation_firewall';
1713
+ let bound = entry;
1714
+ try {
1715
+ const defenceMod = await getDefenceModule();
1716
+ if (typeof defenceMod?.attachEnforcementBinding === 'function') {
1717
+ bound = defenceMod.attachEnforcementBinding(entry, {
1718
+ plane,
1719
+ hookName,
1720
+ pluginId: 'shieldcortex-realtime',
1721
+ actionKey: typeof entry.actionKey === 'string' ? entry.actionKey : `conversation:${hookName}`,
1722
+ });
1723
+ }
1724
+ }
1725
+ catch { /* older package / bind failure — write the unbound row */ }
1693
1726
  await fs.mkdir(dir, { recursive: true });
1694
- await fs.appendFile(path.join(dir, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`), JSON.stringify(entry) + "\n");
1727
+ await fs.appendFile(path.join(dir, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`), JSON.stringify(bound) + "\n");
1695
1728
  return true;
1696
1729
  }
1697
1730
  catch (err) {
@@ -2910,11 +2943,31 @@ export default {
2910
2943
  releaseActionLease: typeof defenceMod.releaseToolCallLease === 'function'
2911
2944
  ? (toolName, args, sessionId) => defenceMod.releaseToolCallLease(toolName, args, { self: sessionId ?? '' })
2912
2945
  : undefined,
2946
+ // #260: the session-guard index. Same formula as the Claude Code
2947
+ // hook. Absent on an older dist — then emitAudit still stamps origin
2948
+ // but does not write an index nobody would summarise.
2949
+ sessionGuard: typeof defenceMod.sessionKeyFor === 'function' && typeof defenceMod.appendSessionGuardIndex === 'function'
2950
+ ? {
2951
+ keyFor: (sessionId) => defenceMod.sessionKeyFor(sessionId),
2952
+ index: (entry) => {
2953
+ defenceMod.appendSessionGuardIndex({ entry: { ...entry } });
2954
+ },
2955
+ }
2956
+ : undefined,
2913
2957
  onAuditEntry: (entry) => syncInterceptEvent(entry, {
2914
2958
  cloudApiKey: scConfig.cloudApiKey ?? '',
2915
2959
  cloudBaseUrl: scConfig.cloudBaseUrl ?? 'https://api.shieldcortex.ai',
2916
2960
  cloudEnabled: scConfig.cloudEnabled ?? false,
2917
2961
  }),
2962
+ bindAudit: typeof defenceMod.attachEnforcementBinding === 'function'
2963
+ ? (entry, args) => defenceMod.attachEnforcementBinding(entry, {
2964
+ plane: 'action_guard',
2965
+ hookName: 'before_tool_call',
2966
+ pluginId: 'shieldcortex-realtime',
2967
+ tool: entry.tool,
2968
+ args: args ?? {},
2969
+ })
2970
+ : undefined,
2918
2971
  });
2919
2972
  const guardState = interceptorConfig.actionGuard?.enabled
2920
2973
  ? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
@@ -2976,8 +3029,9 @@ export default {
2976
3029
  // `before_tool_call`: a registered approval hook changes how OpenClaw
2977
3030
  // resolves tool-call approvals for unattended Codex agents, so an
2978
3031
  // unattended turn waited 120s on a decision nobody could give. `session_end`
2979
- // is a notification — it cannot block, approve, or delay anything — and its
2980
- // handler here only frees local state.
3032
+ // is a notification — it cannot block, approve, or delay anything. It frees
3033
+ // local state and, since #260, best-effort summarises a degraded Action
3034
+ // Guard session. That write is not a decision and cannot stall the host.
2981
3035
  try {
2982
3036
  api.on('session_end', (event, ctx) => {
2983
3037
  interceptorReady?.resetSession();
@@ -2991,11 +3045,28 @@ export default {
2991
3045
  // per-session rule, for the same reason.
2992
3046
  if (endedSession)
2993
3047
  sessionTaint.clear(endedSession);
3048
+ // #260: plane-native summariser. session_end cannot block (#112) —
3049
+ // this is a notification hook. The write is best-effort and
3050
+ // idempotent with agent_end below.
3051
+ summariseGuardSession(endedSession, 'openclaw-session-end');
2994
3052
  });
2995
3053
  }
2996
3054
  catch {
2997
3055
  // session_end may not be a supported hook — TTL safety net handles this
2998
3056
  }
3057
+ // #260: agent_end exists on the engine floor (2026.5.7 already declared
3058
+ // it). An unknown typed hook is warn-and-return, not a throw, so we still
3059
+ // wrap registration. Same summariser as session_end — whichever fires
3060
+ // first writes, the other is a no-op. Do not invent a third sink.
3061
+ try {
3062
+ api.on('agent_end', (event, ctx) => {
3063
+ const endedSession = ctx?.sessionId ?? ctx?.sessionKey ?? event?.sessionId ?? event?.sessionKey ?? null;
3064
+ summariseGuardSession(endedSession, 'openclaw-session-end');
3065
+ });
3066
+ }
3067
+ catch {
3068
+ // Host predates agent_end — session_end is the load-bearing summariser.
3069
+ }
2999
3070
  // llm_input/llm_output are CONVERSATION hooks: OpenClaw drops them at
3000
3071
  // registration for a non-bundled plugin unless the host grants
3001
3072
  // plugins.entries.<id>.hooks.allowConversationAccess = true. Registration is
@@ -509,6 +509,10 @@ export function createInterceptor(config, pipeline, options) {
509
509
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
510
510
  const log = config.logger ?? { info: console.log, warn: console.warn };
511
511
  const onAuditEntry = options?.onAuditEntry;
512
+ let lastSessionId;
513
+ const bindAudit = options?.bindAudit;
514
+ /** Args of the in-flight tool call — used only to mint #224 actionKey. */
515
+ let lastCallArgs;
512
516
  const actionGuardCfg = config.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] };
513
517
  const evaluateToolCall = options?.evaluateToolCall;
514
518
  const broker = options?.broker;
@@ -519,8 +523,19 @@ export function createInterceptor(config, pipeline, options) {
519
523
  /** Bare tool names seen this session, newest last. See buildSessionSummary. */
520
524
  const recentTools = [];
521
525
  function emitAudit(entry) {
522
- writeAuditEntry(entry);
523
- onAuditEntry?.(entry);
526
+ const sessionKey = options?.sessionGuard?.keyFor(lastSessionId) ?? undefined;
527
+ const withOrigin = {
528
+ ...entry,
529
+ origin: 'openclaw-interceptor',
530
+ ...(sessionKey ? { sessionKey } : {}),
531
+ };
532
+ const bound = bindAudit ? bindAudit(withOrigin, lastCallArgs) : withOrigin;
533
+ writeAuditEntry(bound);
534
+ try {
535
+ options?.sessionGuard?.index(bound);
536
+ }
537
+ catch { /* never wedge the turn */ }
538
+ onAuditEntry?.(bound);
524
539
  }
525
540
  function guardAuditBase(toolName, v, preview) {
526
541
  return {
@@ -910,6 +925,8 @@ export function createInterceptor(config, pipeline, options) {
910
925
  throw new Error('ShieldCortex: tool call denied by user');
911
926
  }
912
927
  async function handleToolCall(context) {
928
+ lastSessionId = context.sessionId;
929
+ lastCallArgs = context.arguments;
913
930
  // Remember the NAME only. This is the entirety of what the approval broker's
914
931
  // judge will ever learn about the session — see buildSessionSummary.
915
932
  noteToolForSession(context.toolName);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.50.0",
3
+ "version": "4.51.0",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
@@ -18,7 +18,8 @@
18
18
  "llm_output",
19
19
  "before_agent_run",
20
20
  "before_tool_call",
21
- "session_end"
21
+ "session_end",
22
+ "agent_end"
22
23
  ],
23
24
  "commands": [
24
25
  "shieldcortex-status"
package/index.ts CHANGED
@@ -100,6 +100,27 @@ type DefenceModule = {
100
100
  notification: unknown,
101
101
  deps: { channels: NotifyChannelLike[]; timeoutMs?: number },
102
102
  ) => Promise<{ deliveredVia: string | null; attempts: Array<{ channel: string; result: { delivered: boolean; reason?: string } }> }>;
103
+ /** #260 — session-guard index + degraded-run summary. Optional so an older
104
+ * installed dist degrades to "no index" rather than crashing the hook. */
105
+ sessionKeyFor?: (value: string | undefined, opts?: { home?: string; salt?: string }) => string | null;
106
+ appendSessionGuardIndex?: (opts: { home?: string; entry: Record<string, unknown> }) => boolean;
107
+ recordActionGuardDegraded?: (
108
+ rawSessionId: string | undefined,
109
+ opts?: { home?: string; salt?: string; origin?: string },
110
+ ) => { recorded: boolean; count: number; sessionKey?: string; existing?: boolean };
111
+ /** #224 — stamp binding fields on a realtime audit row. Optional so an
112
+ * older installed package degrades to unbound records, not a crash. */
113
+ attachEnforcementBinding?: (
114
+ entry: Record<string, unknown>,
115
+ ctx: {
116
+ plane: 'action_guard' | 'conversation_firewall';
117
+ hookName: string;
118
+ pluginId: string;
119
+ tool?: string;
120
+ args?: Record<string, unknown>;
121
+ actionKey?: string;
122
+ },
123
+ ) => Record<string, unknown>;
103
124
  };
104
125
 
105
126
  let runtimePromise: Promise<OpenClawRuntime> | null = null;
@@ -322,6 +343,21 @@ export function __setDefenceModuleForTest(mod: DefenceModule | null | undefined)
322
343
  _defenceModOverride = mod;
323
344
  _defenceModPromise = null;
324
345
  }
346
+
347
+ /** #260 — emit action_guard_degraded for this session. Never throws, never
348
+ * waits when the defence module is already injected (the test / in-process
349
+ * path). A missing module is a silent no-op: session_end cannot block (#112). */
350
+ function summariseGuardSession(sessionId: string | null, origin: string): void {
351
+ if (!sessionId) return;
352
+ const run = (mod: DefenceModule | null) => {
353
+ try { mod?.recordActionGuardDegraded?.(sessionId, { origin }); } catch { /* never wedge */ }
354
+ };
355
+ if (_defenceModOverride !== undefined) {
356
+ run(_defenceModOverride);
357
+ return;
358
+ }
359
+ void getDefenceModule().then(run).catch(() => {});
360
+ }
325
361
  export function __setRuntimeForTest(runtime: OpenClawRuntime | null): void {
326
362
  _runtimeOverride = runtime;
327
363
  if (runtime) runtimePromise = null;
@@ -2036,10 +2072,24 @@ const MIN_NOVELTY_CHARS = 40;
2036
2072
  async function auditLog(entry: Record<string, unknown>): Promise<boolean> {
2037
2073
  const dir = auditDir();
2038
2074
  try {
2075
+ const hookName = typeof entry.hook === 'string' && entry.hook ? entry.hook : 'llm_input';
2076
+ const plane = hookName === 'before_tool_call' ? 'action_guard' : 'conversation_firewall';
2077
+ let bound = entry;
2078
+ try {
2079
+ const defenceMod = await getDefenceModule();
2080
+ if (typeof defenceMod?.attachEnforcementBinding === 'function') {
2081
+ bound = defenceMod.attachEnforcementBinding(entry, {
2082
+ plane,
2083
+ hookName,
2084
+ pluginId: 'shieldcortex-realtime',
2085
+ actionKey: typeof entry.actionKey === 'string' ? entry.actionKey : `conversation:${hookName}`,
2086
+ });
2087
+ }
2088
+ } catch { /* older package / bind failure — write the unbound row */ }
2039
2089
  await fs.mkdir(dir, { recursive: true });
2040
2090
  await fs.appendFile(
2041
2091
  path.join(dir, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`),
2042
- JSON.stringify(entry) + "\n",
2092
+ JSON.stringify(bound) + "\n",
2043
2093
  );
2044
2094
  return true;
2045
2095
  } catch (err) {
@@ -3443,11 +3493,31 @@ export default {
3443
3493
  ? (toolName, args, sessionId) =>
3444
3494
  (defenceMod as any).releaseToolCallLease(toolName, args, { self: sessionId ?? '' })
3445
3495
  : undefined,
3496
+ // #260: the session-guard index. Same formula as the Claude Code
3497
+ // hook. Absent on an older dist — then emitAudit still stamps origin
3498
+ // but does not write an index nobody would summarise.
3499
+ sessionGuard: typeof defenceMod.sessionKeyFor === 'function' && typeof defenceMod.appendSessionGuardIndex === 'function'
3500
+ ? {
3501
+ keyFor: (sessionId) => defenceMod.sessionKeyFor!(sessionId),
3502
+ index: (entry) => {
3503
+ defenceMod.appendSessionGuardIndex!({ entry: { ...entry } as Record<string, unknown> });
3504
+ },
3505
+ }
3506
+ : undefined,
3446
3507
  onAuditEntry: (entry) => syncInterceptEvent(entry, {
3447
3508
  cloudApiKey: (scConfig as any).cloudApiKey ?? '',
3448
3509
  cloudBaseUrl: (scConfig as any).cloudBaseUrl ?? 'https://api.shieldcortex.ai',
3449
3510
  cloudEnabled: (scConfig as any).cloudEnabled ?? false,
3450
3511
  }),
3512
+ bindAudit: typeof (defenceMod as any).attachEnforcementBinding === 'function'
3513
+ ? (entry, args) => (defenceMod as any).attachEnforcementBinding(entry, {
3514
+ plane: 'action_guard',
3515
+ hookName: 'before_tool_call',
3516
+ pluginId: 'shieldcortex-realtime',
3517
+ tool: entry.tool,
3518
+ args: args ?? {},
3519
+ }) as typeof entry
3520
+ : undefined,
3451
3521
  });
3452
3522
  const guardState = interceptorConfig.actionGuard?.enabled
3453
3523
  ? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
@@ -3509,8 +3579,9 @@ export default {
3509
3579
  // `before_tool_call`: a registered approval hook changes how OpenClaw
3510
3580
  // resolves tool-call approvals for unattended Codex agents, so an
3511
3581
  // unattended turn waited 120s on a decision nobody could give. `session_end`
3512
- // is a notification — it cannot block, approve, or delay anything — and its
3513
- // handler here only frees local state.
3582
+ // is a notification — it cannot block, approve, or delay anything. It frees
3583
+ // local state and, since #260, best-effort summarises a degraded Action
3584
+ // Guard session. That write is not a decision and cannot stall the host.
3514
3585
  try {
3515
3586
  api.on('session_end', (event?: { sessionId?: string; sessionKey?: string }, ctx?: AgentCtx) => {
3516
3587
  interceptorReady?.resetSession();
@@ -3523,11 +3594,28 @@ export default {
3523
3594
  // #233: a taint must not outlive the conversation that earned it. Same
3524
3595
  // per-session rule, for the same reason.
3525
3596
  if (endedSession) sessionTaint.clear(endedSession);
3597
+ // #260: plane-native summariser. session_end cannot block (#112) —
3598
+ // this is a notification hook. The write is best-effort and
3599
+ // idempotent with agent_end below.
3600
+ summariseGuardSession(endedSession, 'openclaw-session-end');
3526
3601
  });
3527
3602
  } catch {
3528
3603
  // session_end may not be a supported hook — TTL safety net handles this
3529
3604
  }
3530
3605
 
3606
+ // #260: agent_end exists on the engine floor (2026.5.7 already declared
3607
+ // it). An unknown typed hook is warn-and-return, not a throw, so we still
3608
+ // wrap registration. Same summariser as session_end — whichever fires
3609
+ // first writes, the other is a no-op. Do not invent a third sink.
3610
+ try {
3611
+ api.on('agent_end', (event?: { sessionId?: string; sessionKey?: string }, ctx?: AgentCtx) => {
3612
+ const endedSession = ctx?.sessionId ?? ctx?.sessionKey ?? event?.sessionId ?? event?.sessionKey ?? null;
3613
+ summariseGuardSession(endedSession, 'openclaw-session-end');
3614
+ });
3615
+ } catch {
3616
+ // Host predates agent_end — session_end is the load-bearing summariser.
3617
+ }
3618
+
3531
3619
  // llm_input/llm_output are CONVERSATION hooks: OpenClaw drops them at
3532
3620
  // registration for a non-bundled plugin unless the host grants
3533
3621
  // plugins.entries.<id>.hooks.allowConversationAccess = true. Registration is
package/interceptor.ts CHANGED
@@ -202,6 +202,17 @@ export interface InterceptAuditEntry {
202
202
  escalated?: { by: 'session-taint'; from: string; to: string; reason: string };
203
203
  /** Files the reviewed-script allowlist exempted from folding (#189). */
204
204
  reviewedScripts?: string[];
205
+ /** #260 — plane origin so the session-guard summariser can find this row. */
206
+ origin?: 'openclaw-interceptor';
207
+ sessionKey?: string;
208
+ /** #224 — binding fields. Present once the host injects `bindAudit`. */
209
+ plane?: 'action_guard' | 'conversation_firewall';
210
+ gatewayInstanceId?: string;
211
+ hookName?: string;
212
+ pluginId?: string;
213
+ nonce?: string;
214
+ seq?: number;
215
+ actionKey?: string;
205
216
  }
206
217
 
207
218
  const WATCHED_TOOLS = ['remember', 'mcp__memory__remember'] as const;
@@ -798,6 +809,15 @@ interface InterceptorOptions {
798
809
  * limit, so a looping or compromised agent must not be able to spend it
799
810
  * without bound. Exhausting it yields no judge, which yields a hold. */
800
811
  maxJudgeCallsPerMinute?: number;
812
+ /** #260 — session-guard index. Injected from `shieldcortex/defence`. */
813
+ sessionGuard?: {
814
+ keyFor: (sessionId: string | undefined) => string | null;
815
+ index: (entry: InterceptAuditEntry) => void;
816
+ };
817
+ /** #224 — stamp plane/instance/hook/nonce/seq/actionKey before the row hits
818
+ * disk. Injected from `shieldcortex/defence` at runtime so this plugin does
819
+ * not grow a second schema. Absent = unbound (older installed package). */
820
+ bindAudit?: (entry: InterceptAuditEntry, args?: Record<string, unknown>) => InterceptAuditEntry;
801
821
  }
802
822
 
803
823
  /** How many recent tool NAMES the judge is told about. Names only, never
@@ -820,6 +840,10 @@ export function createInterceptor(
820
840
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
821
841
  const log = config.logger ?? { info: console.log, warn: console.warn };
822
842
  const onAuditEntry = options?.onAuditEntry;
843
+ let lastSessionId: string | undefined;
844
+ const bindAudit = options?.bindAudit;
845
+ /** Args of the in-flight tool call — used only to mint #224 actionKey. */
846
+ let lastCallArgs: Record<string, unknown> | undefined;
823
847
  const actionGuardCfg: ActionGuardConfig = config.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] };
824
848
  const evaluateToolCall = options?.evaluateToolCall;
825
849
  const broker = options?.broker;
@@ -831,8 +855,16 @@ export function createInterceptor(
831
855
  const recentTools: string[] = [];
832
856
 
833
857
  function emitAudit(entry: InterceptAuditEntry): void {
834
- writeAuditEntry(entry);
835
- onAuditEntry?.(entry);
858
+ const sessionKey = options?.sessionGuard?.keyFor(lastSessionId) ?? undefined;
859
+ const withOrigin: InterceptAuditEntry = {
860
+ ...entry,
861
+ origin: 'openclaw-interceptor',
862
+ ...(sessionKey ? { sessionKey } : {}),
863
+ };
864
+ const bound = bindAudit ? bindAudit(withOrigin, lastCallArgs) : withOrigin;
865
+ writeAuditEntry(bound);
866
+ try { options?.sessionGuard?.index(bound); } catch { /* never wedge the turn */ }
867
+ onAuditEntry?.(bound);
836
868
  }
837
869
 
838
870
  function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
@@ -1249,6 +1281,8 @@ export function createInterceptor(
1249
1281
  }
1250
1282
 
1251
1283
  async function handleToolCall(context: ToolCallContext): Promise<void> {
1284
+ lastSessionId = context.sessionId;
1285
+ lastCallArgs = context.arguments;
1252
1286
  // Remember the NAME only. This is the entirety of what the approval broker's
1253
1287
  // judge will ever learn about the session — see buildSessionSummary.
1254
1288
  noteToolForSession(context.toolName);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.50.0",
3
+ "version": "4.51.0",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
@@ -18,7 +18,8 @@
18
18
  "llm_output",
19
19
  "before_agent_run",
20
20
  "before_tool_call",
21
- "session_end"
21
+ "session_end",
22
+ "agent_end"
22
23
  ],
23
24
  "commands": [
24
25
  "shieldcortex-status"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.50.0",
3
+ "version": "4.51.0",
4
4
  "description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",