@clawdstrike/openclaw 0.1.0 → 0.1.2

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 (100) hide show
  1. package/README.md +4 -2
  2. package/clawdstrike-security.js +1 -0
  3. package/dist/audit/adapter-logger.d.ts +24 -0
  4. package/dist/audit/adapter-logger.d.ts.map +1 -0
  5. package/dist/audit/adapter-logger.js +42 -0
  6. package/dist/audit/adapter-logger.js.map +1 -0
  7. package/dist/classification.d.ts +41 -0
  8. package/dist/classification.d.ts.map +1 -0
  9. package/dist/classification.js +102 -0
  10. package/dist/classification.js.map +1 -0
  11. package/dist/cli/commands/policy.js +1 -1
  12. package/dist/cli/commands/policy.js.map +1 -1
  13. package/dist/e2e/openclaw-e2e.js +3 -3
  14. package/dist/e2e/openclaw-e2e.js.map +1 -1
  15. package/dist/engine-holder.d.ts +28 -0
  16. package/dist/engine-holder.d.ts.map +1 -0
  17. package/dist/engine-holder.js +38 -0
  18. package/dist/engine-holder.js.map +1 -0
  19. package/dist/guards/egress.d.ts.map +1 -1
  20. package/dist/guards/egress.js +20 -1
  21. package/dist/guards/egress.js.map +1 -1
  22. package/dist/guards/forbidden-path.d.ts.map +1 -1
  23. package/dist/guards/forbidden-path.js +6 -0
  24. package/dist/guards/forbidden-path.js.map +1 -1
  25. package/dist/guards/secret-leak.d.ts.map +1 -1
  26. package/dist/guards/secret-leak.js +21 -0
  27. package/dist/guards/secret-leak.js.map +1 -1
  28. package/dist/hooks/agent-bootstrap/handler.d.ts +4 -0
  29. package/dist/hooks/agent-bootstrap/handler.d.ts.map +1 -1
  30. package/dist/hooks/agent-bootstrap/handler.js +7 -7
  31. package/dist/hooks/agent-bootstrap/handler.js.map +1 -1
  32. package/dist/hooks/approval-state.d.ts +31 -0
  33. package/dist/hooks/approval-state.d.ts.map +1 -0
  34. package/dist/hooks/approval-state.js +189 -0
  35. package/dist/hooks/approval-state.js.map +1 -0
  36. package/dist/hooks/approval-utils.d.ts +5 -0
  37. package/dist/hooks/approval-utils.d.ts.map +1 -0
  38. package/dist/hooks/approval-utils.js +77 -0
  39. package/dist/hooks/approval-utils.js.map +1 -0
  40. package/dist/hooks/audit-logger/handler.d.ts +4 -0
  41. package/dist/hooks/audit-logger/handler.d.ts.map +1 -1
  42. package/dist/hooks/audit-logger/handler.js +4 -0
  43. package/dist/hooks/audit-logger/handler.js.map +1 -1
  44. package/dist/hooks/cua-bridge/handler.d.ts +57 -0
  45. package/dist/hooks/cua-bridge/handler.d.ts.map +1 -0
  46. package/dist/hooks/cua-bridge/handler.js +369 -0
  47. package/dist/hooks/cua-bridge/handler.js.map +1 -0
  48. package/dist/hooks/tool-guard/handler.d.ts +17 -2
  49. package/dist/hooks/tool-guard/handler.d.ts.map +1 -1
  50. package/dist/hooks/tool-guard/handler.js +200 -75
  51. package/dist/hooks/tool-guard/handler.js.map +1 -1
  52. package/dist/hooks/tool-preflight/handler.d.ts +34 -0
  53. package/dist/hooks/tool-preflight/handler.d.ts.map +1 -0
  54. package/dist/hooks/tool-preflight/handler.js +426 -0
  55. package/dist/hooks/tool-preflight/handler.js.map +1 -0
  56. package/dist/index.d.ts +8 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +9 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/openclaw-adapter.d.ts +48 -0
  61. package/dist/openclaw-adapter.d.ts.map +1 -0
  62. package/dist/openclaw-adapter.js +81 -0
  63. package/dist/openclaw-adapter.js.map +1 -0
  64. package/dist/plugin.d.ts +40 -1
  65. package/dist/plugin.d.ts.map +1 -1
  66. package/dist/plugin.js +125 -32
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/policy/engine.d.ts +5 -0
  69. package/dist/policy/engine.d.ts.map +1 -1
  70. package/dist/policy/engine.js +580 -84
  71. package/dist/policy/engine.js.map +1 -1
  72. package/dist/policy/loader.js +57 -0
  73. package/dist/policy/loader.js.map +1 -1
  74. package/dist/policy/validator.d.ts.map +1 -1
  75. package/dist/policy/validator.js +97 -3
  76. package/dist/policy/validator.js.map +1 -1
  77. package/dist/receipt/signer.d.ts +42 -0
  78. package/dist/receipt/signer.d.ts.map +1 -0
  79. package/dist/receipt/signer.js +134 -0
  80. package/dist/receipt/signer.js.map +1 -0
  81. package/dist/receipt/types.d.ts +50 -0
  82. package/dist/receipt/types.d.ts.map +1 -0
  83. package/dist/receipt/types.js +9 -0
  84. package/dist/receipt/types.js.map +1 -0
  85. package/dist/security-prompt.js +1 -1
  86. package/dist/tools/policy-check.d.ts +2 -2
  87. package/dist/tools/policy-check.d.ts.map +1 -1
  88. package/dist/tools/policy-check.js +4 -7
  89. package/dist/tools/policy-check.js.map +1 -1
  90. package/dist/translator/openclaw-translator.d.ts +31 -0
  91. package/dist/translator/openclaw-translator.d.ts.map +1 -0
  92. package/dist/translator/openclaw-translator.js +314 -0
  93. package/dist/translator/openclaw-translator.js.map +1 -0
  94. package/dist/types.d.ts +86 -170
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js +4 -0
  97. package/dist/types.js.map +1 -1
  98. package/package.json +5 -3
  99. package/rulesets/ai-agent-minimal.yaml +25 -0
  100. package/rulesets/ai-agent.yaml +25 -0
