@mono-agent/agent-app 0.6.2 → 0.8.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.
Files changed (211) hide show
  1. package/README.md +210 -19
  2. package/dist/adapter-send-proxy.d.ts +18 -0
  3. package/dist/adapter-send-proxy.d.ts.map +1 -0
  4. package/dist/adapter-send-proxy.js +229 -0
  5. package/dist/adapter-send-proxy.js.map +1 -0
  6. package/dist/adapter-send-tools-main.js +62 -4
  7. package/dist/adapter-send-tools-main.js.map +1 -1
  8. package/dist/adapter-send-tools.d.ts +33 -6
  9. package/dist/adapter-send-tools.d.ts.map +1 -1
  10. package/dist/adapter-send-tools.js +485 -87
  11. package/dist/adapter-send-tools.js.map +1 -1
  12. package/dist/app-config.d.ts.map +1 -1
  13. package/dist/app-config.js +8 -0
  14. package/dist/app-config.js.map +1 -1
  15. package/dist/app.d.ts +2 -0
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +528 -135
  18. package/dist/app.js.map +1 -1
  19. package/dist/broadcast-recorder.d.ts.map +1 -1
  20. package/dist/broadcast-recorder.js +24 -2
  21. package/dist/broadcast-recorder.js.map +1 -1
  22. package/dist/channels.d.ts.map +1 -1
  23. package/dist/channels.js +16 -6
  24. package/dist/channels.js.map +1 -1
  25. package/dist/cli.d.ts +76 -4
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +2102 -102
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config-reference.d.ts.map +1 -1
  30. package/dist/config-reference.js +195 -6
  31. package/dist/config-reference.js.map +1 -1
  32. package/dist/configuration-proposal-main.d.ts +3 -0
  33. package/dist/configuration-proposal-main.d.ts.map +1 -0
  34. package/dist/configuration-proposal-main.js +13 -0
  35. package/dist/configuration-proposal-main.js.map +1 -0
  36. package/dist/configuration-proposal-tool.d.ts +26 -0
  37. package/dist/configuration-proposal-tool.d.ts.map +1 -0
  38. package/dist/configuration-proposal-tool.js +139 -0
  39. package/dist/configuration-proposal-tool.js.map +1 -0
  40. package/dist/configured-agent.d.ts +12 -0
  41. package/dist/configured-agent.d.ts.map +1 -1
  42. package/dist/configured-agent.js +150 -18
  43. package/dist/configured-agent.js.map +1 -1
  44. package/dist/consumer-contract.d.ts +3 -0
  45. package/dist/consumer-contract.d.ts.map +1 -1
  46. package/dist/consumer-contract.js +44 -1
  47. package/dist/consumer-contract.js.map +1 -1
  48. package/dist/doctor.d.ts +27 -5
  49. package/dist/doctor.d.ts.map +1 -1
  50. package/dist/doctor.js +1197 -101
  51. package/dist/doctor.js.map +1 -1
  52. package/dist/first-run-managed-memory.d.ts +39 -0
  53. package/dist/first-run-managed-memory.d.ts.map +1 -0
  54. package/dist/first-run-managed-memory.js +521 -0
  55. package/dist/first-run-managed-memory.js.map +1 -0
  56. package/dist/first-run-readiness.d.ts +111 -0
  57. package/dist/first-run-readiness.d.ts.map +1 -0
  58. package/dist/first-run-readiness.js +757 -0
  59. package/dist/first-run-readiness.js.map +1 -0
  60. package/dist/index.d.ts +8 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +4 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/init.d.ts +88 -3
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +1212 -23
  67. package/dist/init.js.map +1 -1
  68. package/dist/interaction-bridge.d.ts +25 -1
  69. package/dist/interaction-bridge.d.ts.map +1 -1
  70. package/dist/interaction-bridge.js +281 -19
  71. package/dist/interaction-bridge.js.map +1 -1
  72. package/dist/local-configuration.d.ts +56 -0
  73. package/dist/local-configuration.d.ts.map +1 -0
  74. package/dist/local-configuration.js +914 -0
  75. package/dist/local-configuration.js.map +1 -0
  76. package/dist/memory-command.d.ts +4 -0
  77. package/dist/memory-command.d.ts.map +1 -1
  78. package/dist/memory-command.js +751 -27
  79. package/dist/memory-command.js.map +1 -1
  80. package/dist/memory-recall.d.ts +38 -10
  81. package/dist/memory-recall.d.ts.map +1 -1
  82. package/dist/memory-recall.js +82 -25
  83. package/dist/memory-recall.js.map +1 -1
  84. package/dist/memory-retrieval.d.ts +66 -0
  85. package/dist/memory-retrieval.d.ts.map +1 -0
  86. package/dist/memory-retrieval.js +332 -0
  87. package/dist/memory-retrieval.js.map +1 -0
  88. package/dist/modules/base.d.ts +6 -5
  89. package/dist/modules/base.d.ts.map +1 -1
  90. package/dist/modules/base.js +11 -7
  91. package/dist/modules/base.js.map +1 -1
  92. package/dist/modules/catalog.d.ts.map +1 -1
  93. package/dist/modules/catalog.js +54 -20
  94. package/dist/modules/catalog.js.map +1 -1
  95. package/dist/modules/index.d.ts +2 -2
  96. package/dist/modules/index.d.ts.map +1 -1
  97. package/dist/modules/index.js +1 -1
  98. package/dist/modules/index.js.map +1 -1
  99. package/dist/modules/known-tools.d.ts +3 -0
  100. package/dist/modules/known-tools.d.ts.map +1 -1
  101. package/dist/modules/known-tools.js +11 -1
  102. package/dist/modules/known-tools.js.map +1 -1
  103. package/dist/modules/types.d.ts +20 -6
  104. package/dist/modules/types.d.ts.map +1 -1
  105. package/dist/modules/types.js.map +1 -1
  106. package/dist/package-version.d.ts +3 -0
  107. package/dist/package-version.d.ts.map +1 -0
  108. package/dist/package-version.js +14 -0
  109. package/dist/package-version.js.map +1 -0
  110. package/dist/pi-auth-store-inspection.d.ts +21 -0
  111. package/dist/pi-auth-store-inspection.d.ts.map +1 -0
  112. package/dist/pi-auth-store-inspection.js +120 -0
  113. package/dist/pi-auth-store-inspection.js.map +1 -0
  114. package/dist/pi-oauth-login-main.d.ts +3 -0
  115. package/dist/pi-oauth-login-main.d.ts.map +1 -0
  116. package/dist/pi-oauth-login-main.js +14 -0
  117. package/dist/pi-oauth-login-main.js.map +1 -0
  118. package/dist/pi-oauth-login.d.ts +22 -0
  119. package/dist/pi-oauth-login.d.ts.map +1 -0
  120. package/dist/pi-oauth-login.js +79 -0
  121. package/dist/pi-oauth-login.js.map +1 -0
  122. package/dist/project-skills.d.ts +40 -0
  123. package/dist/project-skills.d.ts.map +1 -0
  124. package/dist/project-skills.js +573 -0
  125. package/dist/project-skills.js.map +1 -0
  126. package/dist/provider-setup.d.ts +117 -2
  127. package/dist/provider-setup.d.ts.map +1 -1
  128. package/dist/provider-setup.js +1505 -49
  129. package/dist/provider-setup.js.map +1 -1
  130. package/dist/readiness-probe-worker.d.ts +12 -0
  131. package/dist/readiness-probe-worker.d.ts.map +1 -0
  132. package/dist/readiness-probe-worker.js +276 -0
  133. package/dist/readiness-probe-worker.js.map +1 -0
  134. package/dist/readiness-probe.d.ts +100 -0
  135. package/dist/readiness-probe.d.ts.map +1 -0
  136. package/dist/readiness-probe.js +780 -0
  137. package/dist/readiness-probe.js.map +1 -0
  138. package/dist/request-model-override.d.ts +43 -1
  139. package/dist/request-model-override.d.ts.map +1 -1
  140. package/dist/request-model-override.js +178 -19
  141. package/dist/request-model-override.js.map +1 -1
  142. package/dist/run-history.d.ts +32 -0
  143. package/dist/run-history.d.ts.map +1 -0
  144. package/dist/run-history.js +859 -0
  145. package/dist/run-history.js.map +1 -0
  146. package/dist/runs-health.d.ts.map +1 -1
  147. package/dist/runs-health.js +12 -4
  148. package/dist/runs-health.js.map +1 -1
  149. package/dist/runtime-option-extensions.d.ts +14 -0
  150. package/dist/runtime-option-extensions.d.ts.map +1 -0
  151. package/dist/runtime-option-extensions.js +98 -0
  152. package/dist/runtime-option-extensions.js.map +1 -0
  153. package/dist/runtime-routes.d.ts +9 -0
  154. package/dist/runtime-routes.d.ts.map +1 -0
  155. package/dist/runtime-routes.js +13 -0
  156. package/dist/runtime-routes.js.map +1 -0
  157. package/dist/sandbox-manager.d.ts +69 -0
  158. package/dist/sandbox-manager.d.ts.map +1 -0
  159. package/dist/sandbox-manager.js +856 -0
  160. package/dist/sandbox-manager.js.map +1 -0
  161. package/dist/supermemory-plugin.d.ts +54 -0
  162. package/dist/supermemory-plugin.d.ts.map +1 -0
  163. package/dist/supermemory-plugin.js +136 -0
  164. package/dist/supermemory-plugin.js.map +1 -0
  165. package/dist/tui-command.d.ts +15 -0
  166. package/dist/tui-command.d.ts.map +1 -1
  167. package/dist/tui-command.js +46 -6
  168. package/dist/tui-command.js.map +1 -1
  169. package/dist/web-command.d.ts +8 -0
  170. package/dist/web-command.d.ts.map +1 -1
  171. package/dist/web-command.js +174 -17
  172. package/dist/web-command.js.map +1 -1
  173. package/dist/wizard/answers.d.ts +31 -4
  174. package/dist/wizard/answers.d.ts.map +1 -1
  175. package/dist/wizard/answers.js +145 -16
  176. package/dist/wizard/answers.js.map +1 -1
  177. package/dist/wizard/from-flags.d.ts +7 -0
  178. package/dist/wizard/from-flags.d.ts.map +1 -1
  179. package/dist/wizard/from-flags.js +36 -2
  180. package/dist/wizard/from-flags.js.map +1 -1
  181. package/dist/wizard/index.d.ts +4 -4
  182. package/dist/wizard/index.d.ts.map +1 -1
  183. package/dist/wizard/index.js +2 -2
  184. package/dist/wizard/index.js.map +1 -1
  185. package/dist/wizard/model-discovery.d.ts +33 -2
  186. package/dist/wizard/model-discovery.d.ts.map +1 -1
  187. package/dist/wizard/model-discovery.js +826 -108
  188. package/dist/wizard/model-discovery.js.map +1 -1
  189. package/dist/wizard/presets.d.ts +1 -1
  190. package/dist/wizard/presets.d.ts.map +1 -1
  191. package/dist/wizard/presets.js +7 -12
  192. package/dist/wizard/presets.js.map +1 -1
  193. package/dist/wizard/prompts.d.ts +56 -9
  194. package/dist/wizard/prompts.d.ts.map +1 -1
  195. package/dist/wizard/prompts.js +172 -20
  196. package/dist/wizard/prompts.js.map +1 -1
  197. package/dist/wizard/run.d.ts +59 -3
  198. package/dist/wizard/run.d.ts.map +1 -1
  199. package/dist/wizard/run.js +1133 -191
  200. package/dist/wizard/run.js.map +1 -1
  201. package/package.json +21 -16
  202. package/resources/srt/package-lock.json +66 -0
  203. package/resources/srt/package.json +9 -0
  204. package/schema/mono-agent.config.schema.json +381 -17
  205. package/skills/mono-agent-composer/SKILL.md +4 -4
  206. package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
  207. package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
  208. package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
  209. package/skills/mono-agent-composer/references/package-map.md +1 -1
  210. package/skills/mono-agent-composer/references/playbooks.md +8 -7
  211. package/skills/mono-agent-composer/references/validation.md +1 -1
