@drakon-systems/shieldcortex-realtime 4.47.14 → 4.47.15

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/dist/index.js CHANGED
@@ -148,6 +148,7 @@ export function __resetConfigStateForTest() {
148
148
  _configOverride = null;
149
149
  _lastShieldConfigRef = null;
150
150
  _registered = false;
151
+ _beforeToolCallRegistered = false;
151
152
  }
152
153
  const INTERCEPT_SEVERITIES = ['low', 'medium', 'high', 'critical'];
153
154
  const INTERCEPT_ACTIONS = ['log', 'warn', 'require_approval'];
@@ -290,6 +291,13 @@ try {
290
291
  }
291
292
  catch { /* fallback */ }
292
293
  let _registered = false;
294
+ // Whether register() actually attached the before_tool_call hook this session.
295
+ // False when the host config explicitly disables the interceptor — in that
296
+ // case the hook must not exist at all, so the host's approval pipeline can
297
+ // never route a tool call through this plugin (issue #112 follow-up: on
298
+ // unattended Codex agents even a no-op registered hook changes how OpenClaw
299
+ // resolves approvals).
300
+ let _beforeToolCallRegistered = false;
293
301
  function normaliseConfig(raw) {
294
302
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
295
303
  return {};
@@ -914,20 +922,42 @@ export default {
914
922
  return null;
915
923
  }
916
924
  }
