evolcore 0.0.26 → 0.0.27

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 (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -178,6 +178,35 @@ function approvalCodeBlock(value) {
178
178
  const fence = '`'.repeat(Math.max(3, longestRun + 1));
179
179
  return `${fence}text\n${value}\n${fence}`;
180
180
  }
181
+ function normalizeAidForApprovalRouting(value) {
182
+ const normalized = value?.trim().replace(/^@/, '');
183
+ return normalized || undefined;
184
+ }
185
+ function preferredOriginOwner(context, approval, owners) {
186
+ const originChannel = (approval?.originChannel ?? context.channel ?? '').split('#', 1)[0].toLowerCase();
187
+ if (context.chatType !== 'private' || originChannel !== 'aun')
188
+ return undefined;
189
+ const originPeerId = normalizeAidForApprovalRouting(approval?.originPeerId ?? context.userId);
190
+ if (!originPeerId)
191
+ return undefined;
192
+ return owners.find(owner => normalizeAidForApprovalRouting(owner) === originPeerId);
193
+ }
194
+ /**
195
+ * Owner approvals normally belong to the Owner's private main session. The
196
+ * one exception is an AUN private conversation whose peer is itself a
197
+ * configured Owner: preserve that conversation's topic so the request stays
198
+ * with the Owner's current work.
199
+ */
200
+ function ownerApprovalThreadId(context, approval, approverOperatorId) {
201
+ const originChannel = (approval.originChannel ?? context.channel ?? '').split('#', 1)[0].toLowerCase();
202
+ const originPeerId = normalizeAidForApprovalRouting(approval.originPeerId ?? context.userId);
203
+ const approverId = normalizeAidForApprovalRouting(approverOperatorId);
204
+ const isOwnerPrivateConversation = context.chatType === 'private'
205
+ && originChannel === 'aun'
206
+ && !!originPeerId
207
+ && originPeerId === approverId;
208
+ return isOwnerPrivateConversation ? (approval.originThreadId ?? '') : '';
209
+ }
181
210
  export function planApprovalRoute(challenge, context) {
182
211
  if (!challenge.grantable) {
183
212
  return { kind: 'unavailable', reason: 'challenge_not_grantable' };
@@ -235,9 +264,12 @@ export function planApprovalRoute(challenge, context) {
235
264
  approverPrincipalId: actor.principalId,
236
265
  };
237
266
  }
238
- const handoffCandidates = challenge.approverPolicy === 'agent_manager'
239
- ? Array.from(new Set([...admins, ...owners]))
240
- : owners;
267
+ const preferredOwner = preferredOriginOwner(context ?? {}, routing, owners);
268
+ const handoffCandidates = preferredOwner
269
+ ? [preferredOwner]
270
+ : challenge.approverPolicy === 'agent_manager'
271
+ ? Array.from(new Set([...admins, ...owners]))
272
+ : owners;
241
273
  if (handoffCandidates.length === 0) {
242
274
  return {
243
275
  kind: 'unavailable',
@@ -777,6 +809,7 @@ export class PermissionGateway {
777
809
  }
778
810
  const ownerReplyContext = {
779
811
  delivery: { chatType: 'private' },
812
+ threadId: ownerApprovalThreadId(context, approval, route.approverOperatorId),
780
813
  metadata: {
781
814
  source: 'handoff',
782
815
  chatmode: 'interactive',
@@ -16,6 +16,13 @@ const CODEX_CUSTOM_POWERSHELL_NAMES = new Set(['pwsh.exe', 'powershell.exe']);
16
16
  const CODEX_CUSTOM_CMD_NAMES = new Set(['cmd.exe']);
17
17
  const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
18
18
  const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
19
+ // These switches only suppress interactive/profile UI behavior. `-Command`
20
+ // is handled separately as the required execute switch. Options such as
21
+ // `-NoExit`, `-ExecutionPolicy`, `-EncodedCommand`, and `-File` can change
22
+ // execution semantics and must never be accepted as a trusted carrier.
23
+ const CODEX_POWERSHELL_INERT_SWITCHES = new Set([
24
+ '-noprofile', '-noninteractive', '-nologo',
25
+ ]);
19
26
  // These switches affect only cmd startup/output formatting. `/c` is handled
20
27
  // separately as the required execute switch. Flags such as `/k`, `/v:on`,
21
28
  // `/e:*`, and `/f:*` can change process lifetime or command semantics and are
@@ -222,24 +229,27 @@ export function normalizeEvolcoreCommandArgv(argv) {
222
229
  }
223
230
  return [...argv];
224
231
  }
225
- /** Detect the historical PowerShell pattern that overwrote the thread carrier. */
226
- export function hasPowerShellDelegationAssignment(command) {
227
- return /^\s*\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(command)
232
+ const POWERSHELL_CREDENTIAL_ENV_RE = /^(EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)$/i;
233
+ /** Detect a PowerShell prefix that materializes an EvolCore session credential. */
234
+ export function hasPowerShellCredentialAssignment(command) {
235
+ return /^\s*\$env\s*:\s*(?:EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)\b/i.test(command)
228
236
  && /\bec(?:\.cmd|\.exe)?(?:\s|$)/i.test(command);
229
237
  }
230
238
  /**
231
239
  * Windows Codex may materialize one environment variable assignment directly
232
240
  * in the PowerShell command body instead of inheriting it from the process
233
- * environment. Only the EvolCore delegation variable is accepted, and only
234
- * when followed by one command on the same carrier. The value is validated for
235
- * shape but never trusted for authorization; the runner still arms the stable
236
- * in-memory thread carrier.
241
+ * environment. Only an EvolCore session credential variable is accepted,
242
+ * and only when followed by one command in the same shell invocation. The
243
+ * legacy delegation name remains readable for rolling upgrades, but its value
244
+ * is still compared with the credential bound to the managed session.
237
245
  */
238
- function unwrapPowerShellDelegationPrefix(command) {
246
+ function unwrapPowerShellCredentialPrefix(command) {
239
247
  const input = command.trim();
240
- if (!/^\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(input)) {
248
+ const prefix = input.match(/^\$env\s*:\s*([A-Za-z_][A-Za-z0-9_]*)\b/i);
249
+ if (!prefix || !POWERSHELL_CREDENTIAL_ENV_RE.test(prefix[1])) {
241
250
  return { kind: 'none', command };
242
251
  }
252
+ const credentialEnvName = prefix[1];
243
253
  let index = 0;
244
254
  const skipSpace = () => {
245
255
  while (index < input.length && (input[index] === ' ' || input[index] === '\t'))
@@ -256,7 +266,7 @@ function unwrapPowerShellDelegationPrefix(command) {
256
266
  return { kind: 'invalid', command };
257
267
  index++;
258
268
  skipSpace();
259
- if (!consume('EVOLCORE_DELEGATION_TOKEN'))
269
+ if (!consume(credentialEnvName))
260
270
  return { kind: 'invalid', command };
261
271
  skipSpace();
262
272
  if (input[index] !== '=')
@@ -328,20 +338,20 @@ export function parseCodexCarrierCommand(carrier, options = {}) {
328
338
  }
329
339
  }
330
340
  const unwrapped = carrier.dialect === 'powershell'
331
- ? unwrapPowerShellDelegationPrefix(carrier.command)
341
+ ? unwrapPowerShellCredentialPrefix(carrier.command)
332
342
  : { kind: 'none', command: carrier.command };
333
343
  // Windows PowerShell may materialize the thread's shell environment as a
334
- // single `$env:EVOLCORE_DELEGATION_TOKEN = '...'` prefix. Keep the token as
344
+ // single `$env:EVOLCORE_CODEX_SESSION_TOKEN = '...'` prefix. Keep the token as
335
345
  // metadata for the approval layer, which must compare it with the runner's
336
- // in-memory carrier before arming delegation. Malformed or unrelated
346
+ // session credential before arming delegation. Malformed or unrelated
337
347
  // assignments remain syntax failures and can never be authorized.
338
348
  if (unwrapped.kind === 'invalid'
339
349
  || (carrier.dialect === 'powershell'
340
- && hasPowerShellDelegationAssignment(carrier.command)
350
+ && hasPowerShellCredentialAssignment(carrier.command)
341
351
  && unwrapped.kind === 'none')
342
352
  || (unwrapped.kind === 'valid'
343
- && options.expectedDelegationToken !== undefined
344
- && options.expectedDelegationToken !== unwrapped.token)) {
353
+ && options.expectedCredentialToken !== undefined
354
+ && options.expectedCredentialToken !== unwrapped.token)) {
345
355
  return parseFailure('shell-composition', carrier.command, 0, 0, carrier.dialect);
346
356
  }
347
357
  const parsed = parseLiteralShellCommand(unwrapped.command, {
@@ -352,7 +362,7 @@ export function parseCodexCarrierCommand(carrier, options = {}) {
352
362
  ? {
353
363
  ...parsed,
354
364
  argv: normalizeEvolcoreCommandArgv(parsed.argv),
355
- ...(unwrapped.kind === 'valid' ? { delegationToken: unwrapped.token } : {}),
365
+ ...(unwrapped.kind === 'valid' ? { credentialToken: unwrapped.token } : {}),
356
366
  }
357
367
  : parsed;
358
368
  }
@@ -599,6 +609,25 @@ function resolveCodexCmdCarrierArgv(argv) {
599
609
  ? { command, dialect: 'cmd', executable: argv[0] }
600
610
  : undefined;
601
611
  }
612
+ function resolveCodexPowerShellCarrierArgv(argv) {
613
+ if (argv.length < 3 || !isCodexPowerShellExecutable(argv[0]))
614
+ return undefined;
615
+ const commandIndex = argv.length - 2;
616
+ if (argv[commandIndex].toLowerCase() !== '-command')
617
+ return undefined;
618
+ const seen = new Set();
619
+ for (const rawSwitch of argv.slice(1, commandIndex)) {
620
+ const switchValue = rawSwitch.toLowerCase();
621
+ if (!CODEX_POWERSHELL_INERT_SWITCHES.has(switchValue) || seen.has(switchValue)) {
622
+ return undefined;
623
+ }
624
+ seen.add(switchValue);
625
+ }
626
+ const command = argv.at(-1);
627
+ return typeof command === 'string' && command.trim()
628
+ ? { command, dialect: 'powershell', executable: argv[0] }
629
+ : undefined;
630
+ }
602
631
  /** Resolve a Windows cmd carrier when the app-server provides only a string. */
603
632
  function resolveCodexCmdCarrierString(command) {
604
633
  const input = trimShellSpacing(command);
@@ -614,10 +643,14 @@ function resolveCodexCmdCarrierString(command) {
614
643
  const argv = [executable, ...switches, match[4]];
615
644
  return resolveCodexCmdCarrierArgv(argv);
616
645
  }
617
- /** Resolve a PowerShell carrier when the app-server provides only a string. */
646
+ /**
647
+ * Resolve a PowerShell carrier when the app-server provides only a string.
648
+ * The prefix is converted to the same argv shape used by commandArgv so the
649
+ * startup-switch policy cannot diverge between the two transport forms.
650
+ */
618
651
  function resolveCodexPowerShellCarrierString(command) {
619
652
  const input = trimShellSpacing(command);
620
- const match = input.match(/^(?:"([^"]+)"|(\S+))\s+-command\s+(.+)$/i);
653
+ const match = input.match(/^(?:"([^"\r\n]+)"|(\S+))((?:[ \t]+-[A-Za-z]+)+)[ \t]+(.+)$/);
621
654
  if (!match)
622
655
  return undefined;
623
656
  const executable = match[1] ?? match[2];
@@ -626,13 +659,14 @@ function resolveCodexPowerShellCarrierString(command) {
626
659
  // A string-form carrier has one additional command-line quoting layer. The
627
660
  // command argument is commonly serialized as `"ec ..."`; remove exactly
628
661
  // that outer pair and leave quotes belonging to EC arguments intact.
629
- let nested = match[3].trim();
662
+ let nested = match[4].trim();
630
663
  if (nested.length >= 2
631
664
  && ((nested.startsWith('"') && nested.endsWith('"'))
632
665
  || (nested.startsWith("'") && nested.endsWith("'")))) {
633
666
  nested = nested.slice(1, -1);
634
667
  }
635
- return nested ? { command: nested, dialect: 'powershell', executable } : undefined;
668
+ const switches = match[3].trim().split(/[ \t]+/);
669
+ return resolveCodexPowerShellCarrierArgv([executable, ...switches, nested]);
636
670
  }
637
671
  /** Resolve a POSIX shell carrier when app-server provides only a string. */
638
672
  function resolveCodexPosixShellCarrierString(command) {
@@ -868,11 +902,9 @@ export function resolveCodexShellCarrierArgv(argv) {
868
902
  && isCodexPosixShellExecutable(argv[0])) {
869
903
  return { command: argv[2], dialect: 'posix', executable: argv[0] };
870
904
  }
871
- if (argv.length === 3) {
872
- if (isCodexPowerShellExecutable(argv[0]) && argv[1].toLowerCase() === '-command') {
873
- return { command: argv[2], dialect: 'powershell', executable: argv[0] };
874
- }
875
- }
905
+ const powershellCarrier = resolveCodexPowerShellCarrierArgv(argv);
906
+ if (powershellCarrier)
907
+ return powershellCarrier;
876
908
  // Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
877
909
  // The execute switch must be the final switch before one command argument;
878
910
  // validation itself is shared with string-only Windows payloads below.
@@ -1645,7 +1677,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1645
1677
  if (nested.ok && nested.argv[0] === 'ec')
1646
1678
  return { kind: 'literal' };
1647
1679
  if (!nested.ok && (hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)
1648
- || (carrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(carrier.command)))) {
1680
+ || (carrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(carrier.command)))) {
1649
1681
  return { kind: 'composite', issue: nested.issue };
1650
1682
  }
1651
1683
  }
@@ -1655,7 +1687,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1655
1687
  if (nested.ok && nested.argv[0] === 'ec')
1656
1688
  return { kind: 'literal' };
1657
1689
  if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1658
- || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1690
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
1659
1691
  return { kind: 'composite', issue: nested.issue };
1660
1692
  }
1661
1693
  }
@@ -1677,7 +1709,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1677
1709
  if (nested.ok && nested.argv[0] === 'ec')
1678
1710
  return { kind: 'literal' };
1679
1711
  if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
1680
- || (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
1712
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
1681
1713
  return { kind: 'composite', issue: nested.issue };
1682
1714
  }
1683
1715
  return { kind: 'none' };
@@ -28,9 +28,13 @@ const H_CLASS_RELATIVE_PATTERNS = [
28
28
  /^data\/outbox(?:\/|$)/,
29
29
  /^data\/handoff(?:\/|$)/,
30
30
  /^agents\/[^/]+\/sessions(?:\/|$)/,
31
+ /^agents\/[^/]+\/data\/async-questions(?:\/|$)/,
32
+ /^agents\/[^/]+\/data\/aun(?:\/|$)/,
31
33
  /^agents\/[^/]+\/data\/(?:channels(?:\/|$)|handoff(?:\/|$)|(?:outbox|activity-outbox)\.jsonl(?:_|__|\.tmp-.*)?$|contact-audit\.jsonl$)/,
32
34
  ];
33
35
  const H_CLASS_REFERENCE_PATTERNS = [
36
+ /(?:^|[/\\\s"'=<>])agents[\\/][^/\\\s"']+[\\/]data[\\/]async-questions(?:[\\/]|$|[\s"';&|<>)},])/,
37
+ /(?:^|[/\\\s"'=<>])agents[\\/][^/\\\s"']+[\\/]data[\\/]aun(?:[\\/]|$|[\s"';&|<>)},])/,
34
38
  /(?:^|[^A-Za-z0-9_.-])daemon\.json(?=$|[^A-Za-z0-9_.-])/,
35
39
  /(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|contact-operations\.jsonl|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
36
40
  /(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys|private))(?:[\\/]|$|[\s"';&|<>)},])/,
@@ -43,6 +47,8 @@ const H_CLASS_REFERENCE_PATTERNS = [
43
47
  // protected file. Descendants inherit the directory classification unless an
44
48
  // existing regular file gives us a more precise answer.
45
49
  const H_CLASS_DIRECTORY_RELATIVE_PATTERNS = [
50
+ /^agents\/[^/]+\/data\/aun(?:\/|$)/,
51
+ /^agents\/[^/]+\/data\/async-questions(?:\/|$)/,
46
52
  /^backups\/config(?:\/|$)/,
47
53
  /^\.snapshots(?:\/|$)/,
48
54
  /^CA(?:\/|$)/,
@@ -286,6 +292,8 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
286
292
  path.join('agents', '*', 'sessions', '**'),
287
293
  path.join('agents', '*', 'data', 'channels', '**'),
288
294
  path.join('agents', '*', 'data', 'handoff', '**'),
295
+ path.join('agents', '*', 'data', 'async-questions', '**'),
296
+ path.join('agents', '*', 'data', 'aun', '**'),
289
297
  path.join('agents', '*', 'data', 'outbox.jsonl*'),
290
298
  path.join('agents', '*', 'data', 'activity-outbox.jsonl*'),
291
299
  path.join('agents', '*', 'data', 'contact-audit.jsonl'),
@@ -303,6 +311,15 @@ export function getLClassWritePatterns(root = resolveRoot()) {
303
311
  const CLAUDE_PROTECTED_TARGET_LIMIT = 8192;
304
312
  const CLAUDE_GLOB_MAGIC = /[*?[\]]/;
305
313
  const CODEX_GLOB_MAGIC = /[*?[\]]/;
314
+ // Codex wraps the resolved Windows ACL payload in JSON and base64 before
315
+ // passing it as one CreateProcess argument. Real Codex 0.154 payloads are
316
+ // roughly 3-5x larger than EvolCore's input map, so leave enough room below
317
+ // Windows' 32,767-character command-line ceiling for helper metadata.
318
+ const CODEX_WINDOWS_FILESYSTEM_RULE_BUDGET_BYTES = 7 * 1024;
319
+ const CODEX_WINDOWS_MATERIALIZED_FILE_GLOBS = [
320
+ '**/*.json_',
321
+ '**/*.json.migrated',
322
+ ];
306
323
  function addExistingTarget(targets, value) {
307
324
  try {
308
325
  const stat = fs.lstatSync(value);
@@ -370,6 +387,8 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
370
387
  addExistingTarget(targets, path.join(agentRoot, 'sessions'));
371
388
  addExistingTarget(targets, path.join(agentRoot, 'data', 'channels'));
372
389
  addExistingTarget(targets, path.join(agentRoot, 'data', 'handoff'));
390
+ addExistingTarget(targets, path.join(agentRoot, 'data', 'async-questions'));
391
+ addExistingTarget(targets, path.join(agentRoot, 'data', 'aun'));
373
392
  addExistingTarget(targets, path.join(agentRoot, 'data', 'outbox.jsonl'));
374
393
  addExistingTarget(targets, path.join(agentRoot, 'data', 'activity-outbox.jsonl'));
375
394
  addExistingTarget(targets, path.join(agentRoot, 'data', 'contact-audit.jsonl'));
@@ -427,14 +446,14 @@ export function buildClaudeProtectedFilesystem(root = resolveRoot(), options = {
427
446
  denyWrite: allPaths,
428
447
  };
429
448
  }
430
- export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
449
+ export function buildCodexHClassFilesystemRules(root = resolveRoot(), platform = process.platform) {
431
450
  const filesystem = {
432
451
  // Codex pre-expands unbounded deny globs on Linux/WSL/Windows. Without a
433
452
  // bound, patterns such as **/*.json_ are accepted but may not be enforced.
434
453
  glob_scan_max_depth: 64,
435
- ...Object.fromEntries(filterWindowsCodexFilesystemTargets(getHClassSandboxPatterns(root)).map(pattern => [pattern, 'deny'])),
454
+ ...Object.fromEntries(filterWindowsCodexFilesystemTargets(getHClassSandboxPatterns(root), platform).map(pattern => [pattern, 'deny'])),
436
455
  };
437
- return compactWindowsCodexFilesystemRules(filesystem);
456
+ return materializeWindowsCodexRecursiveFileDenies(compactWindowsCodexFilesystemRules(filesystem, platform), root, platform);
438
457
  }
439
458
  /**
440
459
  * Windows Codex sandbox setup materializes a path while applying its deny-read
@@ -462,20 +481,21 @@ export function filterWindowsCodexFilesystemTargets(patterns, platform = process
462
481
  });
463
482
  }
464
483
  /**
465
- * Codex expands recursive deny globs into individual targets. Large protected
484
+ * Codex expands recursive ACL globs into individual targets. Large protected
466
485
  * trees on Windows can then fail during AGENTS.md loading with os error 206.
467
- * Directory denies inherit to descendants through the Windows sandbox ACLs,
468
- * avoiding redundant targets. Keep broad recursive filename globs intact.
486
+ * Directory deny/read ACLs inherit to descendants, avoiding redundant targets.
487
+ * Keep broad recursive filename globs intact for the materialization pass.
469
488
  */
470
489
  export function compactWindowsCodexFilesystemRules(filesystem, platform = process.platform) {
471
490
  if (platform !== 'win32')
472
491
  return filesystem;
473
492
  const compacted = {};
493
+ const projectedKeys = new Map();
474
494
  for (const [pattern, access] of Object.entries(filesystem)) {
475
- const isTerminalRecursiveDeny = access === 'deny'
495
+ const isTerminalRecursiveAcl = (access === 'deny' || access === 'read')
476
496
  && (pattern.endsWith('/**') || pattern.endsWith('\\**'));
477
- const recursiveRoot = isTerminalRecursiveDeny ? pattern.slice(0, -3) : undefined;
478
- // Only collapse a recursive deny when its root is a real directory. A
497
+ const recursiveRoot = isTerminalRecursiveAcl ? pattern.slice(0, -3) : undefined;
498
+ // Only collapse a recursive ACL when its root is a real directory. A
479
499
  // missing root must remain a glob: turning it into an exact target can
480
500
  // make the Windows sandbox materialize that path as a directory.
481
501
  const canCompact = !!recursiveRoot
@@ -488,12 +508,104 @@ export function compactWindowsCodexFilesystemRules(filesystem, platform = proces
488
508
  }
489
509
  })());
490
510
  const compactPattern = canCompact ? recursiveRoot : pattern;
491
- if (compacted[compactPattern] !== 'deny')
492
- compacted[compactPattern] = access;
511
+ let projectedPattern = compactPattern;
512
+ {
513
+ // Resolve only the literal prefix and only casing aliases. Do not fold
514
+ // glob spelling, unresolved paths, case-sensitive directories or links.
515
+ const globIndex = compactPattern.search(CODEX_GLOB_MAGIC);
516
+ const prefix = globIndex < 0 ? compactPattern : compactPattern.slice(0, Math.max(compactPattern.lastIndexOf('/', globIndex), compactPattern.lastIndexOf('\\', globIndex)));
517
+ let key = compactPattern;
518
+ try {
519
+ const canonical = fs.realpathSync.native(prefix);
520
+ if (canonical.toLowerCase() === prefix.toLowerCase())
521
+ key = canonical + compactPattern.slice(prefix.length);
522
+ }
523
+ catch { /* An unresolved alias must retain its deny rule. */ }
524
+ projectedPattern = projectedKeys.get(key) ?? compactPattern;
525
+ projectedKeys.set(key, projectedPattern);
526
+ }
527
+ if (compacted[projectedPattern] !== 'deny')
528
+ compacted[projectedPattern] = access;
529
+ }
530
+ // Directory deny ACLs inherit. After H/L rules are combined, omit exact
531
+ // file denies already covered by an existing directory deny. Keep globs
532
+ // and any child for which a read/write override could change precedence.
533
+ const entries = Object.entries(compacted);
534
+ const denyDirectories = entries.filter(([pattern, access]) => {
535
+ if (access !== 'deny' || CODEX_GLOB_MAGIC.test(pattern))
536
+ return false;
537
+ try {
538
+ return fs.lstatSync(pattern).isDirectory();
539
+ }
540
+ catch {
541
+ return false;
542
+ }
543
+ }).map(([pattern]) => pattern);
544
+ for (const [pattern, access] of entries) {
545
+ if (access !== 'deny' || CODEX_GLOB_MAGIC.test(pattern) || denyDirectories.includes(pattern))
546
+ continue;
547
+ if (denyDirectories.some(parent => isSameOrDescendant(pattern, parent)
548
+ && !entries.some(([override, value]) => (value === 'read' || value === 'write')
549
+ && (CODEX_GLOB_MAGIC.test(override) || isSameOrDescendant(override, parent))))) {
550
+ delete compacted[pattern];
551
+ }
493
552
  }
494
553
  return compacted;
495
554
  }
555
+ /**
556
+ * Codex 0.154 serializes resolved Windows deny-read targets into a base64
557
+ * command-line argument for codex-windows-sandbox-setup.exe. Recursive file
558
+ * globs can expand into enough absolute paths to exceed CreateProcess' command
559
+ * line limit. Claude avoids the analogous limit by externalizing settings;
560
+ * Codex has no per-thread settings-file input, so materialize only these broad
561
+ * file globs in EvolCore and keep the compact exact projection in thread config.
562
+ *
563
+ * Matches below an already denied directory are omitted because the Windows
564
+ * sandbox ACL on that directory inherits to descendants. Other H-class rules
565
+ * remain dynamic globs, preserving protection for newly created agent/config
566
+ * paths without recreating the unbounded recursive expansion.
567
+ */
568
+ export function materializeWindowsCodexRecursiveFileDenies(filesystem, root = resolveRoot(), platform = process.platform) {
569
+ if (platform !== 'win32')
570
+ return filesystem;
571
+ const absoluteRoot = resolveThroughExistingAncestor(root);
572
+ const materializedPatterns = new Set(CODEX_WINDOWS_MATERIALIZED_FILE_GLOBS.map(pattern => path.join(absoluteRoot, pattern)));
573
+ const deniedDirectories = getExistingHClassMaskTargets(absoluteRoot)
574
+ .filter(target => target.kind === 'directory')
575
+ .map(target => target.path);
576
+ const projected = {};
577
+ for (const [pattern, access] of Object.entries(filesystem)) {
578
+ if (access !== 'deny' || !materializedPatterns.has(pattern)) {
579
+ projected[pattern] = access;
580
+ continue;
581
+ }
582
+ const relativePattern = path.relative(absoluteRoot, pattern).split(path.sep).join('/');
583
+ let matches;
584
+ try {
585
+ matches = fs.globSync(relativePattern, { cwd: absoluteRoot });
586
+ }
587
+ catch (error) {
588
+ throw new Error(`[CodexSandbox] failed to materialize Windows protected-file rules: ${error instanceof Error ? error.message : String(error)}`);
589
+ }
590
+ for (const match of matches) {
591
+ const candidate = path.resolve(absoluteRoot, match);
592
+ if (deniedDirectories.some(directory => isSameOrDescendant(candidate, directory)))
593
+ continue;
594
+ projected[candidate] = access;
595
+ }
596
+ }
597
+ return projected;
598
+ }
599
+ export function assertWindowsCodexFilesystemRuleBudget(filesystem, platform = process.platform) {
600
+ if (platform !== 'win32')
601
+ return;
602
+ const bytes = Buffer.byteLength(JSON.stringify(filesystem), 'utf8');
603
+ if (bytes > CODEX_WINDOWS_FILESYSTEM_RULE_BUDGET_BYTES) {
604
+ throw new Error(`[CodexSandbox] Windows filesystem rule budget exceeded: ${bytes} > ${CODEX_WINDOWS_FILESYSTEM_RULE_BUDGET_BYTES}`);
605
+ }
606
+ }
496
607
  export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
608
+ const platform = options.platform ?? process.platform;
497
609
  const lClassRules = {};
498
610
  const lClassAccess = options.denyLClassRead ? 'deny' : 'read';
499
611
  // Codex's Linux sandbox expands glob deny rules into per-file mount targets.
@@ -506,10 +618,14 @@ export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options
506
618
  const expandLClassReadDeny = options.expandLClassReadDeny ?? true;
507
619
  for (const target of getExistingLClassReadOnlyTargets(root)) {
508
620
  if (target.kind === 'directory') {
509
- if (!expandLClassReadDeny || options.denyLClassRead || path.basename(target.path) === 'triggers') {
621
+ // :workspace/:read-only already grants L-class read access on Windows.
622
+ // Omitting redundant read ACLs keeps Codex's serialized helper payload
623
+ // below CreateProcess' command-line limit. Readonly still needs deny.
624
+ const includeWindowsRead = platform !== 'win32' || options.denyLClassRead;
625
+ if (includeWindowsRead && (!expandLClassReadDeny || options.denyLClassRead || path.basename(target.path) === 'triggers')) {
510
626
  lClassRules[target.path] = lClassAccess;
511
627
  }
512
- if (expandLClassReadDeny || !options.denyLClassRead) {
628
+ if (includeWindowsRead && (expandLClassReadDeny || !options.denyLClassRead)) {
513
629
  // Use '/**' suffix instead of path.join to satisfy Codex's glob requirement
514
630
  lClassRules[target.path + '/**'] = lClassAccess;
515
631
  }
@@ -518,10 +634,12 @@ export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options
518
634
  lClassRules[target.path] = lClassAccess;
519
635
  }
520
636
  }
521
- return compactWindowsCodexFilesystemRules({
522
- ...buildCodexHClassFilesystemRules(root),
637
+ const filesystem = compactWindowsCodexFilesystemRules({
638
+ ...buildCodexHClassFilesystemRules(root, platform),
523
639
  ...lClassRules,
524
- });
640
+ }, platform);
641
+ assertWindowsCodexFilesystemRuleBudget(filesystem, platform);
642
+ return filesystem;
525
643
  }
526
644
  export function hClassGrantIncludesProtectedPath(value, options = {}) {
527
645
  const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
@@ -7,14 +7,46 @@ let cachedBubblewrapPath;
7
7
  let sandboxInitializationCircuit;
8
8
  const sessionSandboxInitializationCircuits = new Map();
9
9
  let sandboxCircuitNoticeAt = 0;
10
+ let sandboxInitializationFailureCounters = emptySandboxInitializationFailureCounters();
10
11
  export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
11
12
  export const SANDBOX_INITIALIZATION_COOLDOWN_MS = 5 * 60 * 1000;
12
13
  export const SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS = 60 * 1000;
14
+ function emptySandboxInitializationFailureCounters() {
15
+ return {
16
+ total: 0,
17
+ applySeccomp: 0,
18
+ byKind: { uid_map: 0, gid_map: 0, seccomp: 0, user_namespace: 0, bubblewrap: 0, unknown: 0 },
19
+ };
20
+ }
21
+ /** Classify already recognized bootstrap evidence, never arbitrary tool output. */
22
+ function sandboxInitializationFailureKind(text) {
23
+ if (/\buid_map\b/i.test(text))
24
+ return 'uid_map';
25
+ if (/\bgid_map\b/i.test(text))
26
+ return 'gid_map';
27
+ if (/\bseccomp\b/i.test(text))
28
+ return 'seccomp';
29
+ if (/user namespace/i.test(text))
30
+ return 'user_namespace';
31
+ if (/\b(?:bwrap|bubblewrap)\b/i.test(text))
32
+ return 'bubblewrap';
33
+ return 'unknown';
34
+ }
35
+ export function getSandboxInitializationFailureCounters() {
36
+ return { ...sandboxInitializationFailureCounters, byKind: { ...sandboxInitializationFailureCounters.byKind } };
37
+ }
13
38
  const SANDBOX_DIAGNOSTIC_TARGET_LIMIT = 256;
14
39
  /** Host bootstrap failures open a process circuit; tool-result failures only
15
40
  * affect their originating session because tool output is not host attestation. */
16
41
  export function recordSandboxInitializationFailure(error, stderr = [], now = Date.now(), sessionId) {
17
- const detail = createSandboxInitializationError(error, stderr).message.slice(0, 512);
42
+ const wrapped = createSandboxInitializationError(error, stderr);
43
+ const evidence = [wrapped.message, ...(wrapped.stderr ?? [])].join('\n');
44
+ const kind = sandboxInitializationFailureKind(evidence);
45
+ sandboxInitializationFailureCounters.total++;
46
+ sandboxInitializationFailureCounters.byKind[kind]++;
47
+ if (/\bapply-seccomp\b/i.test(evidence))
48
+ sandboxInitializationFailureCounters.applySeccomp++;
49
+ const detail = wrapped.message.slice(0, 512);
18
50
  getActiveSandboxInitializationFailure(now);
19
51
  const previous = sessionId ? sessionSandboxInitializationCircuits.get(sessionId) : sandboxInitializationCircuit;
20
52
  const circuit = previous
@@ -23,8 +55,9 @@ export function recordSandboxInitializationFailure(error, stderr = [], now = Dat
23
55
  lastObservedAt: now,
24
56
  occurrences: previous.occurrences + 1,
25
57
  detail,
58
+ kind,
26
59
  }
27
- : { openedAt: now, lastObservedAt: now, occurrences: 1, detail };
60
+ : { openedAt: now, lastObservedAt: now, occurrences: 1, detail, kind };
28
61
  if (sessionId)
29
62
  sessionSandboxInitializationCircuits.set(sessionId, circuit);
30
63
  else
@@ -68,11 +101,13 @@ export function isSandboxInitializationFailure(error, stderr = []) {
68
101
  .join('\n')
69
102
  .toLowerCase();
70
103
  return text.includes(SANDBOX_INITIALIZATION_FAILED)
104
+ || /fs sandbox helper failed(?: with status)?\b/.test(text)
71
105
  || /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
72
106
  || /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
73
107
  || /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
74
108
  || /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text)
75
- || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text);
109
+ || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text)
110
+ || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot) create (?:file|directory) at [^\n]+: read-only file system/.test(text);
76
111
  }
77
112
  export function createSandboxInitializationError(error, stderr = []) {
78
113
  if (error instanceof Error
@@ -98,6 +133,39 @@ export function createSandboxInitializationError(error, stderr = []) {
98
133
  export function shouldFailIfClaudeSandboxUnavailable(platform = process.platform) {
99
134
  return platform !== 'win32';
100
135
  }
136
+ /**
137
+ * In an unprivileged user namespace (for example, root in an LXC container),
138
+ * dropping all capabilities before apply-seccomp creates its child namespace
139
+ * prevents that helper from writing a mapping for UID 0 (CAP_SETFCAP).
140
+ * Use the SDK's nested-sandbox compatibility mode only for confirmed remapped
141
+ * root. Unknown host state must not opt into weaker isolation.
142
+ */
143
+ export function shouldEnableClaudeWeakerNestedSandbox(platform = process.platform, effectiveUid = typeof process.geteuid === 'function' ? process.geteuid() : undefined) {
144
+ if (platform !== 'linux' || effectiveUid !== 0)
145
+ return false;
146
+ let uidMap;
147
+ try {
148
+ uidMap = fs.readFileSync('/proc/self/uid_map', 'utf8').trim();
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ if (!uidMap)
154
+ return false;
155
+ let remappedRoot = false;
156
+ for (const line of uidMap.split(/\r?\n/)) {
157
+ const fields = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)$/);
158
+ if (!fields)
159
+ return false;
160
+ const [inside, outside, count] = fields.slice(1).map(Number);
161
+ if (![inside, outside, count].every(Number.isSafeInteger)
162
+ || count === 0 || inside + count > 0xffffffff || outside + count > 0xffffffff)
163
+ return false;
164
+ if (inside === 0 && outside !== 0)
165
+ remappedRoot = true;
166
+ }
167
+ return remappedRoot;
168
+ }
101
169
  function isExecutableFile(candidate) {
102
170
  if (!candidate)
103
171
  return false;
@@ -586,4 +654,5 @@ export function _resetSandboxRuntimeCache() {
586
654
  cachedBubblewrapPath = undefined;
587
655
  sandboxInitializationCircuit = undefined;
588
656
  sandboxCircuitNoticeAt = 0;
657
+ sandboxInitializationFailureCounters = emptySandboxInitializationFailureCounters();
589
658
  }
@@ -20,6 +20,21 @@ export function classifyToolErrorCode(input) {
20
20
  return 'USER_DENIED';
21
21
  return 'EXECUTION_FAILED';
22
22
  }
23
+ /**
24
+ * Map the internal error taxonomy to the intentionally small AUN activity
25
+ * contract. The detailed code remains available to local audit consumers.
26
+ */
27
+ export function classifyAunToolErrorCode(input) {
28
+ const code = classifyToolErrorCode(input);
29
+ if (code === 'POLICY_DENIED'
30
+ || code === 'ROLE_DENIED'
31
+ || code === 'USER_DENIED'
32
+ || code === 'APPROVAL_TIMEOUT'
33
+ || code === 'DELEGATION_FAILED') {
34
+ return 'PERMISSION_DENIED';
35
+ }
36
+ return 'EXECUTION_FAILED';
37
+ }
23
38
  /**
24
39
  * Compatibility classifier for historical, unstructured logs only.
25
40
  * Production event producers must use classifyToolErrorCode() and explicit