package/dist/doctor.js CHANGED
@@ -1,9 +1,15 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
1
2
  import { constants } from "node:fs";
2
- import { access, mkdir, readFile, stat } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { access, lstat, mkdir, readFile, stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { isIP } from "node:net";
6
+ import { join } from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
4
9
  import { listRecordedRuns } from "@mono-agent/observability";
5
10
  import { serializeTraceSpans } from "@mono-agent/observability/otel";
6
- import { describeMonoRuntimeSupport, modelReferenceKey, parseMonoRuntimeModelReference } from "@mono-agent/runtime-adapter";
11
+ import { loadToolPolicyFromJsonFile } from "@mono-agent/agent-harness";
12
+ import { defaultExecutionModeForModel, describeMonoRuntimeSupport, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
7
13
  import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
8
14
  import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
9
15
  import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, resolveAppArtifactDir, resolveAppObservabilityExporters, } from "./app-config.js";
@@ -12,8 +18,15 @@ import { canonicalToolName, isAllowAllTools, isKnownToolName, isMcpToolName, sug
12
18
  import { collectChannelConfigViews } from "./channel-config-view.js";
13
19
  import { resolveChannelDrivers } from "./channels.js";
14
20
  import { findUnknownAppConfigWarnings } from "./config-reference.js";
21
+ import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interaction-bridge.js";
22
+ import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
15
23
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
16
- import { piAuthWorkingDirectory, piLoginCommandLine } from "./provider-setup.js";
24
+ import { piAuthRecoveryCommand } from "./provider-setup.js";
25
+ import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
26
+ import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
27
+ import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
28
+ import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
29
+ const execFile = promisify(execFileCallback);
17
30
  /**
18
31
  * Loads every config section the app would use at start and reports it
19
32
  * per-section, so an engineer can see exactly what would run, wait, or fail —
@@ -36,6 +49,7 @@ export async function validateMonoAgentFolder(options) {
36
49
  sections.push({ id: "core", label: "Core config", status: "error", details: [error.message] });
37
50
  }
38
51
  if (coreConfig !== undefined) {
52
+ const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(drivers, options);
39
53
  const jsonResult = await readMonoAgentConfigJson(options.configPath);
40
54
  // Channel secrets (bot tokens, API keys) live outside the core view, so the
41
55
  // placement check spans both: core sections + every channel's config view.
@@ -55,9 +69,9 @@ export async function validateMonoAgentFolder(options) {
55
69
  sections.push({ id: "secret-placement", label: "Config warnings", status: "waiting", details: configWarnings });
56
70
  }
57
71
  sections.push(runtimeSection(coreConfig));
58
- sections.push(await credentialsSection(coreConfig, options.env));
59
- sections.push(await contextSection(coreConfig));
60
- sections.push(await memorySection(coreConfig, liveness, allowFilesystemWrites));
72
+ sections.push(await credentialsSection(coreConfig, options.env, options.cwd, liveness, options.verifiedCredentialModelRefs, options.sdkAuthStatusExecFile, staticTriggerCredentialRefs));
73
+ sections.push(await contextSection(coreConfig, options.cwd));
74
+ sections.push(await memorySection(coreConfig, options.cwd, liveness, allowFilesystemWrites));
61
75
  sections.push(await toolsSection(coreConfig, options));
62
76
  sections.push(await sandboxSection(coreConfig, options.sandboxEngine));
63
77
  }
@@ -66,6 +80,9 @@ export async function validateMonoAgentFolder(options) {
66
80
  for (const driver of drivers) {
67
81
  sections.push(await channelSection(driver, options));
68
82
  }
83
+ if (coreConfig !== undefined) {
84
+ await applyRequestModelOverrideCompatibilityChecks(sections, coreConfig, drivers, options);
85
+ }
69
86
  // Cross-check the built `channel:*` statuses against the tool policy and annotate
70
87
  // the tools section (send-tool-allowed-but-channel-disabled, or channel-enabled-
71
88
  // but-no-send-tool). Only meaningful once coreConfig — and thus the tools section
@@ -78,6 +95,236 @@ export async function validateMonoAgentFolder(options) {
78
95
  ok: sections.every((section) => section.status !== "error"),
79
96
  };
80
97
  }
98
+ /** Enabled, statically configured webhook/cron entries that can actually execute. */
99
+ function staticTriggerConfigEntries(driverId, loaded) {
100
+ if (!isUnknownRecord(loaded))
101
+ return [];
102
+ if (driverId === "webhook" && loaded.enabled === false)
103
+ return [];
104
+ const entries = driverId === "webhook" ? loaded.endpoints : loaded.jobs;
105
+ if (!Array.isArray(entries))
106
+ return [];
107
+ return entries.flatMap((entry, index) => {
108
+ if (!isUnknownRecord(entry) || entry.enabled === false)
109
+ return [];
110
+ return [{
111
+ entryPath: `${driverId}.${driverId === "webhook" ? "endpoints" : "jobs"}[${index}]`,
112
+ entry,
113
+ }];
114
+ });
115
+ }
116
+ /**
117
+ * Collect static trigger model overrides for credential readiness. Dynamic
118
+ * webhook request-body overrides are intentionally unavailable at validate time.
119
+ */
120
+ async function collectStaticTriggerCredentialRefs(drivers, input) {
121
+ const refs = [];
122
+ const seen = new Set();
123
+ for (const driver of drivers) {
124
+ if (driver.id !== "webhook" && driver.id !== "cron")
125
+ continue;
126
+ let loaded;
127
+ try {
128
+ loaded = await driver.loadConfig(input);
129
+ }
130
+ catch {
131
+ // The channel section owns malformed/unreadable config diagnostics.
132
+ continue;
133
+ }
134
+ for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
135
+ if (typeof entry.model !== "string")
136
+ continue;
137
+ try {
138
+ const ref = parseMonoRuntimeModelReference(entry.model);
139
+ const key = `${entryPath}\u0000${modelReferenceKey(ref)}`;
140
+ if (seen.has(key))
141
+ continue;
142
+ seen.add(key);
143
+ refs.push({ label: entryPath, ref });
144
+ }
145
+ catch {
146
+ // The channel section owns invalid model-reference syntax.
147
+ }
148
+ }
149
+ }
150
+ return refs;
151
+ }
152
+ async function applyRequestModelOverrideCompatibilityChecks(sections, config, drivers, input) {
153
+ const baseIsDirectCodex = config.runtime.model.sdk === "codex";
154
+ const routeSafety = config.runtime.routeSafety ?? "uniform";
155
+ const directBoundaryConflicts = [];
156
+ const sandboxBypasses = [];
157
+ const toolPolicyBypasses = [];
158
+ const effortBypasses = [];
159
+ const turnCapBypasses = [];
160
+ const mcpBypasses = [];
161
+ const skillBypasses = [];
162
+ const piModelResolutionFailures = [];
163
+ const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
164
+ const restrictiveToolPolicy = !hasExactAllowAllToolPolicy(config.tools);
165
+ let configuredMcpServerNames = [];
166
+ if (config.tools.mcpConfigPath !== undefined) {
167
+ try {
168
+ configuredMcpServerNames = Object.keys((await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath)).mcpServers ?? {});
169
+ }
170
+ catch {
171
+ // The tools section owns missing/malformed MCP-file diagnostics.
172
+ }
173
+ }
174
+ let adapterToolNames = [];
175
+ try {
176
+ const settings = await resolveAdapterSendToolsSettings(input, {
177
+ allowedTools: config.tools.allowedTools,
178
+ disallowedTools: config.tools.disallowedTools,
179
+ });
180
+ if (settings !== undefined)
181
+ adapterToolNames = adapterSendToolNames(settings);
182
+ }
183
+ catch {
184
+ // Channel/tools sections own adapter config diagnostics.
185
+ }
186
+ const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames);
187
+ for (const driver of drivers) {
188
+ if (driver.id !== "webhook" && driver.id !== "cron")
189
+ continue;
190
+ let loaded;
191
+ try {
192
+ loaded = await driver.loadConfig(input);
193
+ }
194
+ catch {
195
+ // The channel section already reports malformed/unreadable config.
196
+ continue;
197
+ }
198
+ for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
199
+ const hasModelOverride = typeof entry.model === "string";
200
+ const hasEffortOverride = typeof entry.effort === "string";
201
+ if (!hasModelOverride && !hasEffortOverride)
202
+ continue;
203
+ try {
204
+ const model = hasModelOverride
205
+ ? parseMonoRuntimeModelReference(entry.model)
206
+ : config.runtime.model;
207
+ const location = hasModelOverride
208
+ ? `${entryPath}.model=${entry.model}`
209
+ : `${entryPath}.effort=${entry.effort}`;
210
+ if (hasModelOverride) {
211
+ const resolutionIssue = piModelResolutionIssue(config, model);
212
+ if (resolutionIssue !== undefined) {
213
+ piModelResolutionFailures.push(`${location}: ${resolutionIssue}.`);
214
+ }
215
+ }
216
+ if (routeSafety === "uniform" && hasModelOverride && (model.sdk === "codex") !== baseIsDirectCodex) {
217
+ directBoundaryConflicts.push(location);
218
+ }
219
+ if (routeSafety === "uniform" && hasModelOverride && monoSandboxActive && (model.sdk === "claude" || model.sdk === "opencode" || model.sdk === "codex")) {
220
+ sandboxBypasses.push(location);
221
+ }
222
+ if (hasModelOverride && restrictiveToolPolicy && model.sdk === "opencode") {
223
+ toolPolicyBypasses.push(location);
224
+ }
225
+ const effectiveEffort = hasEffortOverride ? entry.effort : config.runtime.effort;
226
+ const legacyFallbacks = (config.runtime.fallbacks?.length ?? 0) > 0
227
+ ? []
228
+ : config.runtime.fallbackModels ?? [];
229
+ const directOpenCodeModels = [model, ...legacyFallbacks]
230
+ .filter((candidate) => candidate.sdk === "opencode");
231
+ if (directOpenCodeModels.length > 0 && effectiveEffort !== undefined) {
232
+ effortBypasses.push(`${location} (effective effort=${effectiveEffort}) (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
233
+ }
234
+ if (directOpenCodeModels.length > 0 && Number(config.runtime.maxTurns) > 0) {
235
+ turnCapBypasses.push(`${location} (runtime.maxTurns=${config.runtime.maxTurns}; direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
236
+ }
237
+ if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
238
+ mcpBypasses.push(`${location} (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")}; MCP sources=${effectiveMcpSources.join("; ")})`);
239
+ }
240
+ if (directOpenCodeModels.length > 0
241
+ && config.context.skillDisclosure === "index"
242
+ && config.context.skillsRoot !== undefined) {
243
+ skillBypasses.push(location);
244
+ }
245
+ }
246
+ catch {
247
+ // Adapter configIssues owns syntax diagnostics.
248
+ }
249
+ }
250
+ }
251
+ if (directBoundaryConflicts.length === 0
252
+ && sandboxBypasses.length === 0
253
+ && toolPolicyBypasses.length === 0
254
+ && effortBypasses.length === 0
255
+ && turnCapBypasses.length === 0
256
+ && mcpBypasses.length === 0
257
+ && skillBypasses.length === 0
258
+ && piModelResolutionFailures.length === 0)
259
+ return;
260
+ const index = sections.findIndex((section) => section.id === "runtime");
261
+ if (index < 0)
262
+ return;
263
+ const runtime = sections[index];
264
+ sections[index] = {
265
+ ...runtime,
266
+ status: "error",
267
+ details: [
268
+ ...runtime.details,
269
+ ...(directBoundaryConflicts.length === 0
270
+ ? []
271
+ : [
272
+ "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.",
273
+ ...directBoundaryConflicts,
274
+ ]),
275
+ ...(sandboxBypasses.length === 0
276
+ ? []
277
+ : [
278
+ "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.",
279
+ ...sandboxBypasses,
280
+ ]),
281
+ ...(toolPolicyBypasses.length === 0
282
+ ? []
283
+ : [
284
+ "Per-trigger direct OpenCode model overrides require exact allow-all because OpenCode's provider-owned tool loop does not consume mono-agent allowedTools/disallowedTools.",
285
+ ...toolPolicyBypasses,
286
+ ]),
287
+ ...(effortBypasses.length === 0
288
+ ? []
289
+ : [
290
+ "Per-trigger direct OpenCode routes cannot receive runtime effort because the OpenCode SDK does not expose effort control.",
291
+ ...effortBypasses,
292
+ ]),
293
+ ...(turnCapBypasses.length === 0
294
+ ? []
295
+ : [
296
+ "Per-trigger direct OpenCode routes cannot enforce runtime.maxTurns; omit it, set it to 0, or use a runtime with a hard turn cap.",
297
+ ...turnCapBypasses,
298
+ ]),
299
+ ...(mcpBypasses.length === 0
300
+ ? []
301
+ : [
302
+ "Per-trigger direct OpenCode routes cannot receive configured or auto-provisioned MCP runtime options.",
303
+ ...mcpBypasses,
304
+ ]),
305
+ ...(skillBypasses.length === 0
306
+ ? []
307
+ : [
308
+ "Per-trigger direct OpenCode routes cannot use index skill disclosure because the bridge disables external/runtime skills; use full disclosure or a Pi runtime.",
309
+ ...skillBypasses,
310
+ ]),
311
+ ...(piModelResolutionFailures.length === 0
312
+ ? []
313
+ : [
314
+ "Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
315
+ ...piModelResolutionFailures,
316
+ ]),
317
+ ],
318
+ };
319
+ }
320
+ function isUnknownRecord(value) {
321
+ return typeof value === "object" && value !== null && !Array.isArray(value);
322
+ }
323
+ function hasExactAllowAllToolPolicy(tools) {
324
+ return tools.allowedTools.length === 1
325
+ && tools.allowedTools[0] === "*"
326
+ && tools.disallowedTools.length === 0;
327
+ }
81
328
  /** Adapter send tools each channel owns; an allowed entry needs BOTH the tool AND the enabled channel. */