@@ -1,5 +1,6 @@
1
1
  import { homedir } from 'node:os';
2
2
  import path from 'node:path';
3
+ import { parseNetworkTarget } from '@clawdstrike/adapter-core';
3
4
  import { createPolicyEngineFromPolicy } from '@clawdstrike/policy';
4
5
  import { mergeConfig } from '../config.js';
5
6
  import { EgressGuard, ForbiddenPathGuard, PatchIntegrityGuard, SecretLeakGuard } from '../guards/index.js';
@@ -12,6 +13,176 @@ function expandHome(p) {
12
13
  function normalizePathForPrefix(p) {
13
14
  return path.resolve(expandHome(p));
14
15
  }
16
+ function cleanPathToken(t) {
17
+ return t.trim().replace(/^[("'`]+/, '').replace(/[)"'`;,\]}]+$/, '');
18
+ }
19
+ function isRedirectionOp(t) {
20
+ return t === '>' || t === '>>' || t === '1>' || t === '1>>' || t === '2>' || t === '2>>' || t === '<' || t === '<<';
21
+ }
22
+ function splitInlineRedirection(t) {
23
+ // Support forms like ">/path", "2>>/path", "<input".
24
+ const m = t.match(/^(?:\d)?(?:>>|>)\s*(.+)$/);
25
+ if (m?.[1])
26
+ return m[1];
27
+ const mi = t.match(/^(?:<<|<)\s*(.+)$/);
28
+ if (mi?.[1])
29
+ return mi[1];
30
+ return null;
31
+ }
32
+ function looksLikePathToken(t) {
33
+ if (!t)
34
+ return false;
35
+ if (t.includes('://'))
36
+ return false;
37
+ if (t.startsWith('/') || t.startsWith('~') || t.startsWith('./') || t.startsWith('../'))
38
+ return true;
39
+ if (t === '.env' || t.startsWith('.env.'))
40
+ return true;
41
+ if (t.includes('/.ssh/') || t.includes('/.aws/') || t.includes('/.gnupg/') || t.includes('/.kube/'))
42
+ return true;
43
+ return false;
44
+ }
45
+ const WRITE_PATH_FLAG_NAMES = new Set([
46
+ // Common output flags
47
+ 'o',
48
+ 'out',
49
+ 'output',
50
+ 'outfile',
51
+ 'output-file',
52
+ // Common log file flags
53
+ 'log-file',
54
+ 'logfile',
55
+ 'log-path',
56
+ 'logpath',
57
+ ]);
58
+ function isWritePathFlagToken(t) {
59
+ if (!t)
60
+ return false;
61
+ if (!t.startsWith('-'))
62
+ return false;
63
+ const normalized = t.replace(/^-+/, '').toLowerCase().replace(/_/g, '-');
64
+ return WRITE_PATH_FLAG_NAMES.has(normalized);
65
+ }
66
+ function extractCommandPathCandidates(command, args) {
67
+ const tokens = [command, ...args].map((t) => String(t ?? '')).filter(Boolean);
68
+ const reads = [];
69
+ const writes = [];
70
+ for (let i = 0; i < tokens.length; i++) {
71
+ const t = tokens[i];
72
+ // Redirection operators: treat as write/read targets.
73
+ if (isRedirectionOp(t)) {
74
+ const next = tokens[i + 1];
75
+ if (typeof next === 'string' && next.length > 0) {
76
+ const cleaned = cleanPathToken(next);
77
+ if (cleaned) {
78
+ if (t.startsWith('>') || t === '>' || t === '>>' || t === '1>' || t === '1>>' || t === '2>' || t === '2>>') {
79
+ writes.push(cleaned);
80
+ }
81
+ else {
82
+ reads.push(cleaned);
83
+ }
84
+ }
85
+ }
86
+ continue;
87
+ }
88
+ const inline = splitInlineRedirection(t);
89
+ if (inline) {
90
+ const cleaned = cleanPathToken(inline);
91
+ if (cleaned) {
92
+ if (t.includes('>'))
93
+ writes.push(cleaned);
94
+ else
95
+ reads.push(cleaned);
96
+ }
97
+ continue;
98
+ }
99
+ // Flags like --output /path or -o /path (write targets)
100
+ if (isWritePathFlagToken(t)) {
101
+ const next = tokens[i + 1];
102
+ if (typeof next === 'string' && next.length > 0) {
103
+ const cleaned = cleanPathToken(next);
104
+ if (looksLikePathToken(cleaned)) {
105
+ writes.push(cleaned);
106
+ i += 1;
107
+ continue;
108
+ }
109
+ }
110
+ }
111
+ // Flags like --output=/path
112
+ const eq = t.indexOf('=');
113
+ if (eq > 0) {
114
+ const lhs = t.slice(0, eq);
115
+ const rhs = cleanPathToken(t.slice(eq + 1));
116
+ if (looksLikePathToken(rhs)) {
117
+ if (isWritePathFlagToken(lhs))
118
+ writes.push(rhs);
119
+ else
120
+ reads.push(rhs);
121
+ }
122
+ }
123
+ const cleanedToken = cleanPathToken(t);
124
+ if (looksLikePathToken(cleanedToken)) {
125
+ reads.push(cleanedToken);
126
+ }
127
+ }
128
+ const uniq = (xs) => Array.from(new Set(xs.filter(Boolean)));
129
+ return { reads: uniq(reads), writes: uniq(writes) };
130
+ }
131
+ const POLICY_REASON_CODES = {
132
+ POLICY_DENY: 'ADC_POLICY_DENY',
133
+ POLICY_WARN: 'ADC_POLICY_WARN',
134
+ GUARD_ERROR: 'ADC_GUARD_ERROR',
135
+ CUA_MALFORMED_EVENT: 'OCLAW_CUA_MALFORMED_EVENT',
136
+ CUA_COMPUTER_USE_CONFIG_MISSING: 'OCLAW_CUA_COMPUTER_USE_CONFIG_MISSING',
137
+ CUA_COMPUTER_USE_DISABLED: 'OCLAW_CUA_COMPUTER_USE_DISABLED',
138
+ CUA_ACTION_NOT_ALLOWED: 'OCLAW_CUA_ACTION_NOT_ALLOWED',
139
+ CUA_MODE_UNSUPPORTED: 'OCLAW_CUA_MODE_UNSUPPORTED',
140
+ CUA_CONNECT_METADATA_MISSING: 'OCLAW_CUA_CONNECT_METADATA_MISSING',
141
+ CUA_SIDE_CHANNEL_CONFIG_MISSING: 'OCLAW_CUA_SIDE_CHANNEL_CONFIG_MISSING',
142
+ CUA_SIDE_CHANNEL_DISABLED: 'OCLAW_CUA_SIDE_CHANNEL_DISABLED',
143
+ CUA_SIDE_CHANNEL_POLICY_DENY: 'OCLAW_CUA_SIDE_CHANNEL_POLICY_DENY',
144
+ CUA_TRANSFER_SIZE_CONFIG_INVALID: 'OCLAW_CUA_TRANSFER_SIZE_CONFIG_INVALID',
145
+ CUA_TRANSFER_SIZE_MISSING: 'OCLAW_CUA_TRANSFER_SIZE_MISSING',
146
+ CUA_TRANSFER_SIZE_EXCEEDED: 'OCLAW_CUA_TRANSFER_SIZE_EXCEEDED',
147
+ CUA_INPUT_CONFIG_MISSING: 'OCLAW_CUA_INPUT_CONFIG_MISSING',
148
+ CUA_INPUT_DISABLED: 'OCLAW_CUA_INPUT_DISABLED',
149
+ CUA_INPUT_TYPE_MISSING: 'OCLAW_CUA_INPUT_TYPE_MISSING',
150
+ CUA_INPUT_TYPE_NOT_ALLOWED: 'OCLAW_CUA_INPUT_TYPE_NOT_ALLOWED',
151
+ CUA_POSTCONDITION_PROBE_REQUIRED: 'OCLAW_CUA_POSTCONDITION_PROBE_REQUIRED',
152
+ FILESYSTEM_WRITE_ROOT_DENY: 'OCLAW_FILESYSTEM_WRITE_ROOT_DENY',
153
+ TOOL_DENIED: 'OCLAW_TOOL_DENIED',
154
+ TOOL_NOT_ALLOWLISTED: 'OCLAW_TOOL_NOT_ALLOWLISTED',
155
+ };
156
+ function denyDecision(reason_code, reason, guard, severity = 'high') {
157
+ return {
158
+ status: 'deny',
159
+ reason_code,
160
+ reason,
161
+ message: reason,
162
+ ...(guard !== undefined && { guard }),
163
+ ...(severity !== undefined && { severity }),
164
+ };
165
+ }
166
+ function warnDecision(reason_code, reason, guard, severity = 'medium') {
167
+ return {
168
+ status: 'warn',
169
+ reason_code,
170
+ reason,
171
+ message: reason,
172
+ ...(guard !== undefined && { guard }),
173
+ ...(severity !== undefined && { severity }),
174
+ };
175
+ }
176
+ function ensureReasonCode(decision) {
177
+ if (decision.status === 'allow')
178
+ return decision;
179
+ if (typeof decision.reason_code === 'string' && decision.reason_code.trim().length > 0)
180
+ return decision;
181
+ return {
182
+ ...decision,
183
+ reason_code: decision.status === 'warn' ? POLICY_REASON_CODES.POLICY_WARN : POLICY_REASON_CODES.GUARD_ERROR,
184
+ };
185
+ }
15
186
  export class PolicyEngine {
16
187
  config;
17
188
  policy;
@@ -68,13 +239,11 @@ export class PolicyEngine {
68
239
  async evaluate(event) {
69
240
  const base = this.evaluateDeterministic(event);
70
241
  // Fail fast on deterministic violations to avoid unnecessary external calls.
71
- const baseDenied = base.status === 'deny' || base.denied;
72
- const baseWarn = base.status === 'warn' || base.warn;
73
- if (baseDenied || baseWarn) {
242
+ if (base.status === 'deny' || base.status === 'warn') {
74
243
  return this.applyMode(base, this.config.mode);
75
244
  }
76
245
  if (this.threatIntelEngine) {
77
- const ti = await this.threatIntelEngine.evaluate(toCanonicalEvent(event));
246
+ const ti = await this.threatIntelEngine.evaluate(event);
78
247
  const tiApplied = this.applyOnViolation(ti);
79
248
  const combined = combineDecisions(base, tiApplied);
80
249
  return this.applyMode(combined, this.config.mode);
@@ -83,25 +252,58 @@ export class PolicyEngine {
83
252
  }
84
253
  applyMode(result, mode) {
85
254
  if (mode === 'audit') {
86
- return { status: 'allow', allowed: true, denied: false, warn: false };
87
- }
88
- const isDenied = result.status === 'deny' || result.denied;
89
- if (mode === 'advisory' && isDenied) {
90
255
  return {
91
- status: 'warn',
92
- allowed: true,
93
- denied: false,
94
- warn: true,
256
+ status: 'allow',
257
+ reason_code: result.reason_code,
95
258
  reason: result.reason,
259
+ message: `[audit] Original decision: ${result.status} — ${result.message ?? result.reason ?? 'no reason'}`,
96
260
  guard: result.guard,
97
261
  severity: result.severity,
98
- message: result.reason,
99
262
  };
100
263
  }
101
- return result;
264
+ if (mode === 'advisory' && result.status === 'deny') {
265
+ return ensureReasonCode(warnDecision(result.reason_code, result.reason ?? result.message ?? 'policy deny converted to advisory warning', result.guard, result.severity ?? 'medium'));
266
+ }
267
+ return ensureReasonCode(result);
268
+ }
269
+ getExpectedDataType(eventType) {
270
+ switch (eventType) {
271
+ case 'file_read':
272
+ case 'file_write':
273
+ return 'file';
274
+ case 'command_exec':
275
+ return 'command';
276
+ case 'network_egress':
277
+ return 'network';
278
+ case 'tool_call':
279
+ return 'tool';
280
+ case 'patch_apply':
281
+ return 'patch';
282
+ case 'secret_access':
283
+ return 'secret';
284
+ case 'custom':
285
+ return undefined;
286
+ default:
287
+ // CUA event types (starting with 'remote.' or 'input.')
288
+ if (eventType.startsWith('remote.') || eventType.startsWith('input.')) {
289
+ return 'cua';
290
+ }
291
+ return undefined;
292
+ }
102
293
  }
103
294
  evaluateDeterministic(event) {
104
- const allowed = { status: 'allow', allowed: true, denied: false, warn: false };
295
+ const allowed = { status: 'allow' };
296
+ // Validate eventType/data.type consistency to prevent guard bypass
297
+ const expectedDataType = this.getExpectedDataType(event.eventType);
298
+ if (expectedDataType && event.data.type !== expectedDataType) {
299
+ return {
300
+ status: 'deny',
301
+ reason_code: 'event_type_mismatch',
302
+ reason: `Event type "${event.eventType}" requires data.type "${expectedDataType}" but got "${event.data.type}"`,
303
+ guard: 'policy_engine',
304
+ severity: 'critical',
305
+ };
306
+ }
105
307
  switch (event.eventType) {
106
308
  case 'file_read':
107
309
  case 'file_write':
@@ -114,20 +316,159 @@ export class PolicyEngine {
114
316
  return this.checkToolCall(event);
115
317
  case 'patch_apply':
116
318
  return this.checkPatch(event);
319
+ case 'remote.session.connect':
320
+ case 'remote.session.disconnect':
321
+ case 'remote.session.reconnect':
322
+ case 'input.inject':
323
+ case 'remote.clipboard':
324
+ case 'remote.file_transfer':
325
+ case 'remote.audio':
326
+ case 'remote.drive_mapping':
327
+ case 'remote.printing':
328
+ case 'remote.session_share':
329
+ return this.checkCua(event);
117
330
  default:
118
331
  return allowed;
119
332
  }
120
333
  }
334
+ checkCua(event) {
335
+ if (event.data.type !== 'cua') {
336
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_MALFORMED_EVENT, `Malformed CUA event payload for ${event.eventType}: data.type must be 'cua'`, 'computer_use', 'high'));
337
+ }
338
+ const cuaData = event.data;
339
+ const connectEgressDecision = this.checkCuaConnectEgress(event, cuaData);
340
+ if (connectEgressDecision.status === 'deny' || connectEgressDecision.status === 'warn') {
341
+ return connectEgressDecision;
342
+ }
343
+ const computerUse = this.policy.guards?.computer_use;
344
+ if (!computerUse) {
345
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_COMPUTER_USE_CONFIG_MISSING, `CUA action '${event.eventType}' denied: missing guards.computer_use policy config`, 'computer_use', 'high'));
346
+ }
347
+ if (computerUse.enabled === false) {
348
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_COMPUTER_USE_DISABLED, `CUA action '${event.eventType}' denied: computer_use guard is disabled`, 'computer_use', 'high'));
349
+ }
350
+ const mode = computerUse.mode ?? 'guardrail';
351
+ const allowedActions = normalizeStringList(computerUse.allowed_actions);
352
+ const actionAllowed = allowedActions.length === 0 || allowedActions.includes(event.eventType);
353
+ if (!actionAllowed) {
354
+ const reason = `CUA action '${event.eventType}' is not listed in guards.computer_use.allowed_actions`;
355
+ if (mode === 'observe' || mode === 'guardrail') {
356
+ return warnDecision(POLICY_REASON_CODES.CUA_ACTION_NOT_ALLOWED, reason, 'computer_use', 'medium');
357
+ }
358
+ if (mode !== 'fail_closed') {
359
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_MODE_UNSUPPORTED, `CUA action '${event.eventType}' denied: unsupported computer_use mode '${mode}'`, 'computer_use', 'high'));
360
+ }
361
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_ACTION_NOT_ALLOWED, reason, 'computer_use', 'high'));
362
+ }
363
+ const sideChannelDecision = this.checkRemoteDesktopSideChannel(event, cuaData);
364
+ if (sideChannelDecision.status === 'deny' || sideChannelDecision.status === 'warn') {
365
+ return sideChannelDecision;
366
+ }
367
+ const inputDecision = this.checkInputInjectionCapability(event, cuaData);
368
+ if (inputDecision.status === 'deny' || inputDecision.status === 'warn') {
369
+ return inputDecision;
370
+ }
371
+ return { status: 'allow' };
372
+ }
373
+ checkCuaConnectEgress(event, data) {
374
+ if (event.eventType !== 'remote.session.connect') {
375
+ return { status: 'allow' };
376
+ }
377
+ if (!this.config.guards.egress) {
378
+ return { status: 'allow' };
379
+ }
380
+ const target = extractCuaNetworkTarget(data);
381
+ if (!target) {
382
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_CONNECT_METADATA_MISSING, "CUA connect action denied: missing destination host/url metadata required for egress evaluation", 'egress', 'high'));
383
+ }
384
+ const egressEvent = {
385
+ eventId: `${event.eventId}:cua-connect-egress`,
386
+ eventType: 'network_egress',
387
+ timestamp: event.timestamp,
388
+ sessionId: event.sessionId,
389
+ data: {
390
+ type: 'network',
391
+ host: target.host,
392
+ port: target.port,
393
+ ...(target.protocol ? { protocol: target.protocol } : {}),
394
+ ...(target.url ? { url: target.url } : {}),
395
+ },
396
+ metadata: {
397
+ ...(event.metadata ?? {}),
398
+ derivedFrom: event.eventType,
399
+ },
400
+ };
401
+ return this.checkEgress(egressEvent);
402
+ }
403
+ checkRemoteDesktopSideChannel(event, data) {
404
+ const sideChannelFlag = eventTypeToSideChannelFlag(event.eventType);
405
+ if (!sideChannelFlag) {
406
+ return { status: 'allow' };
407
+ }
408
+ const cfg = this.policy.guards?.remote_desktop_side_channel;
409
+ if (!cfg) {
410
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_CONFIG_MISSING, `CUA side-channel action '${event.eventType}' denied: missing guards.remote_desktop_side_channel policy config`, 'remote_desktop_side_channel', 'high'));
411
+ }
412
+ if (cfg.enabled === false) {
413
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_DISABLED, `CUA side-channel action '${event.eventType}' denied: remote_desktop_side_channel guard is disabled`, 'remote_desktop_side_channel', 'high'));
414
+ }
415
+ if (cfg[sideChannelFlag] === false) {
416
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_POLICY_DENY, `CUA side-channel action '${event.eventType}' denied by policy`, 'remote_desktop_side_channel', 'high'));
417
+ }
418
+ if (event.eventType === 'remote.file_transfer') {
419
+ const maxBytes = cfg.max_transfer_size_bytes;
420
+ if (maxBytes !== undefined) {
421
+ if (typeof maxBytes !== 'number' || !Number.isFinite(maxBytes) || maxBytes < 0) {
422
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_CONFIG_INVALID, `CUA file transfer denied: invalid max_transfer_size_bytes '${String(maxBytes)}'`, 'remote_desktop_side_channel', 'high'));
423
+ }
424
+ const transferSize = extractTransferSize(data);
425
+ if (transferSize === null) {
426
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_MISSING, 'CUA file transfer denied: missing required transfer_size metadata', 'remote_desktop_side_channel', 'high'));
427
+ }
428
+ if (transferSize > maxBytes) {
429
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_EXCEEDED, `CUA file transfer size ${transferSize} exceeds max_transfer_size_bytes ${maxBytes}`, 'remote_desktop_side_channel', 'high'));
430
+ }
431
+ }
432
+ }
433
+ return { status: 'allow' };
434
+ }
435
+ checkInputInjectionCapability(event, data) {
436
+ if (event.eventType !== 'input.inject') {
437
+ return { status: 'allow' };
438
+ }
439
+ const cfg = this.policy.guards?.input_injection_capability;
440
+ if (!cfg) {
441
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_CONFIG_MISSING, `CUA input action '${event.eventType}' denied: missing guards.input_injection_capability policy config`, 'input_injection_capability', 'high'));
442
+ }
443
+ if (cfg.enabled === false) {
444
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_DISABLED, `CUA input action '${event.eventType}' denied: input_injection_capability guard is disabled`, 'input_injection_capability', 'high'));
445
+ }
446
+ const allowedInputTypes = normalizeStringList(cfg.allowed_input_types);
447
+ const inputType = extractInputType(data);
448
+ if (allowedInputTypes.length > 0) {
449
+ if (!inputType) {
450
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_TYPE_MISSING, "CUA input action denied: missing required 'input_type'", 'input_injection_capability', 'high'));
451
+ }
452
+ if (!allowedInputTypes.includes(inputType)) {
453
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_TYPE_NOT_ALLOWED, `CUA input action denied: input_type '${inputType}' is not allowed`, 'input_injection_capability', 'high'));
454
+ }
455
+ }
456
+ if (cfg.require_postcondition_probe === true) {
457
+ const probeHash = data.postconditionProbeHash;
458
+ if (typeof probeHash !== 'string' || probeHash.trim().length === 0) {
459
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_POSTCONDITION_PROBE_REQUIRED, 'CUA input action denied: postcondition probe hash is required', 'input_injection_capability', 'high'));
460
+ }
461
+ }
462
+ return { status: 'allow' };
463
+ }
121
464
  checkFilesystem(event) {
122
465
  if (!this.config.guards.forbidden_path) {
123
- return { status: 'allow', allowed: true, denied: false, warn: false };
466
+ return { status: 'allow' };
124
467
  }
125
468
  // First, enforce forbidden path patterns.
126
469
  const forbidden = this.forbiddenPathGuard.checkSync(event, this.policy);
127
470
  const mapped = this.guardResultToDecision(forbidden);
128
- const mappedDenied = mapped.status === 'deny' || mapped.denied;
129
- const mappedWarn = mapped.status === 'warn' || mapped.warn;
130
- if (mappedDenied || mappedWarn) {
471
+ if (mapped.status === 'deny' || mapped.status === 'warn') {
131
472
  return this.applyOnViolation(mapped);
132
473
  }
133
474
  // Then, enforce write roots if configured.
@@ -140,31 +481,62 @@ export class PolicyEngine {
140
481
  return filePath === rootPath || filePath.startsWith(rootPath + path.sep);
141
482
  });
142
483
  if (!ok) {
143
- return this.applyOnViolation({
144
- status: 'deny',
145
- allowed: false,
146
- denied: true,
147
- warn: false,
148
- reason: 'Write path not in allowed roots',
149
- guard: 'forbidden_path',
150
- severity: 'high',
151
- });
484
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.FILESYSTEM_WRITE_ROOT_DENY, 'Write path not in allowed roots', 'forbidden_path', 'high'));
152
485
  }
