evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -1,6 +1,7 @@
1
+ import { getRunnerCatalog, peekRunnerCatalog, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
1
2
  import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages, resolveSettings as sdkResolveSettings, } from '@anthropic-ai/claude-agent-sdk';
2
3
  import { spawn } from 'child_process';
3
- import { atomicReadJson, atomicWriteJson, ensureDir } from '../utils/atomic-write.js';
4
+ import { ensureDir } from '../utils/atomic-write.js';
4
5
  import { resolveAnthropicConfig } from './baseagent.js';
5
6
  import { buildModelRequestHeaders } from './request-identity.js';
6
7
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
@@ -261,103 +262,6 @@ async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSou
261
262
  }
262
263
  }
263
264
  }
264
- // ── 模型别名解析 ──
265
- // SDK 内置的别名表可能落后于代理实际可用的最新模型,
266
- // 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则使用持久化的最近成功值。
267
- // 已验证可用但尚未出现在 /models 列表中的模型 ID 会被注入候选列表,
268
- // 等列表更新后注入自动变为 no-op。
269
- const MODEL_FAMILIES = ['opus', 'sonnet', 'haiku'];
270
- /** 已验证可用但可能尚未出现在 /models 列表中的模型 ID(注入候选) */
271
- const INJECTED_MODELS = [];
272
- /** 启动默认值:某网关尚无任何成功刷新记录时使用。 */
273
- const BOOTSTRAP_MODEL_ALIASES = {
274
- 'opus': 'claude-opus-4-8',
275
- 'sonnet': 'claude-sonnet-4-6',
276
- 'haiku': 'claude-haiku-4-5-20251001',
277
- };
278
- const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
279
- const MODEL_ALIAS_STORE_VERSION = 1;
280
- const modelAliasCache = new Map(); // key: baseUrl
281
- const modelAliasFallbacks = new Map(); // key: baseUrl,最近一次成功解析值
282
- const modelAliasInFlight = new Set(); // 去重并发刷新
283
- const loadedModelAliasFallbacks = new Set(); // key: store path + gateway hash
284
- function normalizeModelGatewayUrl(baseUrl) {
285
- return baseUrl.trim().replace(/\/+$/, '');
286
- }
287
- function modelGatewayKey(baseUrl) {
288
- return crypto.createHash('sha256').update(normalizeModelGatewayUrl(baseUrl)).digest('hex');
289
- }
290
- function modelAliasStorePath() {
291
- return resolvePaths().claudeModelCache;
292
- }
293
- function sanitizeModelAliases(value) {
294
- if (!value || typeof value !== 'object' || Array.isArray(value))
295
- return {};
296
- const aliases = {};
297
- for (const family of MODEL_FAMILIES) {
298
- const model = value[family];
299
- if (typeof model !== 'string')
300
- continue;
301
- const pattern = new RegExp(`^claude-${family}-[A-Za-z0-9._-]+$`);
302
- if (pattern.test(model))
303
- aliases[family] = model;
304
- }
305
- return aliases;
306
- }
307
- function readPersistedAliasStore() {
308
- try {
309
- const value = atomicReadJson(modelAliasStorePath());
310
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
311
- return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
312
- }
313
- const raw = value;
314
- if (raw.$schema_version !== MODEL_ALIAS_STORE_VERSION || !raw.gateways || typeof raw.gateways !== 'object' || Array.isArray(raw.gateways)) {
315
- return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
316
- }
317
- const gateways = {};
318
- for (const [key, entry] of Object.entries(raw.gateways)) {
319
- if (!/^[a-f0-9]{64}$/.test(key) || !entry || typeof entry !== 'object' || Array.isArray(entry))
320
- continue;
321
- const record = entry;
322
- const aliases = sanitizeModelAliases(record.aliases);
323
- if (Object.keys(aliases).length === 0)
324
- continue;
325
- gateways[key] = {
326
- aliases,
327
- updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt) ? record.updatedAt : 0,
328
- };
329
- }
330
- return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways };
331
- }
332
- catch (error) {
333
- logger.warn(`[AgentRunner] Failed to load persisted model aliases: ${error instanceof Error ? error.message : String(error)}`);
334
- return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
335
- }
336
- }
337
- function loadPersistedModelAliases(baseUrl) {
338
- const cacheKey = normalizeModelGatewayUrl(baseUrl);
339
- const storePath = modelAliasStorePath();
340
- const gatewayKey = modelGatewayKey(cacheKey);
341
- const loadKey = `${storePath}\0${gatewayKey}`;
342
- if (loadedModelAliasFallbacks.has(loadKey))
343
- return;
344
- loadedModelAliasFallbacks.add(loadKey);
345
- const persisted = readPersistedAliasStore().gateways[gatewayKey];
346
- if (!persisted)
347
- return;
348
- modelAliasFallbacks.set(cacheKey, persisted.aliases);
349
- logger.info(`[AgentRunner] Loaded persisted model aliases: ${JSON.stringify(persisted.aliases)}`);
350
- }
351
- function persistModelAliases(baseUrl, aliases, updatedAt) {
352
- try {
353
- const store = readPersistedAliasStore();
354
- store.gateways[modelGatewayKey(baseUrl)] = { aliases, updatedAt };
355
- atomicWriteJson(modelAliasStorePath(), store);
356
- }
357
- catch (error) {
358
- logger.warn(`[AgentRunner] Failed to persist model aliases: ${error instanceof Error ? error.message : String(error)}`);
359
- }
360
- }
361
265
  // ── 网关价格缓存(从 /v1/models 的 pricing/effective_pricing 提取)─────────────
