evolcore 0.0.17 → 0.0.19
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 +51 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +43 -24
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +82 -14
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/data-migration.js +10 -4
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +66 -8
- package/dist/core/message/response-engine.js +147 -10
- package/dist/core/permission/ec-command-parser.js +148 -22
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +19 -7
- package/dist/index.js +357 -48
- package/dist/ipc.js +81 -5
- package/dist/paths.js +0 -3
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/docs/path-registry.md +1 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/02-navigation.md +2 -2
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
|
|
2
2
|
// Codex app-server sends command executions through a fixed shell carrier.
|
|
3
3
|
// Treat the carrier as syntax only after parsing it as literal argv; never
|
|
4
|
-
// accept an arbitrary
|
|
5
|
-
const
|
|
4
|
+
// accept an arbitrary shell command string or unwrap composition here.
|
|
5
|
+
const CODEX_POSIX_SHELL_EXECUTABLES = new Set([
|
|
6
6
|
'bash', '/bin/bash', '/usr/bin/bash',
|
|
7
7
|
'sh', '/bin/sh', '/usr/bin/sh',
|
|
8
8
|
]);
|
|
9
|
+
const CODEX_POWERSHELL_EXECUTABLES = new Set([
|
|
10
|
+
'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
|
|
11
|
+
]);
|
|
12
|
+
const CODEX_POWERSHELL_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Program Files[\\/]PowerShell[\\/][0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9.-]+)?[\\/]pwsh\.exe|Windows[\\/]System32[\\/]WindowsPowerShell[\\/]v1\.0[\\/]powershell\.exe)$/i;
|
|
9
13
|
const SHELL_UNQUOTED_META = new Set([
|
|
10
14
|
';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
|
|
11
15
|
]);
|
|
16
|
+
const POWERSHELL_UNQUOTED_META = new Set([
|
|
17
|
+
';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
|
|
18
|
+
]);
|
|
12
19
|
const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
|
|
13
20
|
const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
|
|
14
21
|
function trimShellSpacing(value) {
|
|
@@ -83,6 +90,106 @@ function isLiteralArgBoundary(value) {
|
|
|
83
90
|
function isShellParameterExpansionStart(value) {
|
|
84
91
|
return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
|
|
85
92
|
}
|
|
93
|
+
function parseLiteralPowerShellCommand(command) {
|
|
94
|
+
const input = trimShellSpacing(command);
|
|
95
|
+
if (!input)
|
|
96
|
+
return { ok: false, issue: 'empty-command' };
|
|
97
|
+
if (input.includes('\0'))
|
|
98
|
+
return { ok: false, issue: 'invalid-control-char' };
|
|
99
|
+
const argv = [];
|
|
100
|
+
let token = '';
|
|
101
|
+
let tokenStarted = false;
|
|
102
|
+
let quote = null;
|
|
103
|
+
const finishToken = () => {
|
|
104
|
+
if (!tokenStarted)
|
|
105
|
+
return;
|
|
106
|
+
argv.push(token);
|
|
107
|
+
token = '';
|
|
108
|
+
tokenStarted = false;
|
|
109
|
+
};
|
|
110
|
+
for (let index = 0; index < input.length; index++) {
|
|
111
|
+
const char = input[index];
|
|
112
|
+
if (quote === 'single') {
|
|
113
|
+
if (char === "'" && input[index + 1] === "'") {
|
|
114
|
+
token += "'";
|
|
115
|
+
index++;
|
|
116
|
+
}
|
|
117
|
+
else if (char === "'") {
|
|
118
|
+
if (!isLiteralArgBoundary(input[index + 1]))
|
|
119
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
120
|
+
quote = null;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
token += char;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (quote === 'double') {
|
|
128
|
+
if (char === '"') {
|
|
129
|
+
if (!isLiteralArgBoundary(input[index + 1]))
|
|
130
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
131
|
+
quote = null;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === '`') {
|
|
135
|
+
const next = input[++index];
|
|
136
|
+
if (next === undefined)
|
|
137
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
138
|
+
if (next === '\r' || next === '\n')
|
|
139
|
+
return { ok: false, issue: 'structural-newline' };
|
|
140
|
+
token += next;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
|
|
144
|
+
return { ok: false, issue: 'unsafe-expansion' };
|
|
145
|
+
}
|
|
146
|
+
token += char;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === ' ' || char === '\t') {
|
|
150
|
+
finishToken();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (char === '\r' || char === '\n')
|
|
154
|
+
return { ok: false, issue: 'structural-newline' };
|
|
155
|
+
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
156
|
+
return { ok: false, issue: 'invalid-control-char' };
|
|
157
|
+
}
|
|
158
|
+
if (char === "'") {
|
|
159
|
+
tokenStarted = true;
|
|
160
|
+
quote = 'single';
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === '"') {
|
|
164
|
+
tokenStarted = true;
|
|
165
|
+
quote = 'double';
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (char === '`') {
|
|
169
|
+
const next = input[++index];
|
|
170
|
+
if (next === undefined)
|
|
171
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
172
|
+
if (next === '\r' || next === '\n')
|
|
173
|
+
return { ok: false, issue: 'structural-newline' };
|
|
174
|
+
tokenStarted = true;
|
|
175
|
+
token += next;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
|
|
179
|
+
const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
|
|
180
|
+
if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
|
|
181
|
+
return { ok: false, issue: 'shell-composition' };
|
|
182
|
+
}
|
|
183
|
+
tokenStarted = true;
|
|
184
|
+
token += char;
|
|
185
|
+
}
|
|
186
|
+
if (quote)
|
|
187
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
188
|
+
finishToken();
|
|
189
|
+
return argv.length > 0
|
|
190
|
+
? { ok: true, argv }
|
|
191
|
+
: { ok: false, issue: 'empty-command' };
|
|
192
|
+
}
|
|
86
193
|
function doubleQuotedIssueAt(input, index) {
|
|
87
194
|
const char = input[index];
|
|
88
195
|
if (char === '`' || (char === '$' && isShellParameterExpansionStart(input[index + 1]))) {
|
|
@@ -102,6 +209,9 @@ function doubleQuotedIssueAt(input, index) {
|
|
|
102
209
|
* Anything outside this subset is never granted the privileged EC path.
|
|
103
210
|
*/
|
|
104
211
|
export function parseLiteralShellCommand(command, options = {}) {
|
|
212
|
+
if (options.dialect === 'powershell') {
|
|
213
|
+
return parseLiteralPowerShellCommand(command);
|
|
214
|
+
}
|
|
105
215
|
const input = trimShellSpacing(options.allowManagedTmpDir
|
|
106
216
|
? substituteManagedTmpDirToken(command)
|
|
107
217
|
: command);
|
|
@@ -209,15 +319,21 @@ export function parseLiteralShellArgv(command, options = {}) {
|
|
|
209
319
|
const result = parseLiteralShellCommand(command, options);
|
|
210
320
|
return result.ok ? result.argv : null;
|
|
211
321
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
* wrapper. The wrapper is accepted only in its exact three-argv form and the
|
|
215
|
-
* caller must parse the returned string with the literal shell parser again.
|
|
216
|
-
*/
|
|
217
|
-
export function unwrapCodexShellCommandArgv(argv) {
|
|
218
|
-
if (argv.length !== 3 || argv[1] !== '-lc')
|
|
322
|
+
export function resolveCodexShellCarrierArgv(argv) {
|
|
323
|
+
if (argv.length !== 3)
|
|
219
324
|
return undefined;
|
|
220
|
-
|
|
325
|
+
if (argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
|
|
326
|
+
return { command: argv[2], dialect: 'posix' };
|
|
327
|
+
}
|
|
328
|
+
const powershellExecutable = argv[0].toLowerCase();
|
|
329
|
+
const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
|
|
330
|
+
|| CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
|
|
331
|
+
return isPowerShell && argv[1].toLowerCase() === '-command'
|
|
332
|
+
? { command: argv[2], dialect: 'powershell' }
|
|
333
|
+
: undefined;
|
|
334
|
+
}
|
|
335
|
+
export function unwrapCodexShellCommandArgv(argv) {
|
|
336
|
+
return resolveCodexShellCarrierArgv(argv)?.command;
|
|
221
337
|
}
|
|
222
338
|
/**
|
|
223
339
|
* Parse either a bare literal command or the exact shell carrier emitted by
|
|
@@ -228,8 +344,10 @@ export function parseCodexShellCommandArgv(command, options = {}) {
|
|
|
228
344
|
const outer = parseLiteralShellArgv(command, options);
|
|
229
345
|
if (!outer)
|
|
230
346
|
return null;
|
|
231
|
-
const
|
|
232
|
-
return
|
|
347
|
+
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
348
|
+
return carrier === undefined
|
|
349
|
+
? outer
|
|
350
|
+
: parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
|
|
233
351
|
}
|
|
234
352
|
/**
|
|
235
353
|
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
@@ -239,10 +357,13 @@ export function parseCodexShellCommandArgv(command, options = {}) {
|
|
|
239
357
|
export function parseCodexToolCommandArgv(input) {
|
|
240
358
|
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
241
359
|
const outer = input.commandArgv;
|
|
242
|
-
const
|
|
243
|
-
return
|
|
360
|
+
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
361
|
+
return carrier === undefined
|
|
244
362
|
? outer
|
|
245
|
-
: parseLiteralShellArgv(
|
|
363
|
+
: parseLiteralShellArgv(carrier.command, {
|
|
364
|
+
allowManagedTmpDir: true,
|
|
365
|
+
dialect: carrier.dialect,
|
|
366
|
+
});
|
|
246
367
|
}
|
|
247
368
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
248
369
|
return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
|
|
@@ -924,24 +1045,29 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
924
1045
|
* policy must not interpret EC subcommands or arguments: a single literal
|
|
925
1046
|
* invocation may leave the sandbox, while shell composition may not.
|
|
926
1047
|
*/
|
|
927
|
-
export function classifyEvolcoreShellCommand(command) {
|
|
928
|
-
const parseOptions = { allowManagedTmpDir: true };
|
|
1048
|
+
export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
1049
|
+
const parseOptions = { allowManagedTmpDir: true, ...options };
|
|
929
1050
|
const literal = parseLiteralShellCommand(command, parseOptions);
|
|
930
1051
|
if (literal.ok) {
|
|
931
1052
|
if (literal.argv[0] === 'ec')
|
|
932
1053
|
return { kind: 'literal' };
|
|
933
|
-
const
|
|
934
|
-
if (
|
|
935
|
-
const nested = parseLiteralShellCommand(
|
|
1054
|
+
const carrier = resolveCodexShellCarrierArgv(literal.argv);
|
|
1055
|
+
if (carrier !== undefined) {
|
|
1056
|
+
const nested = parseLiteralShellCommand(carrier.command, {
|
|
1057
|
+
...parseOptions,
|
|
1058
|
+
dialect: carrier.dialect,
|
|
1059
|
+
});
|
|
936
1060
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
937
1061
|
return { kind: 'literal' };
|
|
938
|
-
if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(
|
|
1062
|
+
if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(carrier.command)) {
|
|
939
1063
|
return { kind: 'composite', issue: nested.issue };
|
|
940
1064
|
}
|
|
941
1065
|
}
|
|
942
1066
|
return { kind: 'none' };
|
|
943
1067
|
}
|
|
944
|
-
const boundedOutput =
|
|
1068
|
+
const boundedOutput = options.dialect === 'powershell'
|
|
1069
|
+
? null
|
|
1070
|
+
: parseBoundedOutputShellCommand(command);
|
|
945
1071
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
946
1072
|
return { kind: 'bounded-output', command: boundedOutput };
|
|
947
1073
|
}
|
|
@@ -4,7 +4,42 @@ import { resolveCommandPath } from '../../utils/cross-platform.js';
|
|
|
4
4
|
import { getExistingHClassMaskTargets, getExistingLClassReadOnlyTargets, isSameOrDescendant, } from '../protected-paths.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
let cachedBubblewrapPath;
|
|
7
|
+
let sandboxInitializationCircuit;
|
|
8
|
+
let sandboxCircuitNoticeAt = 0;
|
|
7
9
|
export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
|
|
10
|
+
export const SANDBOX_INITIALIZATION_COOLDOWN_MS = 5 * 60 * 1000;
|
|
11
|
+
export const SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS = 60 * 1000;
|
|
12
|
+
/** Open a process-wide fail-closed circuit after a confirmed host bootstrap failure. */
|
|
13
|
+
export function recordSandboxInitializationFailure(error, stderr = [], now = Date.now()) {
|
|
14
|
+
const detail = createSandboxInitializationError(error, stderr).message.slice(0, 512);
|
|
15
|
+
sandboxInitializationCircuit = sandboxInitializationCircuit
|
|
16
|
+
? {
|
|
17
|
+
...sandboxInitializationCircuit,
|
|
18
|
+
lastObservedAt: now,
|
|
19
|
+
occurrences: sandboxInitializationCircuit.occurrences + 1,
|
|
20
|
+
detail,
|
|
21
|
+
}
|
|
22
|
+
: { openedAt: now, lastObservedAt: now, occurrences: 1, detail };
|
|
23
|
+
return { ...sandboxInitializationCircuit };
|
|
24
|
+
}
|
|
25
|
+
/** Return an active failure circuit, or allow one probe after the cooldown. */
|
|
26
|
+
export function getActiveSandboxInitializationFailure(now = Date.now(), cooldownMs = SANDBOX_INITIALIZATION_COOLDOWN_MS) {
|
|
27
|
+
if (!sandboxInitializationCircuit)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (now - sandboxInitializationCircuit.lastObservedAt >= cooldownMs) {
|
|
30
|
+
sandboxInitializationCircuit = undefined;
|
|
31
|
+
sandboxCircuitNoticeAt = 0;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return { ...sandboxInitializationCircuit };
|
|
35
|
+
}
|
|
36
|
+
/** Rate-limit human-readable warnings; structured audit still records every blocked task. */
|
|
37
|
+
export function shouldLogSandboxCircuitNotice(now = Date.now(), intervalMs = SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS) {
|
|
38
|
+
if (sandboxCircuitNoticeAt && now - sandboxCircuitNoticeAt < intervalMs)
|
|
39
|
+
return false;
|
|
40
|
+
sandboxCircuitNoticeAt = now;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
8
43
|
/** Recognize sandbox bootstrap failures independently of localized stderr. */
|
|
9
44
|
export function isSandboxInitializationFailure(error, stderr = []) {
|
|
10
45
|
const text = [error instanceof Error ? error.message : String(error ?? ''), ...stderr]
|
|
@@ -14,9 +49,18 @@ export function isSandboxInitializationFailure(error, stderr = []) {
|
|
|
14
49
|
|| /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
|
|
15
50
|
|| /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
|
|
16
51
|
|| /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
|
|
17
|
-
|| /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text)
|
|
52
|
+
|| /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text)
|
|
53
|
+
|| /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text);
|
|
18
54
|
}
|
|
19
55
|
export function createSandboxInitializationError(error, stderr = []) {
|
|
56
|
+
if (error instanceof Error
|
|
57
|
+
&& (error.code === SANDBOX_INITIALIZATION_FAILED
|
|
58
|
+
|| error.message.includes(`${SANDBOX_INITIALIZATION_FAILED}:`))) {
|
|
59
|
+
error.code = SANDBOX_INITIALIZATION_FAILED;
|
|
60
|
+
if (stderr.length > 0)
|
|
61
|
+
error.stderr = [...stderr].slice(-20);
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
20
64
|
const detail = error instanceof Error ? error.message : String(error ?? 'sandbox bootstrap failed');
|
|
21
65
|
const wrapped = new Error(`${SANDBOX_INITIALIZATION_FAILED}: ${detail}`);
|
|
22
66
|
wrapped.name = 'SandboxInitializationError';
|
|
@@ -211,6 +255,16 @@ function appendRuntimeBackedEtcFiles(args) {
|
|
|
211
255
|
args.push('--ro-bind', candidate, resolved);
|
|
212
256
|
}
|
|
213
257
|
}
|
|
258
|
+
function appendContainerEtcFileBinds(args) {
|
|
259
|
+
for (const candidate of ['/etc/resolv.conf', '/etc/hosts', '/etc/hostname']) {
|
|
260
|
+
if (!fs.existsSync(candidate))
|
|
261
|
+
continue;
|
|
262
|
+
// A directory bind does not recursively preserve OCI/container-runtime
|
|
263
|
+
// file mounts. Rebind only the three standard runtime-injected files after
|
|
264
|
+
// /etc becomes readonly; no other /etc mount is carried into the guard.
|
|
265
|
+
args.push('--ro-bind', candidate, candidate);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
214
268
|
function appendHClassMasks(args, root, maskContainerSockets) {
|
|
215
269
|
for (const target of getExistingHClassMaskTargets(root)) {
|
|
216
270
|
if (target.kind === 'directory')
|
|
@@ -251,20 +305,30 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
251
305
|
throw new Error(`Codex managed requirements is not a file: ${requirements}`);
|
|
252
306
|
}
|
|
253
307
|
const managedDirectory = path.dirname(requirements);
|
|
308
|
+
const systemConfigDirectory = '/etc/codex';
|
|
309
|
+
let systemConfigStat;
|
|
310
|
+
try {
|
|
311
|
+
systemConfigStat = fs.lstatSync(systemConfigDirectory);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
315
|
+
+ 'must be created as a real directory during installation');
|
|
316
|
+
}
|
|
317
|
+
if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
|
|
318
|
+
throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
|
|
319
|
+
+ 'must be a real directory');
|
|
320
|
+
}
|
|
254
321
|
// Keep the copied hook immutable even when a session uses bypass mode.
|
|
255
322
|
args.push('--ro-bind', managedDirectory, managedDirectory);
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
args.push('--
|
|
261
|
-
args
|
|
262
|
-
// Replace
|
|
263
|
-
//
|
|
264
|
-
args.push('--
|
|
265
|
-
args.push('--ro-bind', requirements, '/etc/codex/requirements.toml');
|
|
266
|
-
args.push('--remount-ro', '/etc/codex');
|
|
267
|
-
args.push('--remount-ro', '/etc');
|
|
323
|
+
// The guard starts from a writable root so owner-bypass workspaces keep
|
|
324
|
+
// their normal semantics. Make the system configuration tree readonly
|
|
325
|
+
// before replacing only Codex's directory, so a root daemon cannot write
|
|
326
|
+
// unrelated files under /etc from inside this namespace.
|
|
327
|
+
args.push('--ro-bind', '/etc', '/etc');
|
|
328
|
+
appendContainerEtcFileBinds(args);
|
|
329
|
+
// Replace only Codex's system-config directory. Requiring the mountpoint to
|
|
330
|
+
// exist prevents Bubblewrap from creating it on the writable host root.
|
|
331
|
+
args.push('--ro-bind', managedDirectory, systemConfigDirectory);
|
|
268
332
|
}
|
|
269
333
|
args.push('--', executable, ...executableArgs);
|
|
270
334
|
return { command: bubblewrapPath, args };
|
|
@@ -304,4 +368,6 @@ export function buildBubblewrapCommand(executable, executableArgs, options) {
|
|
|
304
368
|
}
|
|
305
369
|
export function _resetSandboxRuntimeCache() {
|
|
306
370
|
cachedBubblewrapPath = undefined;
|
|
371
|
+
sandboxInitializationCircuit = undefined;
|
|
372
|
+
sandboxCircuitNoticeAt = 0;
|
|
307
373
|
}
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
|
|
7
|
-
import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv,
|
|
7
|
+
import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from './ec-command-parser.js';
|
|
8
8
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
9
9
|
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
10
10
|
export function resolveManagedTempDir(env = process.env) {
|
|
@@ -1148,7 +1148,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1148
1148
|
behavior: 'deny',
|
|
1149
1149
|
message: context?.permissionMode === 'readonly'
|
|
1150
1150
|
? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
|
|
1151
|
-
: '🔒 L-class
|
|
1151
|
+
: '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
|
|
1152
1152
|
};
|
|
1153
1153
|
}
|
|
1154
1154
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
@@ -1352,7 +1352,8 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1352
1352
|
? input.commandArgv
|
|
1353
1353
|
: undefined;
|
|
1354
1354
|
const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
|
|
1355
|
-
const
|
|
1355
|
+
const shellCarrier = outerArgv ? resolveCodexShellCarrierArgv(outerArgv) : undefined;
|
|
1356
|
+
const wrappedCommand = shellCarrier?.command;
|
|
1356
1357
|
const policyCommand = wrappedCommand ?? command;
|
|
1357
1358
|
if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
|
|
1358
1359
|
return {
|
|
@@ -1397,7 +1398,9 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1397
1398
|
policyCode: 'managed_ec_aid_scope_forbidden',
|
|
1398
1399
|
};
|
|
1399
1400
|
}
|
|
1400
|
-
const ecCommand = classifyEvolcoreShellCommand(policyCommand
|
|
1401
|
+
const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellCarrier
|
|
1402
|
+
? { dialect: shellCarrier.dialect }
|
|
1403
|
+
: {});
|
|
1401
1404
|
if (ecCommand.kind === 'literal') {
|
|
1402
1405
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
1403
1406
|
}
|
|
@@ -1406,25 +1409,34 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1406
1409
|
}
|
|
1407
1410
|
if (ecCommand.kind === 'composite') {
|
|
1408
1411
|
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1412
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1413
|
+
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
1414
|
+
: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
|
|
1409
1415
|
return {
|
|
1410
1416
|
behavior: 'deny',
|
|
1411
1417
|
input,
|
|
1412
|
-
message
|
|
1418
|
+
message,
|
|
1413
1419
|
policyCode: 'ec_shell_unsafe_expansion',
|
|
1414
1420
|
};
|
|
1415
1421
|
}
|
|
1416
1422
|
if (ecCommand.issue === 'invalid-quote') {
|
|
1423
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1424
|
+
? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
|
|
1425
|
+
: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
|
|
1417
1426
|
return {
|
|
1418
1427
|
behavior: 'deny',
|
|
1419
1428
|
input,
|
|
1420
|
-
message
|
|
1429
|
+
message,
|
|
1421
1430
|
policyCode: 'ec_shell_invalid_quote',
|
|
1422
1431
|
};
|
|
1423
1432
|
}
|
|
1433
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1434
|
+
? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
|
|
1435
|
+
: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
|
|
1424
1436
|
return {
|
|
1425
1437
|
behavior: 'deny',
|
|
1426
1438
|
input,
|
|
1427
|
-
message
|
|
1439
|
+
message,
|
|
1428
1440
|
policyCode: 'ec_shell_composite_command',
|
|
1429
1441
|
};
|
|
1430
1442
|
}
|