82
329
  const CHANNEL_OWNED_SEND_TOOLS = {
83
330
  slack: ["SlackSendMessage"],
@@ -141,55 +388,136 @@ function applyToolChannelCrossChecks(sections, allowedTools, disallowedTools) {
141
388
  function runtimeSection(config) {
142
389
  const details = [];
143
390
  let status = "ok";
144
- const primary = describeMonoRuntimeSupport(config.runtime.model, config.runtime.executionMode);
145
- if (primary.compatible) {
146
- details.push(`Primary model ${referenceOf(config.runtime.model)} runs on ${primary.backend?.label ?? "unknown backend"} (${config.runtime.executionMode}).`);
391
+ const routes = configuredRuntimeRouteChecks(config);
392
+ const routeSafety = config.runtime.routeSafety ?? "uniform";
393
+ details.push(`Route safety: ${routeSafety}.`);
394
+ if (routes.length > 1) {
395
+ details.push(routeSafety === "per-route-native"
396
+ ? "Mixed runtime families are allowed; every attempt uses its explicit route-native safety contract."
397
+ : "Fallback routes use the uniform compatibility contract; validation fails closed when any route cannot represent a required capability.");
398
+ }
399
+ const directOpenCodeModels = routes.filter((route) => route.model.sdk === "opencode");
400
+ if (Number(config.runtime.maxTurns) > 0 && directOpenCodeModels.length > 0) {
401
+ status = "error";
402
+ 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.`);
147
403
  }
148
- else {
404
+ if (directOpenCodeModels.length > 0
405
+ && config.context.skillDisclosure === "index"
406
+ && config.context.skillsRoot !== undefined) {
149
407
  status = "error";
150
- details.push(`Primary model ${referenceOf(config.runtime.model)}: ${primary.incompatibilityReason ?? "unsupported"}.`);
408
+ 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.`);
151
409
  }
152
- for (const fallback of config.runtime.fallbackModels ?? []) {
153
- const support = describeMonoRuntimeSupport(fallback);
154
- if (support.compatible) {
155
- details.push(`Fallback model ${referenceOf(fallback)} runs on ${support.backend?.label ?? "unknown backend"}.`);
410
+ for (const route of routes) {
411
+ try {
412
+ const support = describeMonoRuntimeSupport(route.model, route.executionMode);
413
+ const resolutionIssue = piModelResolutionIssue(config, route.model);
414
+ const effortIssue = runtimeRouteEffortIssue(config, route);
415
+ if (support.compatible && resolutionIssue === undefined) {
416
+ details.push(`${route.label} ${referenceOf(route.model)} runs on ${support.backend?.label ?? "unknown backend"} ` +
417
+ `(effort: ${route.effort ?? "provider default"}).`);
418
+ }
419
+ else {
420
+ status = "error";
421
+ details.push(`${route.label} ${referenceOf(route.model)}: ${resolutionIssue ?? support.incompatibilityReason ?? "unsupported"}.`);
422
+ }
423
+ if (effortIssue !== undefined) {
424
+ status = "error";
425
+ details.push(`${route.label} ${referenceOf(route.model)}: ${effortIssue}`);
426
+ }
156
427
  }
157
- else {
428
+ catch (error) {
158
429
  status = "error";
159
- details.push(`Fallback model ${referenceOf(fallback)}: ${support.incompatibilityReason ?? "unsupported"}.`);
430
+ details.push(`${route.label} ${referenceOf(route.model)}: ${error instanceof Error ? error.message : String(error)}.`);
160
431
  }
161
432
  }
162
433
  return { id: "runtime", label: "Runtime", status, details };
163
434
  }
164
- const PI_API_KEY_ENV_BY_PROVIDER = {
165
- "opencode-go": "OPENCODE_API_KEY",
166
- };
167
- /** Parses the Pi OAuth auth store (provider -> credentials) at `path`, best-effort. */
168
- async function readPiAuthProviders(path) {
169
- try {
170
- const parsed = JSON.parse(await readFile(path, "utf8"));
171
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
172
- return parsed;
173
- }
435
+ function configuredRuntimeRouteChecks(config) {
436
+ const primary = {
437
+ label: "Primary model",
438
+ model: config.runtime.model,
439
+ ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
440
+ executionMode: config.runtime.executionMode,
441
+ };
442
+ if ((config.runtime.fallbacks?.length ?? 0) > 0) {
443
+ return [
444
+ primary,
445
+ ...(config.runtime.fallbacks ?? []).map((fallback) => ({
446
+ label: "Fallback model",
447
+ model: fallback.model,
448
+ ...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
449
+ })),
450
+ ];
174
451
  }
175
- catch {
176
- // Missing/invalid auth file is reported per-provider below, not thrown here.
452
+ return [
453
+ primary,
454
+ ...(config.runtime.fallbackModels ?? []).map((model) => ({
455
+ label: "Fallback model",
456
+ model,
457
+ ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
458
+ })),
459
+ ];
460
+ }
461
+ function runtimeRouteEffortIssue(config, route) {
462
+ if (route.effort === undefined)
463
+ return undefined;
464
+ if (route.model.sdk === "opencode") {
465
+ 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.`;
466
+ }
467
+ const metadata = resolveModelEffortLevels(route.model, config.providers?.local);
468
+ if (metadata.effortLevels !== undefined && !metadata.effortLevels.includes(route.effort)) {
469
+ return `effort=${route.effort} is unsupported by known model metadata; choose ${metadata.effortLevels.join(", ")}, or omit it for provider default.`;
470
+ }
471
+ if (metadata.reasoning === false && route.effort !== "none") {
472
+ return `effort=${route.effort} is unsupported because known model metadata marks this route as non-reasoning; use none or provider default.`;
473
+ }
474
+ if (route.model.sdk === "claude"
475
+ && (route.executionMode ?? defaultExecutionModeForModel(route.model)) === "sdk"
476
+ && !["low", "medium", "high", "xhigh", "max"].includes(route.effort)) {
477
+ return `effort=${route.effort} is unsupported by the Claude Agent SDK; choose low, medium, high, xhigh, max, or provider default.`;
177
478
  }
178
479
  return undefined;
179
480
  }
180
- /** Provider ids configured via the sibling `models.json` (custom/local providers need no OAuth). */
181
- async function readPiCustomProviders(authPath) {
182
- try {
183
- const parsed = JSON.parse(await readFile(join(dirname(authPath), "models.json"), "utf8"));
184
- const providers = parsed?.providers;
185
- if (providers !== null && typeof providers === "object" && !Array.isArray(providers)) {
186
- return new Set(Object.keys(providers));
481
+ /**
482
+ * Mirrors the Pi runtime's actual model-resolution boundary. Mono-agent only
483
+ * registers a custom Pi provider from `providers.local`; it deliberately does
484
+ * not import Pi CLI's ambient sibling `models.json`. Without a matching local
485
+ * provider, `resolvePiRuntimeModel` performs an exact built-in catalog lookup.
486
+ */
487
+ function piModelResolutionIssue(config, model) {
488
+ if (model.sdk !== "pi" || model.provider === undefined) {
489
+ return undefined;
490
+ }
491
+ const localProvider = config.providers?.local?.find((provider) => provider.id === model.provider);
492
+ if (localProvider !== undefined) {
493
+ if (localProvider.enabled === false) {
494
+ return `provider \`${model.provider}\` is disabled in providers.local`;
495
+ }
496
+ const localModel = localProvider.models?.find((candidate) => candidate.name === model.model || candidate.alias === model.model);
497
+ if (localModel?.enabled === false) {
498
+ return `model \`${model.model}\` is disabled in providers.local for provider \`${model.provider}\``;
187
499
  }
500
+ return undefined;
188
501
  }
189
- catch {
190
- // No models.json is fine; such providers simply won't be marked custom.
502
+ if (isPiBuiltinProvider(model.provider)
503
+ && getPiBuiltinModels(model.provider).some((candidate) => candidate.id === model.model)) {
504
+ return undefined;
191
505
  }
192
- return new Set();
506
+ 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. ` +
507
+ "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");
508
+ }
509
+ function isPiBuiltinProvider(provider) {
510
+ return getPiBuiltinProviders().includes(provider);
511
+ }
512
+ const PI_API_KEY_ENV_BY_PROVIDER = {
513
+ "opencode-go": "OPENCODE_API_KEY",
514
+ };
515
+ /** Inspects Pi credentials without following aliases or reading unbounded input. */
516
+ async function readPiAuthProviders(path) {
517
+ const inspection = await inspectPiAuthStore(path);
518
+ if (inspection.status !== "ok")
519
+ return inspection;
520
+ return { status: "ok", providers: inspection.auth };
193
521
  }
194
522
  /**
195
523
  * What each SDK-authenticated backend truthfully reads for credentials. Only the
@@ -215,14 +543,165 @@ const SDK_AUTH_SCHEMES = {
215
543
  loginCommand: "claude /login",
216
544
  loginDetail: "a `claude /login` subscription session or a Bedrock/Vertex configuration (CLAUDE_CODE_USE_BEDROCK/VERTEX)",
217
545
  failureHint: 'the first turn fails with an opaque "Claude Code process exited with code 1" that names nothing',
546
+ statusCommand: "claude auth status --json",
547
+ statusArgs: ["auth", "status", "--json"],
218
548
  },
219
549
  codex: {
220
550
  envKeys: ["OPENAI_API_KEY"],
221
551
  loginCommand: "codex login",
222
552
  loginDetail: "a `codex login` ChatGPT session (`~/.codex/auth.json`, outside the environment)",
223
553
  failureHint: "the first turn fails to authenticate",
554
+ statusCommand: "codex login status",
555
+ statusArgs: ["login", "status"],
224
556
  },
225
557
  };
558
+ const SDK_AUTH_STATUS_TIMEOUT_MS = 5_000;
559
+ const SDK_AUTH_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
560
+ function isSdkAuthName(value) {
561
+ return value === "claude" || value === "codex";
562
+ }
563
+ const defaultSdkAuthStatusExecFile = async (file, args, options) => {
564
+ const { stdout } = await execFile(file, [...args], options);
565
+ return { stdout };
566
+ };
567
+ /**
568
+ * Performs the SDK's local, read-only login-status command. A zero Codex exit
569
+ * confirms its external login; Claude additionally requires strict JSON with
570
+ * `loggedIn: true`. Missing binaries, timeouts, non-zero exits, and malformed
571
+ * output all fail closed without leaking command output into validation.
572
+ */
573
+ async function checkSdkExternalLoginStatus(sdk, env, cwd, run) {
574
+ const scheme = SDK_AUTH_SCHEMES[sdk];
575
+ try {
576
+ const { stdout } = await run(sdk, scheme.statusArgs, {
577
+ cwd,
578
+ env,
579
+ timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
580
+ maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
581
+ encoding: "utf8",
582
+ });
583
+ if (sdk === "codex") {
584
+ return true;
585
+ }
586
+ const parsed = JSON.parse(stdout);
587
+ return parsed !== null
588
+ && typeof parsed === "object"
589
+ && !Array.isArray(parsed)
590
+ && parsed.loggedIn === true;
591
+ }
592
+ catch {
593
+ return false;
594
+ }
595
+ }
596
+ /** Read provider IDs directly from auth.json without launching mutation-capable OpenCode middleware. */
597
+ async function inspectOpenCodeCredentialProviders(env) {
598
+ if (env.OPENCODE_AUTH_CONTENT !== undefined) {
599
+ return { status: "inline_auth_unsupported" };
600
+ }
601
+ const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
602
+ ? env.XDG_DATA_HOME
603
+ : join(typeof env.HOME === "string" && env.HOME.length > 0 ? env.HOME : homedir(), ".local", "share");
604
+ const opencodeData = join(dataHome, "opencode");
605
+ const marker = await regularCurrentUserFile(join(opencodeData, "opencode.db"));
606
+ if (!marker)
607
+ return { status: "migration_required" };
608
+ const authPath = join(opencodeData, "auth.json");
609
+ if (!(await regularCurrentUserFile(authPath)))
610
+ return { status: "auth_missing" };
611
+ try {
612
+ const parsed = JSON.parse(await readFile(authPath, "utf8"));
613
+ if (!isUnknownRecord(parsed))
614
+ return { status: "auth_invalid" };
615
+ const providers = new Set();
616
+ for (const [provider, credential] of Object.entries(parsed)) {
617
+ if (provider.length === 0 || provider.trim() !== provider || !isOpenCodeCredentialEntry(credential)) {
618
+ return { status: "auth_invalid" };
619
+ }
620
+ providers.add(provider);
621
+ }
622
+ return { status: "ok", providers };
623
+ }
624
+ catch {
625
+ return { status: "auth_invalid" };
626
+ }
627
+ }
628
+ async function regularCurrentUserFile(path) {
629
+ try {
630
+ const info = await lstat(path);
631
+ return info.isFile() && (typeof process.getuid !== "function" || info.uid === process.getuid());
632
+ }
633
+ catch {
634
+ return false;
635
+ }
636
+ }
637
+ function isOpenCodeCredentialEntry(value) {
638
+ if (!isUnknownRecord(value))
639
+ return false;
640
+ if (value.type === "oauth") {
641
+ return typeof value.refresh === "string"
642
+ && typeof value.access === "string"
643
+ && (value.refresh.trim().length > 0 || value.access.trim().length > 0)
644
+ && typeof value.expires === "number"
645
+ && Number.isSafeInteger(value.expires)
646
+ && value.expires >= 0
647
+ && (value.accountId === undefined || typeof value.accountId === "string")
648
+ && (value.enterpriseUrl === undefined || typeof value.enterpriseUrl === "string");
649
+ }
650
+ if (value.type === "api") {
651
+ return typeof value.key === "string"
652
+ && value.key.trim().length > 0
653
+ && (value.metadata === undefined
654
+ || (isUnknownRecord(value.metadata) && Object.values(value.metadata).every((entry) => typeof entry === "string")));
655
+ }
656
+ if (value.type === "wellknown") {
657
+ return typeof value.key === "string" && value.key.trim().length > 0
658
+ && typeof value.token === "string" && value.token.trim().length > 0;
659
+ }
660
+ return false;
661
+ }
662
+ async function checkOpenCodeVersion(env, cwd, run) {
663
+ const versionEnv = {};
664
+ for (const key of [
665
+ "PATH",
666
+ "LANG",
667
+ "LC_ALL",
668
+ "LC_CTYPE",
669
+ "SHELL",
670
+ "TMPDIR",
671
+ "TMP",
672
+ "TEMP",
673
+ "SystemRoot",
674
+ "WINDIR",
675
+ "ComSpec",
676
+ "PATHEXT",
677
+ ]) {
678
+ const value = env[key];
679
+ if (typeof value === "string" && value.length > 0)
680
+ versionEnv[key] = value;
681
+ }
682
+ try {
683
+ const { stdout } = await run("opencode", ["--version"], {
684
+ cwd,
685
+ env: versionEnv,
686
+ timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
687
+ maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
688
+ encoding: "utf8",
689
+ });
690
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/u.exec(stdout.trim());
691
+ if (match === null)
692
+ return false;
693
+ const major = Number(match[1]);
694
+ const minor = Number(match[2]);
695
+ const patch = Number(match[3]);
696
+ return Number.isSafeInteger(major)
697
+ && Number.isSafeInteger(minor)
698
+ && Number.isSafeInteger(patch)
699
+ && (major > 1 || (major === 1 && minor >= 15));
700
+ }
701
+ catch {
702
+ return false;
703
+ }
704
+ }
226
705
  /** First env key whose value is present and non-blank, or undefined when none are set. */
227
706
  function firstPresentEnvKey(env, keys) {
228
707
  for (const key of keys) {
@@ -241,26 +720,35 @@ function firstPresentEnvKey(env, keys) {
241
720
  * and the E1 fresh-instance case where `claude:*` with no `ANTHROPIC_API_KEY` "validates"
242
721
  * clean but the first turn dies with an opaque "process exited with code 1").
243
722
  *
244
- * The check is intentionally STATIC and read-only a validator must not mutate the auth
245
- * store or hit the network.
723
+ * The check is read-only. Static validation never launches a process; live validation may
724
+ * run bounded, local SDK login-status commands, which inspect durable external login state
725
+ * without making a model turn or mutating the auth store.
246
726
  *
247
- * - Pi providers: inspect the auth store (`piAuthPath`), the sibling `models.json`, AND the
248
- * config's own `providers.local` custom providers. A custom/local provider needs no OAuth;
249
- * an OAuth provider absent from the store, or whose access token has expired, is flagged
250
- * `waiting` with a re-auth hint.
251
- * - SDK-authenticated providers (`claude:*` / `codex:*`): inspect only the RESOLVED ENV
252
- * (process env + loaded `.env`) for the backend's accepted keys. When none is present we
253
- * flag `waiting` but the warning explicitly acknowledges the interactive login path
254
- * (`claude /login` / `codex login`), which lives outside the environment and CANNOT be
255
- * verified statically, so a logged-in user is not falsely told they are broken.
727
+ * - Pi providers: inspect the auth store (`piAuthPath`) and the config's own
728
+ * `providers.local` custom providers. A custom/local provider follows its declared
729
+ * `apiKey` / `apiKeyEnv` contract instead of Pi OAuth; an OAuth provider absent from
730
+ * the store, or whose access token has expired, is flagged `waiting` with a re-auth hint.
731
+ * - SDK-authenticated providers (`claude:*` / `codex:*`): inspect the RESOLVED ENV (process
732
+ * env + loaded `.env`) for the backend's accepted keys. During live validation only, a
733
+ * missing env credential falls back to `claude auth status --json` / `codex login status`.
734
+ * The commands use the same resolved environment (including PATH and HOME), are bounded,
735
+ * cached once per SDK, and never make a model turn. Static validation remains process-free.
736
+ * - Direct OpenCode (`opencode:<provider>:<model>`): inspect exact provider IDs in the
737
+ * standard auth.json and require the native DB migration marker. Static validation launches
738
+ * no process. Live validation runs only a bounded `opencode --version` preflight; it never
739
+ * runs the mutation-capable auth middleware or makes a model turn.
256
740
  *
257
741
  * `waiting` (never `error`) keeps the verdict non-fatal, mirroring the Ollama/Phoenix
258
742
  * probes — the goal is visibility, not blocking start.
259
743
  */
260
- async function credentialsSection(config, env) {
744
+ async function credentialsSection(config, env, cwd, liveness, verifiedCredentialModelRefs = [], sdkAuthStatusExecFile = defaultSdkAuthStatusExecFile, staticTriggerRefs = []) {
261
745
  const refs = [
262
746
  { label: "Primary", ref: config.runtime.model },
263
- ...(config.runtime.fallbackModels ?? []).map((ref) => ({ label: "Fallback", ref })),
747
+ ...configuredRuntimeRouteChecks(config).slice(1).map((route) => ({
748
+ label: "Fallback",
749
+ ref: route.model,
750
+ })),
751
+ ...staticTriggerRefs,
264
752
  ];
265
753
  if (config.memory?.llm !== undefined && config.memory.llm.provider !== "ollama") {
266
754
  try {
@@ -270,9 +758,10 @@ async function credentialsSection(config, env) {
270
758
  // A malformed memory model reference is surfaced by the memory/runtime shape checks.
271
759
  }
272
760
  }
273
- const piRefs = refs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
274
- const sdkRefs = refs.filter((r) => r.ref.sdk in SDK_AUTH_SCHEMES);
275
- if (piRefs.length === 0 && sdkRefs.length === 0) {
761
+ const authenticatedRefs = refs.filter((r) => (r.ref.sdk === "pi" && typeof r.ref.provider === "string")
762
+ || (r.ref.sdk === "opencode" && typeof r.ref.provider === "string")
763
+ || isSdkAuthName(r.ref.sdk));
764
+ if (authenticatedRefs.length === 0) {
276
765
  return {
277
766
  id: "credentials",
278
767
  label: "Provider credentials",
@@ -282,12 +771,49 @@ async function credentialsSection(config, env) {
282
771
  }
283
772
  const details = [];
284
773
  let status = "ok";
774
+ const verified = new Set(verifiedCredentialModelRefs);
775
+ for (const { label, ref } of authenticatedRefs) {
776
+ const refStr = referenceOf(ref);
777
+ if (verified.has(refStr)) {
778
+ details.push(`${label} ${refStr}: credentials verified by a successful live model check.`);
779
+ }
780
+ }
781
+ const unverifiedRefs = authenticatedRefs.filter(({ ref }) => !verified.has(referenceOf(ref)));
782
+ const piRefs = unverifiedRefs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
783
+ const openCodeRefs = unverifiedRefs.filter((r) => r.ref.sdk === "opencode" && typeof r.ref.provider === "string");
784
+ const sdkRefs = unverifiedRefs.filter((r) => isSdkAuthName(r.ref.sdk));
285
785
  if (piRefs.length > 0) {
286
- const piStatus = await appendPiCredentialDetails(config, piRefs, details);
786
+ const piStatus = await appendPiCredentialDetails(config, piRefs, details, env);
287
787
  if (piStatus === "waiting") {
288
788
  status = "waiting";
289
789
  }
290
790
  }
791
+ const openCodeInspection = openCodeRefs.length > 0
792
+ ? inspectOpenCodeCredentialProviders(env)
793
+ : undefined;
794
+ const openCodeVersion = liveness && openCodeRefs.length > 0
795
+ ? checkOpenCodeVersion(env, cwd, sdkAuthStatusExecFile)
796
+ : undefined;
797
+ const sdkAuthStatuses = new Map();
798
+ const externalLoginStatus = (sdk) => {
799
+ const cached = sdkAuthStatuses.get(sdk);
800
+ if (cached !== undefined) {
801
+ return cached;
802
+ }
803
+ const pending = checkSdkExternalLoginStatus(sdk, env, cwd, sdkAuthStatusExecFile);
804
+ sdkAuthStatuses.set(sdk, pending);
805
+ return pending;
806
+ };
807
+ // Start each unique local status check before awaiting details so two SDKs
808
+ // cost one bounded timeout window rather than running serially.
809
+ if (liveness) {
810
+ for (const { ref } of sdkRefs) {
811
+ const scheme = SDK_AUTH_SCHEMES[ref.sdk];
812
+ if (scheme !== undefined && firstPresentEnvKey(env, scheme.envKeys) === undefined) {
813
+ void externalLoginStatus(ref.sdk);
814
+ }
815
+ }
816
+ }
291
817
  for (const { label, ref } of sdkRefs) {
292
818
  const refStr = referenceOf(ref);
293
819
  const scheme = SDK_AUTH_SCHEMES[ref.sdk];
@@ -296,32 +822,69 @@ async function credentialsSection(config, env) {
296
822
  }
297
823
  const present = firstPresentEnvKey(env, scheme.envKeys);
298
824
  if (present !== undefined) {
299
- details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}).`);
825
+ details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}); credential detected, live model verification is still pending.`);
826
+ continue;
827
+ }
828
+ if (liveness && await externalLoginStatus(ref.sdk)) {
829
+ details.push(`${label} ${refStr}: external sign-in detected by read-only \`${scheme.statusCommand}\`; ` +
830
+ "credentials are not verified until a live model turn succeeds.");
300
831
  continue;
301
832
  }
302
833
  status = "waiting";
303
834
  details.push(`[WARN] ${label} ${refStr}: no SDK credential in the resolved env (checked ${scheme.envKeys.join(", ")}). ` +
304
- `If you authenticated via ${scheme.loginDetail} this is fine and can't be verified here; ` +
835
+ (liveness
836
+ ? `External login was not verified by \`${scheme.statusCommand}\`; `
837
+ : `If you authenticated via ${scheme.loginDetail} this is fine and can't be verified during static validation; `) +
305
838
  `otherwise ${scheme.failureHint} — set ${scheme.envKeys[0]} or run \`${scheme.loginCommand}\`.`);
306
839
  }
840
+ if (openCodeRefs.length > 0) {
841
+ const inspection = openCodeInspection === undefined ? undefined : await openCodeInspection;
842
+ const supportedVersion = openCodeVersion === undefined ? false : await openCodeVersion;
843
+ for (const { label, ref } of openCodeRefs) {
844
+ const refStr = referenceOf(ref);
845
+ const provider = ref.provider;
846
+ const credentialPresent = inspection?.status === "ok" && inspection.providers.has(provider);
847
+ if (credentialPresent && liveness && supportedVersion) {
848
+ 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.`);
849
+ }
850
+ else {
851
+ status = "waiting";
852
+ const warning = inspection?.status === "migration_required"
853
+ ? "the native OpenCode database migration marker is missing or invalid; run `opencode db migrate --pure` once"
854
+ : inspection?.status === "inline_auth_unsupported"
855
+ ? "OPENCODE_AUTH_CONTENT is unsupported for direct runs; persist credentials with `opencode auth login` and unset it"
856
+ : inspection?.status === "auth_invalid"
857
+ ? "the standard OpenCode auth.json is malformed or contains an unsupported credential entry"
858
+ : credentialPresent && !liveness
859
+ ? "credentials and migration marker are present, but the required stable OpenCode CLI >=1.15.0 is unverified during static validation"
860
+ : credentialPresent && !supportedVersion
861
+ ? "credentials are present, but stable OpenCode CLI >=1.15.0 could not be verified"
862
+ : inspection?.status === "ok"
863
+ ? `no exact credential entry exists for provider \`${provider}\`; run \`opencode auth login\` for that provider`
864
+ : "the standard OpenCode auth.json is missing; run `opencode auth login`";
865
+ const safetyNote = liveness
866
+ ? "No model turn or mutation-capable OpenCode command was run."
867
+ : "No OpenCode process was launched.";
868
+ details.push(`[WARN] ${label} ${refStr}: ${warning}. ${safetyNote}`);
869
+ }
870
+ }
871
+ }
307
872
  return { id: "credentials", label: "Provider credentials", status, details };
308
873
  }
309
874
  /**
310
875
  * Appends one detail line per Pi provider reference and returns "waiting" if any
311
- * was flagged (missing/expired), else "ok". Recognizes providers configured via
312
- * the auth store's `models.json` AND the config's `providers.local` custom set.
876
+ * was flagged (missing/expired), else "ok". Recognizes custom providers only
877
+ * through the config's `providers.local` set, matching the runtime boundary.
313
878
  */
314
- async function appendPiCredentialDetails(config, piRefs, details) {
879
+ async function appendPiCredentialDetails(config, piRefs, details, env) {
315
880
  const authPath = config.providers?.piAuthPath;
316
- const authProviders = authPath === undefined ? undefined : await readPiAuthProviders(authPath);
317
- const modelsJsonProviders = authPath === undefined ? new Set() : await readPiCustomProviders(authPath);
318
- // A `pi:<id>:...` model whose `<id>` matches an ENABLED `providers.local` entry
319
- // is a keyless local/custom provider no OAuth, no auth-store entry. A DISABLED
320
- // entry is tracked separately: the runtime throws `provider disabled: <id>` on the
321
- // first turn (pi-models.js), so validate must name that instead of reporting a
322
- // clean OK or the misleading "no Pi credentials" advice.
323
- const enabledLocalProviders = new Set((config.providers?.local ?? []).filter((provider) => provider.enabled !== false).map((provider) => provider.id));
324
- const disabledLocalProviders = new Set((config.providers?.local ?? []).filter((provider) => provider.enabled === false).map((provider) => provider.id));
881
+ const authInspection = authPath === undefined ? undefined : await readPiAuthProviders(authPath);
882
+ const authProviders = authInspection?.status === "ok" ? authInspection.providers : undefined;
883
+ // A matching `providers.local` entry owns the runtime route, even when its ID
884
+ // collides with a Pi built-in provider. Report the local entry's declared auth
885
+ // contract instead of consulting the unrelated Pi auth store. A disabled entry
886
+ // remains authoritative because the runtime rejects it before credential use.
887
+ const localProviders = new Map((config.providers?.local ?? []).map((provider) => [provider.id, provider]));
325
888
  const now = Date.now();
326
889
  let status = "ok";
327
890
  if (authPath !== undefined) {
@@ -330,45 +893,99 @@ async function appendPiCredentialDetails(config, piRefs, details) {
330
893
  for (const { label, ref } of piRefs) {
331
894
  const provider = ref.provider;
332
895
  const refStr = referenceOf(ref);
333
- const loginCommand = piLoginCommandLine(provider);
334
- const loginCwd = piAuthWorkingDirectory(authPath);
335
- if (enabledLocalProviders.has(provider)) {
336
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (keyless local provider).`);
896
+ const loginCommand = piAuthRecoveryCommand(provider, authPath);
897
+ const localProvider = localProviders.get(provider);
898
+ if (localProvider !== undefined) {
899
+ if (localProvider.enabled === false) {
900
+ status = "waiting";
901
+ 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.`);
902
+ }
903
+ else if (localProvider.apiKey !== undefined) {
904
+ details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
905
+ }
906
+ else if (localProvider.apiKeyEnv !== undefined) {
907
+ if (hasNonEmptyCredentialValue(env[localProvider.apiKeyEnv])) {
908
+ 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.`);
909
+ }
910
+ else {
911
+ status = "waiting";
912
+ 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.`);
913
+ }
914
+ }
915
+ else {
916
+ details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (keyless local provider; no API key declared).`);
917
+ }
337
918
  continue;
338
919
  }
339
- if (disabledLocalProviders.has(provider)) {
340
- status = "waiting";
341
- 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.`);
920
+ const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
921
+ if (apiKeyEnv !== undefined && hasNonEmptyCredentialValue(env[apiKeyEnv])) {
922
+ details.push(`${label} ${refStr}: Pi API-key credential for \`${provider}\` present in the resolved environment (${apiKeyEnv}); credential detected, live model verification is still pending.`);
342
923
  continue;
343
924
  }
344
- if (modelsJsonProviders.has(provider)) {
345
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via pi models.json.`);
925
+ if (authInspection?.status === "unsafe") {
926
+ status = "waiting";
927
+ details.push(`[WARN] ${label} ${refStr}: Pi auth store was not trusted because ${describePiAuthStoreUnsafeReason(authInspection.reason)}. ` +
928
+ `Move the unsafe entry aside if needed, then run \`${loginCommand}\` to create or atomically harden a current-user 0600 store. ` +
929
+ "A current-user, non-writable legacy store can be replaced during this explicit repair, but is intentionally never trusted for credential detection. " +
930
+ "No credential values or file contents were displayed.");
346
931
  continue;
347
932
  }
348
933
  const entry = authProviders?.[provider];
349
934
  if (entry === undefined) {
350
935
  status = "waiting";
351
- const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
352
936
  details.push(apiKeyEnv === undefined
353
- ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` (absent from the auth store and models.json). Authenticate it with \`${loginCommand}\` from ${loginCwd}, or set providers.piAuthPath.`
354
- : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\` (absent from the auth store and models.json). Save an API key entry in providers.piAuthPath, or set ${apiKeyEnv}.`);
937
+ ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
938
+ : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
939
+ continue;
940
+ }
941
+ const isOAuth = entry.type === "oauth";
942
+ const isApiKey = entry.type === "api_key";
943
+ if (isApiKey && !hasNonEmptyCredentialValue(entry.key)) {
944
+ status = "waiting";
945
+ details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${provider}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
946
+ continue;
947
+ }
948
+ if (isOAuth && !hasNonEmptyCredentialValue(entry.access) && !hasNonEmptyCredentialValue(entry.refresh)) {
949
+ status = "waiting";
950
+ 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.`);
951
+ continue;
952
+ }
953
+ if (!isOAuth && !isApiKey) {
954
+ status = "waiting";
955
+ details.push(`[WARN] ${label} ${refStr}: stored credential for \`${provider}\` has an unsupported or missing type. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
355
956
  continue;
356
957
  }
357
- const isOAuth = entry.type === "oauth" || typeof entry.expires === "number";
358
958
  const expired = typeof entry.expires === "number" && entry.expires < now;
359
959
  const whenNote = typeof entry.expires === "number" ? ` ${new Date(entry.expires).toISOString()}` : "";
360
960
  if (isOAuth && expired) {
361
961
  status = "waiting";
362
- details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${provider}\` expired${whenNote} — the runtime auto-refreshes, but if runs fail with "No API key for provider: ${provider}" the refresh is dead; re-authenticate with \`${loginCommand}\` from ${loginCwd}.`);
962
+ 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}\`.`);
363
963
  continue;
364
964
  }
