@mono-agent/agent-app 0.20.11 → 0.20.14

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 (220) hide show
  1. package/README.md +33 -18
  2. package/dist/app-controller-channels.d.ts +4 -0
  3. package/dist/app-controller-channels.d.ts.map +1 -1
  4. package/dist/app-controller-channels.js +20 -2
  5. package/dist/app-controller-channels.js.map +1 -1
  6. package/dist/app-controller-continuation.d.ts.map +1 -1
  7. package/dist/app-controller-continuation.js +2 -3
  8. package/dist/app-controller-continuation.js.map +1 -1
  9. package/dist/app-controller-lifecycle.d.ts +4 -0
  10. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  11. package/dist/app-controller-lifecycle.js +7 -0
  12. package/dist/app-controller-lifecycle.js.map +1 -1
  13. package/dist/app-controller-monitors.d.ts +26 -0
  14. package/dist/app-controller-monitors.d.ts.map +1 -0
  15. package/dist/app-controller-monitors.js +127 -0
  16. package/dist/app-controller-monitors.js.map +1 -0
  17. package/dist/app-controller-responder.d.ts +7 -17
  18. package/dist/app-controller-responder.d.ts.map +1 -1
  19. package/dist/app-controller-responder.js +33 -69
  20. package/dist/app-controller-responder.js.map +1 -1
  21. package/dist/app-controller-utils.d.ts +1 -5
  22. package/dist/app-controller-utils.d.ts.map +1 -1
  23. package/dist/app-controller-utils.js +4 -21
  24. package/dist/app-controller-utils.js.map +1 -1
  25. package/dist/app-controller.d.ts +24 -10
  26. package/dist/app-controller.d.ts.map +1 -1
  27. package/dist/app-controller.js +36 -1
  28. package/dist/app-controller.js.map +1 -1
  29. package/dist/channel-drivers/slack.d.ts.map +1 -1
  30. package/dist/channel-drivers/slack.js +18 -0
  31. package/dist/channel-drivers/slack.js.map +1 -1
  32. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  33. package/dist/channel-drivers/telegram.js +26 -0
  34. package/dist/channel-drivers/telegram.js.map +1 -1
  35. package/dist/channel-drivers/tui.d.ts +68 -8
  36. package/dist/channel-drivers/tui.d.ts.map +1 -1
  37. package/dist/channel-drivers/tui.js +388 -89
  38. package/dist/channel-drivers/tui.js.map +1 -1
  39. package/dist/channel-runtime-controls.d.ts +2 -1
  40. package/dist/channel-runtime-controls.d.ts.map +1 -1
  41. package/dist/channel-runtime-controls.js +79 -22
  42. package/dist/channel-runtime-controls.js.map +1 -1
  43. package/dist/cli-args.d.ts +4 -6
  44. package/dist/cli-args.d.ts.map +1 -1
  45. package/dist/cli-args.js +16 -36
  46. package/dist/cli-args.js.map +1 -1
  47. package/dist/cli-commands.d.ts.map +1 -1
  48. package/dist/cli-commands.js +11 -0
  49. package/dist/cli-commands.js.map +1 -1
  50. package/dist/cli-help.d.ts +26 -0
  51. package/dist/cli-help.d.ts.map +1 -1
  52. package/dist/cli-help.js +25 -34
  53. package/dist/cli-help.js.map +1 -1
  54. package/dist/cli-init-command.d.ts +1 -2
  55. package/dist/cli-init-command.d.ts.map +1 -1
  56. package/dist/cli-init-command.js +5 -49
  57. package/dist/cli-init-command.js.map +1 -1
  58. package/dist/config-reference.d.ts.map +1 -1
  59. package/dist/config-reference.js +153 -52
  60. package/dist/config-reference.js.map +1 -1
  61. package/dist/configuration-proposal-tool.d.ts.map +1 -1
  62. package/dist/configuration-proposal-tool.js +3 -11
  63. package/dist/configuration-proposal-tool.js.map +1 -1
  64. package/dist/configured-agent.d.ts +18 -3
  65. package/dist/configured-agent.d.ts.map +1 -1
  66. package/dist/configured-agent.js +67 -86
  67. package/dist/configured-agent.js.map +1 -1
  68. package/dist/conversation-title.d.ts.map +1 -1
  69. package/dist/conversation-title.js +9 -2
  70. package/dist/conversation-title.js.map +1 -1
  71. package/dist/doctor.d.ts +7 -13
  72. package/dist/doctor.d.ts.map +1 -1
  73. package/dist/doctor.js +191 -725
  74. package/dist/doctor.js.map +1 -1
  75. package/dist/first-run-readiness.d.ts.map +1 -1
  76. package/dist/first-run-readiness.js +0 -6
  77. package/dist/first-run-readiness.js.map +1 -1
  78. package/dist/local-configuration.d.ts.map +1 -1
  79. package/dist/local-configuration.js +9 -37
  80. package/dist/local-configuration.js.map +1 -1
  81. package/dist/memory-remember.d.ts +49 -0
  82. package/dist/memory-remember.d.ts.map +1 -0
  83. package/dist/memory-remember.js +190 -0
  84. package/dist/memory-remember.js.map +1 -0
  85. package/dist/memory-retrieval.d.ts +24 -0
  86. package/dist/memory-retrieval.d.ts.map +1 -1
  87. package/dist/memory-retrieval.js +23 -0
  88. package/dist/memory-retrieval.js.map +1 -1
  89. package/dist/model-effort-capabilities.d.ts +8 -9
  90. package/dist/model-effort-capabilities.d.ts.map +1 -1
  91. package/dist/model-effort-capabilities.js +17 -35
  92. package/dist/model-effort-capabilities.js.map +1 -1
  93. package/dist/modules/base.d.ts +3 -4
  94. package/dist/modules/base.d.ts.map +1 -1
  95. package/dist/modules/base.js +3 -5
  96. package/dist/modules/base.js.map +1 -1
  97. package/dist/modules/catalog.d.ts.map +1 -1
  98. package/dist/modules/catalog.js +11 -9
  99. package/dist/modules/catalog.js.map +1 -1
  100. package/dist/modules/known-tools.d.ts +2 -2
  101. package/dist/modules/known-tools.d.ts.map +1 -1
  102. package/dist/modules/known-tools.js +6 -0
  103. package/dist/modules/known-tools.js.map +1 -1
  104. package/dist/monitor-channel-routing.d.ts +20 -0
  105. package/dist/monitor-channel-routing.d.ts.map +1 -0
  106. package/dist/monitor-channel-routing.js +73 -0
  107. package/dist/monitor-channel-routing.js.map +1 -0
  108. package/dist/monitors-command.d.ts +23 -0
  109. package/dist/monitors-command.d.ts.map +1 -0
  110. package/dist/monitors-command.js +207 -0
  111. package/dist/monitors-command.js.map +1 -0
  112. package/dist/monitors-config.d.ts +74 -0
  113. package/dist/monitors-config.d.ts.map +1 -0
  114. package/dist/monitors-config.js +146 -0
  115. package/dist/monitors-config.js.map +1 -0
  116. package/dist/monitors-context.d.ts +58 -0
  117. package/dist/monitors-context.d.ts.map +1 -0
  118. package/dist/monitors-context.js +182 -0
  119. package/dist/monitors-context.js.map +1 -0
  120. package/dist/monitors-runtime.d.ts +36 -0
  121. package/dist/monitors-runtime.d.ts.map +1 -0
  122. package/dist/monitors-runtime.js +103 -0
  123. package/dist/monitors-runtime.js.map +1 -0
  124. package/dist/monitors-service.d.ts +71 -0
  125. package/dist/monitors-service.d.ts.map +1 -0
  126. package/dist/monitors-service.js +1665 -0
  127. package/dist/monitors-service.js.map +1 -0
  128. package/dist/monitors-store.d.ts +83 -0
  129. package/dist/monitors-store.d.ts.map +1 -0
  130. package/dist/monitors-store.js +283 -0
  131. package/dist/monitors-store.js.map +1 -0
  132. package/dist/process-jobs-context.d.ts +8 -0
  133. package/dist/process-jobs-context.d.ts.map +1 -1
  134. package/dist/process-jobs-context.js +11 -0
  135. package/dist/process-jobs-context.js.map +1 -1
  136. package/dist/process-jobs-protection.js +1 -1
  137. package/dist/process-jobs-protection.js.map +1 -1
  138. package/dist/process-jobs-runtime.d.ts.map +1 -1
  139. package/dist/process-jobs-runtime.js +1 -12
  140. package/dist/process-jobs-runtime.js.map +1 -1
  141. package/dist/process-jobs-service.d.ts.map +1 -1
  142. package/dist/process-jobs-service.js +3 -149
  143. package/dist/process-jobs-service.js.map +1 -1
  144. package/dist/process-output-redaction.d.ts +33 -0
  145. package/dist/process-output-redaction.d.ts.map +1 -0
  146. package/dist/process-output-redaction.js +149 -0
  147. package/dist/process-output-redaction.js.map +1 -0
  148. package/dist/provider-model-catalog.d.ts +78 -0
  149. package/dist/provider-model-catalog.d.ts.map +1 -0
  150. package/dist/provider-model-catalog.js +439 -0
  151. package/dist/provider-model-catalog.js.map +1 -0
  152. package/dist/provider-setup.d.ts +4 -13
  153. package/dist/provider-setup.d.ts.map +1 -1
  154. package/dist/provider-setup.js +5 -115
  155. package/dist/provider-setup.js.map +1 -1
  156. package/dist/readiness-probe-worker.d.ts +1 -2
  157. package/dist/readiness-probe-worker.d.ts.map +1 -1
  158. package/dist/readiness-probe-worker.js +3 -8
  159. package/dist/readiness-probe-worker.js.map +1 -1
  160. package/dist/readiness-probe.d.ts +1 -1
  161. package/dist/readiness-probe.d.ts.map +1 -1
  162. package/dist/readiness-probe.js +3 -33
  163. package/dist/readiness-probe.js.map +1 -1
  164. package/dist/request-model-override.d.ts +4 -36
  165. package/dist/request-model-override.d.ts.map +1 -1
  166. package/dist/request-model-override.js +58 -146
  167. package/dist/request-model-override.js.map +1 -1
  168. package/dist/runtime-option-extensions.d.ts +2 -4
  169. package/dist/runtime-option-extensions.d.ts.map +1 -1
  170. package/dist/runtime-option-extensions.js +4 -8
  171. package/dist/runtime-option-extensions.js.map +1 -1
  172. package/dist/runtime-routes.d.ts +5 -5
  173. package/dist/runtime-routes.d.ts.map +1 -1
  174. package/dist/runtime-routes.js +29 -18
  175. package/dist/runtime-routes.js.map +1 -1
  176. package/dist/sandbox-settings-cleanup.d.ts +15 -0
  177. package/dist/sandbox-settings-cleanup.d.ts.map +1 -0
  178. package/dist/sandbox-settings-cleanup.js +88 -0
  179. package/dist/sandbox-settings-cleanup.js.map +1 -0
  180. package/dist/trigger-overrides.d.ts +6 -0
  181. package/dist/trigger-overrides.d.ts.map +1 -1
  182. package/dist/trigger-overrides.js +36 -4
  183. package/dist/trigger-overrides.js.map +1 -1
  184. package/dist/untrusted-text.d.ts +36 -0
  185. package/dist/untrusted-text.d.ts.map +1 -0
  186. package/dist/untrusted-text.js +150 -0
  187. package/dist/untrusted-text.js.map +1 -0
  188. package/dist/wizard/answers.d.ts +3 -9
  189. package/dist/wizard/answers.d.ts.map +1 -1
  190. package/dist/wizard/answers.js +11 -36
  191. package/dist/wizard/answers.js.map +1 -1
  192. package/dist/wizard/from-flags.d.ts +1 -2
  193. package/dist/wizard/from-flags.d.ts.map +1 -1
  194. package/dist/wizard/from-flags.js +8 -3
  195. package/dist/wizard/from-flags.js.map +1 -1
  196. package/dist/wizard/model-discovery.d.ts +7 -8
  197. package/dist/wizard/model-discovery.d.ts.map +1 -1
  198. package/dist/wizard/model-discovery.js +58 -431
  199. package/dist/wizard/model-discovery.js.map +1 -1
  200. package/dist/wizard/presets.js +2 -2
  201. package/dist/wizard/presets.js.map +1 -1
  202. package/dist/wizard/prompts.d.ts +13 -12
  203. package/dist/wizard/prompts.d.ts.map +1 -1
  204. package/dist/wizard/prompts.js +24 -45
  205. package/dist/wizard/prompts.js.map +1 -1
  206. package/dist/wizard/run.d.ts.map +1 -1
  207. package/dist/wizard/run.js +29 -152
  208. package/dist/wizard/run.js.map +1 -1
  209. package/package.json +17 -17
  210. package/schema/mono-agent.config.schema.json +183 -47
  211. package/skills/mono-agent-composer/SKILL.md +2 -2
  212. package/skills/mono-agent-composer/references/config-blueprint.md +43 -40
  213. package/skills/mono-agent-composer/references/discovery-questions.md +8 -13
  214. package/skills/mono-agent-composer/references/feature-coverage.md +10 -8
  215. package/skills/mono-agent-composer/references/package-map.md +21 -15
  216. package/skills/mono-agent-composer/references/playbooks.md +19 -23
  217. package/dist/codex-model-catalog.d.ts +0 -15
  218. package/dist/codex-model-catalog.d.ts.map +0 -1
  219. package/dist/codex-model-catalog.js +0 -220
  220. package/dist/codex-model-catalog.js.map +0 -1
