@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.
- package/README.md +210 -19
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +33 -6
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +485 -87
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +528 -135
- package/dist/app.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +16 -6
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +76 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2102 -102
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +195 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +26 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +139 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +12 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +150 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor.d.ts +27 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1197 -101
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +521 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +757 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +88 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1212 -23
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +25 -1
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +281 -19
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/local-configuration.d.ts +56 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +914 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall.d.ts +38 -10
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +82 -25
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +54 -20
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +117 -2
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1505 -49
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +69 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +856 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +54 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +136 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +15 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +46 -6
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +145 -16
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +33 -2
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +826 -108
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +56 -9
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +172 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +59 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1133 -191
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +381 -17
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- 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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
404
|
+
if (directOpenCodeModels.length > 0
|
|
405
|
+
&& config.context.skillDisclosure === "index"
|
|
406
|
+
&& config.context.skillsRoot !== undefined) {
|
|
149
407
|
status = "error";
|
|
150
|
-
details.push(`
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
428
|
+
catch (error) {
|
|
158
429
|
status = "error";
|
|
159
|
-
details.push(
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
502
|
+
if (isPiBuiltinProvider(model.provider)
|
|
503
|
+
&& getPiBuiltinModels(model.provider).some((candidate) => candidate.id === model.model)) {
|
|
504
|
+
return undefined;
|
|
191
505
|
}
|
|
192
|
-
return
|
|
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
|
|
245
|
-
*
|
|
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`)
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* `waiting` with a re-auth hint.
|
|
251
|
-
* - SDK-authenticated providers (`claude:*` / `codex:*`): inspect
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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.
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
|
312
|
-
*
|
|
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
|
|
317
|
-
const
|
|
318
|
-
// A `
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
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 =
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
details.push(
|
|
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 (
|
|
345
|
-
|
|
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}\`
|
|
354
|
-
: `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\`
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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:
|
|
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 {
|
|
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 {
|
|
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" });
|