evolcore 0.0.19 → 0.0.20
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 +21 -1
- package/README.md +2 -0
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/claude-runner.js +4 -3
- package/dist/agents/codex-runner.js +3 -2
- package/dist/agents/ecagent-runner.js +3 -2
- package/dist/aun/aid/agentmd.js +7 -0
- package/dist/aun/msg/group.js +14 -3
- package/dist/aun/msg/p2p.js +21 -11
- package/dist/aun/outbox.js +144 -19
- package/dist/channels/aun.js +621 -211
- package/dist/cli/daemon-commands.js +12 -6
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +55 -15
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/config/aun-gateway-config.js +2 -0
- package/dist/config/config-manager.js +92 -8
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/gateway-config.js +9 -7
- package/dist/config/lifecycle.js +16 -5
- package/dist/config-store.js +13 -6
- package/dist/core/auth/authorization-audit.js +5 -2
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +21 -36
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/evolagent.js +5 -4
- package/dist/core/message/response-engine.js +9 -4
- package/dist/core/permission/ec-command-parser.js +56 -3
- package/dist/core/permission/sandbox-runtime.js +46 -12
- package/dist/core/permission/tool-policy.js +98 -41
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/eck/kit-renderer.js +17 -8
- package/dist/index.js +30 -19
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +408 -0
- package/kits/schemas/daemon.schema.5.json +136 -0
- package/kits/schemas/defaults.schema.5.json +107 -0
- package/package.json +2 -1
|
@@ -5,7 +5,7 @@ import { kitsTemplatesDir, agentMdPath } from '../paths.js';
|
|
|
5
5
|
import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
|
|
9
9
|
import { renderTemplate } from '../eck/manifest-engine.js';
|
|
10
10
|
import { activeBaseagent } from './model/config-scope.js';
|
|
11
11
|
import { buildEnvelope } from './message/message-utils.js';
|
|
@@ -123,12 +123,18 @@ export class BootstrapService {
|
|
|
123
123
|
this.inFlight.delete(key);
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
126
|
+
const lifecycle = resolveAgentLifecycle(loadedConfig);
|
|
127
|
+
if (lifecycle === 'active') {
|
|
128
128
|
this.inFlight.delete(key);
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
if (lifecycle !== 'created' && lifecycle !== 'bootstrapping') {
|
|
132
|
+
this.inFlight.delete(key);
|
|
133
|
+
logger.warn(`[Bootstrap] Refusing invalid lifecycle for ${aid}: ${String(loadedConfig.lifecycle)}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const config = { ...loadedConfig, lifecycle };
|
|
137
|
+
const starting = lifecycle === 'created';
|
|
132
138
|
const channelType = ctx.channelType || this.channelTypeFromKey(ctx.channelKey);
|
|
133
139
|
const configuredRecipient = this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
|
|
134
140
|
const recipientId = ctx.recipientId || configuredRecipient;
|
|
@@ -140,35 +146,14 @@ export class BootstrapService {
|
|
|
140
146
|
this.inFlight.delete(key);
|
|
141
147
|
return false;
|
|
142
148
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
isGroupRecipient = channelType === 'aun'
|
|
153
|
-
? (await ctx.adapter.isGroup?.(recipientId)) === true
|
|
154
|
-
: false;
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
// Group/private routing is security-sensitive. If the authoritative
|
|
158
|
-
// probe fails, do not guess a route, and release the single-flight key
|
|
159
|
-
// so a later connection or retry can recover.
|
|
160
|
-
this.inFlight.delete(key);
|
|
161
|
-
logger.warn(`[Bootstrap] Failed to resolve recipient type for ${aid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
const delivery = isGroupRecipient === true
|
|
165
|
-
? { chatType: 'group', groupId: recipientId }
|
|
166
|
-
: { chatType: 'private' };
|
|
167
|
-
const channelId = delivery.chatType === 'group'
|
|
168
|
-
? delivery.groupId
|
|
169
|
-
: channelType === 'aun'
|
|
170
|
-
? recipientId
|
|
171
|
-
: ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
|
|
149
|
+
// Bootstrap is initiated by connection/owner binding, not by a reply to
|
|
150
|
+
// an existing group message. `owners[]` contains personal Owner AIDs, so
|
|
151
|
+
// the first prompt must always use the private message route. Inbound
|
|
152
|
+
// group context must never change this initial delivery target.
|
|
153
|
+
const delivery = { chatType: 'private' };
|
|
154
|
+
const channelId = channelType === 'aun'
|
|
155
|
+
? recipientId
|
|
156
|
+
: ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
|
|
172
157
|
if (!channelId) {
|
|
173
158
|
this.inFlight.delete(key);
|
|
174
159
|
return false;
|
|
@@ -234,14 +219,14 @@ export class BootstrapService {
|
|
|
234
219
|
const loadedConfig = loadAgent(aid) || agent?.config;
|
|
235
220
|
if (!loadedConfig)
|
|
236
221
|
return { ok: false, error: `Agent "${aid}" not found` };
|
|
237
|
-
const lifecycle =
|
|
222
|
+
const lifecycle = resolveAgentLifecycle(loadedConfig);
|
|
238
223
|
if (lifecycle === 'active')
|
|
239
224
|
return { ok: true, aid, transitioned: false };
|
|
240
225
|
if (lifecycle !== 'bootstrapping') {
|
|
241
226
|
return {
|
|
242
227
|
ok: false,
|
|
243
228
|
code: 'INVALID_LIFECYCLE',
|
|
244
|
-
error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready`,
|
|
229
|
+
error: `Agent "${aid}" is ${lifecycle ?? `invalid (${String(loadedConfig.lifecycle)})`}; only a bootstrapping agent can become ready`,
|
|
245
230
|
};
|
|
246
231
|
}
|
|
247
232
|
this.setLifecycle(agent, aid, 'active');
|
|
@@ -253,7 +238,7 @@ export class BootstrapService {
|
|
|
253
238
|
const agent = this.agentRegistry.get(aid);
|
|
254
239
|
const loadedConfig = loadAgent(aid) || agent?.config;
|
|
255
240
|
return loadedConfig
|
|
256
|
-
?
|
|
241
|
+
? resolveAgentLifecycle(loadedConfig)
|
|
257
242
|
: null;
|
|
258
243
|
}
|
|
259
244
|
setLifecycle(agent, aid, lifecycle) {
|
|
@@ -65,7 +65,6 @@ export class ChannelLoader {
|
|
|
65
65
|
owner: agent.getOwner(aunEffName),
|
|
66
66
|
enabled: true,
|
|
67
67
|
keystorePath: agent.config.aun?.keystorePath,
|
|
68
|
-
gatewayUrl: agent.config.aun?.gatewayUrl,
|
|
69
68
|
};
|
|
70
69
|
const configInsts = [aunInst];
|
|
71
70
|
for (const inst of agent.config.channels) {
|
|
@@ -255,7 +254,6 @@ export function buildReloadHooks(deps) {
|
|
|
255
254
|
aid,
|
|
256
255
|
enabled: true,
|
|
257
256
|
keystorePath: agent.config?.aun?.keystorePath,
|
|
258
|
-
gatewayUrl: agent.config?.aun?.gatewayUrl,
|
|
259
257
|
}
|
|
260
258
|
: (() => {
|
|
261
259
|
const agentChannels = agent.config?.channels ?? [];
|
package/dist/core/evolagent.js
CHANGED
|
@@ -243,16 +243,17 @@ export class EvolAgent {
|
|
|
243
243
|
b.mentionMode = value;
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
-
/** 读取观察者模式开关(默认 false
|
|
246
|
+
/** 读取观察者模式开关(默认 true;显式 false 可关闭)。 */
|
|
247
247
|
getObservable() {
|
|
248
|
-
return this.merged.observable
|
|
248
|
+
return this.merged.observable ?? true;
|
|
249
249
|
}
|
|
250
250
|
/** 设置观察者模式开关:开启后入站/出站消息各转发一份给 owners[]。 */
|
|
251
251
|
setObservable(value) {
|
|
252
|
+
// The default is enabled, so false must remain an explicit persisted override.
|
|
252
253
|
if (value)
|
|
253
|
-
this.rawAgent.observable = true;
|
|
254
|
-
else
|
|
255
254
|
delete this.rawAgent.observable;
|
|
255
|
+
else
|
|
256
|
+
this.rawAgent.observable = false;
|
|
256
257
|
this.merged.observable = value;
|
|
257
258
|
this.persist();
|
|
258
259
|
}
|
|
@@ -33,6 +33,7 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
|
|
|
33
33
|
import { RetryScheduler } from './retry-scheduler.js';
|
|
34
34
|
import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
35
35
|
import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
|
|
36
|
+
import { resolveAgentLifecycle } from '../../config/lifecycle.js';
|
|
36
37
|
import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
|
|
37
38
|
import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
|
|
38
39
|
import { normalizeUsage } from '../../stats/normalizer.js';
|
|
@@ -1606,7 +1607,9 @@ export class ResponseEngine {
|
|
|
1606
1607
|
const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
|
|
1607
1608
|
?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
|
|
1608
1609
|
const runnerSelfAid = owningAgentForTask?.aid || session.selfAID || message.selfAID;
|
|
1609
|
-
const lifecycle = owningAgentForTask
|
|
1610
|
+
const lifecycle = owningAgentForTask
|
|
1611
|
+
? resolveAgentLifecycle(owningAgentForTask.config)
|
|
1612
|
+
: undefined;
|
|
1610
1613
|
const isActive = lifecycle === 'active';
|
|
1611
1614
|
const isBootstrapping = lifecycle === 'bootstrapping';
|
|
1612
1615
|
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
@@ -1617,10 +1620,12 @@ export class ResponseEngine {
|
|
|
1617
1620
|
return;
|
|
1618
1621
|
}
|
|
1619
1622
|
// Agent-owned turns are runnable only in the two explicit execution
|
|
1620
|
-
// states. An untransitioned `created` agent or
|
|
1621
|
-
//
|
|
1623
|
+
// states. An untransitioned `created` agent or malformed lifecycle must
|
|
1624
|
+
// not fall through to a base runner's default prompt. Missing lifecycle
|
|
1625
|
+
// is normalized to active for legacy configurations.
|
|
1622
1626
|
if (owningAgentForTask && !isActive && !isBootstrapping) {
|
|
1623
|
-
const blockedLifecycle = lifecycle
|
|
1627
|
+
const blockedLifecycle = lifecycle
|
|
1628
|
+
?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
|
|
1624
1629
|
logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
|
|
1625
1630
|
this.publishTriggerExecutionFailure(message, `agent_lifecycle_not_runnable:${blockedLifecycle}`);
|
|
1626
1631
|
return;
|
|
@@ -17,7 +17,57 @@ const POWERSHELL_UNQUOTED_META = new Set([
|
|
|
17
17
|
';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
|
|
18
18
|
]);
|
|
19
19
|
const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
|
|
20
|
-
|
|
20
|
+
// This is intentionally limited to the command boundary. It is not a search
|
|
21
|
+
// for `ec` anywhere in the input: quoted arguments, regexps and log text must
|
|
22
|
+
// never establish an EC invocation.
|
|
23
|
+
function hasEcCommandPrefix(command) {
|
|
24
|
+
let quote = null;
|
|
25
|
+
let escaped = false;
|
|
26
|
+
let boundary = true;
|
|
27
|
+
for (let index = 0; index < command.length; index++) {
|
|
28
|
+
const char = command[index];
|
|
29
|
+
if (escaped) {
|
|
30
|
+
escaped = false;
|
|
31
|
+
boundary = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (quote === 'single') {
|
|
35
|
+
if (char === "'")
|
|
36
|
+
quote = null;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (quote === 'double') {
|
|
40
|
+
if (char === '\\') {
|
|
41
|
+
escaped = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '"')
|
|
45
|
+
quote = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "'") {
|
|
49
|
+
quote = 'single';
|
|
50
|
+
boundary = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === '"') {
|
|
54
|
+
quote = 'double';
|
|
55
|
+
boundary = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (/[;&|`()<>]/.test(char)) {
|
|
59
|
+
boundary = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (boundary && /\s/.test(char))
|
|
63
|
+
continue;
|
|
64
|
+
if (boundary && command.slice(index, index + 2).toLowerCase() === 'ec'
|
|
65
|
+
&& (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
|
|
66
|
+
return true;
|
|
67
|
+
boundary = false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
21
71
|
function trimShellSpacing(value) {
|
|
22
72
|
return value.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
23
73
|
}
|
|
@@ -1059,7 +1109,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1059
1109
|
});
|
|
1060
1110
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
1061
1111
|
return { kind: 'literal' };
|
|
1062
|
-
if (!nested.ok &&
|
|
1112
|
+
if (!nested.ok && hasEcCommandPrefix(carrier.command)) {
|
|
1063
1113
|
return { kind: 'composite', issue: nested.issue };
|
|
1064
1114
|
}
|
|
1065
1115
|
}
|
|
@@ -1071,7 +1121,10 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
1071
1121
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
1072
1122
|
return { kind: 'bounded-output', command: boundedOutput };
|
|
1073
1123
|
}
|
|
1074
|
-
|
|
1124
|
+
// Only a command-boundary prefix is enough to preserve a useful syntax
|
|
1125
|
+
// error for a malformed direct EC invocation. Never scan for EC tokens in
|
|
1126
|
+
// arbitrary raw text after parsing fails.
|
|
1127
|
+
return hasEcCommandPrefix(command)
|
|
1075
1128
|
? { kind: 'composite', issue: literal.issue }
|
|
1076
1129
|
: { kind: 'none' };
|
|
1077
1130
|
}
|
|
@@ -285,6 +285,48 @@ function appendLClassReadOnlyBinds(args, root) {
|
|
|
285
285
|
args.push('--ro-bind', target.path, target.path);
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
+
export function ensureCodexManagedMountpoint(systemConfigDirectory = '/etc/codex', effectiveUid = typeof process.getuid === 'function' ? process.getuid() : undefined) {
|
|
289
|
+
let systemConfigStat;
|
|
290
|
+
try {
|
|
291
|
+
systemConfigStat = fs.lstatSync(systemConfigDirectory);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code !== 'ENOENT') {
|
|
295
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
296
|
+
+ `could not be inspected (${error instanceof Error ? error.message : String(error)})`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!systemConfigStat && process.platform === 'linux' && effectiveUid === 0) {
|
|
300
|
+
let created = false;
|
|
301
|
+
try {
|
|
302
|
+
fs.mkdirSync(systemConfigDirectory, { mode: 0o755 });
|
|
303
|
+
created = true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error.code !== 'EEXIST') {
|
|
307
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
308
|
+
+ `could not be created by the root daemon (${error instanceof Error ? error.message : String(error)})`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (created)
|
|
312
|
+
fs.chmodSync(systemConfigDirectory, 0o755);
|
|
313
|
+
try {
|
|
314
|
+
systemConfigStat = fs.lstatSync(systemConfigDirectory);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
318
|
+
+ `could not be verified after creation (${error instanceof Error ? error.message : String(error)})`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!systemConfigStat) {
|
|
322
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
323
|
+
+ 'must be created as a real directory during installation; run: sudo install -d -m 0755 /etc/codex');
|
|
324
|
+
}
|
|
325
|
+
if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
|
|
326
|
+
throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
|
|
327
|
+
+ 'must be a real directory');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
288
330
|
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
|
|
289
331
|
const bubblewrapPath = resolveBubblewrapPath();
|
|
290
332
|
if (!bubblewrapPath)
|
|
@@ -306,18 +348,10 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
306
348
|
}
|
|
307
349
|
const managedDirectory = path.dirname(requirements);
|
|
308
350
|
const systemConfigDirectory = '/etc/codex';
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
}
|
|
351
|
+
// The npm lifecycle and public installer provision this path. Root-owned
|
|
352
|
+
// legacy daemons may repair only the missing-directory case; unsafe path
|
|
353
|
+
// types and unprivileged processes remain fail-closed.
|
|
354
|
+
ensureCodexManagedMountpoint(systemConfigDirectory);
|
|
321
355
|
// Keep the copied hook immutable even when a session uses bypass mode.
|
|
322
356
|
args.push('--ro-bind', managedDirectory, managedDirectory);
|
|
323
357
|
// The guard starts from a writable root so owner-bypass workspaces keep
|
|
@@ -425,7 +425,7 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
425
425
|
const workspace = options.cwd ? resolveProtectedCandidate(options.cwd) : undefined;
|
|
426
426
|
if (isSameOrDescendant(candidate, managedTemp)
|
|
427
427
|
&& (!workspace || !isSameOrDescendant(candidate, workspace))) {
|
|
428
|
-
return
|
|
428
|
+
return undefined;
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
if (className === 'h' && accessKind === 'metadata' && operand.access === 'exact') {
|
|
@@ -439,27 +439,31 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
439
439
|
followFinalSymlink: false,
|
|
440
440
|
});
|
|
441
441
|
if (options.allowProtectedMetadata === false) {
|
|
442
|
-
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
442
|
+
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
443
|
+
? resolveProtectedCandidate(operand.value, options.cwd)
|
|
444
|
+
: undefined;
|
|
443
445
|
}
|
|
444
446
|
// The no-follow decision is authoritative for metadata. In particular,
|
|
445
447
|
// an ordinary symlink whose target is H-class must remain inspectable as a
|
|
446
448
|
// link; falling through to isHClassPath() would follow it and reject the
|
|
447
449
|
// otherwise safe lstat/stat operation.
|
|
448
450
|
if (metadata.pathClass !== 'unknown' && metadata.allowed) {
|
|
449
|
-
return
|
|
451
|
+
return undefined;
|
|
450
452
|
}
|
|
451
453
|
}
|
|
452
454
|
const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
|
|
453
455
|
if (reference)
|
|
454
|
-
return
|
|
456
|
+
return resolveProtectedCandidate(operand.value, options.cwd);
|
|
455
457
|
if (operand.access === 'recursive') {
|
|
456
|
-
|
|
458
|
+
const matched = className === 'h'
|
|
457
459
|
? hClassReadScopeIncludesProtectedPath(operand.value, options)
|
|
458
460
|
: lClassGrantIncludesProtectedPath(operand.value, options);
|
|
461
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
459
462
|
}
|
|
460
|
-
|
|
463
|
+
const matched = className === 'h'
|
|
461
464
|
? isHClassPath(operand.value, options)
|
|
462
465
|
: isLClassPath(operand.value, options);
|
|
466
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
463
467
|
}
|
|
464
468
|
/**
|
|
465
469
|
* A read-only recursive query only needs to cover H-class targets that exist
|
|
@@ -489,16 +493,41 @@ function analyzeShellProtectedOperands(command, options) {
|
|
|
489
493
|
};
|
|
490
494
|
}
|
|
491
495
|
const operands = queryPathOperands(analysis);
|
|
492
|
-
|
|
493
|
-
|
|
496
|
+
// Only parsed path operands can establish a lock-file diagnostic. Searching
|
|
497
|
+
// the raw command would mistake a grep/rg pattern or report text for an
|
|
498
|
+
// actual protected filesystem target.
|
|
499
|
+
const lockDiagnostic = operands.some(({ operand }) => containsLockPathReference(operand.value))
|
|
500
|
+
|| hasFindLockPattern(analysis);
|
|
501
|
+
const hClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)).find(Boolean);
|
|
502
|
+
const lClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)).find(Boolean);
|
|
494
503
|
return {
|
|
495
504
|
analysis,
|
|
496
|
-
hClass: lockDiagnostic ||
|
|
497
|
-
lClass:
|
|
498
|
-
pathHClass,
|
|
505
|
+
hClass: lockDiagnostic || !!hClassPath,
|
|
506
|
+
lClass: !!lClassPath,
|
|
507
|
+
pathHClass: !!hClassPath,
|
|
499
508
|
lockDiagnostic,
|
|
509
|
+
hClassPath,
|
|
510
|
+
lClassPath,
|
|
500
511
|
};
|
|
501
512
|
}
|
|
513
|
+
/** Detect a find pattern such as `-name '*.lock'` from parsed argv only. */
|
|
514
|
+
function hasFindLockPattern(analysis) {
|
|
515
|
+
if (analysis.kind !== 'proven-readonly')
|
|
516
|
+
return false;
|
|
517
|
+
return analysis.ir.pipelines.some(pipeline => pipeline.some(command => {
|
|
518
|
+
if (command.executable !== 'find')
|
|
519
|
+
return false;
|
|
520
|
+
for (let index = 0; index < command.argv.length - 1; index++) {
|
|
521
|
+
if (!['-name', '-iname', '-path', '-ipath', '-wholename'].includes(command.argv[index]))
|
|
522
|
+
continue;
|
|
523
|
+
if (/(?:^|[\\/])[^\\/]*\.lock(?:$|[\\/])/i.test(command.argv[index + 1]))
|
|
524
|
+
return true;
|
|
525
|
+
if (/^EVOLCORE_RUNTIME_LOCK_DIR$/i.test(command.argv[index + 1]))
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
502
531
|
function protectedReadToolPaths(toolName, input) {
|
|
503
532
|
if (toolName === 'Read') {
|
|
504
533
|
return typeof input.file_path === 'string' ? [input.file_path] : [];
|
|
@@ -513,33 +542,43 @@ function protectedReadToolPaths(toolName, input) {
|
|
|
513
542
|
}
|
|
514
543
|
function protectedReadToolMatchesLClass(toolName, input, options) {
|
|
515
544
|
const paths = protectedReadToolPaths(toolName, input);
|
|
516
|
-
|
|
517
|
-
|
|
545
|
+
const direct = paths.find(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options));
|
|
546
|
+
if (direct) {
|
|
547
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
518
548
|
}
|
|
519
549
|
if (toolName === 'Grep') {
|
|
520
550
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
521
|
-
return lClassGrantIncludesProtectedPath(scope, options)
|
|
551
|
+
return lClassGrantIncludesProtectedPath(scope, options)
|
|
552
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
553
|
+
: undefined;
|
|
522
554
|
}
|
|
523
555
|
if (toolName === 'Glob') {
|
|
524
556
|
const scope = globReadScope(input, options);
|
|
525
|
-
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
557
|
+
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
558
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
559
|
+
: undefined;
|
|
526
560
|
}
|
|
527
|
-
return
|
|
561
|
+
return undefined;
|
|
528
562
|
}
|
|
529
563
|
function protectedReadToolMatchesHClass(toolName, input, options) {
|
|
530
564
|
const paths = protectedReadToolPaths(toolName, input);
|
|
531
|
-
|
|
532
|
-
|
|
565
|
+
const direct = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options));
|
|
566
|
+
if (direct) {
|
|
567
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
533
568
|
}
|
|
534
569
|
if (toolName === 'Grep') {
|
|
535
570
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
536
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
571
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
572
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
573
|
+
: undefined;
|
|
537
574
|
}
|
|
538
575
|
if (toolName === 'Glob') {
|
|
539
576
|
const scope = globReadScope(input, options);
|
|
540
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
577
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
578
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
579
|
+
: undefined;
|
|
541
580
|
}
|
|
542
|
-
return
|
|
581
|
+
return undefined;
|
|
543
582
|
}
|
|
544
583
|
function globLiteralPrefix(pattern) {
|
|
545
584
|
const normalized = pattern.replace(/\\/g, '/');
|
|
@@ -646,14 +685,17 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
646
685
|
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
647
686
|
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
648
687
|
}
|
|
649
|
-
|
|
650
|
-
|
|
688
|
+
const directHPath = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions));
|
|
689
|
+
if (directHPath) {
|
|
690
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: resolveProtectedCandidate(directHPath, projectPath) };
|
|
651
691
|
}
|
|
652
|
-
|
|
653
|
-
|
|
692
|
+
const hPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
693
|
+
if (hPath) {
|
|
694
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: hPath };
|
|
654
695
|
}
|
|
655
|
-
|
|
656
|
-
|
|
696
|
+
const lPath = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
697
|
+
if (lPath) {
|
|
698
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: lPath };
|
|
657
699
|
}
|
|
658
700
|
return { behavior: 'allow' };
|
|
659
701
|
}
|
|
@@ -691,11 +733,15 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
691
733
|
}
|
|
692
734
|
if (protectedOperands.hClass) {
|
|
693
735
|
logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
694
|
-
|
|
736
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
737
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: protectedOperands.hClassPath };
|
|
738
|
+
}
|
|
695
739
|
}
|
|
696
740
|
if (protectedOperands.lClass) {
|
|
697
741
|
logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
698
|
-
|
|
742
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
743
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: protectedOperands.lClassPath };
|
|
744
|
+
}
|
|
699
745
|
}
|
|
700
746
|
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
701
747
|
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
@@ -967,6 +1013,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
967
1013
|
return {
|
|
968
1014
|
behavior: 'deny',
|
|
969
1015
|
message: '🔒 临时文件必须位于当前会话的 $TMPDIR 内',
|
|
1016
|
+
matchedPath: tempCheck.paths[0],
|
|
970
1017
|
};
|
|
971
1018
|
}
|
|
972
1019
|
// A managed temp write is intentionally allowed to contain names such as
|
|
@@ -992,6 +1039,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
992
1039
|
return {
|
|
993
1040
|
behavior: 'deny',
|
|
994
1041
|
message: '🔒 Shell 命令涉及受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1042
|
+
matchedPath: protectedOperands.hClassPath,
|
|
995
1043
|
};
|
|
996
1044
|
}
|
|
997
1045
|
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
@@ -1010,14 +1058,17 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1010
1058
|
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
1011
1059
|
// FileChange, or PermissionGrant above.
|
|
1012
1060
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1013
|
-
if (['Read', 'Glob', 'Grep'].includes(toolName)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1061
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)) {
|
|
1062
|
+
const matchedReadPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
1063
|
+
if (matchedReadPath) {
|
|
1064
|
+
logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
|
|
1065
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
1066
|
+
return {
|
|
1067
|
+
behavior: 'deny',
|
|
1068
|
+
message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1069
|
+
matchedPath: matchedReadPath,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1021
1072
|
}
|
|
1022
1073
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
1023
1074
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
@@ -1036,6 +1087,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1036
1087
|
return {
|
|
1037
1088
|
behavior: 'deny',
|
|
1038
1089
|
message: '🔒 请求的文件系统权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1090
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1039
1091
|
};
|
|
1040
1092
|
}
|
|
1041
1093
|
}
|
|
@@ -1059,6 +1111,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1059
1111
|
return {
|
|
1060
1112
|
behavior: 'deny',
|
|
1061
1113
|
message: '🔒 请求的 project_roots 权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1114
|
+
matchedPath: resolveProtectedCandidate(candidate, permissionWorkspace),
|
|
1062
1115
|
};
|
|
1063
1116
|
}
|
|
1064
1117
|
}
|
|
@@ -1093,7 +1146,8 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1093
1146
|
behavior: 'deny',
|
|
1094
1147
|
message: `🔒 此文件受保护,agent 不可直接写入\n\n` +
|
|
1095
1148
|
`文件:${filePath}\n` +
|
|
1096
|
-
`类型:配置/快照/证书等系统关键文件${suggestion}
|
|
1149
|
+
`类型:配置/快照/证书等系统关键文件${suggestion}`,
|
|
1150
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1097
1151
|
};
|
|
1098
1152
|
}
|
|
1099
1153
|
}
|
|
@@ -1108,7 +1162,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1108
1162
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1109
1163
|
const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
1110
1164
|
if (lClassRead && context?.permissionMode === 'readonly') {
|
|
1111
|
-
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
1165
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', matchedPath: lClassRead };
|
|
1112
1166
|
}
|
|
1113
1167
|
return { behavior: 'allow' };
|
|
1114
1168
|
}
|
|
@@ -1149,6 +1203,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1149
1203
|
message: context?.permissionMode === 'readonly'
|
|
1150
1204
|
? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
|
|
1151
1205
|
: '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
|
|
1206
|
+
matchedPath: protectedOperands.lClassPath,
|
|
1152
1207
|
};
|
|
1153
1208
|
}
|
|
1154
1209
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
@@ -1167,6 +1222,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1167
1222
|
return {
|
|
1168
1223
|
behavior: 'deny',
|
|
1169
1224
|
message: '🔒 请求的文件系统写权限包含 L-class 只读路径,已拒绝授权',
|
|
1225
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1170
1226
|
};
|
|
1171
1227
|
}
|
|
1172
1228
|
}
|
|
@@ -1188,6 +1244,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1188
1244
|
return {
|
|
1189
1245
|
behavior: 'deny',
|
|
1190
1246
|
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
|
|
1247
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1191
1248
|
};
|
|
1192
1249
|
}
|
|
1193
1250
|
return { behavior: 'allow' };
|
|
@@ -1472,7 +1529,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1472
1529
|
allowProtectedMetadata: context.allowProtectedMetadata,
|
|
1473
1530
|
});
|
|
1474
1531
|
if (hClass.behavior === 'deny') {
|
|
1475
|
-
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection' };
|
|
1532
|
+
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection', matchedPath: hClass.matchedPath };
|
|
1476
1533
|
}
|
|
1477
1534
|
const lClass = checkLClassWrite(toolName, checkedInput, {
|
|
1478
1535
|
sessionId: context.sessionId,
|
|
@@ -1486,7 +1543,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1486
1543
|
managedTempDir: context.managedTempDir,
|
|
1487
1544
|
});
|
|
1488
1545
|
if (lClass.behavior === 'deny') {
|
|
1489
|
-
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection' };
|
|
1546
|
+
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
|
|
1490
1547
|
}
|
|
1491
1548
|
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
1492
1549
|
if (dangerous.isDangerous
|