153
486
  }
154
487
  }
155
- return { status: 'allow', allowed: true, denied: false, warn: false };
488
+ return { status: 'allow' };
156
489
  }
157
490
  checkEgress(event) {
158
491
  if (!this.config.guards.egress) {
159
- return { status: 'allow', allowed: true, denied: false, warn: false };
492
+ return { status: 'allow' };
160
493
  }
161
494
  const res = this.egressGuard.checkSync(event, this.policy);
162
495
  const mapped = this.guardResultToDecision(res);
163
496
  return this.applyOnViolation(mapped);
164
497
  }
165
498
  checkExecution(event) {
499
+ // Defense in depth: shell/command execution can still touch the filesystem.
500
+ // Best-effort extract path-like tokens (including redirections) and run them through the
501
+ // filesystem policy checks (forbidden paths + allowed write roots).
502
+ if (this.config.guards.forbidden_path && event.data.type === 'command') {
503
+ const { reads, writes } = extractCommandPathCandidates(event.data.command, event.data.args);
504
+ const maxChecks = 64;
505
+ let checks = 0;
506
+ // Check likely writes first so allowed_write_roots is enforced.
507
+ for (const p of writes) {
508
+ if (checks++ >= maxChecks)
509
+ break;
510
+ const synthetic = {
511
+ eventId: `${event.eventId}:cmdwrite:${checks}`,
512
+ eventType: 'file_write',
513
+ timestamp: event.timestamp,
514
+ sessionId: event.sessionId,
515
+ data: { type: 'file', path: p, operation: 'write' },
516
+ metadata: { ...event.metadata, derivedFrom: 'command_exec' },
517
+ };
518
+ const d = this.checkFilesystem(synthetic);
519
+ if (d.status === 'deny' || d.status === 'warn')
520
+ return d;
521
+ }
522
+ for (const p of reads) {
523
+ if (checks++ >= maxChecks)
524
+ break;
525
+ const synthetic = {
526
+ eventId: `${event.eventId}:cmdread:${checks}`,
527
+ eventType: 'file_read',
528
+ timestamp: event.timestamp,
529
+ sessionId: event.sessionId,
530
+ data: { type: 'file', path: p, operation: 'read' },
531
+ metadata: { ...event.metadata, derivedFrom: 'command_exec' },
532
+ };
533
+ const d = this.checkFilesystem(synthetic);
534
+ if (d.status === 'deny' || d.status === 'warn')
535
+ return d;
536
+ }
537
+ }
166
538
  if (!this.config.guards.patch_integrity) {
167
- return { status: 'allow', allowed: true, denied: false, warn: false };
539
+ return { status: 'allow' };
168
540
  }
169
541
  const res = this.patchIntegrityGuard.checkSync(event, this.policy);
170
542
  const mapped = this.guardResultToDecision(res);
@@ -175,33 +547,37 @@ export class PolicyEngine {
175
547
  if (event.data.type === 'tool') {
176
548
  const tools = this.policy.tools;
177
549
  const toolName = event.data.toolName.toLowerCase();
178
- const denied = tools?.denied?.map((x) => x.toLowerCase()) ?? [];
179
- if (denied.includes(toolName)) {
180
- return this.applyOnViolation({
181
- status: 'deny',
182
- allowed: false,
183
- denied: true,
184
- warn: false,
185
- reason: `Tool '${event.data.toolName}' is denied by policy`,
186
- guard: 'mcp_tool',
187
- severity: 'high',
188
- });
550
+ const deniedTools = tools?.denied?.map((x) => x.toLowerCase()) ?? [];
551
+ if (deniedTools.includes(toolName)) {
552
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.TOOL_DENIED, `Tool '${event.data.toolName}' is denied by policy`, 'mcp_tool', 'high'));
189
553
  }
190
- const allowed = tools?.allowed?.map((x) => x.toLowerCase()) ?? [];
191
- if (allowed.length > 0 && !allowed.includes(toolName)) {
192
- return this.applyOnViolation({
193
- status: 'deny',
194
- allowed: false,
195
- denied: true,
196
- warn: false,
197
- reason: `Tool '${event.data.toolName}' is not in allowed tool list`,
198
- guard: 'mcp_tool',
199
- severity: 'high',
200
- });
554
+ const allowedTools = tools?.allowed?.map((x) => x.toLowerCase()) ?? [];
555
+ if (allowedTools.length > 0 && !allowedTools.includes(toolName)) {
556
+ return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.TOOL_NOT_ALLOWLISTED, `Tool '${event.data.toolName}' is not in allowed tool list`, 'mcp_tool', 'high'));
557
+ }
558
+ }
559
+ // Also check forbidden paths in tool parameters (defense in depth).
560
+ if (this.config.guards.forbidden_path && event.data.type === 'tool') {
561
+ const params = event.data.parameters ?? {};
562
+ const pathKeys = ['path', 'file', 'file_path', 'filepath', 'filename', 'target'];
563
+ for (const key of pathKeys) {
564
+ const val = params[key];
565
+ if (typeof val === 'string' && val.length > 0) {
566
+ const pathEvent = {
567
+ ...event,
568
+ eventType: 'file_write',
569
+ data: { type: 'file', path: val, operation: 'write' },
570
+ };
571
+ const pathCheck = this.forbiddenPathGuard.checkSync(pathEvent, this.policy);
572
+ const pathDecision = this.guardResultToDecision(pathCheck);
573
+ if (pathDecision.status === 'deny' || pathDecision.status === 'warn') {
574
+ return this.applyOnViolation(pathDecision);
575
+ }
576
+ }
201
577
  }
202
578
  }
