evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -56,9 +56,10 @@ export class StreamDebouncer {
56
56
  * 如果窗口只剩这一条,整个窗口取消。
57
57
  * @returns true 如果找到并移除
58
58
  */
59
- cancel(messageId) {
59
+ cancel(messageId, selfAID) {
60
60
  for (const [key, win] of this.pending) {
61
- const idx = win.entries.findIndex(e => e.messageId === messageId);
61
+ const idx = win.entries.findIndex(e => e.messageId === messageId
62
+ && (selfAID === undefined || e.rest.selfAID === selfAID));
62
63
  if (idx === -1)
63
64
  continue;
64
65
  // resolve 被撤回的那条(静默完成,不报错)
@@ -89,8 +90,13 @@ export class StreamDebouncer {
89
90
  const contents = [];
90
91
  const sourceMessageIds = [];
91
92
  const seenSourceMessageIds = new Set();
93
+ let receivedAt;
92
94
  for (const e of entries) {
93
95
  contents.push(e.content);
96
+ const entryReceivedAt = e.rest.receivedAt ?? e.rest.timestamp;
97
+ if (entryReceivedAt !== undefined) {
98
+ receivedAt = receivedAt === undefined ? entryReceivedAt : Math.min(receivedAt, entryReceivedAt);
99
+ }
94
100
  if (e.images)
95
101
  allImages.push(...e.images);
96
102
  if (e.mentions)
@@ -119,6 +125,8 @@ export class StreamDebouncer {
119
125
  replyContext: last.replyContext,
120
126
  messageId: latestMessageId,
121
127
  sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
128
+ receivedAt,
129
+ timestamp: receivedAt ?? last.rest.timestamp,
122
130
  };
123
131
  const resolves = entries.map(e => e.resolve);
124
132
  const rejects = entries.map(e => e.reject);
@@ -566,6 +566,7 @@ export class PermissionGateway {
566
566
  return decisionPromise;
567
567
  }
568
568
  const ownerReplyContext = {
569
+ delivery: { chatType: 'private' },
569
570
  metadata: {
570
571
  source: 'handoff',
571
572
  chatmode: 'interactive',
@@ -1,4 +1,11 @@
1
1
  const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
2
+ // Codex app-server sends command executions through a fixed shell carrier.
3
+ // Treat the carrier as syntax only after parsing it as literal argv; never
4
+ // accept an arbitrary `sh -c` string or unwrap shell composition here.
5
+ const CODEX_SHELL_EXECUTABLES = new Set([
6
+ 'bash', '/bin/bash', '/usr/bin/bash',
7
+ 'sh', '/bin/sh', '/usr/bin/sh',
8
+ ]);
2
9
  const SHELL_UNQUOTED_META = new Set([
3
10
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
4
11
  ]);
@@ -7,6 +14,66 @@ const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
7
14
  function trimShellSpacing(value) {
8
15
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
9
16
  }
17
+ const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
18
+ /**
19
+ * Preserve the one environment reference allowed in managed EC paths.
20
+ * The regular literal parser still rejects all expansion by default. The
21
+ * privileged EC classifier opts into this token only after the surrounding
22
+ * policy has validated the path against the session-owned TMPDIR.
23
+ */
24
+ function substituteManagedTmpDirToken(value) {
25
+ let result = '';
26
+ let quote = null;
27
+ for (let index = 0; index < value.length; index++) {
28
+ const char = value[index];
29
+ if (quote === 'single') {
30
+ result += char;
31
+ if (char === "'")
32
+ quote = null;
33
+ continue;
34
+ }
35
+ if (quote === 'double') {
36
+ if (char === '\\') {
37
+ result += char;
38
+ if (index + 1 < value.length)
39
+ result += value[++index];
40
+ continue;
41
+ }
42
+ if (char === '"') {
43
+ quote = null;
44
+ result += char;
45
+ continue;
46
+ }
47
+ }
48
+ else if (char === "'") {
49
+ quote = 'single';
50
+ result += char;
51
+ continue;
52
+ }
53
+ else if (char === '\\') {
54
+ result += char;
55
+ if (index + 1 < value.length)
56
+ result += value[++index];
57
+ continue;
58
+ }
59
+ if (value.startsWith('${TMPDIR}', index)) {
60
+ result += MANAGED_TMPDIR_TOKEN;
61
+ index += '${TMPDIR}'.length - 1;
62
+ }
63
+ else if (value.startsWith('$TMPDIR', index)
64
+ && !/[A-Za-z0-9_]/.test(value[index + '$TMPDIR'.length] ?? '')) {
65
+ result += MANAGED_TMPDIR_TOKEN;
66
+ index += '$TMPDIR'.length - 1;
67
+ }
68
+ else {
69
+ result += char;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ function restoreManagedTmpDirToken(value, managedTempDir) {
75
+ return value.replaceAll(MANAGED_TMPDIR_TOKEN, managedTempDir ?? '$TMPDIR');
76
+ }
10
77
  function isShellSpacing(value) {
11
78
  return value === ' ' || value === '\t';
12
79
  }
@@ -34,8 +101,10 @@ function doubleQuotedIssueAt(input, index) {
34
101
  * expansion, glob, pipeline, command substitution, redirection, or comment.
35
102
  * Anything outside this subset is never granted the privileged EC path.
36
103
  */
37
- export function parseLiteralShellCommand(command) {
38
- const input = trimShellSpacing(command);
104
+ export function parseLiteralShellCommand(command, options = {}) {
105
+ const input = trimShellSpacing(options.allowManagedTmpDir
106
+ ? substituteManagedTmpDirToken(command)
107
+ : command);
39
108
  if (!input)
40
109
  return { ok: false, issue: 'empty-command' };
41
110
  if (input.includes('\0'))
@@ -128,13 +197,56 @@ export function parseLiteralShellCommand(command) {
128
197
  return { ok: false, issue: 'invalid-quote' };
129
198
  finishToken();
130
199
  return argv.length > 0
131
- ? { ok: true, argv }
200
+ ? {
201
+ ok: true,
202
+ argv: options.allowManagedTmpDir
203
+ ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
204
+ : argv,
205
+ }
132
206
  : { ok: false, issue: 'empty-command' };
133
207
  }
134
- export function parseLiteralShellArgv(command) {
135
- const result = parseLiteralShellCommand(command);
208
+ export function parseLiteralShellArgv(command, options = {}) {
209
+ const result = parseLiteralShellCommand(command, options);
136
210
  return result.ok ? result.argv : null;
137
211
  }
212
+ /**
213
+ * Return the command string carried by Codex's literal `/bin/bash -lc ...`
214
+ * wrapper. The wrapper is accepted only in its exact three-argv form and the
215
+ * caller must parse the returned string with the literal shell parser again.
216
+ */
217
+ export function unwrapCodexShellCommandArgv(argv) {
218
+ if (argv.length !== 3 || argv[1] !== '-lc')
219
+ return undefined;
220
+ return CODEX_SHELL_EXECUTABLES.has(argv[0]) ? argv[2] : undefined;
221
+ }
222
+ /**
223
+ * Parse either a bare literal command or the exact shell carrier emitted by
224
+ * Codex app-server. This does not make shell composition safe: the nested
225
+ * command still has to pass `parseLiteralShellCommand`.
226
+ */
227
+ export function parseCodexShellCommandArgv(command, options = {}) {
228
+ const outer = parseLiteralShellArgv(command, options);
229
+ if (!outer)
230
+ return null;
231
+ const wrapped = unwrapCodexShellCommandArgv(outer);
232
+ return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
233
+ }
234
+ /**
235
+ * Resolve a Codex command input while preserving the raw `commandArgv` form
236
+ * supplied by app-server. Joining that argv with spaces loses the quoting of
237
+ * the inner `-lc` command and was the source of a false proactive denial.
238
+ */
239
+ export function parseCodexToolCommandArgv(input) {
240
+ if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
241
+ const outer = input.commandArgv;
242
+ const wrapped = unwrapCodexShellCommandArgv(outer);
243
+ return wrapped === undefined
244
+ ? outer
245
+ : parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
246
+ }
247
+ const command = typeof input.command === 'string' ? input.command : '';
248
+ return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
249
+ }
138
250
  function scanBoundedOutputOperators(command) {
139
251
  const operators = [];
140
252
  let quote = null;
@@ -247,7 +359,7 @@ function parseStdinOnlyHeadArgs(segment) {
247
359
  * optional stderr merge, one bounded `head`, and one create-only output path.
248
360
  */
249
361
  export function parseBoundedOutputShellCommand(command) {
250
- const input = trimShellSpacing(command);
362
+ const input = trimShellSpacing(substituteManagedTmpDirToken(command));
251
363
  if (!input)
252
364
  return null;
253
365
  const operators = scanBoundedOutputOperators(input);
@@ -268,6 +380,10 @@ export function parseBoundedOutputShellCommand(command) {
268
380
  const argv = parseLiteralShellArgv(sourceText);
269
381
  if (!argv)
270
382
  return null;
383
+ // `$TMPDIR` is a transport-level path reference, never an argument
384
+ // expansion for the privileged command itself.
385
+ if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
386
+ return null;
271
387
  let mergeStderr = false;
272
388
  let pipe;
273
389
  let redirect;
@@ -302,7 +418,12 @@ export function parseBoundedOutputShellCommand(command) {
302
418
  const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(redirect.end)));
303
419
  if (!outputArgv || outputArgv.length !== 1)
304
420
  return null;
305
- outputPath = outputArgv[0];
421
+ // A single-quoted or escaped `$TMPDIR` is literal shell text, not the
422
+ // managed directory reference. Do not reinterpret it as a route to the
423
+ // session temp root.
424
+ if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
425
+ return null;
426
+ outputPath = restoreManagedTmpDirToken(outputArgv[0]);
306
427
  }
307
428
  return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
308
429
  }
@@ -312,11 +433,21 @@ export function parseBoundedOutputShellCommand(command) {
312
433
  * invocation may leave the sandbox, while shell composition may not.
313
434
  */
314
435
  export function classifyEvolcoreShellCommand(command) {
315
- const literal = parseLiteralShellCommand(command);
436
+ const parseOptions = { allowManagedTmpDir: true };
437
+ const literal = parseLiteralShellCommand(command, parseOptions);
316
438
  if (literal.ok) {
317
- return literal.argv[0] === 'ec'
318
- ? { kind: 'literal' }
319
- : { kind: 'none' };
439
+ if (literal.argv[0] === 'ec')
440
+ return { kind: 'literal' };
441
+ const wrapped = unwrapCodexShellCommandArgv(literal.argv);
442
+ if (wrapped !== undefined) {
443
+ const nested = parseLiteralShellCommand(wrapped, parseOptions);
444
+ if (nested.ok && nested.argv[0] === 'ec')
445
+ return { kind: 'literal' };
446
+ if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
447
+ return { kind: 'composite', issue: nested.issue };
448
+ }
449
+ }
450
+ return { kind: 'none' };
320
451
  }
321
452
  const boundedOutput = parseBoundedOutputShellCommand(command);
322
453
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
@@ -350,8 +481,81 @@ function findSingleFileOption(argv, startIndex) {
350
481
  ? { path: filePath, invalid: false }
351
482
  : { invalid: true };
352
483
  }
353
- export function parseEvolcoreSendCommand(command) {
354
- const argv = parseLiteralShellArgv(command);
484
+ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
485
+ '--format', '--app', '--file', '--text-from-file', '--link', '--payload',
486
+ '--title', '--description', '--text', '--transcript', '--as',
487
+ '--content-type', '--mention', '--aun-path', '--thread', '--return',
488
+ ]);
489
+ const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
490
+ '--encrypt', '--no-encrypt', '--mention-all',
491
+ ]);
492
+ function hasExecutableProactiveSendPayload(argv, scope) {
493
+ if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
494
+ return false;
495
+ const positionals = [];
496
+ const modes = new Map();
497
+ let endOfOptions = false;
498
+ for (let index = 5; index < argv.length; index++) {
499
+ const value = argv[index];
500
+ if (endOfOptions) {
501
+ positionals.push(value);
502
+ continue;
503
+ }
504
+ if (value === '--') {
505
+ endOfOptions = true;
506
+ continue;
507
+ }
508
+ if (PROACTIVE_SEND_BOOLEAN_FLAGS.has(value))
509
+ continue;
510
+ if (!PROACTIVE_SEND_VALUE_FLAGS.has(value)) {
511
+ positionals.push(value);
512
+ continue;
513
+ }
514
+ const operand = argv[++index];
515
+ if (operand === undefined || operand.startsWith('--'))
516
+ return false;
517
+ if (value === '--file' || value === '--text-from-file')
518
+ return false;
519
+ if (value === '--app' || value === '--aun-path')
520
+ return false;
521
+ if (value === '--return' && operand !== 'none' && operand !== 'required')
522
+ return false;
523
+ if (value === '--format' && operand !== 'json')
524
+ return false;
525
+ if (value === '--link') {
526
+ if (scope !== 'msg' || modes.has('link'))
527
+ return false;
528
+ modes.set('link', operand);
529
+ }
530
+ if (value === '--payload') {
531
+ if (modes.has('payload'))
532
+ return false;
533
+ modes.set('payload', operand);
534
+ }
535
+ }
536
+ if (modes.size > 1)
537
+ return false;
538
+ if (modes.has('link')) {
539
+ try {
540
+ const url = new URL(modes.get('link'));
541
+ return url.protocol === 'http:' || url.protocol === 'https:';
542
+ }
543
+ catch {
544
+ return false;
545
+ }
546
+ }
547
+ if (modes.has('payload')) {
548
+ try {
549
+ const payload = JSON.parse(modes.get('payload'));
550
+ return !!payload && typeof payload === 'object' && !Array.isArray(payload);
551
+ }
552
+ catch {
553
+ return false;
554
+ }
555
+ }
556
+ return positionals.join(' ').length > 0;
557
+ }
558
+ function parseEvolcoreSendArgv(argv) {
355
559
  if (!argv || argv[0] !== 'ec')
356
560
  return null;
357
561
  if (argv[1] === 'ctl') {
@@ -380,23 +584,44 @@ export function parseEvolcoreSendCommand(command) {
380
584
  if (subcommand === 'file') {
381
585
  if (argv.length !== 6 || !isDeterministicFilePath(argv[5]))
382
586
  return null;
383
- return { scope, action: 'file', targetId, filePath: argv[5] };
587
+ return { scope, action: 'file', sourceId: argv[3], targetId, filePath: argv[5] };
384
588
  }
385
589
  const fileOption = findSingleFileOption(argv, 5);
386
590
  if (fileOption.invalid)
387
591
  return null;
388
592
  return fileOption.path
389
- ? { scope, action: 'file', targetId, filePath: fileOption.path }
390
- : { scope, action: 'send', targetId };
593
+ ? { scope, action: 'file', sourceId: argv[3], targetId, filePath: fileOption.path }
594
+ : { scope, action: 'send', sourceId: argv[3], targetId };
595
+ }
596
+ export function parseEvolcoreSendCommand(command) {
597
+ const argv = parseCodexShellCommandArgv(command);
598
+ return argv ? parseEvolcoreSendArgv(argv) : null;
391
599
  }
392
600
  export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
393
601
  if (toolName !== 'Bash')
394
602
  return false;
395
- const cmd = typeof input.command === 'string' ? input.command : '';
396
- const parsed = parseEvolcoreSendCommand(cmd);
603
+ const argv = parseCodexToolCommandArgv(input);
604
+ const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
397
605
  if (!parsed)
398
606
  return false;
399
607
  if (parsed.scope === 'ctl')
400
608
  return true;
401
609
  return parsed.targetId === channelId;
402
610
  }
611
+ /** Proactive first-send guard: exact channel kind and current target only. */
612
+ export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
613
+ // Codex hooks use the canonical name `Bash`; the app-server event adapter
614
+ // exposes the same commandExecution items as `Shell` to the renderer.
615
+ if ((toolName !== 'Bash' && toolName !== 'Shell') || !selfAid)
616
+ return false;
617
+ const argv = parseCodexToolCommandArgv(input);
618
+ const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
619
+ if (!parsed || parsed.action !== 'send')
620
+ return false;
621
+ const expectedScope = chatType === 'group' ? 'group' : 'msg';
622
+ return parsed.scope === expectedScope
623
+ && parsed.targetId === channelId
624
+ && parsed.sourceId === selfAid
625
+ && !!argv
626
+ && hasExecutableProactiveSendPayload(argv, expectedScope);
627
+ }
@@ -2,6 +2,6 @@ export * from './tool-policy.js';
2
2
  export * from './ec-command-parser.js';
3
3
  export * from './readonly-shell-query.js';
4
4
  export * from './approval-gateway.js';
5
- export * from './execution-sandbox.js';
5
+ export * from './mode.js';
6
6
  // Tool summaries live in utils; keep the historical permission import path compatible.
7
7
  export { summarizeToolInput } from '../../utils/tool-summary.js';
@@ -22,3 +22,18 @@ export function normalizePermissionMode(value) {
22
22
  return { mode: 'readonly', workflow: 'plan', migratedFrom: 'plan' };
23
23
  return { mode: 'readonly' };
24
24
  }
25
+ /**
26
+ * Phase one only disables the task execution sandbox for an authenticated
27
+ * owner running in bypass mode. Every other combination remains fail-closed.
28
+ */
29
+ export function resolvePhaseOneExecutionSandbox(permissionMode, role) {
30
+ const normalizedMode = normalizePermissionMode(permissionMode).mode;
31
+ const normalizedRole = role ?? undefined;
32
+ const sandboxOff = normalizedMode === 'bypass' && normalizedRole === 'owner';
33
+ return {
34
+ permissionMode: normalizedMode,
35
+ role: normalizedRole,
36
+ state: sandboxOff ? 'off' : 'active',
37
+ reason: sandboxOff ? 'owner-bypass' : 'phase-one-required',
38
+ };
39
+ }
@@ -3,7 +3,7 @@ import { parseLiteralShellArgv } from './ec-command-parser.js';
3
3
  const MAX_QUERY_LISTS = 8;
4
4
  const MAX_PIPELINE_STAGES = 3;
5
5
  const SUPPORTED_EXECUTABLES = new Set([
6
- 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort',
6
+ 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat',
7
7
  ]);
8
8
  const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
9
9
  const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
@@ -71,8 +71,17 @@ function splitQueryStructure(command) {
71
71
  segment += char;
72
72
  continue;
73
73
  }
74
- if (char === '\0' || char === '\r' || char === '\n' || char === '&' || char === '<')
74
+ if (char === '\0' || char === '\r' || char === '\n' || char === '<')
75
75
  return null;
76
+ if (char === '&') {
77
+ // A bounded sequence of independently proven read-only commands is
78
+ // safe to run in order. A single ampersand would background execution;
79
+ // `||` remains rejected because it introduces conditional control flow.
80
+ if (command[index + 1] !== '&' || !finishPipeline())
81
+ return null;
82
+ index++;
83
+ continue;
84
+ }
76
85
  if (char === '|') {
77
86
  if (command[index + 1] === '|' || !finishStage())
78
87
  return null;
@@ -89,12 +98,14 @@ function splitQueryStructure(command) {
89
98
  return null;
90
99
  return pipelines;
91
100
  }
92
- function parseStageArgv(segment) {
101
+ function parseStageArgv(segment, options) {
93
102
  const redirectMatch = segment.match(STDERR_DEV_NULL_RE);
94
103
  const withoutRedirect = redirectMatch?.[1] ?? segment;
95
104
  if (withoutRedirect.includes('>'))
96
105
  return null;
97
- return parseLiteralShellArgv(withoutRedirect.trim());
106
+ return parseLiteralShellArgv(withoutRedirect.trim(), options.managedTempDir
107
+ ? { allowManagedTmpDir: true, managedTempDir: options.managedTempDir }
108
+ : undefined);
98
109
  }
99
110
  function exact(value) {
100
111
  return { value, access: 'exact' };
@@ -458,12 +469,118 @@ function analyzeSort(args) {
458
469
  }
459
470
  return paths;
460
471
  }
472
+ /**
473
+ * Git metadata queries do not expose file contents and their output paths are
474
+ * controlled by the repository, not by arbitrary shell operands. Keep this
475
+ * deliberately small: status and read-only diff summaries are the patterns
476
+ * used for workspace diagnostics. Mutating subcommands and pathspecs remain
477
+ * unproven so the normal H-class checks still apply.
478
+ */
479
+ function analyzeGit(args) {
480
+ if (args.length === 0)
481
+ return null;
482
+ const subcommand = args[0];
483
+ if (subcommand === 'status') {
484
+ // Keep the allow-list explicit; callers should request a bounded metadata
485
+ // view such as `--short` or `--branch`, rather than inheriting Git's full
486
+ // default output and any future options.
487
+ if (args.length === 1)
488
+ return null;
489
+ const allowed = new Set([
490
+ '--short', '--porcelain', '--porcelain=v1', '--porcelain=v2', '--branch',
491
+ '--untracked-files=no', '--untracked-files=normal', '--ignored=no',
492
+ '--ignore-submodules', '--renames', '--no-renames', '--ahead-behind',
493
+ '--no-ahead-behind', '--show-stash', '--no-optional-locks',
494
+ ]);
495
+ for (const arg of args.slice(1)) {
496
+ if (arg === '--')
497
+ return null;
498
+ if (!allowed.has(arg))
499
+ return null;
500
+ }
501
+ return [];
502
+ }
503
+ if (subcommand === 'diff') {
504
+ let sawSummary = false;
505
+ let optionsEnded = false;
506
+ const paths = [];
507
+ const allowedFlags = new Set([
508
+ '--stat', '--shortstat', '--numstat', '--name-only', '--name-status',
509
+ '--no-ext-diff', '--no-renames', '--no-textconv', '--no-index',
510
+ '--cached', '--staged', '--merge-base', '--no-color', '--color=never',
511
+ ]);
512
+ for (const arg of args.slice(1)) {
513
+ if (!optionsEnded && arg === '--') {
514
+ optionsEnded = true;
515
+ continue;
516
+ }
517
+ if (!optionsEnded && arg.startsWith('-')) {
518
+ if (arg === '--stat' || arg === '--shortstat' || arg === '--numstat'
519
+ || arg === '--name-only' || arg === '--name-status')
520
+ sawSummary = true;
521
+ if (!allowedFlags.has(arg) && !/^[0-9a-f]{7,64}$/.test(arg))
522
+ return null;
523
+ continue;
524
+ }
525
+ // Revisions are safe literals; pathspecs after -- are bounded exact reads.
526
+ if (optionsEnded)
527
+ paths.push({ value: arg, access: 'exact' });
528
+ else if (!/^[0-9A-Za-z._~^/:-]+$/.test(arg))
529
+ return null;
530
+ }
531
+ return sawSummary ? paths : null;
532
+ }
533
+ return null;
534
+ }
535
+ function analyzeWc(args) {
536
+ const allowed = new Set(['-l', '-w', '-c', '-m', '-L', '--lines', '--words', '--bytes', '--chars', '--max-line-length']);
537
+ const paths = [];
538
+ for (const arg of args) {
539
+ if (arg.startsWith('-')) {
540
+ if (!allowed.has(arg))
541
+ return null;
542
+ }
543
+ else if (arg !== '-') {
544
+ paths.push(exact(arg));
545
+ }
546
+ }
547
+ return paths;
548
+ }
549
+ function analyzeStat(args) {
550
+ // GNU stat defaults to lstat semantics. Following a final symlink would
551
+ // let a metadata-only query reach a protected target through an alias.
552
+ if (args.some(argument => argument === '-L' || argument === '--dereference'))
553
+ return null;
554
+ const valueOptions = new Set(['-c', '--format', '--printf']);
555
+ const paths = [];
556
+ let optionsEnded = false;
557
+ for (let index = 0; index < args.length; index++) {
558
+ const argument = args[index];
559
+ if (!optionsEnded && argument === '--') {
560
+ optionsEnded = true;
561
+ continue;
562
+ }
563
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
564
+ const option = argument.split('=', 1)[0];
565
+ if (valueOptions.has(option) && !argument.includes('=') && ++index >= args.length)
566
+ return null;
567
+ if (!valueOptions.has(option) && !new Set(['-f', '--file-system', '-t', '--terse', '--cached']).has(option)) {
568
+ return null;
569
+ }
570
+ continue;
571
+ }
572
+ if (argument !== '-')
573
+ paths.push(exact(argument));
574
+ }
575
+ return paths.length > 0 ? paths : null;
576
+ }
461
577
  function analyzeCommand(argv) {
462
578
  const executable = normalizeExecutable(argv[0]);
463
579
  if (!executable)
464
580
  return null;
465
581
  const args = argv.slice(1);
466
582
  let pathOperands = null;
583
+ let accessKind = 'content';
467
584
  switch (executable) {
468
585
  case 'echo':
469
586
  pathOperands = [];
@@ -495,12 +612,35 @@ function analyzeCommand(argv) {
495
612
  case 'sort':
496
613
  pathOperands = analyzeSort(args);
497
614
  break;
615
+ case 'git':
616
+ pathOperands = analyzeGit(args);
617
+ break;
618
+ case 'wc':
619
+ pathOperands = analyzeWc(args);
620
+ break;
621
+ case 'stat':
622
+ case 'lstat':
623
+ pathOperands = analyzeStat(args);
624
+ accessKind = 'metadata';
625
+ break;
498
626
  }
499
627
  if (!pathOperands)
500
628
  return null;
501
- return { command: { executable, argv, pathOperands } };
629
+ if (['ls', 'find'].includes(executable))
630
+ accessKind = 'enumerate';
631
+ return {
632
+ command: {
633
+ executable,
634
+ argv,
635
+ pathOperands,
636
+ // Keep the established IR shape for ordinary queries. Only metadata
637
+ // commands need an explicit action marker; content/enumeration are
638
+ // derived from their operands by policy consumers.
639
+ ...(accessKind === 'metadata' ? { accessKind } : {}),
640
+ },
641
+ };
502
642
  }
503
- export function analyzeReadonlyShellQuery(command) {
643
+ export function analyzeReadonlyShellQuery(command, options = {}) {
504
644
  const structure = splitQueryStructure(command);
505
645
  if (!structure)
506
646
  return { kind: 'unproven', reason: 'unsupported-shell-structure' };
@@ -509,14 +649,14 @@ export function analyzeReadonlyShellQuery(command) {
509
649
  for (const stages of structure) {
510
650
  const pipeline = [];
511
651
  for (let index = 0; index < stages.length; index++) {
512
- const argv = parseStageArgv(stages[index]);
652
+ const argv = parseStageArgv(stages[index], options);
513
653
  if (!argv)
514
654
  return { kind: 'unproven', reason: 'non-literal-command' };
515
655
  const analyzed = analyzeCommand(argv);
516
656
  if (!analyzed)
517
657
  return { kind: 'unproven', reason: 'unsupported-command-or-option' };
518
658
  if (index > 0) {
519
- if (!['head', 'sort'].includes(analyzed.command.executable)) {
659
+ if (!['head', 'sort', 'grep', 'wc'].includes(analyzed.command.executable)) {
520
660
  return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
521
661
  }
522
662
  if (analyzed.command.pathOperands.length > 0) {