evolcore 0.0.22 → 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 (75) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +24 -14
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/install-codex-managed-hooks.mjs +373 -95
  6. package/dist/agents/baseagent-availability.js +48 -0
  7. package/dist/agents/baseagent.js +1 -1
  8. package/dist/agents/claude-runner.js +30 -226
  9. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
  10. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  11. package/dist/agents/codex/catalog.js +70 -0
  12. package/dist/agents/codex/diagnostics.js +154 -0
  13. package/dist/agents/codex-runner.js +407 -139
  14. package/dist/agents/ecagent-runner.js +10 -31
  15. package/dist/agents/gemini-runner.js +2 -6
  16. package/dist/aun/aid/store.js +2 -0
  17. package/dist/aun/menu-client.js +272 -0
  18. package/dist/aun/msg/group.js +65 -4
  19. package/dist/aun/msg/index.js +1 -1
  20. package/dist/aun/msg/managed-operation.js +51 -4
  21. package/dist/aun/rpc/client.js +8 -1
  22. package/dist/channels/aun.js +31 -1
  23. package/dist/channels/wechat.js +81 -10
  24. package/dist/cli/aun-commands.js +48 -4
  25. package/dist/cli/command-log.js +21 -2
  26. package/dist/cli/daemon-commands.js +142 -45
  27. package/dist/cli/index.js +20 -0
  28. package/dist/cli/init.js +2 -10
  29. package/dist/cli/menu-client.js +95 -0
  30. package/dist/cli/model.js +23 -8
  31. package/dist/cli/task-context.js +12 -2
  32. package/dist/cli/trigger-command.js +9 -1
  33. package/dist/config/schema-migration.js +14 -2
  34. package/dist/core/auth/agent-delegation.js +10 -7
  35. package/dist/core/auth/authorization-audit.js +4 -0
  36. package/dist/core/auth/operation-catalog.js +4 -0
  37. package/dist/core/auth/remote-access-store.js +135 -0
  38. package/dist/core/command/command-handler.js +41 -5
  39. package/dist/core/command/daemon-diagnostics.js +448 -0
  40. package/dist/core/command/menu-catalog.js +3 -0
  41. package/dist/core/command/menu-handler.js +141 -33
  42. package/dist/core/command/menu-protocol.js +119 -0
  43. package/dist/core/command/slash-gate.js +3 -3
  44. package/dist/core/command/slash-handler.js +42 -22
  45. package/dist/core/message/message-queue.js +20 -15
  46. package/dist/core/message/response-engine.js +74 -73
  47. package/dist/core/model/catalog-service.js +308 -0
  48. package/dist/core/model/model-catalog.js +124 -334
  49. package/dist/core/model/model-diagnostics.js +4 -5
  50. package/dist/core/model/model-fallback.js +9 -16
  51. package/dist/core/model/model-permission.js +1 -1
  52. package/dist/core/permission/approval-gateway.js +16 -1
  53. package/dist/core/permission/protected-paths.js +63 -4
  54. package/dist/core/permission/sandbox-runtime.js +4 -2
  55. package/dist/core/permission/tool-policy.js +244 -25
  56. package/dist/core/runtime-lock.js +13 -8
  57. package/dist/index.js +40 -29
  58. package/dist/ipc.js +123 -81
  59. package/dist/trigger/manager.js +3 -1
  60. package/dist/trigger/parser.js +77 -13
  61. package/dist/trigger/patch.js +104 -1
  62. package/dist/utils/ecweb-supervisor.js +11 -2
  63. package/dist/utils/ecweb-utils.js +8 -26
  64. package/dist/utils/error-utils.js +7 -0
  65. package/dist/utils/npm-ops.js +47 -15
  66. package/kits/docs/evolcore/INDEX.md +1 -0
  67. package/kits/docs/evolcore/agent-create.md +13 -0
  68. package/kits/docs/evolcore/group-collaboration.md +7 -2
  69. package/kits/docs/evolcore/menu-client.md +34 -0
  70. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  71. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  72. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  73. package/kits/templates/roles/visitor.json +1 -0
  74. package/kits/templates/system-fragments/bootstrap.md +8 -1
  75. 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;
@@ -780,31 +592,22 @@ export class AgentRunner {
780
592
  },
781
593
  };
