evolcore 0.0.20 → 0.0.21
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 +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- 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 +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- 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 +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- 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 +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- 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 +290 -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 +279 -80
- 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 +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- 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 +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- 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 +21 -2
- 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/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -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/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -10,6 +10,13 @@ const CODEX_POWERSHELL_EXECUTABLES = new Set([
|
|
|
10
10
|
'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
|
|
11
11
|
]);
|
|
12
12
|
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;
|
|
13
|
+
const CODEX_CMD_EXECUTABLES = new Set(['cmd', 'cmd.exe']);
|
|
14
|
+
const CODEX_CMD_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Windows[\\/](?:System32|SysWOW64|Sysnative)[\\/]cmd\.exe)$/i;
|
|
15
|
+
// These switches affect only cmd startup/output formatting. `/c` is handled
|
|
16
|
+
// separately as the required execute switch. Flags such as `/k`, `/v:on`,
|
|
17
|
+
// `/e:*`, and `/f:*` can change process lifetime or command semantics and are
|
|
18
|
+
// intentionally not treated as a trusted Codex carrier.
|
|
19
|
+
const CODEX_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
|
|
13
20
|
const SHELL_UNQUOTED_META = new Set([
|
|
14
21
|
';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
|
|
15
22
|
]);
|
|
@@ -140,12 +147,31 @@ function isLiteralArgBoundary(value) {
|
|
|
140
147
|
function isShellParameterExpansionStart(value) {
|
|
141
148
|
return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
|
|
142
149
|
}
|
|
150
|
+
function hasCarrierEcCommandPrefix(command, dialect) {
|
|
151
|
+
if (dialect !== 'cmd')
|
|
152
|
+
return hasEcCommandPrefix(command);
|
|
153
|
+
const input = trimShellSpacing(command);
|
|
154
|
+
const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
|
|
155
|
+
? input.slice(1, -1)
|
|
156
|
+
: input;
|
|
157
|
+
return /^\s*ec(?:\s|$)/i.test(inner);
|
|
158
|
+
}
|
|
159
|
+
function parseFailure(issue, input, offset, tokenIndex, dialect) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
issue,
|
|
163
|
+
offset: Math.max(0, Math.min(offset, input.length)),
|
|
164
|
+
tokenIndex: Math.max(0, tokenIndex),
|
|
165
|
+
dialect,
|
|
166
|
+
inputLength: input.length,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
143
169
|
function parseLiteralPowerShellCommand(command) {
|
|
144
170
|
const input = trimShellSpacing(command);
|
|
145
171
|
if (!input)
|
|
146
|
-
return
|
|
172
|
+
return parseFailure('empty-command', input, 0, 0, 'powershell');
|
|
147
173
|
if (input.includes('\0'))
|
|
148
|
-
return
|
|
174
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
|
|
149
175
|
const argv = [];
|
|
150
176
|
let token = '';
|
|
151
177
|
let tokenStarted = false;
|
|
@@ -166,7 +192,7 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
166
192
|
}
|
|
167
193
|
else if (char === "'") {
|
|
168
194
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
169
|
-
return
|
|
195
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
170
196
|
quote = null;
|
|
171
197
|
}
|
|
172
198
|
else {
|
|
@@ -177,21 +203,21 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
177
203
|
if (quote === 'double') {
|
|
178
204
|
if (char === '"') {
|
|
179
205
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
180
|
-
return
|
|
206
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
181
207
|
quote = null;
|
|
182
208
|
continue;
|
|
183
209
|
}
|
|
184
210
|
if (char === '`') {
|
|
185
211
|
const next = input[++index];
|
|
186
212
|
if (next === undefined)
|
|
187
|
-
return
|
|
213
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
188
214
|
if (next === '\r' || next === '\n')
|
|
189
|
-
return
|
|
215
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
190
216
|
token += next;
|
|
191
217
|
continue;
|
|
192
218
|
}
|
|
193
219
|
if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
|
|
194
|
-
return
|
|
220
|
+
return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
|
|
195
221
|
}
|
|
196
222
|
token += char;
|
|
197
223
|
continue;
|
|
@@ -201,9 +227,9 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
201
227
|
continue;
|
|
202
228
|
}
|
|
203
229
|
if (char === '\r' || char === '\n')
|
|
204
|
-
return
|
|
230
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
205
231
|
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
206
|
-
return
|
|
232
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
|
|
207
233
|
}
|
|
208
234
|
if (char === "'") {
|
|
209
235
|
tokenStarted = true;
|
|
@@ -218,9 +244,9 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
218
244
|
if (char === '`') {
|
|
219
245
|
const next = input[++index];
|
|
220
246
|
if (next === undefined)
|
|
221
|
-
return
|
|
247
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
222
248
|
if (next === '\r' || next === '\n')
|
|
223
|
-
return
|
|
249
|
+
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
224
250
|
tokenStarted = true;
|
|
225
251
|
token += next;
|
|
226
252
|
continue;
|
|
@@ -228,17 +254,245 @@ function parseLiteralPowerShellCommand(command) {
|
|
|
228
254
|
const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
|
|
229
255
|
const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
|
|
230
256
|
if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
|
|
231
|
-
return
|
|
257
|
+
return parseFailure('shell-composition', input, index, argv.length, 'powershell');
|
|
232
258
|
}
|
|
233
259
|
tokenStarted = true;
|
|
234
260
|
token += char;
|
|
235
261
|
}
|
|
236
262
|
if (quote)
|
|
237
|
-
return
|
|
263
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'powershell');
|
|
238
264
|
finishToken();
|
|
239
265
|
return argv.length > 0
|
|
240
266
|
? { ok: true, argv }
|
|
241
|
-
:
|
|
267
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'powershell');
|
|
268
|
+
}
|
|
269
|
+
function parseLiteralCmdCommand(command, options = {}) {
|
|
270
|
+
let input = trimShellSpacing(command);
|
|
271
|
+
if (!input)
|
|
272
|
+
return parseFailure('empty-command', input, 0, 0, 'cmd');
|
|
273
|
+
if (input.includes('\0'))
|
|
274
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'cmd');
|
|
275
|
+
// `cmd.exe /c` commonly wraps the complete command in one quote pair.
|
|
276
|
+
// Those quotes belong to the carrier, not to the first EC argument; remove
|
|
277
|
+
// exactly that outer pair before tokenizing the inner command.
|
|
278
|
+
if (input.length >= 2 && input.startsWith('"') && input.endsWith('"')) {
|
|
279
|
+
input = input.slice(1, -1);
|
|
280
|
+
}
|
|
281
|
+
const argv = [];
|
|
282
|
+
let token = '';
|
|
283
|
+
let tokenStarted = false;
|
|
284
|
+
let quote = false;
|
|
285
|
+
const finishToken = () => {
|
|
286
|
+
if (!tokenStarted)
|
|
287
|
+
return;
|
|
288
|
+
argv.push(token);
|
|
289
|
+
token = '';
|
|
290
|
+
tokenStarted = false;
|
|
291
|
+
};
|
|
292
|
+
for (let index = 0; index < input.length; index++) {
|
|
293
|
+
const char = input[index];
|
|
294
|
+
if (quote) {
|
|
295
|
+
if (char === '"') {
|
|
296
|
+
// cmd.exe represents a literal quote in a quoted argument as `""`.
|
|
297
|
+
if (input[index + 1] === '"') {
|
|
298
|
+
token += '"';
|
|
299
|
+
index++;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
quote = false;
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Environment expansion and caret escaping remain active in cmd quoted
|
|
307
|
+
// strings, so accepting either would make the canonical argv differ
|
|
308
|
+
// from what cmd.exe actually executes.
|
|
309
|
+
if (char === '%' || char === '!' || char === '^') {
|
|
310
|
+
return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
|
|
311
|
+
}
|
|
312
|
+
if (char === '\r' || char === '\n')
|
|
313
|
+
return parseFailure('structural-newline', input, index, argv.length, 'cmd');
|
|
314
|
+
token += char;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === ' ' || char === '\t') {
|
|
318
|
+
finishToken();
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (char === '\r' || char === '\n')
|
|
322
|
+
return parseFailure('structural-newline', input, index, argv.length, 'cmd');
|
|
323
|
+
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
324
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'cmd');
|
|
325
|
+
}
|
|
326
|
+
if (char === '"') {
|
|
327
|
+
tokenStarted = true;
|
|
328
|
+
quote = true;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (char === '%' || char === '!') {
|
|
332
|
+
return parseFailure(char === '%' ? 'unsafe-expansion' : 'shell-composition', input, index, argv.length, 'cmd');
|
|
333
|
+
}
|
|
334
|
+
if (char === '^' || char === '&' || char === '|' || char === '<' || char === '>'
|
|
335
|
+
|| char === ';' || char === '(' || char === ')' || char === '*' || char === '?'
|
|
336
|
+
|| char === '[' || char === ']' || char === '{' || char === '}') {
|
|
337
|
+
return parseFailure('shell-composition', input, index, argv.length, 'cmd');
|
|
338
|
+
}
|
|
339
|
+
tokenStarted = true;
|
|
340
|
+
token += char;
|
|
341
|
+
}
|
|
342
|
+
if (quote)
|
|
343
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'cmd');
|
|
344
|
+
finishToken();
|
|
345
|
+
return argv.length > 0
|
|
346
|
+
? {
|
|
347
|
+
ok: true,
|
|
348
|
+
argv: options.allowManagedTmpDir
|
|
349
|
+
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
350
|
+
: argv,
|
|
351
|
+
}
|
|
352
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'cmd');
|
|
353
|
+
}
|
|
354
|
+
function isCodexCmdExecutable(value) {
|
|
355
|
+
if (!value)
|
|
356
|
+
return false;
|
|
357
|
+
const normalized = value.toLowerCase();
|
|
358
|
+
return CODEX_CMD_EXECUTABLES.has(normalized) || CODEX_CMD_INSTALL_PATH_RE.test(value);
|
|
359
|
+
}
|
|
360
|
+
function resolveCodexCmdCarrierArgv(argv) {
|
|
361
|
+
if (argv.length < 3 || !isCodexCmdExecutable(argv[0]))
|
|
362
|
+
return undefined;
|
|
363
|
+
const executeIndex = argv.slice(1, -1).findIndex(value => value.toLowerCase() === '/c');
|
|
364
|
+
if (executeIndex < 0)
|
|
365
|
+
return undefined;
|
|
366
|
+
const absoluteExecuteIndex = executeIndex + 1;
|
|
367
|
+
// The command must remain one argv item. Joining multiple items would lose
|
|
368
|
+
// the quoting boundary that the Windows launcher actually executed.
|
|
369
|
+
if (absoluteExecuteIndex !== argv.length - 2)
|
|
370
|
+
return undefined;
|
|
371
|
+
const seen = new Set();
|
|
372
|
+
for (const rawSwitch of argv.slice(1, absoluteExecuteIndex)) {
|
|
373
|
+
const switchValue = rawSwitch.toLowerCase();
|
|
374
|
+
if (!CODEX_CMD_INERT_SWITCHES.has(switchValue) || seen.has(switchValue))
|
|
375
|
+
return undefined;
|
|
376
|
+
seen.add(switchValue);
|
|
377
|
+
}
|
|
378
|
+
const command = argv.at(-1);
|
|
379
|
+
return typeof command === 'string' && command.trim()
|
|
380
|
+
? { command, dialect: 'cmd' }
|
|
381
|
+
: undefined;
|
|
382
|
+
}
|
|
383
|
+
/** Resolve a Windows cmd carrier when the app-server provides only a string. */
|
|
384
|
+
function resolveCodexCmdCarrierString(command) {
|
|
385
|
+
const input = trimShellSpacing(command);
|
|
386
|
+
// The executable may be quoted when the app-server serializes a Windows
|
|
387
|
+
// path. Parse the prefix into argv-like switches, then use the same
|
|
388
|
+
// validation as the commandArgv path so both representations agree.
|
|
389
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
|
|
390
|
+
if (!match)
|
|
391
|
+
return undefined;
|
|
392
|
+
const executable = match[1] ?? match[2];
|
|
393
|
+
const rawSwitches = match[3].trim();
|
|
394
|
+
const switches = rawSwitches ? rawSwitches.split(/[ \t]+/) : [];
|
|
395
|
+
const argv = [executable, ...switches, match[4]];
|
|
396
|
+
return resolveCodexCmdCarrierArgv(argv);
|
|
397
|
+
}
|
|
398
|
+
/** Resolve a PowerShell carrier when the app-server provides only a string. */
|
|
399
|
+
function resolveCodexPowerShellCarrierString(command) {
|
|
400
|
+
const input = trimShellSpacing(command);
|
|
401
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+-command\s+(.+)$/i);
|
|
402
|
+
if (!match)
|
|
403
|
+
return undefined;
|
|
404
|
+
const executable = match[1] ?? match[2];
|
|
405
|
+
const normalized = executable.toLowerCase();
|
|
406
|
+
const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(normalized)
|
|
407
|
+
|| CODEX_POWERSHELL_INSTALL_PATH_RE.test(executable);
|
|
408
|
+
if (!isPowerShell)
|
|
409
|
+
return undefined;
|
|
410
|
+
// A string-form carrier has one additional command-line quoting layer. The
|
|
411
|
+
// command argument is commonly serialized as `"ec ..."`; remove exactly
|
|
412
|
+
// that outer pair and leave quotes belonging to EC arguments intact.
|
|
413
|
+
let nested = match[3].trim();
|
|
414
|
+
if (nested.length >= 2
|
|
415
|
+
&& ((nested.startsWith('"') && nested.endsWith('"'))
|
|
416
|
+
|| (nested.startsWith("'") && nested.endsWith("'")))) {
|
|
417
|
+
nested = nested.slice(1, -1);
|
|
418
|
+
}
|
|
419
|
+
return nested ? { command: nested, dialect: 'powershell' } : undefined;
|
|
420
|
+
}
|
|
421
|
+
export function resolveCodexShellCarrierString(command) {
|
|
422
|
+
return resolveCodexCmdCarrierString(command)
|
|
423
|
+
?? resolveCodexPowerShellCarrierString(command);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Extract a trusted cmd executable's command body without trusting its startup
|
|
427
|
+
* switches. This is used only to classify malformed EC carriers as a
|
|
428
|
+
* controlled denial; a command with a non-EC body remains an ordinary shell
|
|
429
|
+
* command and follows the normal approval path.
|
|
430
|
+
*/
|
|
431
|
+
function extractCodexCmdCarrierBody(command) {
|
|
432
|
+
const input = trimShellSpacing(command);
|
|
433
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))((?:\s+\/[^\s]+)*)\s+(.+)$/i);
|
|
434
|
+
if (!match)
|
|
435
|
+
return undefined;
|
|
436
|
+
const executable = match[1] ?? match[2];
|
|
437
|
+
if (!isCodexCmdExecutable(executable))
|
|
438
|
+
return undefined;
|
|
439
|
+
const body = match[4].trim();
|
|
440
|
+
return body || undefined;
|
|
441
|
+
}
|
|
442
|
+
function extractCodexCmdCarrierBodyArgv(argv) {
|
|
443
|
+
if (!argv.length || !isCodexCmdExecutable(argv[0]))
|
|
444
|
+
return undefined;
|
|
445
|
+
let bodyIndex = 1;
|
|
446
|
+
for (; bodyIndex < argv.length; bodyIndex++) {
|
|
447
|
+
const value = argv[bodyIndex];
|
|
448
|
+
if (value.toLowerCase() === '/c') {
|
|
449
|
+
bodyIndex++;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
// This is deliberately loose. It is only used to identify a malformed
|
|
453
|
+
// carrier that should be denied, never to authorize a command.
|
|
454
|
+
if (!value.startsWith('/'))
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
const body = argv.slice(bodyIndex).join(' ').trim();
|
|
458
|
+
return body || undefined;
|
|
459
|
+
}
|
|
460
|
+
function looksLikeEcCommandBody(command) {
|
|
461
|
+
if (!command)
|
|
462
|
+
return false;
|
|
463
|
+
const input = command.trim();
|
|
464
|
+
const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
|
|
465
|
+
return /^ec(?:\s|$)/i.test(body);
|
|
466
|
+
}
|
|
467
|
+
/** True only when a trusted cmd executable is carrying an EC command body. */
|
|
468
|
+
export function hasCodexCmdCarrierEcIntent(input) {
|
|
469
|
+
if (Array.isArray(input.commandArgv)
|
|
470
|
+
&& input.commandArgv.every(value => typeof value === 'string')) {
|
|
471
|
+
return looksLikeEcCommandBody(extractCodexCmdCarrierBodyArgv(input.commandArgv));
|
|
472
|
+
}
|
|
473
|
+
return typeof input.command === 'string'
|
|
474
|
+
&& looksLikeEcCommandBody(extractCodexCmdCarrierBody(input.command));
|
|
475
|
+
}
|
|
476
|
+
function parseCodexCmdCarrierArgvFailure(argv) {
|
|
477
|
+
const body = extractCodexCmdCarrierBodyArgv(argv);
|
|
478
|
+
if (!body || !looksLikeEcCommandBody(body))
|
|
479
|
+
return undefined;
|
|
480
|
+
const nested = parseLiteralShellCommand(body, { dialect: 'cmd', allowManagedTmpDir: true });
|
|
481
|
+
if (!nested.ok)
|
|
482
|
+
return nested;
|
|
483
|
+
return parseFailure('shell-composition', body, 0, 0, 'cmd');
|
|
484
|
+
}
|
|
485
|
+
function parseCodexCmdCarrierStringFailure(command, options = {}) {
|
|
486
|
+
const body = extractCodexCmdCarrierBody(command);
|
|
487
|
+
if (!body || !looksLikeEcCommandBody(body))
|
|
488
|
+
return undefined;
|
|
489
|
+
const nested = parseLiteralShellCommand(body, {
|
|
490
|
+
...options,
|
|
491
|
+
dialect: 'cmd',
|
|
492
|
+
});
|
|
493
|
+
return nested.ok
|
|
494
|
+
? parseFailure('shell-composition', body, 0, 0, 'cmd')
|
|
495
|
+
: nested;
|
|
242
496
|
}
|
|
243
497
|
function doubleQuotedIssueAt(input, index) {
|
|
244
498
|
const char = input[index];
|
|
@@ -262,13 +516,16 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
262
516
|
if (options.dialect === 'powershell') {
|
|
263
517
|
return parseLiteralPowerShellCommand(command);
|
|
264
518
|
}
|
|
519
|
+
if (options.dialect === 'cmd') {
|
|
520
|
+
return parseLiteralCmdCommand(command, options);
|
|
521
|
+
}
|
|
265
522
|
const input = trimShellSpacing(options.allowManagedTmpDir
|
|
266
523
|
? substituteManagedTmpDirToken(command)
|
|
267
524
|
: command);
|
|
268
525
|
if (!input)
|
|
269
|
-
return
|
|
526
|
+
return parseFailure('empty-command', input, 0, 0, 'posix');
|
|
270
527
|
if (input.includes('\0'))
|
|
271
|
-
return
|
|
528
|
+
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'posix');
|
|
272
529
|
const argv = [];
|
|
273
530
|
let token = '';
|
|
274
531
|
let tokenStarted = false;
|
|
@@ -285,7 +542,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
285
542
|
if (quote === 'single') {
|
|
286
543
|
if (char === "'") {
|
|
287
544
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
288
|
-
return
|
|
545
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
289
546
|
quote = null;
|
|
290
547
|
}
|
|
291
548
|
else
|
|
@@ -295,13 +552,13 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
295
552
|
if (quote === 'double') {
|
|
296
553
|
if (char === '"') {
|
|
297
554
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
298
|
-
return
|
|
555
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
299
556
|
quote = null;
|
|
300
557
|
continue;
|
|
301
558
|
}
|
|
302
559
|
const issue = doubleQuotedIssueAt(input, index);
|
|
303
560
|
if (issue)
|
|
304
|
-
return
|
|
561
|
+
return parseFailure(issue, input, index, argv.length, 'posix');
|
|
305
562
|
if (char === '\\') {
|
|
306
563
|
const next = input[++index];
|
|
307
564
|
token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
|
|
@@ -321,9 +578,9 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
321
578
|
// keeps inside the executable name; accepting those would misclassify a
|
|
322
579
|
// different executable as `ec`.
|
|
323
580
|
if (char === '\r' || char === '\n')
|
|
324
|
-
return
|
|
581
|
+
return parseFailure('structural-newline', input, index, argv.length, 'posix');
|
|
325
582
|
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
326
|
-
return
|
|
583
|
+
return parseFailure('invalid-control-char', input, index, argv.length, 'posix');
|
|
327
584
|
}
|
|
328
585
|
if (char === "'") {
|
|
329
586
|
tokenStarted = true;
|
|
@@ -338,9 +595,9 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
338
595
|
if (char === '\\') {
|
|
339
596
|
const next = input[++index];
|
|
340
597
|
if (next === undefined)
|
|
341
|
-
return
|
|
598
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
342
599
|
if (next === '\r' || next === '\n')
|
|
343
|
-
return
|
|
600
|
+
return parseFailure('structural-newline', input, index, argv.length, 'posix');
|
|
344
601
|
tokenStarted = true;
|
|
345
602
|
token += next;
|
|
346
603
|
continue;
|
|
@@ -349,12 +606,12 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
349
606
|
// peer keys such as `aun#alice` are ordinary literal arguments.
|
|
350
607
|
if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
|
|
351
608
|
|| (char === '~' && !tokenStarted))
|
|
352
|
-
return
|
|
609
|
+
return parseFailure('shell-composition', input, index, argv.length, 'posix');
|
|
353
610
|
tokenStarted = true;
|
|
354
611
|
token += char;
|
|
355
612
|
}
|
|
356
613
|
if (quote)
|
|
357
|
-
return
|
|
614
|
+
return parseFailure('invalid-quote', input, input.length, argv.length, 'posix');
|
|
358
615
|
finishToken();
|
|
359
616
|
return argv.length > 0
|
|
360
617
|
? {
|
|
@@ -363,24 +620,28 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
363
620
|
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
364
621
|
: argv,
|
|
365
622
|
}
|
|
366
|
-
:
|
|
623
|
+
: parseFailure('empty-command', input, input.length, argv.length, 'posix');
|
|
367
624
|
}
|
|
368
625
|
export function parseLiteralShellArgv(command, options = {}) {
|
|
369
626
|
const result = parseLiteralShellCommand(command, options);
|
|
370
627
|
return result.ok ? result.argv : null;
|
|
371
628
|
}
|
|
372
629
|
export function resolveCodexShellCarrierArgv(argv) {
|
|
373
|
-
if (argv.length
|
|
374
|
-
return undefined;
|
|
375
|
-
if (argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
|
|
630
|
+
if (argv.length === 3 && argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
|
|
376
631
|
return { command: argv[2], dialect: 'posix' };
|
|
377
632
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
633
|
+
if (argv.length === 3) {
|
|
634
|
+
const powershellExecutable = argv[0].toLowerCase();
|
|
635
|
+
const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
|
|
636
|
+
|| CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
|
|
637
|
+
if (isPowerShell && argv[1].toLowerCase() === '-command') {
|
|
638
|
+
return { command: argv[2], dialect: 'powershell' };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Node/cross-spawn and Codex use cmd.exe with a small set of startup flags.
|
|
642
|
+
// The execute switch must be the final switch before one command argument;
|
|
643
|
+
// validation itself is shared with string-only Windows payloads below.
|
|
644
|
+
return resolveCodexCmdCarrierArgv(argv);
|
|
384
645
|
}
|
|
385
646
|
export function unwrapCodexShellCommandArgv(argv) {
|
|
386
647
|
return resolveCodexShellCarrierArgv(argv)?.command;
|
|
@@ -392,31 +653,81 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
392
653
|
*/
|
|
393
654
|
export function parseCodexShellCommandArgv(command, options = {}) {
|
|
394
655
|
const outer = parseLiteralShellArgv(command, options);
|
|
395
|
-
if (
|
|
656
|
+
if (outer) {
|
|
657
|
+
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
658
|
+
if (carrier !== undefined) {
|
|
659
|
+
return parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
|
|
660
|
+
}
|
|
661
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
662
|
+
if (stringCarrier) {
|
|
663
|
+
return parseLiteralShellArgv(stringCarrier.command, { ...options, dialect: stringCarrier.dialect });
|
|
664
|
+
}
|
|
665
|
+
if (parseCodexCmdCarrierStringFailure(command, options))
|
|
666
|
+
return null;
|
|
667
|
+
return outer;
|
|
668
|
+
}
|
|
669
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
670
|
+
if (!stringCarrier)
|
|
396
671
|
return null;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
672
|
+
const parsed = parseLiteralShellCommand(stringCarrier.command, {
|
|
673
|
+
...options,
|
|
674
|
+
dialect: stringCarrier.dialect,
|
|
675
|
+
});
|
|
676
|
+
return parsed.ok ? parsed.argv : null;
|
|
401
677
|
}
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
678
|
+
/** Parse a Codex tool input while preserving safe syntax failure metadata. */
|
|
679
|
+
export function parseCodexToolCommand(input) {
|
|
680
|
+
if (Array.isArray(input.commandArgv)) {
|
|
681
|
+
if (!input.commandArgv.every(value => typeof value === 'string')) {
|
|
682
|
+
return parseFailure('invalid-control-char', '', 0, 0, 'posix');
|
|
683
|
+
}
|
|
409
684
|
const outer = input.commandArgv;
|
|
685
|
+
const nulIndex = outer.findIndex(value => value.includes('\0'));
|
|
686
|
+
if (nulIndex >= 0)
|
|
687
|
+
return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
|
|
410
688
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
689
|
+
if (carrier === undefined) {
|
|
690
|
+
return parseCodexCmdCarrierArgvFailure(outer) ?? { ok: true, argv: outer };
|
|
691
|
+
}
|
|
692
|
+
return parseLiteralShellCommand(carrier.command, {
|
|
693
|
+
allowManagedTmpDir: true,
|
|
694
|
+
dialect: carrier.dialect,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
698
|
+
const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
|
|
699
|
+
if (outer.ok) {
|
|
700
|
+
const carrier = resolveCodexShellCarrierArgv(outer.argv);
|
|
701
|
+
if (carrier !== undefined) {
|
|
702
|
+
return parseLiteralShellCommand(carrier.command, {
|
|
414
703
|
allowManagedTmpDir: true,
|
|
415
704
|
dialect: carrier.dialect,
|
|
416
705
|
});
|
|
706
|
+
}
|
|
707
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
708
|
+
return stringCarrier
|
|
709
|
+
? parseLiteralShellCommand(stringCarrier.command, {
|
|
710
|
+
allowManagedTmpDir: true,
|
|
711
|
+
dialect: stringCarrier.dialect,
|
|
712
|
+
})
|
|
713
|
+
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
417
714
|
}
|
|
418
|
-
const
|
|
419
|
-
return
|
|
715
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
716
|
+
return stringCarrier
|
|
717
|
+
? parseLiteralShellCommand(stringCarrier.command, {
|
|
718
|
+
allowManagedTmpDir: true,
|
|
719
|
+
dialect: stringCarrier.dialect,
|
|
720
|
+
})
|
|
721
|
+
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
725
|
+
* supplied by app-server. Joining that argv with spaces loses the quoting of
|
|
726
|
+
* the inner `-lc` command and was the source of a false proactive denial.
|
|
727
|
+
*/
|
|
728
|
+
export function parseCodexToolCommandArgv(input) {
|
|
729
|
+
const result = parseCodexToolCommand(input);
|
|
730
|
+
return result.ok ? result.argv : null;
|
|
420
731
|
}
|
|
421
732
|
function isSimpleExecStringLiteral(value) {
|
|
422
733
|
const quote = value[0];
|
|
@@ -1109,13 +1420,58 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1109
1420
|
});
|
|
1110
1421
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
1111
1422
|
return { kind: 'literal' };
|
|
1112
|
-
if (!nested.ok &&
|
|
1423
|
+
if (!nested.ok && hasCarrierEcCommandPrefix(carrier.command, carrier.dialect)) {
|
|
1424
|
+
return { kind: 'composite', issue: nested.issue };
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1428
|
+
if (stringCarrier) {
|
|
1429
|
+
const nested = parseLiteralShellCommand(stringCarrier.command, {
|
|
1430
|
+
...parseOptions,
|
|
1431
|
+
dialect: stringCarrier.dialect,
|
|
1432
|
+
});
|
|
1433
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
1434
|
+
return { kind: 'literal' };
|
|
1435
|
+
if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
|
|
1113
1436
|
return { kind: 'composite', issue: nested.issue };
|
|
1114
1437
|
}
|
|
1115
1438
|
}
|
|
1439
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(command);
|
|
1440
|
+
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1441
|
+
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1442
|
+
...parseOptions,
|
|
1443
|
+
dialect: 'cmd',
|
|
1444
|
+
});
|
|
1445
|
+
return nested.ok
|
|
1446
|
+
? { kind: 'composite', issue: 'shell-composition' }
|
|
1447
|
+
: { kind: 'composite', issue: nested.issue };
|
|
1448
|
+
}
|
|
1449
|
+
return { kind: 'none' };
|
|
1450
|
+
}
|
|
1451
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1452
|
+
if (stringCarrier) {
|
|
1453
|
+
const nested = parseLiteralShellCommand(stringCarrier.command, {
|
|
1454
|
+
...parseOptions,
|
|
1455
|
+
dialect: stringCarrier.dialect,
|
|
1456
|
+
});
|
|
1457
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
1458
|
+
return { kind: 'literal' };
|
|
1459
|
+
if (!nested.ok && hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)) {
|
|
1460
|
+
return { kind: 'composite', issue: nested.issue };
|
|
1461
|
+
}
|
|
1116
1462
|
return { kind: 'none' };
|
|
1117
1463
|
}
|
|
1118
|
-
const
|
|
1464
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(command);
|
|
1465
|
+
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1466
|
+
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1467
|
+
...parseOptions,
|
|
1468
|
+
dialect: 'cmd',
|
|
1469
|
+
});
|
|
1470
|
+
return nested.ok
|
|
1471
|
+
? { kind: 'composite', issue: 'shell-composition' }
|
|
1472
|
+
: { kind: 'composite', issue: nested.issue };
|
|
1473
|
+
}
|
|
1474
|
+
const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
|
|
1119
1475
|
? null
|
|
1120
1476
|
: parseBoundedOutputShellCommand(command);
|
|
1121
1477
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|