evolcore 0.0.28 → 0.0.29-test.0
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -1,5 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { isTrustedWindowsShellExecutable } from '../../utils/windows-shell-trust.js';
|
|
2
|
-
|
|
4
|
+
function parseApplyPatchCommand(command) {
|
|
5
|
+
const input = command.trim();
|
|
6
|
+
let commandText = input;
|
|
7
|
+
if (commandText.startsWith('&'))
|
|
8
|
+
commandText = commandText.slice(1).trimStart();
|
|
9
|
+
const executableMatch = commandText.match(/^(?:"([^"]+)"|'([^']+)'|([^\s]+))/u);
|
|
10
|
+
if (!executableMatch)
|
|
11
|
+
return { kind: 'not-apply-patch' };
|
|
12
|
+
const executable = executableMatch[1] ?? executableMatch[2] ?? executableMatch[3] ?? '';
|
|
13
|
+
if (!isApplyPatchExecutable(executable))
|
|
14
|
+
return { kind: 'not-apply-patch' };
|
|
15
|
+
const remainder = commandText.slice(executableMatch[0].length).trimStart();
|
|
16
|
+
if (!remainder)
|
|
17
|
+
return { kind: 'invalid', reason: 'missing patch document' };
|
|
18
|
+
const literal = parseLiteralShellCommand(input, { allowManagedTmpDir: true });
|
|
19
|
+
if (literal.ok && argvUsesShellApplyPatch(literal.argv)) {
|
|
20
|
+
if (literal.argv.length !== 2)
|
|
21
|
+
return { kind: 'invalid', reason: 'apply_patch must carry exactly one patch document' };
|
|
22
|
+
const fileChanges = parseApplyPatchDocument(literal.argv[1]);
|
|
23
|
+
return fileChanges
|
|
24
|
+
? { kind: 'file-change', fileChanges }
|
|
25
|
+
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
26
|
+
}
|
|
27
|
+
const heredoc = remainder.match(/^<<-?\s*(?:'([^']+)'|"([^"]+)"|([^\s]+))\s*\r?\n([\s\S]*)$/u);
|
|
28
|
+
if (!heredoc)
|
|
29
|
+
return { kind: 'invalid', reason: 'apply_patch must use one literal patch document' };
|
|
30
|
+
const delimiter = heredoc[1] ?? heredoc[2] ?? heredoc[3];
|
|
31
|
+
const bodyLines = heredoc[4].replace(/\r\n?/gu, '\n').split('\n');
|
|
32
|
+
const delimiterIndex = bodyLines.findIndex(line => line === delimiter);
|
|
33
|
+
if (delimiterIndex < 0 || bodyLines.slice(delimiterIndex + 1).some(line => line.trim() !== '')) {
|
|
34
|
+
return { kind: 'invalid', reason: 'heredoc delimiter is incomplete or has trailing commands' };
|
|
35
|
+
}
|
|
36
|
+
const fileChanges = parseApplyPatchDocument(bodyLines.slice(0, delimiterIndex).join('\n'));
|
|
37
|
+
return fileChanges
|
|
38
|
+
? { kind: 'file-change', fileChanges }
|
|
39
|
+
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
40
|
+
}
|
|
41
|
+
function composedApplyPatchOnCommandLine(command) {
|
|
42
|
+
const commandLine = command.split(/\r?\n/u, 1)[0] ?? command;
|
|
43
|
+
return commandLine.split(/[|;&]/u).slice(1).some(segment => {
|
|
44
|
+
const executable = leadingShellExecutable(segment);
|
|
45
|
+
return !!executable && isApplyPatchExecutable(executable);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function parseShellApplyPatchInvocation(input, options = {}, parsedArgv) {
|
|
49
|
+
const rawCommand = typeof input.command === 'string' ? input.command : '';
|
|
50
|
+
const rawCommandArgv = Array.isArray(input.commandArgv)
|
|
51
|
+
&& input.commandArgv.every(value => typeof value === 'string')
|
|
52
|
+
? input.commandArgv
|
|
53
|
+
: undefined;
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const addCandidate = (value) => {
|
|
56
|
+
if (value && !candidates.includes(value))
|
|
57
|
+
candidates.push(value);
|
|
58
|
+
};
|
|
59
|
+
addCandidate(rawCommand);
|
|
60
|
+
if (rawCommandArgv) {
|
|
61
|
+
if (argvUsesShellApplyPatch(rawCommandArgv)) {
|
|
62
|
+
if (rawCommandArgv.length === 2) {
|
|
63
|
+
const fileChanges = parseApplyPatchDocument(rawCommandArgv[1]);
|
|
64
|
+
return fileChanges
|
|
65
|
+
? { kind: 'file-change', fileChanges }
|
|
66
|
+
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'invalid', reason: 'apply_patch must carry exactly one patch document' };
|
|
69
|
+
}
|
|
70
|
+
addCandidate(resolveCodexShellCarrierArgv(rawCommandArgv)?.command);
|
|
71
|
+
}
|
|
72
|
+
if (parsedArgv && argvUsesShellApplyPatch(parsedArgv)) {
|
|
73
|
+
return { kind: 'invalid', reason: 'apply_patch payload could not be reconstructed safely' };
|
|
74
|
+
}
|
|
75
|
+
addCandidate(resolveCodexShellCarrierString(rawCommand)?.command);
|
|
76
|
+
let sawApplyPatch = false;
|
|
77
|
+
let invalidReason = 'apply_patch payload could not be reconstructed safely';
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const parsed = parseApplyPatchCommand(candidate);
|
|
80
|
+
if (parsed.kind !== 'not-apply-patch') {
|
|
81
|
+
sawApplyPatch = true;
|
|
82
|
+
if (parsed.kind === 'file-change')
|
|
83
|
+
return parsed;
|
|
84
|
+
invalidReason = parsed.reason;
|
|
85
|
+
}
|
|
86
|
+
else if (composedApplyPatchOnCommandLine(candidate)) {
|
|
87
|
+
sawApplyPatch = true;
|
|
88
|
+
invalidReason = 'apply_patch must be the only shell command';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return sawApplyPatch ? { kind: 'invalid', reason: invalidReason } : { kind: 'not-apply-patch' };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* stdout filters allowed after an EC invocation. Each entry reads stdin and
|
|
95
|
+
* writes stdout only, and its option grammar is an allowlist, so a filter can
|
|
96
|
+
* never acquire a file operand, a write target, or a process spawn. Changing
|
|
97
|
+
* this table means changing `bin/ec-safe-output.js`, which repeats the same
|
|
98
|
+
* validation before it spawns the process.
|
|
99
|
+
*/
|
|
100
|
+
const STDOUT_FILTER_SPECS = new Map([
|
|
101
|
+
// Every `tail` follow/retry option is absent below, so `tail -f` is rejected
|
|
102
|
+
// instead of being allowed to hold the pipe open.
|
|
103
|
+
['head', {
|
|
104
|
+
flags: new Set(),
|
|
105
|
+
valueOptions: new Set(['-n', '-c', '--lines', '--bytes']),
|
|
106
|
+
maxOperands: 0,
|
|
107
|
+
numericShorthand: true,
|
|
108
|
+
}],
|
|
109
|
+
['tail', {
|
|
110
|
+
flags: new Set(),
|
|
111
|
+
valueOptions: new Set(['-n', '-c', '--lines', '--bytes']),
|
|
112
|
+
maxOperands: 0,
|
|
113
|
+
numericShorthand: true,
|
|
114
|
+
}],
|
|
115
|
+
// `grep` needs a pattern: either `-e`/`--regexp` or the single leading
|
|
116
|
+
// operand. Once an explicit pattern option appears, a bare operand would be
|
|
117
|
+
// a file to read, so the operand budget drops to zero. `-r`/`-R`/`-f` are
|
|
118
|
+
// absent from both tables, which keeps recursion and pattern files out.
|
|
119
|
+
['grep', {
|
|
120
|
+
flags: new Set([
|
|
121
|
+
'-i', '--ignore-case', '-v', '--invert-match', '-c', '--count',
|
|
122
|
+
'-l', '--files-with-matches', '-L', '--files-without-match', '-o', '--only-matching',
|
|
123
|
+
'-n', '--line-number', '-h', '--no-filename', '-H', '--with-filename',
|
|
124
|
+
'-q', '--quiet', '--silent', '-s', '--no-messages', '-w', '--word-regexp',
|
|
125
|
+
'-x', '--line-regexp', '-F', '--fixed-strings', '-E', '--extended-regexp',
|
|
126
|
+
'-G', '--basic-regexp', '-P', '--perl-regexp', '-a', '--text', '-I',
|
|
127
|
+
'-z', '--null-data', '-U', '--binary', '--line-buffered', '--no-group-separator',
|
|
128
|
+
]),
|
|
129
|
+
valueOptions: new Set([
|
|
130
|
+
'-m', '--max-count', '-A', '--after-context', '-B', '--before-context',
|
|
131
|
+
'-C', '--context', '--color', '--colour', '--binary-files', '--label',
|
|
132
|
+
'--group-separator', '-d', '--directories', '--include', '--exclude', '--exclude-dir',
|
|
133
|
+
]),
|
|
134
|
+
patternValueOptions: new Set(['-e', '--regexp']),
|
|
135
|
+
maxOperands: 1,
|
|
136
|
+
}],
|
|
137
|
+
// `wc`, `sort`, `cut` and `uniq` take no operands here: a path operand would
|
|
138
|
+
// make them read a file instead of the EC stdout. Options that name an input
|
|
139
|
+
// or output file (`--files0-from`, `sort -o`/`-T`/`--random-source`,
|
|
140
|
+
// `uniq --group`) are absent from both tables and therefore rejected.
|
|
141
|
+
['wc', {
|
|
142
|
+
flags: new Set([
|
|
143
|
+
'-c', '-m', '-l', '-w', '-L',
|
|
144
|
+
'--bytes', '--chars', '--lines', '--words', '--max-line-length',
|
|
145
|
+
]),
|
|
146
|
+
valueOptions: new Set(),
|
|
147
|
+
maxOperands: 0,
|
|
148
|
+
}],
|
|
149
|
+
['sort', {
|
|
150
|
+
flags: new Set([
|
|
151
|
+
'-b', '-d', '-f', '-g', '-h', '-i', '-M', '-n', '-r', '-R', '-s', '-u', '-V', '-z',
|
|
152
|
+
'--ignore-leading-blanks', '--dictionary-order', '--ignore-case', '--general-numeric-sort',
|
|
153
|
+
'--human-numeric-sort', '--ignore-nonprinting', '--month-sort', '--numeric-sort',
|
|
154
|
+
'--random-sort', '--reverse', '--stable', '--unique', '--version-sort', '--zero-terminated',
|
|
155
|
+
'--check', '--merge',
|
|
156
|
+
]),
|
|
157
|
+
valueOptions: new Set([
|
|
158
|
+
'-k', '-t', '-S', '--key', '--field-separator',
|
|
159
|
+
'--batch-size', '--buffer-size', '--parallel', '--sort',
|
|
160
|
+
]),
|
|
161
|
+
maxOperands: 0,
|
|
162
|
+
}],
|
|
163
|
+
['cut', {
|
|
164
|
+
flags: new Set(['-s', '--only-delimited', '-n', '--complement', '-z', '--zero-terminated']),
|
|
165
|
+
valueOptions: new Set([
|
|
166
|
+
'-b', '-c', '-f', '-d', '--bytes', '--characters', '--fields', '--delimiter', '--output-delimiter',
|
|
167
|
+
]),
|
|
168
|
+
maxOperands: 0,
|
|
169
|
+
}],
|
|
170
|
+
['uniq', {
|
|
171
|
+
flags: new Set([
|
|
172
|
+
'-c', '-d', '-D', '-u', '-i', '-z',
|
|
173
|
+
'--count', '--repeated', '--all-repeated', '--unique', '--ignore-case', '--zero-terminated',
|
|
174
|
+
]),
|
|
175
|
+
valueOptions: new Set(['-f', '-s', '--skip-fields', '--skip-chars']),
|
|
176
|
+
maxOperands: 0,
|
|
177
|
+
}],
|
|
178
|
+
// `tr` takes its character sets as operands, which are never files.
|
|
179
|
+
['tr', {
|
|
180
|
+
flags: new Set([
|
|
181
|
+
'-c', '-C', '-d', '-s', '-t',
|
|
182
|
+
'--complement', '--delete', '--squeeze-repeats', '--truncate-set1',
|
|
183
|
+
]),
|
|
184
|
+
valueOptions: new Set(),
|
|
185
|
+
maxOperands: 2,
|
|
186
|
+
}],
|
|
187
|
+
]);
|
|
188
|
+
/** Accept `head`, `/bin/head` and `/usr/bin/head` for every allowed filter. */
|
|
189
|
+
function normalizeStdoutFilterExecutable(value) {
|
|
190
|
+
if (STDOUT_FILTER_SPECS.has(value))
|
|
191
|
+
return value;
|
|
192
|
+
for (const prefix of ['/bin/', '/usr/bin/']) {
|
|
193
|
+
if (value.startsWith(prefix)) {
|
|
194
|
+
const bare = value.slice(prefix.length);
|
|
195
|
+
if (STDOUT_FILTER_SPECS.has(bare))
|
|
196
|
+
return bare;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
3
201
|
// Codex app-server sends command executions through a fixed shell carrier.
|
|
4
202
|
// Treat the carrier as syntax only after parsing it as literal argv; never
|
|
5
203
|
// accept an arbitrary shell command string or unwrap composition here.
|
|
@@ -28,8 +226,22 @@ const CODEX_POWERSHELL_INERT_SWITCHES = new Set([
|
|
|
28
226
|
// `/e:*`, and `/f:*` can change process lifetime or command semantics and are
|
|
29
227
|
// intentionally not treated as a trusted Codex carrier.
|
|
30
228
|
const CODEX_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
|
|
31
|
-
const EVOLCORE_EXECUTABLE_ALIASES = new Set(['ec', 'ec.cmd', 'ec.exe']);
|
|
229
|
+
const EVOLCORE_EXECUTABLE_ALIASES = new Set(['ec', 'ec.cmd', 'ec.exe', 'ec.ps1']);
|
|
32
230
|
const EVOLCORE_NODE_EXECUTABLES = new Set(['node', 'node.exe']);
|
|
231
|
+
const APPLY_PATCH_EXECUTABLE_BASENAMES = new Set([
|
|
232
|
+
'apply_patch',
|
|
233
|
+
'apply_patch.bat',
|
|
234
|
+
'apply_patch.cmd',
|
|
235
|
+
'apply_patch.exe',
|
|
236
|
+
'apply_patch.ps1',
|
|
237
|
+
]);
|
|
238
|
+
const EVOLCORE_SUBCOMMANDS = new Set([
|
|
239
|
+
'agent', 'aid', 'bench', 'config', 'contact', 'ctl', 'data', 'diag', 'diagnose',
|
|
240
|
+
'fs', 'gateway', 'group', 'handoff', 'init', 'link-rules', 'logs', 'menu-client',
|
|
241
|
+
'mode', 'model', 'msg', 'mv', 'net', 'queue', 'response', 'restart',
|
|
242
|
+
'restart-monitor', 'rpc', 'start', 'stats', 'status', 'stop', 'storage', 'task',
|
|
243
|
+
'trigger', 'version', 'watch',
|
|
244
|
+
]);
|
|
33
245
|
// Development/install entrypoints that are part of this repository's CLI.
|
|
34
246
|
// Only these exact relative paths are canonicalized; arbitrary Node scripts
|
|
35
247
|
// must never become an EC command merely because their filename is similar.
|
|
@@ -41,6 +253,45 @@ const POWERSHELL_UNQUOTED_META = new Set([
|
|
|
41
253
|
';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
|
|
42
254
|
]);
|
|
43
255
|
const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
|
|
256
|
+
/**
|
|
257
|
+
* 用空格替换 heredoc 正文,保留命令行本身。
|
|
258
|
+
*
|
|
259
|
+
* heredoc 正文是**数据**(提交信息、脚本片段、文档示例),不是 shell 语法。
|
|
260
|
+
* 若不剥离,正文里出现的 `; ec ...`、`&& ec ...` 会被命令边界扫描当成真的
|
|
261
|
+
* 拼接调用——`git commit -F - <<'MSG'` 的提交信息里写了 `` `cd "x"; ec ...` ``
|
|
262
|
+
* 就会整条被拒,且报的还是"拼接命令",病因完全指错方向。
|
|
263
|
+
*
|
|
264
|
+
* 只处理 heredoc,不动其它多行结构:不支持 heredoc 的场景(比如真正的多行
|
|
265
|
+
* 拼接攻击)仍然走原有的 fail-closed 路径。
|
|
266
|
+
*/
|
|
267
|
+
function stripHeredocBodies(command) {
|
|
268
|
+
if (!command.includes('<<'))
|
|
269
|
+
return command;
|
|
270
|
+
const lines = command.split('\n');
|
|
271
|
+
const out = [];
|
|
272
|
+
for (let index = 0; index < lines.length; index++) {
|
|
273
|
+
const line = lines[index];
|
|
274
|
+
// 收集本行声明的所有 heredoc 终止符(`<<WORD` / `<<'WORD'` / `<<"WORD"` / `<<-WORD`)。
|
|
275
|
+
const delimiters = [];
|
|
276
|
+
const pattern = /<<-?[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = pattern.exec(line)) !== null)
|
|
279
|
+
delimiters.push(match[2]);
|
|
280
|
+
out.push(line);
|
|
281
|
+
if (delimiters.length === 0)
|
|
282
|
+
continue;
|
|
283
|
+
// 逐行丢弃,直到每个终止符都出现过;未闭合时丢弃剩余全部(保守:正文永远不是命令)。
|
|
284
|
+
let remaining = delimiters.slice();
|
|
285
|
+
while (remaining.length > 0 && index + 1 < lines.length) {
|
|
286
|
+
index++;
|
|
287
|
+
const candidate = lines[index].trim();
|
|
288
|
+
const hit = remaining.indexOf(candidate);
|
|
289
|
+
if (hit >= 0)
|
|
290
|
+
remaining.splice(hit, 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out.join('\n');
|
|
294
|
+
}
|
|
44
295
|
// This is intentionally limited to the command boundary. It is not a search
|
|
45
296
|
// for `ec` anywhere in the input: quoted arguments, regexps and log text must
|
|
46
297
|
// never establish an EC invocation.
|
|
@@ -85,7 +336,7 @@ function hasEcCommandPrefix(command) {
|
|
|
85
336
|
}
|
|
86
337
|
if (boundary && /\s/.test(char))
|
|
87
338
|
continue;
|
|
88
|
-
if (boundary && /^ec(?:\.cmd|\.exe)?(?=$|\s|[;&|`()<>])/i.test(command.slice(index)))
|
|
339
|
+
if (boundary && /^ec(?:\.cmd|\.exe|\.ps1)?(?=$|\s|[;&|`()<>])/i.test(command.slice(index)))
|
|
89
340
|
return true;
|
|
90
341
|
boundary = false;
|
|
91
342
|
}
|
|
@@ -100,7 +351,7 @@ function trimShellSpacing(value) {
|
|
|
100
351
|
* Relative names remain restricted to the fixed executable allowlists above.
|
|
101
352
|
*/
|
|
102
353
|
function isAbsoluteCustomShellPath(value, executableNames) {
|
|
103
|
-
if (!value || /[\0\r\n"'
|
|
354
|
+
if (!value || /[\0\r\n"'<>|?*`$;&|(){}\[\]]/.test(value))
|
|
104
355
|
return false;
|
|
105
356
|
const normalized = value.replaceAll('\\', '/');
|
|
106
357
|
if (!/^[A-Za-z]:\//.test(normalized) && !normalized.startsWith('//'))
|
|
@@ -108,6 +359,114 @@ function isAbsoluteCustomShellPath(value, executableNames) {
|
|
|
108
359
|
const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
|
|
109
360
|
return executableNames.has(basename);
|
|
110
361
|
}
|
|
362
|
+
function isStandardNpmEvolcoreShim(content, basename) {
|
|
363
|
+
const lines = content.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split('\n')
|
|
364
|
+
.map(line => line.trim())
|
|
365
|
+
.filter(Boolean);
|
|
366
|
+
if (basename === 'ec.cmd') {
|
|
367
|
+
const patterns = [
|
|
368
|
+
/^@echo off$/i,
|
|
369
|
+
/^goto start$/i,
|
|
370
|
+
/^:find_dp0$/i,
|
|
371
|
+
/^set dp0=%~dp0$/i,
|
|
372
|
+
/^exit \/b$/i,
|
|
373
|
+
/^:start$/i,
|
|
374
|
+
/^setlocal$/i,
|
|
375
|
+
/^call :find_dp0$/i,
|
|
376
|
+
/^if exist "%dp0%[\\/]node\.exe" \($/i,
|
|
377
|
+
/^set "_prog=%dp0%[\\/]node\.exe"$/i,
|
|
378
|
+
/^\) else \($/i,
|
|
379
|
+
/^set "_prog=node"$/i,
|
|
380
|
+
/^set pathext=%pathext:;\.js;=;%$/i,
|
|
381
|
+
/^\)$/i,
|
|
382
|
+
/^endlocal\s*&\s*goto #_undefined_#\s+2>nul\s+\|\|\s+title %comspec%\s*&\s+"%_prog%"\s+"%dp0%[\\/]node_modules[\\/]evolcore[\\/]bin[\\/]ec\.js"\s+%\*$/i,
|
|
383
|
+
];
|
|
384
|
+
return lines.length === patterns.length && lines.every((line, index) => patterns[index].test(line));
|
|
385
|
+
}
|
|
386
|
+
const patterns = [
|
|
387
|
+
/^#!\/usr\/bin\/env\s+pwsh$/i,
|
|
388
|
+
/^\$basedir\s*=\s*split-path\s+\$myinvocation\.mycommand\.definition\s+-parent$/i,
|
|
389
|
+
/^\$exe\s*=\s*""$/i,
|
|
390
|
+
/^if\s*\(\$PSVersionTable\.PSVersion\s+-lt\s+"6\.0"\s+-or\s+\$IsWindows\)\s*\{$/i,
|
|
391
|
+
/^#\s*Fix case when both the Windows and Linux builds of Node$/i,
|
|
392
|
+
/^#\s*are installed in the same directory$/i,
|
|
393
|
+
/^\$exe\s*=\s*"\.exe"$/i,
|
|
394
|
+
/^\}$/,
|
|
395
|
+
/^\$ret\s*=\s*0$/i,
|
|
396
|
+
/^if\s*\(Test-Path\s+"\$basedir\/node\$exe"\)\s*\{$/i,
|
|
397
|
+
/^#\s*Support pipeline input$/i,
|
|
398
|
+
/^if\s*\(\$MyInvocation\.ExpectingInput\)\s*\{$/i,
|
|
399
|
+
/^\$input\s+\|\s+&\s+"\$basedir\/node\$exe"\s+"\$basedir\/node_modules\/evolcore\/bin\/ec\.js"\s+\$args$/i,
|
|
400
|
+
/^\}\s*else\s*\{$/i,
|
|
401
|
+
/^&\s+"\$basedir\/node\$exe"\s+"\$basedir\/node_modules\/evolcore\/bin\/ec\.js"\s+\$args$/i,
|
|
402
|
+
/^\}$/,
|
|
403
|
+
/^\$ret\s*=\s*\$LASTEXITCODE$/i,
|
|
404
|
+
/^\}\s*else\s*\{$/i,
|
|
405
|
+
/^#\s*Support pipeline input$/i,
|
|
406
|
+
/^if\s*\(\$MyInvocation\.ExpectingInput\)\s*\{$/i,
|
|
407
|
+
/^\$input\s+\|\s+&\s+"node\$exe"\s+"\$basedir\/node_modules\/evolcore\/bin\/ec\.js"\s+\$args$/i,
|
|
408
|
+
/^\}\s*else\s*\{$/i,
|
|
409
|
+
/^&\s+"node\$exe"\s+"\$basedir\/node_modules\/evolcore\/bin\/ec\.js"\s+\$args$/i,
|
|
410
|
+
/^\}$/,
|
|
411
|
+
/^\$ret\s*=\s*\$LASTEXITCODE$/i,
|
|
412
|
+
/^\}$/,
|
|
413
|
+
/^exit\s+\$ret$/i,
|
|
414
|
+
];
|
|
415
|
+
return lines.length === patterns.length && lines.every((line, index) => patterns[index].test(line));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* PowerShell's call operator executes the path exactly as supplied. The
|
|
419
|
+
* canonical argv is only safe to arm when that path is the npm shim for this
|
|
420
|
+
* package; accepting an arbitrary same-named `ec.cmd` would let a wrapper run
|
|
421
|
+
* unrelated code while receiving an EC delegation approval.
|
|
422
|
+
*/
|
|
423
|
+
function isTrustedEvolcoreCallPath(value) {
|
|
424
|
+
if (!isAbsoluteCustomShellPath(value, EVOLCORE_EXECUTABLE_ALIASES))
|
|
425
|
+
return false;
|
|
426
|
+
if (process.platform !== 'win32')
|
|
427
|
+
return false;
|
|
428
|
+
const normalized = value.replaceAll('\\', '/');
|
|
429
|
+
const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
|
|
430
|
+
if (basename !== 'ec.cmd' && basename !== 'ec.ps1')
|
|
431
|
+
return false;
|
|
432
|
+
let shimPath;
|
|
433
|
+
try {
|
|
434
|
+
shimPath = fs.realpathSync.native(value);
|
|
435
|
+
const stat = fs.statSync(shimPath);
|
|
436
|
+
if (!stat.isFile())
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
let content;
|
|
443
|
+
try {
|
|
444
|
+
content = fs.readFileSync(shimPath, 'utf8');
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
// npm's Windows shims use either `%dp0%\\node_modules\\evolcore\\...`
|
|
450
|
+
// or `$basedir/node_modules/evolcore/...`. The PowerShell shim references
|
|
451
|
+
// the same target once per input branch, so validate one distinct target
|
|
452
|
+
// rather than counting textual occurrences.
|
|
453
|
+
const nodeModuleRefs = content.match(/node_modules[^"'`\r\n\s]*/gi) ?? [];
|
|
454
|
+
const targetMatches = content.match(/node_modules[\\/]evolcore[\\/]bin[\\/]ec\.js/gi) ?? [];
|
|
455
|
+
const normalizedTargets = new Set(targetMatches.map(value => value.replaceAll('\\', '/').toLowerCase()));
|
|
456
|
+
if (targetMatches.length === 0
|
|
457
|
+
|| normalizedTargets.size !== 1
|
|
458
|
+
|| nodeModuleRefs.some(value => !/^node_modules[\\/]evolcore[\\/]bin[\\/]ec\.js$/i.test(value))
|
|
459
|
+
|| !isStandardNpmEvolcoreShim(content, basename)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
const targetPath = path.win32.join(path.win32.dirname(shimPath), 'node_modules', 'evolcore', 'bin', 'ec.js');
|
|
463
|
+
try {
|
|
464
|
+
return fs.statSync(targetPath).isFile();
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
111
470
|
function isNodeExecutable(value) {
|
|
112
471
|
if (!value)
|
|
113
472
|
return false;
|
|
@@ -203,6 +562,20 @@ function isShellSpacing(value) {
|
|
|
203
562
|
function isLiteralArgBoundary(value) {
|
|
204
563
|
return value === undefined || isShellSpacing(value);
|
|
205
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* POSIX 引号闭合后允许直接出现的字符。
|
|
567
|
+
*
|
|
568
|
+
* 与 `isLiteralArgBoundary` 的区别:`cd "x"; ec ...` 在 bash 里完全合法,
|
|
569
|
+
* 引号后紧跟 `;`/`&`/`|` 等操作符是标准写法。若只认空白,这类输入会被
|
|
570
|
+
* 误判成 `invalid-quote`("引号未闭合")——词法上引号其实配对完好,
|
|
571
|
+
* 报错方向完全错误,把人引去排查转义。
|
|
572
|
+
*
|
|
573
|
+
* 这里放宽的只是**引号是否闭合**的判定;操作符本身仍由后续非引号分支
|
|
574
|
+
* 判为 `shell-composition`,拦截力度不变,只让错误归因准确。
|
|
575
|
+
*/
|
|
576
|
+
function isPosixQuotedTokenBoundary(value) {
|
|
577
|
+
return value === undefined || isShellSpacing(value) || SHELL_UNQUOTED_META.has(value);
|
|
578
|
+
}
|
|
206
579
|
function isShellParameterExpansionStart(value) {
|
|
207
580
|
return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
|
|
208
581
|
}
|
|
@@ -213,7 +586,7 @@ function hasCarrierEcCommandPrefix(command, dialect) {
|
|
|
213
586
|
const inner = input.length >= 2 && input.startsWith('"') && input.endsWith('"')
|
|
214
587
|
? input.slice(1, -1)
|
|
215
588
|
: input;
|
|
216
|
-
return /^\s*ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(inner);
|
|
589
|
+
return /^\s*ec(?:\.cmd|\.exe|\.ps1)?(?:\s|$)/i.test(inner);
|
|
217
590
|
}
|
|
218
591
|
/** Normalize the Windows npm shims to the canonical EvolCore CLI name. */
|
|
219
592
|
export function normalizeEvolcoreCommandArgv(argv) {
|
|
@@ -229,11 +602,151 @@ export function normalizeEvolcoreCommandArgv(argv) {
|
|
|
229
602
|
}
|
|
230
603
|
return [...argv];
|
|
231
604
|
}
|
|
605
|
+
const INLINE_TEXT_FLAGS = {
|
|
606
|
+
agentNew: new Set(['--description', '--persona-text', '--handoff']),
|
|
607
|
+
agentPersona: new Set(['--text']),
|
|
608
|
+
agentWelcome: new Set(['--text']),
|
|
609
|
+
msgSend: new Set(['--description', '--text', '--transcript', '--handoff-context']),
|
|
610
|
+
groupSend: new Set(['--text', '--transcript']),
|
|
611
|
+
groupCreate: new Set(['--description']),
|
|
612
|
+
groupUpdate: new Set(['--description']),
|
|
613
|
+
groupRules: new Set(['--question']),
|
|
614
|
+
groupJoin: new Set(['--message', '--answer']),
|
|
615
|
+
groupBlock: new Set(['--reason']),
|
|
616
|
+
triggerCreate: new Set(['--prompt']),
|
|
617
|
+
triggerUpdate: new Set(['--prompt', '--description']),
|
|
618
|
+
};
|
|
619
|
+
const MESSAGE_POSITIONAL_FLAGS = new Set([
|
|
620
|
+
'--file', '--payload', '--as', '--content-type', '--text', '--transcript',
|
|
621
|
+
'--thread', '--return', '--title', '--description', '--text-from-file',
|
|
622
|
+
'--handoff-context', '--aun-path', '--app', '--format', '--encrypt', '--no-encrypt',
|
|
623
|
+
]);
|
|
624
|
+
/** Decode the EC inline text contract exactly once, after shell parsing. */
|
|
625
|
+
export function decodeEvolcoreInlineText(value) {
|
|
626
|
+
return value.replace(/\\([\\nrt])/g, (_match, escaped) => {
|
|
627
|
+
if (escaped === 'n')
|
|
628
|
+
return '\n';
|
|
629
|
+
if (escaped === 'r')
|
|
630
|
+
return '\r';
|
|
631
|
+
if (escaped === 't')
|
|
632
|
+
return '\t';
|
|
633
|
+
return '\\';
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function decodeInlineFlags(argv, flags) {
|
|
637
|
+
for (let index = 0; index < argv.length; index++) {
|
|
638
|
+
const value = argv[index];
|
|
639
|
+
if (value === '--')
|
|
640
|
+
break;
|
|
641
|
+
if (flags.has(value) && argv[index + 1] !== undefined) {
|
|
642
|
+
argv[index + 1] = decodeEvolcoreInlineText(argv[index + 1]);
|
|
643
|
+
index++;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const separator = value.indexOf('=');
|
|
647
|
+
if (separator > 0 && flags.has(value.slice(0, separator))) {
|
|
648
|
+
argv[index] = value.slice(0, separator + 1) + decodeEvolcoreInlineText(value.slice(separator + 1));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function decodePositionalText(argv, start, optionFlags) {
|
|
653
|
+
for (let index = start; index < argv.length; index++) {
|
|
654
|
+
const value = argv[index];
|
|
655
|
+
if (value === '--') {
|
|
656
|
+
for (let bodyIndex = index + 1; bodyIndex < argv.length; bodyIndex++) {
|
|
657
|
+
argv[bodyIndex] = decodeEvolcoreInlineText(argv[bodyIndex]);
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (value.startsWith('--')) {
|
|
662
|
+
const separator = value.indexOf('=');
|
|
663
|
+
if (separator > 0)
|
|
664
|
+
continue;
|
|
665
|
+
if (optionFlags.has(value) && argv[index + 1] !== undefined)
|
|
666
|
+
index++;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
argv[index] = decodeEvolcoreInlineText(value);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function canonicalizeHandoffText(argv) {
|
|
673
|
+
const action = argv[2];
|
|
674
|
+
if (action !== 'return' && action !== 'recover-return')
|
|
675
|
+
return;
|
|
676
|
+
const delimiter = argv.indexOf('--', 2);
|
|
677
|
+
if (delimiter >= 0) {
|
|
678
|
+
for (let index = delimiter + 1; index < argv.length; index++) {
|
|
679
|
+
argv[index] = decodeEvolcoreInlineText(argv[index]);
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const fileIndex = argv.indexOf('--text-from-file', 2);
|
|
684
|
+
for (let index = 2; index < argv.length; index++) {
|
|
685
|
+
if (index === fileIndex) {
|
|
686
|
+
index++;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!argv[index].startsWith('--'))
|
|
690
|
+
argv[index] = decodeEvolcoreInlineText(argv[index]);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Convert an EC invocation to the one argv representation shared by approval,
|
|
695
|
+
* delegation hashing, IPC, and command execution. Shell syntax is already
|
|
696
|
+
* resolved before this function is called; this function only normalizes the
|
|
697
|
+
* EC entrypoint and applies the command-specific inline text contract.
|
|
698
|
+
*/
|
|
699
|
+
export function canonicalizeEvolcoreCommandArgv(argv) {
|
|
700
|
+
const canonical = normalizeEvolcoreCommandArgv(argv);
|
|
701
|
+
if (canonical[0] !== 'ec')
|
|
702
|
+
return canonical;
|
|
703
|
+
const result = [...canonical];
|
|
704
|
+
const command = result[1];
|
|
705
|
+
const subcommand = result[2];
|
|
706
|
+
if (command === 'agent') {
|
|
707
|
+
if (subcommand === 'new')
|
|
708
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.agentNew);
|
|
709
|
+
else if (subcommand === 'persona')
|
|
710
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.agentPersona);
|
|
711
|
+
else if (subcommand === 'welcome')
|
|
712
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.agentWelcome);
|
|
713
|
+
}
|
|
714
|
+
else if (command === 'msg' && subcommand === 'send') {
|
|
715
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.msgSend);
|
|
716
|
+
decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
|
|
717
|
+
}
|
|
718
|
+
else if (command === 'group') {
|
|
719
|
+
if (subcommand === 'send') {
|
|
720
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupSend);
|
|
721
|
+
decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
|
|
722
|
+
}
|
|
723
|
+
else if (subcommand === 'create')
|
|
724
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupCreate);
|
|
725
|
+
else if (subcommand === 'update')
|
|
726
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupUpdate);
|
|
727
|
+
else if (subcommand === 'rules')
|
|
728
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupRules);
|
|
729
|
+
else if (subcommand === 'join')
|
|
730
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupJoin);
|
|
731
|
+
else if (subcommand === 'block')
|
|
732
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupBlock);
|
|
733
|
+
}
|
|
734
|
+
else if (command === 'trigger') {
|
|
735
|
+
if (subcommand === 'create' || subcommand === 'set')
|
|
736
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.triggerCreate);
|
|
737
|
+
else if (subcommand === 'update')
|
|
738
|
+
decodeInlineFlags(result, INLINE_TEXT_FLAGS.triggerUpdate);
|
|
739
|
+
}
|
|
740
|
+
else if (command === 'handoff') {
|
|
741
|
+
canonicalizeHandoffText(result);
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
232
745
|
const POWERSHELL_CREDENTIAL_ENV_RE = /^(EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)$/i;
|
|
233
746
|
/** Detect a PowerShell prefix that materializes an EvolCore session credential. */
|
|
234
747
|
export function hasPowerShellCredentialAssignment(command) {
|
|
235
748
|
return /^\s*\$env\s*:\s*(?:EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)\b/i.test(command)
|
|
236
|
-
&& /\bec(?:\.cmd|\.exe)?(?:\s|$)/i.test(command);
|
|
749
|
+
&& /\bec(?:\.cmd|\.exe|\.ps1)?(?:\s|$)/i.test(command);
|
|
237
750
|
}
|
|
238
751
|
/**
|
|
239
752
|
* Windows Codex may materialize one environment variable assignment directly
|
|
@@ -354,10 +867,41 @@ export function parseCodexCarrierCommand(carrier, options = {}) {
|
|
|
354
867
|
&& options.expectedCredentialToken !== unwrapped.token)) {
|
|
355
868
|
return parseFailure('shell-composition', carrier.command, 0, 0, carrier.dialect);
|
|
356
869
|
}
|
|
357
|
-
const
|
|
870
|
+
const materialized = parseLiteralShellCommand(unwrapped.command, {
|
|
358
871
|
...options,
|
|
359
872
|
dialect: carrier.dialect,
|
|
360
873
|
});
|
|
874
|
+
if (materialized.ok && carrier.dialect === 'powershell') {
|
|
875
|
+
const nestedCarrier = resolveCodexCmdCarrierString(unwrapped.command);
|
|
876
|
+
if (nestedCarrier) {
|
|
877
|
+
const nested = parseCodexCarrierCommand(nestedCarrier, options);
|
|
878
|
+
if (!nested.ok)
|
|
879
|
+
return nested;
|
|
880
|
+
const executeIndex = materialized.argv.findIndex((value, index) => index > 0 && value.toLowerCase() === '/c');
|
|
881
|
+
if (executeIndex < 1 || executeIndex >= materialized.argv.length - 1) {
|
|
882
|
+
return parseFailure('shell-composition', unwrapped.command, 0, 0, carrier.dialect);
|
|
883
|
+
}
|
|
884
|
+
const bodyArgv = materialized.argv.slice(executeIndex + 1);
|
|
885
|
+
const materializedBody = bodyArgv.length === 1
|
|
886
|
+
? parseLiteralShellCommand(bodyArgv[0], { ...options, dialect: 'cmd' })
|
|
887
|
+
: { ok: true, argv: normalizeEvolcoreCommandArgv(bodyArgv) };
|
|
888
|
+
if (!materializedBody.ok
|
|
889
|
+
|| materializedBody.argv.length !== nested.argv.length
|
|
890
|
+
|| materializedBody.argv.some((value, index) => value !== nested.argv[index])) {
|
|
891
|
+
return parseFailure('shell-composition', unwrapped.command, 0, 0, carrier.dialect);
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
...nested,
|
|
895
|
+
...(unwrapped.kind === 'valid' ? { credentialToken: unwrapped.token } : {}),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const parsed = materialized;
|
|
900
|
+
if (parsed.ok && carrier.dialect === 'powershell'
|
|
901
|
+
&& /^\s*ec\.cmd(?:\s|$)/i.test(unwrapped.command)
|
|
902
|
+
&& parsed.argv.some(value => /[\r\n]/.test(value))) {
|
|
903
|
+
return parseFailure('lossy-cmd-shim', unwrapped.command, 0, 0, 'powershell');
|
|
904
|
+
}
|
|
361
905
|
return parsed.ok
|
|
362
906
|
? {
|
|
363
907
|
...parsed,
|
|
@@ -384,12 +928,107 @@ function isPowerShellSingleQuote(char) {
|
|
|
384
928
|
function isPowerShellDoubleQuote(char) {
|
|
385
929
|
return char !== undefined && /["\u201c\u201d\u201e]/u.test(char);
|
|
386
930
|
}
|
|
931
|
+
function powerShellCallQuoteKind(char) {
|
|
932
|
+
if (isPowerShellSingleQuote(char))
|
|
933
|
+
return 'single';
|
|
934
|
+
if (isPowerShellDoubleQuote(char))
|
|
935
|
+
return 'double';
|
|
936
|
+
return undefined;
|
|
937
|
+
}
|
|
938
|
+
function isPowerShellCallQuote(kind, char) {
|
|
939
|
+
return kind === 'single' ? isPowerShellSingleQuote(char) : isPowerShellDoubleQuote(char);
|
|
940
|
+
}
|
|
941
|
+
function parsePowerShellCallOperator(command) {
|
|
942
|
+
const input = trimShellSpacing(command);
|
|
943
|
+
if (!input.startsWith('&'))
|
|
944
|
+
return undefined;
|
|
945
|
+
let index = 1;
|
|
946
|
+
while (isShellSpacing(input[index]))
|
|
947
|
+
index++;
|
|
948
|
+
const quote = powerShellCallQuoteKind(input[index]);
|
|
949
|
+
if (!quote)
|
|
950
|
+
return undefined;
|
|
951
|
+
const pathStart = ++index;
|
|
952
|
+
while (index < input.length && !isPowerShellCallQuote(quote, input[index]))
|
|
953
|
+
index++;
|
|
954
|
+
if (index >= input.length)
|
|
955
|
+
return undefined;
|
|
956
|
+
const executable = input.slice(pathStart, index);
|
|
957
|
+
if (!isTrustedEvolcoreCallPath(executable))
|
|
958
|
+
return undefined;
|
|
959
|
+
index++;
|
|
960
|
+
if (!isShellSpacing(input[index]))
|
|
961
|
+
return undefined;
|
|
962
|
+
while (isShellSpacing(input[index]))
|
|
963
|
+
index++;
|
|
964
|
+
if (index >= input.length)
|
|
965
|
+
return undefined;
|
|
966
|
+
return `ec ${input.slice(index)}`;
|
|
967
|
+
}
|
|
968
|
+
function looksLikePowerShellEvolcoreCallOperator(command, dialect) {
|
|
969
|
+
const input = trimShellSpacing(command);
|
|
970
|
+
if (!input.startsWith('&'))
|
|
971
|
+
return false;
|
|
972
|
+
let index = 1;
|
|
973
|
+
while (isShellSpacing(input[index]))
|
|
974
|
+
index++;
|
|
975
|
+
const quote = powerShellCallQuoteKind(input[index]);
|
|
976
|
+
let executable;
|
|
977
|
+
if (quote) {
|
|
978
|
+
const start = ++index;
|
|
979
|
+
while (index < input.length && !isPowerShellCallQuote(quote, input[index]))
|
|
980
|
+
index++;
|
|
981
|
+
if (index >= input.length) {
|
|
982
|
+
const lower = input.slice(start).toLowerCase();
|
|
983
|
+
return [...EVOLCORE_EXECUTABLE_ALIASES].some(alias => lower.includes(alias));
|
|
984
|
+
}
|
|
985
|
+
executable = input.slice(start, index);
|
|
986
|
+
index++;
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
const start = index;
|
|
990
|
+
while (index < input.length && !isShellSpacing(input[index]))
|
|
991
|
+
index++;
|
|
992
|
+
executable = input.slice(start, index);
|
|
993
|
+
}
|
|
994
|
+
while (isShellSpacing(input[index]))
|
|
995
|
+
index++;
|
|
996
|
+
if (!executable)
|
|
997
|
+
return false;
|
|
998
|
+
const normalizedExecutable = executable.replaceAll('\\', '/');
|
|
999
|
+
const basename = normalizedExecutable.slice(normalizedExecutable.lastIndexOf('/') + 1).toLowerCase();
|
|
1000
|
+
if (dialect && dialect !== 'powershell')
|
|
1001
|
+
return false;
|
|
1002
|
+
if (EVOLCORE_EXECUTABLE_ALIASES.has(basename))
|
|
1003
|
+
return true;
|
|
1004
|
+
if (index >= input.length)
|
|
1005
|
+
return false;
|
|
1006
|
+
const commandName = input.slice(index).match(/^([^\s;&|<>]+)/)?.[1]?.toLowerCase();
|
|
1007
|
+
if (!commandName || !EVOLCORE_SUBCOMMANDS.has(commandName))
|
|
1008
|
+
return false;
|
|
1009
|
+
if (dialect !== 'powershell') {
|
|
1010
|
+
// A quoted or dynamic call target followed by a known CLI subcommand is
|
|
1011
|
+
// treated as a possible PowerShell EC call until proven otherwise.
|
|
1012
|
+
return !!quote || /^[\$([{]/.test(executable);
|
|
1013
|
+
}
|
|
1014
|
+
// A dynamic or non-EC path is still an EC intent when its first literal
|
|
1015
|
+
// argument is an EC command. This keeps `& $path ctl ...` fail-closed.
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
387
1018
|
function parseLiteralPowerShellCommand(command, options = {}) {
|
|
388
1019
|
const input = trimShellSpacing(command);
|
|
389
1020
|
if (!input)
|
|
390
1021
|
return parseFailure('empty-command', input, 0, 0, 'powershell');
|
|
391
1022
|
if (input.includes('\0'))
|
|
392
1023
|
return parseFailure('invalid-control-char', input, input.indexOf('\0'), 0, 'powershell');
|
|
1024
|
+
const callOperatorCommand = parsePowerShellCallOperator(input);
|
|
1025
|
+
if (callOperatorCommand) {
|
|
1026
|
+
const parsed = parseLiteralPowerShellCommand(callOperatorCommand, options);
|
|
1027
|
+
return parsed.ok
|
|
1028
|
+
? { ...parsed, argv: normalizeEvolcoreCommandArgv(parsed.argv) }
|
|
1029
|
+
: { ...parsed, inputLength: input.length };
|
|
1030
|
+
}
|
|
1031
|
+
const isEcCommand = hasEcCommandPrefix(input);
|
|
393
1032
|
const argv = [];
|
|
394
1033
|
let token = '';
|
|
395
1034
|
let tokenStarted = false;
|
|
@@ -451,6 +1090,8 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
451
1090
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
452
1091
|
if (next === '\r' || next === '\n')
|
|
453
1092
|
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
1093
|
+
if (isEcCommand && next === 'n')
|
|
1094
|
+
return parseFailure('unsupported-escape', input, index - 1, argv.length, 'powershell');
|
|
454
1095
|
token += next;
|
|
455
1096
|
continue;
|
|
456
1097
|
}
|
|
@@ -482,6 +1123,8 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
482
1123
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
483
1124
|
if (next === '\r' || next === '\n')
|
|
484
1125
|
return parseFailure('structural-newline', input, index, argv.length, 'powershell');
|
|
1126
|
+
if (isEcCommand && next === 'n')
|
|
1127
|
+
return parseFailure('unsupported-escape', input, index - 1, argv.length, 'powershell');
|
|
485
1128
|
tokenStarted = true;
|
|
486
1129
|
token += next;
|
|
487
1130
|
continue;
|
|
@@ -679,7 +1322,7 @@ function resolveCodexPowerShellCarrierString(command) {
|
|
|
679
1322
|
/** Resolve a POSIX shell carrier when app-server provides only a string. */
|
|
680
1323
|
function resolveCodexPosixShellCarrierString(command) {
|
|
681
1324
|
const input = trimShellSpacing(command);
|
|
682
|
-
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/);
|
|
1325
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/s);
|
|
683
1326
|
if (!match)
|
|
684
1327
|
return undefined;
|
|
685
1328
|
const executable = match[1] ?? match[2];
|
|
@@ -740,7 +1383,7 @@ function looksLikeEcCommandBody(command) {
|
|
|
740
1383
|
return false;
|
|
741
1384
|
const input = command.trim();
|
|
742
1385
|
const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
|
|
743
|
-
return /^ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(body);
|
|
1386
|
+
return /^ec(?:\.cmd|\.exe|\.ps1)?(?:\s|$)/i.test(body);
|
|
744
1387
|
}
|
|
745
1388
|
/** True only when a trusted cmd executable is carrying an EC command body. */
|
|
746
1389
|
export function hasCodexCmdCarrierEcIntent(input) {
|
|
@@ -819,7 +1462,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
819
1462
|
const char = input[index];
|
|
820
1463
|
if (quote === 'single') {
|
|
821
1464
|
if (char === "'") {
|
|
822
|
-
if (!
|
|
1465
|
+
if (!isPosixQuotedTokenBoundary(input[index + 1]))
|
|
823
1466
|
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
824
1467
|
quote = null;
|
|
825
1468
|
}
|
|
@@ -829,7 +1472,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
829
1472
|
}
|
|
830
1473
|
if (quote === 'double') {
|
|
831
1474
|
if (char === '"') {
|
|
832
|
-
if (!
|
|
1475
|
+
if (!isPosixQuotedTokenBoundary(input[index + 1]))
|
|
833
1476
|
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
834
1477
|
quote = null;
|
|
835
1478
|
continue;
|
|
@@ -927,6 +1570,14 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
927
1570
|
* command still has to pass `parseLiteralShellCommand`.
|
|
928
1571
|
*/
|
|
929
1572
|
export function parseCodexShellCommandArgv(command, options = {}) {
|
|
1573
|
+
const powerShellCallOperatorCommand = parsePowerShellCallOperator(command);
|
|
1574
|
+
if (powerShellCallOperatorCommand) {
|
|
1575
|
+
const parsed = parseLiteralShellCommand(powerShellCallOperatorCommand, {
|
|
1576
|
+
...options,
|
|
1577
|
+
dialect: 'powershell',
|
|
1578
|
+
});
|
|
1579
|
+
return parsed.ok ? canonicalizeEvolcoreCommandArgv(parsed.argv) : null;
|
|
1580
|
+
}
|
|
930
1581
|
const outer = parseLiteralShellArgv(command, options);
|
|
931
1582
|
if (outer) {
|
|
932
1583
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
@@ -959,6 +1610,19 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
959
1610
|
const nulIndex = outer.findIndex(value => value.includes('\0'));
|
|
960
1611
|
if (nulIndex >= 0)
|
|
961
1612
|
return parseFailure('invalid-control-char', '', 0, nulIndex, 'posix');
|
|
1613
|
+
if (outer.length === 1) {
|
|
1614
|
+
const callOperatorCommand = parsePowerShellCallOperator(outer[0]);
|
|
1615
|
+
if (callOperatorCommand) {
|
|
1616
|
+
const parsed = parseLiteralShellCommand(callOperatorCommand, {
|
|
1617
|
+
allowManagedTmpDir: true,
|
|
1618
|
+
...options,
|
|
1619
|
+
dialect: 'powershell',
|
|
1620
|
+
});
|
|
1621
|
+
return parsed.ok
|
|
1622
|
+
? { ...parsed, argv: normalizeEvolcoreCommandArgv(parsed.argv) }
|
|
1623
|
+
: parsed;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
962
1626
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
963
1627
|
if (carrier === undefined) {
|
|
964
1628
|
return parseCodexCmdCarrierArgvFailure(outer)
|
|
@@ -967,6 +1631,17 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
967
1631
|
return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
|
|
968
1632
|
}
|
|
969
1633
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
1634
|
+
const callOperatorCommand = parsePowerShellCallOperator(command);
|
|
1635
|
+
if (callOperatorCommand) {
|
|
1636
|
+
const parsed = parseLiteralShellCommand(callOperatorCommand, {
|
|
1637
|
+
allowManagedTmpDir: true,
|
|
1638
|
+
...options,
|
|
1639
|
+
dialect: 'powershell',
|
|
1640
|
+
});
|
|
1641
|
+
return parsed.ok
|
|
1642
|
+
? { ...parsed, argv: normalizeEvolcoreCommandArgv(parsed.argv) }
|
|
1643
|
+
: parsed;
|
|
1644
|
+
}
|
|
970
1645
|
const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
|
|
971
1646
|
if (outer.ok) {
|
|
972
1647
|
const carrier = resolveCodexShellCarrierArgv(outer.argv);
|
|
@@ -984,6 +1659,98 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
984
1659
|
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
985
1660
|
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
986
1661
|
}
|
|
1662
|
+
function isApplyPatchExecutable(value) {
|
|
1663
|
+
const normalized = value.replaceAll('\\', '/').replace(/\/+$/u, '');
|
|
1664
|
+
const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
|
|
1665
|
+
return APPLY_PATCH_EXECUTABLE_BASENAMES.has(basename);
|
|
1666
|
+
}
|
|
1667
|
+
function normalizeApplyPatchPath(value) {
|
|
1668
|
+
const normalized = value.trim();
|
|
1669
|
+
if (!normalized || /[\0\r\n]/u.test(normalized))
|
|
1670
|
+
return undefined;
|
|
1671
|
+
return normalized;
|
|
1672
|
+
}
|
|
1673
|
+
function parseApplyPatchDocument(source) {
|
|
1674
|
+
const lines = source.replace(/\r\n?/gu, '\n').split('\n');
|
|
1675
|
+
while (lines.at(-1) === '')
|
|
1676
|
+
lines.pop();
|
|
1677
|
+
if (lines[0] !== '*** Begin Patch' || lines.at(-1) !== '*** End Patch')
|
|
1678
|
+
return undefined;
|
|
1679
|
+
const changes = [];
|
|
1680
|
+
const seen = new Set();
|
|
1681
|
+
let index = 1;
|
|
1682
|
+
while (index < lines.length - 1) {
|
|
1683
|
+
const header = lines[index].match(/^\*\*\* (Add|Delete|Update) File: (.+)$/u);
|
|
1684
|
+
if (!header)
|
|
1685
|
+
return undefined;
|
|
1686
|
+
const kind = header[1].toLowerCase();
|
|
1687
|
+
const filePath = normalizeApplyPatchPath(header[2]);
|
|
1688
|
+
if (!filePath)
|
|
1689
|
+
return undefined;
|
|
1690
|
+
const key = filePath.replaceAll('\\', '/').toLowerCase();
|
|
1691
|
+
if (seen.has(key))
|
|
1692
|
+
return undefined;
|
|
1693
|
+
seen.add(key);
|
|
1694
|
+
index++;
|
|
1695
|
+
let movePath;
|
|
1696
|
+
if (kind === 'update' && lines[index]?.startsWith('*** Move to: ')) {
|
|
1697
|
+
movePath = normalizeApplyPatchPath(lines[index].slice('*** Move to: '.length));
|
|
1698
|
+
if (!movePath)
|
|
1699
|
+
return undefined;
|
|
1700
|
+
index++;
|
|
1701
|
+
}
|
|
1702
|
+
while (index < lines.length - 1 && !/^\*\*\* (?:Add|Delete|Update) File: /u.test(lines[index])) {
|
|
1703
|
+
const line = lines[index];
|
|
1704
|
+
if (kind === 'delete')
|
|
1705
|
+
return undefined;
|
|
1706
|
+
if (line !== '*** End of File') {
|
|
1707
|
+
const validLine = kind === 'add'
|
|
1708
|
+
? line.startsWith('+')
|
|
1709
|
+
: line.startsWith('@@')
|
|
1710
|
+
|| line.startsWith('+')
|
|
1711
|
+
|| line.startsWith('-')
|
|
1712
|
+
|| line.startsWith(' ')
|
|
1713
|
+
|| line === '\';
|
|
1714
|
+
if (!validLine)
|
|
1715
|
+
return undefined;
|
|
1716
|
+
}
|
|
1717
|
+
index++;
|
|
1718
|
+
}
|
|
1719
|
+
changes.push({ path: filePath, kind, ...(movePath ? { movePath } : {}) });
|
|
1720
|
+
}
|
|
1721
|
+
return changes.length > 0 ? changes : undefined;
|
|
1722
|
+
}
|
|
1723
|
+
function argvUsesShellApplyPatch(argv) {
|
|
1724
|
+
if (argv.length === 0)
|
|
1725
|
+
return false;
|
|
1726
|
+
return isApplyPatchExecutable(argv[0]);
|
|
1727
|
+
}
|
|
1728
|
+
function leadingShellExecutable(command) {
|
|
1729
|
+
let input = command.trim();
|
|
1730
|
+
if (input.startsWith('&'))
|
|
1731
|
+
input = input.slice(1).trimStart();
|
|
1732
|
+
if (!input)
|
|
1733
|
+
return undefined;
|
|
1734
|
+
const quote = input[0];
|
|
1735
|
+
if (quote === '"' || quote === "'") {
|
|
1736
|
+
const end = input.indexOf(quote, 1);
|
|
1737
|
+
return end > 1 ? input.slice(1, end) : undefined;
|
|
1738
|
+
}
|
|
1739
|
+
return input.split(/[\s]/u, 1)[0] || undefined;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Detect the shell fallback that tries to perform a file edit outside Codex's
|
|
1743
|
+
* structured FileChange/Edit protocol. The executable check intentionally
|
|
1744
|
+
* looks only at argv[0], so ordinary searches such as `rg apply_patch` remain
|
|
1745
|
+
* valid.
|
|
1746
|
+
*/
|
|
1747
|
+
export function isShellApplyPatchInvocation(input, parsedArgv, options = {}) {
|
|
1748
|
+
const parsed = parseShellApplyPatchInvocation(input, {
|
|
1749
|
+
...options,
|
|
1750
|
+
verifyShellExecutable: false,
|
|
1751
|
+
}, parsedArgv);
|
|
1752
|
+
return parsed.kind !== 'not-apply-patch';
|
|
1753
|
+
}
|
|
987
1754
|
/**
|
|
988
1755
|
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
989
1756
|
* supplied by app-server. Joining that argv with spaces loses the quoting of
|
|
@@ -1555,29 +2322,38 @@ function scanBoundedOutputOperators(command) {
|
|
|
1555
2322
|
return quote ? null : operators;
|
|
1556
2323
|
}
|
|
1557
2324
|
/**
|
|
1558
|
-
* Accept
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
2325
|
+
* Accept one trusted stdout filter after an EC invocation. The security
|
|
2326
|
+
* boundary is that the filter may only consume the EC process' stdout: every
|
|
2327
|
+
* non-option operand must be accounted for by the filter's operand budget, and
|
|
2328
|
+
* every option must appear in its allowlist. Unknown or malformed options make
|
|
2329
|
+
* the filter itself fail rather than widening what it can reach.
|
|
1561
2330
|
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
* or malformed options simply make head fail.
|
|
2331
|
+
* Returns the full filter argv (executable included) so the spawn helper can
|
|
2332
|
+
* execute exactly what policy validated.
|
|
1565
2333
|
*/
|
|
1566
|
-
function
|
|
2334
|
+
export function parseStdinOnlyFilterArgv(segment) {
|
|
1567
2335
|
const argv = parseLiteralShellArgv(segment);
|
|
1568
|
-
if (!argv
|
|
2336
|
+
if (!argv)
|
|
2337
|
+
return null;
|
|
2338
|
+
const executable = normalizeStdoutFilterExecutable(argv[0]);
|
|
2339
|
+
if (!executable)
|
|
1569
2340
|
return null;
|
|
1570
|
-
const
|
|
2341
|
+
const spec = STDOUT_FILTER_SPECS.get(executable);
|
|
2342
|
+
if (!spec)
|
|
2343
|
+
return null;
|
|
2344
|
+
let operands = 0;
|
|
2345
|
+
let sawPatternValue = false;
|
|
1571
2346
|
let optionsEnded = false;
|
|
1572
2347
|
let expectsOptionValue = false;
|
|
1573
|
-
for (
|
|
2348
|
+
for (let index = 1; index < argv.length; index++) {
|
|
2349
|
+
const arg = argv[index];
|
|
1574
2350
|
if (expectsOptionValue) {
|
|
1575
2351
|
expectsOptionValue = false;
|
|
1576
2352
|
continue;
|
|
1577
2353
|
}
|
|
1578
2354
|
if (optionsEnded) {
|
|
1579
2355
|
if (arg !== '-')
|
|
1580
|
-
|
|
2356
|
+
operands++;
|
|
1581
2357
|
continue;
|
|
1582
2358
|
}
|
|
1583
2359
|
if (arg === '--') {
|
|
@@ -1586,18 +2362,43 @@ function parseStdinOnlyHeadArgs(segment) {
|
|
|
1586
2362
|
}
|
|
1587
2363
|
if (arg === '-')
|
|
1588
2364
|
continue;
|
|
1589
|
-
if (!arg.startsWith('-'))
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
2365
|
+
if (!arg.startsWith('-')) {
|
|
2366
|
+
operands++;
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
if (spec.numericShorthand && /^-\d+$/.test(arg))
|
|
2370
|
+
continue;
|
|
2371
|
+
const option = arg.split('=', 1)[0];
|
|
2372
|
+
if (spec.patternValueOptions?.has(option)) {
|
|
2373
|
+
sawPatternValue = true;
|
|
2374
|
+
// `-ePATTERN` and `--regexp=PATTERN` carry their own value; only the
|
|
2375
|
+
// bare forms consume the next token.
|
|
2376
|
+
if (!arg.includes('=') && arg.length === option.length)
|
|
2377
|
+
expectsOptionValue = true;
|
|
1593
2378
|
continue;
|
|
1594
2379
|
}
|
|
2380
|
+
if (spec.valueOptions.has(option)) {
|
|
2381
|
+
if (!arg.includes('=') && arg.length === option.length)
|
|
2382
|
+
expectsOptionValue = true;
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (spec.flags.has(arg))
|
|
2386
|
+
continue;
|
|
2387
|
+
return null;
|
|
1595
2388
|
}
|
|
1596
|
-
|
|
2389
|
+
// An explicit pattern option means every remaining operand would be a file
|
|
2390
|
+
// to read, so the operand budget drops to zero.
|
|
2391
|
+
const operandBudget = sawPatternValue ? 0 : spec.maxOperands;
|
|
2392
|
+
if (operands > operandBudget)
|
|
2393
|
+
return null;
|
|
2394
|
+
if (spec.patternValueOptions && !sawPatternValue && operands === 0)
|
|
2395
|
+
return null;
|
|
2396
|
+
return [executable, ...argv.slice(1)];
|
|
1597
2397
|
}
|
|
1598
2398
|
/**
|
|
1599
2399
|
* Parse the only shell wrapper allowed around a privileged literal process:
|
|
1600
|
-
* optional stderr merge, one
|
|
2400
|
+
* optional stderr merge, one trusted stdin-only stdout filter, and one
|
|
2401
|
+
* create-only output path.
|
|
1601
2402
|
*/
|
|
1602
2403
|
export function parseBoundedOutputShellCommand(command) {
|
|
1603
2404
|
const input = trimShellSpacing(substituteManagedTmpDirToken(command));
|
|
@@ -1606,16 +2407,17 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1606
2407
|
const operators = scanBoundedOutputOperators(input);
|
|
1607
2408
|
if (!operators || operators.length === 0)
|
|
1608
2409
|
return null;
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2410
|
+
// 允许的形态:可选的 2>&1 合并 + **任意长度**的受控过滤器管道链 + 可选重定向。
|
|
2411
|
+
// 不再枚举具体管道段数(旧实现只列了单段+重定向,模型写 `| grep | head` 就掉出去)。
|
|
2412
|
+
// 放宽的依据:每一级过滤器都要过同一个 stdin-only 白名单,能力面不随段数增长。
|
|
2413
|
+
let cursor = 0;
|
|
2414
|
+
let mergeStderr = false;
|
|
2415
|
+
if (operators[cursor]?.kind === 'merge-stderr') {
|
|
2416
|
+
mergeStderr = true;
|
|
2417
|
+
cursor++;
|
|
2418
|
+
}
|
|
2419
|
+
// 源命令是第一个操作符**之前**的全部文本。合并 stderr 的 `2>&1` 本身也是操作符,
|
|
2420
|
+
// 所以两种情况都切在 operators[0].start,绝不能按 end 切——那会把 `2>&1` 混进 argv。
|
|
1619
2421
|
const first = operators[0];
|
|
1620
2422
|
const sourceText = trimShellSpacing(input.slice(0, first.start));
|
|
1621
2423
|
const argv = parseLiteralShellArgv(sourceText);
|
|
@@ -1625,48 +2427,161 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1625
2427
|
// expansion for the privileged command itself.
|
|
1626
2428
|
if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
|
|
1627
2429
|
return null;
|
|
1628
|
-
|
|
1629
|
-
let
|
|
1630
|
-
let
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
pipe = operators[1];
|
|
1636
|
-
if (!pipe || pipe.kind !== 'pipe')
|
|
2430
|
+
const filterChain = [];
|
|
2431
|
+
let outputPath;
|
|
2432
|
+
let prevEnd = mergeStderr ? operators[0].end : first.start;
|
|
2433
|
+
while (cursor < operators.length) {
|
|
2434
|
+
const operator = operators[cursor];
|
|
2435
|
+
// 段与段之间只允许空白(合并 stderr 与首个管道之间同理)。
|
|
2436
|
+
if (trimShellSpacing(input.slice(prevEnd, operator.start)))
|
|
1637
2437
|
return null;
|
|
1638
|
-
if (
|
|
2438
|
+
if (operator.kind === 'redirect') {
|
|
2439
|
+
// 重定向必须是最后一段;其后再出现操作符即拒绝。
|
|
2440
|
+
if (cursor !== operators.length - 1)
|
|
2441
|
+
return null;
|
|
2442
|
+
const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(operator.end)));
|
|
2443
|
+
if (!outputArgv || outputArgv.length !== 1)
|
|
2444
|
+
return null;
|
|
2445
|
+
if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
|
|
2446
|
+
return null;
|
|
2447
|
+
outputPath = restoreManagedTmpDirToken(outputArgv[0]);
|
|
2448
|
+
cursor++;
|
|
2449
|
+
break;
|
|
2450
|
+
}
|
|
2451
|
+
if (operator.kind !== 'pipe')
|
|
1639
2452
|
return null;
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
redirect = operators[1];
|
|
1645
|
-
}
|
|
1646
|
-
else {
|
|
1647
|
-
redirect = first;
|
|
1648
|
-
}
|
|
1649
|
-
let headArgs;
|
|
1650
|
-
if (pipe) {
|
|
1651
|
-
const headEnd = redirect?.start ?? input.length;
|
|
1652
|
-
const parsedArgs = parseStdinOnlyHeadArgs(trimShellSpacing(input.slice(pipe.end, headEnd)));
|
|
2453
|
+
// 该管道的下游终点:下一个操作符起点,或输入末尾。
|
|
2454
|
+
const next = operators[cursor + 1];
|
|
2455
|
+
const segmentEnd = next ? next.start : input.length;
|
|
2456
|
+
const parsedArgs = parseStdinOnlyFilterArgv(trimShellSpacing(input.slice(operator.end, segmentEnd)));
|
|
1653
2457
|
if (parsedArgs === null)
|
|
1654
2458
|
return null;
|
|
1655
|
-
|
|
2459
|
+
filterChain.push(parsedArgs);
|
|
2460
|
+
prevEnd = segmentEnd;
|
|
2461
|
+
cursor++;
|
|
1656
2462
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2463
|
+
return {
|
|
2464
|
+
argv,
|
|
2465
|
+
mergeStderr,
|
|
2466
|
+
...(filterChain.length > 0 ? { filterChain } : {}),
|
|
2467
|
+
...(outputPath ? { outputPath } : {}),
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* 每段必须自己就能通过零副作用校验,否则整条拒绝。
|
|
2472
|
+
*
|
|
2473
|
+
* 判据是「这段的字面量 argv 不含任何会被 shell 二次解释的字符」:`$`、反引号、
|
|
2474
|
+
* 通配、括号、重定向、管道、以及 `&&`/`;` 本身。引号包裹的普通文本(如
|
|
2475
|
+
* `echo "extra"`)合法;`echo $?`、`echo \`id\``、`echo *` 一律不合法。
|
|
2476
|
+
*/
|
|
2477
|
+
function parseInertSegment(segment) {
|
|
2478
|
+
const text = trimShellSpacing(segment);
|
|
2479
|
+
if (!text)
|
|
2480
|
+
return null;
|
|
2481
|
+
const argv = parseLiteralShellArgv(text);
|
|
2482
|
+
if (!argv || argv.length === 0)
|
|
2483
|
+
return null;
|
|
2484
|
+
// 字面量解析已处理引号;拒绝任何仍可能造成展开或结构变化的字符。
|
|
2485
|
+
// 注意**不含反斜杠**:Windows 路径必然带 `\`,而解析后的 argv 直接交给
|
|
2486
|
+
// spawn、不经 shell,反斜杠不会被再解释一次;`cd` 的目标另有范围校验。
|
|
2487
|
+
if (argv.some(value => /[$`*?\[\]{}()<>|;&]/.test(value)))
|
|
2488
|
+
return null;
|
|
2489
|
+
return argv;
|
|
2490
|
+
}
|
|
2491
|
+
function isInertSegment(argv) {
|
|
2492
|
+
if (argv.length === 0)
|
|
2493
|
+
return false;
|
|
2494
|
+
// cd 的额外约束(唯一参数、必须在前缀位、目录范围)在调用处单独校验。
|
|
2495
|
+
return argv[0] === 'pwd' || argv[0] === 'echo' || argv[0] === 'cd';
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* 识别「零副作用前后缀 + 恰好一条 ec」的可证拼接。
|
|
2499
|
+
*
|
|
2500
|
+
* 模型最常见的写法是"先切目录再跑命令"或"跑完打印一行提示",而这些会被通用
|
|
2501
|
+
* 拼接规则一律拒绝。这里只放行**结构可证**的窄形态:
|
|
2502
|
+
* - 按顶层 `&&` / `;` 切段(`||`、`&`、`|` 语义不同,直接拒绝不参与切分)
|
|
2503
|
+
* - 恰好一段能解析成 ec 命令(`ec ...`,或 `ec ... | 受控过滤器`)
|
|
2504
|
+
* - 其余每一段都必须是零副作用段:`cd <字面量目录>` / `pwd` / `echo <字面量>`
|
|
2505
|
+
* - `cd` 至多一段且必须是前缀;目录无变量/通配,由 policy 层校验范围
|
|
2506
|
+
*
|
|
2507
|
+
* 返回 ec 段的 argv(调用方按 bounded-output 同样的方式直接 spawn,不经 shell)。
|
|
2508
|
+
* 前后的零副作用段只用来判定结构,不参与执行。
|
|
2509
|
+
*/
|
|
2510
|
+
export function parseCdPrefixedEcCommand(input) {
|
|
2511
|
+
const text = trimShellSpacing(input);
|
|
2512
|
+
if (!text)
|
|
2513
|
+
return null;
|
|
2514
|
+
// 只看 `&&` 与 `;` 两种分隔符;以下形态语义不同,一律拒绝:
|
|
2515
|
+
// `||` 短路、`&&&`、以及真正的后台执行 `&`。
|
|
2516
|
+
// 两点必须注意:
|
|
2517
|
+
// 1. 不能写成 `[^|]&[^&]`——它会命中 ` && ` 里的「空格 + & + 空格」,
|
|
2518
|
+
// 把正常拼接一并拒掉,所以用前后断言。
|
|
2519
|
+
// 2. `2>&1` 里的 `&` 是重定向,不是后台符号,必须先剔除再找裸 `&`。
|
|
2520
|
+
const withoutFdRedirect = text.replace(/[0-9]?>>?&[0-9]+/g, ' ');
|
|
2521
|
+
if (/\|\||&&&/.test(text) || /(?<!&)&(?!&)/.test(withoutFdRedirect))
|
|
2522
|
+
return null;
|
|
2523
|
+
const segments = text.split(/(?:\s*&&\s*|\s*;\s*)/).map(trimShellSpacing).filter(Boolean);
|
|
2524
|
+
if (segments.length === 0)
|
|
2525
|
+
return null;
|
|
2526
|
+
let cwd;
|
|
2527
|
+
let ecCommand;
|
|
2528
|
+
let suffix;
|
|
2529
|
+
let ecIndex = -1;
|
|
2530
|
+
for (let index = 0; index < segments.length; index++) {
|
|
2531
|
+
const segment = segments[index];
|
|
2532
|
+
const parsed = parseEcTail(segment);
|
|
2533
|
+
if (parsed) {
|
|
2534
|
+
if (ecCommand)
|
|
2535
|
+
return null; // 两条 ec,拒绝
|
|
2536
|
+
ecCommand = parsed;
|
|
2537
|
+
ecIndex = index;
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
// 非 ec 段:只允许零副作用形态。
|
|
2541
|
+
const argv = parseInertSegment(segment);
|
|
2542
|
+
if (!argv || !isInertSegment(argv))
|
|
1666
2543
|
return null;
|
|
1667
|
-
|
|
2544
|
+
if (argv[0] === 'cd') {
|
|
2545
|
+
// cd 必须以字面量目录为唯一参数,且只能出现在 ec 之前。
|
|
2546
|
+
// ecIndex 为 -1 表示尚未遇到 ec,此时按定义 cd 就在前面。
|
|
2547
|
+
if (argv.length !== 2 || cwd !== undefined || (ecIndex >= 0 && index > ecIndex))
|
|
2548
|
+
return null;
|
|
2549
|
+
const dir = argv[1];
|
|
2550
|
+
if (!dir || dir.startsWith('-'))
|
|
2551
|
+
return null;
|
|
2552
|
+
cwd = dir;
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
// pwd / echo:ec 之前只是分隔提示,不执行;ec 之后作为尾部命令执行一次。
|
|
2556
|
+
// 实参已在 parseInertSegment 里过滤成纯字面量,不含变量/通配/重定向。
|
|
2557
|
+
if (ecIndex >= 0) {
|
|
2558
|
+
if (suffix)
|
|
2559
|
+
return null; // 至多一段尾部命令
|
|
2560
|
+
suffix = argv;
|
|
2561
|
+
}
|
|
1668
2562
|
}
|
|
1669
|
-
|
|
2563
|
+
if (!ecCommand)
|
|
2564
|
+
return null;
|
|
2565
|
+
return {
|
|
2566
|
+
...(cwd ? { cwd } : {}),
|
|
2567
|
+
command: { ...ecCommand, ...(suffix ? { suffixArgv: suffix } : {}) },
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* 解析白名单里 ec 那一段,接受两种形态:
|
|
2572
|
+
* 1. 单条字面量 `ec ...`
|
|
2573
|
+
* 2. `ec ... 2>&1 | head` 这类已有受控过滤器(bounded-output)
|
|
2574
|
+
* 两者都直接产出 argv 交给运行器,不再经 shell。其它一律返回 null。
|
|
2575
|
+
*/
|
|
2576
|
+
function parseEcTail(segment) {
|
|
2577
|
+
const literal = parseLiteralShellArgv(segment);
|
|
2578
|
+
if (literal && literal.length > 0 && normalizeEvolcoreCommandArgv(literal)[0] === 'ec') {
|
|
2579
|
+
return { argv: literal, mergeStderr: false };
|
|
2580
|
+
}
|
|
2581
|
+
const bounded = parseBoundedOutputShellCommand(segment);
|
|
2582
|
+
if (bounded && bounded.argv[0] === 'ec')
|
|
2583
|
+
return bounded;
|
|
2584
|
+
return null;
|
|
1670
2585
|
}
|
|
1671
2586
|
/**
|
|
1672
2587
|
* Classify only the shell boundary around an EC process invocation. Runner
|
|
@@ -1675,7 +2590,30 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1675
2590
|
*/
|
|
1676
2591
|
export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
1677
2592
|
const parseOptions = { allowManagedTmpDir: true, ...options };
|
|
1678
|
-
|
|
2593
|
+
// heredoc 正文是数据不是语法,先剥掉再判——否则提交信息里的 `; ec ...`
|
|
2594
|
+
// 会被当成本轮真的拼接调用(详见 stripHeredocBodies)。
|
|
2595
|
+
const stripped = stripHeredocBodies(command);
|
|
2596
|
+
const powerShellCallOperatorIntent = looksLikePowerShellEvolcoreCallOperator(stripped, options.dialect);
|
|
2597
|
+
const powerShellCallOperatorCommand = powerShellCallOperatorIntent
|
|
2598
|
+
? parsePowerShellCallOperator(stripped)
|
|
2599
|
+
: undefined;
|
|
2600
|
+
if (powerShellCallOperatorCommand) {
|
|
2601
|
+
const parsed = parseLiteralShellCommand(powerShellCallOperatorCommand, {
|
|
2602
|
+
...parseOptions,
|
|
2603
|
+
dialect: 'powershell',
|
|
2604
|
+
});
|
|
2605
|
+
if (parsed.ok && normalizeEvolcoreCommandArgv(parsed.argv)[0] === 'ec')
|
|
2606
|
+
return { kind: 'literal' };
|
|
2607
|
+
return { kind: 'composite', issue: parsed.ok ? 'shell-composition' : parsed.issue };
|
|
2608
|
+
}
|
|
2609
|
+
if (powerShellCallOperatorIntent) {
|
|
2610
|
+
const parsed = parseLiteralShellCommand(stripped, {
|
|
2611
|
+
...parseOptions,
|
|
2612
|
+
dialect: 'powershell',
|
|
2613
|
+
});
|
|
2614
|
+
return { kind: 'composite', issue: parsed.ok ? 'shell-composition' : parsed.issue };
|
|
2615
|
+
}
|
|
2616
|
+
const literal = parseLiteralShellCommand(stripped, parseOptions);
|
|
1679
2617
|
if (literal.ok) {
|
|
1680
2618
|
if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
|
|
1681
2619
|
return { kind: 'literal' };
|
|
@@ -1689,7 +2627,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1689
2627
|
return { kind: 'composite', issue: nested.issue };
|
|
1690
2628
|
}
|
|
1691
2629
|
}
|
|
1692
|
-
const stringCarrier = resolveCodexShellCarrierString(
|
|
2630
|
+
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
1693
2631
|
if (stringCarrier) {
|
|
1694
2632
|
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1695
2633
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
@@ -1699,7 +2637,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1699
2637
|
return { kind: 'composite', issue: nested.issue };
|
|
1700
2638
|
}
|
|
1701
2639
|
}
|
|
1702
|
-
const malformedCmdBody = extractCodexCmdCarrierBody(
|
|
2640
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
1703
2641
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1704
2642
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1705
2643
|
...parseOptions,
|
|
@@ -1711,7 +2649,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1711
2649
|
}
|
|
1712
2650
|
return { kind: 'none' };
|
|
1713
2651
|
}
|
|
1714
|
-
const stringCarrier = resolveCodexShellCarrierString(
|
|
2652
|
+
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
1715
2653
|
if (stringCarrier) {
|
|
1716
2654
|
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1717
2655
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
@@ -1722,7 +2660,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1722
2660
|
}
|
|
1723
2661
|
return { kind: 'none' };
|
|
1724
2662
|
}
|
|
1725
|
-
const malformedCmdBody = extractCodexCmdCarrierBody(
|
|
2663
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
1726
2664
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1727
2665
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1728
2666
|
...parseOptions,
|
|
@@ -1734,14 +2672,21 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1734
2672
|
}
|
|
1735
2673
|
const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
|
|
1736
2674
|
? null
|
|
1737
|
-
: parseBoundedOutputShellCommand(
|
|
2675
|
+
: parseBoundedOutputShellCommand(stripped);
|
|
1738
2676
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
1739
2677
|
return { kind: 'bounded-output', command: boundedOutput };
|
|
1740
2678
|
}
|
|
2679
|
+
// `cd <字面量目录> && ec ...`:切目录是模型最常见的写法,结构可证时放行。
|
|
2680
|
+
// 只放宽前缀,ec 段仍走完整的字面量解析。
|
|
2681
|
+
const cdPrefixed = options.dialect === 'powershell' || options.dialect === 'cmd'
|
|
2682
|
+
? null
|
|
2683
|
+
: parseCdPrefixedEcCommand(stripped);
|
|
2684
|
+
if (cdPrefixed)
|
|
2685
|
+
return { kind: 'cd-prefixed', command: cdPrefixed.command, cwd: cdPrefixed.cwd };
|
|
1741
2686
|
// Only a command-boundary prefix is enough to preserve a useful syntax
|
|
1742
2687
|
// error for a malformed direct EC invocation. Never scan for EC tokens in
|
|
1743
2688
|
// arbitrary raw text after parsing fails.
|
|
1744
|
-
return hasEcCommandPrefix(
|
|
2689
|
+
return hasEcCommandPrefix(stripped)
|
|
1745
2690
|
? { kind: 'composite', issue: literal.issue }
|
|
1746
2691
|
: { kind: 'none' };
|
|
1747
2692
|
}
|
|
@@ -1837,6 +2782,10 @@ export function containsLiteralManagedTmpDirOutsideSendContent(argv) {
|
|
|
1837
2782
|
return true;
|
|
1838
2783
|
return literalIndexes.some(index => !textIndexes.has(index));
|
|
1839
2784
|
}
|
|
2785
|
+
/** Shell-escaped TMPDIR references are literal text and must not authorize file paths. */
|
|
2786
|
+
export function containsUnresolvedManagedTmpDirReference(argv) {
|
|
2787
|
+
return argv.some(value => /(?:\\\$TMPDIR|\\\$\{TMPDIR\})/i.test(value));
|
|
2788
|
+
}
|
|
1840
2789
|
function hasExecutableProactiveSendPayload(argv, scope) {
|
|
1841
2790
|
if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
|
|
1842
2791
|
return false;
|