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.
- package/CHANGELOG.md +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -2
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +243 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/command/evol-menu-version-gate.js +0 -39
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- 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);
|
|
@@ -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(
|
|
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
|
-
? {
|
|
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
|
-
|
|
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
|
|
436
|
+
const parseOptions = { allowManagedTmpDir: true };
|
|
437
|
+
const literal = parseLiteralShellCommand(command, parseOptions);
|
|
316
438
|
if (literal.ok) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
396
|
-
const parsed =
|
|
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 './
|
|
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 === '
|
|
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
|
-
|
|
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) {
|