evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -1,15 +1,32 @@
|
|
|
1
|
+
import { isTrustedWindowsShellExecutable } from '../../utils/windows-shell-trust.js';
|
|
1
2
|
const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
|
|
2
3
|
// Codex app-server sends command executions through a fixed shell carrier.
|
|
3
4
|
// Treat the carrier as syntax only after parsing it as literal argv; never
|
|
4
5
|
// accept an arbitrary shell command string or unwrap composition here.
|
|
5
6
|
const CODEX_POSIX_SHELL_EXECUTABLES = new Set([
|
|
6
|
-
'bash', '/bin/bash', '/usr/bin/bash',
|
|
7
|
-
'sh', '/bin/sh', '/usr/bin/sh',
|
|
7
|
+
'bash', 'bash.exe', '/bin/bash', '/usr/bin/bash',
|
|
8
|
+
'sh', 'sh.exe', '/bin/sh', '/usr/bin/sh',
|
|
8
9
|
]);
|
|
9
10
|
const CODEX_POWERSHELL_EXECUTABLES = new Set([
|
|
10
11
|
'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
|
|
11
12
|
]);
|
|
12
13
|
const CODEX_POWERSHELL_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Program Files[\\/]PowerShell[\\/][0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9.-]+)?[\\/]pwsh\.exe|Windows[\\/]System32[\\/]WindowsPowerShell[\\/]v1\.0[\\/]powershell\.exe)$/i;
|
|
14
|
+
const CODEX_CUSTOM_POSIX_SHELL_NAMES = new Set(['bash', 'bash.exe', 'sh', 'sh.exe']);
|
|
15
|
+
const CODEX_CUSTOM_POWERSHELL_NAMES = new Set(['pwsh.exe', 'powershell.exe']);
|
|
16
|
+
const CODEX_CUSTOM_CMD_NAMES = new Set(['cmd.exe']);
|
|
17
|
+
const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
|
|
18
|
+
const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
|
|
19
|
+
// These switches affect only cmd startup/output formatting. `/c` is handled
|
|
20
|
+
// separately as the required execute switch. Flags such as `/k`, `/v:on`,
|
|
21
|
+
// `/e:*`, and `/f:*` can change process lifetime or command semantics and are
|
|
22
|
+
// intentionally not treated as a trusted Codex carrier.
|
|
23
|
+
const CODEX_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
|
|
24
|
+
const EVOLCORE_EXECUTABLE_ALIASES = new Set(['ec', 'ec.cmd', 'ec.exe']);
|
|
25
|
+
const EVOLCORE_NODE_EXECUTABLES = new Set(['node', 'node.exe']);
|
|
26
|
+
// Development/install entrypoints that are part of this repository's CLI.
|
|
27
|
+
// Only these exact relative paths are canonicalized; arbitrary Node scripts
|
|
28
|
+
// must never become an EC command merely because their filename is similar.
|
|
29
|
+
const EVOLCORE_NODE_WRAPPER_PATHS = new Set(['bin/ec.js', 'dist/cli/index.js']);
|
|
13
30
|
const SHELL_UNQUOTED_META = new Set([
|
|
14
31
|
';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
|
|
15
32
|
]);
|
|
@@ -61,8 +78,7 @@ function hasEcCommandPrefix(command) {
|
|
|
61
78
|
}
|
|
62
79
|
if (boundary && /\s/.test(char))
|
|
63
80
|
continue;
|
|
64
|
-
if (boundary && command.slice(index
|
|
65
|
-
&& (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
|
|
81
|
+
if (boundary && /^ec(?:\.cmd|\.exe)?(?=$|\s|[;&|`()<>])/i.test(command.slice(index)))
|
|
66
82
|
return true;
|
|
67
83
|
boundary = false;
|
|
68
84
|
}
|
|
@@ -71,6 +87,45 @@ function hasEcCommandPrefix(command) {
|
|
|
71
87
|
function trimShellSpacing(value) {
|
|
72
88
|
return value.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
73
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Accept an explicitly materialized absolute shell path, including Windows
|
|
92
|
+
* Store/App Execution Alias paths such as `...\\WindowsApps\\pwsh.exe`.
|
|
93
|
+
* Relative names remain restricted to the fixed executable allowlists above.
|
|
94
|
+
*/
|
|
95
|
+
function isAbsoluteCustomShellPath(value, executableNames) {
|
|
96
|
+
if (!value || /[\0\r\n"'<>|?*]/.test(value))
|
|
97
|
+
return false;
|
|
98
|
+
const normalized = value.replaceAll('\\', '/');
|
|
99
|
+
if (!/^[A-Za-z]:\//.test(normalized) && !normalized.startsWith('//'))
|
|
100
|
+
return false;
|
|
101
|
+
const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
|
|
102
|
+
return executableNames.has(basename);
|
|
103
|
+
}
|
|
104
|
+
function isNodeExecutable(value) {
|
|
105
|
+
if (!value)
|
|
106
|
+
return false;
|
|
107
|
+
const normalized = value.toLowerCase();
|
|
108
|
+
return EVOLCORE_NODE_EXECUTABLES.has(normalized)
|
|
109
|
+
|| isAbsoluteCustomShellPath(value, EVOLCORE_NODE_EXECUTABLES);
|
|
110
|
+
}
|
|
111
|
+
function normalizeNodeWrapperPath(value) {
|
|
112
|
+
const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '').toLowerCase();
|
|
113
|
+
return EVOLCORE_NODE_WRAPPER_PATHS.has(normalized) ? normalized : undefined;
|
|
114
|
+
}
|
|
115
|
+
function isCodexPosixShellExecutable(value) {
|
|
116
|
+
if (!value)
|
|
117
|
+
return false;
|
|
118
|
+
return CODEX_POSIX_SHELL_EXECUTABLES.has(value.toLowerCase())
|
|
119
|
+
|| isAbsoluteCustomShellPath(value, CODEX_CUSTOM_POSIX_SHELL_NAMES);
|
|
120
|
+
}
|
|
121
|
+
function isCodexPowerShellExecutable(value) {
|
|
122
|
+
if (!value)
|
|
123
|
+
return false;
|
|
124
|
+
const normalized = value.toLowerCase();
|
|
125
|
+
return CODEX_POWERSHELL_EXECUTABLES.has(normalized)
|
|
126
|
+
|| CODEX_POWERSHELL_INSTALL_PATH_RE.test(value)
|
|
127
|
+
|| isAbsoluteCustomShellPath(value, CODEX_CUSTOM_POWERSHELL_NAMES);
|
|
128
|
+
}
|
|
74
129
|
const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
|
|
75
130
|
/**
|
|
76
131
|
* Preserve the one environment reference allowed in managed EC paths.
|
|
@@ -140,12 +195,179 @@ function isLiteralArgBoundary(value) {
|
|
|
140
195
|
function isShellParameterExpansionStart(value) {
|
|
141
196
|
return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
|
|
142
197
|
}
|
|
198
|
+
function hasCarrierEcCommandPrefix(command, dialect) {
|
|
199
|
+
if (dialect !== 'cmd')
|
|
200
|
+
return hasEcCommandPrefix(command);
|
|
201
|
+
const input = trimShellSpacing(command);
|
|
202
|
+
const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
|
|
203
|
+
? input.slice(1, -1)
|
|
204
|
+
: input;
|
|
205
|
+
return /^\s*ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(inner);
|
|
206
|
+
}
|
|
207
|
+
/** Normalize the Windows npm shims to the canonical EvolCore CLI name. */
|
|
208
|
+
export function normalizeEvolcoreCommandArgv(argv) {
|
|
209
|
+
if (argv.length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const executable = argv[0];
|
|
212
|
+
if (typeof executable === 'string' && EVOLCORE_EXECUTABLE_ALIASES.has(executable.toLowerCase())) {
|
|
213
|
+
return ['ec', ...argv.slice(1)];
|
|
214
|
+
}
|
|
215
|
+
if (typeof executable === 'string' && isNodeExecutable(executable) && argv.length >= 2
|
|
216
|
+
&& typeof argv[1] === 'string' && normalizeNodeWrapperPath(argv[1])) {
|
|
217
|
+
return ['ec', ...argv.slice(2)];
|
|
218
|
+
}
|
|
219
|
+
return [...argv];
|
|
220
|
+
}
|
|
221
|
+
/** Detect the historical PowerShell pattern that overwrote the thread carrier. */
|
|
222
|
+
export function hasPowerShellDelegationAssignment(command) {
|
|
223
|
+
return /^\s*\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(command)
|
|
224
|
+
&& /\bec(?:\.cmd|\.exe)?(?:\s|$)/i.test(command);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Windows Codex may materialize one environment variable assignment directly
|
|
228
|
+
* in the PowerShell command body instead of inheriting it from the process
|
|
229
|
+
* environment. Only the EvolCore delegation variable is accepted, and only
|
|
230
|
+
* when followed by one command on the same carrier. The value is validated for
|
|
231
|
+
* shape but never trusted for authorization; the runner still arms the stable
|
|
232
|
+
* in-memory thread carrier.
|
|
233
|
+
*/
|
|
234
|
+
function unwrapPowerShellDelegationPrefix(command) {
|
|
235
|
+
const input = command.trim();
|
|
236
|
+
if (!/^\$env\s*:\s*EVOLCORE_DELEGATION_TOKEN\b/i.test(input)) {
|
|
237
|
+
return { kind: 'none', command };
|
|
238
|
+
}
|
|
239
|
+
let index = 0;
|
|
240
|
+
const skipSpace = () => {
|
|
241
|
+
while (index < input.length && (input[index] === ' ' || input[index] === '\t'))
|
|
242
|
+
index++;
|
|
243
|
+
};
|
|
244
|
+
const consume = (value) => {
|
|
245
|
+
if (input.slice(index, index + value.length).toLowerCase() !== value.toLowerCase())
|
|
246
|
+
return false;
|
|
247
|
+
index += value.length;
|
|
248
|
+
return true;
|
|
249
|
+
};
|
|
250
|
+
skipSpace();
|
|
251
|
+
if (!consume('$env') || input[index] !== ':')
|
|
252
|
+
return { kind: 'invalid', command };
|
|
253
|
+
index++;
|
|
254
|
+
skipSpace();
|
|
255
|
+
if (!consume('EVOLCORE_DELEGATION_TOKEN'))
|
|
256
|
+
return { kind: 'invalid', command };
|
|
257
|
+
skipSpace();
|
|
258
|
+
if (input[index] !== '=')
|
|
259
|
+
return { kind: 'invalid', command };
|
|
260
|
+
index++;
|
|
261
|
+
skipSpace();
|
|
262
|
+
const quote = input[index];
|
|
263
|
+
if (quote !== "'" && quote !== '"')
|
|
264
|
+
return { kind: 'invalid', command };
|
|
265
|
+
const valueStart = index++;
|
|
266
|
+
let value = '';
|
|
267
|
+
let closed = false;
|
|
268
|
+
// When the complete -Command body is itself single-quoted, command-line
|
|
269
|
+
// serialization can leave the assignment value in a doubled `''token''`
|
|
270
|
+
// form. Its token alphabet cannot contain quotes, so this exact shape is
|
|
271
|
+
// unambiguous and can be decoded without executing PowerShell grammar.
|
|
272
|
+
if (quote === "'" && input[valueStart + 1] === "'") {
|
|
273
|
+
const contentStart = valueStart + 2;
|
|
274
|
+
const end = input.indexOf("''", contentStart);
|
|
275
|
+
if (end < contentStart)
|
|
276
|
+
return { kind: 'invalid', command };
|
|
277
|
+
value = input.slice(contentStart, end);
|
|
278
|
+
index = end + 2;
|
|
279
|
+
closed = true;
|
|
280
|
+
}
|
|
281
|
+
while (index < input.length) {
|
|
282
|
+
if (closed)
|
|
283
|
+
break;
|
|
284
|
+
const char = input[index];
|
|
285
|
+
if (char === quote) {
|
|
286
|
+
// PowerShell single-quoted strings escape a quote as `''`. For the
|
|
287
|
+
// delegation token alphabet this is not needed, but accepting it lets us
|
|
288
|
+
// decode the doubled-quote form emitted by powershell.exe argv quoting.
|
|
289
|
+
if (quote === "'" && input[index + 1] === "'") {
|
|
290
|
+
value += "'";
|
|
291
|
+
index += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
index++;
|
|
295
|
+
closed = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
if (quote === '"' && char === '`')
|
|
299
|
+
return { kind: 'invalid', command };
|
|
300
|
+
if (char === '\r' || char === '\n')
|
|
301
|
+
return { kind: 'invalid', command };
|
|
302
|
+
value += char;
|
|
303
|
+
index++;
|
|
304
|
+
}
|
|
305
|
+
if (!closed || index <= valueStart + 1 || !/^[A-Za-z0-9_-]{32,128}$/.test(value)) {
|
|
306
|
+
return { kind: 'invalid', command };
|
|
307
|
+
}
|
|
308
|
+
if (input[index] !== ';')
|
|
309
|
+
return { kind: 'invalid', command };
|
|
310
|
+
index++;
|
|
311
|
+
const remainder = input.slice(index).trim();
|
|
312
|
+
if (!remainder)
|
|
313
|
+
return { kind: 'invalid', command };
|
|
314
|
+
return { kind: 'valid', command: remainder, token: value };
|
|
315
|
+
}
|
|
316
|
+
export function parseCodexCarrierCommand(carrier, options = {}) {
|
|
317
|
+
if (options.verifyShellExecutable) {
|
|
318
|
+
if (!carrier.executable) {
|
|
319
|
+
return parseFailure('untrusted-shell', carrier.command, 0, 0, carrier.dialect);
|
|
320
|
+
}
|
|
321
|
+
const kind = carrier.dialect === 'posix' ? 'bash' : carrier.dialect;
|
|
322
|
+
if (!isTrustedWindowsShellExecutable(carrier.executable, kind)) {
|
|
323
|
+
return parseFailure('untrusted-shell', carrier.command, 0, 0, carrier.dialect);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const unwrapped = carrier.dialect === 'powershell'
|
|
327
|
+
? unwrapPowerShellDelegationPrefix(carrier.command)
|
|
328
|
+
: { kind: 'none', command: carrier.command };
|
|
329
|
+
// Windows PowerShell may materialize the thread's shell environment as a
|
|
330
|
+
// single `$env:EVOLCORE_DELEGATION_TOKEN = '...'` prefix. Keep the token as
|
|
331
|
+
// metadata for the approval layer, which must compare it with the runner's
|
|
332
|
+
// in-memory carrier before arming delegation. Malformed or unrelated
|
|
333
|
+
// assignments remain syntax failures and can never be authorized.
|
|
334
|
+
if (unwrapped.kind === 'invalid'
|
|
335
|
+
|| (carrier.dialect === 'powershell'
|
|
336
|
+
&& hasPowerShellDelegationAssignment(carrier.command)
|
|
337
|
+
&& unwrapped.kind === 'none')
|
|
338
|
+
|| (unwrapped.kind === 'valid'
|
|
339
|
+
&& options.expectedDelegationToken !== undefined
|
|
340
|
+
&& options.expectedDelegationToken !== unwrapped.token)) {
|
|
341
|
+
return parseFailure('shell-composition', carrier.command, 0, 0, carrier.dialect);
|
|
342
|
+
}
|
|
343
|
+
const parsed = parseLiteralShellCommand(unwrapped.command, {
|
|
344
|
+
...options,
|
|
345
|
+
dialect: carrier.dialect,
|
|
346
|
+
});
|
|
347
|
+
return parsed.ok
|
|
348
|
+
? {
|
|
349
|
+
...parsed,
|
|
350
|
+
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
351
|
+
...(unwrapped.kind === 'valid' ? { delegationToken: unwrapped.token } : {}),
|
|
352
|
+
}
|
|
353
|
+
: parsed;
|
|
354
|
+
}
|
|
355
|
+
function parseFailure(issue, input, offset, tokenIndex, dialect) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
issue,
|
|
359
|
+
offset: Math.max(0, Math.min(offset, input.length)),
|
|
360
|
+
tokenIndex: Math.max(0, tokenIndex),
|
|
361
|
+
dialect,
|
|
362
|
+
inputLength: input.length,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
143
365
|
function parseLiteralPowerShellCommand(command) {
|
|
144
366
|
const input = trimShellSpacing(command);
|
|
145
367
|
if (!input)
|
|
146
|
-
return
|
|
368
|
+
return parseFailure('empty-command', input, 0, 0, 'powershell');
|
|
147
369
|
if (input.includes('\0'))
|
|
148
|
-
return
|
|
370
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
|
|
149
371
|
const argv = [];
|
|
150
372
|
let token = '';
|
|
151
373
|
let tokenStarted = false;
|
|
@@ -166,7 +388,7 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
166
388
|
}
|
|
167
389
|
else if (char === "'") {
|
|
168
390
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
169
|
-
return
|
|
391
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
170
392
|
quote = null;
|
|
171
393
|
}
|
|
172
394
|
else {
|
|
@@ -177,21 +399,21 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
177
399
|
if (quote === 'double') {
|
|
178
400
|
if (char === '"') {
|
|
179
401
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
180
|
-
return
|
|
402
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
181
403
|
quote = null;
|
|
182
404
|
continue;
|
|
183
405
|
}
|
|
184
406
|
if (char === '`') {
|
|
185
407
|
const next = input[++index];
|
|
186
408
|
if (next === undefined)
|
|
187
|
-
return
|
|
409
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
188
410
|
if (next === '\r' || next === '\n')
|
|
189
|
-
return
|
|
411
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
190
412
|
token += next;
|
|
191
413
|
continue;
|
|
192
414
|
}
|
|
193
415
|
if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
|
|
194
|
-
return
|
|
416
|
+
return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
|
|
195
417
|
}
|
|
196
418
|
token += char;
|
|
197
419
|
continue;
|
|
@@ -201,9 +423,9 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
201
423
|
continue;
|
|
202
424
|
}
|
|
203
425
|
if (char === '\r' || char === '\n')
|
|
204
|
-
return
|
|
426
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
205
427
|
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
206
|
-
return
|
|
428
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
|
|
207
429
|
}
|
|
208
430
|
if (char === "'") {
|
|
209
431
|
tokenStarted = true;
|
|
@@ -218,9 +440,9 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
218
440
|
if (char === '`') {
|
|
219
441
|
const next = input[++index];
|
|
220
442
|
if (next === undefined)
|
|
221
|
-
return
|
|
443
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
222
444
|
if (next === '\r' || next === '\n')
|
|
223
|
-
return
|
|
445
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
224
446
|
tokenStarted = true;
|
|
225
447
|
token += next;
|
|
226
448
|
continue;
|
|
@@ -228,17 +450,264 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
228
450
|
const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
|
|
229
451
|
const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
|
|
230
452
|
if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
|
|
231
|
-
return
|
|
453
|
+
return parseFailure('shell-composition', input, index, argv.length, 'powershell');
|
|
232
454
|
}
|
|
233
455
|
tokenStarted = true;
|
|
234
456
|
token += char;
|
|
235
457
|
}
|
|
236
458
|
if (quote)
|
|
237
|
-
return
|
|
459
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'powershell');
|
|
238
460
|
finishToken();
|
|
239
461
|
return argv.length > 0
|
|
240
462
|
? { ok: true, argv }
|
|
241
|
-
:
|
|
463
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'powershell');
|
|
464
|
+
}
|
|
465
|
+
function parseLiteralCmdCommand(command, options = {}) {
|
|
466
|
+
let input = trimShellSpacing(command);
|
|
467
|
+
if (!input)
|
|
468
|
+
return parseFailure('empty-command', input, 0, 0, 'cmd');
|
|
469
|
+
if (input.includes('\0'))
|
|
470
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'cmd');
|
|
471
|
+
// `cmd.exe /c` commonly wraps the complete command in one quote pair.
|
|
472
|
+
// Those quotes belong to the carrier, not to the first EC argument; remove
|
|
473
|
+
// exactly that outer pair before tokenizing the inner command.
|
|
474
|
+
if (input.length >= 2 && input.startsWith('"') && input.endsWith('"')) {
|
|
475
|
+
input = input.slice(1, -1);
|
|
476
|
+
}
|
|
477
|
+
const argv = [];
|
|
478
|
+
let token = '';
|
|
479
|
+
let tokenStarted = false;
|
|
480
|
+
let quote = false;
|
|
481
|
+
const finishToken = () => {
|
|
482
|
+
if (!tokenStarted)
|
|
483
|
+
return;
|
|
484
|
+
argv.push(token);
|
|
485
|
+
token = '';
|
|
486
|
+
tokenStarted = false;
|
|
487
|
+
};
|
|
488
|
+
for (let index = 0; index < input.length; index++) {
|
|
489
|
+
const char = input[index];
|
|
490
|
+
if (quote) {
|
|
491
|
+
if (char === '"') {
|
|
492
|
+
// cmd.exe represents a literal quote in a quoted argument as `""`.
|
|
493
|
+
if (input[index + 1] === '"') {
|
|
494
|
+
token += '"';
|
|
495
|
+
index++;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
quote = false;
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
// Environment expansion and caret escaping remain active in cmd quoted
|
|
503
|
+
// strings, so accepting either would make the canonical argv differ
|
|
504
|
+
// from what cmd.exe actually executes.
|
|
505
|
+
if (char === '%' || char === '!' || char === '^') {
|
|
506
|
+
return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
|
|
507
|
+
}
|
|
508
|
+
if (char === '\r' || char === '\n')
|
|
509
|
+
return parseFailure('structural-newline', input, index, argv.length, 'cmd');
|
|
510
|
+
token += char;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (char === ' ' || char === '\t') {
|
|
514
|
+
finishToken();
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (char === '\r' || char === '\n')
|
|
518
|
+
return parseFailure('structural-newline', input, index, argv.length, 'cmd');
|
|
519
|
+
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
520
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'cmd');
|
|
521
|
+
}
|
|
522
|
+
if (char === '"') {
|
|
523
|
+
tokenStarted = true;
|
|
524
|
+
quote = true;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (char === '%' || char === '!') {
|
|
528
|
+
return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
|
|
529
|
+
}
|
|
530
|
+
if (char === '^' || char === '&' || char === '|' || char === '<' || char === '>'
|
|
531
|
+
|| char === ';' || char === '(' || char === ')' || char === '*' || char === '?'
|
|
532
|
+
|| char === '[' || char === ']' || char === '{' || char === '}') {
|
|
533
|
+
return parseFailure('shell-composition', input, index, argv.length, 'cmd');
|
|
534
|
+
}
|
|
535
|
+
tokenStarted = true;
|
|
536
|
+
token += char;
|
|
537
|
+
}
|
|
538
|
+
if (quote)
|
|
539
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'cmd');
|
|
540
|
+
finishToken();
|
|
541
|
+
return argv.length > 0
|
|
542
|
+
? {
|
|
543
|
+
ok: true,
|
|
544
|
+
argv: options.allowManagedTmpDir
|
|
545
|
+
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
546
|
+
: argv,
|
|
547
|
+
}
|
|
548
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'cmd');
|
|
549
|
+
}
|
|
550
|
+
function isCodexCmdExecutable(value) {
|
|
551
|
+
if (!value)
|
|
552
|
+
return false;
|
|
553
|
+
const normalized = value.toLowerCase();
|
|
554
|
+
return CODEX_CMD_EXECUTABLES.has(normalized)
|
|
555
|
+
|| CODEX_CMD_INSTALL_PATH_RE.test(value)
|
|
556
|
+
|| isAbsoluteCustomShellPath(value, CODEX_CUSTOM_CMD_NAMES);
|
|
557
|
+
}
|
|
558
|
+
function resolveCodexCmdCarrierArgv(argv) {
|
|
559
|
+
if (argv.length < 3 || !isCodexCmdExecutable(argv[0]))
|
|
560
|
+
return undefined;
|
|
561
|
+
const executeIndex = argv.slice(1, -1).findIndex(value => value.toLowerCase() === '/c');
|
|
562
|
+
if (executeIndex < 0)
|
|
563
|
+
return undefined;
|
|
564
|
+
const absoluteExecuteIndex = executeIndex + 1;
|
|
565
|
+
// The command must remain one argv item. Joining multiple items would lose
|
|
566
|
+
// the quoting boundary that the Windows launcher actually executed.
|
|
567
|
+
if (absoluteExecuteIndex !== argv.length - 2)
|
|
568
|
+
return undefined;
|
|
569
|
+
const seen = new Set();
|
|
570
|
+
for (const rawSwitch of argv.slice(1, absoluteExecuteIndex)) {
|
|
571
|
+
const switchValue = rawSwitch.toLowerCase();
|
|
572
|
+
if (!CODEX_CMD_INERT_SWITCHES.has(switchValue) || seen.has(switchValue))
|
|
573
|
+
return undefined;
|
|
574
|
+
seen.add(switchValue);
|
|
575
|
+
}
|
|
576
|
+
const command = argv.at(-1);
|
|
577
|
+
return typeof command === 'string' && command.trim()
|
|
578
|
+
? { command, dialect: 'cmd', executable: argv[0] }
|
|
579
|
+
: undefined;
|
|
580
|
+
}
|
|
581
|
+
/** Resolve a Windows cmd carrier when the app-server provides only a string. */
|
|
582
|
+
function resolveCodexCmdCarrierString(command) {
|
|
583
|
+
const input = trimShellSpacing(command);
|
|
584
|
+
// The executable may be quoted when the app-server serializes a Windows
|
|
585
|
+
// path. Parse the prefix into argv-like switches, then use the same
|
|
586
|
+
// validation as the commandArgv path so both representations agree.
|
|
587
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
|
|
588
|
+
if (!match)
|
|
589
|
+
return undefined;
|
|
590
|
+
const executable = match[1] ?? match[2];
|
|
591
|
+
const rawSwitches = match[3].trim();
|
|
592
|
+
const switches = rawSwitches ? rawSwitches.split(/[ \t]+/) : [];
|
|
593
|
+
const argv = [executable, ...switches, match[4]];
|
|
594
|
+
return resolveCodexCmdCarrierArgv(argv);
|
|
595
|
+
}
|
|
596
|
+
/** Resolve a PowerShell carrier when the app-server provides only a string. */
|
|
597
|
+
function resolveCodexPowerShellCarrierString(command) {
|
|
598
|
+
const input = trimShellSpacing(command);
|
|
599
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+-command\s+(.+)$/i);
|
|
600
|
+
if (!match)
|
|
601
|
+
return undefined;
|
|
602
|
+
const executable = match[1] ?? match[2];
|
|
603
|
+
if (!isCodexPowerShellExecutable(executable))
|
|
604
|
+
return undefined;
|
|
605
|
+
// A string-form carrier has one additional command-line quoting layer. The
|
|
606
|
+
// command argument is commonly serialized as `"ec ..."`; remove exactly
|
|
607
|
+
// that outer pair and leave quotes belonging to EC arguments intact.
|
|
608
|
+
let nested = match[3].trim();
|
|
609
|
+
if (nested.length >= 2
|
|
610
|
+
&& ((nested.startsWith('"') && nested.endsWith('"'))
|
|
611
|
+
|| (nested.startsWith("'") && nested.endsWith("'")))) {
|
|
612
|
+
nested = nested.slice(1, -1);
|
|
613
|
+
}
|
|
614
|
+
return nested ? { command: nested, dialect: 'powershell', executable } : undefined;
|
|
615
|
+
}
|
|
616
|
+
/** Resolve a POSIX shell carrier when app-server provides only a string. */
|
|
617
|
+
function resolveCodexPosixShellCarrierString(command) {
|
|
618
|
+
const input = trimShellSpacing(command);
|
|
619
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/);
|
|
620
|
+
if (!match)
|
|
621
|
+
return undefined;
|
|
622
|
+
const executable = match[1] ?? match[2];
|
|
623
|
+
if (!isCodexPosixShellExecutable(executable))
|
|
624
|
+
return undefined;
|
|
625
|
+
// A string-form carrier has one additional command-line quoting layer. Strip
|
|
626
|
+
// only the outer pair; quotes belonging to the nested command remain intact.
|
|
627
|
+
let nested = match[4].trim();
|
|
628
|
+
if (nested.length >= 2
|
|
629
|
+
&& ((nested.startsWith('"') && nested.endsWith('"'))
|
|
630
|
+
|| (nested.startsWith("'") && nested.endsWith("'")))) {
|
|
631
|
+
nested = nested.slice(1, -1);
|
|
632
|
+
}
|
|
633
|
+
return nested ? { command: nested, dialect: 'posix', executable } : undefined;
|
|
634
|
+
}
|
|
635
|
+
export function resolveCodexShellCarrierString(command) {
|
|
636
|
+
return resolveCodexCmdCarrierString(command)
|
|
637
|
+
?? resolveCodexPowerShellCarrierString(command)
|
|
638
|
+
?? resolveCodexPosixShellCarrierString(command);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Extract a trusted cmd executable's command body without trusting its startup
|
|
642
|
+
* switches. This is used only to classify malformed EC carriers as a
|
|
643
|
+
* controlled denial; a command with a non-EC body remains an ordinary shell
|
|
644
|
+
* command and follows the normal approval path.
|
|
645
|
+
*/
|
|
646
|
+
function extractCodexCmdCarrierBody(command) {
|
|
647
|
+
const input = trimShellSpacing(command);
|
|
648
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
|
|
649
|
+
if (!match)
|
|
650
|
+
return undefined;
|
|
651
|
+
const executable = match[1] ?? match[2];
|
|
652
|
+
if (!isCodexCmdExecutable(executable))
|
|
653
|
+
return undefined;
|
|
654
|
+
const body = match[4].trim();
|
|
655
|
+
return body || undefined;
|
|
656
|
+
}
|
|
657
|
+
function extractCodexCmdCarrierBodyArgv(argv) {
|
|
658
|
+
if (!argv.length || !isCodexCmdExecutable(argv[0]))
|
|
659
|
+
return undefined;
|
|
660
|
+
let bodyIndex = 1;
|
|
661
|
+
for (; bodyIndex < argv.length; bodyIndex++) {
|
|
662
|
+
const value = argv[bodyIndex];
|
|
663
|
+
if (value.toLowerCase() === '/c') {
|
|
664
|
+
bodyIndex++;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
// This is deliberately loose. It is only used to identify a malformed
|
|
668
|
+
// carrier that should be denied, never to authorize a command.
|
|
669
|
+
if (!value.startsWith('/'))
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
const body = argv.slice(bodyIndex).join(' ').trim();
|
|
673
|
+
return body || undefined;
|
|
674
|
+
}
|
|
675
|
+
function looksLikeEcCommandBody(command) {
|
|
676
|
+
if (!command)
|
|
677
|
+
return false;
|
|
678
|
+
const input = command.trim();
|
|
679
|
+
const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
|
|
680
|
+
return /^ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(body);
|
|
681
|
+
}
|
|
682
|
+
/** True only when a trusted cmd executable is carrying an EC command body. */
|
|
683
|
+
export function hasCodexCmdCarrierEcIntent(input) {
|
|
684
|
+
if (Array.isArray(input.commandArgv)
|
|
685
|
+
&& input.commandArgv.every(value => typeof value === 'string')) {
|
|
686
|
+
return looksLikeEcCommandBody(extractCodexCmdCarrierBodyArgv(input.commandArgv));
|
|
687
|
+
}
|
|
688
|
+
return typeof input.command === 'string'
|
|
689
|
+
&& looksLikeEcCommandBody(extractCodexCmdCarrierBody(input.command));
|
|
690
|
+
}
|
|
691
|
+
function parseCodexCmdCarrierArgvFailure(argv) {
|
|
692
|
+
const body = extractCodexCmdCarrierBodyArgv(argv);
|
|
693
|
+
if (!body || !looksLikeEcCommandBody(body))
|
|
694
|
+
return undefined;
|
|
695
|
+
const nested = parseLiteralShellCommand(body, { dialect: 'cmd', allowManagedTmpDir: true });
|
|
696
|
+
if (!nested.ok)
|
|
697
|
+
return nested;
|
|
698
|
+
return parseFailure('shell-composition', body, 0, 0, 'cmd');
|
|
699
|
+
}
|
|
700
|
+
function parseCodexCmdCarrierStringFailure(command, options = {}) {
|
|
701
|
+
const body = extractCodexCmdCarrierBody(command);
|
|
702
|
+
if (!body || !looksLikeEcCommandBody(body))
|
|
703
|
+
return undefined;
|
|
704
|
+
const nested = parseLiteralShellCommand(body, {
|
|
705
|
+
...options,
|
|
706
|
+
dialect: 'cmd',
|
|
707
|
+
});
|
|
708
|
+
return nested.ok
|
|
709
|
+
? parseFailure('shell-composition', body, 0, 0, 'cmd')
|
|
710
|
+
: nested;
|
|
242
711
|
}
|
|
243
712
|
function doubleQuotedIssueAt(input, index) {
|
|
244
713
|
const char = input[index];
|
|
@@ -262,13 +731,16 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
262
731
|
if (options.dialect === 'powershell') {
|
|
263
732
|
return parseLiteralPowerShellCommand(command);
|
|
264
733
|
}
|
|
734
|
+
if (options.dialect === 'cmd') {
|
|
735
|
+
return parseLiteralCmdCommand(command, options);
|
|
736
|
+
}
|
|
265
737
|
const input = trimShellSpacing(options.allowManagedTmpDir
|
|
266
738
|
? substituteManagedTmpDirToken(command)
|
|
267
739
|
: command);
|
|
268
740
|
if (!input)
|
|
269
|
-
return
|
|
741
|
+
return parseFailure('empty-command', input, 0, 0, 'posix');
|
|
270
742
|
if (input.includes('\0'))
|
|
271
|
-
return
|
|
743
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'posix');
|
|
272
744
|
const argv = [];
|
|
273
745
|
let token = '';
|
|
274
746
|
let tokenStarted = false;
|
|
@@ -285,7 +757,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
285
757
|
if (quote === 'single') {
|
|
286
758
|
if (char === "'") {
|
|
287
759
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
288
|
-
return
|
|
760
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
289
761
|
quote = null;
|
|
290
762
|
}
|
|
291
763
|
else
|
|
@@ -295,13 +767,13 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
295
767
|
if (quote === 'double') {
|
|
296
768
|
if (char === '"') {
|
|
297
769
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
298
|
-
return
|
|
770
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
299
771
|
quote = null;
|
|
300
772
|
continue;
|
|
301
773
|
}
|
|
302
774
|
const issue = doubleQuotedIssueAt(input, index);
|
|
303
775
|
if (issue)
|
|
304
|
-
return
|
|
776
|
+
return parseFailure(issue, input, index, argv.length, 'posix');
|
|
305
777
|
if (char === '\\') {
|
|
306
778
|
const next = input[++index];
|
|
307
779
|
token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
|
|
@@ -321,9 +793,9 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
321
793
|
// keeps inside the executable name; accepting those would misclassify a
|
|
322
794
|
// different executable as `ec`.
|
|
323
795
|
if (char === '\r' || char === '\n')
|
|
324
|
-
return
|
|
796
|
+
return parseFailure('structural-newline', input, index, argv.length, 'posix');
|
|
325
797
|
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
326
|
-
return
|
|
798
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'posix');
|
|
327
799
|
}
|
|
328
800
|
if (char === "'") {
|
|
329
801
|
tokenStarted = true;
|
|
@@ -338,9 +810,9 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
338
810
|
if (char === '\\') {
|
|
339
811
|
const next = input[++index];
|
|
340
812
|
if (next === undefined)
|
|
341
|
-
return
|
|
813
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
342
814
|
if (next === '\r' || next === '\n')
|
|
343
|
-
return
|
|
815
|
+
return parseFailure('structural-newline', input, index, argv.length, 'posix');
|
|
344
816
|
tokenStarted = true;
|
|
345
817
|
token += next;
|
|
346
818
|
continue;
|
|
@@ -349,12 +821,12 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
349
821
|
// peer keys such as `aun#alice` are ordinary literal arguments.
|
|
350
822
|
if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
|
|
351
823
|
|| (char === '~' && !tokenStarted))
|
|
352
|
-
return
|
|
824
|
+
return parseFailure('shell-composition', input, index, argv.length, 'posix');
|
|
353
825
|
tokenStarted = true;
|
|
354
826
|
token += char;
|
|
355
827
|
}
|
|
356
828
|
if (quote)
|
|
357
|
-
return
|
|
829
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'posix');
|
|
358
830
|
finishToken();
|
|
359
831
|
return argv.length > 0
|
|
360
832
|
? {
|
|
@@ -363,24 +835,27 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
363
835
|
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
364
836
|
: argv,
|
|
365
837
|
}
|
|
366
|
-
:
|
|
838
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'posix');
|
|
367
839
|
}
|
|
368
840
|
export function parseLiteralShellArgv(command, options = {}) {
|
|
369
841
|
const result = parseLiteralShellCommand(command, options);
|
|
370
842
|
return result.ok ? result.argv : null;
|
|
371
843
|
}
|
|
372
844
|
export function resolveCodexShellCarrierArgv(argv) {
|
|
373
|
-
if (argv.length
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return { command: argv[2], dialect: 'posix' };
|
|
845
|
+
if (argv.length === 3
|
|
846
|
+
&& (argv[1] === '-lc' || argv[1] === '-c')
|
|
847
|
+
&& isCodexPosixShellExecutable(argv[0])) {
|
|
848
|
+
return { command: argv[2], dialect: 'posix', executable: argv[0] };
|
|
377
849
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
850
|
+
if (argv.length === 3) {
|
|
851
|
+
if (isCodexPowerShellExecutable(argv[0]) && argv[1].toLowerCase() === '-command') {
|
|
852
|
+
return { command: argv[2], dialect: 'powershell', executable: argv[0] };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
|
|
856
|
+
// The execute switch must be the final switch before one command argument;
|
|
857
|
+
// validation itself is shared with string-only Windows payloads below.
|
|
858
|
+
return resolveCodexCmdCarrierArgv(argv);
|
|
384
859
|
}
|
|
385
860
|
export function unwrapCodexShellCommandArgv(argv) {
|
|
386
861
|
return resolveCodexShellCarrierArgv(argv)?.command;
|
|
@@ -392,31 +867,70 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
392
867
|
*/
|
|
393
868
|
export function parseCodexShellCommandArgv(command, options = {}) {
|
|
394
869
|
const outer = parseLiteralShellArgv(command, options);
|
|
395
|
-
if (
|
|
870
|
+
if (outer) {
|
|
871
|
+
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
872
|
+
if (carrier !== undefined) {
|
|
873
|
+
const parsed = parseCodexCarrierCommand(carrier, options);
|
|
874
|
+
return parsed.ok ? parsed.argv : null;
|
|
875
|
+
}
|
|
876
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
877
|
+
if (stringCarrier) {
|
|
878
|
+
const parsed = parseCodexCarrierCommand(stringCarrier, options);
|
|
879
|
+
return parsed.ok ? parsed.argv : null;
|
|
880
|
+
}
|
|
881
|
+
if (parseCodexCmdCarrierStringFailure(command, options))
|
|
882
|
+
return null;
|
|
883
|
+
return normalizeEvolcoreCommandArgv(outer);
|
|
884
|
+
}
|
|
885
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
886
|
+
if (!stringCarrier)
|
|
396
887
|
return null;
|
|
397
|
-
const
|
|
398
|
-
return
|
|
399
|
-
? outer
|
|
400
|
-
: parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
|
|
888
|
+
const parsed = parseCodexCarrierCommand(stringCarrier, options);
|
|
889
|
+
return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
|
|
401
890
|
}
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
891
|
+
/** Parse a Codex tool input while preserving safe syntax failure metadata. */
|
|
892
|
+
export function parseCodexToolCommand(input, options = {}) {
|
|
893
|
+
if (Array.isArray(input.commandArgv)) {
|
|
894
|
+
if (!input.commandArgv.every(value => typeof value === 'string')) {
|
|
895
|
+
return parseFailure('invalid-control-char', '', 0, 0, 'posix');
|
|
896
|
+
}
|
|
409
897
|
const outer = input.commandArgv;
|
|
898
|
+
const nulIndex = outer.findIndex(value => value.includes('\0'));
|
|
899
|
+
if (nulIndex >= 0)
|
|
900
|
+
return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
|
|
410
901
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
902
|
+
if (carrier === undefined) {
|
|
903
|
+
return parseCodexCmdCarrierArgvFailure(outer)
|
|
904
|
+
?? { ok: true, argv: normalizeEvolcoreCommandArgv(outer) };
|
|
905
|
+
}
|
|
906
|
+
return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
|
|
417
907
|
}
|
|
418
908
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
419
|
-
|
|
909
|
+
const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
|
|
910
|
+
if (outer.ok) {
|
|
911
|
+
const carrier = resolveCodexShellCarrierArgv(outer.argv);
|
|
912
|
+
if (carrier !== undefined) {
|
|
913
|
+
return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
|
|
914
|
+
}
|
|
915
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
916
|
+
return stringCarrier
|
|
917
|
+
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
918
|
+
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
|
|
919
|
+
?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
|
|
920
|
+
}
|
|
921
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
922
|
+
return stringCarrier
|
|
923
|
+
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
924
|
+
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
928
|
+
* supplied by app-server. Joining that argv with spaces loses the quoting of
|
|
929
|
+
* the inner `-c`/`-lc` command and was the source of a false proactive denial.
|
|
930
|
+
*/
|
|
931
|
+
export function parseCodexToolCommandArgv(input, options = {}) {
|
|
932
|
+
const result = parseCodexToolCommand(input, options);
|
|
933
|
+
return result.ok ? result.argv : null;
|
|
420
934
|
}
|
|
421
935
|
function isSimpleExecStringLiteral(value) {
|
|
422
936
|
const quote = value[0];
|
|
@@ -903,12 +1417,15 @@ export function parseCodexExecToolCommand(input) {
|
|
|
903
1417
|
return decoded;
|
|
904
1418
|
}
|
|
905
1419
|
function parseProactiveToolCommandArgv(toolName, input) {
|
|
1420
|
+
const options = {
|
|
1421
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
1422
|
+
};
|
|
906
1423
|
if (toolName === 'Bash' || toolName === 'Shell')
|
|
907
|
-
return parseCodexToolCommandArgv(input);
|
|
1424
|
+
return parseCodexToolCommandArgv(input, options);
|
|
908
1425
|
if (toolName !== 'exec')
|
|
909
1426
|
return null;
|
|
910
1427
|
const command = parseCodexExecToolCommand(input);
|
|
911
|
-
return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true }) : null;
|
|
1428
|
+
return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true, ...options }) : null;
|
|
912
1429
|
}
|
|
913
1430
|
function scanBoundedOutputOperators(command) {
|
|
914
1431
|
const operators = [];
|
|
@@ -1099,23 +1616,62 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1099
1616
|
const parseOptions = { allowManagedTmpDir: true, ...options };
|
|
1100
1617
|
const literal = parseLiteralShellCommand(command, parseOptions);
|
|
1101
1618
|
if (literal.ok) {
|
|
1102
|
-
if (literal.argv[0] === 'ec')
|
|
1619
|
+
if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
|
|
1103
1620
|
return { kind: 'literal' };
|
|
1104
1621
|
const carrier = resolveCodexShellCarrierArgv(literal.argv);
|
|
1105
1622
|
if (carrier !== undefined) {
|
|
1106
|
-
const nested =
|
|
1107
|
-
...parseOptions,
|
|
1108
|
-
dialect: carrier.dialect,
|
|
1109
|
-
});
|
|
1623
|
+
const nested = parseCodexCarrierCommand(carrier, parseOptions);
|
|
1110
1624
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
1111
1625
|
return { kind: 'literal' };
|
|
1112
|
-
if (!nested.ok &&
|
|
1626
|
+
if (!nested.ok && (hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)
|
|
1627
|
+
|| (carrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(carrier.command)))) {
|
|
1113
1628
|
return { kind: 'composite', issue: nested.issue };
|
|
1114
1629
|
}
|
|
1115
1630
|
}
|
|
1631
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1632
|
+
if (stringCarrier) {
|
|
1633
|
+
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1634
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
1635
|
+
return { kind: 'literal' };
|
|
1636
|
+
if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
|
|
1637
|
+
|| (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
|
|
1638
|
+
return { kind: 'composite', issue: nested.issue };
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(command);
|
|
1642
|
+
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1643
|
+
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1644
|
+
...parseOptions,
|
|
1645
|
+
dialect: 'cmd',
|
|
1646
|
+
});
|
|
1647
|
+
return nested.ok
|
|
1648
|
+
? { kind: 'composite', issue: 'shell-composition' }
|
|
1649
|
+
: { kind: 'composite', issue: nested.issue };
|
|
1650
|
+
}
|
|
1116
1651
|
return { kind: 'none' };
|
|
1117
1652
|
}
|
|
1118
|
-
const
|
|
1653
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1654
|
+
if (stringCarrier) {
|
|
1655
|
+
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1656
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
1657
|
+
return { kind: 'literal' };
|
|
1658
|
+
if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
|
|
1659
|
+
|| (stringCarrier.dialect === 'powershell' && hasPowerShellDelegationAssignment(stringCarrier.command)))) {
|
|
1660
|
+
return { kind: 'composite', issue: nested.issue };
|
|
1661
|
+
}
|
|
1662
|
+
return { kind: 'none' };
|
|
1663
|
+
}
|
|
1664
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(command);
|
|
1665
|
+
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1666
|
+
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1667
|
+
...parseOptions,
|
|
1668
|
+
dialect: 'cmd',
|
|
1669
|
+
});
|
|
1670
|
+
return nested.ok
|
|
1671
|
+
? { kind: 'composite', issue: 'shell-composition' }
|
|
1672
|
+
: { kind: 'composite', issue: nested.issue };
|
|
1673
|
+
}
|
|
1674
|
+
const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
|
|
1119
1675
|
? null
|
|
1120
1676
|
: parseBoundedOutputShellCommand(command);
|
|
1121
1677
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
@@ -1331,7 +1887,9 @@ export function parseEvolcoreSendCommand(command) {
|
|
|
1331
1887
|
export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
|
|
1332
1888
|
if (toolName !== 'Bash')
|
|
1333
1889
|
return false;
|
|
1334
|
-
const argv = parseCodexToolCommandArgv(input
|
|
1890
|
+
const argv = parseCodexToolCommandArgv(input, {
|
|
1891
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
1892
|
+
});
|
|
1335
1893
|
const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
|
|
1336
1894
|
if (!parsed)
|
|
1337
1895
|
return false;
|