365
965
  details.push(isOAuth
366
- ? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}).`
367
- : `${label} ${refStr}: API key credentials for \`${provider}\` present.`);
966
+ ? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
967
+ : `${label} ${refStr}: API key credentials for \`${provider}\` present; credential detected, live model verification is still pending.`);
368
968
  }
369
969
  return status;
370
970
  }
371
- async function contextSection(config) {
971
+ function describePiAuthStoreUnsafeReason(reason) {
972
+ switch (reason) {
973
+ case "owner-check-unavailable": return "the current file owner could not be verified";
974
+ case "symbolic-link": return "the configured entry is a symbolic link";
975
+ case "not-regular-file": return "the configured entry is not a regular file";
976
+ case "multiple-hard-links": return "the file has multiple hard links";
977
+ case "oversized": return "the file exceeds the 1 MiB inspection limit";
978
+ case "foreign-owner": return "the file is not owned by the current user";
979
+ case "not-owner-only": return "its permissions are not owner-only";
980
+ case "changed-during-read": return "its identity or metadata changed during inspection";
981
+ case "malformed-json": return "it is not a valid JSON object";
982
+ case "unreadable": return "it could not be opened and inspected safely";
983
+ }
984
+ }
985
+ function hasNonEmptyCredentialValue(value) {
986
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 65_536 && !value.includes("\0");
987
+ }
988
+ async function contextSection(config, cwd) {
372
989
  const details = [];
373
990
  let status = "ok";
374
991
  if (await pathExists(config.context.identityPath)) {
@@ -405,10 +1022,23 @@ async function contextSection(config) {
405
1022
  status = "error";
406
1023
  details.push("Skills are selected but context.skillsRoot is not set.");
407
1024
  }
1025
+ if (await managedProjectSkillsExist(cwd)) {
1026
+ const managed = await checkManagedProjectSkills(cwd);
1027
+ const drift = managed.statuses.filter((entry) => entry.status !== "ready");
1028
+ if (drift.length === 0) {
1029
+ details.push(`Managed project skills: current (${managed.manifestVersion ?? "unknown version"}).`);
1030
+ }
1031
+ else {
1032
+ if (status === "ok")
1033
+ status = "waiting";
1034
+ details.push(`Managed project skill drift: ${drift.map((entry) => `${entry.name}=${entry.status}`).join(", ")}. ` +
1035
+ "Run `mono-agent install-skill --project --check`; use --update only after reconciling modified copies.");
1036
+ }
1037
+ }
408
1038
  return { id: "context", label: "Context & skills", status, details };
409
1039
  }
410
1040
  const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
411
- async function memorySection(config, liveness, allowFilesystemWrites) {
1041
+ async function memorySection(config, cwd, liveness, allowFilesystemWrites) {
412
1042
  if (config.memory === undefined) {
413
1043
  return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
414
1044
  }
@@ -425,6 +1055,32 @@ async function memorySection(config, liveness, allowFilesystemWrites) {
425
1055
  details: ["[ERROR] backend 'supermemory' requires a memory.supermemory block."],
426
1056
  };
427
1057
  }
1058
+ try {
1059
+ const plugin = await loadSupermemoryPlugin({ cwd });
1060
+ const validation = plugin.validateSupermemoryConfig({
1061
+ baseUrl: sm.baseUrl,
1062
+ container: resolveSupermemoryContainer(config),
1063
+ ...(sm.apiKey === undefined ? {} : { apiKey: sm.apiKey }),
1064
+ ...(sm.timeoutMs === undefined ? {} : { timeoutMs: sm.timeoutMs }),
1065
+ ...(config.memory.maxBytes === undefined ? {} : { maxBytes: config.memory.maxBytes }),
1066
+ });
1067
+ if (!validation.valid) {
1068
+ return {
1069
+ id: "memory",
1070
+ label: "Memory",
1071
+ status: "error",
1072
+ details: validation.errors.map((detail) => `[ERROR] ${detail}`),
1073
+ };
1074
+ }
1075
+ }
1076
+ catch (error) {
1077
+ return {
1078
+ id: "memory",
1079
+ label: "Memory",
1080
+ status: "error",
1081
+ details: [`[ERROR] ${error instanceof Error ? error.message : String(error)}`],
1082
+ };
1083
+ }
428
1084
  return {
429
1085
  id: "memory",
430
1086
  label: "Memory",
@@ -442,39 +1098,191 @@ async function memorySection(config, liveness, allowFilesystemWrites) {
442
1098
  const details = [
443
1099
  `Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
444
1100
  ];
1101
+ let status = "ok";
445
1102
  if (config.memory.llm !== undefined) {
446
1103
  details.push(`Chat LLM: ${memoryLlmLabel(config.memory.llm)}.`);
1104
+ if (config.memory.llm.provider === "agent-host") {
1105
+ try {
1106
+ const model = parseMonoRuntimeModelReference(config.memory.llm.model);
1107
+ const resolutionIssue = piModelResolutionIssue(config, model);
1108
+ if (resolutionIssue !== undefined) {
1109
+ status = "error";
1110
+ details.push(`Agent-host memory LLM ${referenceOf(model)}: ${resolutionIssue}.`);
1111
+ }
1112
+ }
1113
+ catch (error) {
1114
+ status = "error";
1115
+ details.push(`Agent-host memory LLM ${config.memory.llm.model}: ${error instanceof Error ? error.message : String(error)}.`);
1116
+ }
1117
+ }
447
1118
  }
448
1119
  if (config.memory.mode === "bujo") {
449
1120
  // Report consolidation scheduler cadence.
450
1121
  const consolidationEnabled = config.memory.consolidation?.enabled !== false;
451
1122
  const consolidationCron = config.memory.consolidation?.cron ?? DEFAULT_CONSOLIDATION_CRON;
452
- const hasLlm = config.memory.llm !== undefined;
453
- if (hasLlm) {
454
- if (consolidationEnabled) {
455
- details.push(`Consolidation: ${consolidationCron} (auto).`);
456
- }
457
- else {
458
- details.push("Consolidation: disabled.");
459
- }
1123
+ if (consolidationEnabled) {
1124
+ details.push(`Consolidation: ${consolidationCron} (auto).`);
460
1125
  }
461
1126
  else {
462
- details.push("Consolidation: not scheduled (no chat model — bujo runtime tier downgrades to journal).");
1127
+ details.push("Consolidation: disabled.");
463
1128
  }
464
1129
  }
1130
+ const managedIdentity = await managedMemoryIdentityStatus(config.memory);
1131
+ if (managedIdentity !== undefined) {
1132
+ return {
1133
+ id: "memory",
1134
+ label: "Memory",
1135
+ status: status === "error" ? "error" : managedIdentity.status,
1136
+ details: [...details, ...managedIdentity.details],
1137
+ };
1138
+ }
1139
+ const nativeAvailability = await builtInMemoryNativeStatus(config.memory);
1140
+ if (nativeAvailability !== undefined) {
1141
+ return {
1142
+ id: "memory",
1143
+ label: "Memory",
1144
+ status: "error",
1145
+ details: [...details, nativeAvailability],
1146
+ };
1147
+ }
465
1148
  if (config.memory.mode === "journal" || config.memory.mode === "bujo") {
466
1149
  const warns = await memoryLivenessWarnings(config.memory, liveness, allowFilesystemWrites);
467
1150
  if (warns.length > 0) {
468
- return { id: "memory", label: "Memory", status: "waiting", details: [...details, ...warns] };
1151
+ return {
1152
+ id: "memory",
1153
+ label: "Memory",
1154
+ status: status === "error" ? "error" : "waiting",
1155
+ details: [...details, ...warns],
1156
+ };
469
1157
  }
470
1158
  }
471
1159
  if (config.memory.mode === "lite") {
472
1160
  const liteWarns = await liteRootWritableWarning(config.memory.path, allowFilesystemWrites);
473
1161
  if (liteWarns.length > 0) {
474
- return { id: "memory", label: "Memory", status: "waiting", details: [...details, ...liteWarns] };
1162
+ return {
1163
+ id: "memory",
1164
+ label: "Memory",
1165
+ status: status === "error" ? "error" : "waiting",
1166
+ details: [...details, ...liteWarns],
1167
+ };
1168
+ }
1169
+ }
1170
+ return { id: "memory", label: "Memory", status, details };
1171
+ }
1172
+ async function managedMemoryIdentityStatus(memory) {
1173
+ if (await pathExists(join(memory.path, FIRST_RUN_MEMORY_INITIALIZING_MARKER))) {
1174
+ return {
1175
+ status: "error",
1176
+ details: [
1177
+ "[ERROR] First-run managed memory initialization is incomplete.",
1178
+ "Re-run `mono-agent init` in a clean target or remove only the failed first-run root after inspecting it.",
1179
+ ],
1180
+ };
1181
+ }
1182
+ const manifestPath = join(memory.path, ".index", "manifest.json");
1183
+ if (!(await pathExists(manifestPath))) {
1184
+ // Lite has no semantic index authority. Journal/BuJo readiness is strict:
1185
+ // a missing manifest is fatal even for a wholly new/unmanaged root, and the
1186
+ // provider must not be probed until rebuild establishes that authority.
1187
+ if (memory.mode === "lite")
1188
+ return undefined;
1189
+ return {
1190
+ status: "error",
1191
+ details: [
1192
+ "[ERROR] Managed memory generation metadata is missing for Journal/BuJo memory.",
1193
+ "Stop the agent with `mono-agent stop`, run `mono-agent memory rebuild`, then re-run `mono-agent validate` before restarting.",
1194
+ ],
1195
+ };
1196
+ }
1197
+ let manifest;
1198
+ try {
1199
+ const { readManagedIndexManifest } = await import("@mono-agent/memory/bujo");
1200
+ manifest = readManagedIndexManifest(memory.path);
1201
+ }
1202
+ catch {
1203
+ return {
1204
+ status: "error",
1205
+ details: ["[ERROR] Managed memory generation metadata is invalid or unavailable."],
1206
+ };
1207
+ }
1208
+ if (manifest === undefined) {
1209
+ return {
1210
+ status: "error",
1211
+ details: ["[ERROR] Managed memory generation metadata disappeared during validation."],
1212
+ };
1213
+ }
1214
+ const configuredModel = memory.embeddings === undefined
1215
+ ? undefined
1216
+ : `${memory.embeddings.provider}:${memory.embeddings.model}`;
1217
+ const configuredDimension = memory.embeddings === undefined ? undefined : memory.embeddings.dim ?? 768;
1218
+ const active = manifest.active;
1219
+ if (active.tier === memory.mode
1220
+ && active.embeddingModel === configuredModel
1221
+ && active.dimension === configuredDimension) {
1222
+ return undefined;
1223
+ }
1224
+ const identity = (tier, model, dimension) => `tier=${tier}, model=${model ?? "none"}, dim=${dimension ?? "none"}`;
1225
+ return {
1226
+ status: "error",
1227
+ details: [
1228
+ `[ERROR] Active managed generation does not match the configured memory identity: active ${identity(active.tier, active.embeddingModel, active.dimension)}; configured ${identity(memory.mode, configuredModel, configuredDimension)}.`,
1229
+ "Stop the agent with `mono-agent stop`, run `mono-agent memory rebuild`, then re-run `mono-agent validate` before restarting.",
1230
+ ],
1231
+ };
1232
+ }
1233
+ async function builtInMemoryNativeStatus(memory) {
1234
+ let database;
1235
+ let managedGeneration = false;
1236
+ try {
1237
+ const { openMemoryDb } = await import("@mono-agent/memory/store");
1238
+ const manifestPath = join(memory.path, ".index", "manifest.json");
1239
+ if (await pathExists(manifestPath)) {
1240
+ managedGeneration = true;
1241
+ const { resolveActiveMemoryDbPath } = await import("@mono-agent/memory/bujo");
1242
+ database = openMemoryDb({ path: resolveActiveMemoryDbPath(memory.path), readOnly: true });
1243
+ }
1244
+ else {
1245
+ // Lite roots may not exist yet and validation must remain read-only. An
1246
+ // in-memory open exercises the exact native ABI + extension load without
1247
+ // scanning durable memory or creating the configured root.
1248
+ database = openMemoryDb({ path: ":memory:" });
1249
+ }
1250
+ // A single schema-row lookup proves the opened handle can actually read
1251
+ // SQLite state (constructor-only opens can accept a truncated file). This
1252
+ // remains constant-work and avoids the corpus/queue scans of strict audit.
1253
+ database.indexMetadata();
1254
+ database.close();
1255
+ database = undefined;
1256
+ return undefined;
1257
+ }
1258
+ catch (error) {
1259
+ if (isBuiltInMemoryNativeFailure(error)) {
1260
+ return "[ERROR] Built-in memory native module is unavailable for this Node runtime. Rebuild dependencies with the launch runtime, then re-run `mono-agent validate`.";
1261
+ }
1262
+ if (managedGeneration) {
1263
+ return "[ERROR] Built-in memory active generation is unavailable. Stop the agent with `mono-agent stop`, run `mono-agent memory rebuild`, then re-run `mono-agent validate` before restarting.";
1264
+ }
1265
+ return "[ERROR] Built-in memory database smoke check failed. Rebuild dependencies with the launch runtime; if the runtime is compatible, stop the agent and run `mono-agent memory rebuild`, then re-run `mono-agent validate`.";
1266
+ }
1267
+ finally {
1268
+ try {
1269
+ database?.close();
475
1270
  }
1271
+ catch { /* best-effort smoke cleanup */ }
1272
+ }
1273
+ }
1274
+ function isBuiltInMemoryNativeFailure(error) {
1275
+ try {
1276
+ const message = error instanceof Error ? error.message : "";
1277
+ const code = typeof error === "object" && error !== null && "code" in error
1278
+ ? String(error.code ?? "")
1279
+ : "";
1280
+ return /(?:err_(?:dlopen|module_not_found)|module_not_found|better[-_ ]?sqlite|sqlite[-_ ]?vec|node_module_version|native module|dlopen|\.node\b)/iu
1281
+ .test(`${code} ${message}`);
1282
+ }
1283
+ catch {
1284
+ return false;
476
1285
  }
477
- return { id: "memory", label: "Memory", status: "ok", details };
478
1286
  }
479
1287
  async function liteRootWritableWarning(memoryPath, allowFilesystemWrites) {
480
1288
  return await memoryRootWritableWarnings("lite", memoryPath, allowFilesystemWrites);
@@ -652,18 +1460,80 @@ async function toolsSection(config, input) {
652
1460
  // Under allow-all the disallow list is already folded into the "except" clause above.
653
1461
  details.push(`Disallowed tools: ${config.tools.disallowedTools.join(", ")}.`);
654
1462
  }
1463
+ const directCodexModels = configuredRuntimeModels(config.runtime)
1464
+ .filter((model) => model.sdk === "codex")
1465
+ .map(referenceOf);
1466
+ const directOpenCodeModels = configuredRuntimeModels(config.runtime)
1467
+ .filter((model) => model.sdk === "opencode")
1468
+ .map(referenceOf);
1469
+ const claudeCliModels = [
1470
+ { model: config.runtime.model, executionMode: config.runtime.executionMode },
1471
+ ...configuredRuntimeFallbackModels(config.runtime).map((model) => ({
1472
+ model,
1473
+ executionMode: defaultExecutionModeForModel(model),
1474
+ })),
1475
+ ]
1476
+ .filter(({ model, executionMode }) => model.sdk === "claude" && executionMode === "cli")
1477
+ .map(({ model }) => referenceOf(model));
1478
+ const exactAllowAll = hasExactAllowAllToolPolicy(config.tools);
1479
+ const dedicatedNoToolsProbe = input.codexNoToolsProbe === true
1480
+ && allowedTools.length === 0
1481
+ && config.tools.disallowedTools.length === 0;
1482
+ if (directCodexModels.length > 0
1483
+ && !dedicatedNoToolsProbe
1484
+ && !exactAllowAll) {
1485
+ status = "error";
1486
+ details.push(`Direct Codex model${directCodexModels.length === 1 ? "" : "s"} ${directCodexModels.join(", ")} cannot enforce ` +
1487
+ "tools.allowedTools/tools.disallowedTools. Use exact allow-all (allowedTools: [\"*\"] with no disallowedTools), " +
1488
+ "or select a runtime that supports restrictive tool policies.");
1489
+ }
1490
+ if (directOpenCodeModels.length > 0 && !exactAllowAll) {
1491
+ status = "error";
1492
+ details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot enforce ` +
1493
+ "tools.allowedTools/tools.disallowedTools. Use exact allow-all (allowedTools: [\"*\"] with no disallowedTools), " +
1494
+ "or use a Pi runtime (including pi:opencode-go:*).");
1495
+ }
1496
+ if (claudeCliModels.length > 0 && allowedTools.length === 0) {
1497
+ status = "error";
1498
+ details.push(`Claude CLI model${claudeCliModels.length === 1 ? "" : "s"} ${claudeCliModels.join(", ")} cannot enforce an empty ` +
1499
+ "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.");
1500
+ }
1501
+ let configuredMcpServerNames = [];
1502
+ let configuredMcpServers = {};
655
1503
  if (config.tools.mcpConfigPath !== undefined) {
656
1504
  if (await pathExists(config.tools.mcpConfigPath)) {
657
1505
  details.push(`MCP config: ${config.tools.mcpConfigPath}`);
1506
+ try {
1507
+ const policy = await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath);
1508
+ configuredMcpServers = policy.mcpServers ?? {};
1509
+ configuredMcpServerNames = Object.keys(configuredMcpServers);
1510
+ }
1511
+ catch {
1512
+ status = "error";
1513
+ details.push(`MCP config is malformed or unreadable: ${config.tools.mcpConfigPath}`);
1514
+ }
658
1515
  }
659
1516
  else {
660
1517
  status = "error";
661
1518
  details.push(`MCP config file is missing: ${config.tools.mcpConfigPath}`);
662
1519
  }
663
1520
  }
1521
+ for (const serverName of config.tools.mcpRequestContextServers ?? []) {
1522
+ const spec = configuredMcpServers[serverName];
1523
+ if (spec === undefined) {
1524
+ status = "error";
1525
+ details.push(`tools.mcpRequestContextServers names unknown MCP server "${serverName}"; declare it in tools.mcpConfigPath.`);
1526
+ continue;
1527
+ }
1528
+ if (!isStdioMcpSpec(spec)) {
1529
+ status = "error";
1530
+ details.push(`tools.mcpRequestContextServers entry "${serverName}" must reference a stdio MCP server (command/type:stdio), not HTTP/SSE.`);
1531
+ }
1532
+ }
664
1533
  const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
665
1534
  allowedTools: config.tools.allowedTools,
666
1535
  disallowedTools: config.tools.disallowedTools,
1536
+ suppressInteractionTools: directOpenCodeModels.length > 0,
667
1537
  });
668
1538
  if (adapterSendTools === undefined) {
669
1539
  details.push("No adapter-derived send tools enabled.");
@@ -671,9 +1541,197 @@ async function toolsSection(config, input) {
671
1541
  else {
672
1542
  details.push(`Adapter send tools: ${adapterSendToolNames(adapterSendTools).join(", ")}.`);
673
1543
  }
1544
+ const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools, directOpenCodeModels.length > 0);
1545
+ if (blockedAdapterEndpoints.length > 0) {
1546
+ if (status !== "error") {
1547
+ status = "waiting";
1548
+ }
1549
+ details.push(...blockedAdapterEndpoints);
1550
+ }
1551
+ const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterSendTools === undefined ? [] : adapterSendToolNames(adapterSendTools));
1552
+ if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
1553
+ status = "error";
1554
+ details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot safely consume ` +
1555
+ `MCP runtime options from ${effectiveMcpSources.join("; ")}. Disable those MCP sources or use a Pi runtime (including pi:opencode-go:*).`);
1556
+ }
674
1557
  return { id: "tools", label: "Tools & MCP", status, details };
675
1558
  }
1559
+ function isStdioMcpSpec(value) {
1560
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1561
+ return false;
1562
+ const spec = value;
1563
+ return spec.type === "stdio"
1564
+ || (typeof spec.command === "string" && spec.type !== "http" && spec.type !== "sse");
1565
+ }
1566
+ /**
1567
+ * Adapter-send tools run in a stdio MCP child governed by the native SRT
1568
+ * policy. Channel readiness alone therefore is not enough: the child also
1569
+ * needs the remote adapter API (or the app-owned interaction bridge) admitted
1570
+ * by sandbox.network.
1571
+ */
1572
+ async function adapterSendToolNetworkPolicyWarnings(config, input, settings, suppressInteractionTools) {
1573
+ if (config.sandbox === undefined || config.sandbox.mode !== "native") {
1574
+ return [];
1575
+ }
1576
+ const toolNames = settings === undefined ? [] : adapterSendToolNames(settings);
1577
+ const requirements = [];
1578
+ if (settings?.slack !== undefined) {
1579
+ requirements.push({
1580
+ label: "Slack adapter-send API",
1581
+ tools: ["SlackSendMessage"],
1582
+ url: "https://slack.com/api",
1583
+ });
1584
+ }
1585
+ if (settings?.telegram !== undefined) {
1586
+ requirements.push({
1587
+ label: "Telegram adapter-send API",
1588
+ tools: toolNames.filter((name) => name.startsWith("Telegram")),
1589
+ url: settings.telegram.apiRoot ?? "https://api.telegram.org",
1590
+ });
1591
+ }
1592
+ const askUserAllowed = !suppressInteractionTools && isAdapterSendToolAllowed("AskUser", {
1593
+ allowedTools: config.tools.allowedTools,
1594
+ disallowedTools: config.tools.disallowedTools,
1595
+ });
1596
+ const bridgeTools = [
1597
+ ...(askUserAllowed ? ["AskUser"] : []),
1598
+ ...(toolNames.includes("TelegramAskButtons") ? ["TelegramAskButtons"] : []),
1599
+ ];
1600
+ if (bridgeTools.length > 0) {
1601
+ const configuredBridgeUrl = input.env.MONO_AGENT_INTERACTION_BRIDGE_URL?.trim();
1602
+ const interaction = await loadInteractionSettings(input);
1603
+ const bridgeUrl = configuredBridgeUrl === undefined || configuredBridgeUrl.length === 0
1604
+ ? formatInteractionBridgeUrl(interaction.host, interaction.port)
1605
+ : configuredBridgeUrl;
1606
+ requirements.push({ label: "AskUser interaction bridge", tools: bridgeTools, url: bridgeUrl });
1607
+ }
1608
+ return requirements.flatMap((requirement) => {
1609
+ if (adapterSendChildNetworkAllowsUrl(config, requirement.url)) {
1610
+ return [];
1611
+ }
1612
+ const host = endpointHost(requirement.url);
1613
+ const allowlistHost = host === "::1" ? "localhost" : host;
1614
+ return [
1615
+ `Native sandbox network policy blocks ${requirement.label} host "${host}", required by ${requirement.tools.join(", ")}. ` +
1616
+ `Set sandbox.network.mode to "allowlist" and add "${allowlistHost}" to sandbox.network.allowlist, ` +
1617
+ `or disable ${requirement.tools.join(", ")}.`,
1618
+ ];
1619
+ });
1620
+ }
1621
+ function adapterSendChildNetworkAllowsUrl(config, url) {
1622
+ if (networkPolicyAllowsUrl(config.sandbox, url))
1623
+ return true;
1624
+ if (config.sandbox?.mode !== "native" || config.sandbox.network.mode !== "allowlist")
1625
+ return false;
1626
+ const host = endpointHost(url);
1627
+ return isLoopbackEndpointHost(host) && config.sandbox.network.allowlist.some(isLoopbackEndpointHost);
1628
+ }
1629
+ function isLoopbackEndpointHost(host) {
1630
+ return host === "localhost" || host === "::1" || (isIP(host) === 4 && host.split(".")[0] === "127");
1631
+ }
1632
+ function endpointHost(url) {
1633
+ try {
1634
+ const host = new URL(url).hostname.toLowerCase();
1635
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
1636
+ }
1637
+ catch {
1638
+ return url;
1639
+ }
1640
+ }
1641
+ function effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames) {
1642
+ const sources = [];
1643
+ if (configuredMcpServerNames.length > 0) {
1644
+ sources.push(`tools.mcpConfigPath (${configuredMcpServerNames.join(", ")})`);
1645
+ }
1646
+ if (config.memory?.recallTool?.enabled === true) {
1647
+ sources.push("memory.recallTool");
1648
+ }
1649
+ if (config.memory?.backend === "supermemory"
1650
+ && config.memory.supermemory?.exposeMcpServer === true
1651
+ && config.memory.supermemory.apiKey !== undefined) {
1652
+ sources.push("memory.supermemory.exposeMcpServer");
1653
+ }
1654
+ if (adapterToolNames.length > 0) {
1655
+ sources.push(`adapter send tools (${adapterToolNames.join(", ")})`);
1656
+ }
1657
+ return sources;
1658
+ }
676
1659
  async function sandboxSection(config, engine) {
1660
+ const runtimeModels = configuredRuntimeModels(config.runtime);
1661
+ const directCodexRefs = runtimeModels
1662
+ .filter((model) => model.sdk === "codex")
1663
+ .map((model) => model.reference ?? `codex:${model.model}`);
1664
+ const claudeRefs = runtimeModels
1665
+ .filter((model) => model.sdk === "claude")
1666
+ .map((model) => model.reference ?? `claude:${model.model}`);
1667
+ const directOpenCodeRefs = runtimeModels
1668
+ .filter((model) => model.sdk === "opencode")
1669
+ .map((model) => model.reference ?? `opencode:${model.provider ?? "unknown"}:${model.model}`);
1670
+ const routeSafety = config.runtime.routeSafety ?? "uniform";
1671
+ if (routeSafety === "per-route-native") {
1672
+ const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
1673
+ const piRoutes = runtimeModels.filter((model) => model.sdk === "pi");
1674
+ const details = runtimeModels.map((model, index) => routeNativeSafetyDetail(model, index, config));
1675
+ let status = !monoSandboxActive && piRoutes.length > 0 ? "disabled" : "ok";
1676
+ if (monoSandboxActive && piRoutes.length > 0) {
1677
+ const state = await resolveSandboxEffectiveState({
1678
+ policy: config.sandbox,
1679
+ ...(engine === undefined ? {} : { engine }),
1680
+ });
1681
+ const warning = sandboxEffectiveStateWarning(state);
1682
+ details.push(`Pi route SRT policy: mode ${config.sandbox.mode}, network ${config.sandbox.network.mode}, fallback ${config.sandbox.fallback}.`, describeSandboxEffectiveState(state), ...(warning === undefined ? [] : [warning]));
1683
+ if (warning !== undefined || state.effective === "blocked")
1684
+ status = "waiting";
1685
+ }
1686
+ else if (piRoutes.length > 0) {
1687
+ details.push("Pi route SRT policy: disabled; Bash and stdio MCP subprocesses run unsandboxed.");
1688
+ }
1689
+ else if (monoSandboxActive) {
1690
+ details.push("The configured mono-agent SRT policy has no Pi route to enforce it; provider-owned route contracts below apply instead.");
1691
+ status = "waiting";
1692
+ }
1693
+ if (monoSandboxActive && runtimeModels.some((model) => model.sdk !== "pi")) {
1694
+ 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.");
1695
+ status = "waiting";
1696
+ }
1697
+ if (runtimeModels.some((model) => model.sdk === "codex") && config.runtime.permissionMode === "bypassPermissions") {
1698
+ status = "waiting";
1699
+ }
1700
+ return { id: "sandbox", label: "Sandbox", status, details };
1701
+ }
1702
+ const incompatibleDetails = [];
1703
+ if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directCodexRefs.length > 0) {
1704
+ const codexPosture = directCodexSandboxPosture(config.runtime.permissionMode);
1705
+ 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.`);
1706
+ }
1707
+ if (config.sandbox !== undefined && config.sandbox.mode !== "off" && claudeRefs.length > 0) {
1708
+ 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.");
1709
+ }
1710
+ if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directOpenCodeRefs.length > 0) {
1711
+ 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.");
1712
+ }
1713
+ if (incompatibleDetails.length > 0) {
1714
+ return {
1715
+ id: "sandbox",
1716
+ label: "Sandbox",
1717
+ status: "error",
1718
+ details: incompatibleDetails,
1719
+ };
1720
+ }
1721
+ if (directCodexRefs.length > 0 && (config.sandbox === undefined || config.sandbox.mode === "off")) {
1722
+ const posture = directCodexSandboxPosture(config.runtime.permissionMode);
1723
+ return {
1724
+ id: "sandbox",
1725
+ label: "Sandbox",
1726
+ status: posture.status,
1727
+ details: [
1728
+ posture.detail,
1729
+ config.sandbox === undefined
1730
+ ? "No mono-agent native srt policy is configured."
1731
+ : "The mono-agent native srt policy is explicitly off; the Codex-native posture still applies.",
1732
+ ],
1733
+ };
1734
+ }
677
1735
  if (config.sandbox === undefined) {
678
1736
  return { id: "sandbox", label: "Sandbox", status: "disabled", details: ["No sandbox policy configured."] };
679
1737
  }
