evolcore 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -14,6 +14,66 @@ const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
|
|
|
14
14
|
function trimShellSpacing(value) {
|
|
15
15
|
return value.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
16
16
|
}
|
|
17
|
+
const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
|
|
18
|
+
/**
|
|
19
|
+
* Preserve the one environment reference allowed in managed EC paths.
|
|
20
|
+
* The regular literal parser still rejects all expansion by default. The
|
|
21
|
+
* privileged EC classifier opts into this token only after the surrounding
|
|
22
|
+
* policy has validated the path against the session-owned TMPDIR.
|
|
23
|
+
*/
|
|
24
|
+
function substituteManagedTmpDirToken(value) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let quote = null;
|
|
27
|
+
for (let index = 0; index < value.length; index++) {
|
|
28
|
+
const char = value[index];
|
|
29
|
+
if (quote === 'single') {
|
|
30
|
+
result += char;
|
|
31
|
+
if (char === "'")
|
|
32
|
+
quote = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (quote === 'double') {
|
|
36
|
+
if (char === '\\') {
|
|
37
|
+
result += char;
|
|
38
|
+
if (index + 1 < value.length)
|
|
39
|
+
result += value[++index];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"') {
|
|
43
|
+
quote = null;
|
|
44
|
+
result += char;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === "'") {
|
|
49
|
+
quote = 'single';
|
|
50
|
+
result += char;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
else if (char === '\\') {
|
|
54
|
+
result += char;
|
|
55
|
+
if (index + 1 < value.length)
|
|
56
|
+
result += value[++index];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (value.startsWith('${TMPDIR}', index)) {
|
|
60
|
+
result += MANAGED_TMPDIR_TOKEN;
|
|
61
|
+
index += '${TMPDIR}'.length - 1;
|
|
62
|
+
}
|
|
63
|
+
else if (value.startsWith('$TMPDIR', index)
|
|
64
|
+
&& !/[A-Za-z0-9_]/.test(value[index + '$TMPDIR'.length] ?? '')) {
|
|
65
|
+
result += MANAGED_TMPDIR_TOKEN;
|
|
66
|
+
index += '$TMPDIR'.length - 1;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result += char;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function restoreManagedTmpDirToken(value, managedTempDir) {
|
|
75
|
+
return value.replaceAll(MANAGED_TMPDIR_TOKEN, managedTempDir ?? '$TMPDIR');
|
|
76
|
+
}
|
|
17
77
|
function isShellSpacing(value) {
|
|
18
78
|
return value === ' ' || value === '\t';
|
|
19
79
|
}
|
|
@@ -41,8 +101,10 @@ function doubleQuotedIssueAt(input, index) {
|
|
|
41
101
|
* expansion, glob, pipeline, command substitution, redirection, or comment.
|
|
42
102
|
* Anything outside this subset is never granted the privileged EC path.
|
|
43
103
|
*/
|
|
44
|
-
export function parseLiteralShellCommand(command) {
|
|
45
|
-
const input = trimShellSpacing(
|
|
104
|
+
export function parseLiteralShellCommand(command, options = {}) {
|
|
105
|
+
const input = trimShellSpacing(options.allowManagedTmpDir
|
|
106
|
+
? substituteManagedTmpDirToken(command)
|
|
107
|
+
: command);
|
|
46
108
|
if (!input)
|
|
47
109
|
return { ok: false, issue: 'empty-command' };
|
|
48
110
|
if (input.includes('\0'))
|
|
@@ -135,11 +197,16 @@ export function parseLiteralShellCommand(command) {
|
|
|
135
197
|
return { ok: false, issue: 'invalid-quote' };
|
|
136
198
|
finishToken();
|
|
137
199
|
return argv.length > 0
|
|
138
|
-
? {
|
|
200
|
+
? {
|
|
201
|
+
ok: true,
|
|
202
|
+
argv: options.allowManagedTmpDir
|
|
203
|
+
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
204
|
+
: argv,
|
|
205
|
+
}
|
|
139
206
|
: { ok: false, issue: 'empty-command' };
|
|
140
207
|
}
|
|
141
|
-
export function parseLiteralShellArgv(command) {
|
|
142
|
-
const result = parseLiteralShellCommand(command);
|
|
208
|
+
export function parseLiteralShellArgv(command, options = {}) {
|
|
209
|
+
const result = parseLiteralShellCommand(command, options);
|
|
143
210
|
return result.ok ? result.argv : null;
|
|
144
211
|
}
|
|
145
212
|
/**
|
|
@@ -157,12 +224,12 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
157
224
|
* Codex app-server. This does not make shell composition safe: the nested
|
|
158
225
|
* command still has to pass `parseLiteralShellCommand`.
|
|
159
226
|
*/
|
|
160
|
-
export function parseCodexShellCommandArgv(command) {
|
|
161
|
-
const outer = parseLiteralShellArgv(command);
|
|
227
|
+
export function parseCodexShellCommandArgv(command, options = {}) {
|
|
228
|
+
const outer = parseLiteralShellArgv(command, options);
|
|
162
229
|
if (!outer)
|
|
163
230
|
return null;
|
|
164
231
|
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
165
|
-
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
|
|
232
|
+
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
|
|
166
233
|
}
|
|
167
234
|
/**
|
|
168
235
|
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
@@ -173,10 +240,12 @@ export function parseCodexToolCommandArgv(input) {
|
|
|
173
240
|
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
174
241
|
const outer = input.commandArgv;
|
|
175
242
|
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
176
|
-
return wrapped === undefined
|
|
243
|
+
return wrapped === undefined
|
|
244
|
+
? outer
|
|
245
|
+
: parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
|
|
177
246
|
}
|
|
178
247
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
179
|
-
return parseCodexShellCommandArgv(command);
|
|
248
|
+
return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
|
|
180
249
|
}
|
|
181
250
|
function scanBoundedOutputOperators(command) {
|
|
182
251
|
const operators = [];
|
|
@@ -290,7 +359,7 @@ function parseStdinOnlyHeadArgs(segment) {
|
|
|
290
359
|
* optional stderr merge, one bounded `head`, and one create-only output path.
|
|
291
360
|
*/
|
|
292
361
|
export function parseBoundedOutputShellCommand(command) {
|
|
293
|
-
const input = trimShellSpacing(command);
|
|
362
|
+
const input = trimShellSpacing(substituteManagedTmpDirToken(command));
|
|
294
363
|
if (!input)
|
|
295
364
|
return null;
|
|
296
365
|
const operators = scanBoundedOutputOperators(input);
|
|
@@ -311,6 +380,10 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
311
380
|
const argv = parseLiteralShellArgv(sourceText);
|
|
312
381
|
if (!argv)
|
|
313
382
|
return null;
|
|
383
|
+
// `$TMPDIR` is a transport-level path reference, never an argument
|
|
384
|
+
// expansion for the privileged command itself.
|
|
385
|
+
if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
|
|
386
|
+
return null;
|
|
314
387
|
let mergeStderr = false;
|
|
315
388
|
let pipe;
|
|
316
389
|
let redirect;
|
|
@@ -345,7 +418,12 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
345
418
|
const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(redirect.end)));
|
|
346
419
|
if (!outputArgv || outputArgv.length !== 1)
|
|
347
420
|
return null;
|
|
348
|
-
|
|
421
|
+
// A single-quoted or escaped `$TMPDIR` is literal shell text, not the
|
|
422
|
+
// managed directory reference. Do not reinterpret it as a route to the
|
|
423
|
+
// session temp root.
|
|
424
|
+
if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
|
|
425
|
+
return null;
|
|
426
|
+
outputPath = restoreManagedTmpDirToken(outputArgv[0]);
|
|
349
427
|
}
|
|
350
428
|
return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
|
|
351
429
|
}
|
|
@@ -355,13 +433,14 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
355
433
|
* invocation may leave the sandbox, while shell composition may not.
|
|
356
434
|
*/
|
|
357
435
|
export function classifyEvolcoreShellCommand(command) {
|
|
358
|
-
const
|
|
436
|
+
const parseOptions = { allowManagedTmpDir: true };
|
|
437
|
+
const literal = parseLiteralShellCommand(command, parseOptions);
|
|
359
438
|
if (literal.ok) {
|
|
360
439
|
if (literal.argv[0] === 'ec')
|
|
361
440
|
return { kind: 'literal' };
|
|
362
441
|
const wrapped = unwrapCodexShellCommandArgv(literal.argv);
|
|
363
442
|
if (wrapped !== undefined) {
|
|
364
|
-
const nested = parseLiteralShellCommand(wrapped);
|
|
443
|
+
const nested = parseLiteralShellCommand(wrapped, parseOptions);
|
|
365
444
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
366
445
|
return { kind: 'literal' };
|
|
367
446
|
if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
|
|
@@ -2,6 +2,6 @@ export * from './tool-policy.js';
|
|
|
2
2
|
export * from './ec-command-parser.js';
|
|
3
3
|
export * from './readonly-shell-query.js';
|
|
4
4
|
export * from './approval-gateway.js';
|
|
5
|
-
export * from './
|
|
5
|
+
export * from './mode.js';
|
|
6
6
|
// Tool summaries live in utils; keep the historical permission import path compatible.
|
|
7
7
|
export { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
@@ -22,3 +22,18 @@ export function normalizePermissionMode(value) {
|
|
|
22
22
|
return { mode: 'readonly', workflow: 'plan', migratedFrom: 'plan' };
|
|
23
23
|
return { mode: 'readonly' };
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Phase one only disables the task execution sandbox for an authenticated
|
|
27
|
+
* owner running in bypass mode. Every other combination remains fail-closed.
|
|
28
|
+
*/
|
|
29
|
+
export function resolvePhaseOneExecutionSandbox(permissionMode, role) {
|
|
30
|
+
const normalizedMode = normalizePermissionMode(permissionMode).mode;
|
|
31
|
+
const normalizedRole = role ?? undefined;
|
|
32
|
+
const sandboxOff = normalizedMode === 'bypass' && normalizedRole === 'owner';
|
|
33
|
+
return {
|
|
34
|
+
permissionMode: normalizedMode,
|
|
35
|
+
role: normalizedRole,
|
|
36
|
+
state: sandboxOff ? 'off' : 'active',
|
|
37
|
+
reason: sandboxOff ? 'owner-bypass' : 'phase-one-required',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -3,7 +3,7 @@ import { parseLiteralShellArgv } from './ec-command-parser.js';
|
|
|
3
3
|
const MAX_QUERY_LISTS = 8;
|
|
4
4
|
const MAX_PIPELINE_STAGES = 3;
|
|
5
5
|
const SUPPORTED_EXECUTABLES = new Set([
|
|
6
|
-
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort',
|
|
6
|
+
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat',
|
|
7
7
|
]);
|
|
8
8
|
const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
|
|
9
9
|
const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
|
|
@@ -71,8 +71,17 @@ function splitQueryStructure(command) {
|
|
|
71
71
|
segment += char;
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
|
-
if (char === '\0' || char === '\r' || char === '\n' || char === '
|
|
74
|
+
if (char === '\0' || char === '\r' || char === '\n' || char === '<')
|
|
75
75
|
return null;
|
|
76
|
+
if (char === '&') {
|
|
77
|
+
// A bounded sequence of independently proven read-only commands is
|
|
78
|
+
// safe to run in order. A single ampersand would background execution;
|
|
79
|
+
// `||` remains rejected because it introduces conditional control flow.
|
|
80
|
+
if (command[index + 1] !== '&' || !finishPipeline())
|
|
81
|
+
return null;
|
|
82
|
+
index++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
76
85
|
if (char === '|') {
|
|
77
86
|
if (command[index + 1] === '|' || !finishStage())
|
|
78
87
|
return null;
|
|
@@ -89,12 +98,14 @@ function splitQueryStructure(command) {
|
|
|
89
98
|
return null;
|
|
90
99
|
return pipelines;
|
|
91
100
|
}
|
|
92
|
-
function parseStageArgv(segment) {
|
|
101
|
+
function parseStageArgv(segment, options) {
|
|
93
102
|
const redirectMatch = segment.match(STDERR_DEV_NULL_RE);
|
|
94
103
|
const withoutRedirect = redirectMatch?.[1] ?? segment;
|
|
95
104
|
if (withoutRedirect.includes('>'))
|
|
96
105
|
return null;
|
|
97
|
-
return parseLiteralShellArgv(withoutRedirect.trim()
|
|
106
|
+
return parseLiteralShellArgv(withoutRedirect.trim(), options.managedTempDir
|
|
107
|
+
? { allowManagedTmpDir: true, managedTempDir: options.managedTempDir }
|
|
108
|
+
: undefined);
|
|
98
109
|
}
|
|
99
110
|
function exact(value) {
|
|
100
111
|
return { value, access: 'exact' };
|
|
@@ -458,12 +469,118 @@ function analyzeSort(args) {
|
|
|
458
469
|
}
|
|
459
470
|
return paths;
|
|
460
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Git metadata queries do not expose file contents and their output paths are
|
|
474
|
+
* controlled by the repository, not by arbitrary shell operands. Keep this
|
|
475
|
+
* deliberately small: status and read-only diff summaries are the patterns
|
|
476
|
+
* used for workspace diagnostics. Mutating subcommands and pathspecs remain
|
|
477
|
+
* unproven so the normal H-class checks still apply.
|
|
478
|
+
*/
|
|
479
|
+
function analyzeGit(args) {
|
|
480
|
+
if (args.length === 0)
|
|
481
|
+
return null;
|
|
482
|
+
const subcommand = args[0];
|
|
483
|
+
if (subcommand === 'status') {
|
|
484
|
+
// Keep the allow-list explicit; callers should request a bounded metadata
|
|
485
|
+
// view such as `--short` or `--branch`, rather than inheriting Git's full
|
|
486
|
+
// default output and any future options.
|
|
487
|
+
if (args.length === 1)
|
|
488
|
+
return null;
|
|
489
|
+
const allowed = new Set([
|
|
490
|
+
'--short', '--porcelain', '--porcelain=v1', '--porcelain=v2', '--branch',
|
|
491
|
+
'--untracked-files=no', '--untracked-files=normal', '--ignored=no',
|
|
492
|
+
'--ignore-submodules', '--renames', '--no-renames', '--ahead-behind',
|
|
493
|
+
'--no-ahead-behind', '--show-stash', '--no-optional-locks',
|
|
494
|
+
]);
|
|
495
|
+
for (const arg of args.slice(1)) {
|
|
496
|
+
if (arg === '--')
|
|
497
|
+
return null;
|
|
498
|
+
if (!allowed.has(arg))
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
if (subcommand === 'diff') {
|
|
504
|
+
let sawSummary = false;
|
|
505
|
+
let optionsEnded = false;
|
|
506
|
+
const paths = [];
|
|
507
|
+
const allowedFlags = new Set([
|
|
508
|
+
'--stat', '--shortstat', '--numstat', '--name-only', '--name-status',
|
|
509
|
+
'--no-ext-diff', '--no-renames', '--no-textconv', '--no-index',
|
|
510
|
+
'--cached', '--staged', '--merge-base', '--no-color', '--color=never',
|
|
511
|
+
]);
|
|
512
|
+
for (const arg of args.slice(1)) {
|
|
513
|
+
if (!optionsEnded && arg === '--') {
|
|
514
|
+
optionsEnded = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!optionsEnded && arg.startsWith('-')) {
|
|
518
|
+
if (arg === '--stat' || arg === '--shortstat' || arg === '--numstat'
|
|
519
|
+
|| arg === '--name-only' || arg === '--name-status')
|
|
520
|
+
sawSummary = true;
|
|
521
|
+
if (!allowedFlags.has(arg) && !/^[0-9a-f]{7,64}$/.test(arg))
|
|
522
|
+
return null;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
// Revisions are safe literals; pathspecs after -- are bounded exact reads.
|
|
526
|
+
if (optionsEnded)
|
|
527
|
+
paths.push({ value: arg, access: 'exact' });
|
|
528
|
+
else if (!/^[0-9A-Za-z._~^/:-]+$/.test(arg))
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
return sawSummary ? paths : null;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
function analyzeWc(args) {
|
|
536
|
+
const allowed = new Set(['-l', '-w', '-c', '-m', '-L', '--lines', '--words', '--bytes', '--chars', '--max-line-length']);
|
|
537
|
+
const paths = [];
|
|
538
|
+
for (const arg of args) {
|
|
539
|
+
if (arg.startsWith('-')) {
|
|
540
|
+
if (!allowed.has(arg))
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
else if (arg !== '-') {
|
|
544
|
+
paths.push(exact(arg));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return paths;
|
|
548
|
+
}
|
|
549
|
+
function analyzeStat(args) {
|
|
550
|
+
// GNU stat defaults to lstat semantics. Following a final symlink would
|
|
551
|
+
// let a metadata-only query reach a protected target through an alias.
|
|
552
|
+
if (args.some(argument => argument === '-L' || argument === '--dereference'))
|
|
553
|
+
return null;
|
|
554
|
+
const valueOptions = new Set(['-c', '--format', '--printf']);
|
|
555
|
+
const paths = [];
|
|
556
|
+
let optionsEnded = false;
|
|
557
|
+
for (let index = 0; index < args.length; index++) {
|
|
558
|
+
const argument = args[index];
|
|
559
|
+
if (!optionsEnded && argument === '--') {
|
|
560
|
+
optionsEnded = true;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
|
|
564
|
+
const option = argument.split('=', 1)[0];
|
|
565
|
+
if (valueOptions.has(option) && !argument.includes('=') && ++index >= args.length)
|
|
566
|
+
return null;
|
|
567
|
+
if (!valueOptions.has(option) && !new Set(['-f', '--file-system', '-t', '--terse', '--cached']).has(option)) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (argument !== '-')
|
|
573
|
+
paths.push(exact(argument));
|
|
574
|
+
}
|
|
575
|
+
return paths.length > 0 ? paths : null;
|
|
576
|
+
}
|
|
461
577
|
function analyzeCommand(argv) {
|
|
462
578
|
const executable = normalizeExecutable(argv[0]);
|
|
463
579
|
if (!executable)
|
|
464
580
|
return null;
|
|
465
581
|
const args = argv.slice(1);
|
|
466
582
|
let pathOperands = null;
|
|
583
|
+
let accessKind = 'content';
|
|
467
584
|
switch (executable) {
|
|
468
585
|
case 'echo':
|
|
469
586
|
pathOperands = [];
|
|
@@ -495,12 +612,35 @@ function analyzeCommand(argv) {
|
|
|
495
612
|
case 'sort':
|
|
496
613
|
pathOperands = analyzeSort(args);
|
|
497
614
|
break;
|
|
615
|
+
case 'git':
|
|
616
|
+
pathOperands = analyzeGit(args);
|
|
617
|
+
break;
|
|
618
|
+
case 'wc':
|
|
619
|
+
pathOperands = analyzeWc(args);
|
|
620
|
+
break;
|
|
621
|
+
case 'stat':
|
|
622
|
+
case 'lstat':
|
|
623
|
+
pathOperands = analyzeStat(args);
|
|
624
|
+
accessKind = 'metadata';
|
|
625
|
+
break;
|
|
498
626
|
}
|
|
499
627
|
if (!pathOperands)
|
|
500
628
|
return null;
|
|
501
|
-
|
|
629
|
+
if (['ls', 'find'].includes(executable))
|
|
630
|
+
accessKind = 'enumerate';
|
|
631
|
+
return {
|
|
632
|
+
command: {
|
|
633
|
+
executable,
|
|
634
|
+
argv,
|
|
635
|
+
pathOperands,
|
|
636
|
+
// Keep the established IR shape for ordinary queries. Only metadata
|
|
637
|
+
// commands need an explicit action marker; content/enumeration are
|
|
638
|
+
// derived from their operands by policy consumers.
|
|
639
|
+
...(accessKind === 'metadata' ? { accessKind } : {}),
|
|
640
|
+
},
|
|
641
|
+
};
|
|
502
642
|
}
|
|
503
|
-
export function analyzeReadonlyShellQuery(command) {
|
|
643
|
+
export function analyzeReadonlyShellQuery(command, options = {}) {
|
|
504
644
|
const structure = splitQueryStructure(command);
|
|
505
645
|
if (!structure)
|
|
506
646
|
return { kind: 'unproven', reason: 'unsupported-shell-structure' };
|
|
@@ -509,14 +649,14 @@ export function analyzeReadonlyShellQuery(command) {
|
|
|
509
649
|
for (const stages of structure) {
|
|
510
650
|
const pipeline = [];
|
|
511
651
|
for (let index = 0; index < stages.length; index++) {
|
|
512
|
-
const argv = parseStageArgv(stages[index]);
|
|
652
|
+
const argv = parseStageArgv(stages[index], options);
|
|
513
653
|
if (!argv)
|
|
514
654
|
return { kind: 'unproven', reason: 'non-literal-command' };
|
|
515
655
|
const analyzed = analyzeCommand(argv);
|
|
516
656
|
if (!analyzed)
|
|
517
657
|
return { kind: 'unproven', reason: 'unsupported-command-or-option' };
|
|
518
658
|
if (index > 0) {
|
|
519
|
-
if (!['head', 'sort'].includes(analyzed.command.executable)) {
|
|
659
|
+
if (!['head', 'sort', 'grep', 'wc'].includes(analyzed.command.executable)) {
|
|
520
660
|
return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
|
|
521
661
|
}
|
|
522
662
|
if (analyzed.command.pathOperands.length > 0) {
|
|
@@ -4,6 +4,27 @@ 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
|
+
export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
|
|
8
|
+
/** Recognize sandbox bootstrap failures independently of localized stderr. */
|
|
9
|
+
export function isSandboxInitializationFailure(error, stderr = []) {
|
|
10
|
+
const text = [error instanceof Error ? error.message : String(error ?? ''), ...stderr]
|
|
11
|
+
.join('\n')
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
return text.includes(SANDBOX_INITIALIZATION_FAILED)
|
|
14
|
+
|| /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
|
|
15
|
+
|| /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
|
|
16
|
+
|| /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
|
|
17
|
+
|| /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text);
|
|
18
|
+
}
|
|
19
|
+
export function createSandboxInitializationError(error, stderr = []) {
|
|
20
|
+
const detail = error instanceof Error ? error.message : String(error ?? 'sandbox bootstrap failed');
|
|
21
|
+
const wrapped = new Error(`${SANDBOX_INITIALIZATION_FAILED}: ${detail}`);
|
|
22
|
+
wrapped.name = 'SandboxInitializationError';
|
|
23
|
+
wrapped.code = SANDBOX_INITIALIZATION_FAILED;
|
|
24
|
+
if (stderr.length > 0)
|
|
25
|
+
wrapped.stderr = [...stderr].slice(-20);
|
|
26
|
+
return wrapped;
|
|
27
|
+
}
|
|
7
28
|
/**
|
|
8
29
|
* Claude Agent SDK's native sandbox is unavailable on Windows. WSL2 reports
|
|
9
30
|
* itself as Linux, so it remains fail-closed there together with macOS/Linux.
|