package/dist/doctor.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
3
  import { access, lstat, mkdir, readdir, readFile, realpath, stat } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
4
  import { isIP } from "node:net";
6
5
  import { basename, join, relative, resolve, sep } from "node:path";
7
6
  import { promisify } from "node:util";
@@ -12,10 +11,11 @@ import { promisify } from "node:util";
12
11
  import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
13
12
  import { validateCronExpression } from "@mono-agent/cron-adapter";
14
13
  import { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
15
- import { defaultExecutionModeForModel, describeMonoRuntimeSupport, inspectCodexSubscriptionSearch, isValidMcpServerName, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
16
- import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
17
- import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
14
+ import { describeMonoRuntimeSupport, inspectCodexSubscriptionSearch, isValidMcpServerName, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, } from "@mono-agent/runtime-adapter";
15
+ import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
16
+ import { describeSandboxEffectiveState, MODEL_REFERENCE_ECHO_MAX_BYTES, MODEL_REFERENCE_REASON_MAX_BYTES, resolveSandboxEffectiveState, RuntimeAdapterError, sandboxEffectiveStateWarning, sanitizeModelReferenceText, } from "@mono-agent/runtime-adapter";
18
17
  import { isAppCoreConfigError, loadAppCoreConfig, } from "./app-config.js";
18
+ import { isRememberToolAllowed, isRememberToolPolicyName } from "./memory-remember.js";
19
19
  import { adapterSendToolNames, isAdapterSendToolAllowed, resolveAdapterSendToolsSettings } from "./adapter-send-tools.js";
20
20
  import { canonicalToolName, isAllowAllTools, isKnownToolName, isMcpToolName, suggestToolName } from "./modules/known-tools.js";
21
21
  import { collectChannelConfigViews } from "./channel-config-view.js";
@@ -39,17 +39,22 @@ import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } fr
39
39
  import { piAuthRecoveryCommand } from "./provider-setup.js";
40
40
  import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
41
41
  import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
42
- import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
43
42
  import { runtimeProvenanceDetail } from "./runtime-provenance.js";
43
+ import { resolveAdvertisedModelEffort } from "./model-effort-capabilities.js";
44
44
  import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
45
45
  import { DEFAULT_LAUNCHD_LOG_POLICY, inspectLaunchdLogs, LAUNCHD_LOG_MONITOR_INTERVAL_SECONDS, launchdLogPathsForConfig, } from "./launchd-logs.js";
46
46
  import { readLaunchdLogMonitorStatus } from "./launchd-log-monitor-status.js";
47
47
  import { exporterSection, runsSection } from "./doctor-observability.js";
48
48
  import { sessionToolHistorySection } from "./doctor-session-history.js";
49
- import { runtimeRouteContainsUnsupportedHistoryTool } from "./app-controller-utils.js";
50
49
  const execFile = promisify(execFileCallback);
51
50
  const CONTINUATION_V2_ROLLBACK_GUARD = "UPGRADED-TO-RECORDS-V3";
52
51
  const CONTINUATION_V2_ROLLBACK_GUARD_CONTENT = "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n";
52
+ const DOCTOR_STATUS_TIMEOUT_MS = 5_000;
53
+ const DOCTOR_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
54
+ const defaultDoctorStatusExecFile = async (file, args, options) => {
55
+ const { stdout } = await execFile(file, [...args], options);
56
+ return { stdout };
57
+ };
53
58
  /**
54
59
  * Loads every config section the app would use at start and reports it
55
60
  * per-section, so an engineer can see exactly what would run, wait, or fail —
@@ -92,13 +97,13 @@ export async function validateMonoAgentFolder(options) {
92
97
  sections.push({ id: "secret-placement", label: "Config warnings", status: "waiting", details: configWarnings });
93
98
  }
94
99
  sections.push(runtimeSection(coreConfig));
95
- sections.push(await credentialsSection(coreConfig, options.env, options.cwd, liveness, options.verifiedCredentialModelRefs, options.sdkAuthStatusExecFile, staticTriggerCredentialRefs));
100
+ sections.push(await credentialsSection(coreConfig, options.env, options.verifiedCredentialModelRefs, staticTriggerCredentialRefs));
96
101
  sections.push(await contextSection(coreConfig, options.cwd));
97
102
  sections.push(await memorySection(coreConfig, options.cwd, options.env, liveness, allowFilesystemWrites, options.preferAppPluginInstall === true));
98
103
  sections.push(await toolsSection(coreConfig, options));
99
104
  sections.push(await sessionToolHistorySection({
100
105
  historyRoot: join(coreConfig.artifacts.dir, "..", "history"),
101
- requestScopedToolSupported: !runtimeRouteContainsUnsupportedHistoryTool(coreConfig),
106
+ requestScopedToolSupported: true,
102
107
  }));
103
108
  sections.push(await webToolsSection(coreConfig, options, liveness));
104
109
  sections.push(await continuationSection(coreConfig, options));
@@ -266,40 +271,7 @@ async function collectStaticTriggerCredentialRefs(drivers, input) {
266
271
  return refs;
267
272
  }
268
273
  async function applyRequestModelOverrideCompatibilityChecks(sections, config, drivers, input) {
269
- const baseIsDirectCodex = config.runtime.model.sdk === "codex";
270
- const routeSafety = config.runtime.routeSafety ?? "uniform";
271
- const directBoundaryConflicts = [];
272
- const sandboxBypasses = [];
273
- const toolPolicyBypasses = [];
274
- const effortBypasses = [];
275
- const turnCapBypasses = [];
276
- const mcpBypasses = [];
277
- const skillBypasses = [];
278
274
  const piModelResolutionFailures = [];
279
- const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
280
- const restrictiveToolPolicy = !hasAllowAllOnlyToolPolicy(config.tools);
281
- let configuredMcpServerNames = [];
282
- if (config.tools.mcpConfigPath !== undefined) {
283
- try {
284
- configuredMcpServerNames = Object.keys((await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath)).mcpServers ?? {});
285
- }
286
- catch {
287
- // The tools section owns missing/malformed MCP-file diagnostics.
288
- }
289
- }
290
- let adapterToolNames = [];
291
- try {
292
- const settings = await resolveAdapterSendToolsSettings(input, {
293
- allowedTools: config.tools.allowedTools,
294
- disallowedTools: config.tools.disallowedTools,
295
- });
296
- if (settings !== undefined)
297
- adapterToolNames = adapterSendToolNames(settings);
298
- }
299
- catch {
300
- // Channel/tools sections own adapter config diagnostics.
301
- }
302
- const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames);
303
275
  for (const driver of drivers) {
304
276
  if (driver.id !== "webhook" && driver.id !== "cron")
305
277
  continue;
@@ -313,50 +285,19 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
313
285
  }
314
286
  for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
315
287
  const hasModelOverride = typeof entry.model === "string";
316
- const hasEffortOverride = typeof entry.effort === "string";
317
- if (!hasModelOverride && !hasEffortOverride)
288
+ if (!hasModelOverride)
318
289
  continue;
319
290
  try {
320
- const model = hasModelOverride
321
- ? parseMonoRuntimeModelReference(entry.model)
322
- : config.runtime.model;
323
- const location = hasModelOverride
324
- ? `${entryPath}.model=${entry.model}`
325
- : `${entryPath}.effort=${entry.effort}`;
326
- if (hasModelOverride) {
327
- const resolutionIssue = piModelResolutionIssue(config, model);
328
- if (resolutionIssue !== undefined) {
329
- piModelResolutionFailures.push(`${location}: ${resolutionIssue}.`);
330
- }
331
- }
332
- if (routeSafety === "uniform" && hasModelOverride && (model.sdk === "codex") !== baseIsDirectCodex) {
333
- directBoundaryConflicts.push(location);
334
- }
335
- if (routeSafety === "uniform" && hasModelOverride && monoSandboxActive && (model.sdk === "claude" || model.sdk === "opencode" || model.sdk === "codex")) {
336
- sandboxBypasses.push(location);
337
- }
338
- if (hasModelOverride && restrictiveToolPolicy && model.sdk === "opencode") {
339
- toolPolicyBypasses.push(location);
340
- }
341
- const effectiveEffort = hasEffortOverride ? entry.effort : config.runtime.effort;
342
- const legacyFallbacks = (config.runtime.fallbacks?.length ?? 0) > 0
343
- ? []
344
- : config.runtime.fallbackModels ?? [];
345
- const directOpenCodeModels = [model, ...legacyFallbacks]
346
- .filter((candidate) => candidate.sdk === "opencode");
347
- if (directOpenCodeModels.length > 0 && effectiveEffort !== undefined) {
348
- effortBypasses.push(`${location} (effective effort=${effectiveEffort}) (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
349
- }
350
- if (directOpenCodeModels.length > 0 && Number(config.runtime.maxTurns) > 0) {
351
- turnCapBypasses.push(`${location} (runtime.maxTurns=${config.runtime.maxTurns}; direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
352
- }
353
- if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
354
- mcpBypasses.push(`${location} (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")}; MCP sources=${effectiveMcpSources.join("; ")})`);
355
- }
356
- if (directOpenCodeModels.length > 0
357
- && config.context.skillDisclosure === "index"
358
- && config.context.skillsRoot !== undefined) {
359
- skillBypasses.push(location);
291
+ const model = parseMonoRuntimeModelReference(entry.model);
292
+ const resolutionIssue = piModelResolutionIssue(config, model);
293
+ if (resolutionIssue !== undefined) {
294
+ // The parser accepted this value, but acceptance is no longer a length
295
+ // guarantee: the byte ceiling was removed because no constant could
296
+ // hold for every provider, so a legitimately long id parses. This is a
297
+ // diagnostic, not the protocol string, so it is bounded here rather
298
+ // than at the grammar.
299
+ const echoed = sanitizeModelReferenceText(entry.model, MODEL_REFERENCE_ECHO_MAX_BYTES);
300
+ piModelResolutionFailures.push(`${entryPath}.model=${echoed}: ${resolutionIssue}.`);
360
301
  }
361
302
  }
362
303
  catch {
@@ -364,14 +305,7 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
364
305
  }
365
306
  }
366
307
  }
367
- if (directBoundaryConflicts.length === 0
368
- && sandboxBypasses.length === 0
369
- && toolPolicyBypasses.length === 0
370
- && effortBypasses.length === 0
371
- && turnCapBypasses.length === 0
372
- && mcpBypasses.length === 0
373
- && skillBypasses.length === 0
374
- && piModelResolutionFailures.length === 0)
308
+ if (piModelResolutionFailures.length === 0)
375
309
  return;
376
310
  const index = sections.findIndex((section) => section.id === "runtime");
377
311
  if (index < 0)
@@ -382,63 +316,14 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
382
316
  status: "error",
383
317
  details: [
384
318
  ...runtime.details,
385
- ...(directBoundaryConflicts.length === 0
386
- ? []
387
- : [
388
- "Uniform route safety cannot cross the direct-Codex runtime boundary because tool and sandbox contracts would change mid-agent. Choose per-route-native to opt into explicit route-local contracts.",
389
- ...directBoundaryConflicts,
390
- ]),
391
- ...(sandboxBypasses.length === 0
392
- ? []
393
- : [
394
- "Claude or direct OpenCode model overrides cannot run under uniform route safety while mono-agent SRT is active; direct Codex is also provider-owned. Choose per-route-native only after reviewing the explicit route-local contracts.",
395
- ...sandboxBypasses,
396
- ]),
397
- ...(toolPolicyBypasses.length === 0
398
- ? []
399
- : [
400
- "Per-trigger direct OpenCode model overrides require an effective allow-all policy because OpenCode's provider-owned tool loop does not consume mono-agent allowedTools/disallowedTools.",
401
- ...toolPolicyBypasses,
402
- ]),
403
- ...(effortBypasses.length === 0
404
- ? []
405
- : [
406
- "Per-trigger direct OpenCode routes cannot receive runtime effort because the OpenCode SDK does not expose effort control.",
407
- ...effortBypasses,
408
- ]),
409
- ...(turnCapBypasses.length === 0
410
- ? []
411
- : [
412
- "Per-trigger direct OpenCode routes cannot enforce runtime.maxTurns; omit it, set it to 0, or use a runtime with a hard turn cap.",
413
- ...turnCapBypasses,
414
- ]),
415
- ...(mcpBypasses.length === 0
416
- ? []
417
- : [
418
- "Per-trigger direct OpenCode routes cannot receive configured or auto-provisioned MCP runtime options.",
419
- ...mcpBypasses,
420
- ]),
421
- ...(skillBypasses.length === 0
422
- ? []
423
- : [
424
- "Per-trigger direct OpenCode routes cannot use index skill disclosure because the bridge disables external/runtime skills; use full disclosure or a Pi runtime.",
425
- ...skillBypasses,
426
- ]),
427
- ...(piModelResolutionFailures.length === 0
428
- ? []
429
- : [
430
- "Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
431
- ...piModelResolutionFailures,
432
- ]),
319
+ "Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
320
+ ...piModelResolutionFailures,
433
321
  ],
434
322
  };
435
323
  }
436
324
  function isUnknownRecord(value) {
437
325
  return typeof value === "object" && value !== null && !Array.isArray(value);
438
326
  }
439
- function hasAllowAllOnlyToolPolicy(tools) {
440
- return isAllowAllTools(tools.allowedTools) && tools.disallowedTools.length === 0;
441
- }
442
327
  /** Adapter send tools each channel owns; an allowed entry needs BOTH the tool AND the enabled channel. */
443
328
  const CHANNEL_OWNED_SEND_TOOLS = {
444
329
  slack: ["SlackSendMessage"],
@@ -511,45 +396,28 @@ function runtimeSection(config) {
511
396
  const details = [];
512
397
  let status = "ok";
513
398
  const routes = configuredRuntimeRouteChecks(config);
514
- const routeSafety = config.runtime.routeSafety ?? "uniform";
515
- details.push(`Route safety: ${routeSafety}.`);
516
- if (routes.length > 1) {
517
- details.push(routeSafety === "per-route-native"
518
- ? "Mixed runtime families are allowed; every attempt uses its explicit route-native safety contract."
519
- : "Fallback routes use the uniform compatibility contract; validation fails closed when any route cannot represent a required capability.");
520
- }
521
- const directOpenCodeModels = routes.filter((route) => route.model.sdk === "opencode");
522
- if (Number(config.runtime.maxTurns) > 0 && directOpenCodeModels.length > 0) {
523
- status = "error";
524
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.map((route) => referenceOf(route.model)).join(", ")} cannot enforce runtime.maxTurns=${config.runtime.maxTurns}; omit it, set it to 0, or use a runtime with a hard turn cap.`);
525
- }
526
- if (directOpenCodeModels.length > 0
527
- && config.context.skillDisclosure === "index"
528
- && config.context.skillsRoot !== undefined) {
529
- status = "error";
530
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.map((route) => referenceOf(route.model)).join(", ")} cannot use context.skillDisclosure=index because runtime skills are disabled; use full disclosure or a Pi runtime.`);
531
- }
532
399
  for (const route of routes) {
533
400
  try {
534
- const support = describeMonoRuntimeSupport(route.model, route.executionMode);
401
+ const support = describeMonoRuntimeSupport(route.model);
535
402
  const resolutionIssue = piModelResolutionIssue(config, route.model);
536
- const effortIssue = runtimeRouteEffortIssue(config, route);
537
- if (support.compatible && resolutionIssue === undefined) {
538
- details.push(`${route.label} ${referenceOf(route.model)} runs on ${support.backend?.label ?? "unknown backend"} ` +
403
+ const effortWarning = runtimeRouteEffortWarning(config, route);
404
+ if (resolutionIssue === undefined) {
405
+ details.push(`${route.label} ${displayReferenceOf(route.model)} runs on ${support.backend.label} ` +
539
406
  `(effort: ${route.effort ?? "provider default"}).`);
540
407
  }
541
408
  else {
542
409
  status = "error";
543
- details.push(`${route.label} ${referenceOf(route.model)}: ${resolutionIssue ?? support.incompatibilityReason ?? "unsupported"}.`);
410
+ details.push(`${route.label} ${displayReferenceOf(route.model)}: ${resolutionIssue}.`);
544
411
  }
545
- if (effortIssue !== undefined) {
546
- status = "error";
547
- details.push(`${route.label} ${referenceOf(route.model)}: ${effortIssue}`);
412
+ if (effortWarning !== undefined) {
413
+ if (status !== "error")
414
+ status = "waiting";
415
+ details.push(effortWarning);
548
416
  }
549
417
  }
550
418
  catch (error) {
551
419
  status = "error";
552
- details.push(`${route.label} ${referenceOf(route.model)}: ${error instanceof Error ? error.message : String(error)}.`);
420
+ details.push(`${route.label} ${displayReferenceOf(route.model)}: ${displayReason(error)}.`);
553
421
  }
554
422
  }
555
423
  return { id: "runtime", label: "Runtime", status, details };
@@ -557,48 +425,52 @@ function runtimeSection(config) {
557
425
  function configuredRuntimeRouteChecks(config) {
558
426
  const primary = {
559
427
  label: "Primary model",
428
+ configPath: "runtime.effort",
560
429
  model: config.runtime.model,
561
430
  ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
562
- executionMode: config.runtime.executionMode,
563
431
  };
564
- if ((config.runtime.fallbacks?.length ?? 0) > 0) {
565
- return [
566
- primary,
567
- ...(config.runtime.fallbacks ?? []).map((fallback) => ({
568
- label: "Fallback model",
569
- model: fallback.model,
570
- ...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
571
- })),
572
- ];
573
- }
574
432
  return [
575
433
  primary,
576
- ...(config.runtime.fallbackModels ?? []).map((model) => ({
434
+ ...(config.runtime.fallbacks ?? []).map((fallback, index) => ({
577
435
  label: "Fallback model",
578
- model,
579
- ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
436
+ configPath: `runtime.fallbacks[${index}].effort`,
437
+ model: fallback.model,
438
+ ...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
580
439
  })),
581
440
  ];
582
441
  }
583
- function runtimeRouteEffortIssue(config, route) {
442
+ function runtimeRouteEffortWarning(config, route) {
584
443
  if (route.effort === undefined)
585
444
  return undefined;
586
- if (route.model.sdk === "opencode") {
587
- return `Direct OpenCode model ${referenceOf(route.model)} cannot receive runtime.effort=${route.effort}; the OpenCode SDK exposes no reasoning-effort input. Omit the route effort.`;
588
- }
589
- const metadata = resolveModelEffortLevels(route.model, config.providers?.local);
590
- if (metadata.effortLevels !== undefined && !metadata.effortLevels.includes(route.effort)) {
591
- return `effort=${route.effort} is unsupported by known model metadata; choose ${metadata.effortLevels.join(", ")}, or omit it for provider default.`;
592
- }
593
- if (metadata.reasoning === false && route.effort !== "none") {
594
- return `effort=${route.effort} is unsupported because known model metadata marks this route as non-reasoning; use none or provider default.`;
595
- }
596
- if (route.model.sdk === "claude"
597
- && (route.executionMode ?? defaultExecutionModeForModel(route.model)) === "sdk"
598
- && !["low", "medium", "high", "xhigh", "max"].includes(route.effort)) {
599
- return `effort=${route.effort} is unsupported by the Claude Agent SDK; choose low, medium, high, xhigh, max, or provider default.`;
600
- }
601
- return undefined;
445
+ // Always go through the shared resolver. Deriving the ladder from
446
+ // `thinkingLevelMap`'s KEYS treated an override table as the complete
447
+ // supported set: claude-fable-5 maps only {off, xhigh, max}, which yielded
448
+ // [none, xhigh, max] and made a perfectly valid `effort: medium` warn and
449
+ // recommend xhigh. The resolver reports [minimal, low, medium, high, xhigh,
450
+ // max] for that model, and it is the same one the catalog and pickers use --
451
+ // doctor disagreeing with what the selector offers is its own bug.
452
+ const resolved = resolveAdvertisedModelEffort(route.model, {
453
+ ...(config.providers?.local === undefined ? {} : { localProviders: config.providers.local }),
454
+ });
455
+ // A NON-REASONING route has no advertised ladder at all, so the level-list
456
+ // check below returns clean and `effort: high` was reported ready. Keep this
457
+ // ahead of that check: a local model can declare `reasoning_mode: "none"`
458
+ // WITH `reasoning_levels`, and "this route does not reason" is the accurate
459
+ // diagnosis there, not "pick another level".
460
+ if (resolved.reasoning === false && route.effort !== "none") {
461
+ return `[WARN] ${route.configPath}=${route.effort} is unsupported because known model metadata marks ${displayReferenceOf(route.model)} as non-reasoning; use none, or omit it for the provider default. The runtime remains permissive and will forward the configured value.`;
462
+ }
463
+ const advertised = resolved.effortLevels
464
+ ?.filter((level) => EFFORT_LEVELS.includes(level));
465
+ if (advertised === undefined || advertised.length === 0 || advertised.includes(route.effort))
466
+ return undefined;
467
+ const configuredIndex = EFFORT_LEVELS.indexOf(route.effort);
468
+ const nearest = advertised.reduce((best, candidate) => {
469
+ const candidateIndex = EFFORT_LEVELS.indexOf(candidate);
470
+ const bestIndex = EFFORT_LEVELS.indexOf(best);
471
+ return Math.abs(candidateIndex - configuredIndex) < Math.abs(bestIndex - configuredIndex) ? candidate : best;
472
+ });
473
+ return `[WARN] ${route.configPath}=${route.effort} is outside ${displayReferenceOf(route.model)}'s advertised effort levels (${advertised.join(", ")}); nearest advertised level: ${nearest}. The runtime remains permissive and will forward the configured value.`;
602
474
  }
603
475
  /**
604
476
  * Mirrors the Pi runtime's actual model-resolution boundary. Mono-agent only
@@ -607,17 +479,14 @@ function runtimeRouteEffortIssue(config, route) {
607
479
  * provider, `resolvePiRuntimeModel` performs an exact built-in catalog lookup.
608
480
  */
609
481
  function piModelResolutionIssue(config, model) {
610
- if (model.sdk !== "pi" || model.provider === undefined) {
611
- return undefined;
612
- }
613
482
  const localProvider = config.providers?.local?.find((provider) => provider.id === model.provider);
614
483
  if (localProvider !== undefined) {
615
484
  if (localProvider.enabled === false) {
616
- return `provider \`${model.provider}\` is disabled in providers.local`;
485
+ return `provider \`${displayText(model.provider)}\` is disabled in providers.local`;
617
486
  }
618
487
  const localModel = localProvider.models?.find((candidate) => candidate.name === model.model || candidate.alias === model.model);
619
488
  if (localModel?.enabled === false) {
620
- return `model \`${model.model}\` is disabled in providers.local for provider \`${model.provider}\``;
489
+ return `model \`${displayText(model.model)}\` is disabled in providers.local for provider \`${displayText(model.provider)}\``;
621
490
  }
622
491
  return undefined;
623
492
  }
@@ -625,7 +494,14 @@ function piModelResolutionIssue(config, model) {
625
494
  && getPiBuiltinModels(model.provider).some((candidate) => candidate.id === model.model)) {
626
495
  return undefined;
627
496
  }
628
- return (`pi model not found: ${model.provider}:${model.model}; no matching providers.local entry exists and Pi's built-in catalog has no exact model. ` +
497
+ // This is doctor's own validate-time diagnostic, not the runtime failure
498
+ // string. The one that must stay byte-identical for `NON_RETRYABLE_PROVIDER_RE`
499
+ // is built in agent-runtime's `ai/providers/pi-models.js`; nothing matches
500
+ // against this text. So the reference is bounded here, which matters now that
501
+ // the parser imposes no length ceiling and a legitimately long id reaches this
502
+ // line whole.
503
+ const reference = displayText(`${model.provider}:${model.model}`);
504
+ return (`pi model not found: ${reference}; no matching providers.local entry exists and Pi's built-in catalog has no exact model. ` +
629
505
  "The sibling Pi CLI models.json is not a mono-agent runtime source; add providers.local for a self-hosted provider or choose a built-in Pi model");
630
506
  }
631
507
  function isPiBuiltinProvider(provider) {
@@ -641,229 +517,21 @@ async function readPiAuthProviders(path) {
641
517
  return inspection;
642
518
  return { status: "ok", providers: inspection.auth };
643
519
  }
644
- /**
645
- * What each SDK-authenticated backend truthfully reads for credentials. Only the
646
- * env vars are statically checkable; the login paths are recorded so the warning
647
- * can stay honest (a logged-in user is fine and we must not claim otherwise).
648
- *
649
- * - claude (`claude:*`, sdk + cli): the Claude Code process authenticates from
650
- * `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` in
651
- * the env, OR from a `claude /login` subscription session stored outside the
652
- * environment (macOS Keychain / `~/.claude`), OR from a Bedrock/Vertex
653
- * configuration (`CLAUDE_CODE_USE_BEDROCK`/`CLAUDE_CODE_USE_VERTEX` + cloud
654
- * credentials) — none of which the env-key check can see, hence the hedge in
655
- * the warning. Its own error string is verbatim: "Claude Code authentication
656
- * failed. Run `claude /login` or configure ANTHROPIC_API_KEY,
657
- * ANTHROPIC_AUTH_TOKEN, or CLAUDE_CODE_OAUTH_TOKEN."
658
- * - codex (`codex:*`, cli): the Codex app-server authenticates from
659
- * `OPENAI_API_KEY` in the env, OR from a `codex login` ChatGPT session stored
660
- * in `~/.codex/auth.json` — also outside the environment.
661
- */
662
- const SDK_AUTH_SCHEMES = {
663
- claude: {
664
- envKeys: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"],
665
- loginCommand: "claude /login",
666
- loginDetail: "a `claude /login` subscription session or a Bedrock/Vertex configuration (CLAUDE_CODE_USE_BEDROCK/VERTEX)",
667
- failureHint: 'the first turn fails with an opaque "Claude Code process exited with code 1" that names nothing',
668
- statusCommand: "claude auth status --json",
669
- statusArgs: ["auth", "status", "--json"],
670
- },
671
- codex: {
672
- envKeys: ["OPENAI_API_KEY"],
673
- loginCommand: "codex login",
674
- loginDetail: "a `codex login` ChatGPT session (`~/.codex/auth.json`, outside the environment)",
675
- failureHint: "the first turn fails to authenticate",
676
- statusCommand: "codex login status",
677
- statusArgs: ["login", "status"],
678
- },
679
- };
680
- const SDK_AUTH_STATUS_TIMEOUT_MS = 5_000;
681
- const SDK_AUTH_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
682
- function isSdkAuthName(value) {
683
- return value === "claude" || value === "codex";
684
- }
685
- const defaultSdkAuthStatusExecFile = async (file, args, options) => {
686
- const { stdout } = await execFile(file, [...args], options);
687
- return { stdout };
688
- };
689
- /**
690
- * Performs the SDK's local, read-only login-status command. A zero Codex exit
691
- * confirms its external login; Claude additionally requires strict JSON with
692
- * `loggedIn: true`. Missing binaries, timeouts, non-zero exits, and malformed
693
- * output all fail closed without leaking command output into validation.
694
- */
695
- async function checkSdkExternalLoginStatus(sdk, env, cwd, run) {
696
- const scheme = SDK_AUTH_SCHEMES[sdk];
697
- try {
698
- const { stdout } = await run(sdk, scheme.statusArgs, {
699
- cwd,
700
- env,
701
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
702
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
703
- encoding: "utf8",
704
- });
705
- if (sdk === "codex") {
706
- return true;
707
- }
708
- const parsed = JSON.parse(stdout);
709
- return parsed !== null
710
- && typeof parsed === "object"
711
- && !Array.isArray(parsed)
712
- && parsed.loggedIn === true;
713
- }
714
- catch {
715
- return false;
716
- }
717
- }
718
- /** Read provider IDs directly from auth.json without launching mutation-capable OpenCode middleware. */
719
- async function inspectOpenCodeCredentialProviders(env) {
720
- if (env.OPENCODE_AUTH_CONTENT !== undefined) {
721
- return { status: "inline_auth_unsupported" };
722
- }
723
- const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
724
- ? env.XDG_DATA_HOME
725
- : join(typeof env.HOME === "string" && env.HOME.length > 0 ? env.HOME : homedir(), ".local", "share");
726
- const opencodeData = join(dataHome, "opencode");
727
- const marker = await regularCurrentUserFile(join(opencodeData, "opencode.db"));
728
- if (!marker)
729
- return { status: "migration_required" };
730
- const authPath = join(opencodeData, "auth.json");
731
- if (!(await regularCurrentUserFile(authPath)))
732
- return { status: "auth_missing" };
733
- try {
734
- const parsed = JSON.parse(await readFile(authPath, "utf8"));
735
- if (!isUnknownRecord(parsed))
736
- return { status: "auth_invalid" };
737
- const providers = new Set();
738
- for (const [provider, credential] of Object.entries(parsed)) {
739
- if (provider.length === 0 || provider.trim() !== provider || !isOpenCodeCredentialEntry(credential)) {
740
- return { status: "auth_invalid" };
741
- }
742
- providers.add(provider);
743
- }
744
- return { status: "ok", providers };
745
- }
746
- catch {
747
- return { status: "auth_invalid" };
748
- }
749
- }
750
- async function regularCurrentUserFile(path) {
751
- try {
752
- const info = await lstat(path);
753
- return info.isFile() && (typeof process.getuid !== "function" || info.uid === process.getuid());
754
- }
755
- catch {
756
- return false;
757
- }
758
- }
759
- function isOpenCodeCredentialEntry(value) {
760
- if (!isUnknownRecord(value))
761
- return false;
762
- if (value.type === "oauth") {
763
- return typeof value.refresh === "string"
764
- && typeof value.access === "string"
765
- && (value.refresh.trim().length > 0 || value.access.trim().length > 0)
766
- && typeof value.expires === "number"
767
- && Number.isSafeInteger(value.expires)
768
- && value.expires >= 0
769
- && (value.accountId === undefined || typeof value.accountId === "string")
770
- && (value.enterpriseUrl === undefined || typeof value.enterpriseUrl === "string");
771
- }
772
- if (value.type === "api") {
773
- return typeof value.key === "string"
774
- && value.key.trim().length > 0
775
- && (value.metadata === undefined
776
- || (isUnknownRecord(value.metadata) && Object.values(value.metadata).every((entry) => typeof entry === "string")));
777
- }
778
- if (value.type === "wellknown") {
779
- return typeof value.key === "string" && value.key.trim().length > 0
780
- && typeof value.token === "string" && value.token.trim().length > 0;
781
- }
782
- return false;
783
- }
784
- async function checkOpenCodeVersion(env, cwd, run) {
785
- const versionEnv = {};
786
- for (const key of [
787
- "PATH",
788
- "LANG",
789
- "LC_ALL",
790
- "LC_CTYPE",
791
- "SHELL",
792
- "TMPDIR",
793
- "TMP",
794
- "TEMP",
795
- "SystemRoot",
796
- "WINDIR",
797
- "ComSpec",
798
- "PATHEXT",
799
- ]) {
800
- const value = env[key];
801
- if (typeof value === "string" && value.length > 0)
802
- versionEnv[key] = value;
803
- }
804
- try {
805
- const { stdout } = await run("opencode", ["--version"], {
806
- cwd,
807
- env: versionEnv,
808
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
809
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
810
- encoding: "utf8",
811
- });
812
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/u.exec(stdout.trim());
813
- if (match === null)
814
- return false;
815
- const major = Number(match[1]);
816
- const minor = Number(match[2]);
817
- const patch = Number(match[3]);
818
- return Number.isSafeInteger(major)
819
- && Number.isSafeInteger(minor)
820
- && Number.isSafeInteger(patch)
821
- && (major > 1 || (major === 1 && minor >= 15));
822
- }
823
- catch {
824
- return false;
825
- }
826
- }
827
- /** First env key whose value is present and non-blank, or undefined when none are set. */
828
- function firstPresentEnvKey(env, keys) {
829
- for (const key of keys) {
830
- const value = env[key];
831
- if (typeof value === "string" && value.trim().length > 0) {
832
- return key;
833
- }
834
- }
835
- return undefined;
836
- }
837
520
  /**
838
521
  * Checks that every referenced model (primary, fallbacks, and the agent-host memory LLM)
839
522
  * has discoverable credentials, so a keyless or expired-OAuth provider is caught at
840
523
  * `validate` time instead of degrading crons/memory silently at runtime (the failure mode
841
- * that broke memory capture for ~10 days: the auth store's OAuth token had quietly expired,
842
- * and the E1 fresh-instance case where `claude:*` with no `ANTHROPIC_API_KEY` "validates"
843
- * clean but the first turn dies with an opaque "process exited with code 1").
844
- *
845
- * The check is read-only. Static validation never launches a process; live validation may
846
- * run bounded, local SDK login-status commands, which inspect durable external login state
847
- * without making a model turn or mutating the auth store.
524
+ * that broke memory capture for ~10 days when an auth-store OAuth token quietly expired.
848
525
  *
849
- * - Pi providers: inspect the auth store (`piAuthPath`) and the config's own
526
+ * The read-only check inspects the auth store (`piAuthPath`) and the config's own
850
527
  * `providers.local` custom providers. A custom/local provider follows its declared
851
528
  * `apiKey` / `apiKeyEnv` contract instead of Pi OAuth; an OAuth provider absent from
852
529
  * the store, or whose access token has expired, is flagged `waiting` with a re-auth hint.
853
- * - SDK-authenticated providers (`claude:*` / `codex:*`): inspect the RESOLVED ENV (process
854
- * env + loaded `.env`) for the backend's accepted keys. During live validation only, a
855
- * missing env credential falls back to `claude auth status --json` / `codex login status`.
856
- * The commands use the same resolved environment (including PATH and HOME), are bounded,
857
- * cached once per SDK, and never make a model turn. Static validation remains process-free.
858
- * - Direct OpenCode (`opencode:<provider>:<model>`): inspect exact provider IDs in the
859
- * standard auth.json and require the native DB migration marker. Static validation launches
860
- * no process. Live validation runs only a bounded `opencode --version` preflight; it never
861
- * runs the mutation-capable auth middleware or makes a model turn.
862
530
  *
863
531
  * `waiting` (never `error`) keeps the verdict non-fatal, mirroring the Ollama/Phoenix
864
532
  * probes — the goal is visibility, not blocking start.
865
533
  */
866
- async function credentialsSection(config, env, cwd, liveness, verifiedCredentialModelRefs = [], sdkAuthStatusExecFile = defaultSdkAuthStatusExecFile, staticTriggerRefs = []) {
534
+ async function credentialsSection(config, env, verifiedCredentialModelRefs = [], staticTriggerRefs = []) {
867
535
  const refs = [
868
536
  { label: "Primary", ref: config.runtime.model },
869
537
  ...configuredRuntimeRouteChecks(config).slice(1).map((route) => ({
@@ -872,7 +540,7 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
872
540
  })),
873
541
  ...staticTriggerRefs,
874
542
  ];
875
- if (config.memory?.llm !== undefined && config.memory.llm.provider !== "ollama") {
543
+ if (config.memory?.llm?.provider === "agent-host") {
876
544
  try {
877
545
  refs.push({ label: "Memory LLM", ref: parseMonoRuntimeModelReference(config.memory.llm.model) });
878
546
  }
@@ -880,9 +548,7 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
880
548
  // A malformed memory model reference is surfaced by the memory/runtime shape checks.
881
549
  }
882
550
  }
883
- const authenticatedRefs = refs.filter((r) => (r.ref.sdk === "pi" && typeof r.ref.provider === "string")
884
- || (r.ref.sdk === "opencode" && typeof r.ref.provider === "string")
885
- || isSdkAuthName(r.ref.sdk));
551
+ const authenticatedRefs = refs;
886
552
  if (authenticatedRefs.length === 0) {
887
553
  return {
888
554
  id: "credentials",
@@ -895,102 +561,19 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
895
561
  let status = "ok";
896
562
  const verified = new Set(verifiedCredentialModelRefs);
897
563
  for (const { label, ref } of authenticatedRefs) {
898
- const refStr = referenceOf(ref);
899
- if (verified.has(refStr)) {
900
- details.push(`${label} ${refStr}: credentials verified by a successful live model check.`);
564
+ // The set holds whole canonical references, so the LOOKUP key stays whole;
565
+ // only the printed form is bounded.
566
+ if (verified.has(referenceOf(ref))) {
567
+ details.push(`${label} ${displayReferenceOf(ref)}: credentials verified by a successful live model check.`);
901
568
  }
902
569
  }
903
570
  const unverifiedRefs = authenticatedRefs.filter(({ ref }) => !verified.has(referenceOf(ref)));
904
- const piRefs = unverifiedRefs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
905
- const openCodeRefs = unverifiedRefs.filter((r) => r.ref.sdk === "opencode" && typeof r.ref.provider === "string");
906
- const sdkRefs = unverifiedRefs.filter((r) => isSdkAuthName(r.ref.sdk));
907
- if (piRefs.length > 0) {
908
- const piStatus = await appendPiCredentialDetails(config, piRefs, details, env);
571
+ if (unverifiedRefs.length > 0) {
572
+ const piStatus = await appendPiCredentialDetails(config, unverifiedRefs, details, env);
909
573
  if (piStatus === "waiting") {
910
574
  status = "waiting";
911
575
  }
912
576
  }
913
- const openCodeInspection = openCodeRefs.length > 0
914
- ? inspectOpenCodeCredentialProviders(env)
915
- : undefined;
916
- const openCodeVersion = liveness && openCodeRefs.length > 0
917
- ? checkOpenCodeVersion(env, cwd, sdkAuthStatusExecFile)
918
- : undefined;
919
- const sdkAuthStatuses = new Map();
920
- const externalLoginStatus = (sdk) => {
921
- const cached = sdkAuthStatuses.get(sdk);
922
- if (cached !== undefined) {
923
- return cached;
924
- }
925
- const pending = checkSdkExternalLoginStatus(sdk, env, cwd, sdkAuthStatusExecFile);
926
- sdkAuthStatuses.set(sdk, pending);
927
- return pending;
928
- };
929
- // Start each unique local status check before awaiting details so two SDKs
930
- // cost one bounded timeout window rather than running serially.
931
- if (liveness) {
932
- for (const { ref } of sdkRefs) {
933
- const scheme = SDK_AUTH_SCHEMES[ref.sdk];
934
- if (scheme !== undefined && firstPresentEnvKey(env, scheme.envKeys) === undefined) {
935
- void externalLoginStatus(ref.sdk);
936
- }
937
- }
938
- }
939
- for (const { label, ref } of sdkRefs) {
940
- const refStr = referenceOf(ref);
941
- const scheme = SDK_AUTH_SCHEMES[ref.sdk];
942
- if (scheme === undefined) {
943
- continue;
944
- }
945
- const present = firstPresentEnvKey(env, scheme.envKeys);
946
- if (present !== undefined) {
947
- details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}); credential detected, live model verification is still pending.`);
948
- continue;
949
- }
950
- if (liveness && await externalLoginStatus(ref.sdk)) {
951
- details.push(`${label} ${refStr}: external sign-in detected by read-only \`${scheme.statusCommand}\`; ` +
952
- "credentials are not verified until a live model turn succeeds.");
953
- continue;
954
- }
955
- status = "waiting";
956
- details.push(`[WARN] ${label} ${refStr}: no SDK credential in the resolved env (checked ${scheme.envKeys.join(", ")}). ` +
957
- (liveness
958
- ? `External login was not verified by \`${scheme.statusCommand}\`; `
959
- : `If you authenticated via ${scheme.loginDetail} this is fine and can't be verified during static validation; `) +
960
- `otherwise ${scheme.failureHint} — set ${scheme.envKeys[0]} or run \`${scheme.loginCommand}\`.`);
961
- }
962
- if (openCodeRefs.length > 0) {
963
- const inspection = openCodeInspection === undefined ? undefined : await openCodeInspection;
964
- const supportedVersion = openCodeVersion === undefined ? false : await openCodeVersion;
965
- for (const { label, ref } of openCodeRefs) {
966
- const refStr = referenceOf(ref);
967
- const provider = ref.provider;
968
- const credentialPresent = inspection?.status === "ok" && inspection.providers.has(provider);
969
- if (credentialPresent && liveness && supportedVersion) {
970
- details.push(`${label} ${refStr}: provider \`${provider}\` credential present in the standard OpenCode auth store; stable OpenCode CLI >=1.15.0 detected without a model turn. Credential detected; live model verification is still pending.`);
971
- }
972
- else {
973
- status = "waiting";
974
- const warning = inspection?.status === "migration_required"
975
- ? "the native OpenCode database migration marker is missing or invalid; run `opencode db migrate --pure` once"
976
- : inspection?.status === "inline_auth_unsupported"
977
- ? "OPENCODE_AUTH_CONTENT is unsupported for direct runs; persist credentials with `opencode auth login` and unset it"
978
- : inspection?.status === "auth_invalid"
979
- ? "the standard OpenCode auth.json is malformed or contains an unsupported credential entry"
980
- : credentialPresent && !liveness
981
- ? "credentials and migration marker are present, but the required stable OpenCode CLI >=1.15.0 is unverified during static validation"
982
- : credentialPresent && !supportedVersion
983
- ? "credentials are present, but stable OpenCode CLI >=1.15.0 could not be verified"
984
- : inspection?.status === "ok"
985
- ? `no exact credential entry exists for provider \`${provider}\`; run \`opencode auth login\` for that provider`
986
- : "the standard OpenCode auth.json is missing; run `opencode auth login`";
987
- const safetyNote = liveness
988
- ? "No model turn or mutation-capable OpenCode command was run."
989
- : "No OpenCode process was launched.";
990
- details.push(`[WARN] ${label} ${refStr}: ${warning}. ${safetyNote}`);
991
- }
992
- }
993
- }
994
577
  return { id: "credentials", label: "Provider credentials", status, details };
995
578
  }
996
579
  /**
@@ -1014,34 +597,38 @@ async function appendPiCredentialDetails(config, piRefs, details, env) {
1014
597
  }
1015
598
  for (const { label, ref } of piRefs) {
1016
599
  const provider = ref.provider;
1017
- const refStr = referenceOf(ref);
1018
- const loginCommand = piAuthRecoveryCommand(provider, authPath);
600
+ // Display-only from here down: `provider` is still the lookup key into the
601
+ // auth store and providers.local, `providerLabel` and `refStr` are what the
602
+ // detail lines quote back at the operator.
603
+ const providerLabel = displayText(provider);
604
+ const refStr = displayReferenceOf(ref);
605
+ const loginCommand = piAuthRecoveryCommand(providerLabel, authPath);
1019
606
  const localProvider = localProviders.get(provider);
1020
607
  if (localProvider !== undefined) {
1021
608
  if (localProvider.enabled === false) {
1022
609
  status = "waiting";
1023
- details.push(`[WARN] ${label} ${refStr}: provider \`${provider}\` is configured in providers.local but disabled (\`enabled: false\`); the runtime will throw \`provider disabled: ${provider}\` on the first turn. Set \`enabled: true\` on that providers.local entry.`);
610
+ details.push(`[WARN] ${label} ${refStr}: provider \`${providerLabel}\` is configured in providers.local but disabled (\`enabled: false\`); the runtime will throw \`provider disabled: ${providerLabel}\` on the first turn. Set \`enabled: true\` on that providers.local entry.`);
1024
611
  }
1025
612
  else if (localProvider.apiKey !== undefined) {
1026
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
613
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
1027
614
  }
1028
615
  else if (localProvider.apiKeyEnv !== undefined) {
1029
616
  if (hasNonEmptyCredentialValue(env[localProvider.apiKeyEnv])) {
1030
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local with ${localProvider.apiKeyEnv} present in the resolved environment; credential detected, live model verification is still pending.`);
617
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local with ${localProvider.apiKeyEnv} present in the resolved environment; credential detected, live model verification is still pending.`);
1031
618
  }
1032
619
  else {
1033
620
  status = "waiting";
1034
- details.push(`[WARN] ${label} ${refStr}: provider \`${provider}\` declares apiKeyEnv \`${localProvider.apiKeyEnv}\`, but the resolved environment has no non-empty value and no inline apiKey fallback. Set ${localProvider.apiKeyEnv} before starting.`);
621
+ details.push(`[WARN] ${label} ${refStr}: provider \`${providerLabel}\` declares apiKeyEnv \`${localProvider.apiKeyEnv}\`, but the resolved environment has no non-empty value and no inline apiKey fallback. Set ${localProvider.apiKeyEnv} before starting.`);
1035
622
  }
1036
623
  }
1037
624
  else {
1038
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (keyless local provider; no API key declared).`);
625
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local (keyless local provider; no API key declared).`);
1039
626
  }
1040
627
  continue;
1041
628
  }
1042
629
  const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
1043
630
  if (apiKeyEnv !== undefined && hasNonEmptyCredentialValue(env[apiKeyEnv])) {
1044
- details.push(`${label} ${refStr}: Pi API-key credential for \`${provider}\` present in the resolved environment (${apiKeyEnv}); credential detected, live model verification is still pending.`);
631
+ details.push(`${label} ${refStr}: Pi API-key credential for \`${providerLabel}\` present in the resolved environment (${apiKeyEnv}); credential detected, live model verification is still pending.`);
1045
632
  continue;
1046
633
  }
1047
634
  if (authInspection?.status === "unsafe") {
@@ -1056,37 +643,37 @@ async function appendPiCredentialDetails(config, piRefs, details, env) {
1056
643
  if (entry === undefined) {
1057
644
  status = "waiting";
1058
645
  details.push(apiKeyEnv === undefined
1059
- ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
1060
- : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
646
+ ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${providerLabel}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
647
+ : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${providerLabel}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
1061
648
  continue;
1062
649
  }
1063
650
  const isOAuth = entry.type === "oauth";
1064
651
  const isApiKey = entry.type === "api_key";
1065
652
  if (isApiKey && !hasNonEmptyCredentialValue(entry.key)) {
1066
653
  status = "waiting";
1067
- details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${provider}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
654
+ details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${providerLabel}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
1068
655
  continue;
1069
656
  }
1070
657
  if (isOAuth && !hasNonEmptyCredentialValue(entry.access) && !hasNonEmptyCredentialValue(entry.refresh)) {
1071
658
  status = "waiting";
1072
- details.push(`[WARN] ${label} ${refStr}: stored OAuth credential for \`${provider}\` has no usable access or refresh token. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
659
+ details.push(`[WARN] ${label} ${refStr}: stored OAuth credential for \`${providerLabel}\` has no usable access or refresh token. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
1073
660
  continue;
1074
661
  }
1075
662
  if (!isOAuth && !isApiKey) {
1076
663
  status = "waiting";
1077
- details.push(`[WARN] ${label} ${refStr}: stored credential for \`${provider}\` has an unsupported or missing type. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
664
+ details.push(`[WARN] ${label} ${refStr}: stored credential for \`${providerLabel}\` has an unsupported or missing type. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
1078
665
  continue;
1079
666
  }
1080
667
  const expired = typeof entry.expires === "number" && entry.expires < now;
1081
668
  const whenNote = typeof entry.expires === "number" ? ` ${new Date(entry.expires).toISOString()}` : "";
1082
669
  if (isOAuth && expired) {
1083
670
  status = "waiting";
1084
- details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${provider}\` expired${whenNote} — the runtime may auto-refresh, but this credential is not ready until a request succeeds; if runs fail with "No API key for provider: ${provider}" re-authenticate with \`${loginCommand}\`.`);
671
+ details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${providerLabel}\` expired${whenNote} — the runtime may auto-refresh, but this credential is not ready until a request succeeds; if runs fail with "No API key for provider: ${providerLabel}" re-authenticate with \`${loginCommand}\`.`);
1085
672
  continue;
1086
673
  }
1087
674
  details.push(isOAuth
1088
- ? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
1089
- : `${label} ${refStr}: API key credentials for \`${provider}\` present; credential detected, live model verification is still pending.`);
675
+ ? `${label} ${refStr}: OAuth credentials for \`${providerLabel}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
676
+ : `${label} ${refStr}: API key credentials for \`${providerLabel}\` present; credential detected, live model verification is still pending.`);
1090
677
  }
1091
678
  return status;
1092
679
  }
@@ -1248,12 +835,12 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1248
835
  const resolutionIssue = piModelResolutionIssue(config, model);
1249
836
  if (resolutionIssue !== undefined) {
1250
837
  status = "error";
1251
- details.push(`Agent-host memory LLM ${referenceOf(model)}: ${resolutionIssue}.`);
838
+ details.push(`Agent-host memory LLM ${displayReferenceOf(model)}: ${resolutionIssue}.`);
1252
839
  }
1253
840
  }
1254
841
  catch (error) {
1255
842
  status = "error";
1256
- details.push(`Agent-host memory LLM ${config.memory.llm.model}: ${error instanceof Error ? error.message : String(error)}.`);
843
+ details.push(`Agent-host memory LLM ${displayText(config.memory.llm.model)}: ${displayReason(error)}.`);
1257
844
  }
1258
845
  }
1259
846
  }
@@ -1573,12 +1160,12 @@ async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
1573
1160
  }
1574
1161
  if (provider === "ollama" && /HTTP 404/u.test(reason)) {
1575
1162
  return [
1576
- `[WARN] Ollama embedding model ${embeddings.model} could not be proved at ${endpoint} (${reason}); ` +
1577
- `run \`ollama pull ${embeddings.model}\` and verify its embedding capability.`,
1163
+ `[WARN] Ollama embedding model ${displayText(embeddings.model)} could not be proved at ${endpoint} (${reason}); ` +
1164
+ `run \`ollama pull ${displayText(embeddings.model)}\` and verify its embedding capability.`,
1578
1165
  ];
1579
1166
  }
1580
1167
  return [
1581
- `[WARN] ${label} embedding readiness failed for ${embeddings.model} at ${endpoint} (${reason}). ` +
1168
+ `[WARN] ${label} embedding readiness failed for ${displayText(embeddings.model)} at ${endpoint} (${reason}). ` +
1582
1169
  "Verify the selected model, authentication, and configured dimension.",
1583
1170
  ];
1584
1171
  }
@@ -1620,8 +1207,8 @@ function memoryLlmLabel(llm) {
1620
1207
  return "none";
1621
1208
  }
1622
1209
  return llm.provider === "ollama"
1623
- ? `ollama:${llm.model}`
1624
- : `agent-host:${llm.model}${llm.executionMode === undefined ? "" : ` (${llm.executionMode})`}`;
1210
+ ? `ollama:${displayText(llm.model)}`
1211
+ : `agent-host:${displayText(llm.model)}`;
1625
1212
  }
1626
1213
  async function toolsSection(config, input) {
1627
1214
  const details = [];
@@ -1650,6 +1237,32 @@ async function toolsSection(config, input) {
1650
1237
  details.push(`Allowed tools: ${allowedTools.join(", ")}.`);
1651
1238
  let mcpNoteAdded = false;
1652
1239
  for (const name of allowedTools) {
1240
+ if (isRememberToolPolicyName(name)) {
1241
+ // Reconcile with the runtime's own resolution, which honours the
1242
+ // `mcp__mono-agent-memory-write__*` spellings and deny-wins. This check
1243
+ // must precede the generic MCP branch below.
1244
+ if (!isRememberToolAllowed(config.tools)) {
1245
+ status = "waiting";
1246
+ details.push(`${name} is listed but tool policy still resolves to denied (deny wins, including the mcp__mono-agent-memory-write__* spellings) - the tool will not be offered.`);
1247
+ continue;
1248
+ }
1249
+ // Unlike MemoryRecall, Remember IS allowlist-gated, so listing it is
1250
+ // correct and required under a restrictive policy. It still needs a
1251
+ // configured memory block with the write surface left enabled.
1252
+ if (config.memory === undefined) {
1253
+ status = "waiting";
1254
+ details.push(`${name} is in allowedTools but no memory block is configured - durable writes will not work. Configure memory (or remove this entry).`);
1255
+ }
1256
+ else if (config.memory.rememberTool?.enabled === false) {
1257
+ status = "waiting";
1258
+ details.push(`${name} is in allowedTools but memory.rememberTool.enabled is off - durable writes will not work. Enable memory.rememberTool (or remove this entry).`);
1259
+ }
1260
+ else if (config.memory.backend === "supermemory") {
1261
+ status = "waiting";
1262
+ details.push(`${name} is in allowedTools but the supermemory backend exposes no durable write surface - the tool will not be offered.`);
1263
+ }
1264
+ continue;
1265
+ }
1653
1266
  if (isMcpToolName(name)) {
1654
1267
  // MCP tool names are owned by their servers; we cannot verify them offline.
1655
1268
  if (!mcpNoteAdded) {
@@ -1685,45 +1298,6 @@ async function toolsSection(config, input) {
1685
1298
  // Under allow-all the disallow list is already folded into the "except" clause above.
1686
1299
  details.push(`Disallowed tools: ${config.tools.disallowedTools.join(", ")}.`);
1687
1300
  }
1688
- const runtimeModels = configuredRuntimeModels(config.runtime);
1689
- const directCodexModels = runtimeModels
1690
- .filter((model) => model.sdk === "codex")
1691
- .map(referenceOf);
1692
- const directOpenCodeModels = runtimeModels
1693
- .filter((model) => model.sdk === "opencode")
1694
- .map(referenceOf);
1695
- const claudeCliModels = [
1696
- { model: config.runtime.model, executionMode: config.runtime.executionMode },
1697
- ...configuredRuntimeFallbackModels(config.runtime).map((model) => ({
1698
- model,
1699
- executionMode: defaultExecutionModeForModel(model),
1700
- })),
1701
- ]
1702
- .filter(({ model, executionMode }) => model.sdk === "claude" && executionMode === "cli")
1703
- .map(({ model }) => referenceOf(model));
1704
- const allowAllOnly = hasAllowAllOnlyToolPolicy(config.tools);
1705
- const dedicatedNoToolsProbe = input.codexNoToolsProbe === true
1706
- && allowedTools.length === 0
1707
- && config.tools.disallowedTools.length === 0;
1708
- if (directCodexModels.length > 0
1709
- && !dedicatedNoToolsProbe
1710
- && !allowAllOnly) {
1711
- status = "error";
1712
- details.push(`Direct Codex model${directCodexModels.length === 1 ? "" : "s"} ${directCodexModels.join(", ")} cannot enforce ` +
1713
- "tools.allowedTools/tools.disallowedTools. Use allow-all-only (allowedTools omitted or containing \"*\", with no disallowedTools), " +
1714
- "or select a runtime that supports restrictive tool policies.");
1715
- }
1716
- if (directOpenCodeModels.length > 0 && !allowAllOnly) {
1717
- status = "error";
1718
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot enforce ` +
1719
- "tools.allowedTools/tools.disallowedTools. Use allow-all-only (allowedTools omitted or containing \"*\", with no disallowedTools), " +
1720
- "or use a Pi runtime (including pi:opencode-go:*).");
1721
- }
1722
- if (claudeCliModels.length > 0 && allowedTools.length === 0) {
1723
- status = "error";
1724
- details.push(`Claude CLI model${claudeCliModels.length === 1 ? "" : "s"} ${claudeCliModels.join(", ")} cannot enforce an empty ` +
1725
- "tools.allowedTools list because omitting --tools enables Claude Code's default tool set. Use Claude SDK for a chat-only agent, or configure a non-empty enforceable tool list.");
1726
- }
1727
1301
  let configuredMcpServerNames = [];
1728
1302
  let configuredMcpServers = {};
1729
1303
  if (config.tools.mcpConfigPath !== undefined) {
@@ -1781,7 +1355,6 @@ async function toolsSection(config, input) {
1781
1355
  const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
1782
1356
  allowedTools: config.tools.allowedTools,
1783
1357
  disallowedTools: config.tools.disallowedTools,
1784
- suppressInteractionTools: directOpenCodeModels.length > 0,
1785
1358
  });
1786
1359
  if (adapterSendTools === undefined) {
1787
1360
  details.push("No adapter-derived send tools enabled.");
@@ -1789,19 +1362,13 @@ async function toolsSection(config, input) {
1789
1362
  else {
1790
1363
  details.push(`Adapter send tools: ${adapterSendToolNames(adapterSendTools).join(", ")}.`);
1791
1364
  }
1792
- const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools, directOpenCodeModels.length > 0);
1365
+ const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools);
1793
1366
  if (blockedAdapterEndpoints.length > 0) {
1794
1367
  if (status !== "error") {
1795
1368
  status = "waiting";
1796
1369
  }
1797
1370
  details.push(...blockedAdapterEndpoints);
1798
1371
  }
1799
- const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterSendTools === undefined ? [] : adapterSendToolNames(adapterSendTools));
1800
- if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
1801
- status = "error";
1802
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot safely consume ` +
1803
- `MCP runtime options from ${effectiveMcpSources.join("; ")}. Disable those MCP sources or use a Pi runtime (including pi:opencode-go:*).`);
1804
- }
1805
1372
  // Subagents: the Agent tool is registered only when BOTH the capability is
1806
1373
  // enabled and the tool is allowed, so surface either half being missing.
1807
1374
  const subagents = config.subagents;
@@ -1824,14 +1391,6 @@ async function toolsSection(config, input) {
1824
1391
  }
1825
1392
  else {
1826
1393
  details.push("Subagents inherit the parent's skill index and the ReadSkill tool.");
1827
- // The parent's own chain is capability-checked elsewhere; a profile that
1828
- // pins its own model is not, so a direct-OpenCode profile would silently
1829
- // run without the index it was told it had.
1830
- for (const definition of subagents.definitions ?? []) {
1831
- if (definition.model?.sdk === "opencode") {
1832
- details.push(`Subagent "${definition.name}" is pinned to a direct OpenCode model, which cannot use ReadSkill; it runs without the skill index.`);
1833
- }
1834
- }
1835
1394
  }
1836
1395
  for (const tool of subagents.inline?.allowedTools ?? []) {
1837
1396
  if (!isKnownToolName(tool) && !isMcpToolName(tool)) {
@@ -2001,14 +1560,14 @@ async function probeSearxngEndpoint(endpoint) {
2001
1560
  }
2002
1561
  }
2003
1562
  async function readAgentBrowserVersion(command, input) {
2004
- const run = input.sdkAuthStatusExecFile ?? defaultSdkAuthStatusExecFile;
1563
+ const run = input.statusExecFile ?? defaultDoctorStatusExecFile;
2005
1564
  try {
2006
1565
  const { stdout } = await run(command, ["--version"], {
2007
1566
  cwd: input.cwd,
2008
1567
  env: Object.fromEntries(["PATH", "LANG", "LC_ALL", "LC_CTYPE", "SHELL", "TMPDIR", "TMP", "TEMP"]
2009
1568
  .flatMap((key) => typeof input.env[key] === "string" ? [[key, input.env[key]]] : [])),
2010
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
2011
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
1569
+ timeout: DOCTOR_STATUS_TIMEOUT_MS,
1570
+ maxBuffer: DOCTOR_STATUS_MAX_BUFFER_BYTES,
2012
1571
  encoding: "utf8",
2013
1572
  });
2014
1573
  const match = /(?:^|\s)v?(\d+)\.(\d+)\.(\d+)(?:\s|$)/u.exec(stdout.trim());
@@ -2166,7 +1725,7 @@ async function processJobsSection(config, input) {
2166
1725
  `Owner-only local state: ${settings.stateDir}.`,
2167
1726
  `Concurrency: ${String(settings.maxConcurrent)} global, ${String(settings.maxActivePerConversation)} per conversation, ${String(settings.maxQueued)} queued.`,
2168
1727
  `Caps: runtime=${String(settings.maxRuntimeMs)}ms, queue-age=${String(settings.maxQueueAgeMs)}ms, output=${String(settings.maxOutputBytes)} bytes, chain-depth=${String(settings.maxChainDepth)}.`,
2169
- `Runtime availability: Pi-native Exec/Bash only; configured primary is ${config.runtime.model.sdk}.`,
1728
+ `Runtime availability: Pi-native Exec/Bash only; configured primary provider is ${displayText(config.runtime.model.provider)}.`,
2170
1729
  ];
2171
1730
  const inspection = await inspectProcessJobState(input.cwd, settings.stateDir);
2172
1731
  return {
@@ -3205,7 +2764,7 @@ function formatContinuationHost(host) {
3205
2764
  * needs the remote adapter API (or the app-owned interaction bridge) admitted
3206
2765
  * by sandbox.network.
3207
2766
  */
3208
- async function adapterSendToolNetworkPolicyWarnings(config, input, settings, suppressInteractionTools) {
2767
+ async function adapterSendToolNetworkPolicyWarnings(config, input, settings) {
3209
2768
  if (config.sandbox === undefined || config.sandbox.mode !== "native") {
3210
2769
  return [];
3211
2770
  }
@@ -3225,7 +2784,7 @@ async function adapterSendToolNetworkPolicyWarnings(config, input, settings, sup
3225
2784
  url: settings.telegram.apiRoot ?? "https://api.telegram.org",
3226
2785
  });
3227
2786
  }
3228
- const askUserAllowed = !suppressInteractionTools && isAdapterSendToolAllowed("AskUser", {
2787
+ const askUserAllowed = isAdapterSendToolAllowed("AskUser", {
3229
2788
  allowedTools: config.tools.allowedTools,
3230
2789
  disallowedTools: config.tools.disallowedTools,
3231
2790
  });
@@ -3273,100 +2832,7 @@ function endpointHost(url) {
3273
2832
  return url;
3274
2833
  }
3275
2834
  }
3276
- function effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames) {
3277
- const sources = [];
3278
- if (configuredMcpServerNames.length > 0) {
3279
- sources.push(`tools.mcpConfigPath (${configuredMcpServerNames.join(", ")})`);
3280
- }
3281
- if (config.memory?.recallTool?.enabled === true) {
3282
- sources.push("memory.recallTool");
3283
- }
3284
- if (config.memory?.backend === "supermemory"
3285
- && config.memory.supermemory?.exposeMcpServer === true
3286
- && config.memory.supermemory.apiKey !== undefined) {
3287
- sources.push("memory.supermemory.exposeMcpServer");
3288
- }
3289
- if (adapterToolNames.length > 0) {
3290
- sources.push(`adapter send tools (${adapterToolNames.join(", ")})`);
3291
- }
3292
- return sources;
3293
- }
3294
2835
  async function sandboxSection(config, engine) {
3295
- const runtimeModels = configuredRuntimeModels(config.runtime);
3296
- const directCodexRefs = runtimeModels
3297
- .filter((model) => model.sdk === "codex")
3298
- .map((model) => model.reference ?? `codex:${model.model}`);
3299
- const claudeRefs = runtimeModels
3300
- .filter((model) => model.sdk === "claude")
3301
- .map((model) => model.reference ?? `claude:${model.model}`);
3302
- const directOpenCodeRefs = runtimeModels
3303
- .filter((model) => model.sdk === "opencode")
3304
- .map((model) => model.reference ?? `opencode:${model.provider ?? "unknown"}:${model.model}`);
3305
- const routeSafety = config.runtime.routeSafety ?? "uniform";
3306
- if (routeSafety === "per-route-native") {
3307
- const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
3308
- const piRoutes = runtimeModels.filter((model) => model.sdk === "pi");
3309
- const details = runtimeModels.map((model, index) => routeNativeSafetyDetail(model, index, config));
3310
- let status = !monoSandboxActive && piRoutes.length > 0 ? "disabled" : "ok";
3311
- if (monoSandboxActive && piRoutes.length > 0) {
3312
- const state = await resolveSandboxEffectiveState({
3313
- policy: config.sandbox,
3314
- ...(engine === undefined ? {} : { engine }),
3315
- });
3316
- const warning = sandboxEffectiveStateWarning(state);
3317
- details.push(`Pi route SRT policy: mode ${config.sandbox.mode}, network ${config.sandbox.network.mode}, fallback ${config.sandbox.fallback}.`, describeSandboxEffectiveState(state), ...(warning === undefined ? [] : [warning]));
3318
- if (warning !== undefined || state.effective === "blocked")
3319
- status = "waiting";
3320
- }
3321
- else if (piRoutes.length > 0) {
3322
- details.push("Pi route SRT policy: disabled; Bash and stdio MCP subprocesses run unsandboxed.");
3323
- }
3324
- else if (monoSandboxActive) {
3325
- details.push("The configured mono-agent SRT policy has no Pi route to enforce it; provider-owned route contracts below apply instead.");
3326
- status = "waiting";
3327
- }
3328
- if (monoSandboxActive && runtimeModels.some((model) => model.sdk !== "pi")) {
3329
- details.push("[WARN] Per-route-native explicitly does not project mono-agent readableRoots, writableRoots, denyWrite, or network rules onto non-Pi routes; review each route contract before start.");
3330
- status = "waiting";
3331
- }
3332
- if (runtimeModels.some((model) => model.sdk === "codex") && config.runtime.permissionMode === "bypassPermissions") {
3333
- status = "waiting";
3334
- }
3335
- return { id: "sandbox", label: "Sandbox", status, details };
3336
- }
3337
- const incompatibleDetails = [];
3338
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directCodexRefs.length > 0) {
3339
- const codexPosture = directCodexSandboxPosture(config.runtime.permissionMode);
3340
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern direct Codex runtime${directCodexRefs.length === 1 ? "" : "s"}: ${directCodexRefs.join(", ")}.`, `${codexPosture.detail} Remove the mono-agent sandbox block or use Pi when exact srt roots, denyWrite, or network policy are required.`);
3341
- }
3342
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && claudeRefs.length > 0) {
3343
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern Claude runtime${claudeRefs.length === 1 ? "" : "s"}: ${claudeRefs.join(", ")}.`, "Claude's provider-owned tool loop does not consume mono-agent sandboxPolicy. Set sandbox.mode to off, remove the sandbox block, or use a Pi runtime when mono-agent srt enforcement is required.");
3344
- }
3345
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directOpenCodeRefs.length > 0) {
3346
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern direct OpenCode runtime${directOpenCodeRefs.length === 1 ? "" : "s"}: ${directOpenCodeRefs.join(", ")}.`, "Direct OpenCode's provider-owned tool loop does not consume mono-agent sandboxPolicy. Set sandbox.mode to off, remove the sandbox block, or use a pi:opencode-go:* runtime when mono-agent srt enforcement is required.");
3347
- }
3348
- if (incompatibleDetails.length > 0) {
3349
- return {
3350
- id: "sandbox",
3351
- label: "Sandbox",
3352
- status: "error",
3353
- details: incompatibleDetails,
3354
- };
3355
- }
3356
- if (directCodexRefs.length > 0 && (config.sandbox === undefined || config.sandbox.mode === "off")) {
3357
- const posture = directCodexSandboxPosture(config.runtime.permissionMode);
3358
- return {
3359
- id: "sandbox",
3360
- label: "Sandbox",
3361
- status: posture.status,
3362
- details: [
3363
- posture.detail,
3364
- config.sandbox === undefined
3365
- ? "No mono-agent native srt policy is configured."
3366
- : "The mono-agent native srt policy is explicitly off; the Codex-native posture still applies.",
3367
- ],
3368
- };
3369
- }
3370
2836
  if (config.sandbox === undefined) {
3371
2837
  return { id: "sandbox", label: "Sandbox", status: "disabled", details: ["No sandbox policy configured."] };
3372
2838
  }
@@ -3394,44 +2860,6 @@ async function sandboxSection(config, engine) {
3394
2860
  details,
3395
2861
  };
3396
2862
  }
3397
- function routeNativeSafetyDetail(model, index, config) {
3398
- const label = index === 0 ? "Primary" : `Fallback ${index}`;
3399
- const ref = referenceOf(model);
3400
- if (model.sdk === "pi") {
3401
- if (config.sandbox === undefined || config.sandbox.mode === "off") {
3402
- return `${label} ${ref}: Pi-owned tools use mono-agent tool policy; SRT is disabled, so Bash and stdio MCP subprocesses run unsandboxed.`;
3403
- }
3404
- if (config.sandbox.fallback === "unsafe-host-process") {
3405
- return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy; its explicit unsafe-host-process fallback can run subprocesses unsandboxed when SRT is unavailable.`;
3406
- }
3407
- return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy and fail closed when it is unavailable.`;
3408
- }
3409
- if (model.sdk === "claude") {
3410
- return `${label} ${ref}: Claude provider-owned permissions apply; mono-agent SRT filesystem/network rules are not projected.`;
3411
- }
3412
- if (model.sdk === "codex") {
3413
- return `${label} ${ref}: ${directCodexSandboxPosture(config.runtime.permissionMode).detail}`;
3414
- }
3415
- return `${label} ${ref}: OpenCode provider-owned execution with an effective allow-all tool policy applies; mono-agent SRT rules are not projected.`;
3416
- }
3417
- function directCodexSandboxPosture(permissionMode) {
3418
- if (permissionMode === "bypassPermissions") {
3419
- return {
3420
- status: "waiting",
3421
- detail: "[WARN] Direct Codex bypassPermissions uses native danger-full-access with no filesystem or network sandbox; unattended approval prompts are still disabled.",
3422
- };
3423
- }
3424
- if (permissionMode === "plan") {
3425
- return {
3426
- status: "ok",
3427
- detail: "Direct Codex plan mode uses its native read-only sandbox with network disabled; unattended escalation requests are denied.",
3428
- };
3429
- }
3430
- return {
3431
- status: "ok",
3432
- detail: "Direct Codex default/acceptEdits mode uses its native workspace-write sandbox with network disabled; unattended escalation requests are denied.",
3433
- };
3434
- }
3435
2863
  async function channelSection(driver, input) {
3436
2864
  const id = `channel:${driver.id}`;
3437
2865
  try {
@@ -3464,6 +2892,44 @@ async function channelSection(driver, input) {
3464
2892
  function referenceOf(model) {
3465
2893
  return modelReferenceKey(model);
3466
2894
  }
2895
+ /**
2896
+ * Bounded, single-line rendering of operator-supplied text for a diagnostic.
2897
+ *
2898
+ * `referenceOf` is the canonical KEY -- it is compared against
2899
+ * `verifiedCredentialModelRefs`, which carries whole references -- so it must
2900
+ * stay unbounded. What gets PRINTED must not be. The parser deliberately has no
2901
+ * length ceiling any more (a reference cannot be truncated into validity, so a
2902
+ * ceiling there could only refuse a model that runs), which means acceptance
2903
+ * stopped being a length guarantee: a 500,000-byte reference parses today and
2904
+ * every one of these lines is echoed by `mono-agent validate`, the daemon log
2905
+ * and launchd's captured stdout. Escaping comes with the clamp, so a reference
2906
+ * carrying a newline also cannot forge a detail line that reads as doctor's own.
2907
+ */
2908
+ function displayText(value) {
2909
+ return sanitizeModelReferenceText(value, MODEL_REFERENCE_ECHO_MAX_BYTES);
2910
+ }
2911
+ /** Bounded display form of a model reference. Never use it as a map or set key. */
2912
+ function displayReferenceOf(model) {
2913
+ return displayText(referenceOf(model));
2914
+ }
2915
+ /**
2916
+ * Bounded display form of a thrown reason. A reason gets the larger budget because it is one
2917
+ * fixed repair sentence plus at most one echo of the value.
2918
+ *
2919
+ * Unwrap one layer first, as `modelReferenceReason` in @mono-agent/config and `reasonOf` in
2920
+ * trigger-overrides already do: the adapter's `message` wraps a 32-byte generic prefix around
2921
+ * a reason ALREADY at the full reason budget, so clamping the wrapped form spends the budget
2922
+ * on the prefix and pays for it out of the tail -- which is where the kernel parser puts the
2923
+ * concrete repair whenever it quotes the operator's value first.
2924
+ */
2925
+ function displayReason(error) {
2926
+ const reason = error instanceof RuntimeAdapterError && typeof error.details.reason === "string"
2927
+ ? error.details.reason
2928
+ : error instanceof Error
2929
+ ? error.message
2930
+ : String(error);
2931
+ return sanitizeModelReferenceText(reason, MODEL_REFERENCE_REASON_MAX_BYTES);
2932
+ }
3467
2933
  async function pathExists(path) {
3468
2934
  try {
3469
2935
  await stat(path);