@@ -701,6 +1759,44 @@ async function sandboxSection(config, engine) {
701
1759
  details,
702
1760
  };
703
1761
  }
1762
+ function routeNativeSafetyDetail(model, index, config) {
1763
+ const label = index === 0 ? "Primary" : `Fallback ${index}`;
1764
+ const ref = referenceOf(model);
1765
+ if (model.sdk === "pi") {
1766
+ if (config.sandbox === undefined || config.sandbox.mode === "off") {
1767
+ return `${label} ${ref}: Pi-owned tools use mono-agent tool policy; SRT is disabled, so Bash and stdio MCP subprocesses run unsandboxed.`;
1768
+ }
1769
+ if (config.sandbox.fallback === "unsafe-host-process") {
1770
+ 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.`;
1771
+ }
1772
+ return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy and fail closed when it is unavailable.`;
1773
+ }
1774
+ if (model.sdk === "claude") {
1775
+ return `${label} ${ref}: Claude provider-owned permissions apply; mono-agent SRT filesystem/network rules are not projected.`;
1776
+ }
1777
+ if (model.sdk === "codex") {
1778
+ return `${label} ${ref}: ${directCodexSandboxPosture(config.runtime.permissionMode).detail}`;
1779
+ }
1780
+ return `${label} ${ref}: OpenCode provider-owned execution with exact allow-all tool policy applies; mono-agent SRT rules are not projected.`;
1781
+ }
1782
+ function directCodexSandboxPosture(permissionMode) {
1783
+ if (permissionMode === "bypassPermissions") {
1784
+ return {
1785
+ status: "waiting",
1786
+ detail: "[WARN] Direct Codex bypassPermissions uses native danger-full-access with no filesystem or network sandbox; unattended approval prompts are still disabled.",
1787
+ };
1788
+ }
1789
+ if (permissionMode === "plan") {
1790
+ return {
1791
+ status: "ok",
1792
+ detail: "Direct Codex plan mode uses its native read-only sandbox with network disabled; unattended escalation requests are denied.",
1793
+ };
1794
+ }
1795
+ return {
1796
+ status: "ok",
1797
+ detail: "Direct Codex default/acceptEdits mode uses its native workspace-write sandbox with network disabled; unattended escalation requests are denied.",
1798
+ };
1799
+ }
704
1800
  async function runsSection(input, config) {
705
1801
  const artifactDir = await resolveAppArtifactDir(input);
706
1802
  const { totalRuns, runs, warnings } = await listRecordedRuns({ artifactDir, maxRuns: RUNS_HEALTH_MAX_RUNS, scope: "agent" });