evolcore 0.0.11 → 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 +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- 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 +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- 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 +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- 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 +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- 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 -2
- 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 +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- 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 +243 -18
- 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 +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- 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/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -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/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- 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/command/evol-menu-version-gate.js +0 -39
- 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
|
@@ -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.
|
|
@@ -210,7 +231,7 @@ function appendLClassReadOnlyBinds(args, root) {
|
|
|
210
231
|
args.push('--ro-bind', target.path, target.path);
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
|
-
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot()) {
|
|
234
|
+
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
|
|
214
235
|
const bubblewrapPath = resolveBubblewrapPath();
|
|
215
236
|
if (!bubblewrapPath)
|
|
216
237
|
return undefined;
|
|
@@ -224,6 +245,27 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
224
245
|
];
|
|
225
246
|
appendLClassReadOnlyBinds(args, path.resolve(root));
|
|
226
247
|
appendHClassMasks(args, path.resolve(root), true);
|
|
248
|
+
if (options.managedCodexRequirements) {
|
|
249
|
+
const requirements = path.resolve(options.managedCodexRequirements);
|
|
250
|
+
if (!fs.statSync(requirements).isFile()) {
|
|
251
|
+
throw new Error(`Codex managed requirements is not a file: ${requirements}`);
|
|
252
|
+
}
|
|
253
|
+
const managedDirectory = path.dirname(requirements);
|
|
254
|
+
// Keep the copied hook immutable even when a session uses bypass mode.
|
|
255
|
+
args.push('--ro-bind', managedDirectory, managedDirectory);
|
|
256
|
+
// A host may not have /etc/codex yet. Overlay /etc inside this namespace so
|
|
257
|
+
// Bubblewrap can create the mountpoint without modifying the host, while
|
|
258
|
+
// retaining the complete host /etc as the read-only lower layer.
|
|
259
|
+
args.push('--overlay-src', '/etc');
|
|
260
|
+
args.push('--tmp-overlay', '/etc');
|
|
261
|
+
args.push('--dir', '/etc/codex');
|
|
262
|
+
// Replace the complete Codex system-config directory inside this app-server
|
|
263
|
+
// namespace so host system hooks cannot join EvolCore's managed hook set.
|
|
264
|
+
args.push('--tmpfs', '/etc/codex');
|
|
265
|
+
args.push('--ro-bind', requirements, '/etc/codex/requirements.toml');
|
|
266
|
+
args.push('--remount-ro', '/etc/codex');
|
|
267
|
+
args.push('--remount-ro', '/etc');
|
|
268
|
+
}
|
|
227
269
|
args.push('--', executable, ...executableArgs);
|
|
228
270
|
return { command: bubblewrapPath, args };
|
|
229
271
|
}
|
|
@@ -3,9 +3,94 @@ import path from 'path';
|
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
|
-
import { containsHClassReference, containsLClassReference, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
7
|
-
import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
|
|
6
|
+
import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
|
|
7
|
+
import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from './ec-command-parser.js';
|
|
8
8
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
9
|
+
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
10
|
+
export function resolveManagedTempDir(env = process.env) {
|
|
11
|
+
const raw = typeof env.TMPDIR === 'string' ? env.TMPDIR.trim() : '';
|
|
12
|
+
if (!raw || !path.isAbsolute(raw))
|
|
13
|
+
return undefined;
|
|
14
|
+
const resolved = resolveProtectedCandidate(raw);
|
|
15
|
+
// A process-wide shared temporary root is not a session capability. The
|
|
16
|
+
// runner must register a private subdirectory before `$TMPDIR` is trusted.
|
|
17
|
+
if (['/tmp', '/var/tmp', '/dev/shm'].includes(resolved))
|
|
18
|
+
return undefined;
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
function substituteTmpDir(value, root) {
|
|
22
|
+
if (!root)
|
|
23
|
+
return value;
|
|
24
|
+
return value
|
|
25
|
+
.replace(/\$\{TMPDIR\}/g, root)
|
|
26
|
+
.replace(/\$TMPDIR\b/g, root);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A temp path is valid only when its canonical existing ancestor remains below
|
|
30
|
+
* the current session's TMPDIR. This also closes symlink and `..` escapes.
|
|
31
|
+
*/
|
|
32
|
+
export function isManagedTempPath(value, options = {}) {
|
|
33
|
+
const root = resolveManagedTempDir(options.env);
|
|
34
|
+
if (!root || typeof value !== 'string' || !value.trim())
|
|
35
|
+
return false;
|
|
36
|
+
const candidate = resolveProtectedCandidate(substituteTmpDir(value, root), options.cwd);
|
|
37
|
+
return isSameOrDescendant(candidate, root);
|
|
38
|
+
}
|
|
39
|
+
export function expandManagedTempPath(value, options = {}) {
|
|
40
|
+
return substituteTmpDir(value, resolveManagedTempDir(options.env));
|
|
41
|
+
}
|
|
42
|
+
const WRITE_INTENT_RE = /(?:^|[\s;&|])(?:mkdir|mktemp|touch|tee|cp|mv|install|chmod|chown|ln|rm)(?:[\s;&|]|$)|(?:^|[\s])(?:\d>>?|>>?)\s*/i;
|
|
43
|
+
const TEMP_TOKEN_RE = /(?:\$TMPDIR|\$\{TMPDIR\}|\/tmp(?:\/|$)|\/var\/tmp(?:\/|$)|\/dev\/shm(?:\/|$))[^\s'";&|<>)]*/g;
|
|
44
|
+
/**
|
|
45
|
+
* Inspect shell write targets that look like temporary paths. Ordinary
|
|
46
|
+
* project writes are left to the existing workspace/H-class policy; any
|
|
47
|
+
* explicit /tmp-style target must be contained by TMPDIR.
|
|
48
|
+
*/
|
|
49
|
+
export function checkManagedTempShellCommand(command, options = {}) {
|
|
50
|
+
if (!WRITE_INTENT_RE.test(command))
|
|
51
|
+
return { kind: 'none' };
|
|
52
|
+
const root = resolveManagedTempDir(options.env);
|
|
53
|
+
const normalized = substituteTmpDir(command, root);
|
|
54
|
+
const candidates = [...normalized.matchAll(TEMP_TOKEN_RE)].map(match => match[0].replace(/[.,:]+$/, ''));
|
|
55
|
+
if (candidates.length === 0)
|
|
56
|
+
return { kind: 'none' };
|
|
57
|
+
if (!root) {
|
|
58
|
+
return { kind: 'deny', paths: candidates, reason: 'TMPDIR is unset or not an absolute managed path' };
|
|
59
|
+
}
|
|
60
|
+
const outside = candidates.filter(candidate => !isManagedTempPath(candidate, {
|
|
61
|
+
...options,
|
|
62
|
+
env: { ...(options.env ?? process.env), TMPDIR: root },
|
|
63
|
+
}));
|
|
64
|
+
if (outside.length > 0) {
|
|
65
|
+
return { kind: 'deny', paths: outside, reason: 'temporary write target is outside the session-managed TMPDIR' };
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'allow', paths: candidates };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replace only already-validated temporary path operands before a second
|
|
71
|
+
* policy pass. This prevents a harmless `$TMPDIR/CA/root` name from looking
|
|
72
|
+
* like the real EvolCore CA directory while preserving any unrelated H-class
|
|
73
|
+
* operand in the same command.
|
|
74
|
+
*/
|
|
75
|
+
export function maskManagedTempShellPaths(command, options = {}) {
|
|
76
|
+
const check = checkManagedTempShellCommand(command, options);
|
|
77
|
+
if (check.kind !== 'allow')
|
|
78
|
+
return command;
|
|
79
|
+
const root = resolveManagedTempDir(options.env);
|
|
80
|
+
const normalized = substituteTmpDir(command, root);
|
|
81
|
+
let index = 0;
|
|
82
|
+
return normalized.replace(TEMP_TOKEN_RE, token => {
|
|
83
|
+
const candidate = token.replace(/[.,:]+$/, '');
|
|
84
|
+
if (!check.paths.includes(candidate) || !isManagedTempPath(candidate, {
|
|
85
|
+
...options,
|
|
86
|
+
env: { ...(options.env ?? process.env), TMPDIR: root },
|
|
87
|
+
})) {
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
index += 1;
|
|
91
|
+
return `__managed_tmp_${index}__`;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
9
94
|
// 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
|
|
10
95
|
// 这些是系统级破坏操作,任何情况下都不应该允许
|
|
11
96
|
const ABSOLUTE_FORBIDDEN = [
|
|
@@ -320,9 +405,50 @@ function containsNetStopCommand(command, depth = 0) {
|
|
|
320
405
|
});
|
|
321
406
|
}
|
|
322
407
|
function queryPathOperands(analysis) {
|
|
323
|
-
|
|
408
|
+
if (analysis.kind !== 'proven-readonly')
|
|
409
|
+
return [];
|
|
410
|
+
return analysis.ir.pipelines.flatMap(pipeline => pipeline.flatMap(command => command.pathOperands.map(operand => ({
|
|
411
|
+
operand,
|
|
412
|
+
accessKind: command.accessKind ?? (operand.access === 'recursive' ? 'enumerate' : 'content'),
|
|
413
|
+
}))));
|
|
324
414
|
}
|
|
325
|
-
function shellOperandMatchesClass(operand, className, options) {
|
|
415
|
+
function shellOperandMatchesClass(operand, className, options, accessKind = operand.access === 'recursive' ? 'enumerate' : 'content') {
|
|
416
|
+
// `$TMPDIR` is a private session namespace, so names such as `CA` or
|
|
417
|
+
// `config.json` inside it are ordinary temporary data rather than paths in
|
|
418
|
+
// the EvolCore root. Resolve the candidate first so a symlink escape does
|
|
419
|
+
// not inherit this exception.
|
|
420
|
+
if (options.managedTempDir) {
|
|
421
|
+
const managedTemp = resolveProtectedCandidate(options.managedTempDir);
|
|
422
|
+
const candidate = accessKind === 'metadata' && operand.access === 'exact'
|
|
423
|
+
? resolveProtectedCandidateWithoutFinalSymlink(operand.value, options.cwd)
|
|
424
|
+
: resolveProtectedCandidate(operand.value, options.cwd);
|
|
425
|
+
const workspace = options.cwd ? resolveProtectedCandidate(options.cwd) : undefined;
|
|
426
|
+
if (isSameOrDescendant(candidate, managedTemp)
|
|
427
|
+
&& (!workspace || !isSameOrDescendant(candidate, workspace))) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (className === 'h' && accessKind === 'metadata' && operand.access === 'exact') {
|
|
432
|
+
// Filesystem sandboxes currently expose only path-wide H-class deny
|
|
433
|
+
// rules. A runner that cannot provide an action-aware metadata channel
|
|
434
|
+
// must keep the policy decision aligned with that lower-level boundary,
|
|
435
|
+
// but ordinary workspace metadata remains safe and should stay usable.
|
|
436
|
+
const metadata = checkProtectedPathAccess(operand.value, 'metadata', {
|
|
437
|
+
...options,
|
|
438
|
+
// stat/lstat must inspect the final symlink itself, never its target.
|
|
439
|
+
followFinalSymlink: false,
|
|
440
|
+
});
|
|
441
|
+
if (options.allowProtectedMetadata === false) {
|
|
442
|
+
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory';
|
|
443
|
+
}
|
|
444
|
+
// The no-follow decision is authoritative for metadata. In particular,
|
|
445
|
+
// an ordinary symlink whose target is H-class must remain inspectable as a
|
|
446
|
+
// link; falling through to isHClassPath() would follow it and reject the
|
|
447
|
+
// otherwise safe lstat/stat operation.
|
|
448
|
+
if (metadata.pathClass !== 'unknown' && metadata.allowed) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
326
452
|
const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
|
|
327
453
|
if (reference)
|
|
328
454
|
return true;
|
|
@@ -352,7 +478,7 @@ function hClassReadScopeIncludesProtectedPath(value, options) {
|
|
|
352
478
|
return getExistingHClassMaskTargets(root).some(target => isSameOrDescendant(target.path, scope));
|
|
353
479
|
}
|
|
354
480
|
function analyzeShellProtectedOperands(command, options) {
|
|
355
|
-
const analysis = analyzeReadonlyShellQuery(command);
|
|
481
|
+
const analysis = analyzeReadonlyShellQuery(command, { managedTempDir: options.managedTempDir });
|
|
356
482
|
if (analysis.kind === 'unproven') {
|
|
357
483
|
return {
|
|
358
484
|
analysis,
|
|
@@ -363,8 +489,8 @@ function analyzeShellProtectedOperands(command, options) {
|
|
|
363
489
|
const operands = queryPathOperands(analysis);
|
|
364
490
|
return {
|
|
365
491
|
analysis,
|
|
366
|
-
hClass: operands.some(operand => shellOperandMatchesClass(operand, 'h', options)),
|
|
367
|
-
lClass: operands.some(operand => shellOperandMatchesClass(operand, 'l', options)),
|
|
492
|
+
hClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)),
|
|
493
|
+
lClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)),
|
|
368
494
|
};
|
|
369
495
|
}
|
|
370
496
|
function protectedReadToolPaths(toolName, input) {
|
|
@@ -444,11 +570,21 @@ function protectedReadToolStaysInWorkspace(toolName, input, projectPath) {
|
|
|
444
570
|
}
|
|
445
571
|
return true;
|
|
446
572
|
}
|
|
447
|
-
function readonlyShellStaysInWorkspace(analysis, projectPath) {
|
|
573
|
+
function readonlyShellStaysInWorkspace(analysis, projectPath, managedTempDir) {
|
|
448
574
|
if (analysis.kind !== 'proven-readonly')
|
|
449
575
|
return false;
|
|
450
576
|
const workspace = resolveProtectedCandidate(projectPath);
|
|
451
|
-
|
|
577
|
+
const managedTemp = managedTempDir ? resolveProtectedCandidate(managedTempDir) : undefined;
|
|
578
|
+
return analysis.ir.pipelines.flatMap(pipeline => pipeline.flatMap(command => command.pathOperands.map(operand => ({
|
|
579
|
+
operand,
|
|
580
|
+
accessKind: command.accessKind ?? (operand.access === 'recursive' ? 'enumerate' : 'content'),
|
|
581
|
+
})))).every(({ operand, accessKind }) => (() => {
|
|
582
|
+
const candidate = accessKind === 'metadata' && operand.access === 'exact'
|
|
583
|
+
? resolveProtectedCandidateWithoutFinalSymlink(operand.value, projectPath)
|
|
584
|
+
: resolveProtectedCandidate(operand.value, projectPath);
|
|
585
|
+
return isSameOrDescendant(candidate, workspace)
|
|
586
|
+
|| (!!managedTemp && isSameOrDescendant(candidate, managedTemp));
|
|
587
|
+
})());
|
|
452
588
|
}
|
|
453
589
|
/**
|
|
454
590
|
* 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)。普通
|
|
@@ -463,50 +599,61 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
463
599
|
const paths = protectedReadToolPaths(toolName, input);
|
|
464
600
|
const pathOptions = { cwd: projectPath, root: context?.root };
|
|
465
601
|
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
466
|
-
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录' };
|
|
602
|
+
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
467
603
|
}
|
|
468
604
|
if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions))) {
|
|
469
|
-
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
605
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
|
|
470
606
|
}
|
|
471
607
|
if (protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
|
|
472
|
-
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
608
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
|
|
473
609
|
}
|
|
474
610
|
if (protectedReadToolMatchesLClass(toolName, input, pathOptions)) {
|
|
475
|
-
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
611
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
|
|
476
612
|
}
|
|
477
613
|
return { behavior: 'allow' };
|
|
478
614
|
}
|
|
479
615
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
|
|
480
616
|
const filePath = (input.file_path || input.notebook_path);
|
|
481
617
|
logger.warn(`[ReadonlyCheck] 🔒 File write blocked: tool=${toolName} path=${filePath} project=${projectPath} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
482
|
-
return { behavior: 'deny', message: '🔒 只读模式:禁止写入或修改文件' };
|
|
618
|
+
return { behavior: 'deny', message: '🔒 只读模式:禁止写入或修改文件', policyCode: 'readonly_file_write' };
|
|
483
619
|
}
|
|
484
620
|
if (toolName === 'Bash') {
|
|
485
621
|
const cmd = input.command || '';
|
|
486
|
-
|
|
622
|
+
// A managed session must provide its runner-registered temp root. For
|
|
623
|
+
// direct policy callers without a session, the process TMPDIR remains a
|
|
624
|
+
// useful compatibility fallback after the shared-root check above.
|
|
625
|
+
const managedTempDir = context?.sessionId
|
|
626
|
+
? context.managedTempDir
|
|
627
|
+
: context?.managedTempDir ?? resolveManagedTempDir();
|
|
628
|
+
const protectedOperands = analyzeShellProtectedOperands(cmd, {
|
|
629
|
+
cwd: projectPath,
|
|
630
|
+
root: context?.root,
|
|
631
|
+
managedTempDir,
|
|
632
|
+
allowProtectedMetadata: context?.allowProtectedMetadata,
|
|
633
|
+
});
|
|
487
634
|
if (protectedOperands.analysis.kind === 'proven-readonly'
|
|
488
635
|
&& !protectedOperands.hClass
|
|
489
636
|
&& !protectedOperands.lClass
|
|
490
|
-
&& readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath)) {
|
|
637
|
+
&& readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath, managedTempDir)) {
|
|
491
638
|
return { behavior: 'allow' };
|
|
492
639
|
}
|
|
493
640
|
if (protectedOperands.hClass) {
|
|
494
641
|
logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
495
|
-
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
642
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
|
|
496
643
|
}
|
|
497
644
|
if (protectedOperands.lClass) {
|
|
498
645
|
logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
499
|
-
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
646
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
|
|
500
647
|
}
|
|
501
648
|
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
502
|
-
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录' };
|
|
649
|
+
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
503
650
|
}
|
|
504
651
|
const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
|
|
505
652
|
logger.warn(`[ReadonlyCheck] 🔒 Bash blocked by default: cmd="${cmdPreview}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
506
|
-
return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许受限、可证明只读的普通路径查询' };
|
|
653
|
+
return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许受限、可证明只读的普通路径查询', policyCode: 'readonly_shell_unproven' };
|
|
507
654
|
}
|
|
508
655
|
logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
509
|
-
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName}
|
|
656
|
+
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行`, policyCode: 'readonly_tool_unavailable' };
|
|
510
657
|
}
|
|
511
658
|
const NON_FILESYSTEM_METADATA_TOOLS = new Set([
|
|
512
659
|
'Agent',
|
|
@@ -752,9 +899,35 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
752
899
|
}
|
|
753
900
|
else if (toolName === 'Bash') {
|
|
754
901
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
755
|
-
|
|
902
|
+
// Keep this H-class pass aligned with the shared preflight pass. A
|
|
903
|
+
// managed session must not fall back to the daemon's process-wide TMPDIR;
|
|
904
|
+
// without a registered root, explicit temporary paths fail closed.
|
|
905
|
+
const tempOptions = {
|
|
906
|
+
cwd: context?.projectPath,
|
|
907
|
+
env: context?.sessionId
|
|
908
|
+
? { ...process.env, TMPDIR: context.managedTempDir ?? '' }
|
|
909
|
+
: context?.managedTempDir !== undefined
|
|
910
|
+
? { ...process.env, TMPDIR: context.managedTempDir }
|
|
911
|
+
: process.env,
|
|
912
|
+
};
|
|
913
|
+
const tempCheck = checkManagedTempShellCommand(command, tempOptions);
|
|
914
|
+
if (tempCheck.kind === 'deny') {
|
|
915
|
+
return {
|
|
916
|
+
behavior: 'deny',
|
|
917
|
+
message: '🔒 临时文件必须位于当前会话的 $TMPDIR 内',
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
// A managed temp write is intentionally allowed to contain names such as
|
|
921
|
+
// CA/root, but only that validated operand is masked. Any other protected
|
|
922
|
+
// operand in a mixed command must still be rejected.
|
|
923
|
+
const policyCommand = tempCheck.kind === 'allow'
|
|
924
|
+
? maskManagedTempShellPaths(command, tempOptions)
|
|
925
|
+
: command;
|
|
926
|
+
const protectedOperands = analyzeShellProtectedOperands(policyCommand, {
|
|
756
927
|
cwd: context?.projectPath,
|
|
757
928
|
root: context?.root,
|
|
929
|
+
managedTempDir: context?.managedTempDir,
|
|
930
|
+
allowProtectedMetadata: context?.allowProtectedMetadata,
|
|
758
931
|
});
|
|
759
932
|
if (protectedOperands.hClass) {
|
|
760
933
|
logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
|
|
@@ -905,7 +1078,10 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
905
1078
|
else if (toolName === 'Bash') {
|
|
906
1079
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
907
1080
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
908
|
-
const protectedOperands = analyzeShellProtectedOperands(command,
|
|
1081
|
+
const protectedOperands = analyzeShellProtectedOperands(command, {
|
|
1082
|
+
...pathOptions,
|
|
1083
|
+
managedTempDir: context?.managedTempDir,
|
|
1084
|
+
});
|
|
909
1085
|
const lClassDenied = protectedOperands.lClass
|
|
910
1086
|
&& (context?.permissionMode === 'readonly' || protectedOperands.analysis.kind === 'unproven');
|
|
911
1087
|
if (lClassDenied) {
|
|
@@ -992,14 +1168,46 @@ export function checkBlacklist(toolName, input) {
|
|
|
992
1168
|
// 默认允许
|
|
993
1169
|
return { behavior: 'allow', updatedInput: input };
|
|
994
1170
|
}
|
|
1171
|
+
function managedTempOptions(context) {
|
|
1172
|
+
return {
|
|
1173
|
+
cwd: context.projectPath,
|
|
1174
|
+
// A managed session must never inherit the daemon's unrelated TMPDIR.
|
|
1175
|
+
env: context.sessionId
|
|
1176
|
+
? { ...process.env, TMPDIR: context.managedTempDir ?? '' }
|
|
1177
|
+
: context.managedTempDir !== undefined
|
|
1178
|
+
? { ...process.env, TMPDIR: context.managedTempDir }
|
|
1179
|
+
: process.env,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
995
1182
|
function validateBoundedOutputPath(command, context) {
|
|
996
1183
|
const outputPath = command.outputPath;
|
|
997
1184
|
if (!outputPath)
|
|
998
1185
|
return undefined;
|
|
999
1186
|
if (context.permissionMode === 'readonly')
|
|
1000
1187
|
return '🔒 只读模式:禁止将命令输出写入文件';
|
|
1001
|
-
|
|
1002
|
-
|
|
1188
|
+
const tempOptions = managedTempOptions(context);
|
|
1189
|
+
const expandedOutputPath = expandManagedTempPath(outputPath, tempOptions);
|
|
1190
|
+
if (path.isAbsolute(expandedOutputPath)) {
|
|
1191
|
+
// A literal `/tmp/...` path is not proof that the caller is using this
|
|
1192
|
+
// session's managed workspace. Only an explicit `$TMPDIR` reference may
|
|
1193
|
+
// opt into the absolute temporary-path branch; the runner owns the value
|
|
1194
|
+
// of TMPDIR and validates its containment below.
|
|
1195
|
+
const explicitManagedTemp = /^\$(?:\{TMPDIR\}|TMPDIR)(?:[\\/]|$)/.test(outputPath);
|
|
1196
|
+
if (!explicitManagedTemp || !isManagedTempPath(outputPath, tempOptions)) {
|
|
1197
|
+
return '🔒 输出文件必须使用项目目录内的相对路径,或当前会话的 $TMPDIR';
|
|
1198
|
+
}
|
|
1199
|
+
const tempTarget = resolveProtectedCandidate(expandedOutputPath);
|
|
1200
|
+
if (fs.existsSync(tempTarget))
|
|
1201
|
+
return '🔒 临时安全输出只允许创建新文件,不能覆盖或追加现有文件';
|
|
1202
|
+
try {
|
|
1203
|
+
if (!fs.statSync(path.dirname(tempTarget)).isDirectory())
|
|
1204
|
+
return '🔒 临时输出文件的父目录无效';
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
return '🔒 临时输出文件的父目录必须已经存在';
|
|
1208
|
+
}
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1003
1211
|
const workspace = resolveProtectedCandidate(context.workspacePath ?? context.projectPath);
|
|
1004
1212
|
const cwd = resolveProtectedCandidate(context.projectPath);
|
|
1005
1213
|
if (!isSameOrDescendant(cwd, workspace))
|
|
@@ -1033,7 +1241,7 @@ function quotePosixShellArg(value) {
|
|
|
1033
1241
|
function prepareBoundedOutputInput(input, command, kind, context) {
|
|
1034
1242
|
const validationError = validateBoundedOutputPath(command, context);
|
|
1035
1243
|
if (validationError)
|
|
1036
|
-
return { behavior: 'deny', input, message: validationError };
|
|
1244
|
+
return { behavior: 'deny', input, message: validationError, policyCode: 'bounded_output_invalid' };
|
|
1037
1245
|
const helperPath = context.safeOutputHelperPath;
|
|
1038
1246
|
if (!helperPath) {
|
|
1039
1247
|
return {
|
|
@@ -1047,7 +1255,7 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
1047
1255
|
throw new Error('not a file');
|
|
1048
1256
|
}
|
|
1049
1257
|
catch {
|
|
1050
|
-
return { behavior: 'deny', input, message: '🔒 安全输出 helper 不可用,命令已拒绝' };
|
|
1258
|
+
return { behavior: 'deny', input, message: '🔒 安全输出 helper 不可用,命令已拒绝', policyCode: 'bounded_output_helper_unavailable' };
|
|
1051
1259
|
}
|
|
1052
1260
|
const payload = Buffer.from(JSON.stringify({
|
|
1053
1261
|
v: 1,
|
|
@@ -1058,7 +1266,9 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
1058
1266
|
argv: command.argv,
|
|
1059
1267
|
mergeStderr: command.mergeStderr,
|
|
1060
1268
|
...(command.headArgs ? { headArgs: command.headArgs } : {}),
|
|
1061
|
-
...(command.outputPath
|
|
1269
|
+
...(command.outputPath
|
|
1270
|
+
? { outputPath: expandManagedTempPath(command.outputPath, managedTempOptions(context)) }
|
|
1271
|
+
: {}),
|
|
1062
1272
|
}), 'utf8').toString('base64url');
|
|
1063
1273
|
const rewrittenCommand = `${quotePosixShellArg(process.execPath)} ${quotePosixShellArg(helperPath)} ${quotePosixShellArg(payload)}`;
|
|
1064
1274
|
return {
|
|
@@ -1071,30 +1281,66 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
1071
1281
|
export function evaluateToolPreflight(toolName, input, context) {
|
|
1072
1282
|
if (toolName === 'Bash') {
|
|
1073
1283
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
1074
|
-
|
|
1284
|
+
const tempCheck = checkManagedTempShellCommand(command, managedTempOptions(context));
|
|
1285
|
+
if (tempCheck.kind === 'deny') {
|
|
1286
|
+
return {
|
|
1287
|
+
behavior: 'deny',
|
|
1288
|
+
input,
|
|
1289
|
+
message: `🔒 临时文件必须位于当前会话的 $TMPDIR 内:${tempCheck.reason}`,
|
|
1290
|
+
policyCode: 'temporary_path_outside_tmpdir',
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
const explicitCommandArgv = Array.isArray(input.commandArgv)
|
|
1294
|
+
&& input.commandArgv.every(value => typeof value === 'string')
|
|
1295
|
+
? input.commandArgv
|
|
1296
|
+
: undefined;
|
|
1297
|
+
const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
|
|
1298
|
+
const wrappedCommand = outerArgv ? unwrapCodexShellCommandArgv(outerArgv) : undefined;
|
|
1299
|
+
const policyCommand = wrappedCommand ?? command;
|
|
1300
|
+
if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
|
|
1075
1301
|
return {
|
|
1076
1302
|
behavior: 'deny',
|
|
1077
1303
|
input,
|
|
1078
1304
|
message: '🔒 EvolCore 托管会话不需要探测 `ec` 是否存在;请不要调用 `command -v`/`which`/`type`,也不要拼接其它命令。通信必须走当前 session 的 delegated sender。',
|
|
1305
|
+
policyCode: 'managed_ec_discovery_forbidden',
|
|
1079
1306
|
};
|
|
1080
1307
|
}
|
|
1081
|
-
const argv = context.sessionId ?
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
&& argv[2] === 'agentmd'
|
|
1087
|
-
&& argv[3] === 'put'
|
|
1308
|
+
const argv = context.sessionId ? parseCodexToolCommandArgv(input) : null;
|
|
1309
|
+
const managedAidArgv = argv?.at(-2) === '--format' && argv.at(-1) === 'json'
|
|
1310
|
+
? argv.slice(0, -2)
|
|
1311
|
+
: argv;
|
|
1312
|
+
const ownAid = !!managedAidArgv
|
|
1088
1313
|
&& !!context.selfAid
|
|
1089
|
-
&&
|
|
1090
|
-
|
|
1314
|
+
&& managedAidArgv.at(-1)?.replace(/^@/, '') === context.selfAid.replace(/^@/, '');
|
|
1315
|
+
const isSelfManagedAidRead = !!managedAidArgv
|
|
1316
|
+
&& managedAidArgv[0] === 'ec'
|
|
1317
|
+
&& managedAidArgv[1] === 'aid'
|
|
1318
|
+
&& ownAid
|
|
1319
|
+
&& ((managedAidArgv.length === 4 && ['show', 'lookup'].includes(managedAidArgv[2]))
|
|
1320
|
+
|| (managedAidArgv.length === 5
|
|
1321
|
+
&& managedAidArgv[2] === 'agentmd'
|
|
1322
|
+
&& managedAidArgv[3] === 'get'));
|
|
1323
|
+
const isSelfAgentMdPublish = !!managedAidArgv
|
|
1324
|
+
&& managedAidArgv.length === 5
|
|
1325
|
+
&& managedAidArgv[0] === 'ec'
|
|
1326
|
+
&& managedAidArgv[1] === 'aid'
|
|
1327
|
+
&& managedAidArgv[2] === 'agentmd'
|
|
1328
|
+
&& managedAidArgv[3] === 'put'
|
|
1329
|
+
&& ownAid;
|
|
1330
|
+
const canPublishSelfAgentMd = isSelfAgentMdPublish
|
|
1331
|
+
&& ['request', 'bypass'].includes(context.permissionMode ?? '');
|
|
1332
|
+
const isAllowedManagedAidCommand = isSelfManagedAidRead || canPublishSelfAgentMd;
|
|
1333
|
+
if (context.sessionId && /^\s*ec\s+aid(?:\s|$)/i.test(policyCommand) && !isAllowedManagedAidCommand) {
|
|
1091
1334
|
return {
|
|
1092
1335
|
behavior: 'deny',
|
|
1093
1336
|
input,
|
|
1094
|
-
message:
|
|
1337
|
+
message: isSelfAgentMdPublish && (context.permissionMode === 'readonly' || context.permissionMode === 'auto')
|
|
1338
|
+
? `🔒 ${context.permissionMode} 模式禁止发布 agent.md;请切换到 request 或 bypass 后再执行 \`ec aid agentmd put <当前 self AID>\`。`
|
|
1339
|
+
: '🔒 EvolCore 托管会话仅允许针对当前 self AID 精确调用 `ec aid show`、`ec aid lookup` 或 `ec aid agentmd get`;`ec aid agentmd put` 还要求 request 或 bypass。可选后缀仅限 `--format json`。禁止枚举、切换或操作其他身份。',
|
|
1340
|
+
policyCode: 'managed_ec_aid_scope_forbidden',
|
|
1095
1341
|
};
|
|
1096
1342
|
}
|
|
1097
|
-
const ecCommand = classifyEvolcoreShellCommand(
|
|
1343
|
+
const ecCommand = classifyEvolcoreShellCommand(policyCommand);
|
|
1098
1344
|
if (ecCommand.kind === 'literal') {
|
|
1099
1345
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
1100
1346
|
}
|
|
@@ -1102,25 +1348,40 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1102
1348
|
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
1103
1349
|
}
|
|
1104
1350
|
if (ecCommand.kind === 'composite') {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1351
|
+
// Older app-server approval callbacks may provide only the rendered
|
|
1352
|
+
// `/bin/bash -lc ...` command string. A composite inside that carrier
|
|
1353
|
+
// is not eligible for a delegation ticket, but owner bypass still
|
|
1354
|
+
// handles it as an ordinary shell request. Structured `commandArgv`
|
|
1355
|
+
// callbacks remain fail-closed below because their command boundary is
|
|
1356
|
+
// authoritative.
|
|
1357
|
+
const legacyRenderedCarrier = !explicitCommandArgv && wrappedCommand !== undefined;
|
|
1358
|
+
const allowLegacyBypassComposition = legacyRenderedCarrier
|
|
1359
|
+
&& context.permissionMode === 'bypass'
|
|
1360
|
+
&& ecCommand.issue === 'shell-composition';
|
|
1361
|
+
if (!allowLegacyBypassComposition) {
|
|
1362
|
+
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1363
|
+
return {
|
|
1364
|
+
behavior: 'deny',
|
|
1365
|
+
input,
|
|
1366
|
+
message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
|
|
1367
|
+
policyCode: 'ec_shell_unsafe_expansion',
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (ecCommand.issue === 'invalid-quote') {
|
|
1371
|
+
return {
|
|
1372
|
+
behavior: 'deny',
|
|
1373
|
+
input,
|
|
1374
|
+
message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
|
|
1375
|
+
policyCode: 'ec_shell_invalid_quote',
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1113
1378
|
return {
|
|
1114
1379
|
behavior: 'deny',
|
|
1115
1380
|
input,
|
|
1116
|
-
message: '🔒 EC
|
|
1381
|
+
message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
|
|
1382
|
+
policyCode: 'ec_shell_composite_command',
|
|
1117
1383
|
};
|
|
1118
1384
|
}
|
|
1119
|
-
return {
|
|
1120
|
-
behavior: 'deny',
|
|
1121
|
-
input,
|
|
1122
|
-
message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
|
|
1123
|
-
};
|
|
1124
1385
|
}
|
|
1125
1386
|
const boundedOutput = parseBoundedOutputShellCommand(command);
|
|
1126
1387
|
const hostProcessCommand = boundedOutput
|
|
@@ -1137,7 +1398,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1137
1398
|
}
|
|
1138
1399
|
const blacklist = checkBlacklist(toolName, input);
|
|
1139
1400
|
if (blacklist.behavior === 'deny') {
|
|
1140
|
-
return { behavior: 'deny', input, message: blacklist.message };
|
|
1401
|
+
return { behavior: 'deny', input, message: blacklist.message, policyCode: 'tool_blacklist' };
|
|
1141
1402
|
}
|
|
1142
1403
|
const checkedInput = blacklist.updatedInput;
|
|
1143
1404
|
const hClass = checkHClassWrite(toolName, checkedInput, {
|
|
@@ -1148,9 +1409,11 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1148
1409
|
projectPath: context.projectPath,
|
|
1149
1410
|
workspacePath: context.workspacePath,
|
|
1150
1411
|
root: context.root,
|
|
1412
|
+
managedTempDir: context.managedTempDir,
|
|
1413
|
+
allowProtectedMetadata: context.allowProtectedMetadata,
|
|
1151
1414
|
});
|
|
1152
1415
|
if (hClass.behavior === 'deny') {
|
|
1153
|
-
return { behavior: 'deny', input: checkedInput, message: hClass.message };
|
|
1416
|
+
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection' };
|
|
1154
1417
|
}
|
|
1155
1418
|
const lClass = checkLClassWrite(toolName, checkedInput, {
|
|
1156
1419
|
sessionId: context.sessionId,
|
|
@@ -1161,9 +1424,10 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1161
1424
|
projectPath: context.projectPath,
|
|
1162
1425
|
workspacePath: context.workspacePath,
|
|
1163
1426
|
root: context.root,
|
|
1427
|
+
managedTempDir: context.managedTempDir,
|
|
1164
1428
|
});
|
|
1165
1429
|
if (lClass.behavior === 'deny') {
|
|
1166
|
-
return { behavior: 'deny', input: checkedInput, message: lClass.message };
|
|
1430
|
+
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection' };
|
|
1167
1431
|
}
|
|
1168
1432
|
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
1169
1433
|
if (dangerous.isDangerous
|
|
@@ -1174,6 +1438,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1174
1438
|
behavior: 'deny',
|
|
1175
1439
|
input: checkedInput,
|
|
1176
1440
|
message: '🔒 Git 破坏性操作仅允许 owner/admin 调用;当前角色无权执行',
|
|
1441
|
+
policyCode: 'role_git_destructive_forbidden',
|
|
1177
1442
|
};
|
|
1178
1443
|
}
|
|
1179
1444
|
return { behavior: 'continue', input: checkedInput };
|