917
- // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
918
- // can block or require approval before the selected tool executes.
919
- api.on('before_tool_call', async (event) => {
920
- const interceptor = await initInterceptor();
921
- if (!interceptor)
922
- return;
923
- return handleTypedBeforeToolCall(event, interceptor, api.logger);
924
- }, { priority: 80, timeoutMs: 30_000 });
925
- // Try to register session_end for cache cleanup
926
- try {
927
- api.on('session_end', () => { interceptorReady?.resetSession(); });
925
+ // #112 follow-up: when the host config (openclaw.json plugin entry)
926
+ // explicitly disables the interceptor, do NOT register before_tool_call at
927
+ // all. A registered-but-no-op hook still appears in the host's hook roster
928
+ // and changes how OpenClaw resolves tool-call approvals for unattended
929
+ // Codex agents (plugin.approval.waitDecision waits on a decision no one
930
+ // can give, times out after 120s, and the turn emits no reply). Absent
931
+ // hook = the host's native approval path is untouched.
932
+ //
933
+ // Only the synchronously-available plugin config can gate registration
934
+ // (the shield config file loads async via the runtime); a shield-file-only
935
+ // `enabled:false` keeps the lazy no-op behaviour, which returns
936
+ // immediately and never requests approval — covered by regression tests.
937
+ // Note: re-enabling the interceptor from openclaw.json requires a gateway
938
+ // restart, since registration happens once at plugin load.
939
+ const interceptorDisabledInHostConfig = _configOverride?.interceptor?.enabled === false;
940
+ if (!interceptorDisabledInHostConfig) {
941
+ // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
942
+ // can block or require approval before the selected tool executes.
943
+ api.on('before_tool_call', async (event) => {
944
+ const interceptor = await initInterceptor();
945
+ if (!interceptor)
946
+ return;
947
+ return handleTypedBeforeToolCall(event, interceptor, api.logger);
948
+ }, { priority: 80, timeoutMs: 30_000 });
949
+ _beforeToolCallRegistered = true;
950
+ // Try to register session_end for cache cleanup (only meaningful while
951
+ // an interceptor can exist)
952
+ try {
953
+ api.on('session_end', () => { interceptorReady?.resetSession(); });
954
+ }
955
+ catch {
956
+ // session_end may not be a supported hook — TTL safety net handles this
957
+ }
928
958
  }
929
- catch {
930
- // session_end may not be a supported hook — TTL safety net handles this
959
+ else {
960
+ api.logger?.info?.('[shieldcortex] interceptor.enabled:false in plugin config — before_tool_call hook not registered');
931
961
  }
932
962
  api.on("llm_input", handleLlmInput, { timeoutMs: 30_000 });
933
963
  api.on("llm_output", handleLlmOutput, { timeoutMs: 30_000 });
@@ -949,19 +979,24 @@ export default {
949
979
  };
950
980
  const interceptorOn = (rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.enabled : undefined) ?? DEFAULT_INTERCEPTOR_CONFIG.enabled;
951
981
  const autoApproved = Array.isArray(guardCfg.autoApprove) ? guardCfg.autoApprove.length : 0;
952
- const guardState = !interceptorOn || !guardCfg.enabled
953
- ? "off"
954
- : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
982
+ const guardState = !_beforeToolCallRegistered
983
+ ? "off (before_tool_call not registered — interceptor disabled in plugin config)"
984
+ : !interceptorOn || !guardCfg.enabled
985
+ ? "off"
986
+ : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
987
+ const hooksLine = _beforeToolCallRegistered
988
+ ? "llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)"
989
+ : "llm_input (scan), llm_output (memory)";
955
990
  return {
956
991
  text: `ShieldCortex v${_version}\n` +
957
- ` Hooks: llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)\n` +
992
+ ` Hooks: ${hooksLine}\n` +
958
993
  ` Action guard: ${guardState}\n` +
959
994
  ` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
960
995
  ` Cloud sync: ${cloud}`,
961
996
  };
962
997
  },
963
998
  });
964
- api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + before_tool_call + /shieldcortex-status)`);
999
+ api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output${_beforeToolCallRegistered ? " + before_tool_call" : ""} + /shieldcortex-status)`);
965
1000
  }
966
1001
  catch (err) {
967
1002
  // Plugin must never block channel startup — warn and bail gracefully
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.14",
3
+ "version": "4.47.15",
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,
package/index.ts CHANGED
@@ -183,6 +183,7 @@ export function __resetConfigStateForTest(): void {
183
183
  _configOverride = null;
184
184
  _lastShieldConfigRef = null;
185
185
  _registered = false;
186
+ _beforeToolCallRegistered = false;
186
187
  }
187
188
 
188
189
  type LlmInputEvent = {
@@ -402,6 +403,13 @@ try {
402
403
  } catch { /* fallback */ }
403
404
 
404
405
  let _registered = false;
406
+ // Whether register() actually attached the before_tool_call hook this session.
407
+ // False when the host config explicitly disables the interceptor — in that
408
+ // case the hook must not exist at all, so the host's approval pipeline can
409
+ // never route a tool call through this plugin (issue #112 follow-up: on
410
+ // unattended Codex agents even a no-op registered hook changes how OpenClaw
411
+ // resolves approvals).
412
+ let _beforeToolCallRegistered = false;
405
413
 
406
414
  function normaliseConfig(raw: unknown): SCConfig {
407
415
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -1089,19 +1097,41 @@ export default {
1089
1097
  }
1090
1098
  }
1091
1099
 
1092
- // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
1093
- // can block or require approval before the selected tool executes.
1094
- api.on('before_tool_call', async (event: TypedBeforeToolCallEvent) => {
1095
- const interceptor = await initInterceptor();
1096
- if (!interceptor) return;
1097
- return handleTypedBeforeToolCall(event, interceptor, api.logger);
1098
- }, { priority: 80, timeoutMs: 30_000 });
1099
-
1100
- // Try to register session_end for cache cleanup
1101
- try {
1102
- api.on('session_end', () => { interceptorReady?.resetSession(); });
1103
- } catch {
1104
- // session_end may not be a supported hook — TTL safety net handles this
1100
+ // #112 follow-up: when the host config (openclaw.json plugin entry)
1101
+ // explicitly disables the interceptor, do NOT register before_tool_call at
1102
+ // all. A registered-but-no-op hook still appears in the host's hook roster
1103
+ // and changes how OpenClaw resolves tool-call approvals for unattended
1104
+ // Codex agents (plugin.approval.waitDecision waits on a decision no one
1105
+ // can give, times out after 120s, and the turn emits no reply). Absent
1106
+ // hook = the host's native approval path is untouched.
1107
+ //
1108
+ // Only the synchronously-available plugin config can gate registration
1109
+ // (the shield config file loads async via the runtime); a shield-file-only
1110
+ // `enabled:false` keeps the lazy no-op behaviour, which returns
1111
+ // immediately and never requests approval — covered by regression tests.
1112
+ // Note: re-enabling the interceptor from openclaw.json requires a gateway
1113
+ // restart, since registration happens once at plugin load.
1114
+ const interceptorDisabledInHostConfig = _configOverride?.interceptor?.enabled === false;
1115
+
1116
+ if (!interceptorDisabledInHostConfig) {
1117
+ // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
1118
+ // can block or require approval before the selected tool executes.
1119
+ api.on('before_tool_call', async (event: TypedBeforeToolCallEvent) => {
1120
+ const interceptor = await initInterceptor();
1121
+ if (!interceptor) return;
1122
+ return handleTypedBeforeToolCall(event, interceptor, api.logger);
1123
+ }, { priority: 80, timeoutMs: 30_000 });
1124
+ _beforeToolCallRegistered = true;
1125
+
1126
+ // Try to register session_end for cache cleanup (only meaningful while
1127
+ // an interceptor can exist)
1128
+ try {
1129
+ api.on('session_end', () => { interceptorReady?.resetSession(); });
1130
+ } catch {
1131
+ // session_end may not be a supported hook — TTL safety net handles this
1132
+ }
1133
+ } else {
1134
+ api.logger?.info?.('[shieldcortex] interceptor.enabled:false in plugin config — before_tool_call hook not registered');
1105
1135
  }
1106
1136
 
1107
1137
  api.on("llm_input", handleLlmInput, { timeoutMs: 30_000 });
@@ -1125,13 +1155,18 @@ export default {
1125
1155
  };
1126
1156
  const interceptorOn = (rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.enabled : undefined) ?? DEFAULT_INTERCEPTOR_CONFIG.enabled;
1127
1157
  const autoApproved = Array.isArray(guardCfg.autoApprove) ? guardCfg.autoApprove.length : 0;
1128
- const guardState = !interceptorOn || !guardCfg.enabled
1129
- ? "off"
1130
- : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
1158
+ const guardState = !_beforeToolCallRegistered
1159
+ ? "off (before_tool_call not registered — interceptor disabled in plugin config)"
1160
+ : !interceptorOn || !guardCfg.enabled
1161
+ ? "off"
1162
+ : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
1163
+ const hooksLine = _beforeToolCallRegistered
1164
+ ? "llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)"
1165
+ : "llm_input (scan), llm_output (memory)";
1131
1166
  return {
1132
1167
  text:
1133
1168
  `ShieldCortex v${_version}\n` +
1134
- ` Hooks: llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)\n` +
1169
+ ` Hooks: ${hooksLine}\n` +
1135
1170
  ` Action guard: ${guardState}\n` +
1136
1171
  ` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
1137
1172
  ` Cloud sync: ${cloud}`,
@@ -1139,7 +1174,7 @@ export default {
1139
1174
  },
1140
1175
  });
1141
1176
 
1142
- api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + before_tool_call + /shieldcortex-status)`);
1177
+ api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output${_beforeToolCallRegistered ? " + before_tool_call" : ""} + /shieldcortex-status)`);
1143
1178
  } catch (err) {
1144
1179
  // Plugin must never block channel startup — warn and bail gracefully
1145
1180
  const msg = err instanceof Error ? err.message : String(err);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.14",
3
+ "version": "4.47.15",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.14",
3
+ "version": "4.47.15",
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",