@kyo-so/cli 0.9.0 → 0.10.0

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/dist/index.js CHANGED
@@ -169500,6 +169500,9 @@ function defineConfig(config) {
169500
169500
  // src/core/runReview.ts
169501
169501
  import { resolve as resolve6 } from "node:path";
169502
169502
 
169503
+ // src/config/schema.ts
169504
+ import { isAbsolute } from "node:path";
169505
+
169503
169506
  // node_modules/zod/v4/classic/external.js
169504
169507
  var exports_external = {};
169505
169508
  __export(exports_external, {
@@ -183777,7 +183780,10 @@ function date4(params) {
183777
183780
  // node_modules/zod/v4/classic/external.js
183778
183781
  config(en_default());
183779
183782
  // src/config/schema.ts
183780
- var agentSchema = exports_external.object({
183783
+ var CODEX_OPENROUTER_PROVIDER = "openrouter";
183784
+ var CODEX_DEFAULT_PROVIDER = "default";
183785
+ var CODEX_OPENROUTER_MODEL_REQUIRED_ISSUE = "codex_openrouter_model_required";
183786
+ var baseAgentSchema = exports_external.object({
183781
183787
  enabled: exports_external.boolean().default(true),
183782
183788
  type: exports_external.literal("acp").default("acp"),
183783
183789
  command: exports_external.string(),
@@ -183799,6 +183805,24 @@ var agentSchema = exports_external.object({
183799
183805
  envWhitelist: exports_external.array(exports_external.string())
183800
183806
  })
183801
183807
  });
183808
+ var codexAgentSchema = baseAgentSchema.extend({
183809
+ provider: exports_external.enum([CODEX_OPENROUTER_PROVIDER, CODEX_DEFAULT_PROVIDER]).optional(),
183810
+ allowProjectProvider: exports_external.array(exports_external.string().min(1).refine(isAbsolute, {
183811
+ message: "must contain only absolute project directory paths for exact matching"
183812
+ })).default([])
183813
+ }).superRefine((agent, context) => {
183814
+ if (agent.provider !== CODEX_OPENROUTER_PROVIDER || (agent.model?.trim().length ?? 0) > 0) {
183815
+ return;
183816
+ }
183817
+ context.addIssue({
183818
+ code: exports_external.ZodIssueCode.custom,
183819
+ path: ["model"],
183820
+ message: 'model must be a non-empty string when provider is "openrouter".',
183821
+ params: {
183822
+ kyosoIssue: CODEX_OPENROUTER_MODEL_REQUIRED_ISSUE
183823
+ }
183824
+ });
183825
+ });
183802
183826
  var kyosoConfigSchema = exports_external.object({
183803
183827
  entrypoints: exports_external.object({
183804
183828
  mcp: exports_external.boolean(),
@@ -183811,8 +183835,8 @@ var kyosoConfigSchema = exports_external.object({
183811
183835
  diffReview: exports_external.boolean()
183812
183836
  }),
183813
183837
  agents: exports_external.object({
183814
- codex: agentSchema,
183815
- claude: agentSchema
183838
+ codex: codexAgentSchema,
183839
+ claude: baseAgentSchema
183816
183840
  }),
183817
183841
  workspace: exports_external.object({
183818
183842
  mode: exports_external.literal("temp_snapshot"),
@@ -183865,7 +183889,7 @@ var kyosoConfigSchema = exports_external.object({
183865
183889
  })
183866
183890
  });
183867
183891
  function agentConfigLeafPaths(agent) {
183868
- return [
183892
+ const paths = [
183869
183893
  `agents.${agent}.enabled`,
183870
183894
  `agents.${agent}.type`,
183871
183895
  `agents.${agent}.command`,
@@ -183881,6 +183905,10 @@ function agentConfigLeafPaths(agent) {
183881
183905
  `agents.${agent}.auth.recommendedEnv`,
183882
183906
  `agents.${agent}.auth.envWhitelist`
183883
183907
  ];
183908
+ if (agent === "codex") {
183909
+ paths.push("agents.codex.provider", "agents.codex.allowProjectProvider");
183910
+ }
183911
+ return paths;
183884
183912
  }
183885
183913
  var kyosoConfigKnownLeafPaths = [
183886
183914
  "entrypoints.mcp",
@@ -183953,9 +183981,10 @@ var defaultConfig = {
183953
183981
  enabled: true,
183954
183982
  type: "acp",
183955
183983
  command: "npx",
183956
- args: ["-y", "@agentclientprotocol/codex-acp@1.1.0"],
183984
+ args: ["-y", "@agentclientprotocol/codex-acp@1.1.2"],
183957
183985
  role: "implementation_reviewer",
183958
183986
  timeoutMs: 120000,
183987
+ allowProjectProvider: [],
183959
183988
  env: {
183960
183989
  INITIAL_AGENT_MODE: "read-only",
183961
183990
  KYOSO_CHILD_AGENT: "1"
@@ -183977,7 +184006,7 @@ var defaultConfig = {
183977
184006
  enabled: true,
183978
184007
  type: "acp",
183979
184008
  command: "npx",
183980
- args: ["-y", "@agentclientprotocol/claude-agent-acp@0.57.0"],
184009
+ args: ["-y", "@agentclientprotocol/claude-agent-acp@0.58.1"],
183981
184010
  role: "architecture_security_reviewer",
183982
184011
  timeoutMs: 300000,
183983
184012
  env: {
@@ -184068,17 +184097,21 @@ var defaultConfig = {
184068
184097
  };
184069
184098
 
184070
184099
  // src/config/loadConfig.ts
184071
- import { access, readFile as readFile3 } from "node:fs/promises";
184100
+ import { access, readFile as readFile3, realpath } from "node:fs/promises";
184072
184101
  import { homedir as homedir2 } from "node:os";
184073
184102
  import { stderr, stdin } from "node:process";
184074
- import { extname as extname2, join as join2, resolve as resolve2 } from "node:path";
184103
+ import { dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
184075
184104
  import { createInterface } from "node:readline/promises";
184076
184105
 
184077
184106
  // src/config/projectScope.ts
184078
184107
  var PROJECT_GLOBAL_ONLY_MESSAGE = "Move global-only settings to the user global config:";
184108
+ var PROJECT_GLOBAL_ONLY_REASONS = {
184109
+ "agents.codex.allowProjectProvider": "must be a user-global exact project-directory allowlist"
184110
+ };
184079
184111
  var kyosoConfigOverridePaths = [
184080
184112
  "agents.codex.enabled",
184081
184113
  "agents.codex.model",
184114
+ "agents.codex.provider",
184082
184115
  "agents.codex.effort",
184083
184116
  "agents.codex.role",
184084
184117
  "agents.codex.timeoutMs",
@@ -184118,6 +184151,11 @@ function collectProjectScopeViolations(config2) {
184118
184151
  const violations = [];
184119
184152
  for (const leaf of leaves) {
184120
184153
  const path = leaf.path.join(".");
184154
+ const globalOnlyReason = PROJECT_GLOBAL_ONLY_REASONS[path];
184155
+ if (globalOnlyReason) {
184156
+ violations.push({ path, reason: globalOnlyReason });
184157
+ continue;
184158
+ }
184121
184159
  if (!isAllowedProjectPath(leaf.path)) {
184122
184160
  violations.push({ path });
184123
184161
  continue;
@@ -184278,7 +184316,7 @@ function sanitizeText(value) {
184278
184316
  return SENSITIVE_TEXT_PATTERNS.reduce((text, pattern) => text.replace(pattern, REDACTION), value);
184279
184317
  }
184280
184318
  function sanitizeTextForDisplay(value, maxChars = 240) {
184281
- const compact = sanitizeText(value).replace(/\s+/g, " ").trim();
184319
+ const compact = sanitizeText(value).replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "").replace(/\s+/g, " ").trim();
184282
184320
  if (compact.length <= maxChars)
184283
184321
  return compact;
184284
184322
  return `${compact.slice(0, Math.max(0, maxChars - 3))}...`;
@@ -185322,6 +185360,25 @@ function isMissingPathError(error51) {
185322
185360
  }
185323
185361
 
185324
185362
  // src/config/loadConfig.ts
185363
+ var configValidationContexts = new WeakMap;
185364
+ class ProjectOpenRouterAuthorizationError extends Error {
185365
+ code = "PROJECT_OPENROUTER_AUTHORIZATION_REQUIRED";
185366
+ projectPath;
185367
+ projectDirectory;
185368
+ layer;
185369
+ globalConfigPath;
185370
+ constructor(input) {
185371
+ const projectPath = sanitizeWarningText(input.projectPath);
185372
+ const projectDirectory = sanitizeWarningText(input.projectDirectory);
185373
+ const globalConfigPath = sanitizeWarningText(input.globalConfigPath);
185374
+ super(`Project config ${projectPath} changes Codex OpenRouter routing, but its directory ${projectDirectory} is not in the user-global allowlist. Add ${JSON.stringify(projectDirectory)} to agents.codex.allowProjectProvider in ${globalConfigPath} to permit project-level external provider routing.`);
185375
+ this.name = "ProjectOpenRouterAuthorizationError";
185376
+ this.projectPath = projectPath;
185377
+ this.projectDirectory = projectDirectory;
185378
+ this.layer = input.layer;
185379
+ this.globalConfigPath = globalConfigPath;
185380
+ }
185381
+ }
185325
185382
  var KNOWN_GLOBAL_CONFIG_LEAF_PATHS = new Set(kyosoConfigKnownLeafPaths);
185326
185383
  var GLOBAL_CONFIG_RECORD_PREFIXES = kyosoConfigRecordPrefixes.map((path) => path.split("."));
185327
185384
  var SECURITY_SENSITIVE_GLOBAL_PREFIXES = kyosoConfigSecuritySensitivePrefixes.map((path) => path.split("."));
@@ -185349,11 +185406,12 @@ async function loadConfig(options = {}) {
185349
185406
  }
185350
185407
  if (options.configPath) {
185351
185408
  const explicitConfigPath = resolve2(cwd, options.configPath);
185352
- if (!await exists(explicitConfigPath)) {
185409
+ const projectConfig = await resolveProjectConfigIdentity(explicitConfigPath);
185410
+ if (!projectConfig) {
185353
185411
  throw new Error(`Config file not found: ${explicitConfigPath} (from --config)`);
185354
185412
  }
185355
185413
  const loaded = await loadProjectConfig({
185356
- configPath: explicitConfigPath,
185414
+ projectConfig,
185357
185415
  baseConfig: mergedConfig,
185358
185416
  globalConfigPath,
185359
185417
  options
@@ -185367,19 +185425,29 @@ async function loadConfig(options = {}) {
185367
185425
  } else {
185368
185426
  const projectTomlPath = resolve2(cwd, "kyoso.toml");
185369
185427
  const projectTsPath = resolve2(cwd, "kyoso.config.ts");
185370
- const hasProjectToml = await exists(projectTomlPath);
185371
- const hasProjectTs = await exists(projectTsPath);
185372
- if (hasProjectToml) {
185373
- mergedConfig = mergeProjectTomlConfig(mergedConfig, await loadTomlConfigFile(projectTomlPath), { projectPath: projectTomlPath, globalConfigPath });
185374
- configPath = projectTomlPath;
185375
- sources.push({ path: projectTomlPath, layer: "project_toml" });
185376
- if (hasProjectTs) {
185428
+ const projectToml = await resolveProjectConfigIdentity(projectTomlPath);
185429
+ const projectTs = await resolveProjectConfigIdentity(projectTsPath);
185430
+ if (projectToml) {
185431
+ const loaded = await loadProjectConfig({
185432
+ projectConfig: projectToml,
185433
+ baseConfig: mergedConfig,
185434
+ globalConfigPath,
185435
+ options
185436
+ });
185437
+ mergedConfig = loaded.mergedConfig;
185438
+ configPath = loaded.configPath;
185439
+ configHash = loaded.configHash;
185440
+ configTrustStatus = loaded.configTrustStatus;
185441
+ sources.push(loaded.source);
185442
+ warnings.push(...loaded.warnings);
185443
+ if (projectTs) {
185377
185444
  warnings.push(`kyoso.config.ts was ignored because kyoso.toml takes precedence: ${projectTsPath}`);
185378
185445
  }
185379
- } else if (hasProjectTs) {
185446
+ } else if (projectTs) {
185380
185447
  const loaded = await loadProjectTsConfig({
185381
- configPath: projectTsPath,
185448
+ projectConfig: projectTs,
185382
185449
  baseConfig: mergedConfig,
185450
+ globalConfigPath,
185383
185451
  options
185384
185452
  });
185385
185453
  mergedConfig = loaded.mergedConfig;
@@ -185391,9 +185459,17 @@ async function loadConfig(options = {}) {
185391
185459
  }
185392
185460
  }
185393
185461
  }
185394
- const parsed = kyosoConfigSchema.parse(mergedConfig);
185462
+ const parsed = kyosoConfigSchema.safeParse(mergedConfig);
185463
+ if (!parsed.success) {
185464
+ const source = sources.at(-1);
185465
+ configValidationContexts.set(parsed.error, {
185466
+ source,
185467
+ projectTsExecuted: source?.layer === "project_ts" && isTrustedProjectConfigExecution(configTrustStatus)
185468
+ });
185469
+ throw parsed.error;
185470
+ }
185395
185471
  return {
185396
- config: parsed,
185472
+ config: parsed.data,
185397
185473
  configPath,
185398
185474
  configHash,
185399
185475
  configTrustStatus,
@@ -185441,62 +185517,170 @@ function pathStartsWith(path, prefix) {
185441
185517
  return path.length >= prefix.length && prefix.every((part, index) => path[index] === part);
185442
185518
  }
185443
185519
  async function loadProjectConfig(input) {
185444
- const extension = extname2(input.configPath);
185520
+ const { canonicalDirectory, canonicalPath, requestedPath } = input.projectConfig;
185521
+ const extension = extname2(requestedPath);
185445
185522
  if (extension === ".toml") {
185523
+ const projectTomlConfig = await loadTomlConfigFile(canonicalPath);
185524
+ const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(projectTomlConfig, input.baseConfig);
185525
+ await assertProjectOpenRouterAuthorization({
185526
+ projectConfig: projectTomlConfig,
185527
+ projectPath: requestedPath,
185528
+ projectDirectory: canonicalDirectory,
185529
+ layer: "project_toml",
185530
+ baseConfig: input.baseConfig,
185531
+ globalConfigPath: input.globalConfigPath
185532
+ });
185533
+ const projectMergedConfig = mergeProjectTomlConfig(input.baseConfig, projectTomlConfig, {
185534
+ projectPath: requestedPath,
185535
+ globalConfigPath: input.globalConfigPath
185536
+ });
185446
185537
  return {
185447
- mergedConfig: mergeProjectTomlConfig(input.baseConfig, await loadTomlConfigFile(input.configPath), {
185448
- projectPath: input.configPath,
185449
- globalConfigPath: input.globalConfigPath
185450
- }),
185451
- configPath: input.configPath,
185538
+ mergedConfig: applyProjectCodexProviderReset(input.baseConfig, projectTomlConfig, projectMergedConfig),
185539
+ configPath: requestedPath,
185452
185540
  configTrustStatus: "not_found",
185453
- source: { path: input.configPath, layer: "project_toml" },
185454
- warnings: []
185541
+ source: { path: requestedPath, layer: "project_toml" },
185542
+ warnings: projectChangesOpenRouterRoute ? [openRouterProjectConfigWarning(requestedPath)] : []
185455
185543
  };
185456
185544
  }
185457
185545
  if (extension === ".ts") {
185458
185546
  return await loadProjectTsConfig(input);
185459
185547
  }
185460
- throw new Error(`Unsupported config file extension for ${input.configPath}. Expected .toml or .ts.`);
185548
+ throw new Error(`Unsupported config file extension for ${requestedPath}. Expected .toml or .ts.`);
185549
+ }
185550
+ function projectConfigSelectsOpenRouter(config2) {
185551
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_OPENROUTER_PROVIDER);
185552
+ }
185553
+ function projectConfigSelectsDefaultProvider(config2) {
185554
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_DEFAULT_PROVIDER);
185555
+ }
185556
+ function projectConfigSuppliesCodexModel(config2) {
185557
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.model");
185558
+ }
185559
+ function projectConfigChangesOpenRouterRoute(projectConfig, baseConfig) {
185560
+ return projectConfigSelectsOpenRouter(projectConfig) || configSelectsOpenRouter(baseConfig) && projectConfigSuppliesCodexModel(projectConfig) && !projectConfigSelectsDefaultProvider(projectConfig);
185561
+ }
185562
+ function configSelectsOpenRouter(config2) {
185563
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_OPENROUTER_PROVIDER);
185564
+ }
185565
+ function applyProjectCodexProviderReset(baseConfig, projectConfig, mergedConfig) {
185566
+ if (!projectConfigSelectsDefaultProvider(projectConfig) || projectConfigSuppliesCodexModel(projectConfig) || !configSelectsOpenRouter(baseConfig) || !isRecord3(mergedConfig) || !isRecord3(mergedConfig.agents) || !isRecord3(mergedConfig.agents.codex)) {
185567
+ return mergedConfig;
185568
+ }
185569
+ const codexWithoutInheritedModel = Object.fromEntries(Object.entries(mergedConfig.agents.codex).filter(([key]) => key !== "model"));
185570
+ return {
185571
+ ...mergedConfig,
185572
+ agents: {
185573
+ ...mergedConfig.agents,
185574
+ codex: codexWithoutInheritedModel
185575
+ }
185576
+ };
185577
+ }
185578
+ function openRouterProjectConfigWarning(configPath) {
185579
+ return `Project config ${sanitizeWarningText(configPath)} changes Codex OpenRouter routing under user-global authorization; it can route Codex review content through OpenRouter.`;
185580
+ }
185581
+ async function assertProjectOpenRouterAuthorization(input) {
185582
+ if (!projectConfigChangesOpenRouterRoute(input.projectConfig, input.baseConfig)) {
185583
+ return;
185584
+ }
185585
+ if (await projectProviderIsAuthorized(input.baseConfig, input.projectDirectory)) {
185586
+ return;
185587
+ }
185588
+ throw new ProjectOpenRouterAuthorizationError({
185589
+ projectPath: input.projectPath,
185590
+ projectDirectory: input.projectDirectory,
185591
+ layer: input.layer,
185592
+ globalConfigPath: input.globalConfigPath
185593
+ });
185594
+ }
185595
+ async function projectProviderIsAuthorized(config2, projectDirectory) {
185596
+ if (!isRecord3(config2))
185597
+ return false;
185598
+ const agents = config2.agents;
185599
+ if (!isRecord3(agents))
185600
+ return false;
185601
+ const codex = agents.codex;
185602
+ if (!isRecord3(codex) || !Array.isArray(codex.allowProjectProvider)) {
185603
+ return false;
185604
+ }
185605
+ for (const directory of codex.allowProjectProvider) {
185606
+ if (typeof directory !== "string" || !isAbsolute2(directory))
185607
+ continue;
185608
+ const allowedDirectory = await existingRealpath(directory);
185609
+ if (allowedDirectory === projectDirectory)
185610
+ return true;
185611
+ }
185612
+ return false;
185613
+ }
185614
+ async function resolveProjectConfigIdentity(requestedPath) {
185615
+ const canonicalPath = await existingRealpath(requestedPath);
185616
+ return canonicalPath === undefined ? undefined : {
185617
+ requestedPath,
185618
+ canonicalPath,
185619
+ canonicalDirectory: dirname3(canonicalPath)
185620
+ };
185621
+ }
185622
+ async function existingRealpath(path) {
185623
+ try {
185624
+ return await realpath(path);
185625
+ } catch {
185626
+ return;
185627
+ }
185461
185628
  }
185462
185629
  async function loadProjectTsConfig(input) {
185630
+ const { canonicalDirectory, canonicalPath, requestedPath } = input.projectConfig;
185463
185631
  const warnings = [
185464
185632
  'kyoso.config.ts is deprecated; migrate to kyoso.toml (see README "Configuration")'
185465
185633
  ];
185466
- const source = await readFile3(input.configPath, "utf8");
185634
+ const source = await readFile3(canonicalPath, "utf8");
185467
185635
  const configHash = hashConfigSource(source);
185468
185636
  const trustStorePath = input.options.trustStorePath ?? defaultTrustedConfigStorePath(input.options.env);
185469
- const trusted = await isTrustedConfig(trustStorePath, input.configPath, configHash);
185637
+ const trusted = await isTrustedConfig(trustStorePath, canonicalPath, configHash);
185470
185638
  const trustDecision = await resolveTrustDecision({
185471
- configPath: input.configPath,
185639
+ configPath: requestedPath,
185472
185640
  configHash,
185473
185641
  trusted,
185474
185642
  options: input.options
185475
185643
  });
185476
185644
  if (trustDecision.execute) {
185477
- const userConfig = await loadUserConfig(input.configPath, source);
185645
+ const userConfig = await loadUserConfig(canonicalPath, source);
185646
+ const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(userConfig, input.baseConfig);
185647
+ await assertProjectOpenRouterAuthorization({
185648
+ projectConfig: userConfig,
185649
+ projectPath: requestedPath,
185650
+ projectDirectory: canonicalDirectory,
185651
+ layer: "project_ts",
185652
+ baseConfig: input.baseConfig,
185653
+ globalConfigPath: input.globalConfigPath
185654
+ });
185478
185655
  if (trustDecision.shouldPersist) {
185479
- await trustConfig(trustStorePath, input.configPath, configHash);
185656
+ await trustConfig(trustStorePath, canonicalPath, configHash);
185657
+ }
185658
+ const projectMergedConfig = deepMerge2(input.baseConfig, userConfig);
185659
+ if (projectChangesOpenRouterRoute) {
185660
+ warnings.push(openRouterProjectConfigWarning(requestedPath));
185480
185661
  }
185481
185662
  return {
185482
- mergedConfig: deepMerge2(input.baseConfig, userConfig),
185483
- configPath: input.configPath,
185663
+ mergedConfig: applyProjectCodexProviderReset(input.baseConfig, userConfig, projectMergedConfig),
185664
+ configPath: requestedPath,
185484
185665
  configHash,
185485
185666
  configTrustStatus: trustDecision.status,
185486
- source: { path: input.configPath, layer: "project_ts" },
185667
+ source: { path: requestedPath, layer: "project_ts" },
185487
185668
  warnings
185488
185669
  };
185489
185670
  }
185490
- warnings.push(`untrusted config was not executed: ${input.configPath}; run \`kyoso doctor --trust-config\` or pass \`--trust-config\` once to trust it`);
185671
+ warnings.push(`untrusted config was not executed: ${requestedPath}; run \`kyoso doctor --trust-config\` or pass \`--trust-config\` once to trust it`);
185491
185672
  return {
185492
185673
  mergedConfig: input.baseConfig,
185493
- configPath: input.configPath,
185674
+ configPath: requestedPath,
185494
185675
  configHash,
185495
185676
  configTrustStatus: trustDecision.status,
185496
- source: { path: input.configPath, layer: "project_ts" },
185677
+ source: { path: requestedPath, layer: "project_ts" },
185497
185678
  warnings
185498
185679
  };
185499
185680
  }
185681
+ function isTrustedProjectConfigExecution(status) {
185682
+ return status === "trusted" || status === "trusted_by_flag" || status === "trusted_interactively";
185683
+ }
185500
185684
  async function loadUserConfig(configPath, source) {
185501
185685
  try {
185502
185686
  const loaded = await loadConfigModule(configPath, source);
@@ -185575,6 +185759,8 @@ function applyConfigOverrides(config2, assignments) {
185575
185759
  for (const override of overrides) {
185576
185760
  writePath2(overridden, override.path, parseConfigOverrideValue(override.value, readPath2(baseConfig, override.path)));
185577
185761
  }
185762
+ clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides);
185763
+ assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, overrides);
185578
185764
  const parsed = kyosoConfigSchema.safeParse(overridden);
185579
185765
  if (parsed.success)
185580
185766
  return parsed.data;
@@ -185583,6 +185769,29 @@ function applyConfigOverrides(config2, assignments) {
185583
185769
  const assignment = findAssignmentForPath(overrides, issuePath) ?? assignments.at(-1) ?? "";
185584
185770
  throw new Error(`Invalid --set value ${JSON.stringify(assignment)}: ${issuePath}: ${issue2?.message ?? "config validation failed"}.`);
185585
185771
  }
185772
+ function assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, overrides) {
185773
+ const providerPath = ["agents", "codex", "provider"];
185774
+ const modelPath = ["agents", "codex", "model"];
185775
+ const selectsOpenRouter = readPath2(overridden, providerPath) === CODEX_OPENROUTER_PROVIDER;
185776
+ const alreadySelected = readPath2(baseConfig, providerPath) === CODEX_OPENROUTER_PROVIDER;
185777
+ const suppliesModel = overrides.some((override) => override.path.join(".") === modelPath.join("."));
185778
+ if (!selectsOpenRouter || alreadySelected || suppliesModel)
185779
+ return;
185780
+ const assignment = findAssignmentForPath(overrides, providerPath.join(".")) ?? "agents.codex.provider=openrouter";
185781
+ throw new Error(`Invalid --set value ${JSON.stringify(assignment)}: selecting agents.codex.provider=openrouter requires agents.codex.model in the same --set invocation.`);
185782
+ }
185783
+ function clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides) {
185784
+ const providerPath = ["agents", "codex", "provider"];
185785
+ const modelPath = ["agents", "codex", "model"];
185786
+ const selectsDefaultProvider = overrides.some((override) => override.path.join(".") === providerPath.join(".") && override.value === CODEX_DEFAULT_PROVIDER);
185787
+ const suppliesModel = overrides.some((override) => override.path.join(".") === modelPath.join("."));
185788
+ if (!selectsDefaultProvider || suppliesModel || readPath2(baseConfig, providerPath) !== CODEX_OPENROUTER_PROVIDER || readPath2(overridden, providerPath) !== CODEX_DEFAULT_PROVIDER) {
185789
+ return;
185790
+ }
185791
+ const codex = readPath2(overridden, ["agents", "codex"]);
185792
+ if (isRecord4(codex))
185793
+ delete codex.model;
185794
+ }
185586
185795
  function findAssignmentForPath(overrides, path) {
185587
185796
  for (let index = overrides.length - 1;index >= 0; index -= 1) {
185588
185797
  const override = overrides[index];
@@ -185650,8 +185859,8 @@ function isRecord4(value) {
185650
185859
 
185651
185860
  // src/acp/AcpAgentProcess.ts
185652
185861
  import { spawn } from "node:child_process";
185653
- import { readFile as readFile4, realpath } from "node:fs/promises";
185654
- import { isAbsolute, relative, resolve as resolve3 } from "node:path";
185862
+ import { readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
185863
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve3 } from "node:path";
185655
185864
  import { Readable, Writable } from "node:stream";
185656
185865
 
185657
185866
  // node_modules/@agentclientprotocol/sdk/dist/schema/index.js
@@ -189583,6 +189792,7 @@ var legacyClientNotificationMethods = new Set([
189583
189792
  ]);
189584
189793
 
189585
189794
  // src/utils/env.ts
189795
+ import { stderr as stderr2 } from "node:process";
189586
189796
  var MINIMAL_ENV_KEYS = [
189587
189797
  "PATH",
189588
189798
  "HOME",
@@ -189596,29 +189806,110 @@ var MINIMAL_ENV_KEYS = [
189596
189806
  "USERNAME",
189597
189807
  "SystemRoot"
189598
189808
  ];
189809
+ var CREDENTIAL_ENV_KEYS = new Set([
189810
+ "ANTHROPIC_API_KEY",
189811
+ "CLAUDE_CODE_OAUTH_TOKEN",
189812
+ "CODEX_ACCESS_TOKEN",
189813
+ "CODEX_API_KEY",
189814
+ "OPENAI_API_KEY",
189815
+ "OPENROUTER_API_KEY"
189816
+ ]);
189817
+ var CREDENTIAL_LIKE_ENV_KEY_PATTERN = /(?:^|_)(?:KEY|TOKEN|SECRET|PASSWORD)$/i;
189818
+ var UNEXPANDED_ENV_PLACEHOLDER_PATTERN = /^\s*(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%)\s*$/;
189819
+ var OPENROUTER_EXCLUDED_CREDENTIAL_ENV_KEYS = [
189820
+ "OPENAI_API_KEY",
189821
+ "CODEX_API_KEY",
189822
+ "CODEX_ACCESS_TOKEN"
189823
+ ];
189824
+ var OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
189825
+ var KYOSO_OPENROUTER_PROVIDER_ID = "kyoso-openrouter";
189826
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
189827
+ var OPENROUTER_PROVIDER_PRESET = {
189828
+ name: "OpenRouter",
189829
+ base_url: OPENROUTER_BASE_URL,
189830
+ env_key: OPENROUTER_API_KEY_ENV,
189831
+ wire_api: "responses",
189832
+ requires_openai_auth: false
189833
+ };
189834
+
189835
+ class ChildEnvPreflightError extends Error {
189836
+ code;
189837
+ constructor(code, message) {
189838
+ super(message);
189839
+ this.code = code;
189840
+ this.name = "ChildEnvPreflightError";
189841
+ }
189842
+ }
189599
189843
  function buildChildEnv(parentEnv, whitelist, explicit, options = {}) {
189600
189844
  if (!parentEnv.PATH) {
189601
189845
  throw new Error("PATH is required to launch ACP child agents.");
189602
189846
  }
189603
189847
  const env = {};
189848
+ const warnedCredentialPlaceholderKeys = new Set;
189849
+ const onCredentialPlaceholderDiscarded = (key) => {
189850
+ if (warnedCredentialPlaceholderKeys.has(key))
189851
+ return;
189852
+ warnedCredentialPlaceholderKeys.add(key);
189853
+ (options.onCredentialPlaceholderDiscarded ?? warnCredentialPlaceholderDiscarded)(key);
189854
+ };
189855
+ const openRouterSelected = options.agent === "codex" && options.provider === CODEX_OPENROUTER_PROVIDER;
189604
189856
  for (const key of MINIMAL_ENV_KEYS) {
189605
189857
  if (parentEnv[key])
189606
189858
  env[key] = parentEnv[key];
189607
189859
  }
189608
189860
  for (const key of whitelist) {
189609
- if (parentEnv[key])
189861
+ if (key === OPENROUTER_API_KEY_ENV && !openRouterSelected)
189862
+ continue;
189863
+ if (parentEnv[key] && canCopyEnvValue(key, parentEnv[key], onCredentialPlaceholderDiscarded)) {
189610
189864
  env[key] = parentEnv[key];
189865
+ }
189611
189866
  }
189612
189867
  for (const [key, value] of Object.entries(explicit)) {
189613
- env[key] = value;
189868
+ if (key === OPENROUTER_API_KEY_ENV && !openRouterSelected) {
189869
+ if (value.trim().length > 0) {
189870
+ (options.onOpenRouterCredentialWithheld ?? warnOpenRouterCredentialWithheld)(key);
189871
+ }
189872
+ continue;
189873
+ }
189874
+ if (canCopyEnvValue(key, value, onCredentialPlaceholderDiscarded)) {
189875
+ env[key] = value;
189876
+ }
189877
+ }
189878
+ if (openRouterSelected) {
189879
+ discardOpenRouterExcludedCredentials(env);
189880
+ applyOpenRouterConfig(env, parentEnv, options.model, onCredentialPlaceholderDiscarded, options.onOpenRouterProvidersDiscarded ?? warnOpenRouterProvidersDiscarded);
189881
+ } else {
189882
+ applyModelConfig(env, options.agent, options.model);
189614
189883
  }
189615
- applyModelConfig(env, options.agent, options.model);
189616
189884
  env.KYOSO_CHILD_AGENT = "1";
189617
189885
  if (options.agent === "claude") {
189618
189886
  applyClaudeAuthPreference(env, options.preferApiKey === true);
189619
189887
  }
189620
189888
  return env;
189621
189889
  }
189890
+ function canCopyEnvValue(key, value, onCredentialPlaceholderDiscarded) {
189891
+ if (!isUnexpandedCredentialEnvValue(key, value))
189892
+ return true;
189893
+ (onCredentialPlaceholderDiscarded ?? warnCredentialPlaceholderDiscarded)(key);
189894
+ return false;
189895
+ }
189896
+ function warnCredentialPlaceholderDiscarded(key) {
189897
+ stderr2.write(`kyoso: ignored an unexpanded credential placeholder for ${key}; ensure the client expands it before starting Kyoso.
189898
+ `);
189899
+ }
189900
+ function warnOpenRouterCredentialWithheld(key) {
189901
+ stderr2.write(`kyoso: ignored explicit ${key} because the OpenRouter provider is not selected for this child.
189902
+ `);
189903
+ }
189904
+ function warnOpenRouterProvidersDiscarded(count) {
189905
+ stderr2.write(`kyoso: discarded foreign CODEX_CONFIG.model_providers entries: ${count}; provider IDs and configuration values were not displayed.
189906
+ `);
189907
+ }
189908
+ function discardOpenRouterExcludedCredentials(env) {
189909
+ for (const key of OPENROUTER_EXCLUDED_CREDENTIAL_ENV_KEYS) {
189910
+ delete env[key];
189911
+ }
189912
+ }
189622
189913
  function applyModelConfig(env, agent, model) {
189623
189914
  if (!model)
189624
189915
  return;
@@ -189630,7 +189921,67 @@ function applyModelConfig(env, agent, model) {
189630
189921
  env.CODEX_CONFIG = JSON.stringify({ model });
189631
189922
  }
189632
189923
  }
189924
+ function applyOpenRouterConfig(env, parentEnv, model, onCredentialPlaceholderDiscarded, onOpenRouterProvidersDiscarded) {
189925
+ const configuredModel = model?.trim();
189926
+ if (!configuredModel) {
189927
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", 'agents.codex.provider="openrouter" requires a non-empty agents.codex.model.');
189928
+ }
189929
+ if (!hasEnv(env, OPENROUTER_API_KEY_ENV)) {
189930
+ const parentKey = nonEmptyEnv(parentEnv, OPENROUTER_API_KEY_ENV);
189931
+ if (parentKey)
189932
+ env[OPENROUTER_API_KEY_ENV] = parentKey;
189933
+ else if (isUnexpandedCredentialEnvValue(OPENROUTER_API_KEY_ENV, parentEnv[OPENROUTER_API_KEY_ENV] ?? "")) {
189934
+ onCredentialPlaceholderDiscarded(OPENROUTER_API_KEY_ENV);
189935
+ }
189936
+ }
189937
+ if (!hasEnv(env, OPENROUTER_API_KEY_ENV)) {
189938
+ throw new ChildEnvPreflightError("OPENROUTER_KEY_MISSING", 'agents.codex.provider="openrouter" requires OPENROUTER_API_KEY, but it is not visible to the Kyoso process. Add OPENROUTER_API_KEY to the MCP registration, restart the client, then run `kyoso doctor`.');
189939
+ }
189940
+ const config2 = parseCodexConfig(env.CODEX_CONFIG);
189941
+ assertOpenRouterConfigDoesNotSelectProfile(config2);
189942
+ if (config2.model_providers !== undefined && !isPlainObject2(config2.model_providers)) {
189943
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG.model_providers must be a JSON object for the OpenRouter provider.");
189944
+ }
189945
+ reportDiscardedOpenRouterProviders(config2.model_providers, onOpenRouterProvidersDiscarded);
189946
+ env.MODEL_PROVIDER = KYOSO_OPENROUTER_PROVIDER_ID;
189947
+ env.CODEX_CONFIG = JSON.stringify({
189948
+ ...config2,
189949
+ model: configuredModel,
189950
+ model_provider: KYOSO_OPENROUTER_PROVIDER_ID,
189951
+ model_providers: {
189952
+ [KYOSO_OPENROUTER_PROVIDER_ID]: OPENROUTER_PROVIDER_PRESET
189953
+ }
189954
+ });
189955
+ }
189956
+ function reportDiscardedOpenRouterProviders(modelProviders, onDiscarded) {
189957
+ if (!isPlainObject2(modelProviders))
189958
+ return;
189959
+ const foreignProviderCount = Object.keys(modelProviders).filter((providerId) => providerId !== KYOSO_OPENROUTER_PROVIDER_ID).length;
189960
+ if (foreignProviderCount > 0)
189961
+ onDiscarded(foreignProviderCount);
189962
+ }
189963
+ function parseCodexConfig(value) {
189964
+ if (!value)
189965
+ return {};
189966
+ let parsed;
189967
+ try {
189968
+ parsed = JSON.parse(value);
189969
+ } catch {
189970
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG must contain a JSON object for the OpenRouter provider.");
189971
+ }
189972
+ if (!isPlainObject2(parsed)) {
189973
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG must contain a JSON object for the OpenRouter provider.");
189974
+ }
189975
+ return parsed;
189976
+ }
189977
+ function assertOpenRouterConfigDoesNotSelectProfile(config2) {
189978
+ if (Object.hasOwn(config2, "profile") || Object.hasOwn(config2, "profiles")) {
189979
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG.profile and CODEX_CONFIG.profiles are not supported for the OpenRouter provider.");
189980
+ }
189981
+ }
189633
189982
  function applyClaudeAuthPreference(env, preferApiKey) {
189983
+ discardUnusableEnvValue(env, "ANTHROPIC_API_KEY");
189984
+ discardUnusableEnvValue(env, "CLAUDE_CODE_OAUTH_TOKEN");
189634
189985
  const hasApiKey = hasEnv(env, "ANTHROPIC_API_KEY");
189635
189986
  const hasOAuthToken = hasEnv(env, "CLAUDE_CODE_OAUTH_TOKEN");
189636
189987
  if (!hasApiKey || !hasOAuthToken)
@@ -189641,8 +189992,32 @@ function applyClaudeAuthPreference(env, preferApiKey) {
189641
189992
  delete env.ANTHROPIC_API_KEY;
189642
189993
  }
189643
189994
  }
189995
+ function discardUnusableEnvValue(env, key) {
189996
+ if (env[key] !== undefined && !hasEnv(env, key)) {
189997
+ delete env[key];
189998
+ }
189999
+ }
190000
+ function hasUsableEnvValue(env, key) {
190001
+ return nonEmptyEnv(env, key) !== undefined;
190002
+ }
190003
+ function isUnexpandedEnvPlaceholder(value) {
190004
+ return typeof value === "string" && UNEXPANDED_ENV_PLACEHOLDER_PATTERN.test(value);
190005
+ }
189644
190006
  function hasEnv(env, key) {
189645
- return typeof env[key] === "string" && env[key].trim().length > 0;
190007
+ return hasUsableEnvValue(env, key);
190008
+ }
190009
+ function nonEmptyEnv(env, key) {
190010
+ const value = env[key];
190011
+ return typeof value === "string" && value.trim().length > 0 && !isUnexpandedCredentialEnvValue(key, value) ? value : undefined;
190012
+ }
190013
+ function isUnexpandedCredentialEnvValue(key, value) {
190014
+ return (CREDENTIAL_ENV_KEYS.has(key) || CREDENTIAL_LIKE_ENV_KEY_PATTERN.test(key)) && isUnexpandedEnvPlaceholder(value);
190015
+ }
190016
+ function isPlainObject2(value) {
190017
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
190018
+ return false;
190019
+ }
190020
+ return Object.getPrototypeOf(value) === Object.prototype;
189646
190021
  }
189647
190022
 
189648
190023
  // src/acp/AcpAgentManager.ts
@@ -189828,31 +190203,59 @@ function isRecord6(value) {
189828
190203
  // src/acp/AcpAgentProcess.ts
189829
190204
  class SubprocessAcpAgentManager extends BaseAcpAgentManager {
189830
190205
  config;
189831
- constructor(config2) {
190206
+ parentEnv;
190207
+ constructor(config2, parentEnv = process.env) {
189832
190208
  super();
189833
190209
  this.config = config2;
190210
+ this.parentEnv = parentEnv;
189834
190211
  }
189835
190212
  async runAgent(input) {
189836
190213
  const agentConfig = this.config.agents[input.agent];
190214
+ const startedAt = new Date().toISOString();
189837
190215
  if (!agentConfig.enabled) {
189838
190216
  return {
189839
190217
  agent: input.agent,
189840
190218
  role: input.role,
189841
190219
  status: "skipped",
189842
- startedAt: new Date().toISOString(),
189843
- completedAt: new Date().toISOString()
190220
+ startedAt,
190221
+ completedAt: startedAt
190222
+ };
190223
+ }
190224
+ const provider = input.agent === "codex" ? this.config.agents.codex.provider : undefined;
190225
+ let env;
190226
+ try {
190227
+ env = buildChildEnv(this.parentEnv, agentConfig.auth.envWhitelist, agentConfig.env, {
190228
+ agent: input.agent,
190229
+ model: agentConfig.model,
190230
+ provider,
190231
+ preferApiKey: agentConfig.auth.preferApiKey
190232
+ });
190233
+ } catch (error51) {
190234
+ return {
190235
+ agent: input.agent,
190236
+ role: input.role,
190237
+ status: "failed",
190238
+ startedAt,
190239
+ completedAt: new Date().toISOString(),
190240
+ error: buildPreflightFailure(error51)
190241
+ };
190242
+ }
190243
+ try {
190244
+ return await runSubprocessAgent(input.agent, agentConfig, input, env);
190245
+ } catch (error51) {
190246
+ return {
190247
+ agent: input.agent,
190248
+ role: input.role,
190249
+ status: "failed",
190250
+ startedAt,
190251
+ completedAt: new Date().toISOString(),
190252
+ error: buildAgentFailure(formatAgentErrorDetail(error51), "Agent process failed.")
189844
190253
  };
189845
190254
  }
189846
- return runSubprocessAgent(input.agent, agentConfig, input);
189847
190255
  }
189848
190256
  }
189849
- async function runSubprocessAgent(agent, agentConfig, input) {
190257
+ async function runSubprocessAgent(agent, agentConfig, input, env) {
189850
190258
  const startedAt = new Date().toISOString();
189851
- const env = buildChildEnv(process.env, agentConfig.auth.envWhitelist, agentConfig.env, {
189852
- agent,
189853
- model: agentConfig.model,
189854
- preferApiKey: agentConfig.auth.preferApiKey
189855
- });
189856
190259
  return new Promise((resolveResult) => {
189857
190260
  const child = spawn(agentConfig.command, agentConfig.args, {
189858
190261
  cwd: input.workspaceDir,
@@ -189860,15 +190263,23 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189860
190263
  stdio: ["pipe", "pipe", "pipe"]
189861
190264
  });
189862
190265
  let stdout = "";
189863
- let stderr2 = "";
190266
+ let stderr3 = "";
189864
190267
  let settled = false;
190268
+ let startedWrite;
190269
+ child.once("spawn", () => {
190270
+ if (settled)
190271
+ return;
190272
+ startedWrite = Promise.resolve().then(() => input.onStarted?.()).catch(() => {
190273
+ return;
190274
+ });
190275
+ });
189865
190276
  const abortController = new AbortController;
189866
190277
  const resolveOnce = (result) => {
189867
190278
  if (settled)
189868
190279
  return;
189869
190280
  settled = true;
189870
190281
  clearTimeout(timeout);
189871
- resolveResult(result);
190282
+ (startedWrite ?? Promise.resolve()).then(() => resolveResult(result));
189872
190283
  };
189873
190284
  const timeout = setTimeout(() => {
189874
190285
  abortController.abort(new Error("Kyoso agent timeout"));
@@ -189886,7 +190297,7 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189886
190297
  });
189887
190298
  }, input.timeoutMs);
189888
190299
  child.stderr.on("data", (chunk) => {
189889
- stderr2 += chunk.toString("utf8");
190300
+ stderr3 += chunk.toString("utf8");
189890
190301
  });
189891
190302
  child.on("error", (error51) => {
189892
190303
  const failure = buildAgentFailure(error51.message, "Agent process could not be started.");
@@ -189915,7 +190326,7 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189915
190326
  }).catch((error51) => {
189916
190327
  if (abortController.signal.aborted)
189917
190328
  return;
189918
- const failureText = [stderr2, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
190329
+ const failureText = [stderr3, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
189919
190330
  `);
189920
190331
  resolveOnce({
189921
190332
  agent,
@@ -189940,7 +190351,7 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189940
190351
  rawText: stdout,
189941
190352
  startedAt,
189942
190353
  completedAt: new Date().toISOString(),
189943
- error: buildAgentFailure(stderr2, fallback)
190354
+ error: buildAgentFailure(stderr3, fallback)
189944
190355
  });
189945
190356
  });
189946
190357
  });
@@ -190024,7 +190435,7 @@ function resolveEffortConfigOption(agent, effort) {
190024
190435
  return;
190025
190436
  }
190026
190437
  async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
190027
- const workspaceRoot = await realpath(workspaceDir);
190438
+ const workspaceRoot = await realpath2(workspaceDir);
190028
190439
  const candidates = resolveReadablePaths(workspaceRoot, requestedPath);
190029
190440
  let content;
190030
190441
  for (const absolute of candidates) {
@@ -190047,7 +190458,7 @@ async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
190047
190458
  `);
190048
190459
  }
190049
190460
  async function resolveReadableFile(workspaceRoot, absolute) {
190050
- const realPath = await realpath(absolute).catch((error51) => {
190461
+ const realPath = await realpath2(absolute).catch((error51) => {
190051
190462
  if (isMissingPathError2(error51))
190052
190463
  return;
190053
190464
  throw error51;
@@ -190066,7 +190477,7 @@ function resolveReadablePaths(workspaceRoot, requestedPath) {
190066
190477
  }
190067
190478
  const repoPath = resolve3(workspaceRoot, "repo", relativePath);
190068
190479
  assertWithinWorkspace(workspaceRoot, repoPath);
190069
- return isAbsolute(requestedPath) ? [primary, repoPath] : [repoPath, primary];
190480
+ return isAbsolute3(requestedPath) ? [primary, repoPath] : [repoPath, primary];
190070
190481
  }
190071
190482
  function assertWithinWorkspace(workspaceRoot, absolute) {
190072
190483
  const relativePath = relative(workspaceRoot, absolute);
@@ -190098,6 +190509,20 @@ function buildAgentFailure(rawDetail, fallbackMessage) {
190098
190509
  ...detail ? { detail } : {}
190099
190510
  };
190100
190511
  }
190512
+ function buildPreflightFailure(error51) {
190513
+ if (error51 instanceof ChildEnvPreflightError) {
190514
+ return {
190515
+ code: error51.code,
190516
+ message: error51.message
190517
+ };
190518
+ }
190519
+ const detail = sanitizeTextForDisplay(formatAgentErrorDetail(error51));
190520
+ return {
190521
+ code: "AGENT_CONFIG_INVALID",
190522
+ message: "Agent configuration is invalid. Run kyoso doctor and check agent configuration.",
190523
+ ...detail ? { detail } : {}
190524
+ };
190525
+ }
190101
190526
  function safeAgentFailureMessage(code, fallbackMessage) {
190102
190527
  if (code === "AUTH_FAILED") {
190103
190528
  return "Agent authentication failed. Run kyoso doctor and check configured credentials.";
@@ -190113,15 +190538,15 @@ function safeAgentFailureMessage(code, fallbackMessage) {
190113
190538
  }
190114
190539
  return sanitizeTextForDisplay(fallbackMessage);
190115
190540
  }
190116
- function classifyAgentFailure(stderr2) {
190117
- if (/spawn/i.test(stderr2))
190541
+ function classifyAgentFailure(stderr3) {
190542
+ if (/spawn/i.test(stderr3))
190118
190543
  return "AGENT_SPAWN_FAILED";
190119
- if (/ENOTFOUND|ENOTCACHED|ECONNREFUSED|ETIMEDOUT|registry\.npmjs\.org|network request|cache mode/i.test(stderr2)) {
190544
+ if (/ENOTFOUND|ENOTCACHED|ECONNREFUSED|ETIMEDOUT|registry\.npmjs\.org|network request|cache mode/i.test(stderr3)) {
190120
190545
  return "AGENT_NETWORK_FAILED";
190121
190546
  }
190122
- if (/auth|api key|login|credential/i.test(stderr2))
190547
+ if (/auth|api key|login|credential/i.test(stderr3))
190123
190548
  return "AUTH_FAILED";
190124
- if (/permission|policy|write|terminal/i.test(stderr2))
190549
+ if (/permission|policy|write|terminal/i.test(stderr3))
190125
190550
  return "PERMISSION_DENIED";
190126
190551
  return "AGENT_FAILED";
190127
190552
  }
@@ -190158,9 +190583,24 @@ class FakeAgentManager extends BaseAcpAgentManager {
190158
190583
  this.calls.push(input);
190159
190584
  const startedAt = new Date().toISOString();
190160
190585
  if (input.role === "finding_verifier") {
190586
+ await input.onStarted?.();
190161
190587
  return verifierResult(input, startedAt, this.verifierScenarios[input.agent] ?? "confirmed");
190162
190588
  }
190163
190589
  const scenario = this.scenarios[input.agent] ?? "success";
190590
+ if (scenario === "preflight_failure" || scenario === "openrouter_key_missing") {
190591
+ return {
190592
+ agent: input.agent,
190593
+ role: input.role,
190594
+ status: "failed",
190595
+ startedAt,
190596
+ completedAt: new Date().toISOString(),
190597
+ error: {
190598
+ code: scenario === "openrouter_key_missing" ? "OPENROUTER_KEY_MISSING" : "AGENT_CONFIG_INVALID",
190599
+ message: scenario === "openrouter_key_missing" ? "Fake OpenRouter key is missing." : "Fake agent configuration is invalid."
190600
+ }
190601
+ };
190602
+ }
190603
+ await input.onStarted?.();
190164
190604
  if (scenario === "timeout") {
190165
190605
  return {
190166
190606
  agent: input.agent,
@@ -190784,8 +191224,8 @@ function normalizeTitle(value) {
190784
191224
 
190785
191225
  // src/audit/stateRoot.ts
190786
191226
  import { createHash as createHash2 } from "node:crypto";
190787
- import { lstat, mkdir as mkdir2, realpath as realpath2 } from "node:fs/promises";
190788
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve5 } from "node:path";
191227
+ import { lstat, mkdir as mkdir2, realpath as realpath3 } from "node:fs/promises";
191228
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute4, join as join3, resolve as resolve5 } from "node:path";
190789
191229
 
190790
191230
  // src/context/pathPolicy.ts
190791
191231
  import { normalize, sep } from "node:path";
@@ -190876,7 +191316,7 @@ async function resolveAuditStateRoot(options) {
190876
191316
  return { warnings: [...warnings, AUDIT_WARNING_UNSUPPORTED_CAPABILITY] };
190877
191317
  }
190878
191318
  try {
190879
- const workspaceRoot = await realpath2(resolve5(options.cwd));
191319
+ const workspaceRoot = await realpath3(resolve5(options.cwd));
190880
191320
  const candidate = resolveStateBaseCandidate(options.env ?? process.env);
190881
191321
  if (!candidate)
190882
191322
  throw new Error("missing trusted state base");
@@ -190905,7 +191345,7 @@ async function resolveAuditStateRoot(options) {
190905
191345
  }
190906
191346
  }
190907
191347
  async function ensureTrustedDirectory(options) {
190908
- const realRoot = await realpath2(options.root);
191348
+ const realRoot = await realpath3(options.root);
190909
191349
  assertSafeDirectory(realRoot, options.uid, await lstat(realRoot));
190910
191350
  if (options.workspaceRoot && isPathWithin(realRoot, options.workspaceRoot)) {
190911
191351
  throw new Error("trusted directory resolves inside workspace");
@@ -190922,7 +191362,7 @@ async function ensureTrustedDirectory(options) {
190922
191362
  entry = await lstat(next);
190923
191363
  }
190924
191364
  assertSafeDirectory(next, options.uid, entry);
190925
- const realNext = await realpath2(next);
191365
+ const realNext = await realpath3(next);
190926
191366
  if (!isPathWithin(realNext, realRoot)) {
190927
191367
  throw new Error("managed directory escaped trusted root");
190928
191368
  }
@@ -190938,7 +191378,7 @@ function isResolvedAuditStateRoot(resolution) {
190938
191378
  }
190939
191379
  function validateAuditDirectory(directory, warnings) {
190940
191380
  try {
190941
- if (directory.trim().length === 0 || isAbsolute2(directory) || directory.split(/[\\/]+/).includes("..")) {
191381
+ if (directory.trim().length === 0 || isAbsolute4(directory) || directory.split(/[\\/]+/).includes("..")) {
190942
191382
  throw new Error("unsafe logical directory");
190943
191383
  }
190944
191384
  const normalized = normalizeRelativePath(directory);
@@ -190953,11 +191393,11 @@ function validateAuditDirectory(directory, warnings) {
190953
191393
  }
190954
191394
  function resolveStateBaseCandidate(env) {
190955
191395
  const xdgStateHome = env.XDG_STATE_HOME?.trim();
190956
- if (xdgStateHome && isAbsolute2(xdgStateHome)) {
191396
+ if (xdgStateHome && isAbsolute4(xdgStateHome)) {
190957
191397
  return resolve5(xdgStateHome);
190958
191398
  }
190959
191399
  const home = env.HOME?.trim();
190960
- if (!home || !isAbsolute2(home))
191400
+ if (!home || !isAbsolute4(home))
190961
191401
  return;
190962
191402
  return join3(resolve5(home), ".local", "state");
190963
191403
  }
@@ -190969,7 +191409,7 @@ async function ensureTrustedStateBase(options) {
190969
191409
  assertSafeDirectory(existing.path, options.uid, await lstat(existing.path), {
190970
191410
  allowFilesystemRoot: true
190971
191411
  });
190972
- const realExisting = await realpath2(existing.path);
191412
+ const realExisting = await realpath3(existing.path);
190973
191413
  if (isPathWithin(realExisting, options.workspaceRoot)) {
190974
191414
  throw new Error("state base resolves inside workspace");
190975
191415
  }
@@ -190980,7 +191420,7 @@ async function ensureTrustedStateBase(options) {
190980
191420
  await createDirectory(current);
190981
191421
  const entry = await lstat(current);
190982
191422
  assertSafeDirectory(current, options.uid, entry);
190983
- const realCurrent = await realpath2(current);
191423
+ const realCurrent = await realpath3(current);
190984
191424
  if (!isPathWithin(realCurrent, realExisting)) {
190985
191425
  throw new Error("state base changed while being created");
190986
191426
  }
@@ -190989,7 +191429,7 @@ async function ensureTrustedStateBase(options) {
190989
191429
  }
190990
191430
  current = realCurrent;
190991
191431
  }
190992
- const stateBase = await realpath2(current);
191432
+ const stateBase = await realpath3(current);
190993
191433
  assertSafeDirectory(stateBase, options.uid, await lstat(stateBase));
190994
191434
  if (isPathWithin(stateBase, options.workspaceRoot)) {
190995
191435
  throw new Error("state base resolves inside workspace");
@@ -191008,7 +191448,7 @@ async function findExistingAncestor(candidate) {
191008
191448
  }
191009
191449
  return { path: current, missingSegments };
191010
191450
  }
191011
- const parent = dirname3(current);
191451
+ const parent = dirname4(current);
191012
191452
  if (parent === current)
191013
191453
  throw new Error("state base has no existing ancestor");
191014
191454
  missingSegments.unshift(basename(current));
@@ -191041,12 +191481,12 @@ function getCurrentUid(getuid) {
191041
191481
  }
191042
191482
  }
191043
191483
  function isFilesystemRoot(path) {
191044
- return dirname3(path) === path;
191484
+ return dirname4(path) === path;
191045
191485
  }
191046
191486
  async function assertTrustedAncestorChain(path, uid) {
191047
191487
  let child = path;
191048
191488
  while (!isFilesystemRoot(child)) {
191049
- const parent = dirname3(child);
191489
+ const parent = dirname4(child);
191050
191490
  const [parentStat, childStat] = await Promise.all([
191051
191491
  lstat(parent),
191052
191492
  lstat(child)
@@ -191094,7 +191534,7 @@ function isAlreadyExistsError(error51) {
191094
191534
 
191095
191535
  // src/audit/safeTraceFile.ts
191096
191536
  import { constants } from "node:fs";
191097
- import { lstat as lstat2, open, realpath as realpath3, stat } from "node:fs/promises";
191537
+ import { lstat as lstat2, open, realpath as realpath4, stat } from "node:fs/promises";
191098
191538
  import { join as join4 } from "node:path";
191099
191539
  var AUDIT_WARNING_UNSUPPORTED_OPEN_CAPABILITY = "AUDIT_DISABLED_UNSUPPORTED_CAPABILITY: Audit trace writing requires unavailable filesystem capabilities.";
191100
191540
  async function openVerifiedTraceFile(options) {
@@ -191124,7 +191564,7 @@ async function openVerifiedTraceFile(options) {
191124
191564
  const [handleStat, pathStat, realTracePath] = await Promise.all([
191125
191565
  handle.stat({ bigint: true }),
191126
191566
  stat(tracePath, { bigint: true }),
191127
- realpath3(tracePath)
191567
+ realpath4(tracePath)
191128
191568
  ]);
191129
191569
  if (!handleStat.isFile() || !pathStat.isFile() || handleStat.dev !== pathStat.dev || handleStat.ino !== pathStat.ino || !isPathWithin(realTracePath, options.kyosoRoot)) {
191130
191570
  throw new Error("trace file identity could not be verified");
@@ -192020,7 +192460,7 @@ function decide(input) {
192020
192460
 
192021
192461
  // src/workspace/createSnapshot.ts
192022
192462
  import { chmod, mkdir as mkdir3, mkdtemp, writeFile as writeFile2 } from "node:fs/promises";
192023
- import { dirname as dirname4, join as join5 } from "node:path";
192463
+ import { dirname as dirname5, join as join5 } from "node:path";
192024
192464
  import { tmpdir } from "node:os";
192025
192465
  async function createSnapshot(traceId, tool, request, options = {}) {
192026
192466
  const root = await mkdtemp(join5(tmpdir(), `kyoso-${traceId}-`));
@@ -192036,7 +192476,7 @@ async function createSnapshot(traceId, tool, request, options = {}) {
192036
192476
  if (!isAllowedPath(relative2, options.allowPatterns ?? []))
192037
192477
  continue;
192038
192478
  const dest = join5(repoDir, relative2);
192039
- await mkdir3(dirname4(dest), { recursive: true });
192479
+ await mkdir3(dirname5(dest), { recursive: true });
192040
192480
  await writeFile2(dest, file2.content, "utf8");
192041
192481
  await chmod(dest, 292).catch(() => {
192042
192482
  return;
@@ -192374,7 +192814,7 @@ async function runReview(tool, request, options = {}) {
192374
192814
  fileCount: snapshot.fileCount,
192375
192815
  timestamp: new Date().toISOString()
192376
192816
  });
192377
- const manager = options.agentManager ?? defaultAgentManager(loaded.config);
192817
+ const manager = options.agentManager ?? defaultAgentManager(loaded.config, options.env ?? process.env);
192378
192818
  const agentResults = await runAgents({
192379
192819
  tool,
192380
192820
  request: built.request,
@@ -192383,7 +192823,8 @@ async function runReview(tool, request, options = {}) {
192383
192823
  workspaceDir: snapshot.root,
192384
192824
  networkMode,
192385
192825
  manager,
192386
- trace
192826
+ trace,
192827
+ warnings
192387
192828
  });
192388
192829
  warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
192389
192830
  const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
@@ -192658,24 +193099,51 @@ function buildCrossModelAnalysis(judge, reviewMode) {
192658
193099
  }
192659
193100
  async function runAgents(input) {
192660
193101
  const agentRoles = resolveAgentRoles(input.config);
192661
- const agentInputs = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => ({
192662
- traceId: input.traceId,
192663
- agent,
192664
- role: agentRoles[agent] ?? input.config.agents[agent].role,
192665
- tool: input.tool,
192666
- prompt: buildAgentPrompt(input.tool, input.request, agent, agentRoles[agent] ?? input.config.agents[agent].role),
192667
- workspaceDir: input.workspaceDir,
192668
- timeoutMs: input.request.options?.maxAgentTimeoutMs ?? input.config.agents[agent].timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
192669
- networkMode: input.networkMode
192670
- }));
192671
- await Promise.all(agentInputs.map((agentInput) => input.trace.write({
192672
- type: "agent_started",
192673
- traceId: input.traceId,
192674
- agent: agentInput.agent,
192675
- role: agentInput.role,
192676
- timestamp: new Date().toISOString()
192677
- })));
193102
+ const startedWrites = [];
193103
+ let acceptingStartedEvents = true;
193104
+ const agentInputs = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => {
193105
+ const agentConfig = input.config.agents[agent];
193106
+ const role = agentRoles[agent] ?? agentConfig.role;
193107
+ return {
193108
+ traceId: input.traceId,
193109
+ agent,
193110
+ role,
193111
+ tool: input.tool,
193112
+ prompt: buildAgentPrompt(input.tool, input.request, agent, role),
193113
+ workspaceDir: input.workspaceDir,
193114
+ timeoutMs: input.request.options?.maxAgentTimeoutMs ?? agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
193115
+ networkMode: input.networkMode,
193116
+ onStarted: () => {
193117
+ if (!acceptingStartedEvents)
193118
+ return Promise.resolve();
193119
+ const event = {
193120
+ type: "agent_started",
193121
+ traceId: input.traceId,
193122
+ agent,
193123
+ role,
193124
+ timestamp: new Date().toISOString()
193125
+ };
193126
+ if (agentConfig.model) {
193127
+ event.model = sanitizeTextForDisplay(agentConfig.model);
193128
+ }
193129
+ if (agent === "codex" && input.config.agents.codex.provider === CODEX_OPENROUTER_PROVIDER) {
193130
+ event.provider = CODEX_OPENROUTER_PROVIDER;
193131
+ }
193132
+ const write = (async () => {
193133
+ try {
193134
+ await input.trace.write(event);
193135
+ } catch {
193136
+ input.warnings.push("AUDIT_WRITE_FAILED: agent_started event could not be recorded.");
193137
+ }
193138
+ })();
193139
+ startedWrites.push(write);
193140
+ return write;
193141
+ }
193142
+ };
193143
+ });
192678
193144
  const results = await input.manager.runAll(agentInputs);
193145
+ acceptingStartedEvents = false;
193146
+ await Promise.all(startedWrites);
192679
193147
  await Promise.all(results.map((result) => {
192680
193148
  const event = {
192681
193149
  type: "agent_completed",
@@ -192707,10 +193175,11 @@ function resolveAgentRoles(config2) {
192707
193175
  }
192708
193176
  return roles;
192709
193177
  }
192710
- function defaultAgentManager(config2) {
192711
- if (process.env.KYOSO_TEST_FAKE_AGENTS === "1")
193178
+ function defaultAgentManager(config2, parentEnv) {
193179
+ if (parentEnv.KYOSO_TEST_FAKE_AGENTS === "1") {
192712
193180
  return new FakeAgentManager;
192713
- return new SubprocessAcpAgentManager(config2);
193181
+ }
193182
+ return new SubprocessAcpAgentManager(config2, parentEnv);
192714
193183
  }
192715
193184
  function normalizeAgentRunResult(result) {
192716
193185
  if (result.status === "completed" && result.rawText && !result.normalized) {