203
579
  if (!this.config.guards.secret_leak) {
204
- return { status: 'allow', allowed: true, denied: false, warn: false };
580
+ return { status: 'allow' };
205
581
  }
206
582
  const res = this.secretLeakGuard.checkSync(event, this.policy);
207
583
  const mapped = this.guardResultToDecision(res);
@@ -212,48 +588,37 @@ export class PolicyEngine {
212
588
  const r1 = this.patchIntegrityGuard.checkSync(event, this.policy);
213
589
  const mapped1 = this.guardResultToDecision(r1);
214
590
  const applied1 = this.applyOnViolation(mapped1);
215
- const applied1Denied = applied1.status === 'deny' || applied1.denied;
216
- const applied1Warn = applied1.status === 'warn' || applied1.warn;
217
- if (applied1Denied || applied1Warn)
591
+ if (applied1.status === 'deny' || applied1.status === 'warn')
218
592
  return applied1;
219
593
  }
220
594
  if (this.config.guards.secret_leak) {
221
595
  const r2 = this.secretLeakGuard.checkSync(event, this.policy);
222
596
  const mapped2 = this.guardResultToDecision(r2);
223
597
  const applied2 = this.applyOnViolation(mapped2);
224
- const applied2Denied = applied2.status === 'deny' || applied2.denied;
225
- const applied2Warn = applied2.status === 'warn' || applied2.warn;
226
- if (applied2Denied || applied2Warn)
598
+ if (applied2.status === 'deny' || applied2.status === 'warn')
227
599
  return applied2;
228
600
  }
