evolcore 0.0.17 → 0.0.18

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { isHostChinese } from '../../utils/locale.js';
13
13
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
14
  import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
15
  import { summarizeToolInput } from '../../utils/tool-summary.js';
16
- import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
16
+ import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
17
  import { renderKitSections } from '../../eck/kit-renderer.js';
18
18
  import { renderMessageBody } from '../../eck/message-renderer.js';
19
19
  import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
@@ -47,6 +47,8 @@ import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/sessi
47
47
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
48
48
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
49
49
  import { classifyToolErrorCode } from '../permission/tool-error-code.js';
50
+ import { buildToolLifecycleEventKey } from '../audit/event-key.js';
51
+ import { PauseController } from './pause-controller.js';
50
52
  function isShowActivitiesMode(value) {
51
53
  return value === 'all' || value === 'text' || value === 'none';
52
54
  }
@@ -592,6 +594,28 @@ export class ResponseEngine {
592
594
  registerBuiltinModes(registry);
593
595
  this.responseCoordinator = new ResponseModeCoordinator(registry);
594
596
  }
597
+ pauseControllers = new Map();
598
+ getPauseController(sessionId) {
599
+ let controller = this.pauseControllers.get(sessionId);
600
+ if (!controller) {
601
+ controller = new PauseController();
602
+ this.pauseControllers.set(sessionId, controller);
603
+ }
604
+ return controller;
605
+ }
606
+ pauseSession(sessionId) { return this.getPauseController(sessionId).requestPause(); }
607
+ resumeSession(sessionId) { return this.pauseControllers.get(sessionId)?.resume() ?? false; }
608
+ isPauseRequested(sessionId) {
609
+ const controller = this.pauseControllers.get(sessionId);
610
+ return !!controller && (controller.isPauseRequested() || controller.isPaused());
611
+ }
612
+ clearPauseSession(sessionId, expected) {
613
+ const controller = this.pauseControllers.get(sessionId);
614
+ if (expected && controller !== expected)
615
+ return;
616
+ controller?.clear();
617
+ this.pauseControllers.delete(sessionId);
618
+ }
595
619
  setInteractionRouter(router) {
596
620
  this.interactionRouter = router;
597
621
  // 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
@@ -609,6 +633,7 @@ export class ResponseEngine {
609
633
  this.messageQueue = queue;
610
634
  }
611
635
  async interruptSession(sessionId, reason) {
636
+ this.clearPauseSession(sessionId);
612
637
  // A retry delay has no active runner stream to abort. Wake it before any
613
638
  // session-store I/O so /stop and a newer message take effect immediately.
614
639
  this.cancelRetryDelays(sessionId);
@@ -1165,7 +1190,7 @@ export class ResponseEngine {
1165
1190
  static COMMAND_PREFIXES = [
1166
1191
  '/new', '/pwd', '/help', '/status', '/restart',
1167
1192
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1168
- '/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check',
1193
+ '/stop', '/pause', '/resume', '/clear', '/compact', '/del', '/perm', '/file', '/check',
1169
1194
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1170
1195
  '/aid', '/upgrade', '/evolagent',
1171
1196
  ];
@@ -1293,7 +1318,10 @@ export class ResponseEngine {
1293
1318
  // 按 session.baseagent 选择 agent 后端(idle-kill 路径需要 interrupt)
1294
1319
  let agent;
1295
1320
  try {
1296
- agent = this.getAgent(channelKey, session.baseagent, session.selfAID || message.selfAID);
1321
+ const runnerSelfAid = this.agentRegistry?.resolveByChannel(channelKey)?.aid
1322
+ || session.selfAID
1323
+ || message.selfAID;
1324
+ agent = this.getAgent(channelKey, session.baseagent, runnerSelfAid);
1297
1325
  }
1298
1326
  catch (error) {
1299
1327
  if (error instanceof BaseagentRunnerUnavailableError) {
@@ -1526,6 +1554,10 @@ export class ResponseEngine {
1526
1554
  const taskAgentAid = message.selfAID || session.selfAID;
1527
1555
  const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
1528
1556
  ?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
1557
+ const runnerSelfAid = owningAgentForTask?.aid || session.selfAID || message.selfAID;
1558
+ const lifecycle = owningAgentForTask?.config?.lifecycle;
1559
+ const isActive = lifecycle === 'active';
1560
+ const isBootstrapping = lifecycle === 'bootstrapping';
1529
1561
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
1530
1562
  const agentNameForStats = owningAgentForTask?.name ?? taskAgentAid ?? '<unknown>';
1531
1563
  if (!channelInfo) {
@@ -1533,10 +1565,19 @@ export class ResponseEngine {
1533
1565
  this.publishTriggerExecutionFailure(message, `unknown_channel:${channelKey}`);
1534
1566
  return;
1535
1567
  }
1568
+ // Agent-owned turns are runnable only in the two explicit execution
1569
+ // states. An untransitioned `created` agent or a malformed/missing
1570
+ // lifecycle must not fall through to a base runner's default prompt.
1571
+ if (owningAgentForTask && !isActive && !isBootstrapping) {
1572
+ const blockedLifecycle = lifecycle ?? 'missing';
1573
+ logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
1574
+ this.publishTriggerExecutionFailure(message, `agent_lifecycle_not_runnable:${blockedLifecycle}`);
1575
+ return;
1576
+ }
1536
1577
  // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1537
1578
  // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1538
1579
  const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1539
- if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1580
+ if (!isBootstrapping && rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1540
1581
  logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1541
1582
  this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1542
1583
  return;
@@ -1546,7 +1587,7 @@ export class ResponseEngine {
1546
1587
  const identityRole = session.identity?.role || 'none';
1547
1588
  let agent;
1548
1589
  try {
1549
- agent = this.getAgent(channelKey, session.baseagent, session.selfAID || message.selfAID);
1590
+ agent = this.getAgent(channelKey, session.baseagent, runnerSelfAid);
1550
1591
  }
1551
1592
  catch (error) {
1552
1593
  if (error instanceof BaseagentRunnerUnavailableError) {
@@ -1569,6 +1610,7 @@ export class ResponseEngine {
1569
1610
  }
1570
1611
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
1571
1612
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
1613
+ const taskPauseController = this.getPauseController(session.id);
1572
1614
  let turnLease;
1573
1615
  const inputCausation = normalizeCausation(message.causation) ?? createRootCausation();
1574
1616
  const taskCausation = deriveCausation(inputCausation);
@@ -1604,7 +1646,12 @@ export class ResponseEngine {
1604
1646
  const currentChannelType = options?.channelType || message.channel;
1605
1647
  const adapterAny = channelInfo.adapter;
1606
1648
  const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
1607
- const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
1649
+ // During bootstrap the lifecycle owner is the authoritative identity. Do
1650
+ // not let a stale/crafted message or session selfAID redirect agent.md
1651
+ // access or the two lifecycle commands to another local AID.
1652
+ const selfAid = isBootstrapping
1653
+ ? owningAgentForTask?.aid
1654
+ : adapterSelfAid || message.selfAID || session.selfAID || undefined;
1608
1655
  const registrySelfName = selfAid ? this.agentRegistry?.resolveDisplayName?.(selfAid) : undefined;
1609
1656
  const adapterSelfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
1610
1657
  const selfName = registrySelfName ?? adapterSelfName;
@@ -1684,9 +1731,10 @@ export class ResponseEngine {
1684
1731
  chatType,
1685
1732
  peerType,
1686
1733
  });
1734
+ const responseModeAgentDir = selfAid ? resolveAgentDir(selfAid) : undefined;
1687
1735
  // mentionMode:顶层通用参数(先读出备用,投递/入队策略后续接入)
1688
1736
  const mentionMode = effectiveAgentConfig?.mentionMode ?? 'disabled';
1689
- const resolvedMode = triggerChatModeOverride || systemOrServicePeer
1737
+ const resolvedMode = triggerChatModeOverride || systemOrServicePeer || !responseModeAgentDir
1690
1738
  ? null // trigger 强制覆盖或 system/service 强制 interactive 时,不走插件解析
1691
1739
  : this.responseCoordinator.resolveMode(effectiveAgentConfig?.responseMode, // 标量:关系级>agent级>注册表首选
1692
1740
  chatModeFallback, effectiveAgentConfig?.responseModeParams, // 按模式分桶的参数字典
@@ -1706,7 +1754,7 @@ export class ResponseEngine {
1706
1754
  send: async () => { }, // 引擎自行发送,插件 handleOutbound 只做决策
1707
1755
  },
1708
1756
  logger,
1709
- agentDir: resolvePaths().agentsDir,
1757
+ agentDir: responseModeAgentDir,
1710
1758
  });
1711
1759
  if (resolvedMode) {
1712
1760
  logger.info('[ResponseSystem] selected mode=' + resolvedMode.mode.id + ' source=' + resolvedMode.source + ' chatType=' + chatType + ' peerKey=' + (peerKey ?? 'none') + ' chatMode=' + chatModeFallback);
@@ -2208,6 +2256,7 @@ export class ResponseEngine {
2208
2256
  approvalRouting,
2209
2257
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2210
2258
  recordExecutionAnomaly,
2259
+ pauseController: taskPauseController,
2211
2260
  preToolUsePolicyHook: pureSessionPolicyHook,
2212
2261
  armApprovedDelegationCommand: this.agentDelegationRegistry
2213
2262
  ? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
@@ -2363,8 +2412,8 @@ export class ResponseEngine {
2363
2412
  // Personal state is exposed to the manifest as vars. The manifest owns
2364
2413
  // loading persona.md and working.md into the system prompt.
2365
2414
  const owningAgent = owningAgentForTask;
2366
- const persona = owningAgent?.getPersona?.() || undefined;
2367
- const working = owningAgent?.getWorkingMemory?.() || undefined;
2415
+ const persona = isActive ? (owningAgent?.getPersona?.() || undefined) : undefined;
2416
+ const working = isActive ? (owningAgent?.getWorkingMemory?.() || undefined) : undefined;
2368
2417
  // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
2369
2418
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2370
2419
  const normalizedBaseagent = normalizeBaseagent(agent.name);
@@ -2521,6 +2570,12 @@ export class ResponseEngine {
2521
2570
  if (shouldPassPermissionMode) {
2522
2571
  modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
2523
2572
  }
2573
+ if (isBootstrapping && selfAid) {
2574
+ modelOverride = {
2575
+ ...(modelOverride || {}),
2576
+ bootstrapAgentMdPath: agentMdPath(selfAid),
2577
+ };
2578
+ }
2524
2579
  if (normalizedBaseagent.canonical === 'claude') {
2525
2580
  modelOverride = {
2526
2581
  ...(modelOverride || {}),
@@ -2567,6 +2622,9 @@ export class ResponseEngine {
2567
2622
  VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
2568
2623
  selfAid: selfAid || undefined,
2569
2624
  selfName: selfName || undefined,
2625
+ agentMdPath: selfAid ? agentMdPath(selfAid) : undefined,
2626
+ lifecycle,
2627
+ isBootstrapping,
2570
2628
  hasPersona: !!persona,
2571
2629
  hasWorkingMemory: !!working,
2572
2630
  peerId: peerIdRaw || undefined,
@@ -2637,7 +2695,12 @@ export class ResponseEngine {
2637
2695
  // 按会话原型(sessionType)选 manifest 文件:config.sessionManifests 映射,缺省回退主 manifest。
2638
2696
  const sessionType = session.sessionType ?? 'main';
2639
2697
  const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
2640
- const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2698
+ // Bootstrap always uses the lifecycle-gated main manifest. A custom
2699
+ // session manifest (for example the auxiliary manifest with `always`
2700
+ // sections) must not reintroduce normal rules or command context.
2701
+ const manifestFile = isBootstrapping
2702
+ ? 'eck_manifest.json'
2703
+ : sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2641
2704
  const kitContext = renderKitSections(kitCtx, manifestFile);
2642
2705
  effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
2643
2706
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
@@ -3834,6 +3897,7 @@ export class ResponseEngine {
3834
3897
  if (this.activeRenderers.get(session.id)?.taskId === taskId) {
3835
3898
  this.activeRenderers.delete(session.id);
3836
3899
  }
3900
+ this.clearPauseSession(session.id, taskPauseController);
3837
3901
  }
3838
3902
  // [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
3839
3903
  if (snapshot.isEnabled()) {
@@ -4420,8 +4484,15 @@ export class ResponseEngine {
4420
4484
  // 重置最后回复追踪
4421
4485
  lastReplyText = '';
4422
4486
  hasProjectedCurrentReplyText = false;
4487
+ const lifecycleEventKey = buildToolLifecycleEventKey({
4488
+ sessionId: session.id,
4489
+ callId: event.callId,
4490
+ correlationId: event.correlationId,
4491
+ });
4423
4492
  this.eventBus.publish({
4424
4493
  type: 'tool:use',
4494
+ eventKey: lifecycleEventKey,
4495
+ eventPhase: 'use',
4425
4496
  sessionId: session.id,
4426
4497
  toolName: event.name,
4427
4498
  input: event.input,
@@ -4429,6 +4500,10 @@ export class ResponseEngine {
4429
4500
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4430
4501
  agentAid: session.selfAID ?? 'unknown',
4431
4502
  permissionMode: permissionMode ?? 'unknown',
4503
+ decision: 'pending',
4504
+ decisionSource: 'runner',
4505
+ executed: false,
4506
+ executionState: 'requested',
4432
4507
  timestamp: Date.now(),
4433
4508
  causation,
4434
4509
  });
@@ -4487,8 +4562,15 @@ export class ResponseEngine {
4487
4562
  logger.warn('[ResponseEngine] auto clear queue after ec ctl queue failed:', error);
4488
4563
  }
4489
4564
  }
4565
+ const lifecycleEventKey = buildToolLifecycleEventKey({
4566
+ sessionId: session.id,
4567
+ callId: event.callId,
4568
+ correlationId: event.correlationId,
4569
+ });
4490
4570
  this.eventBus.publish({
4491
4571
  type: 'tool:result',
4572
+ eventKey: lifecycleEventKey,
4573
+ eventPhase: 'result',
4492
4574
  sessionId: session.id,
4493
4575
  toolName: event.name,
4494
4576
  isError: event.isError,
@@ -4498,6 +4580,10 @@ export class ResponseEngine {
4498
4580
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4499
4581
  agentAid: session.selfAID ?? 'unknown',
4500
4582
  permissionMode: permissionMode ?? 'unknown',
4583
+ decision: event.isError ? 'error' : 'allow',
4584
+ decisionSource: 'runner',
4585
+ executed: true,
4586
+ executionState: event.isError ? 'failed' : 'completed',
4501
4587
  timestamp: Date.now(),
4502
4588
  causation,
4503
4589
  });
@@ -4,7 +4,42 @@ import { resolveCommandPath } from '../../utils/cross-platform.js';
4
4
  import { getExistingHClassMaskTargets, getExistingLClassReadOnlyTargets, isSameOrDescendant, } from '../protected-paths.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  let cachedBubblewrapPath;
7
+ let sandboxInitializationCircuit;
8
+ let sandboxCircuitNoticeAt = 0;
7
9
  export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
10
+ export const SANDBOX_INITIALIZATION_COOLDOWN_MS = 5 * 60 * 1000;
11
+ export const SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS = 60 * 1000;
12
+ /** Open a process-wide fail-closed circuit after a confirmed host bootstrap failure. */
13
+ export function recordSandboxInitializationFailure(error, stderr = [], now = Date.now()) {
14
+ const detail = createSandboxInitializationError(error, stderr).message.slice(0, 512);
15
+ sandboxInitializationCircuit = sandboxInitializationCircuit
16
+ ? {
17
+ ...sandboxInitializationCircuit,
18
+ lastObservedAt: now,
19
+ occurrences: sandboxInitializationCircuit.occurrences + 1,
20
+ detail,
21
+ }
22
+ : { openedAt: now, lastObservedAt: now, occurrences: 1, detail };
23
+ return { ...sandboxInitializationCircuit };
24
+ }
25
+ /** Return an active failure circuit, or allow one probe after the cooldown. */
26
+ export function getActiveSandboxInitializationFailure(now = Date.now(), cooldownMs = SANDBOX_INITIALIZATION_COOLDOWN_MS) {
27
+ if (!sandboxInitializationCircuit)
28
+ return undefined;
29
+ if (now - sandboxInitializationCircuit.lastObservedAt >= cooldownMs) {
30
+ sandboxInitializationCircuit = undefined;
31
+ sandboxCircuitNoticeAt = 0;
32
+ return undefined;
33
+ }
34
+ return { ...sandboxInitializationCircuit };
35
+ }
36
+ /** Rate-limit human-readable warnings; structured audit still records every blocked task. */
37
+ export function shouldLogSandboxCircuitNotice(now = Date.now(), intervalMs = SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS) {
38
+ if (sandboxCircuitNoticeAt && now - sandboxCircuitNoticeAt < intervalMs)
39
+ return false;
40
+ sandboxCircuitNoticeAt = now;
41
+ return true;
42
+ }
8
43
  /** Recognize sandbox bootstrap failures independently of localized stderr. */
9
44
  export function isSandboxInitializationFailure(error, stderr = []) {
10
45
  const text = [error instanceof Error ? error.message : String(error ?? ''), ...stderr]
@@ -14,9 +49,18 @@ export function isSandboxInitializationFailure(error, stderr = []) {
14
49
  || /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
15
50
  || /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
16
51
  || /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
17
- || /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text);
52
+ || /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text)
53
+ || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text);
18
54
  }
19
55
  export function createSandboxInitializationError(error, stderr = []) {
56
+ if (error instanceof Error
57
+ && (error.code === SANDBOX_INITIALIZATION_FAILED
58
+ || error.message.includes(`${SANDBOX_INITIALIZATION_FAILED}:`))) {
59
+ error.code = SANDBOX_INITIALIZATION_FAILED;
60
+ if (stderr.length > 0)
61
+ error.stderr = [...stderr].slice(-20);
62
+ return error;
63
+ }
20
64
  const detail = error instanceof Error ? error.message : String(error ?? 'sandbox bootstrap failed');
21
65
  const wrapped = new Error(`${SANDBOX_INITIALIZATION_FAILED}: ${detail}`);
22
66
  wrapped.name = 'SandboxInitializationError';
@@ -211,6 +255,16 @@ function appendRuntimeBackedEtcFiles(args) {
211
255
  args.push('--ro-bind', candidate, resolved);
212
256
  }
213
257
  }
258
+ function appendContainerEtcFileBinds(args) {
259
+ for (const candidate of ['/etc/resolv.conf', '/etc/hosts', '/etc/hostname']) {
260
+ if (!fs.existsSync(candidate))
261
+ continue;
262
+ // A directory bind does not recursively preserve OCI/container-runtime
263
+ // file mounts. Rebind only the three standard runtime-injected files after
264
+ // /etc becomes readonly; no other /etc mount is carried into the guard.
265
+ args.push('--ro-bind', candidate, candidate);
266
+ }
267
+ }
214
268
  function appendHClassMasks(args, root, maskContainerSockets) {
215
269
  for (const target of getExistingHClassMaskTargets(root)) {
216
270
  if (target.kind === 'directory')
@@ -251,20 +305,30 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
251
305
  throw new Error(`Codex managed requirements is not a file: ${requirements}`);
252
306
  }
253
307
  const managedDirectory = path.dirname(requirements);
308
+ const systemConfigDirectory = '/etc/codex';
309
+ let systemConfigStat;
310
+ try {
311
+ systemConfigStat = fs.lstatSync(systemConfigDirectory);
312
+ }
313
+ catch {
314
+ throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
315
+ + 'must be created as a real directory during installation');
316
+ }
317
+ if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
318
+ throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
319
+ + 'must be a real directory');
320
+ }
254
321
  // Keep the copied hook immutable even when a session uses bypass mode.
255
322
  args.push('--ro-bind', managedDirectory, managedDirectory);
256
- // 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');
323
+ // The guard starts from a writable root so owner-bypass workspaces keep
324
+ // their normal semantics. Make the system configuration tree readonly
325
+ // before replacing only Codex's directory, so a root daemon cannot write
326
+ // unrelated files under /etc from inside this namespace.
327
+ args.push('--ro-bind', '/etc', '/etc');
328
+ appendContainerEtcFileBinds(args);
329
+ // Replace only Codex's system-config directory. Requiring the mountpoint to
330
+ // exist prevents Bubblewrap from creating it on the writable host root.
331
+ args.push('--ro-bind', managedDirectory, systemConfigDirectory);
268
332
  }
269
333
  args.push('--', executable, ...executableArgs);
270
334
  return { command: bubblewrapPath, args };
@@ -304,4 +368,6 @@ export function buildBubblewrapCommand(executable, executableArgs, options) {
304
368
  }
305
369
  export function _resetSandboxRuntimeCache() {
306
370
  cachedBubblewrapPath = undefined;
371
+ sandboxInitializationCircuit = undefined;
372
+ sandboxCircuitNoticeAt = 0;
307
373
  }
@@ -1148,7 +1148,7 @@ export function checkLClassWrite(toolName, input, context) {
1148
1148
  behavior: 'deny',
1149
1149
  message: context?.permissionMode === 'readonly'
1150
1150
  ? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
1151
- : '🔒 L-class 路径仅允许受限、可证明只读的命令;未知 Shell 语法失败关闭。修改必须通过受控的 EvolCore 命令',
1151
+ : '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
1152
1152
  };
1153
1153
  }
1154
1154
  collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);