782
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
+ }
783
601
  async listModels() {
784
- if (this.baseUrl) {
785
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
786
- loadPersistedModelAliases(cacheKey);
787
- let cached = modelAliasCache.get(cacheKey);
788
- const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
789
- // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
790
- if (!cached) {
791
- await refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
792
- cached = modelAliasCache.get(cacheKey);
793
- }
794
- else if (stale) {
795
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
796
- }
797
- // 有缓存时返回网关 /models 的全量原始 ID
798
- if (cached && cached.ids.length > 0)
799
- return cached.ids;
800
- }
801
- // 无 baseUrl / 刷新超时或失败 → 回退短别名
802
- const fallback = this.baseUrl ? modelAliasFallbacks.get(normalizeModelGatewayUrl(this.baseUrl)) : undefined;
803
- return Object.values({ ...BOOTSTRAP_MODEL_ALIASES, ...fallback });
602
+ const catalog = await this.getModelCatalog();
603
+ assertCatalogSelectable(catalog);
604
+ return catalog.models.map(m => m.id);
804
605
  }
805
- /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
806
606
  resolveModelId(model) {
807
- 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');
808
611
  }
809
612
  setEffort(effort) {
810
613
  this.effort = effort;
@@ -1967,13 +1770,9 @@ export class AgentRunner {
1967
1770
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1968
1771
  // 同步用户级配置到内存
1969
1772
  this.syncFromUserSettings();
1970
- // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
1773
+ // Run preparation and model menus resolve aliases from the same snapshot.
1774
+ const runCatalog = await this.getModelCatalog();
1971
1775
  if (this.baseUrl) {
1972
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
1973
- const cached = modelAliasCache.get(cacheKey);
1974
- if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
1975
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
1976
- }
1977
1776
  // 顺带预热网关价格缓存(1h TTL),让本轮结束写库时已就绪
1978
1777
  const pricing = gatewayPricingCache.get(this.baseUrl);
1979
1778
  if (!pricing || (Date.now() - pricing.fetchedAt > GATEWAY_PRICING_TTL_MS)) {
@@ -2539,7 +2338,7 @@ export class AgentRunner {
2539
2338
  else {
2540
2339
  logger.info(`[AgentRunner] systemPromptAppend: none`);
2541
2340
  }
2542
- const sdkModel = resolveSdkModel(callModel, this.baseUrl);
2341
+ const sdkModel = applyContextWindow(resolveCatalogModel(runCatalog, callModel));
2543
2342
  const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
2544
2343
  const managedSettings = callPermissionMode === 'fullaccess'
2545
2344
  ? undefined
@@ -2946,16 +2745,19 @@ export class AgentRunner {
2946
2745
  }
2947
2746
  });
2948
2747
  }
2949
- runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
2950
- const callModel = modelOverride?.model || this.model;
2748
+ async runSessionCommand(prompt, agentSessionId, projectPath, modelOverride, useModel = true) {
2749
+ const callModel = modelOverride?.model || (useModel ? this.model : undefined);
2951
2750
  const callEffort = (modelOverride?.effortMode === 'model_default'
2952
2751
  ? undefined
2953
- : 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);
2954
2756
  return query({
2955
2757
  prompt,
2956
2758
  options: {
2957
2759
  cwd: projectPath,
2958
- model: resolveSdkModel(callModel, this.baseUrl),
2760
+ ...(catalog && callModel ? { model: applyContextWindow(resolveCatalogModel(catalog, callModel)) } : {}),
2959
2761
  resume: agentSessionId,
2960
2762
  maxTurns: 1,
2961
2763
  tools: [],
@@ -2964,7 +2766,7 @@ export class AgentRunner {
2964
2766
  strictMcpConfig: true,
2965
2767
  settingSources: [],
2966
2768
  managedSettings: buildClaudeManagedLockdownSettings(),
2967
- ...(callEffort ? { effort: callEffort } : {}),
2769
+ ...(catalog && callEffort ? { effort: callEffort } : {}),
2968
2770
  permissionMode: this.toSdkPermissionMode(),
2969
2771
  env: this.getAgentEnv()
2970
2772
  }
@@ -2979,7 +2781,7 @@ export class AgentRunner {
2979
2781
  let lastEvent;
2980
2782
  try {
2981
2783
  logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
2982
- const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2784
+ const stream = await this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2983
2785
  this.activeStreams.set(sessionId, stream);
2984
2786
  try {
2985
2787
  let boundary;
@@ -3049,7 +2851,9 @@ export class AgentRunner {
3049
2851
  async clearSession(sessionId, agentSessionId, projectPath) {
3050
2852
  try {
3051
2853
  logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
3052
- 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);
3053
2857
  this.activeStreams.set(sessionId, stream);
3054
2858
  try {
3055
2859
  let cleared = false;
@@ -1,17 +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 { killProcess } from '../utils/cross-platform.js';
13
- 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';
14
14
  const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
15
+ export const CODEX_THREAD_ACTIVE_WRITER = 'THREAD_ACTIVE_WRITER';
15
16
  function tomlProviderKey(value) {
16
17
  if (!value || /[\0\r\n]/.test(value)) {
17
18
  throw new Error(`Invalid Codex provider name: ${value}`);
@@ -115,6 +116,61 @@ export class CodexAppServerClient {
115
116
  ...(options?.selectedCapabilityRoots ? { selectedCapabilityRoots: options.selectedCapabilityRoots } : {}),
116
117
  });
117
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
+ }
118
174
  async turnStart(threadId, input, options) {
119
175
  const sandboxPolicy = this.toTurnSandboxPolicy(options?.sandbox);
120
176
  return this.request('turn/start', {
@@ -295,8 +351,8 @@ export class CodexAppServerClient {
295
351
  await this.request('turn/interrupt', { threadId, turnId });
296
352
  return true;
297
353
  }
298
- async modelList(includeHidden = false) {
299
- return this.request('model/list', { includeHidden });
354
+ async modelList(includeHidden = false, cursor) {
355
+ return this.request('model/list', { includeHidden, ...(cursor ? { cursor } : {}) });
300
356
  }
301
357
  async modelProviderCapabilitiesRead() {
302
358
  return this.request('modelProvider/capabilities/read', {});
@@ -431,6 +487,7 @@ export class CodexAppServerClient {
431
487
  const managedRequirements = this.ensureManagedHookRuntime();
432
488
  const guardedCommand = buildHClassGuardCommand(command, commandArgs, guardRoot, {
433
489
  managedCodexRequirements: managedRequirements,
490
+ protectEvolcoreFilesystem: this.options.protectEvolcoreFilesystem !== false,
434
491
  });
435
492
  if (!guardedCommand) {
436
493
  throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
@@ -576,8 +633,8 @@ export class CodexAppServerClient {
576
633
  }
577
634
  const stat = fs.lstatSync(runtimeLockDir);
578
635
  if (!stat.isDirectory() || stat.isSymbolicLink()
579
- || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
580
- || (stat.mode & 0o077) !== 0) {
636
+ || (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
637
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
581
638
  throw new Error('runtime lock directory is not private');
582
639
  }
583
640
  env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
@@ -677,7 +734,7 @@ export class CodexAppServerClient {
677
734
  const headerEnvNames = Object.fromEntries(Object.keys(this.options.headers ?? {}).map(name => [name, codexHeaderEnvName(name)]));
678
735
  const hasHeaders = Object.keys(headerEnvNames).length > 0;
679
736
  const hasQueryParams = Object.keys(this.options.queryParams ?? {}).length > 0;
680
- if (hasHeaders || hasQueryParams) {
737
+ if (hasHeaders || hasQueryParams || this.options.baseUrl || this.options.provider) {
681
738
  const configuredProvider = this.options.provider?.trim();
682
739
  const useRuntimeProvider = !configuredProvider || BUILT_IN_CODEX_PROVIDER_IDS.has(configuredProvider.toLowerCase());
683
740
  const providerName = !useRuntimeProvider
@@ -792,6 +849,9 @@ export class CodexAppServerClient {
792
849
  if (/no rollout found for thread id/i.test(detail)) {
793
850
  error.code = 'CODEX_ROLLOUT_NOT_FOUND';
794
851
  }
852
+ else if (/already has an active writer/i.test(detail)) {
853
+ error.code = CODEX_THREAD_ACTIVE_WRITER;
854
+ }
795
855
  pending.reject(error);
796
856
  return;
797
857
  }
@@ -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
+ }