229
- return { status: 'allow', allowed: true, denied: false, warn: false };
601
+ return { status: 'allow' };
230
602
  }
231
603
  applyOnViolation(decision) {
232
604
  const action = this.policy.on_violation;
233
- const isDenied = decision.status === 'deny' || decision.denied;
234
- if (!isDenied)
605
+ if (decision.status !== 'deny')
235
606
  return decision;
236
607
  if (action === 'warn') {
237
- return {
238
- status: 'warn',
239
- allowed: true,
240
- denied: false,
241
- warn: true,
242
- reason: decision.reason,
243
- guard: decision.guard,
244
- severity: decision.severity,
245
- message: decision.reason,
246
- };
608
+ return warnDecision(decision.reason_code, decision.reason ?? decision.message ?? 'Policy violation downgraded to warning', decision.guard, decision.severity ?? 'medium');
609
+ }
610
+ if (action && action !== 'cancel') {
611
+ console.warn(`[clawdstrike] Unhandled on_violation action: "${action}" — treating as deny`);
247
612
  }
248
613
  return decision;
249
614
  }
250
615
  guardResultToDecision(result) {
251
616
  if (result.status === 'allow')
252
- return { status: 'allow', allowed: true, denied: false, warn: false };
617
+ return { status: 'allow' };
253
618
  if (result.status === 'warn') {
254
- return { status: 'warn', allowed: true, denied: false, warn: true, reason: result.reason, guard: result.guard, message: result.reason };
619
+ return warnDecision(POLICY_REASON_CODES.POLICY_WARN, result.reason ?? `${result.guard} returned warning`, result.guard, 'medium');
255
620
  }
256
- return { status: 'deny', allowed: false, denied: true, warn: false, reason: result.reason, guard: result.guard, severity: result.severity };
621
+ return denyDecision(POLICY_REASON_CODES.GUARD_ERROR, result.reason ?? `${result.guard} denied request`, result.guard, result.severity ?? 'high');
257
622
  }
