evolcore 0.0.27 → 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 +38 -15
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +29 -14
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +13 -2
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +444 -106
- 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/client.js +1 -0
- 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 +262 -86
- 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 +116 -149
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/diag.js +1 -1
- 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/menu-client.js +4 -0
- package/dist/cli/restart-monitor.js +127 -23
- 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 +80 -24
- package/dist/core/command/diag-service.js +292 -27
- 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 +331 -79
- package/dist/core/command/menu-protocol.js +45 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +230 -79
- 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 +90 -20
- package/dist/core/message/message-bridge.js +333 -23
- 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 +596 -90
- package/dist/core/permission/ec-command-parser.js +1044 -87
- 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 +277 -24
- 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 +482 -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 +722 -171
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/product.js +1 -0
- 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/atomic-write.js +98 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/cross-platform.js +87 -16
- 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/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/welcome.js +37 -8
- package/dist/utils/windows-shell-trust.js +37 -0
- 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/channels/aun.md +8 -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,
|
|
@@ -376,12 +920,115 @@ function parseFailure(issue, input, offset, tokenIndex, dialect) {
|
|
|
376
920
|
inputLength: input.length,
|
|
377
921
|
};
|
|
378
922
|
}
|
|
923
|
+
// PowerShell uses smart quotes as delimiters, including when the opening
|
|
924
|
+
// quote was ASCII. Treating them as text changes argv and delegation hashes.
|
|
925
|
+
function isPowerShellSingleQuote(char) {
|
|
926
|
+
return char !== undefined && /['\u2018\u2019\u201a\u201b]/u.test(char);
|
|
927
|
+
}
|
|
928
|
+
function isPowerShellDoubleQuote(char) {
|
|
929
|
+
return char !== undefined && /["\u201c\u201d\u201e]/u.test(char);
|
|
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
|
+
}
|
|
379
1018
|
function parseLiteralPowerShellCommand(command, options = {}) {
|
|
380
1019
|
const input = trimShellSpacing(command);
|
|
381
1020
|
if (!input)
|
|
382
1021
|
return parseFailure('empty-command', input, 0, 0, 'powershell');
|
|
383
1022
|
if (input.includes('\0'))
|
|
384
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);
|
|
385
1032
|
const argv = [];
|
|
386
1033
|
let token = '';
|
|
387
1034
|
let tokenStarted = false;
|
|
@@ -399,11 +1046,11 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
399
1046
|
for (let index = 0; index < input.length; index++) {
|
|
400
1047
|
const char = input[index];
|
|
401
1048
|
if (quote === 'single') {
|
|
402
|
-
if (char
|
|
403
|
-
token +=
|
|
1049
|
+
if (isPowerShellSingleQuote(char) && isPowerShellSingleQuote(input[index + 1])) {
|
|
1050
|
+
token += input[index + 1];
|
|
404
1051
|
index++;
|
|
405
1052
|
}
|
|
406
|
-
else if (char
|
|
1053
|
+
else if (isPowerShellSingleQuote(char)) {
|
|
407
1054
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
408
1055
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
409
1056
|
quote = null;
|
|
@@ -420,7 +1067,7 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
420
1067
|
return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
|
|
421
1068
|
}
|
|
422
1069
|
if (!options.allowManagedTmpDir || !options.managedTempDir
|
|
423
|
-
|| (next !== undefined && !/[\\/\s"']/u.test(next))) {
|
|
1070
|
+
|| (next !== undefined && !/[\\/\s"']/u.test(next) && !isPowerShellSingleQuote(next) && !isPowerShellDoubleQuote(next))) {
|
|
424
1071
|
return parseFailure('unsafe-expansion', input, index, argv.length, 'powershell');
|
|
425
1072
|
}
|
|
426
1073
|
// Append the registered value directly: spaces remain in this argument,
|
|
@@ -431,7 +1078,7 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
431
1078
|
continue;
|
|
432
1079
|
}
|
|
433
1080
|
if (quote === 'double') {
|
|
434
|
-
if (char
|
|
1081
|
+
if (isPowerShellDoubleQuote(char)) {
|
|
435
1082
|
if (!isLiteralArgBoundary(input[index + 1]))
|
|
436
1083
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
437
1084
|
quote = null;
|
|
@@ -443,6 +1090,8 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
443
1090
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
444
1091
|
if (next === '\r' || next === '\n')
|
|
445
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');
|
|
446
1095
|
token += next;
|
|
447
1096
|
continue;
|
|
448
1097
|
}
|
|
@@ -458,12 +1107,12 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
458
1107
|
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
459
1108
|
return parseFailure('invalid-control-char', input, index, argv.length, 'powershell');
|
|
460
1109
|
}
|
|
461
|
-
if (char
|
|
1110
|
+
if (isPowerShellSingleQuote(char)) {
|
|
462
1111
|
tokenStarted = true;
|
|
463
1112
|
quote = 'single';
|
|
464
1113
|
continue;
|
|
465
1114
|
}
|
|
466
|
-
if (char
|
|
1115
|
+
if (isPowerShellDoubleQuote(char)) {
|
|
467
1116
|
tokenStarted = true;
|
|
468
1117
|
quote = 'double';
|
|
469
1118
|
continue;
|
|
@@ -474,6 +1123,8 @@ function parseLiteralPowerShellCommand(command, options = {}) {
|
|
|
474
1123
|
return parseFailure('invalid-quote', input, index, argv.length, 'powershell');
|
|
475
1124
|
if (next === '\r' || next === '\n')
|
|
476
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');
|
|
477
1128
|
tokenStarted = true;
|
|
478
1129
|
token += next;
|
|
479
1130
|
continue;
|
|
@@ -671,7 +1322,7 @@ function resolveCodexPowerShellCarrierString(command) {
|
|
|
671
1322
|
/** Resolve a POSIX shell carrier when app-server provides only a string. */
|
|
672
1323
|
function resolveCodexPosixShellCarrierString(command) {
|
|
673
1324
|
const input = trimShellSpacing(command);
|
|
674
|
-
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/);
|
|
1325
|
+
const match = input.match(/^(?:"([^"]+)"|(\S+))\s+(-lc|-c)\s+(.+)$/s);
|
|
675
1326
|
if (!match)
|
|
676
1327
|
return undefined;
|
|
677
1328
|
const executable = match[1] ?? match[2];
|
|
@@ -732,7 +1383,7 @@ function looksLikeEcCommandBody(command) {
|
|
|
732
1383
|
return false;
|
|
733
1384
|
const input = command.trim();
|
|
734
1385
|
const body = (input.startsWith('"') ? input.slice(1) : input).trimStart();
|
|
735
|
-
return /^ec(?:\.cmd|\.exe)?(?:\s|$)/i.test(body);
|
|
1386
|
+
return /^ec(?:\.cmd|\.exe|\.ps1)?(?:\s|$)/i.test(body);
|
|
736
1387
|
}
|
|
737
1388
|
/** True only when a trusted cmd executable is carrying an EC command body. */
|
|
738
1389
|
export function hasCodexCmdCarrierEcIntent(input) {
|
|
@@ -811,7 +1462,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
811
1462
|
const char = input[index];
|
|
812
1463
|
if (quote === 'single') {
|
|
813
1464
|
if (char === "'") {
|
|
814
|
-
if (!
|
|
1465
|
+
if (!isPosixQuotedTokenBoundary(input[index + 1]))
|
|
815
1466
|
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
816
1467
|
quote = null;
|
|
817
1468
|
}
|
|
@@ -821,7 +1472,7 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
821
1472
|
}
|
|
822
1473
|
if (quote === 'double') {
|
|
823
1474
|
if (char === '"') {
|
|
824
|
-
if (!
|
|
1475
|
+
if (!isPosixQuotedTokenBoundary(input[index + 1]))
|
|
825
1476
|
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
826
1477
|
quote = null;
|
|
827
1478
|
continue;
|
|
@@ -919,6 +1570,14 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
919
1570
|
* command still has to pass `parseLiteralShellCommand`.
|
|
920
1571
|
*/
|
|
921
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
|
+
}
|
|
922
1581
|
const outer = parseLiteralShellArgv(command, options);
|
|
923
1582
|
if (outer) {
|
|
924
1583
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
@@ -951,6 +1610,19 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
951
1610
|
const nulIndex = outer.findIndex(value => value.includes('\0'));
|
|
952
1611
|
if (nulIndex >= 0)
|
|
953
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
|
+
}
|
|
954
1626
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
955
1627
|
if (carrier === undefined) {
|
|
956
1628
|
return parseCodexCmdCarrierArgvFailure(outer)
|
|
@@ -959,6 +1631,17 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
959
1631
|
return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
|
|
960
1632
|
}
|
|
961
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
|
+
}
|
|
962
1645
|
const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
|
|
963
1646
|
if (outer.ok) {
|
|
964
1647
|
const carrier = resolveCodexShellCarrierArgv(outer.argv);
|
|
@@ -976,6 +1659,98 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
976
1659
|
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
977
1660
|
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
978
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
|
+
}
|
|
979
1754
|
/**
|
|
980
1755
|
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
981
1756
|
* supplied by app-server. Joining that argv with spaces loses the quoting of
|
|
@@ -1547,29 +2322,38 @@ function scanBoundedOutputOperators(command) {
|
|
|
1547
2322
|
return quote ? null : operators;
|
|
1548
2323
|
}
|
|
1549
2324
|
/**
|
|
1550
|
-
* Accept
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
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.
|
|
1553
2330
|
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
* 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.
|
|
1557
2333
|
*/
|
|
1558
|
-
function
|
|
2334
|
+
export function parseStdinOnlyFilterArgv(segment) {
|
|
1559
2335
|
const argv = parseLiteralShellArgv(segment);
|
|
1560
|
-
if (!argv
|
|
2336
|
+
if (!argv)
|
|
2337
|
+
return null;
|
|
2338
|
+
const executable = normalizeStdoutFilterExecutable(argv[0]);
|
|
2339
|
+
if (!executable)
|
|
1561
2340
|
return null;
|
|
1562
|
-
const
|
|
2341
|
+
const spec = STDOUT_FILTER_SPECS.get(executable);
|
|
2342
|
+
if (!spec)
|
|
2343
|
+
return null;
|
|
2344
|
+
let operands = 0;
|
|
2345
|
+
let sawPatternValue = false;
|
|
1563
2346
|
let optionsEnded = false;
|
|
1564
2347
|
let expectsOptionValue = false;
|
|
1565
|
-
for (
|
|
2348
|
+
for (let index = 1; index < argv.length; index++) {
|
|
2349
|
+
const arg = argv[index];
|
|
1566
2350
|
if (expectsOptionValue) {
|
|
1567
2351
|
expectsOptionValue = false;
|
|
1568
2352
|
continue;
|
|
1569
2353
|
}
|
|
1570
2354
|
if (optionsEnded) {
|
|
1571
2355
|
if (arg !== '-')
|
|
1572
|
-
|
|
2356
|
+
operands++;
|
|
1573
2357
|
continue;
|
|
1574
2358
|
}
|
|
1575
2359
|
if (arg === '--') {
|
|
@@ -1578,18 +2362,43 @@ function parseStdinOnlyHeadArgs(segment) {
|
|
|
1578
2362
|
}
|
|
1579
2363
|
if (arg === '-')
|
|
1580
2364
|
continue;
|
|
1581
|
-
if (!arg.startsWith('-'))
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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;
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
if (spec.valueOptions.has(option)) {
|
|
2381
|
+
if (!arg.includes('=') && arg.length === option.length)
|
|
2382
|
+
expectsOptionValue = true;
|
|
1585
2383
|
continue;
|
|
1586
2384
|
}
|
|
2385
|
+
if (spec.flags.has(arg))
|
|
2386
|
+
continue;
|
|
2387
|
+
return null;
|
|
1587
2388
|
}
|
|
1588
|
-
|
|
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)];
|
|
1589
2397
|
}
|
|
1590
2398
|
/**
|
|
1591
2399
|
* Parse the only shell wrapper allowed around a privileged literal process:
|
|
1592
|
-
* optional stderr merge, one
|
|
2400
|
+
* optional stderr merge, one trusted stdin-only stdout filter, and one
|
|
2401
|
+
* create-only output path.
|
|
1593
2402
|
*/
|
|
1594
2403
|
export function parseBoundedOutputShellCommand(command) {
|
|
1595
2404
|
const input = trimShellSpacing(substituteManagedTmpDirToken(command));
|
|
@@ -1598,16 +2407,17 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1598
2407
|
const operators = scanBoundedOutputOperators(input);
|
|
1599
2408
|
if (!operators || operators.length === 0)
|
|
1600
2409
|
return null;
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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。
|
|
1611
2421
|
const first = operators[0];
|
|
1612
2422
|
const sourceText = trimShellSpacing(input.slice(0, first.start));
|
|
1613
2423
|
const argv = parseLiteralShellArgv(sourceText);
|
|
@@ -1617,48 +2427,161 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1617
2427
|
// expansion for the privileged command itself.
|
|
1618
2428
|
if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
|
|
1619
2429
|
return null;
|
|
1620
|
-
|
|
1621
|
-
let
|
|
1622
|
-
let
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
pipe = operators[1];
|
|
1628
|
-
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)))
|
|
1629
2437
|
return null;
|
|
1630
|
-
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')
|
|
1631
2452
|
return null;
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
redirect = operators[1];
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
redirect = first;
|
|
1640
|
-
}
|
|
1641
|
-
let headArgs;
|
|
1642
|
-
if (pipe) {
|
|
1643
|
-
const headEnd = redirect?.start ?? input.length;
|
|
1644
|
-
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)));
|
|
1645
2457
|
if (parsedArgs === null)
|
|
1646
2458
|
return null;
|
|
1647
|
-
|
|
2459
|
+
filterChain.push(parsedArgs);
|
|
2460
|
+
prevEnd = segmentEnd;
|
|
2461
|
+
cursor++;
|
|
1648
2462
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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))
|
|
1658
2543
|
return null;
|
|
1659
|
-
|
|
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
|
+
}
|
|
1660
2562
|
}
|
|
1661
|
-
|
|
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;
|
|
1662
2585
|
}
|
|
1663
2586
|
/**
|
|
1664
2587
|
* Classify only the shell boundary around an EC process invocation. Runner
|
|
@@ -1667,7 +2590,30 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
1667
2590
|
*/
|
|
1668
2591
|
export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
1669
2592
|
const parseOptions = { allowManagedTmpDir: true, ...options };
|
|
1670
|
-
|
|
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);
|
|
1671
2617
|
if (literal.ok) {
|
|
1672
2618
|
if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
|
|
1673
2619
|
return { kind: 'literal' };
|
|
@@ -1681,7 +2627,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1681
2627
|
return { kind: 'composite', issue: nested.issue };
|
|
1682
2628
|
}
|
|
1683
2629
|
}
|
|
1684
|
-
const stringCarrier = resolveCodexShellCarrierString(
|
|
2630
|
+
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
1685
2631
|
if (stringCarrier) {
|
|
1686
2632
|
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1687
2633
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
@@ -1691,7 +2637,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1691
2637
|
return { kind: 'composite', issue: nested.issue };
|
|
1692
2638
|
}
|
|
1693
2639
|
}
|
|
1694
|
-
const malformedCmdBody = extractCodexCmdCarrierBody(
|
|
2640
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
1695
2641
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1696
2642
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1697
2643
|
...parseOptions,
|
|
@@ -1703,7 +2649,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1703
2649
|
}
|
|
1704
2650
|
return { kind: 'none' };
|
|
1705
2651
|
}
|
|
1706
|
-
const stringCarrier = resolveCodexShellCarrierString(
|
|
2652
|
+
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
1707
2653
|
if (stringCarrier) {
|
|
1708
2654
|
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
1709
2655
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
@@ -1714,7 +2660,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1714
2660
|
}
|
|
1715
2661
|
return { kind: 'none' };
|
|
1716
2662
|
}
|
|
1717
|
-
const malformedCmdBody = extractCodexCmdCarrierBody(
|
|
2663
|
+
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
1718
2664
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
1719
2665
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
1720
2666
|
...parseOptions,
|
|
@@ -1726,14 +2672,21 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1726
2672
|
}
|
|
1727
2673
|
const boundedOutput = options.dialect === 'powershell' || options.dialect === 'cmd'
|
|
1728
2674
|
? null
|
|
1729
|
-
: parseBoundedOutputShellCommand(
|
|
2675
|
+
: parseBoundedOutputShellCommand(stripped);
|
|
1730
2676
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
1731
2677
|
return { kind: 'bounded-output', command: boundedOutput };
|
|
1732
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 };
|
|
1733
2686
|
// Only a command-boundary prefix is enough to preserve a useful syntax
|
|
1734
2687
|
// error for a malformed direct EC invocation. Never scan for EC tokens in
|
|
1735
2688
|
// arbitrary raw text after parsing fails.
|
|
1736
|
-
return hasEcCommandPrefix(
|
|
2689
|
+
return hasEcCommandPrefix(stripped)
|
|
1737
2690
|
? { kind: 'composite', issue: literal.issue }
|
|
1738
2691
|
: { kind: 'none' };
|
|
1739
2692
|
}
|
|
@@ -1829,6 +2782,10 @@ export function containsLiteralManagedTmpDirOutsideSendContent(argv) {
|
|
|
1829
2782
|
return true;
|
|
1830
2783
|
return literalIndexes.some(index => !textIndexes.has(index));
|
|
1831
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
|
+
}
|
|
1832
2789
|
function hasExecutableProactiveSendPayload(argv, scope) {
|
|
1833
2790
|
if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
|
|
1834
2791
|
return false;
|