@mono-agent/agent-app 0.6.2 → 0.9.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 +259 -19
- package/dist/account-home.d.ts +6 -0
- package/dist/account-home.d.ts.map +1 -0
- package/dist/account-home.js +9 -0
- package/dist/account-home.js.map +1 -0
- 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 +3 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +16 -5
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +27 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +761 -147
- package/dist/app.js.map +1 -1
- package/dist/background-environment.d.ts +16 -0
- package/dist/background-environment.d.ts.map +1 -0
- package/dist/background-environment.js +68 -0
- package/dist/background-environment.js.map +1 -0
- package/dist/background-runtime.d.ts +62 -0
- package/dist/background-runtime.d.ts.map +1 -0
- package/dist/background-runtime.js +1115 -0
- package/dist/background-runtime.js.map +1 -0
- package/dist/background-snapshot-key.d.ts +13 -0
- package/dist/background-snapshot-key.d.ts.map +1 -0
- package/dist/background-snapshot-key.js +194 -0
- package/dist/background-snapshot-key.js.map +1 -0
- package/dist/background-snapshot.d.ts +96 -0
- package/dist/background-snapshot.d.ts.map +1 -0
- package/dist/background-snapshot.js +450 -0
- package/dist/background-snapshot.js.map +1 -0
- package/dist/background-worker-lease.d.ts +39 -0
- package/dist/background-worker-lease.d.ts.map +1 -0
- package/dist/background-worker-lease.js +356 -0
- package/dist/background-worker-lease.js.map +1 -0
- package/dist/background.d.ts +74 -2
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +815 -73
- package/dist/background.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/channel-plugins.d.ts +2 -0
- package/dist/channel-plugins.d.ts.map +1 -1
- package/dist/channel-plugins.js +8 -0
- package/dist/channel-plugins.js.map +1 -1
- package/dist/channels.d.ts +10 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +87 -9
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +86 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2421 -143
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +448 -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 +33 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +188 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +16 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +165 -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/continuation-command.d.ts +15 -0
- package/dist/continuation-command.d.ts.map +1 -0
- package/dist/continuation-command.js +170 -0
- package/dist/continuation-command.js.map +1 -0
- package/dist/continuation-config.d.ts +25 -0
- package/dist/continuation-config.d.ts.map +1 -0
- package/dist/continuation-config.js +236 -0
- package/dist/continuation-config.js.map +1 -0
- package/dist/continuation-service.d.ts +99 -0
- package/dist/continuation-service.d.ts.map +1 -0
- package/dist/continuation-service.js +1313 -0
- package/dist/continuation-service.js.map +1 -0
- package/dist/continuation-store.d.ts +104 -0
- package/dist/continuation-store.d.ts.map +1 -0
- package/dist/continuation-store.js +688 -0
- package/dist/continuation-store.js.map +1 -0
- package/dist/continuations.d.ts +167 -0
- package/dist/continuations.d.ts.map +1 -0
- package/dist/continuations.js +71 -0
- package/dist/continuations.js.map +1 -0
- package/dist/doctor.d.ts +29 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1611 -116
- 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 +584 -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 +728 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +95 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1218 -24
- 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/launchd.d.ts +16 -6
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +30 -15
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts +99 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +1299 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/managed-runtime-packages.d.ts +9 -0
- package/dist/managed-runtime-packages.d.ts.map +1 -0
- package/dist/managed-runtime-packages.js +56 -0
- package/dist/managed-runtime-packages.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-embedding-service.d.ts +33 -0
- package/dist/memory-embedding-service.d.ts.map +1 -0
- package/dist/memory-embedding-service.js +207 -0
- package/dist/memory-embedding-service.js.map +1 -0
- package/dist/memory-recall.d.ts +43 -14
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +103 -33
- 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 +141 -25
- 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/notify-runtime.d.ts +26 -0
- package/dist/notify-runtime.d.ts.map +1 -0
- package/dist/notify-runtime.js +27 -0
- package/dist/notify-runtime.js.map +1 -0
- package/dist/notify-tool.d.ts +51 -0
- package/dist/notify-tool.d.ts.map +1 -0
- package/dist/notify-tool.js +182 -0
- package/dist/notify-tool.js.map +1 -0
- 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/proactive-notify.d.ts +2 -0
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +1 -0
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.d.ts.map +1 -0
- package/dist/process-incarnation.js +208 -0
- package/dist/process-incarnation.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/recipes/base.d.ts +15 -0
- package/dist/recipes/base.d.ts.map +1 -0
- package/dist/recipes/base.js +51 -0
- package/dist/recipes/base.js.map +1 -0
- package/dist/recipes/catalog.d.ts +4 -0
- package/dist/recipes/catalog.d.ts.map +1 -0
- package/dist/recipes/catalog.js +525 -0
- package/dist/recipes/catalog.js.map +1 -0
- package/dist/recipes/index.d.ts +11 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +14 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/types.d.ts +70 -0
- package/dist/recipes/types.d.ts.map +1 -0
- package/dist/recipes/types.js +15 -0
- package/dist/recipes/types.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 +86 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +1402 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/setup.d.ts +29 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +97 -0
- package/dist/setup.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +56 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +142 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +56 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +305 -31
- 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 +141 -29
- 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 +1290 -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 +587 -20
- package/skills/mono-agent-composer/SKILL.md +6 -6
- package/skills/mono-agent-composer/references/config-blueprint.md +11 -10
- package/skills/mono-agent-composer/references/discovery-questions.md +33 -18
- package/skills/mono-agent-composer/references/feature-coverage.md +14 -12
- package/skills/mono-agent-composer/references/package-map.md +2 -2
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +3 -3
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, readdir, 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 { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
|
|
12
|
+
import { defaultExecutionModeForModel, describeMonoRuntimeSupport, isValidMcpServerName, 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,19 @@ 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 { loadContinuationSettings } from "./continuation-config.js";
|
|
22
|
+
import { CONTINUATION_RECORD_STORE_SCHEMA_VERSION, CONTINUATION_STORE_SCHEMA_VERSION, } from "./continuation-store.js";
|
|
23
|
+
import { CONTINUATION_STATES } from "./continuations.js";
|
|
24
|
+
import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interaction-bridge.js";
|
|
25
|
+
import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
|
|
26
|
+
import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } from "./memory-embedding-service.js";
|
|
15
27
|
import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
|
|
16
|
-
import {
|
|
28
|
+
import { piAuthRecoveryCommand } from "./provider-setup.js";
|
|
29
|
+
import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
|
|
30
|
+
import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
|
|
31
|
+
import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
|
|
32
|
+
import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
|
|
33
|
+
const execFile = promisify(execFileCallback);
|
|
17
34
|
/**
|
|
18
35
|
* Loads every config section the app would use at start and reports it
|
|
19
36
|
* per-section, so an engineer can see exactly what would run, wait, or fail —
|
|
@@ -36,6 +53,7 @@ export async function validateMonoAgentFolder(options) {
|
|
|
36
53
|
sections.push({ id: "core", label: "Core config", status: "error", details: [error.message] });
|
|
37
54
|
}
|
|
38
55
|
if (coreConfig !== undefined) {
|
|
56
|
+
const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(drivers, options);
|
|
39
57
|
const jsonResult = await readMonoAgentConfigJson(options.configPath);
|
|
40
58
|
// Channel secrets (bot tokens, API keys) live outside the core view, so the
|
|
41
59
|
// placement check spans both: core sections + every channel's config view.
|
|
@@ -55,10 +73,11 @@ export async function validateMonoAgentFolder(options) {
|
|
|
55
73
|
sections.push({ id: "secret-placement", label: "Config warnings", status: "waiting", details: configWarnings });
|
|
56
74
|
}
|
|
57
75
|
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));
|
|
76
|
+
sections.push(await credentialsSection(coreConfig, options.env, options.cwd, liveness, options.verifiedCredentialModelRefs, options.sdkAuthStatusExecFile, staticTriggerCredentialRefs));
|
|
77
|
+
sections.push(await contextSection(coreConfig, options.cwd));
|
|
78
|
+
sections.push(await memorySection(coreConfig, options.cwd, options.env, liveness, allowFilesystemWrites, options.preferAppPluginInstall === true));
|
|
61
79
|
sections.push(await toolsSection(coreConfig, options));
|
|
80
|
+
sections.push(await continuationSection(coreConfig, options));
|
|
62
81
|
sections.push(await sandboxSection(coreConfig, options.sandboxEngine));
|
|
63
82
|
}
|
|
64
83
|
sections.push(await exporterSection(options, liveness));
|
|
@@ -66,6 +85,9 @@ export async function validateMonoAgentFolder(options) {
|
|
|
66
85
|
for (const driver of drivers) {
|
|
67
86
|
sections.push(await channelSection(driver, options));
|
|
68
87
|
}
|
|
88
|
+
if (coreConfig !== undefined) {
|
|
89
|
+
await applyRequestModelOverrideCompatibilityChecks(sections, coreConfig, drivers, options);
|
|
90
|
+
}
|
|
69
91
|
// Cross-check the built `channel:*` statuses against the tool policy and annotate
|
|
70
92
|
// the tools section (send-tool-allowed-but-channel-disabled, or channel-enabled-
|
|
71
93
|
// but-no-send-tool). Only meaningful once coreConfig — and thus the tools section
|
|
@@ -78,6 +100,236 @@ export async function validateMonoAgentFolder(options) {
|
|
|
78
100
|
ok: sections.every((section) => section.status !== "error"),
|
|
79
101
|
};
|
|
80
102
|
}
|
|
103
|
+
/** Enabled, statically configured webhook/cron entries that can actually execute. */
|
|
104
|
+
function staticTriggerConfigEntries(driverId, loaded) {
|
|
105
|
+
if (!isUnknownRecord(loaded))
|
|
106
|
+
return [];
|
|
107
|
+
if (driverId === "webhook" && loaded.enabled === false)
|
|
108
|
+
return [];
|
|
109
|
+
const entries = driverId === "webhook" ? loaded.endpoints : loaded.jobs;
|
|
110
|
+
if (!Array.isArray(entries))
|
|
111
|
+
return [];
|
|
112
|
+
return entries.flatMap((entry, index) => {
|
|
113
|
+
if (!isUnknownRecord(entry) || entry.enabled === false)
|
|
114
|
+
return [];
|
|
115
|
+
return [{
|
|
116
|
+
entryPath: `${driverId}.${driverId === "webhook" ? "endpoints" : "jobs"}[${index}]`,
|
|
117
|
+
entry,
|
|
118
|
+
}];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Collect static trigger model overrides for credential readiness. Dynamic
|
|
123
|
+
* webhook request-body overrides are intentionally unavailable at validate time.
|
|
124
|
+
*/
|
|
125
|
+
async function collectStaticTriggerCredentialRefs(drivers, input) {
|
|
126
|
+
const refs = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const driver of drivers) {
|
|
129
|
+
if (driver.id !== "webhook" && driver.id !== "cron")
|
|
130
|
+
continue;
|
|
131
|
+
let loaded;
|
|
132
|
+
try {
|
|
133
|
+
loaded = await driver.loadConfig(input);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// The channel section owns malformed/unreadable config diagnostics.
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
|
|
140
|
+
if (typeof entry.model !== "string")
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const ref = parseMonoRuntimeModelReference(entry.model);
|
|
144
|
+
const key = `${entryPath}\u0000${modelReferenceKey(ref)}`;
|
|
145
|
+
if (seen.has(key))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(key);
|
|
148
|
+
refs.push({ label: entryPath, ref });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// The channel section owns invalid model-reference syntax.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return refs;
|
|
156
|
+
}
|
|
157
|
+
async function applyRequestModelOverrideCompatibilityChecks(sections, config, drivers, input) {
|
|
158
|
+
const baseIsDirectCodex = config.runtime.model.sdk === "codex";
|
|
159
|
+
const routeSafety = config.runtime.routeSafety ?? "uniform";
|
|
160
|
+
const directBoundaryConflicts = [];
|
|
161
|
+
const sandboxBypasses = [];
|
|
162
|
+
const toolPolicyBypasses = [];
|
|
163
|
+
const effortBypasses = [];
|
|
164
|
+
const turnCapBypasses = [];
|
|
165
|
+
const mcpBypasses = [];
|
|
166
|
+
const skillBypasses = [];
|
|
167
|
+
const piModelResolutionFailures = [];
|
|
168
|
+
const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
|
|
169
|
+
const restrictiveToolPolicy = !hasExactAllowAllToolPolicy(config.tools);
|
|
170
|
+
let configuredMcpServerNames = [];
|
|
171
|
+
if (config.tools.mcpConfigPath !== undefined) {
|
|
172
|
+
try {
|
|
173
|
+
configuredMcpServerNames = Object.keys((await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath)).mcpServers ?? {});
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// The tools section owns missing/malformed MCP-file diagnostics.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let adapterToolNames = [];
|
|
180
|
+
try {
|
|
181
|
+
const settings = await resolveAdapterSendToolsSettings(input, {
|
|
182
|
+
allowedTools: config.tools.allowedTools,
|
|
183
|
+
disallowedTools: config.tools.disallowedTools,
|
|
184
|
+
});
|
|
185
|
+
if (settings !== undefined)
|
|
186
|
+
adapterToolNames = adapterSendToolNames(settings);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Channel/tools sections own adapter config diagnostics.
|
|
190
|
+
}
|
|
191
|
+
const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames);
|
|
192
|
+
for (const driver of drivers) {
|
|
193
|
+
if (driver.id !== "webhook" && driver.id !== "cron")
|
|
194
|
+
continue;
|
|
195
|
+
let loaded;
|
|
196
|
+
try {
|
|
197
|
+
loaded = await driver.loadConfig(input);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// The channel section already reports malformed/unreadable config.
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
|
|
204
|
+
const hasModelOverride = typeof entry.model === "string";
|
|
205
|
+
const hasEffortOverride = typeof entry.effort === "string";
|
|
206
|
+
if (!hasModelOverride && !hasEffortOverride)
|
|
207
|
+
continue;
|
|
208
|
+
try {
|
|
209
|
+
const model = hasModelOverride
|
|
210
|
+
? parseMonoRuntimeModelReference(entry.model)
|
|
211
|
+
: config.runtime.model;
|
|
212
|
+
const location = hasModelOverride
|
|
213
|
+
? `${entryPath}.model=${entry.model}`
|
|
214
|
+
: `${entryPath}.effort=${entry.effort}`;
|
|
215
|
+
if (hasModelOverride) {
|
|
216
|
+
const resolutionIssue = piModelResolutionIssue(config, model);
|
|
217
|
+
if (resolutionIssue !== undefined) {
|
|
218
|
+
piModelResolutionFailures.push(`${location}: ${resolutionIssue}.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (routeSafety === "uniform" && hasModelOverride && (model.sdk === "codex") !== baseIsDirectCodex) {
|
|
222
|
+
directBoundaryConflicts.push(location);
|
|
223
|
+
}
|
|
224
|
+
if (routeSafety === "uniform" && hasModelOverride && monoSandboxActive && (model.sdk === "claude" || model.sdk === "opencode" || model.sdk === "codex")) {
|
|
225
|
+
sandboxBypasses.push(location);
|
|
226
|
+
}
|
|
227
|
+
if (hasModelOverride && restrictiveToolPolicy && model.sdk === "opencode") {
|
|
228
|
+
toolPolicyBypasses.push(location);
|
|
229
|
+
}
|
|
230
|
+
const effectiveEffort = hasEffortOverride ? entry.effort : config.runtime.effort;
|
|
231
|
+
const legacyFallbacks = (config.runtime.fallbacks?.length ?? 0) > 0
|
|
232
|
+
? []
|
|
233
|
+
: config.runtime.fallbackModels ?? [];
|
|
234
|
+
const directOpenCodeModels = [model, ...legacyFallbacks]
|
|
235
|
+
.filter((candidate) => candidate.sdk === "opencode");
|
|
236
|
+
if (directOpenCodeModels.length > 0 && effectiveEffort !== undefined) {
|
|
237
|
+
effortBypasses.push(`${location} (effective effort=${effectiveEffort}) (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
|
|
238
|
+
}
|
|
239
|
+
if (directOpenCodeModels.length > 0 && Number(config.runtime.maxTurns) > 0) {
|
|
240
|
+
turnCapBypasses.push(`${location} (runtime.maxTurns=${config.runtime.maxTurns}; direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
|
|
241
|
+
}
|
|
242
|
+
if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
|
|
243
|
+
mcpBypasses.push(`${location} (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")}; MCP sources=${effectiveMcpSources.join("; ")})`);
|
|
244
|
+
}
|
|
245
|
+
if (directOpenCodeModels.length > 0
|
|
246
|
+
&& config.context.skillDisclosure === "index"
|
|
247
|
+
&& config.context.skillsRoot !== undefined) {
|
|
248
|
+
skillBypasses.push(location);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Adapter configIssues owns syntax diagnostics.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (directBoundaryConflicts.length === 0
|
|
257
|
+
&& sandboxBypasses.length === 0
|
|
258
|
+
&& toolPolicyBypasses.length === 0
|
|
259
|
+
&& effortBypasses.length === 0
|
|
260
|
+
&& turnCapBypasses.length === 0
|
|
261
|
+
&& mcpBypasses.length === 0
|
|
262
|
+
&& skillBypasses.length === 0
|
|
263
|
+
&& piModelResolutionFailures.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
const index = sections.findIndex((section) => section.id === "runtime");
|
|
266
|
+
if (index < 0)
|
|
267
|
+
return;
|
|
268
|
+
const runtime = sections[index];
|
|
269
|
+
sections[index] = {
|
|
270
|
+
...runtime,
|
|
271
|
+
status: "error",
|
|
272
|
+
details: [
|
|
273
|
+
...runtime.details,
|
|
274
|
+
...(directBoundaryConflicts.length === 0
|
|
275
|
+
? []
|
|
276
|
+
: [
|
|
277
|
+
"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.",
|
|
278
|
+
...directBoundaryConflicts,
|
|
279
|
+
]),
|
|
280
|
+
...(sandboxBypasses.length === 0
|
|
281
|
+
? []
|
|
282
|
+
: [
|
|
283
|
+
"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.",
|
|
284
|
+
...sandboxBypasses,
|
|
285
|
+
]),
|
|
286
|
+
...(toolPolicyBypasses.length === 0
|
|
287
|
+
? []
|
|
288
|
+
: [
|
|
289
|
+
"Per-trigger direct OpenCode model overrides require exact allow-all because OpenCode's provider-owned tool loop does not consume mono-agent allowedTools/disallowedTools.",
|
|
290
|
+
...toolPolicyBypasses,
|
|
291
|
+
]),
|
|
292
|
+
...(effortBypasses.length === 0
|
|
293
|
+
? []
|
|
294
|
+
: [
|
|
295
|
+
"Per-trigger direct OpenCode routes cannot receive runtime effort because the OpenCode SDK does not expose effort control.",
|
|
296
|
+
...effortBypasses,
|
|
297
|
+
]),
|
|
298
|
+
...(turnCapBypasses.length === 0
|
|
299
|
+
? []
|
|
300
|
+
: [
|
|
301
|
+
"Per-trigger direct OpenCode routes cannot enforce runtime.maxTurns; omit it, set it to 0, or use a runtime with a hard turn cap.",
|
|
302
|
+
...turnCapBypasses,
|
|
303
|
+
]),
|
|
304
|
+
...(mcpBypasses.length === 0
|
|
305
|
+
? []
|
|
306
|
+
: [
|
|
307
|
+
"Per-trigger direct OpenCode routes cannot receive configured or auto-provisioned MCP runtime options.",
|
|
308
|
+
...mcpBypasses,
|
|
309
|
+
]),
|
|
310
|
+
...(skillBypasses.length === 0
|
|
311
|
+
? []
|
|
312
|
+
: [
|
|
313
|
+
"Per-trigger direct OpenCode routes cannot use index skill disclosure because the bridge disables external/runtime skills; use full disclosure or a Pi runtime.",
|
|
314
|
+
...skillBypasses,
|
|
315
|
+
]),
|
|
316
|
+
...(piModelResolutionFailures.length === 0
|
|
317
|
+
? []
|
|
318
|
+
: [
|
|
319
|
+
"Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
|
|
320
|
+
...piModelResolutionFailures,
|
|
321
|
+
]),
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function isUnknownRecord(value) {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
|
+
}
|
|
328
|
+
function hasExactAllowAllToolPolicy(tools) {
|
|
329
|
+
return tools.allowedTools.length === 1
|
|
330
|
+
&& tools.allowedTools[0] === "*"
|
|
331
|
+
&& tools.disallowedTools.length === 0;
|
|
332
|
+
}
|
|
81
333
|
/** Adapter send tools each channel owns; an allowed entry needs BOTH the tool AND the enabled channel. */
|
|
82
334
|
const CHANNEL_OWNED_SEND_TOOLS = {
|
|
83
335
|
slack: ["SlackSendMessage"],
|
|
@@ -141,55 +393,136 @@ function applyToolChannelCrossChecks(sections, allowedTools, disallowedTools) {
|
|
|
141
393
|
function runtimeSection(config) {
|
|
142
394
|
const details = [];
|
|
143
395
|
let status = "ok";
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
396
|
+
const routes = configuredRuntimeRouteChecks(config);
|
|
397
|
+
const routeSafety = config.runtime.routeSafety ?? "uniform";
|
|
398
|
+
details.push(`Route safety: ${routeSafety}.`);
|
|
399
|
+
if (routes.length > 1) {
|
|
400
|
+
details.push(routeSafety === "per-route-native"
|
|
401
|
+
? "Mixed runtime families are allowed; every attempt uses its explicit route-native safety contract."
|
|
402
|
+
: "Fallback routes use the uniform compatibility contract; validation fails closed when any route cannot represent a required capability.");
|
|
403
|
+
}
|
|
404
|
+
const directOpenCodeModels = routes.filter((route) => route.model.sdk === "opencode");
|
|
405
|
+
if (Number(config.runtime.maxTurns) > 0 && directOpenCodeModels.length > 0) {
|
|
406
|
+
status = "error";
|
|
407
|
+
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
408
|
}
|
|
148
|
-
|
|
409
|
+
if (directOpenCodeModels.length > 0
|
|
410
|
+
&& config.context.skillDisclosure === "index"
|
|
411
|
+
&& config.context.skillsRoot !== undefined) {
|
|
149
412
|
status = "error";
|
|
150
|
-
details.push(`
|
|
413
|
+
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
414
|
}
|
|
152
|
-
for (const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
415
|
+
for (const route of routes) {
|
|
416
|
+
try {
|
|
417
|
+
const support = describeMonoRuntimeSupport(route.model, route.executionMode);
|
|
418
|
+
const resolutionIssue = piModelResolutionIssue(config, route.model);
|
|
419
|
+
const effortIssue = runtimeRouteEffortIssue(config, route);
|
|
420
|
+
if (support.compatible && resolutionIssue === undefined) {
|
|
421
|
+
details.push(`${route.label} ${referenceOf(route.model)} runs on ${support.backend?.label ?? "unknown backend"} ` +
|
|
422
|
+
`(effort: ${route.effort ?? "provider default"}).`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
status = "error";
|
|
426
|
+
details.push(`${route.label} ${referenceOf(route.model)}: ${resolutionIssue ?? support.incompatibilityReason ?? "unsupported"}.`);
|
|
427
|
+
}
|
|
428
|
+
if (effortIssue !== undefined) {
|
|
429
|
+
status = "error";
|
|
430
|
+
details.push(`${route.label} ${referenceOf(route.model)}: ${effortIssue}`);
|
|
431
|
+
}
|
|
156
432
|
}
|
|
157
|
-
|
|
433
|
+
catch (error) {
|
|
158
434
|
status = "error";
|
|
159
|
-
details.push(
|
|
435
|
+
details.push(`${route.label} ${referenceOf(route.model)}: ${error instanceof Error ? error.message : String(error)}.`);
|
|
160
436
|
}
|
|
161
437
|
}
|
|
162
438
|
return { id: "runtime", label: "Runtime", status, details };
|
|
163
439
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
440
|
+
function configuredRuntimeRouteChecks(config) {
|
|
441
|
+
const primary = {
|
|
442
|
+
label: "Primary model",
|
|
443
|
+
model: config.runtime.model,
|
|
444
|
+
...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
|
|
445
|
+
executionMode: config.runtime.executionMode,
|
|
446
|
+
};
|
|
447
|
+
if ((config.runtime.fallbacks?.length ?? 0) > 0) {
|
|
448
|
+
return [
|
|
449
|
+
primary,
|
|
450
|
+
...(config.runtime.fallbacks ?? []).map((fallback) => ({
|
|
451
|
+
label: "Fallback model",
|
|
452
|
+
model: fallback.model,
|
|
453
|
+
...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
|
|
454
|
+
})),
|
|
455
|
+
];
|
|
174
456
|
}
|
|
175
|
-
|
|
176
|
-
|
|
457
|
+
return [
|
|
458
|
+
primary,
|
|
459
|
+
...(config.runtime.fallbackModels ?? []).map((model) => ({
|
|
460
|
+
label: "Fallback model",
|
|
461
|
+
model,
|
|
462
|
+
...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
|
|
463
|
+
})),
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
function runtimeRouteEffortIssue(config, route) {
|
|
467
|
+
if (route.effort === undefined)
|
|
468
|
+
return undefined;
|
|
469
|
+
if (route.model.sdk === "opencode") {
|
|
470
|
+
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.`;
|
|
471
|
+
}
|
|
472
|
+
const metadata = resolveModelEffortLevels(route.model, config.providers?.local);
|
|
473
|
+
if (metadata.effortLevels !== undefined && !metadata.effortLevels.includes(route.effort)) {
|
|
474
|
+
return `effort=${route.effort} is unsupported by known model metadata; choose ${metadata.effortLevels.join(", ")}, or omit it for provider default.`;
|
|
475
|
+
}
|
|
476
|
+
if (metadata.reasoning === false && route.effort !== "none") {
|
|
477
|
+
return `effort=${route.effort} is unsupported because known model metadata marks this route as non-reasoning; use none or provider default.`;
|
|
478
|
+
}
|
|
479
|
+
if (route.model.sdk === "claude"
|
|
480
|
+
&& (route.executionMode ?? defaultExecutionModeForModel(route.model)) === "sdk"
|
|
481
|
+
&& !["low", "medium", "high", "xhigh", "max"].includes(route.effort)) {
|
|
482
|
+
return `effort=${route.effort} is unsupported by the Claude Agent SDK; choose low, medium, high, xhigh, max, or provider default.`;
|
|
177
483
|
}
|
|
178
484
|
return undefined;
|
|
179
485
|
}
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Mirrors the Pi runtime's actual model-resolution boundary. Mono-agent only
|
|
488
|
+
* registers a custom Pi provider from `providers.local`; it deliberately does
|
|
489
|
+
* not import Pi CLI's ambient sibling `models.json`. Without a matching local
|
|
490
|
+
* provider, `resolvePiRuntimeModel` performs an exact built-in catalog lookup.
|
|
491
|
+
*/
|
|
492
|
+
function piModelResolutionIssue(config, model) {
|
|
493
|
+
if (model.sdk !== "pi" || model.provider === undefined) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const localProvider = config.providers?.local?.find((provider) => provider.id === model.provider);
|
|
497
|
+
if (localProvider !== undefined) {
|
|
498
|
+
if (localProvider.enabled === false) {
|
|
499
|
+
return `provider \`${model.provider}\` is disabled in providers.local`;
|
|
187
500
|
}
|
|
501
|
+
const localModel = localProvider.models?.find((candidate) => candidate.name === model.model || candidate.alias === model.model);
|
|
502
|
+
if (localModel?.enabled === false) {
|
|
503
|
+
return `model \`${model.model}\` is disabled in providers.local for provider \`${model.provider}\``;
|
|
504
|
+
}
|
|
505
|
+
return undefined;
|
|
188
506
|
}
|
|
189
|
-
|
|
190
|
-
|
|
507
|
+
if (isPiBuiltinProvider(model.provider)
|
|
508
|
+
&& getPiBuiltinModels(model.provider).some((candidate) => candidate.id === model.model)) {
|
|
509
|
+
return undefined;
|
|
191
510
|
}
|
|
192
|
-
return
|
|
511
|
+
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. ` +
|
|
512
|
+
"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");
|
|
513
|
+
}
|
|
514
|
+
function isPiBuiltinProvider(provider) {
|
|
515
|
+
return getPiBuiltinProviders().includes(provider);
|
|
516
|
+
}
|
|
517
|
+
const PI_API_KEY_ENV_BY_PROVIDER = {
|
|
518
|
+
"opencode-go": "OPENCODE_API_KEY",
|
|
519
|
+
};
|
|
520
|
+
/** Inspects Pi credentials without following aliases or reading unbounded input. */
|
|
521
|
+
async function readPiAuthProviders(path) {
|
|
522
|
+
const inspection = await inspectPiAuthStore(path);
|
|
523
|
+
if (inspection.status !== "ok")
|
|
524
|
+
return inspection;
|
|
525
|
+
return { status: "ok", providers: inspection.auth };
|
|
193
526
|
}
|
|
194
527
|
/**
|
|
195
528
|
* What each SDK-authenticated backend truthfully reads for credentials. Only the
|
|
@@ -215,14 +548,165 @@ const SDK_AUTH_SCHEMES = {
|
|
|
215
548
|
loginCommand: "claude /login",
|
|
216
549
|
loginDetail: "a `claude /login` subscription session or a Bedrock/Vertex configuration (CLAUDE_CODE_USE_BEDROCK/VERTEX)",
|
|
217
550
|
failureHint: 'the first turn fails with an opaque "Claude Code process exited with code 1" that names nothing',
|
|
551
|
+
statusCommand: "claude auth status --json",
|
|
552
|
+
statusArgs: ["auth", "status", "--json"],
|
|
218
553
|
},
|
|
219
554
|
codex: {
|
|
220
555
|
envKeys: ["OPENAI_API_KEY"],
|
|
221
556
|
loginCommand: "codex login",
|
|
222
557
|
loginDetail: "a `codex login` ChatGPT session (`~/.codex/auth.json`, outside the environment)",
|
|
223
558
|
failureHint: "the first turn fails to authenticate",
|
|
559
|
+
statusCommand: "codex login status",
|
|
560
|
+
statusArgs: ["login", "status"],
|
|
224
561
|
},
|
|
225
562
|
};
|
|
563
|
+
const SDK_AUTH_STATUS_TIMEOUT_MS = 5_000;
|
|
564
|
+
const SDK_AUTH_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
|
|
565
|
+
function isSdkAuthName(value) {
|
|
566
|
+
return value === "claude" || value === "codex";
|
|
567
|
+
}
|
|
568
|
+
const defaultSdkAuthStatusExecFile = async (file, args, options) => {
|
|
569
|
+
const { stdout } = await execFile(file, [...args], options);
|
|
570
|
+
return { stdout };
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* Performs the SDK's local, read-only login-status command. A zero Codex exit
|
|
574
|
+
* confirms its external login; Claude additionally requires strict JSON with
|
|
575
|
+
* `loggedIn: true`. Missing binaries, timeouts, non-zero exits, and malformed
|
|
576
|
+
* output all fail closed without leaking command output into validation.
|
|
577
|
+
*/
|
|
578
|
+
async function checkSdkExternalLoginStatus(sdk, env, cwd, run) {
|
|
579
|
+
const scheme = SDK_AUTH_SCHEMES[sdk];
|
|
580
|
+
try {
|
|
581
|
+
const { stdout } = await run(sdk, scheme.statusArgs, {
|
|
582
|
+
cwd,
|
|
583
|
+
env,
|
|
584
|
+
timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
|
|
585
|
+
maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
|
|
586
|
+
encoding: "utf8",
|
|
587
|
+
});
|
|
588
|
+
if (sdk === "codex") {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
const parsed = JSON.parse(stdout);
|
|
592
|
+
return parsed !== null
|
|
593
|
+
&& typeof parsed === "object"
|
|
594
|
+
&& !Array.isArray(parsed)
|
|
595
|
+
&& parsed.loggedIn === true;
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/** Read provider IDs directly from auth.json without launching mutation-capable OpenCode middleware. */
|
|
602
|
+
async function inspectOpenCodeCredentialProviders(env) {
|
|
603
|
+
if (env.OPENCODE_AUTH_CONTENT !== undefined) {
|
|
604
|
+
return { status: "inline_auth_unsupported" };
|
|
605
|
+
}
|
|
606
|
+
const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
|
|
607
|
+
? env.XDG_DATA_HOME
|
|
608
|
+
: join(typeof env.HOME === "string" && env.HOME.length > 0 ? env.HOME : homedir(), ".local", "share");
|
|
609
|
+
const opencodeData = join(dataHome, "opencode");
|
|
610
|
+
const marker = await regularCurrentUserFile(join(opencodeData, "opencode.db"));
|
|
611
|
+
if (!marker)
|
|
612
|
+
return { status: "migration_required" };
|
|
613
|
+
const authPath = join(opencodeData, "auth.json");
|
|
614
|
+
if (!(await regularCurrentUserFile(authPath)))
|
|
615
|
+
return { status: "auth_missing" };
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(await readFile(authPath, "utf8"));
|
|
618
|
+
if (!isUnknownRecord(parsed))
|
|
619
|
+
return { status: "auth_invalid" };
|
|
620
|
+
const providers = new Set();
|
|
621
|
+
for (const [provider, credential] of Object.entries(parsed)) {
|
|
622
|
+
if (provider.length === 0 || provider.trim() !== provider || !isOpenCodeCredentialEntry(credential)) {
|
|
623
|
+
return { status: "auth_invalid" };
|
|
624
|
+
}
|
|
625
|
+
providers.add(provider);
|
|
626
|
+
}
|
|
627
|
+
return { status: "ok", providers };
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return { status: "auth_invalid" };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function regularCurrentUserFile(path) {
|
|
634
|
+
try {
|
|
635
|
+
const info = await lstat(path);
|
|
636
|
+
return info.isFile() && (typeof process.getuid !== "function" || info.uid === process.getuid());
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function isOpenCodeCredentialEntry(value) {
|
|
643
|
+
if (!isUnknownRecord(value))
|
|
644
|
+
return false;
|
|
645
|
+
if (value.type === "oauth") {
|
|
646
|
+
return typeof value.refresh === "string"
|
|
647
|
+
&& typeof value.access === "string"
|
|
648
|
+
&& (value.refresh.trim().length > 0 || value.access.trim().length > 0)
|
|
649
|
+
&& typeof value.expires === "number"
|
|
650
|
+
&& Number.isSafeInteger(value.expires)
|
|
651
|
+
&& value.expires >= 0
|
|
652
|
+
&& (value.accountId === undefined || typeof value.accountId === "string")
|
|
653
|
+
&& (value.enterpriseUrl === undefined || typeof value.enterpriseUrl === "string");
|
|
654
|
+
}
|
|
655
|
+
if (value.type === "api") {
|
|
656
|
+
return typeof value.key === "string"
|
|
657
|
+
&& value.key.trim().length > 0
|
|
658
|
+
&& (value.metadata === undefined
|
|
659
|
+
|| (isUnknownRecord(value.metadata) && Object.values(value.metadata).every((entry) => typeof entry === "string")));
|
|
660
|
+
}
|
|
661
|
+
if (value.type === "wellknown") {
|
|
662
|
+
return typeof value.key === "string" && value.key.trim().length > 0
|
|
663
|
+
&& typeof value.token === "string" && value.token.trim().length > 0;
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
async function checkOpenCodeVersion(env, cwd, run) {
|
|
668
|
+
const versionEnv = {};
|
|
669
|
+
for (const key of [
|
|
670
|
+
"PATH",
|
|
671
|
+
"LANG",
|
|
672
|
+
"LC_ALL",
|
|
673
|
+
"LC_CTYPE",
|
|
674
|
+
"SHELL",
|
|
675
|
+
"TMPDIR",
|
|
676
|
+
"TMP",
|
|
677
|
+
"TEMP",
|
|
678
|
+
"SystemRoot",
|
|
679
|
+
"WINDIR",
|
|
680
|
+
"ComSpec",
|
|
681
|
+
"PATHEXT",
|
|
682
|
+
]) {
|
|
683
|
+
const value = env[key];
|
|
684
|
+
if (typeof value === "string" && value.length > 0)
|
|
685
|
+
versionEnv[key] = value;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
const { stdout } = await run("opencode", ["--version"], {
|
|
689
|
+
cwd,
|
|
690
|
+
env: versionEnv,
|
|
691
|
+
timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
|
|
692
|
+
maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
|
|
693
|
+
encoding: "utf8",
|
|
694
|
+
});
|
|
695
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/u.exec(stdout.trim());
|
|
696
|
+
if (match === null)
|
|
697
|
+
return false;
|
|
698
|
+
const major = Number(match[1]);
|
|
699
|
+
const minor = Number(match[2]);
|
|
700
|
+
const patch = Number(match[3]);
|
|
701
|
+
return Number.isSafeInteger(major)
|
|
702
|
+
&& Number.isSafeInteger(minor)
|
|
703
|
+
&& Number.isSafeInteger(patch)
|
|
704
|
+
&& (major > 1 || (major === 1 && minor >= 15));
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
226
710
|
/** First env key whose value is present and non-blank, or undefined when none are set. */
|
|
227
711
|
function firstPresentEnvKey(env, keys) {
|
|
228
712
|
for (const key of keys) {
|
|
@@ -241,26 +725,35 @@ function firstPresentEnvKey(env, keys) {
|
|
|
241
725
|
* and the E1 fresh-instance case where `claude:*` with no `ANTHROPIC_API_KEY` "validates"
|
|
242
726
|
* clean but the first turn dies with an opaque "process exited with code 1").
|
|
243
727
|
*
|
|
244
|
-
* The check is
|
|
245
|
-
*
|
|
728
|
+
* The check is read-only. Static validation never launches a process; live validation may
|
|
729
|
+
* run bounded, local SDK login-status commands, which inspect durable external login state
|
|
730
|
+
* without making a model turn or mutating the auth store.
|
|
246
731
|
*
|
|
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
|
-
*
|
|
732
|
+
* - Pi providers: inspect the auth store (`piAuthPath`) and the config's own
|
|
733
|
+
* `providers.local` custom providers. A custom/local provider follows its declared
|
|
734
|
+
* `apiKey` / `apiKeyEnv` contract instead of Pi OAuth; an OAuth provider absent from
|
|
735
|
+
* the store, or whose access token has expired, is flagged `waiting` with a re-auth hint.
|
|
736
|
+
* - SDK-authenticated providers (`claude:*` / `codex:*`): inspect the RESOLVED ENV (process
|
|
737
|
+
* env + loaded `.env`) for the backend's accepted keys. During live validation only, a
|
|
738
|
+
* missing env credential falls back to `claude auth status --json` / `codex login status`.
|
|
739
|
+
* The commands use the same resolved environment (including PATH and HOME), are bounded,
|
|
740
|
+
* cached once per SDK, and never make a model turn. Static validation remains process-free.
|
|
741
|
+
* - Direct OpenCode (`opencode:<provider>:<model>`): inspect exact provider IDs in the
|
|
742
|
+
* standard auth.json and require the native DB migration marker. Static validation launches
|
|
743
|
+
* no process. Live validation runs only a bounded `opencode --version` preflight; it never
|
|
744
|
+
* runs the mutation-capable auth middleware or makes a model turn.
|
|
256
745
|
*
|
|
257
746
|
* `waiting` (never `error`) keeps the verdict non-fatal, mirroring the Ollama/Phoenix
|
|
258
747
|
* probes — the goal is visibility, not blocking start.
|
|
259
748
|
*/
|
|
260
|
-
async function credentialsSection(config, env) {
|
|
749
|
+
async function credentialsSection(config, env, cwd, liveness, verifiedCredentialModelRefs = [], sdkAuthStatusExecFile = defaultSdkAuthStatusExecFile, staticTriggerRefs = []) {
|
|
261
750
|
const refs = [
|
|
262
751
|
{ label: "Primary", ref: config.runtime.model },
|
|
263
|
-
...(config.
|
|
752
|
+
...configuredRuntimeRouteChecks(config).slice(1).map((route) => ({
|
|
753
|
+
label: "Fallback",
|
|
754
|
+
ref: route.model,
|
|
755
|
+
})),
|
|
756
|
+
...staticTriggerRefs,
|
|
264
757
|
];
|
|
265
758
|
if (config.memory?.llm !== undefined && config.memory.llm.provider !== "ollama") {
|
|
266
759
|
try {
|
|
@@ -270,9 +763,10 @@ async function credentialsSection(config, env) {
|
|
|
270
763
|
// A malformed memory model reference is surfaced by the memory/runtime shape checks.
|
|
271
764
|
}
|
|
272
765
|
}
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
766
|
+
const authenticatedRefs = refs.filter((r) => (r.ref.sdk === "pi" && typeof r.ref.provider === "string")
|
|
767
|
+
|| (r.ref.sdk === "opencode" && typeof r.ref.provider === "string")
|
|
768
|
+
|| isSdkAuthName(r.ref.sdk));
|
|
769
|
+
if (authenticatedRefs.length === 0) {
|
|
276
770
|
return {
|
|
277
771
|
id: "credentials",
|
|
278
772
|
label: "Provider credentials",
|
|
@@ -282,12 +776,49 @@ async function credentialsSection(config, env) {
|
|
|
282
776
|
}
|
|
283
777
|
const details = [];
|
|
284
778
|
let status = "ok";
|
|
779
|
+
const verified = new Set(verifiedCredentialModelRefs);
|
|
780
|
+
for (const { label, ref } of authenticatedRefs) {
|
|
781
|
+
const refStr = referenceOf(ref);
|
|
782
|
+
if (verified.has(refStr)) {
|
|
783
|
+
details.push(`${label} ${refStr}: credentials verified by a successful live model check.`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const unverifiedRefs = authenticatedRefs.filter(({ ref }) => !verified.has(referenceOf(ref)));
|
|
787
|
+
const piRefs = unverifiedRefs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
|
|
788
|
+
const openCodeRefs = unverifiedRefs.filter((r) => r.ref.sdk === "opencode" && typeof r.ref.provider === "string");
|
|
789
|
+
const sdkRefs = unverifiedRefs.filter((r) => isSdkAuthName(r.ref.sdk));
|
|
285
790
|
if (piRefs.length > 0) {
|
|
286
|
-
const piStatus = await appendPiCredentialDetails(config, piRefs, details);
|
|
791
|
+
const piStatus = await appendPiCredentialDetails(config, piRefs, details, env);
|
|
287
792
|
if (piStatus === "waiting") {
|
|
288
793
|
status = "waiting";
|
|
289
794
|
}
|
|
290
795
|
}
|
|
796
|
+
const openCodeInspection = openCodeRefs.length > 0
|
|
797
|
+
? inspectOpenCodeCredentialProviders(env)
|
|
798
|
+
: undefined;
|
|
799
|
+
const openCodeVersion = liveness && openCodeRefs.length > 0
|
|
800
|
+
? checkOpenCodeVersion(env, cwd, sdkAuthStatusExecFile)
|
|
801
|
+
: undefined;
|
|
802
|
+
const sdkAuthStatuses = new Map();
|
|
803
|
+
const externalLoginStatus = (sdk) => {
|
|
804
|
+
const cached = sdkAuthStatuses.get(sdk);
|
|
805
|
+
if (cached !== undefined) {
|
|
806
|
+
return cached;
|
|
807
|
+
}
|
|
808
|
+
const pending = checkSdkExternalLoginStatus(sdk, env, cwd, sdkAuthStatusExecFile);
|
|
809
|
+
sdkAuthStatuses.set(sdk, pending);
|
|
810
|
+
return pending;
|
|
811
|
+
};
|
|
812
|
+
// Start each unique local status check before awaiting details so two SDKs
|
|
813
|
+
// cost one bounded timeout window rather than running serially.
|
|
814
|
+
if (liveness) {
|
|
815
|
+
for (const { ref } of sdkRefs) {
|
|
816
|
+
const scheme = SDK_AUTH_SCHEMES[ref.sdk];
|
|
817
|
+
if (scheme !== undefined && firstPresentEnvKey(env, scheme.envKeys) === undefined) {
|
|
818
|
+
void externalLoginStatus(ref.sdk);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
291
822
|
for (const { label, ref } of sdkRefs) {
|
|
292
823
|
const refStr = referenceOf(ref);
|
|
293
824
|
const scheme = SDK_AUTH_SCHEMES[ref.sdk];
|
|
@@ -296,32 +827,69 @@ async function credentialsSection(config, env) {
|
|
|
296
827
|
}
|
|
297
828
|
const present = firstPresentEnvKey(env, scheme.envKeys);
|
|
298
829
|
if (present !== undefined) {
|
|
299
|
-
details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}).`);
|
|
830
|
+
details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}); credential detected, live model verification is still pending.`);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (liveness && await externalLoginStatus(ref.sdk)) {
|
|
834
|
+
details.push(`${label} ${refStr}: external sign-in detected by read-only \`${scheme.statusCommand}\`; ` +
|
|
835
|
+
"credentials are not verified until a live model turn succeeds.");
|
|
300
836
|
continue;
|
|
301
837
|
}
|
|
302
838
|
status = "waiting";
|
|
303
839
|
details.push(`[WARN] ${label} ${refStr}: no SDK credential in the resolved env (checked ${scheme.envKeys.join(", ")}). ` +
|
|
304
|
-
|
|
840
|
+
(liveness
|
|
841
|
+
? `External login was not verified by \`${scheme.statusCommand}\`; `
|
|
842
|
+
: `If you authenticated via ${scheme.loginDetail} this is fine and can't be verified during static validation; `) +
|
|
305
843
|
`otherwise ${scheme.failureHint} — set ${scheme.envKeys[0]} or run \`${scheme.loginCommand}\`.`);
|
|
306
844
|
}
|
|
845
|
+
if (openCodeRefs.length > 0) {
|
|
846
|
+
const inspection = openCodeInspection === undefined ? undefined : await openCodeInspection;
|
|
847
|
+
const supportedVersion = openCodeVersion === undefined ? false : await openCodeVersion;
|
|
848
|
+
for (const { label, ref } of openCodeRefs) {
|
|
849
|
+
const refStr = referenceOf(ref);
|
|
850
|
+
const provider = ref.provider;
|
|
851
|
+
const credentialPresent = inspection?.status === "ok" && inspection.providers.has(provider);
|
|
852
|
+
if (credentialPresent && liveness && supportedVersion) {
|
|
853
|
+
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.`);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
status = "waiting";
|
|
857
|
+
const warning = inspection?.status === "migration_required"
|
|
858
|
+
? "the native OpenCode database migration marker is missing or invalid; run `opencode db migrate --pure` once"
|
|
859
|
+
: inspection?.status === "inline_auth_unsupported"
|
|
860
|
+
? "OPENCODE_AUTH_CONTENT is unsupported for direct runs; persist credentials with `opencode auth login` and unset it"
|
|
861
|
+
: inspection?.status === "auth_invalid"
|
|
862
|
+
? "the standard OpenCode auth.json is malformed or contains an unsupported credential entry"
|
|
863
|
+
: credentialPresent && !liveness
|
|
864
|
+
? "credentials and migration marker are present, but the required stable OpenCode CLI >=1.15.0 is unverified during static validation"
|
|
865
|
+
: credentialPresent && !supportedVersion
|
|
866
|
+
? "credentials are present, but stable OpenCode CLI >=1.15.0 could not be verified"
|
|
867
|
+
: inspection?.status === "ok"
|
|
868
|
+
? `no exact credential entry exists for provider \`${provider}\`; run \`opencode auth login\` for that provider`
|
|
869
|
+
: "the standard OpenCode auth.json is missing; run `opencode auth login`";
|
|
870
|
+
const safetyNote = liveness
|
|
871
|
+
? "No model turn or mutation-capable OpenCode command was run."
|
|
872
|
+
: "No OpenCode process was launched.";
|
|
873
|
+
details.push(`[WARN] ${label} ${refStr}: ${warning}. ${safetyNote}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
307
877
|
return { id: "credentials", label: "Provider credentials", status, details };
|
|
308
878
|
}
|
|
309
879
|
/**
|
|
310
880
|
* Appends one detail line per Pi provider reference and returns "waiting" if any
|
|
311
|
-
* was flagged (missing/expired), else "ok". Recognizes providers
|
|
312
|
-
*
|
|
881
|
+
* was flagged (missing/expired), else "ok". Recognizes custom providers only
|
|
882
|
+
* through the config's `providers.local` set, matching the runtime boundary.
|
|
313
883
|
*/
|
|
314
|
-
async function appendPiCredentialDetails(config, piRefs, details) {
|
|
884
|
+
async function appendPiCredentialDetails(config, piRefs, details, env) {
|
|
315
885
|
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));
|
|
886
|
+
const authInspection = authPath === undefined ? undefined : await readPiAuthProviders(authPath);
|
|
887
|
+
const authProviders = authInspection?.status === "ok" ? authInspection.providers : undefined;
|
|
888
|
+
// A matching `providers.local` entry owns the runtime route, even when its ID
|
|
889
|
+
// collides with a Pi built-in provider. Report the local entry's declared auth
|
|
890
|
+
// contract instead of consulting the unrelated Pi auth store. A disabled entry
|
|
891
|
+
// remains authoritative because the runtime rejects it before credential use.
|
|
892
|
+
const localProviders = new Map((config.providers?.local ?? []).map((provider) => [provider.id, provider]));
|
|
325
893
|
const now = Date.now();
|
|
326
894
|
let status = "ok";
|
|
327
895
|
if (authPath !== undefined) {
|
|
@@ -330,45 +898,99 @@ async function appendPiCredentialDetails(config, piRefs, details) {
|
|
|
330
898
|
for (const { label, ref } of piRefs) {
|
|
331
899
|
const provider = ref.provider;
|
|
332
900
|
const refStr = referenceOf(ref);
|
|
333
|
-
const loginCommand =
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
|
|
901
|
+
const loginCommand = piAuthRecoveryCommand(provider, authPath);
|
|
902
|
+
const localProvider = localProviders.get(provider);
|
|
903
|
+
if (localProvider !== undefined) {
|
|
904
|
+
if (localProvider.enabled === false) {
|
|
905
|
+
status = "waiting";
|
|
906
|
+
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.`);
|
|
907
|
+
}
|
|
908
|
+
else if (localProvider.apiKey !== undefined) {
|
|
909
|
+
details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
|
|
910
|
+
}
|
|
911
|
+
else if (localProvider.apiKeyEnv !== undefined) {
|
|
912
|
+
if (hasNonEmptyCredentialValue(env[localProvider.apiKeyEnv])) {
|
|
913
|
+
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.`);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
status = "waiting";
|
|
917
|
+
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.`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (keyless local provider; no API key declared).`);
|
|
922
|
+
}
|
|
337
923
|
continue;
|
|
338
924
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
details.push(
|
|
925
|
+
const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
|
|
926
|
+
if (apiKeyEnv !== undefined && hasNonEmptyCredentialValue(env[apiKeyEnv])) {
|
|
927
|
+
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
928
|
continue;
|
|
343
929
|
}
|
|
344
|
-
if (
|
|
345
|
-
|
|
930
|
+
if (authInspection?.status === "unsafe") {
|
|
931
|
+
status = "waiting";
|
|
932
|
+
details.push(`[WARN] ${label} ${refStr}: Pi auth store was not trusted because ${describePiAuthStoreUnsafeReason(authInspection.reason)}. ` +
|
|
933
|
+
`Move the unsafe entry aside if needed, then run \`${loginCommand}\` to create or atomically harden a current-user 0600 store. ` +
|
|
934
|
+
"A current-user, non-writable legacy store can be replaced during this explicit repair, but is intentionally never trusted for credential detection. " +
|
|
935
|
+
"No credential values or file contents were displayed.");
|
|
346
936
|
continue;
|
|
347
937
|
}
|
|
348
938
|
const entry = authProviders?.[provider];
|
|
349
939
|
if (entry === undefined) {
|
|
350
940
|
status = "waiting";
|
|
351
|
-
const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
|
|
352
941
|
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}\`
|
|
942
|
+
? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
|
|
943
|
+
: `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const isOAuth = entry.type === "oauth";
|
|
947
|
+
const isApiKey = entry.type === "api_key";
|
|
948
|
+
if (isApiKey && !hasNonEmptyCredentialValue(entry.key)) {
|
|
949
|
+
status = "waiting";
|
|
950
|
+
details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${provider}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (isOAuth && !hasNonEmptyCredentialValue(entry.access) && !hasNonEmptyCredentialValue(entry.refresh)) {
|
|
954
|
+
status = "waiting";
|
|
955
|
+
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.`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (!isOAuth && !isApiKey) {
|
|
959
|
+
status = "waiting";
|
|
960
|
+
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
961
|
continue;
|
|
356
962
|
}
|
|
357
|
-
const isOAuth = entry.type === "oauth" || typeof entry.expires === "number";
|
|
358
963
|
const expired = typeof entry.expires === "number" && entry.expires < now;
|
|
359
964
|
const whenNote = typeof entry.expires === "number" ? ` ${new Date(entry.expires).toISOString()}` : "";
|
|
360
965
|
if (isOAuth && expired) {
|
|
361
966
|
status = "waiting";
|
|
362
|
-
details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${provider}\` expired${whenNote} — the runtime auto-
|
|
967
|
+
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
968
|
continue;
|
|
364
969
|
}
|
|
365
970
|
details.push(isOAuth
|
|
366
|
-
? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}).`
|
|
367
|
-
: `${label} ${refStr}: API key credentials for \`${provider}\` present.`);
|
|
971
|
+
? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
|
|
972
|
+
: `${label} ${refStr}: API key credentials for \`${provider}\` present; credential detected, live model verification is still pending.`);
|
|
368
973
|
}
|
|
369
974
|
return status;
|
|
370
975
|
}
|
|
371
|
-
|
|
976
|
+
function describePiAuthStoreUnsafeReason(reason) {
|
|
977
|
+
switch (reason) {
|
|
978
|
+
case "owner-check-unavailable": return "the current file owner could not be verified";
|
|
979
|
+
case "symbolic-link": return "the configured entry is a symbolic link";
|
|
980
|
+
case "not-regular-file": return "the configured entry is not a regular file";
|
|
981
|
+
case "multiple-hard-links": return "the file has multiple hard links";
|
|
982
|
+
case "oversized": return "the file exceeds the 1 MiB inspection limit";
|
|
983
|
+
case "foreign-owner": return "the file is not owned by the current user";
|
|
984
|
+
case "not-owner-only": return "its permissions are not owner-only";
|
|
985
|
+
case "changed-during-read": return "its identity or metadata changed during inspection";
|
|
986
|
+
case "malformed-json": return "it is not a valid JSON object";
|
|
987
|
+
case "unreadable": return "it could not be opened and inspected safely";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function hasNonEmptyCredentialValue(value) {
|
|
991
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= 65_536 && !value.includes("\0");
|
|
992
|
+
}
|
|
993
|
+
async function contextSection(config, cwd) {
|
|
372
994
|
const details = [];
|
|
373
995
|
let status = "ok";
|
|
374
996
|
if (await pathExists(config.context.identityPath)) {
|
|
@@ -405,10 +1027,23 @@ async function contextSection(config) {
|
|
|
405
1027
|
status = "error";
|
|
406
1028
|
details.push("Skills are selected but context.skillsRoot is not set.");
|
|
407
1029
|
}
|
|
1030
|
+
if (await managedProjectSkillsExist(cwd)) {
|
|
1031
|
+
const managed = await checkManagedProjectSkills(cwd);
|
|
1032
|
+
const drift = managed.statuses.filter((entry) => entry.status !== "ready");
|
|
1033
|
+
if (drift.length === 0) {
|
|
1034
|
+
details.push(`Managed project skills: current (${managed.manifestVersion ?? "unknown version"}).`);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
if (status === "ok")
|
|
1038
|
+
status = "waiting";
|
|
1039
|
+
details.push(`Managed project skill drift: ${drift.map((entry) => `${entry.name}=${entry.status}`).join(", ")}. ` +
|
|
1040
|
+
"Run `mono-agent install-skill --project --check`; use --update only after reconciling modified copies.");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
408
1043
|
return { id: "context", label: "Context & skills", status, details };
|
|
409
1044
|
}
|
|
410
1045
|
const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
|
|
411
|
-
async function memorySection(config, liveness, allowFilesystemWrites) {
|
|
1046
|
+
async function memorySection(config, cwd, env, liveness, allowFilesystemWrites, preferAppPluginInstall) {
|
|
412
1047
|
if (config.memory === undefined) {
|
|
413
1048
|
return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
|
|
414
1049
|
}
|
|
@@ -425,6 +1060,32 @@ async function memorySection(config, liveness, allowFilesystemWrites) {
|
|
|
425
1060
|
details: ["[ERROR] backend 'supermemory' requires a memory.supermemory block."],
|
|
426
1061
|
};
|
|
427
1062
|
}
|
|
1063
|
+
try {
|
|
1064
|
+
const plugin = await loadSupermemoryPlugin({ cwd, preferAppInstall: preferAppPluginInstall });
|
|
1065
|
+
const validation = plugin.validateSupermemoryConfig({
|
|
1066
|
+
baseUrl: sm.baseUrl,
|
|
1067
|
+
container: resolveSupermemoryContainer(config),
|
|
1068
|
+
...(sm.apiKey === undefined ? {} : { apiKey: sm.apiKey }),
|
|
1069
|
+
...(sm.timeoutMs === undefined ? {} : { timeoutMs: sm.timeoutMs }),
|
|
1070
|
+
...(config.memory.maxBytes === undefined ? {} : { maxBytes: config.memory.maxBytes }),
|
|
1071
|
+
});
|
|
1072
|
+
if (!validation.valid) {
|
|
1073
|
+
return {
|
|
1074
|
+
id: "memory",
|
|
1075
|
+
label: "Memory",
|
|
1076
|
+
status: "error",
|
|
1077
|
+
details: validation.errors.map((detail) => `[ERROR] ${detail}`),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
id: "memory",
|
|
1084
|
+
label: "Memory",
|
|
1085
|
+
status: "error",
|
|
1086
|
+
details: [`[ERROR] ${error instanceof Error ? error.message : String(error)}`],
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
428
1089
|
return {
|
|
429
1090
|
id: "memory",
|
|
430
1091
|
label: "Memory",
|
|
@@ -442,39 +1103,191 @@ async function memorySection(config, liveness, allowFilesystemWrites) {
|
|
|
442
1103
|
const details = [
|
|
443
1104
|
`Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
|
|
444
1105
|
];
|
|
1106
|
+
let status = "ok";
|
|
445
1107
|
if (config.memory.llm !== undefined) {
|
|
446
1108
|
details.push(`Chat LLM: ${memoryLlmLabel(config.memory.llm)}.`);
|
|
1109
|
+
if (config.memory.llm.provider === "agent-host") {
|
|
1110
|
+
try {
|
|
1111
|
+
const model = parseMonoRuntimeModelReference(config.memory.llm.model);
|
|
1112
|
+
const resolutionIssue = piModelResolutionIssue(config, model);
|
|
1113
|
+
if (resolutionIssue !== undefined) {
|
|
1114
|
+
status = "error";
|
|
1115
|
+
details.push(`Agent-host memory LLM ${referenceOf(model)}: ${resolutionIssue}.`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
status = "error";
|
|
1120
|
+
details.push(`Agent-host memory LLM ${config.memory.llm.model}: ${error instanceof Error ? error.message : String(error)}.`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
447
1123
|
}
|
|
448
1124
|
if (config.memory.mode === "bujo") {
|
|
449
1125
|
// Report consolidation scheduler cadence.
|
|
450
1126
|
const consolidationEnabled = config.memory.consolidation?.enabled !== false;
|
|
451
1127
|
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
|
-
}
|
|
1128
|
+
if (consolidationEnabled) {
|
|
1129
|
+
details.push(`Consolidation: ${consolidationCron} (auto).`);
|
|
460
1130
|
}
|
|
461
1131
|
else {
|
|
462
|
-
details.push("Consolidation:
|
|
1132
|
+
details.push("Consolidation: disabled.");
|
|
463
1133
|
}
|
|
464
1134
|
}
|
|
1135
|
+
const managedIdentity = await managedMemoryIdentityStatus(config.memory);
|
|
1136
|
+
if (managedIdentity !== undefined) {
|
|
1137
|
+
return {
|
|
1138
|
+
id: "memory",
|
|
1139
|
+
label: "Memory",
|
|
1140
|
+
status: status === "error" ? "error" : managedIdentity.status,
|
|
1141
|
+
details: [...details, ...managedIdentity.details],
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
const nativeAvailability = await builtInMemoryNativeStatus(config.memory);
|
|
1145
|
+
if (nativeAvailability !== undefined) {
|
|
1146
|
+
return {
|
|
1147
|
+
id: "memory",
|
|
1148
|
+
label: "Memory",
|
|
1149
|
+
status: "error",
|
|
1150
|
+
details: [...details, nativeAvailability],
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
465
1153
|
if (config.memory.mode === "journal" || config.memory.mode === "bujo") {
|
|
466
|
-
const warns = await memoryLivenessWarnings(config.memory, liveness, allowFilesystemWrites);
|
|
1154
|
+
const warns = await memoryLivenessWarnings(config.memory, env, liveness, allowFilesystemWrites);
|
|
467
1155
|
if (warns.length > 0) {
|
|
468
|
-
return {
|
|
1156
|
+
return {
|
|
1157
|
+
id: "memory",
|
|
1158
|
+
label: "Memory",
|
|
1159
|
+
status: status === "error" ? "error" : "waiting",
|
|
1160
|
+
details: [...details, ...warns],
|
|
1161
|
+
};
|
|
469
1162
|
}
|
|
470
1163
|
}
|
|
471
1164
|
if (config.memory.mode === "lite") {
|
|
472
1165
|
const liteWarns = await liteRootWritableWarning(config.memory.path, allowFilesystemWrites);
|
|
473
1166
|
if (liteWarns.length > 0) {
|
|
474
|
-
return {
|
|
1167
|
+
return {
|
|
1168
|
+
id: "memory",
|
|
1169
|
+
label: "Memory",
|
|
1170
|
+
status: status === "error" ? "error" : "waiting",
|
|
1171
|
+
details: [...details, ...liteWarns],
|
|
1172
|
+
};
|
|
475
1173
|
}
|
|
476
1174
|
}
|
|
477
|
-
return { id: "memory", label: "Memory", status
|
|
1175
|
+
return { id: "memory", label: "Memory", status, details };
|
|
1176
|
+
}
|
|
1177
|
+
async function managedMemoryIdentityStatus(memory) {
|
|
1178
|
+
if (await pathExists(join(memory.path, FIRST_RUN_MEMORY_INITIALIZING_MARKER))) {
|
|
1179
|
+
return {
|
|
1180
|
+
status: "error",
|
|
1181
|
+
details: [
|
|
1182
|
+
"[ERROR] First-run managed memory initialization is incomplete.",
|
|
1183
|
+
"Re-run `mono-agent init` in a clean target or remove only the failed first-run root after inspecting it.",
|
|
1184
|
+
],
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
const manifestPath = join(memory.path, ".index", "manifest.json");
|
|
1188
|
+
if (!(await pathExists(manifestPath))) {
|
|
1189
|
+
// Lite has no semantic index authority. Journal/BuJo readiness is strict:
|
|
1190
|
+
// a missing manifest is fatal even for a wholly new/unmanaged root, and the
|
|
1191
|
+
// provider must not be probed until rebuild establishes that authority.
|
|
1192
|
+
if (memory.mode === "lite")
|
|
1193
|
+
return undefined;
|
|
1194
|
+
return {
|
|
1195
|
+
status: "error",
|
|
1196
|
+
details: [
|
|
1197
|
+
"[ERROR] Managed memory generation metadata is missing for Journal/BuJo memory.",
|
|
1198
|
+
"Stop the agent with `mono-agent stop`, run `mono-agent memory rebuild`, then re-run `mono-agent validate` before restarting.",
|
|
1199
|
+
],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
let manifest;
|
|
1203
|
+
try {
|
|
1204
|
+
const { readManagedIndexManifest } = await import("@mono-agent/memory/bujo");
|
|
1205
|
+
manifest = readManagedIndexManifest(memory.path);
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return {
|
|
1209
|
+
status: "error",
|
|
1210
|
+
details: ["[ERROR] Managed memory generation metadata is invalid or unavailable."],
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
if (manifest === undefined) {
|
|
1214
|
+
return {
|
|
1215
|
+
status: "error",
|
|
1216
|
+
details: ["[ERROR] Managed memory generation metadata disappeared during validation."],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const configuredModel = memory.embeddings === undefined
|
|
1220
|
+
? undefined
|
|
1221
|
+
: `${memory.embeddings.provider}:${memory.embeddings.model}`;
|
|
1222
|
+
const configuredDimension = memory.embeddings === undefined ? undefined : memory.embeddings.dim ?? 768;
|
|
1223
|
+
const active = manifest.active;
|
|
1224
|
+
if (active.tier === memory.mode
|
|
1225
|
+
&& active.embeddingModel === configuredModel
|
|
1226
|
+
&& active.dimension === configuredDimension) {
|
|
1227
|
+
return undefined;
|
|
1228
|
+
}
|
|
1229
|
+
const identity = (tier, model, dimension) => `tier=${tier}, model=${model ?? "none"}, dim=${dimension ?? "none"}`;
|
|
1230
|
+
return {
|
|
1231
|
+
status: "error",
|
|
1232
|
+
details: [
|
|
1233
|
+
`[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)}.`,
|
|
1234
|
+
"Stop the agent with `mono-agent stop`, run `mono-agent memory rebuild`, then re-run `mono-agent validate` before restarting.",
|
|
1235
|
+
],
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
async function builtInMemoryNativeStatus(memory) {
|
|
1239
|
+
let database;
|
|
1240
|
+
let managedGeneration = false;
|
|
1241
|
+
try {
|
|
1242
|
+
const { openMemoryDb } = await import("@mono-agent/memory/store");
|
|
1243
|
+
const manifestPath = join(memory.path, ".index", "manifest.json");
|
|
1244
|
+
if (await pathExists(manifestPath)) {
|
|
1245
|
+
managedGeneration = true;
|
|
1246
|
+
const { resolveActiveMemoryDbPath } = await import("@mono-agent/memory/bujo");
|
|
1247
|
+
database = openMemoryDb({ path: resolveActiveMemoryDbPath(memory.path), readOnly: true });
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
// Lite roots may not exist yet and validation must remain read-only. An
|
|
1251
|
+
// in-memory open exercises the exact native ABI + extension load without
|
|
1252
|
+
// scanning durable memory or creating the configured root.
|
|
1253
|
+
database = openMemoryDb({ path: ":memory:" });
|
|
1254
|
+
}
|
|
1255
|
+
// A single schema-row lookup proves the opened handle can actually read
|
|
1256
|
+
// SQLite state (constructor-only opens can accept a truncated file). This
|
|
1257
|
+
// remains constant-work and avoids the corpus/queue scans of strict audit.
|
|
1258
|
+
database.indexMetadata();
|
|
1259
|
+
database.close();
|
|
1260
|
+
database = undefined;
|
|
1261
|
+
return undefined;
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
if (isBuiltInMemoryNativeFailure(error)) {
|
|
1265
|
+
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`.";
|
|
1266
|
+
}
|
|
1267
|
+
if (managedGeneration) {
|
|
1268
|
+
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.";
|
|
1269
|
+
}
|
|
1270
|
+
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`.";
|
|
1271
|
+
}
|
|
1272
|
+
finally {
|
|
1273
|
+
try {
|
|
1274
|
+
database?.close();
|
|
1275
|
+
}
|
|
1276
|
+
catch { /* best-effort smoke cleanup */ }
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function isBuiltInMemoryNativeFailure(error) {
|
|
1280
|
+
try {
|
|
1281
|
+
const message = error instanceof Error ? error.message : "";
|
|
1282
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1283
|
+
? String(error.code ?? "")
|
|
1284
|
+
: "";
|
|
1285
|
+
return /(?:err_(?:dlopen|module_not_found)|module_not_found|better[-_ ]?sqlite|sqlite[-_ ]?vec|node_module_version|native module|dlopen|\.node\b)/iu
|
|
1286
|
+
.test(`${code} ${message}`);
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
478
1291
|
}
|
|
479
1292
|
async function liteRootWritableWarning(memoryPath, allowFilesystemWrites) {
|
|
480
1293
|
return await memoryRootWritableWarnings("lite", memoryPath, allowFilesystemWrites);
|
|
@@ -496,20 +1309,34 @@ async function fetchOllamaModels(endpoint) {
|
|
|
496
1309
|
clearTimeout(timer);
|
|
497
1310
|
}
|
|
498
1311
|
}
|
|
499
|
-
async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
|
|
1312
|
+
async function memoryLivenessWarnings(memory, env, liveness, allowFilesystemWrites) {
|
|
500
1313
|
const warns = [];
|
|
501
1314
|
const mode = memory.mode;
|
|
502
|
-
const embeddingsUsesOllama = (memory.embeddings?.provider ?? "ollama") === "ollama";
|
|
503
1315
|
const llmUsesOllama = memory.llm?.provider === "ollama";
|
|
504
1316
|
const ollamaModelsByEndpoint = new Map();
|
|
505
1317
|
// 1. Memory root writable (every embedded tier) — local I/O, always checked.
|
|
506
1318
|
const rootWarns = await memoryRootWritableWarnings(mode, memory.path, allowFilesystemWrites);
|
|
507
1319
|
warns.push(...rootWarns);
|
|
1320
|
+
const embeddings = memory.embeddings;
|
|
1321
|
+
let embeddingApiKey = embeddings?.apiKey;
|
|
1322
|
+
if (embeddings?.apiKeyEnv !== undefined) {
|
|
1323
|
+
embeddingApiKey = env[embeddings.apiKeyEnv]?.trim();
|
|
1324
|
+
if (embeddingApiKey === undefined || embeddingApiKey.length === 0) {
|
|
1325
|
+
warns.push(`[WARN] Embeddings apiKeyEnv ${embeddings.apiKeyEnv} is declared, but the resolved environment has no ` +
|
|
1326
|
+
`non-empty value. Set ${embeddings.apiKeyEnv} before starting managed memory; no keyless probe was attempted.`);
|
|
1327
|
+
embeddingApiKey = undefined;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
508
1330
|
// Network-dependent probes below only ever produce `waiting`, so the start
|
|
509
1331
|
// preflight skips them (liveness=false) without changing the pass/fail verdict.
|
|
510
1332
|
if (!liveness) {
|
|
511
1333
|
return warns;
|
|
512
1334
|
}
|
|
1335
|
+
if (embeddings !== undefined
|
|
1336
|
+
&& (embeddings.provider === "ollama" || embeddings.provider === "lmstudio")
|
|
1337
|
+
&& (embeddings.apiKeyEnv === undefined || embeddingApiKey !== undefined)) {
|
|
1338
|
+
warns.push(...await localEmbeddingLivenessWarnings(embeddings, embeddingApiKey));
|
|
1339
|
+
}
|
|
513
1340
|
async function modelsForOllamaEndpoint(endpoint) {
|
|
514
1341
|
const normalizedEndpoint = endpoint.replace(/\/$/u, "");
|
|
515
1342
|
if (ollamaModelsByEndpoint.has(normalizedEndpoint)) {
|
|
@@ -526,18 +1353,9 @@ async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
|
|
|
526
1353
|
return undefined;
|
|
527
1354
|
}
|
|
528
1355
|
}
|
|
529
|
-
// 2. Ollama liveness
|
|
530
|
-
// agent-host chat LLMs have no
|
|
531
|
-
|
|
532
|
-
const endpoint = memory.embeddings?.endpoint ?? "http://localhost:11434";
|
|
533
|
-
const ollamaModels = await modelsForOllamaEndpoint(endpoint);
|
|
534
|
-
if (ollamaModels !== undefined) {
|
|
535
|
-
const embeddingsModel = memory.embeddings?.model ?? "nomic-embed-text:v1.5";
|
|
536
|
-
if (!ollamaModels.includes(embeddingsModel)) {
|
|
537
|
-
warns.push(`[WARN] Embeddings model ${embeddingsModel} not pulled — run \`ollama pull ${embeddingsModel}\`.`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
1356
|
+
// 2. Chat-LLM Ollama liveness remains independent from the selected
|
|
1357
|
+
// embeddings service. OpenAI embeddings and agent-host chat LLMs have no
|
|
1358
|
+
// local typed model catalog to validate here.
|
|
541
1359
|
if (llmUsesOllama && memory.llm !== undefined) {
|
|
542
1360
|
const endpoint = memory.llm.endpoint ?? "http://localhost:11434";
|
|
543
1361
|
const ollamaModels = await modelsForOllamaEndpoint(endpoint);
|
|
@@ -550,6 +1368,52 @@ async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
|
|
|
550
1368
|
}
|
|
551
1369
|
return warns;
|
|
552
1370
|
}
|
|
1371
|
+
async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
|
|
1372
|
+
if (embeddings.provider !== "ollama" && embeddings.provider !== "lmstudio")
|
|
1373
|
+
return [];
|
|
1374
|
+
const provider = embeddings.provider;
|
|
1375
|
+
const label = provider === "ollama" ? "Ollama" : "LM Studio";
|
|
1376
|
+
const endpoint = (embeddings.endpoint ?? DEFAULT_MEMORY_EMBEDDING_ENDPOINTS[provider]).replace(/\/+$/u, "");
|
|
1377
|
+
// Runtime readiness is authoritative on the exact configured model. Typed
|
|
1378
|
+
// catalogs help the wizard offer choices, but an unavailable/incomplete
|
|
1379
|
+
// catalog must not make its explicit manual fallback permanently unready.
|
|
1380
|
+
try {
|
|
1381
|
+
await probeMemoryEmbeddingSelection({
|
|
1382
|
+
provider,
|
|
1383
|
+
endpoint,
|
|
1384
|
+
model: embeddings.model,
|
|
1385
|
+
expectedDimension: embeddings.dim ?? 768,
|
|
1386
|
+
timeoutMs: BUJO_PROBE_TIMEOUT_MS,
|
|
1387
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
1388
|
+
});
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1393
|
+
if (/HTTP 401/u.test(reason)) {
|
|
1394
|
+
return [
|
|
1395
|
+
`[WARN] ${label} authentication failed at ${endpoint} (HTTP 401). ` +
|
|
1396
|
+
"Set the configured embeddings apiKeyEnv to a valid bearer token and retry.",
|
|
1397
|
+
];
|
|
1398
|
+
}
|
|
1399
|
+
if (/could not connect|timed out/iu.test(reason)) {
|
|
1400
|
+
return [
|
|
1401
|
+
`[WARN] ${label} not reachable at ${endpoint}; embeddings will fail at runtime (${reason}). ` +
|
|
1402
|
+
`Start ${label} or fix the endpoint.`,
|
|
1403
|
+
];
|
|
1404
|
+
}
|
|
1405
|
+
if (provider === "ollama" && /HTTP 404/u.test(reason)) {
|
|
1406
|
+
return [
|
|
1407
|
+
`[WARN] Ollama embedding model ${embeddings.model} could not be proved at ${endpoint} (${reason}); ` +
|
|
1408
|
+
`run \`ollama pull ${embeddings.model}\` and verify its embedding capability.`,
|
|
1409
|
+
];
|
|
1410
|
+
}
|
|
1411
|
+
return [
|
|
1412
|
+
`[WARN] ${label} embedding readiness failed for ${embeddings.model} at ${endpoint} (${reason}). ` +
|
|
1413
|
+
"Verify the selected model, authentication, and configured dimension.",
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
553
1417
|
async function memoryRootWritableWarnings(mode, memoryPath, allowFilesystemWrites) {
|
|
554
1418
|
if (allowFilesystemWrites) {
|
|
555
1419
|
try {
|
|
@@ -652,18 +1516,102 @@ async function toolsSection(config, input) {
|
|
|
652
1516
|
// Under allow-all the disallow list is already folded into the "except" clause above.
|
|
653
1517
|
details.push(`Disallowed tools: ${config.tools.disallowedTools.join(", ")}.`);
|
|
654
1518
|
}
|
|
1519
|
+
const directCodexModels = configuredRuntimeModels(config.runtime)
|
|
1520
|
+
.filter((model) => model.sdk === "codex")
|
|
1521
|
+
.map(referenceOf);
|
|
1522
|
+
const directOpenCodeModels = configuredRuntimeModels(config.runtime)
|
|
1523
|
+
.filter((model) => model.sdk === "opencode")
|
|
1524
|
+
.map(referenceOf);
|
|
1525
|
+
const claudeCliModels = [
|
|
1526
|
+
{ model: config.runtime.model, executionMode: config.runtime.executionMode },
|
|
1527
|
+
...configuredRuntimeFallbackModels(config.runtime).map((model) => ({
|
|
1528
|
+
model,
|
|
1529
|
+
executionMode: defaultExecutionModeForModel(model),
|
|
1530
|
+
})),
|
|
1531
|
+
]
|
|
1532
|
+
.filter(({ model, executionMode }) => model.sdk === "claude" && executionMode === "cli")
|
|
1533
|
+
.map(({ model }) => referenceOf(model));
|
|
1534
|
+
const exactAllowAll = hasExactAllowAllToolPolicy(config.tools);
|
|
1535
|
+
const dedicatedNoToolsProbe = input.codexNoToolsProbe === true
|
|
1536
|
+
&& allowedTools.length === 0
|
|
1537
|
+
&& config.tools.disallowedTools.length === 0;
|
|
1538
|
+
if (directCodexModels.length > 0
|
|
1539
|
+
&& !dedicatedNoToolsProbe
|
|
1540
|
+
&& !exactAllowAll) {
|
|
1541
|
+
status = "error";
|
|
1542
|
+
details.push(`Direct Codex model${directCodexModels.length === 1 ? "" : "s"} ${directCodexModels.join(", ")} cannot enforce ` +
|
|
1543
|
+
"tools.allowedTools/tools.disallowedTools. Use exact allow-all (allowedTools: [\"*\"] with no disallowedTools), " +
|
|
1544
|
+
"or select a runtime that supports restrictive tool policies.");
|
|
1545
|
+
}
|
|
1546
|
+
if (directOpenCodeModels.length > 0 && !exactAllowAll) {
|
|
1547
|
+
status = "error";
|
|
1548
|
+
details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot enforce ` +
|
|
1549
|
+
"tools.allowedTools/tools.disallowedTools. Use exact allow-all (allowedTools: [\"*\"] with no disallowedTools), " +
|
|
1550
|
+
"or use a Pi runtime (including pi:opencode-go:*).");
|
|
1551
|
+
}
|
|
1552
|
+
if (claudeCliModels.length > 0 && allowedTools.length === 0) {
|
|
1553
|
+
status = "error";
|
|
1554
|
+
details.push(`Claude CLI model${claudeCliModels.length === 1 ? "" : "s"} ${claudeCliModels.join(", ")} cannot enforce an empty ` +
|
|
1555
|
+
"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.");
|
|
1556
|
+
}
|
|
1557
|
+
let configuredMcpServerNames = [];
|
|
1558
|
+
let configuredMcpServers = {};
|
|
655
1559
|
if (config.tools.mcpConfigPath !== undefined) {
|
|
656
1560
|
if (await pathExists(config.tools.mcpConfigPath)) {
|
|
657
1561
|
details.push(`MCP config: ${config.tools.mcpConfigPath}`);
|
|
1562
|
+
try {
|
|
1563
|
+
const policy = await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath);
|
|
1564
|
+
configuredMcpServers = policy.mcpServers ?? {};
|
|
1565
|
+
configuredMcpServerNames = Object.keys(configuredMcpServers);
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
status = "error";
|
|
1569
|
+
details.push(`MCP config is malformed or unreadable: ${config.tools.mcpConfigPath}`);
|
|
1570
|
+
}
|
|
658
1571
|
}
|
|
659
1572
|
else {
|
|
660
1573
|
status = "error";
|
|
661
1574
|
details.push(`MCP config file is missing: ${config.tools.mcpConfigPath}`);
|
|
662
1575
|
}
|
|
663
1576
|
}
|
|
1577
|
+
for (const serverName of config.tools.mcpRequestContextServers ?? []) {
|
|
1578
|
+
if (!isValidMcpServerName(serverName)) {
|
|
1579
|
+
status = "error";
|
|
1580
|
+
details.push(`tools.mcpRequestContextServers entry "${serverName}" is not a runtime-valid MCP server name (letters, digits, underscores, and hyphens only).`);
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
const spec = configuredMcpServers[serverName];
|
|
1584
|
+
if (spec === undefined) {
|
|
1585
|
+
status = "error";
|
|
1586
|
+
details.push(`tools.mcpRequestContextServers names unknown MCP server "${serverName}"; declare it in tools.mcpConfigPath.`);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (!isStdioMcpServerSpec(spec)) {
|
|
1590
|
+
status = "error";
|
|
1591
|
+
details.push(`tools.mcpRequestContextServers entry "${serverName}" must reference a stdio MCP server (command/type:stdio), not HTTP/SSE.`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
for (const serverName of config.tools.continuationServers ?? []) {
|
|
1595
|
+
if (!isValidMcpServerName(serverName)) {
|
|
1596
|
+
status = "error";
|
|
1597
|
+
details.push(`tools.continuationServers entry "${serverName}" is not a runtime-valid MCP server name (letters, digits, underscores, and hyphens only).`);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const spec = configuredMcpServers[serverName];
|
|
1601
|
+
if (spec === undefined) {
|
|
1602
|
+
status = "error";
|
|
1603
|
+
details.push(`tools.continuationServers names unknown MCP server "${serverName}"; declare it in tools.mcpConfigPath.`);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (classifyContinuationMcpServerTransport(spec) === "unsupported") {
|
|
1607
|
+
status = "error";
|
|
1608
|
+
details.push(`tools.continuationServers entry "${serverName}" must reference a stdio or loopback HTTP MCP server; remote HTTP and SSE are not supported.`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
664
1611
|
const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
|
|
665
1612
|
allowedTools: config.tools.allowedTools,
|
|
666
1613
|
disallowedTools: config.tools.disallowedTools,
|
|
1614
|
+
suppressInteractionTools: directOpenCodeModels.length > 0,
|
|
667
1615
|
});
|
|
668
1616
|
if (adapterSendTools === undefined) {
|
|
669
1617
|
details.push("No adapter-derived send tools enabled.");
|
|
@@ -671,9 +1619,518 @@ async function toolsSection(config, input) {
|
|
|
671
1619
|
else {
|
|
672
1620
|
details.push(`Adapter send tools: ${adapterSendToolNames(adapterSendTools).join(", ")}.`);
|
|
673
1621
|
}
|
|
1622
|
+
const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools, directOpenCodeModels.length > 0);
|
|
1623
|
+
if (blockedAdapterEndpoints.length > 0) {
|
|
1624
|
+
if (status !== "error") {
|
|
1625
|
+
status = "waiting";
|
|
1626
|
+
}
|
|
1627
|
+
details.push(...blockedAdapterEndpoints);
|
|
1628
|
+
}
|
|
1629
|
+
const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterSendTools === undefined ? [] : adapterSendToolNames(adapterSendTools));
|
|
1630
|
+
if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
|
|
1631
|
+
status = "error";
|
|
1632
|
+
details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot safely consume ` +
|
|
1633
|
+
`MCP runtime options from ${effectiveMcpSources.join("; ")}. Disable those MCP sources or use a Pi runtime (including pi:opencode-go:*).`);
|
|
1634
|
+
}
|
|
674
1635
|
return { id: "tools", label: "Tools & MCP", status, details };
|
|
675
1636
|
}
|
|
1637
|
+
async function continuationSection(config, input) {
|
|
1638
|
+
let settings;
|
|
1639
|
+
try {
|
|
1640
|
+
settings = await loadContinuationSettings({
|
|
1641
|
+
cwd: input.cwd,
|
|
1642
|
+
configPath: input.configPath,
|
|
1643
|
+
env: input.env,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
catch (error) {
|
|
1647
|
+
return {
|
|
1648
|
+
id: "continuations",
|
|
1649
|
+
label: "Durable continuations",
|
|
1650
|
+
status: "error",
|
|
1651
|
+
details: [`Continuation configuration is invalid: ${continuationReason(error)}`],
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const continuationServers = config.tools.continuationServers ?? [];
|
|
1655
|
+
if (!settings.configured && continuationServers.length === 0) {
|
|
1656
|
+
return {
|
|
1657
|
+
id: "continuations",
|
|
1658
|
+
label: "Durable continuations",
|
|
1659
|
+
status: "disabled",
|
|
1660
|
+
details: ["No continuation-capable MCP servers or detached continuation routes are configured."],
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (!settings.enabled) {
|
|
1664
|
+
return {
|
|
1665
|
+
id: "continuations",
|
|
1666
|
+
label: "Durable continuations",
|
|
1667
|
+
status: "error",
|
|
1668
|
+
details: ["Continuation service is disabled while continuation functionality is configured."],
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
const details = [
|
|
1672
|
+
`Loopback service: http://${formatContinuationHost(settings.host)}:${String(settings.port)}.`,
|
|
1673
|
+
`Owner-only state: ${settings.stateDir}.`,
|
|
1674
|
+
continuationServers.length === 0
|
|
1675
|
+
? "Run-scoped continuation MCP servers: none."
|
|
1676
|
+
: `Run-scoped continuation MCP servers: ${continuationServers.join(", ")}.`,
|
|
1677
|
+
];
|
|
1678
|
+
const routeNames = Object.entries(settings.namedRoutes).map(([name, route]) => `${name} (${route.mode})`);
|
|
1679
|
+
details.push(routeNames.length === 0 ? "Named detached routes: none." : `Named detached routes: ${routeNames.join(", ")}.`);
|
|
1680
|
+
const detachedNames = Object.keys(settings.detachedServices);
|
|
1681
|
+
details.push(detachedNames.length === 0 ? "Detached services: none." : `Detached services: ${detachedNames.join(", ")}.`);
|
|
1682
|
+
const state = await inspectContinuationState(settings.stateDir);
|
|
1683
|
+
details.push(...state.details);
|
|
1684
|
+
return {
|
|
1685
|
+
id: "continuations",
|
|
1686
|
+
label: "Durable continuations",
|
|
1687
|
+
status: state.status,
|
|
1688
|
+
details,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
async function inspectContinuationState(stateDir) {
|
|
1692
|
+
const details = [];
|
|
1693
|
+
let directory;
|
|
1694
|
+
try {
|
|
1695
|
+
directory = await lstat(stateDir);
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
if (continuationFsCode(error) === "ENOENT") {
|
|
1699
|
+
return {
|
|
1700
|
+
status: "ok",
|
|
1701
|
+
details: ["State has not been initialized; the app will create it with owner-only permissions on first start."],
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
return { status: "error", details: [`Continuation state cannot be inspected: ${continuationReason(error)}`] };
|
|
1705
|
+
}
|
|
1706
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
1707
|
+
return { status: "error", details: ["Continuation state path must be a real directory, not a file or symlink."] };
|
|
1708
|
+
}
|
|
1709
|
+
const directorySecurityError = continuationOwnershipError(directory, "state directory", 0o700);
|
|
1710
|
+
if (directorySecurityError !== undefined) {
|
|
1711
|
+
return { status: "error", details: [directorySecurityError] };
|
|
1712
|
+
}
|
|
1713
|
+
const secretError = await inspectContinuationSecret(join(stateDir, "continuation-secret"));
|
|
1714
|
+
if (secretError !== undefined) {
|
|
1715
|
+
return { status: "error", details: [secretError] };
|
|
1716
|
+
}
|
|
1717
|
+
const manifestPath = join(stateDir, "continuation-store-v2.json");
|
|
1718
|
+
let manifestInfo;
|
|
1719
|
+
try {
|
|
1720
|
+
manifestInfo = await lstat(manifestPath);
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
if (continuationFsCode(error) !== "ENOENT") {
|
|
1724
|
+
return { status: "error", details: [`Continuation store manifest cannot be inspected: ${continuationReason(error)}`] };
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (manifestInfo !== undefined) {
|
|
1728
|
+
if (!manifestInfo.isFile() || manifestInfo.isSymbolicLink()) {
|
|
1729
|
+
return { status: "error", details: ["Continuation store manifest must be a regular file, not a symlink."] };
|
|
1730
|
+
}
|
|
1731
|
+
const manifestSecurityError = continuationOwnershipError(manifestInfo, "store manifest", 0o600);
|
|
1732
|
+
if (manifestSecurityError !== undefined)
|
|
1733
|
+
return { status: "error", details: [manifestSecurityError] };
|
|
1734
|
+
let manifest;
|
|
1735
|
+
try {
|
|
1736
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1737
|
+
}
|
|
1738
|
+
catch (error) {
|
|
1739
|
+
return { status: "error", details: [`Continuation store manifest contains invalid JSON: ${continuationReason(error)}`] };
|
|
1740
|
+
}
|
|
1741
|
+
if (!isContinuationStoreManifest(manifest)) {
|
|
1742
|
+
return { status: "error", details: ["Continuation store manifest has an unsupported or malformed schema."] };
|
|
1743
|
+
}
|
|
1744
|
+
const recordsDirectoryError = await inspectContinuationRecordsDirectory(join(stateDir, "records-v2"));
|
|
1745
|
+
if (recordsDirectoryError !== undefined)
|
|
1746
|
+
return { status: "error", details: [recordsDirectoryError] };
|
|
1747
|
+
const transaction = await inspectContinuationTransaction(join(stateDir, "continuation-transaction-v2.json"));
|
|
1748
|
+
if (!transaction.valid)
|
|
1749
|
+
return { status: "error", details: [transaction.detail] };
|
|
1750
|
+
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1751
|
+
if (!owner.valid)
|
|
1752
|
+
return { status: "error", details: [owner.detail] };
|
|
1753
|
+
details.push(`Store v2: ${String(manifest.stats.records)} retained; ${String(manifest.stats.active)} active; ${String(manifest.stats.unresolvedDelivery)} delivery unknown; ${String(manifest.stats.deadLettered)} dead-lettered; ${String(manifest.stats.historyDegraded)} history-degraded deliveries; ${String(manifest.stats.terminalTombstones)} terminal tombstones; ${String(manifest.stats.capturedText)} captured answers.`, `Retention: at most ${String(manifest.stats.limits.terminalMaxRecords)} terminal tombstones and ${String(manifest.stats.limits.capturedTextMaxRecords)} captured answers.`, transaction.detail, owner.detail);
|
|
1754
|
+
return {
|
|
1755
|
+
status: transaction.pending
|
|
1756
|
+
|| manifest.stats.unresolvedDelivery > 0
|
|
1757
|
+
|| manifest.stats.deadLettered > 0
|
|
1758
|
+
|| manifest.stats.historyDegraded > 0
|
|
1759
|
+
? "waiting"
|
|
1760
|
+
: "ok",
|
|
1761
|
+
details,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
const storePath = join(stateDir, "continuations-v1.json");
|
|
1765
|
+
let storeInfo;
|
|
1766
|
+
try {
|
|
1767
|
+
storeInfo = await lstat(storePath);
|
|
1768
|
+
}
|
|
1769
|
+
catch (error) {
|
|
1770
|
+
if (continuationFsCode(error) === "ENOENT") {
|
|
1771
|
+
details.push("No continuation ledger has been written yet.");
|
|
1772
|
+
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1773
|
+
details.push(owner.detail);
|
|
1774
|
+
return { status: owner.valid ? "ok" : "error", details };
|
|
1775
|
+
}
|
|
1776
|
+
return { status: "error", details: [`Continuation ledger cannot be inspected: ${continuationReason(error)}`] };
|
|
1777
|
+
}
|
|
1778
|
+
if (!storeInfo.isFile() || storeInfo.isSymbolicLink()) {
|
|
1779
|
+
return { status: "error", details: ["Continuation ledger must be a regular file, not a symlink."] };
|
|
1780
|
+
}
|
|
1781
|
+
const storeSecurityError = continuationOwnershipError(storeInfo, "ledger", 0o600);
|
|
1782
|
+
if (storeSecurityError !== undefined) {
|
|
1783
|
+
return { status: "error", details: [storeSecurityError] };
|
|
1784
|
+
}
|
|
1785
|
+
let parsed;
|
|
1786
|
+
try {
|
|
1787
|
+
parsed = JSON.parse(await readFile(storePath, "utf8"));
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
return { status: "error", details: [`Continuation ledger contains invalid JSON: ${continuationReason(error)}`] };
|
|
1791
|
+
}
|
|
1792
|
+
if (!isContinuationLedger(parsed)) {
|
|
1793
|
+
return { status: "error", details: ["Continuation ledger has an unsupported or malformed schema."] };
|
|
1794
|
+
}
|
|
1795
|
+
const counts = Object.fromEntries(CONTINUATION_STATES.map((state) => [state, 0]));
|
|
1796
|
+
for (const record of Object.values(parsed.records)) {
|
|
1797
|
+
if (!isDoctorObject(record) || typeof record.state !== "string" || !CONTINUATION_STATES.includes(record.state)) {
|
|
1798
|
+
return { status: "error", details: ["Continuation ledger contains a record with an invalid lifecycle state."] };
|
|
1799
|
+
}
|
|
1800
|
+
counts[record.state] += 1;
|
|
1801
|
+
}
|
|
1802
|
+
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
|
|
1803
|
+
const pending = counts.claimed + counts.result_received + counts.synthesizing + counts.ready_to_deliver + counts.delivery_retry;
|
|
1804
|
+
details.push(`Legacy ledger awaiting v2 migration: ${String(total)} total; ${String(pending)} pending; ${String(counts.delivery_unknown)} delivery unknown; ${String(counts.dead_lettered)} dead-lettered.`);
|
|
1805
|
+
const owner = await inspectContinuationOwnerDatabase(join(stateDir, "continuations-owner.sqlite"));
|
|
1806
|
+
details.push(owner.detail);
|
|
1807
|
+
return {
|
|
1808
|
+
status: !owner.valid
|
|
1809
|
+
? "error"
|
|
1810
|
+
: counts.delivery_unknown > 0 || counts.dead_lettered > 0
|
|
1811
|
+
? "waiting"
|
|
1812
|
+
: "ok",
|
|
1813
|
+
details,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
async function inspectContinuationSecret(path) {
|
|
1817
|
+
let info;
|
|
1818
|
+
try {
|
|
1819
|
+
info = await lstat(path);
|
|
1820
|
+
}
|
|
1821
|
+
catch (error) {
|
|
1822
|
+
return continuationFsCode(error) === "ENOENT"
|
|
1823
|
+
? undefined
|
|
1824
|
+
: `Continuation secret cannot be inspected: ${continuationReason(error)}`;
|
|
1825
|
+
}
|
|
1826
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
1827
|
+
return "Continuation secret must be a regular file, not a symlink.";
|
|
1828
|
+
const securityError = continuationOwnershipError(info, "secret", 0o600);
|
|
1829
|
+
if (securityError !== undefined)
|
|
1830
|
+
return securityError;
|
|
1831
|
+
try {
|
|
1832
|
+
const secret = Buffer.from((await readFile(path, "utf8")).trim(), "base64url");
|
|
1833
|
+
if (secret.length !== 32)
|
|
1834
|
+
return "Continuation secret contents are invalid.";
|
|
1835
|
+
}
|
|
1836
|
+
catch (error) {
|
|
1837
|
+
return `Continuation secret cannot be read: ${continuationReason(error)}`;
|
|
1838
|
+
}
|
|
1839
|
+
return undefined;
|
|
1840
|
+
}
|
|
1841
|
+
async function inspectContinuationOwnerDatabase(path) {
|
|
1842
|
+
let info;
|
|
1843
|
+
try {
|
|
1844
|
+
info = await lstat(path);
|
|
1845
|
+
}
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
return continuationFsCode(error) === "ENOENT"
|
|
1848
|
+
? { valid: true, detail: "The OS-backed continuation ownership database has not been initialized yet." }
|
|
1849
|
+
: { valid: false, detail: `Continuation ownership database cannot be inspected: ${continuationReason(error)}` };
|
|
1850
|
+
}
|
|
1851
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1852
|
+
return { valid: false, detail: "Continuation ownership database is not a regular file." };
|
|
1853
|
+
}
|
|
1854
|
+
const securityError = continuationOwnershipError(info, "ownership database", 0o600);
|
|
1855
|
+
if (securityError !== undefined)
|
|
1856
|
+
return { valid: false, detail: securityError };
|
|
1857
|
+
return { valid: true, detail: "OS-backed exclusive ownership is released automatically on clean stop or process death." };
|
|
1858
|
+
}
|
|
1859
|
+
async function inspectContinuationRecordsDirectory(path) {
|
|
1860
|
+
let info;
|
|
1861
|
+
try {
|
|
1862
|
+
info = await lstat(path);
|
|
1863
|
+
}
|
|
1864
|
+
catch (error) {
|
|
1865
|
+
return continuationFsCode(error) === "ENOENT"
|
|
1866
|
+
? "Continuation record directory is missing."
|
|
1867
|
+
: `Continuation record directory cannot be inspected: ${continuationReason(error)}`;
|
|
1868
|
+
}
|
|
1869
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
1870
|
+
return "Continuation record directory must be a real directory, not a file or symlink.";
|
|
1871
|
+
}
|
|
1872
|
+
const directorySecurityError = continuationOwnershipError(info, "record directory", 0o700);
|
|
1873
|
+
if (directorySecurityError !== undefined)
|
|
1874
|
+
return directorySecurityError;
|
|
1875
|
+
try {
|
|
1876
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
1877
|
+
if (!entry.name.endsWith(".json") && !(entry.name.startsWith(".") && entry.name.endsWith(".tmp"))) {
|
|
1878
|
+
return `Continuation record directory contains an unexpected entry: ${entry.name}.`;
|
|
1879
|
+
}
|
|
1880
|
+
const recordInfo = await lstat(join(path, entry.name));
|
|
1881
|
+
if (!recordInfo.isFile() || recordInfo.isSymbolicLink()) {
|
|
1882
|
+
return `Continuation record entry is not a regular file: ${entry.name}.`;
|
|
1883
|
+
}
|
|
1884
|
+
const securityError = continuationOwnershipError(recordInfo, "record", 0o600);
|
|
1885
|
+
if (securityError !== undefined)
|
|
1886
|
+
return securityError;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
return `Continuation record directory cannot be inspected: ${continuationReason(error)}`;
|
|
1891
|
+
}
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
async function inspectContinuationTransaction(path) {
|
|
1895
|
+
let info;
|
|
1896
|
+
try {
|
|
1897
|
+
info = await lstat(path);
|
|
1898
|
+
}
|
|
1899
|
+
catch (error) {
|
|
1900
|
+
return continuationFsCode(error) === "ENOENT"
|
|
1901
|
+
? { valid: true, pending: false, detail: "No interrupted durable transaction is awaiting recovery." }
|
|
1902
|
+
: { valid: false, pending: false, detail: `Continuation transaction cannot be inspected: ${continuationReason(error)}` };
|
|
1903
|
+
}
|
|
1904
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1905
|
+
return { valid: false, pending: true, detail: "Continuation transaction must be a regular file, not a symlink." };
|
|
1906
|
+
}
|
|
1907
|
+
const securityError = continuationOwnershipError(info, "transaction", 0o600);
|
|
1908
|
+
if (securityError !== undefined)
|
|
1909
|
+
return { valid: false, pending: true, detail: securityError };
|
|
1910
|
+
return {
|
|
1911
|
+
valid: true,
|
|
1912
|
+
pending: true,
|
|
1913
|
+
detail: "An interrupted durable transaction is present and will be completed idempotently by the state owner.",
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function continuationOwnershipError(info, label, expectedMode) {
|
|
1917
|
+
if (typeof process.getuid === "function" && Number(info.uid) !== process.getuid()) {
|
|
1918
|
+
return `Continuation ${label} is not owned by the current user.`;
|
|
1919
|
+
}
|
|
1920
|
+
if (process.platform !== "win32" && (Number(info.mode) & 0o777) !== expectedMode) {
|
|
1921
|
+
return `Continuation ${label} permissions must be ${expectedMode.toString(8)}.`;
|
|
1922
|
+
}
|
|
1923
|
+
return undefined;
|
|
1924
|
+
}
|
|
1925
|
+
function isContinuationLedger(value) {
|
|
1926
|
+
return isDoctorObject(value)
|
|
1927
|
+
&& value.schemaVersion === CONTINUATION_STORE_SCHEMA_VERSION
|
|
1928
|
+
&& isDoctorObject(value.records);
|
|
1929
|
+
}
|
|
1930
|
+
function isContinuationStoreManifest(value) {
|
|
1931
|
+
if (!isDoctorObject(value)
|
|
1932
|
+
|| value.schemaVersion !== CONTINUATION_RECORD_STORE_SCHEMA_VERSION
|
|
1933
|
+
|| typeof value.generation !== "string"
|
|
1934
|
+
|| !isDoctorObject(value.stats)
|
|
1935
|
+
|| value.stats.format !== "per-record-v2"
|
|
1936
|
+
|| !isDoctorObject(value.stats.limits))
|
|
1937
|
+
return false;
|
|
1938
|
+
return [
|
|
1939
|
+
value.stats.records,
|
|
1940
|
+
value.stats.active,
|
|
1941
|
+
value.stats.unresolvedDelivery,
|
|
1942
|
+
value.stats.deadLettered,
|
|
1943
|
+
value.stats.historyDegraded,
|
|
1944
|
+
value.stats.terminalTombstones,
|
|
1945
|
+
value.stats.compacted,
|
|
1946
|
+
value.stats.capturedText,
|
|
1947
|
+
value.stats.limits.terminalMaxRecords,
|
|
1948
|
+
value.stats.limits.terminalMaxAgeMs,
|
|
1949
|
+
value.stats.limits.capturedTextMaxRecords,
|
|
1950
|
+
value.stats.limits.capturedTextMaxAgeMs,
|
|
1951
|
+
].every((entry) => Number.isSafeInteger(entry) && Number(entry) >= 0);
|
|
1952
|
+
}
|
|
1953
|
+
function isDoctorObject(value) {
|
|
1954
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1955
|
+
}
|
|
1956
|
+
function continuationFsCode(error) {
|
|
1957
|
+
return isDoctorObject(error) && typeof error.code === "string" ? error.code : undefined;
|
|
1958
|
+
}
|
|
1959
|
+
function continuationReason(error) {
|
|
1960
|
+
return error instanceof Error ? error.message : String(error);
|
|
1961
|
+
}
|
|
1962
|
+
function formatContinuationHost(host) {
|
|
1963
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Adapter-send tools run in a stdio MCP child governed by the native SRT
|
|
1967
|
+
* policy. Channel readiness alone therefore is not enough: the child also
|
|
1968
|
+
* needs the remote adapter API (or the app-owned interaction bridge) admitted
|
|
1969
|
+
* by sandbox.network.
|
|
1970
|
+
*/
|
|
1971
|
+
async function adapterSendToolNetworkPolicyWarnings(config, input, settings, suppressInteractionTools) {
|
|
1972
|
+
if (config.sandbox === undefined || config.sandbox.mode !== "native") {
|
|
1973
|
+
return [];
|
|
1974
|
+
}
|
|
1975
|
+
const toolNames = settings === undefined ? [] : adapterSendToolNames(settings);
|
|
1976
|
+
const requirements = [];
|
|
1977
|
+
if (settings?.slack !== undefined) {
|
|
1978
|
+
requirements.push({
|
|
1979
|
+
label: "Slack adapter-send API",
|
|
1980
|
+
tools: ["SlackSendMessage"],
|
|
1981
|
+
url: "https://slack.com/api",
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (settings?.telegram !== undefined) {
|
|
1985
|
+
requirements.push({
|
|
1986
|
+
label: "Telegram adapter-send API",
|
|
1987
|
+
tools: toolNames.filter((name) => name.startsWith("Telegram")),
|
|
1988
|
+
url: settings.telegram.apiRoot ?? "https://api.telegram.org",
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
const askUserAllowed = !suppressInteractionTools && isAdapterSendToolAllowed("AskUser", {
|
|
1992
|
+
allowedTools: config.tools.allowedTools,
|
|
1993
|
+
disallowedTools: config.tools.disallowedTools,
|
|
1994
|
+
});
|
|
1995
|
+
const bridgeTools = [
|
|
1996
|
+
...(askUserAllowed ? ["AskUser"] : []),
|
|
1997
|
+
...(toolNames.includes("TelegramAskButtons") ? ["TelegramAskButtons"] : []),
|
|
1998
|
+
];
|
|
1999
|
+
if (bridgeTools.length > 0) {
|
|
2000
|
+
const configuredBridgeUrl = input.env.MONO_AGENT_INTERACTION_BRIDGE_URL?.trim();
|
|
2001
|
+
const interaction = await loadInteractionSettings(input);
|
|
2002
|
+
const bridgeUrl = configuredBridgeUrl === undefined || configuredBridgeUrl.length === 0
|
|
2003
|
+
? formatInteractionBridgeUrl(interaction.host, interaction.port)
|
|
2004
|
+
: configuredBridgeUrl;
|
|
2005
|
+
requirements.push({ label: "AskUser interaction bridge", tools: bridgeTools, url: bridgeUrl });
|
|
2006
|
+
}
|
|
2007
|
+
return requirements.flatMap((requirement) => {
|
|
2008
|
+
if (adapterSendChildNetworkAllowsUrl(config, requirement.url)) {
|
|
2009
|
+
return [];
|
|
2010
|
+
}
|
|
2011
|
+
const host = endpointHost(requirement.url);
|
|
2012
|
+
const allowlistHost = host === "::1" ? "localhost" : host;
|
|
2013
|
+
return [
|
|
2014
|
+
`Native sandbox network policy blocks ${requirement.label} host "${host}", required by ${requirement.tools.join(", ")}. ` +
|
|
2015
|
+
`Set sandbox.network.mode to "allowlist" and add "${allowlistHost}" to sandbox.network.allowlist, ` +
|
|
2016
|
+
`or disable ${requirement.tools.join(", ")}.`,
|
|
2017
|
+
];
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
function adapterSendChildNetworkAllowsUrl(config, url) {
|
|
2021
|
+
if (networkPolicyAllowsUrl(config.sandbox, url))
|
|
2022
|
+
return true;
|
|
2023
|
+
if (config.sandbox?.mode !== "native" || config.sandbox.network.mode !== "allowlist")
|
|
2024
|
+
return false;
|
|
2025
|
+
const host = endpointHost(url);
|
|
2026
|
+
return isLoopbackEndpointHost(host) && config.sandbox.network.allowlist.some(isLoopbackEndpointHost);
|
|
2027
|
+
}
|
|
2028
|
+
function isLoopbackEndpointHost(host) {
|
|
2029
|
+
return host === "localhost" || host === "::1" || (isIP(host) === 4 && host.split(".")[0] === "127");
|
|
2030
|
+
}
|
|
2031
|
+
function endpointHost(url) {
|
|
2032
|
+
try {
|
|
2033
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
2034
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
2035
|
+
}
|
|
2036
|
+
catch {
|
|
2037
|
+
return url;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames) {
|
|
2041
|
+
const sources = [];
|
|
2042
|
+
if (configuredMcpServerNames.length > 0) {
|
|
2043
|
+
sources.push(`tools.mcpConfigPath (${configuredMcpServerNames.join(", ")})`);
|
|
2044
|
+
}
|
|
2045
|
+
if (config.memory?.recallTool?.enabled === true) {
|
|
2046
|
+
sources.push("memory.recallTool");
|
|
2047
|
+
}
|
|
2048
|
+
if (config.memory?.backend === "supermemory"
|
|
2049
|
+
&& config.memory.supermemory?.exposeMcpServer === true
|
|
2050
|
+
&& config.memory.supermemory.apiKey !== undefined) {
|
|
2051
|
+
sources.push("memory.supermemory.exposeMcpServer");
|
|
2052
|
+
}
|
|
2053
|
+
if (adapterToolNames.length > 0) {
|
|
2054
|
+
sources.push(`adapter send tools (${adapterToolNames.join(", ")})`);
|
|
2055
|
+
}
|
|
2056
|
+
return sources;
|
|
2057
|
+
}
|
|
676
2058
|
async function sandboxSection(config, engine) {
|
|
2059
|
+
const runtimeModels = configuredRuntimeModels(config.runtime);
|
|
2060
|
+
const directCodexRefs = runtimeModels
|
|
2061
|
+
.filter((model) => model.sdk === "codex")
|
|
2062
|
+
.map((model) => model.reference ?? `codex:${model.model}`);
|
|
2063
|
+
const claudeRefs = runtimeModels
|
|
2064
|
+
.filter((model) => model.sdk === "claude")
|
|
2065
|
+
.map((model) => model.reference ?? `claude:${model.model}`);
|
|
2066
|
+
const directOpenCodeRefs = runtimeModels
|
|
2067
|
+
.filter((model) => model.sdk === "opencode")
|
|
2068
|
+
.map((model) => model.reference ?? `opencode:${model.provider ?? "unknown"}:${model.model}`);
|
|
2069
|
+
const routeSafety = config.runtime.routeSafety ?? "uniform";
|
|
2070
|
+
if (routeSafety === "per-route-native") {
|
|
2071
|
+
const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
|
|
2072
|
+
const piRoutes = runtimeModels.filter((model) => model.sdk === "pi");
|
|
2073
|
+
const details = runtimeModels.map((model, index) => routeNativeSafetyDetail(model, index, config));
|
|
2074
|
+
let status = !monoSandboxActive && piRoutes.length > 0 ? "disabled" : "ok";
|
|
2075
|
+
if (monoSandboxActive && piRoutes.length > 0) {
|
|
2076
|
+
const state = await resolveSandboxEffectiveState({
|
|
2077
|
+
policy: config.sandbox,
|
|
2078
|
+
...(engine === undefined ? {} : { engine }),
|
|
2079
|
+
});
|
|
2080
|
+
const warning = sandboxEffectiveStateWarning(state);
|
|
2081
|
+
details.push(`Pi route SRT policy: mode ${config.sandbox.mode}, network ${config.sandbox.network.mode}, fallback ${config.sandbox.fallback}.`, describeSandboxEffectiveState(state), ...(warning === undefined ? [] : [warning]));
|
|
2082
|
+
if (warning !== undefined || state.effective === "blocked")
|
|
2083
|
+
status = "waiting";
|
|
2084
|
+
}
|
|
2085
|
+
else if (piRoutes.length > 0) {
|
|
2086
|
+
details.push("Pi route SRT policy: disabled; Bash and stdio MCP subprocesses run unsandboxed.");
|
|
2087
|
+
}
|
|
2088
|
+
else if (monoSandboxActive) {
|
|
2089
|
+
details.push("The configured mono-agent SRT policy has no Pi route to enforce it; provider-owned route contracts below apply instead.");
|
|
2090
|
+
status = "waiting";
|
|
2091
|
+
}
|
|
2092
|
+
if (monoSandboxActive && runtimeModels.some((model) => model.sdk !== "pi")) {
|
|
2093
|
+
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.");
|
|
2094
|
+
status = "waiting";
|
|
2095
|
+
}
|
|
2096
|
+
if (runtimeModels.some((model) => model.sdk === "codex") && config.runtime.permissionMode === "bypassPermissions") {
|
|
2097
|
+
status = "waiting";
|
|
2098
|
+
}
|
|
2099
|
+
return { id: "sandbox", label: "Sandbox", status, details };
|
|
2100
|
+
}
|
|
2101
|
+
const incompatibleDetails = [];
|
|
2102
|
+
if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directCodexRefs.length > 0) {
|
|
2103
|
+
const codexPosture = directCodexSandboxPosture(config.runtime.permissionMode);
|
|
2104
|
+
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.`);
|
|
2105
|
+
}
|
|
2106
|
+
if (config.sandbox !== undefined && config.sandbox.mode !== "off" && claudeRefs.length > 0) {
|
|
2107
|
+
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.");
|
|
2108
|
+
}
|
|
2109
|
+
if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directOpenCodeRefs.length > 0) {
|
|
2110
|
+
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.");
|
|
2111
|
+
}
|
|
2112
|
+
if (incompatibleDetails.length > 0) {
|
|
2113
|
+
return {
|
|
2114
|
+
id: "sandbox",
|
|
2115
|
+
label: "Sandbox",
|
|
2116
|
+
status: "error",
|
|
2117
|
+
details: incompatibleDetails,
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
if (directCodexRefs.length > 0 && (config.sandbox === undefined || config.sandbox.mode === "off")) {
|
|
2121
|
+
const posture = directCodexSandboxPosture(config.runtime.permissionMode);
|
|
2122
|
+
return {
|
|
2123
|
+
id: "sandbox",
|
|
2124
|
+
label: "Sandbox",
|
|
2125
|
+
status: posture.status,
|
|
2126
|
+
details: [
|
|
2127
|
+
posture.detail,
|
|
2128
|
+
config.sandbox === undefined
|
|
2129
|
+
? "No mono-agent native srt policy is configured."
|
|
2130
|
+
: "The mono-agent native srt policy is explicitly off; the Codex-native posture still applies.",
|
|
2131
|
+
],
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
677
2134
|
if (config.sandbox === undefined) {
|
|
678
2135
|
return { id: "sandbox", label: "Sandbox", status: "disabled", details: ["No sandbox policy configured."] };
|
|
679
2136
|
}
|
|
@@ -701,6 +2158,44 @@ async function sandboxSection(config, engine) {
|
|
|
701
2158
|
details,
|
|
702
2159
|
};
|
|
703
2160
|
}
|
|
2161
|
+
function routeNativeSafetyDetail(model, index, config) {
|
|
2162
|
+
const label = index === 0 ? "Primary" : `Fallback ${index}`;
|
|
2163
|
+
const ref = referenceOf(model);
|
|
2164
|
+
if (model.sdk === "pi") {
|
|
2165
|
+
if (config.sandbox === undefined || config.sandbox.mode === "off") {
|
|
2166
|
+
return `${label} ${ref}: Pi-owned tools use mono-agent tool policy; SRT is disabled, so Bash and stdio MCP subprocesses run unsandboxed.`;
|
|
2167
|
+
}
|
|
2168
|
+
if (config.sandbox.fallback === "unsafe-host-process") {
|
|
2169
|
+
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.`;
|
|
2170
|
+
}
|
|
2171
|
+
return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy and fail closed when it is unavailable.`;
|
|
2172
|
+
}
|
|
2173
|
+
if (model.sdk === "claude") {
|
|
2174
|
+
return `${label} ${ref}: Claude provider-owned permissions apply; mono-agent SRT filesystem/network rules are not projected.`;
|
|
2175
|
+
}
|
|
2176
|
+
if (model.sdk === "codex") {
|
|
2177
|
+
return `${label} ${ref}: ${directCodexSandboxPosture(config.runtime.permissionMode).detail}`;
|
|
2178
|
+
}
|
|
2179
|
+
return `${label} ${ref}: OpenCode provider-owned execution with exact allow-all tool policy applies; mono-agent SRT rules are not projected.`;
|
|
2180
|
+
}
|
|
2181
|
+
function directCodexSandboxPosture(permissionMode) {
|
|
2182
|
+
if (permissionMode === "bypassPermissions") {
|
|
2183
|
+
return {
|
|
2184
|
+
status: "waiting",
|
|
2185
|
+
detail: "[WARN] Direct Codex bypassPermissions uses native danger-full-access with no filesystem or network sandbox; unattended approval prompts are still disabled.",
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
if (permissionMode === "plan") {
|
|
2189
|
+
return {
|
|
2190
|
+
status: "ok",
|
|
2191
|
+
detail: "Direct Codex plan mode uses its native read-only sandbox with network disabled; unattended escalation requests are denied.",
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
return {
|
|
2195
|
+
status: "ok",
|
|
2196
|
+
detail: "Direct Codex default/acceptEdits mode uses its native workspace-write sandbox with network disabled; unattended escalation requests are denied.",
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
704
2199
|
async function runsSection(input, config) {
|
|
705
2200
|
const artifactDir = await resolveAppArtifactDir(input);
|
|
706
2201
|
const { totalRuns, runs, warnings } = await listRecordedRuns({ artifactDir, maxRuns: RUNS_HEALTH_MAX_RUNS, scope: "agent" });
|