362
266
  // 与别名刷新同范式:按 baseUrl 缓存,1h TTL,stale-while-revalidate(缺失/过期时
363
267
  // fire-and-forget 触发刷新,本轮先用旧值或回退,不阻塞查询)。
@@ -432,94 +336,6 @@ async function refreshGatewayPricing(baseUrl, apiKey, requestHeaders) {
432
336
  gatewayPricingInFlight.delete(baseUrl);
433
337
  }
434
338
  }
435
- /** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高,minor 可省略) */
436
- function deriveAliasesFromModelIds(ids) {
437
- // 注入已验证可用的模型(如果列表中已有则去重无影响)
438
- const allIds = [...new Set([...ids, ...INJECTED_MODELS])];
439
- const best = {};
440
- for (const id of allIds) {
441
- const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?/);
442
- if (!m)
443
- continue;
444
- const [, family, majorStr, minorStr] = m;
445
- const major = parseInt(majorStr, 10);
446
- const minor = minorStr ? parseInt(minorStr, 10) : 0;
447
- const cur = best[family];
448
- if (!cur || major > cur.major || (major === cur.major && minor > cur.minor)) {
449
- best[family] = { id, major, minor };
450
- }
451
- }
452
- const aliases = {};
453
- for (const family of MODEL_FAMILIES) {
454
- const info = best[family];
455
- if (info)
456
- aliases[family] = info.id;
457
- }
458
- return aliases;
459
- }
460
- /** 异步刷新某 baseUrl 的别名缓存(失败静默,不抛出) */
461
- async function refreshModelAliases(baseUrl, apiKey, requestHeaders) {
462
- const cacheKey = normalizeModelGatewayUrl(baseUrl);
463
- if (modelAliasInFlight.has(cacheKey))
464
- return;
465
- modelAliasInFlight.add(cacheKey);
466
- loadPersistedModelAliases(cacheKey);
467
- try {
468
- const url = `${cacheKey}/models`;
469
- const controller = new AbortController();
470
- const timer = setTimeout(() => controller.abort(), 5000);
471
- const resp = await fetch(url, {
472
- signal: controller.signal,
473
- headers: {
474
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
475
- ...requestHeaders,
476
- },
477
- });
478
- clearTimeout(timer);
479
- if (!resp.ok)
480
- return;
481
- const json = await resp.json();
482
- const ids = Array.isArray(json?.data)
483
- ? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
484
- : [];
485
- const aliases = deriveAliasesFromModelIds(ids);
486
- if (ids.length > 0 || Object.keys(aliases).length > 0) {
487
- const updatedAt = Date.now();
488
- modelAliasCache.set(cacheKey, { aliases, ids, fetchedAt: updatedAt });
489
- if (Object.keys(aliases).length > 0) {
490
- const previousAliases = modelAliasFallbacks.get(cacheKey) || {};
491
- const latestAliases = { ...previousAliases, ...aliases };
492
- modelAliasFallbacks.set(cacheKey, latestAliases);
493
- persistModelAliases(cacheKey, latestAliases, updatedAt);
494
- }
495
- logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
496
- }
497
- }
498
- catch {
499
- // 网络/解析失败:保留该网关最近一次成功结果,不打断查询
500
- }
501
- finally {
502
- modelAliasInFlight.delete(cacheKey);
503
- }
504
- }
505
- /** 将短别名展开为完整 model ID,已是完整 ID 则原样返回 */
506
- function resolveModelAlias(model, baseUrl) {
507
- // 非短别名(已经是完整 ID)直接返回
508
- if (!MODEL_FAMILIES.includes(model))
509
- return model;
510
- const family = model;
511
- // 优先使用动态缓存
512
- if (baseUrl) {
513
- const cacheKey = normalizeModelGatewayUrl(baseUrl);
514
- loadPersistedModelAliases(cacheKey);
515
- const cached = modelAliasCache.get(cacheKey);
516
- if (cached && (Date.now() - cached.fetchedAt < MODEL_ALIAS_TTL_MS)) {
517
- return cached.aliases[family] || modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
518
- }
519
- return modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
520
- }
521
- return BOOTSTRAP_MODEL_ALIASES[family];
522
- }
523
339
  /**
524
340
  * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
525
341
  * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
@@ -531,10 +347,6 @@ function applyContextWindow(modelId) {
531
347
  return `${modelId}[1m]`;
532
348
  return modelId;
533
349
  }
534
- /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
535
- function resolveSdkModel(model, baseUrl) {
536
- return applyContextWindow(resolveModelAlias(model, baseUrl));
537
- }
538
350
  class MessageStream {
539
351
  queue = [];
540
352
  waiting = null;
@@ -719,6 +531,20 @@ export class AgentRunner {
719
531
  ...(evolcoreSessionId ? { EVOLCORE_SESSION_ID: evolcoreSessionId } : {}),
720
532
  ...(runtimeEnv ?? {}),
721
533
  }, { lockExecutableResolution: permissionMode === 'readonly' });
534
+ const trustedRootFullAccess = permissionMode === 'fullaccess'
535
+ && typeof process.getuid === 'function'
536
+ && process.getuid() === 0
537
+ && typeof evolcoreSessionId === 'string'
538
+ && hasTrustedFullAccessContext(this.permissionContexts.get(evolcoreSessionId));
539
+ // Claude Code refuses --dangerously-skip-permissions as root unless its
540
+ // caller asserts that the execution is intentionally sandbox-managed.
541
+ // EvolCore fullaccess is an explicit daemon-owner-authorized host run, so
542
+ // scope this assertion to that single trusted call. Never inherit a
543
+ // process-wide IS_SANDBOX into ordinary or untrusted Claude sessions.
544
+ if (trustedRootFullAccess)
545
+ env.IS_SANDBOX = '1';
546
+ else
547
+ delete env.IS_SANDBOX;
722
548
  const bubblewrapPath = resolveBubblewrapPath();
723
549
  return bubblewrapPath
724
550
  ? prependExecutableDirectory(env, bubblewrapPath)
@@ -766,31 +592,22 @@ export class AgentRunner {
766
592
  },
767
593
  };
768
594
  }
595
+ catalogConnection() {
596
+ return { baseUrl: this.baseUrl, apiKey: this.apiKey, headers: this.customHeaders, queryParams: this.customQueryParams };
597
+ }
598
+ getModelCatalog() {
599
+ return getRunnerCatalog('claude', this.agentAid, this.catalogConnection());
600
+ }
769
601
  async listModels() {
770
- if (this.baseUrl) {
771
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
772
- loadPersistedModelAliases(cacheKey);
773
- let cached = modelAliasCache.get(cacheKey);
774
- const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
775
- // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
776
- if (!cached) {
777
- await refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
778
- cached = modelAliasCache.get(cacheKey);
779
- }
780
- else if (stale) {
781
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
782
- }
783
- // 有缓存时返回网关 /models 的全量原始 ID
784
- if (cached && cached.ids.length > 0)
785
- return cached.ids;
786
- }
787
- // 无 baseUrl / 刷新超时或失败 → 回退短别名
788
- const fallback = this.baseUrl ? modelAliasFallbacks.get(normalizeModelGatewayUrl(this.baseUrl)) : undefined;
789
- return Object.values({ ...BOOTSTRAP_MODEL_ALIASES, ...fallback });
602
+ const catalog = await this.getModelCatalog();
603
+ assertCatalogSelectable(catalog);
604
+ return catalog.models.map(m => m.id);
790
605
  }
791
- /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
792
606
  resolveModelId(model) {
793
- return resolveModelAlias(model, this.baseUrl);
607
+ return resolveCatalogModel(peekRunnerCatalog('claude', this.agentAid, this.catalogConnection()), model);
608
+ }
609
+ async getAvailableEfforts(model) {
610
+ return catalogEfforts(await this.getModelCatalog(), model, 'claude');
794
611
  }
795
612
  setEffort(effort) {
796
613
  this.effort = effort;
@@ -1953,13 +1770,9 @@ export class AgentRunner {
1953
1770
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1954
1771
  // 同步用户级配置到内存
1955
1772
  this.syncFromUserSettings();
1956
- // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
1773
+ // Run preparation and model menus resolve aliases from the same snapshot.
1774
+ const runCatalog = await this.getModelCatalog();
1957
1775
  if (this.baseUrl) {
1958
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
1959
- const cached = modelAliasCache.get(cacheKey);
1960
- if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
1961
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
1962
- }
1963
1776
  // 顺带预热网关价格缓存(1h TTL),让本轮结束写库时已就绪
1964
1777
  const pricing = gatewayPricingCache.get(this.baseUrl);
1965
1778
  if (!pricing || (Date.now() - pricing.fetchedAt > GATEWAY_PRICING_TTL_MS)) {
@@ -2525,7 +2338,7 @@ export class AgentRunner {
2525
2338
  else {
2526
2339
  logger.info(`[AgentRunner] systemPromptAppend: none`);
2527
2340
  }
2528
- const sdkModel = resolveSdkModel(callModel, this.baseUrl);
2341
+ const sdkModel = applyContextWindow(resolveCatalogModel(runCatalog, callModel));
2529
2342
  const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
2530
2343
  const managedSettings = callPermissionMode === 'fullaccess'
2531
2344
  ? undefined
@@ -2932,16 +2745,19 @@ export class AgentRunner {
2932
2745
  }
2933
2746
  });
2934
2747
  }
2935
- runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
2936
- const callModel = modelOverride?.model || this.model;
2748
+ async runSessionCommand(prompt, agentSessionId, projectPath, modelOverride, useModel = true) {
2749
+ const callModel = modelOverride?.model || (useModel ? this.model : undefined);
2937
2750
  const callEffort = (modelOverride?.effortMode === 'model_default'
2938
2751
  ? undefined
2939
- : modelOverride?.effort ?? this.effort);
2752
+ : modelOverride?.effort ?? (useModel ? this.effort : undefined));
2753
+ const catalog = useModel ? await this.getModelCatalog() : undefined;
2754
+ if (catalog)
2755
+ assertCatalogSelectable(catalog);
2940
2756
  return query({
2941
2757
  prompt,
2942
2758
  options: {
2943
2759
  cwd: projectPath,
2944
- model: resolveSdkModel(callModel, this.baseUrl),
2760
+ ...(catalog && callModel ? { model: applyContextWindow(resolveCatalogModel(catalog, callModel)) } : {}),
2945
2761
  resume: agentSessionId,
2946
2762
  maxTurns: 1,
2947
2763
  tools: [],
@@ -2950,7 +2766,7 @@ export class AgentRunner {
2950
2766
  strictMcpConfig: true,
2951
2767
  settingSources: [],
2952
2768
  managedSettings: buildClaudeManagedLockdownSettings(),
2953
- ...(callEffort ? { effort: callEffort } : {}),
2769
+ ...(catalog && callEffort ? { effort: callEffort } : {}),
2954
2770
  permissionMode: this.toSdkPermissionMode(),
2955
2771
  env: this.getAgentEnv()
2956
2772
  }
@@ -2965,7 +2781,7 @@ export class AgentRunner {
2965
2781
  let lastEvent;
2966
2782
  try {
2967
2783
  logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
2968
- const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2784
+ const stream = await this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2969
2785
  this.activeStreams.set(sessionId, stream);
2970
2786
  try {
2971
2787
  let boundary;
@@ -3035,7 +2851,9 @@ export class AgentRunner {
3035
2851
  async clearSession(sessionId, agentSessionId, projectPath) {
3036
2852
  try {
3037
2853
  logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
3038
- const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
2854
+ // Clearing is a backend lifecycle operation. It does not depend on any
2855
+ // Session model or model configuration/selection.
2856
+ const stream = await this.runSessionCommand('/clear', agentSessionId, projectPath, undefined, false);
3039
2857
  this.activeStreams.set(sessionId, stream);
3040
2858
  try {
3041
2859
  let cleared = false;
@@ -1,16 +1,18 @@
1
1
  import { spawn } from 'child_process';
2
2
  import readline from 'readline';
3
- import { logger } from '../utils/logger.js';
4
- import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../core/permission/sandbox-runtime.js';
5
- import { resolveCodexLaunchCommand } from '../utils/codex-cli.js';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../../core/permission/sandbox-runtime.js';
5
+ import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
6
6
  import { createHash } from 'crypto';
7
- import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
7
+ import { sanitizeShellExecutionEnvironment } from '../../core/permission/shell-environment.js';
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
- import { ensureProcessManagedTempDir } from '../cli/task-context.js';
11
- import { getPackageRoot, resolvePaths } from '../paths.js';
12
- import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '../utils/codex-app-server-registry.js';
10
+ import { ensureProcessManagedTempDir } from '../../cli/task-context.js';
11
+ import { getPackageRoot, resolvePaths } from '../../paths.js';
12
+ import { killProcess } from '../../utils/cross-platform.js';
13
+ import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from './app-server-registry.js';
13
14
  const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
15
+ export const CODEX_THREAD_ACTIVE_WRITER = 'THREAD_ACTIVE_WRITER';
14
16
  function tomlProviderKey(value) {
15
17
  if (!value || /[\0\r\n]/.test(value)) {
16
18
  throw new Error(`Invalid Codex provider name: ${value}`);
@@ -114,6 +116,61 @@ export class CodexAppServerClient {
114
116
  ...(options?.selectedCapabilityRoots ? { selectedCapabilityRoots: options.selectedCapabilityRoots } : {}),
115
117
  });
116
118
  }
119
+ async threadArchive(threadId) {
120
+ await this.request('thread/archive', { threadId });
121
+ return true;
122
+ }
123
+ async threadUnarchive(threadId) {
124
+ return this.request('thread/unarchive', { threadId });
125
+ }
126
+ async threadUnsubscribe(threadId) {
127
+ return this.request('thread/unsubscribe', { threadId });
128
+ }
129
+ /**
130
+ * Drop this connection's subscription and wait until the server has released
131
+ * the in-memory thread. This is deliberately scoped to one thread; closing
132
+ * the app-server process would also disrupt unrelated concurrent sessions.
133
+ */
134
+ async threadUnsubscribeAndWait(threadId, timeoutMs = 2_000) {
135
+ let closed = false;
136
+ let resolveClosed;
137
+ const closedPromise = new Promise(resolve => {
138
+ resolveClosed = resolve;
139
+ });
140
+ const removeNotificationHandler = this.onNotification(notification => {
141
+ if (notification.method !== 'thread/closed')
142
+ return;
143
+ const params = (notification.params || {});
144
+ const closedThreadId = params.threadId ?? params.thread_id;
145
+ if (closedThreadId !== threadId || closed)
146
+ return;
147
+ closed = true;
148
+ resolveClosed();
149
+ });
150
+ let timer;
151
+ try {
152
+ const response = await this.threadUnsubscribe(threadId);
153
+ if (response.status !== 'unsubscribed')
154
+ return response;
155
+ const timeoutPromise = new Promise((_, reject) => {
156
+ timer = setTimeout(() => {
157
+ if (closed)
158
+ return;
159
+ const error = new Error(`thread/closed timed out for thread ${threadId}`);
160
+ error.code = 'CODEX_THREAD_CLOSE_TIMEOUT';
161
+ reject(error);
162
+ }, timeoutMs);
163
+ timer.unref?.();
164
+ });
165
+ await Promise.race([closedPromise, timeoutPromise]);
166
+ return response;
167
+ }
168
+ finally {
169
+ if (timer)
170
+ clearTimeout(timer);
171
+ removeNotificationHandler();
172
+ }
173
+ }
117
174
  async turnStart(threadId, input, options) {
118
175
  const sandboxPolicy = this.toTurnSandboxPolicy(options?.sandbox);
119
176
  return this.request('turn/start', {
@@ -126,6 +183,14 @@ export class CodexAppServerClient {
126
183
  ...(options?.effort ? { effort: options.effort } : {}),
127
184
  });
128
185
  }
186
+ async turnSteer(threadId, expectedTurnId, input, additionalContext) {
187
+ return this.request('turn/steer', {
188
+ threadId,
189
+ expectedTurnId,
190
+ input,
191
+ ...(additionalContext ? { additionalContext } : {}),
192
+ });
193
+ }
129
194
  toTurnSandboxPolicy(sandbox) {
130
195
  if (!sandbox)
131
196
  return undefined;
@@ -286,8 +351,8 @@ export class CodexAppServerClient {
286
351
  await this.request('turn/interrupt', { threadId, turnId });
287
352
  return true;
288
353
  }
289
- async modelList(includeHidden = false) {
290
- return this.request('model/list', { includeHidden });
354
+ async modelList(includeHidden = false, cursor) {
355
+ return this.request('model/list', { includeHidden, ...(cursor ? { cursor } : {}) });
291
356
  }
292
357
  async modelProviderCapabilitiesRead() {
293
358
  return this.request('modelProvider/capabilities/read', {});
@@ -332,7 +397,10 @@ export class CodexAppServerClient {
332
397
  }
333
398
  // 优雅关闭:先关闭 stdin,然后发送 SIGTERM,等待进程退出
334
399
  proc.stdin.end();
335
- proc.kill('SIGTERM');
400
+ if (process.platform === 'win32' && proc.pid)
401
+ killProcess(proc.pid, false);
402
+ else
403
+ proc.kill('SIGTERM');
336
404
  // 等待进程退出,最多等待 5 秒
337
405
  await new Promise((resolve) => {
338
406
  let exited = false;
@@ -341,7 +409,10 @@ export class CodexAppServerClient {
341
409
  return;
342
410
  logger.warn('[CodexAppServer] Process did not exit after SIGTERM, sending SIGKILL');
343
411
  try {
344
- proc.kill('SIGKILL');
412
+ if (process.platform === 'win32' && proc.pid)
413
+ killProcess(proc.pid, true);
414
+ else
415
+ proc.kill('SIGKILL');
345
416
  }
346
417
  catch { }
347
418
  resolve();
@@ -416,6 +487,7 @@ export class CodexAppServerClient {
416
487
  const managedRequirements = this.ensureManagedHookRuntime();
417
488
  const guardedCommand = buildHClassGuardCommand(command, commandArgs, guardRoot, {
418
489
  managedCodexRequirements: managedRequirements,
490
+ protectEvolcoreFilesystem: this.options.protectEvolcoreFilesystem !== false,
419
491
  });
420
492
  if (!guardedCommand) {
421
493
  throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
@@ -561,8 +633,8 @@ export class CodexAppServerClient {
561
633
  }
562
634
  const stat = fs.lstatSync(runtimeLockDir);
563
635
  if (!stat.isDirectory() || stat.isSymbolicLink()
564
- || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
565
- || (stat.mode & 0o077) !== 0) {
636
+ || (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
637
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
566
638
  throw new Error('runtime lock directory is not private');
567
639
  }
568
640
  env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
@@ -613,7 +685,10 @@ export class CodexAppServerClient {
613
685
  // second command or changing the hook's behavior.
614
686
  if (!command || !hook || /[\r\n;&|<>]/.test(command))
615
687
  return false;
616
- return command.replace(/["']+$/, '').endsWith(hook);
688
+ const commandWithoutSocket = command
689
+ .replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '')
690
+ .trim();
691
+ return commandWithoutSocket.replace(/["']+$/, '').endsWith(hook);
617
692
  };
618
693
  const managedHookLoaded = Array.isArray(preToolUse)
619
694
  && preToolUse.some(group => {
@@ -659,7 +734,7 @@ export class CodexAppServerClient {
659
734
  const headerEnvNames = Object.fromEntries(Object.keys(this.options.headers ?? {}).map(name => [name, codexHeaderEnvName(name)]));
660
735
  const hasHeaders = Object.keys(headerEnvNames).length > 0;
661
736
  const hasQueryParams = Object.keys(this.options.queryParams ?? {}).length > 0;
662
- if (hasHeaders || hasQueryParams) {
737
+ if (hasHeaders || hasQueryParams || this.options.baseUrl || this.options.provider) {
663
738
  const configuredProvider = this.options.provider?.trim();
664
739
  const useRuntimeProvider = !configuredProvider || BUILT_IN_CODEX_PROVIDER_IDS.has(configuredProvider.toLowerCase());
665
740
  const providerName = !useRuntimeProvider
@@ -765,8 +840,19 @@ export class CodexAppServerClient {
765
840
  this.pending.delete(messageId);
766
841
  if (message.error) {
767
842
  const detail = typeof message.error?.message === 'string' ? message.error.message : JSON.stringify(message.error);
768
- const stderr = this.stderrBuffer.length ? `\n${this.stderrBuffer.join('\n')}` : '';
769
- pending.reject(new Error(`${pending.method} failed: ${detail}${stderr}`));
843
+ // The CLI's one-time bubblewrap configuration warning is process-wide
844
+ // startup noise, not evidence about this RPC. Do not append it to an
845
+ // unrelated request failure such as thread/resume.
846
+ const stderrLines = this.stderrBuffer.filter(line => !/could not find bubblewrap on PATH/i.test(line));
847
+ const stderr = stderrLines.length ? `\n${stderrLines.join('\n')}` : '';
848
+ const error = new Error(`${pending.method} failed: ${detail}${stderr}`);
849
+ if (/no rollout found for thread id/i.test(detail)) {
850
+ error.code = 'CODEX_ROLLOUT_NOT_FOUND';
851
+ }
852
+ else if (/already has an active writer/i.test(detail)) {
853
+ error.code = CODEX_THREAD_ACTIVE_WRITER;
854
+ }
855
+ pending.reject(error);
770
856
  return;
771
857
  }
772
858
  pending.resolve(message.result ?? null);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { resolvePaths } from '../paths.js';
4
- import { getProcessStartTime } from './process-introspect.js';
3
+ import { resolvePaths } from '../../paths.js';
4
+ import { getProcessStartTime } from '../../utils/process-introspect.js';
5
5
  const CODEX_APP_SERVER_RECORD_PREFIX = 'codex-app-server-';
6
6
  const CODEX_APP_SERVER_RECORD_SUFFIX = '.json';
7
7
  function recordFilePath(pid) {
@@ -0,0 +1,70 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { CodexAppServerClient } from './app-server-client.js';
3
+ import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
4
+ import { parseModelList } from '../../core/model/catalog-service.js';
5
+ async function cliModels() {
6
+ const launch = resolveCodexLaunchCommand(['debug', 'models']);
7
+ if (!launch)
8
+ return null;
9
+ return new Promise(resolve => {
10
+ execFile(launch.command, launch.args, { timeout: 3000, windowsHide: true, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8' }, (error, stdout) => {
11
+ try {
12
+ if (error)
13
+ return resolve(null);
14
+ const json = JSON.parse(stdout);
15
+ if (!Array.isArray(json?.models))
16
+ return resolve(null);
17
+ resolve(parseModelList(json.models.filter((m) => m.visibility === 'list'))
18
+ .map(m => ({ ...m, verification: 'unverified' })));
19
+ }
20
+ catch {
21
+ resolve(null);
22
+ }
23
+ });
24
+ });
25
+ }
26
+ /** A catalog-only client uses the same transport configuration as inference. */
27
+ export async function codexNativeCatalog(context) {
28
+ if (context.baseagent !== 'codex')
29
+ return null;
30
+ const client = new CodexAppServerClient({
31
+ apiKey: context.apiKey ?? '', baseUrl: context.baseUrl, provider: context.provider,
32
+ headers: context.headers, queryParams: context.queryParams,
33
+ });
34
+ let timer;
35
+ let expired = false;
36
+ try {
37
+ return await Promise.race([
38
+ (async () => {
39
+ const models = [];
40
+ const seen = new Set();
41
+ let cursor;
42
+ do {
43
+ const page = await client.modelList(false, cursor);
44
+ if (expired)
45
+ throw new Error('Model catalog timeout');
46
+ if (!Array.isArray(page.data))
47
+ throw new Error('Invalid native model list');
48
+ models.push(...parseModelList(page.data.filter(m => !m.hidden)));
49
+ cursor = page.nextCursor || undefined;
50
+ if (cursor && (seen.has(cursor) || seen.size >= 100))
51
+ throw new Error('Invalid native model pagination');
52
+ if (cursor)
53
+ seen.add(cursor);
54
+ } while (cursor);
55
+ return [...new Map(models.map(m => [m.id, m])).values()];
56
+ })(),
57
+ new Promise((_, reject) => { timer = setTimeout(() => { expired = true; reject(new Error('Model catalog timeout')); }, 5000); }),
58
+ ]);
59
+ }
60
+ catch {
61
+ // The host CLI catalog has no guarantee of matching this Agent connection.
62
+ // It is useful metadata/suggestions and always remains unverified.
63
+ return await cliModels();
64
+ }
65
+ finally {
66
+ if (timer)
67
+ clearTimeout(timer);
68
+ await client.close();
69
+ }
70
+ }