258
623
  }
259
624
  function buildThreatIntelEngine(policy) {
@@ -261,22 +626,153 @@ function buildThreatIntelEngine(policy) {
261
626
  if (!Array.isArray(custom) || custom.length === 0) {
262
627
  return null;
263
628
  }
629
+ // The openclaw Policy types `custom` as `unknown`; the canonical Policy
630
+ // expects `CustomGuardSpec[]`. We've validated it's an array above.
631
+ // GuardConfigs has an index signature so `unknown[]` is assignable.
264
632
  const canonicalPolicy = {
265
633
  version: '1.1.0',
266
634
  guards: { custom },
267
635
  };
268
636
  return createPolicyEngineFromPolicy(canonicalPolicy);
269
637
  }
270
- function toCanonicalEvent(event) {
271
- // OpenClaw events are compatible with adapter-core's PolicyEvent shape. Keep the
272
- // raw eventId/timestamp/metadata for audit trails.
273
- return event;
274
- }
275
638
  function combineDecisions(base, next) {
276
- const nextDenied = next.status === 'deny' || next.denied;
277
- const nextWarn = next.status === 'warn' || next.warn;
278
- if (nextDenied || nextWarn)
639
+ const rank = { deny: 2, warn: 1, allow: 0 };
640
+ const baseRank = rank[base.status] ?? 0;
641
+ const nextRank = rank[next.status] ?? 0;
642
+ if (nextRank > baseRank)
279
643
  return next;
644
+ if (nextRank === baseRank && nextRank > 0 && next.reason) {
645
+ // On ties for non-allow decisions, merge the reasons
646
+ return {
647
+ ...base,
648
+ message: base.message
649
+ ? `${base.message}; ${next.message ?? next.reason}`
650
+ : next.message ?? next.reason,
651
+ };
652
+ }
280
653
  return base;
281
654
  }
655
+ function normalizeStringList(values) {
656
+ if (!Array.isArray(values))
657
+ return [];
658
+ const out = [];
659
+ for (const value of values) {
660
+ if (typeof value !== 'string')
661
+ continue;
662
+ const normalized = value.trim();
663
+ if (normalized.length > 0)
664
+ out.push(normalized);
665
+ }
666
+ return out;
667
+ }
668
+ function extractInputType(data) {
669
+ const candidates = [data.input_type, data.inputType];
670
+ for (const candidate of candidates) {
671
+ if (typeof candidate === 'string') {
672
+ const normalized = candidate.trim().toLowerCase();
673
+ if (normalized.length > 0)
674
+ return normalized;
675
+ }
676
+ }
677
+ return null;
678
+ }
679
+ function extractTransferSize(data) {
680
+ const candidates = [
681
+ data.transfer_size,
682
+ data.transferSize,
683
+ data.size_bytes,
684
+ data.sizeBytes,
685
+ ];
686
+ for (const candidate of candidates) {
687
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0) {
688
+ return candidate;
689
+ }
690
+ if (typeof candidate === 'string') {
691
+ const parsed = Number.parseInt(candidate, 10);
692
+ if (Number.isFinite(parsed) && parsed >= 0) {
693
+ return parsed;
694
+ }
695
+ }
696
+ }
697
+ return null;
698
+ }
699
+ function parsePort(value) {
700
+ if (typeof value === 'number' && Number.isFinite(value)) {
701
+ const port = Math.trunc(value);
702
+ if (port > 0 && port <= 65535)
703
+ return port;
704
+ }
705
+ if (typeof value === 'string') {
706
+ const trimmed = value.trim();
707
+ if (/^[0-9]+$/.test(trimmed)) {
708
+ const parsed = Number.parseInt(trimmed, 10);
709
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535)
710
+ return parsed;
711
+ }
712
+ }
713
+ return null;
714
+ }
715
+ function firstNonEmptyString(values) {
716
+ for (const value of values) {
717
+ if (typeof value !== 'string')
718
+ continue;
719
+ const trimmed = value.trim();
720
+ if (trimmed.length > 0)
721
+ return trimmed;
722
+ }
723
+ return null;
724
+ }
725
+ function extractCuaNetworkTarget(data) {
726
+ const url = firstNonEmptyString([
727
+ data.url,
728
+ data.endpoint,
729
+ data.href,
730
+ data.target_url,
731
+ data.targetUrl,
732
+ ]);
733
+ const parsed = parseNetworkTarget(url ?? '', { emptyPort: 'default' });
734
+ const host = firstNonEmptyString([
735
+ data.host,
736
+ data.hostname,
737
+ data.remote_host,
738
+ data.remoteHost,
739
+ data.destination_host,
740
+ data.destinationHost,
741
+ parsed.host,
742
+ ])?.toLowerCase();
743
+ if (!host) {
744
+ return null;
745
+ }
746
+ const protocol = firstNonEmptyString([data.protocol, data.scheme])?.toLowerCase();
747
+ const explicitPort = parsePort(data.port
748
+ ?? data.remote_port
749
+ ?? data.remotePort
750
+ ?? data.destination_port
751
+ ?? data.destinationPort);
752
+ const port = explicitPort ?? (parsed.host ? parsed.port : protocol === 'http' ? 80 : 443);
753
+ return {
754
+ host,
755
+ port,
756
+ ...(protocol ? { protocol } : {}),
757
+ ...(url ? { url } : {}),
758
+ };
759
+ }
760
+ function eventTypeToSideChannelFlag(eventType) {
761
+ switch (eventType) {
762
+ case 'remote.clipboard':
763
+ return 'clipboard_enabled';
764
+ case 'remote.file_transfer':
765
+ return 'file_transfer_enabled';
766
+ case 'remote.audio':
767
+ return 'audio_enabled';
768
+ case 'remote.drive_mapping':
769
+ return 'drive_mapping_enabled';
770
+ case 'remote.printing':
771
+ return 'printing_enabled';
772
+ case 'remote.session_share':
773
+ return 'session_share_enabled';
774
+ default:
775
+ return null;
776
+ }
777
+ }
282
778
  //# sourceMappingURL=engine.js.map