@mono-agent/agent-app 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -19
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +33 -6
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +485 -87
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +528 -135
- package/dist/app.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +16 -6
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +76 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2102 -102
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +195 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +26 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +139 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +12 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +150 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor.d.ts +27 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1197 -101
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +521 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +757 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +88 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1212 -23
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +25 -1
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +281 -19
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/local-configuration.d.ts +56 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +914 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall.d.ts +38 -10
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +82 -25
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +54 -20
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +117 -2
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1505 -49
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +69 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +856 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +54 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +136 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +15 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +46 -6
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +145 -16
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +33 -2
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +826 -108
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +56 -9
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +172 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +59 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1133 -191
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +381 -17
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { stat } from "node:fs/promises";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
|
-
import { basename, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
7
|
+
import { emitKeypressEvents } from "node:readline";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
|
|
9
|
+
import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, MAX_AGENT_NAME_LENGTH, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
|
|
8
10
|
import { listRecordedRuns } from "@mono-agent/observability";
|
|
9
|
-
import { describeSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
|
|
11
|
+
import { describeSandboxEffectiveState, parseMonoRuntimeModelReference, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
|
|
10
12
|
import { startMonoAgentApp } from "./app.js";
|
|
11
13
|
import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, } from "./app-config.js";
|
|
12
14
|
import { runAuditRuns } from "./audit-runs.js";
|
|
@@ -17,16 +19,21 @@ import { formatChannelFactValue } from "./channel-fact-format.js";
|
|
|
17
19
|
import { resolveChannelDrivers } from "./channels.js";
|
|
18
20
|
import { findUnknownAppConfigWarnings } from "./config-reference.js";
|
|
19
21
|
import { validateMonoAgentFolder } from "./doctor.js";
|
|
20
|
-
import {
|
|
22
|
+
import { effectiveFirstRunEnvironment, evaluateFirstRunConfigurationReadiness, evaluateFirstRunReadiness, hasSensitivePersistedEnvironmentValue, piAuthPathBackgroundConflict, readCliConfigSnapshot, readCliDotenvFile, readCliDotenvSnapshot, resolveEffectivePiAuthPath, selectedSecretEnvironmentConflicts, selectedSecretValues, unexpectedPersistedMonoAgentOverrides, validateWizardPlanInStaging, withExactProcessEnvironment, } from "./first-run-readiness.js";
|
|
23
|
+
import { initMonoAgentFolder, SecretEnvConcurrentModificationError, verifySecretEnvPersistenceGuard, } from "./init.js";
|
|
21
24
|
import { installComposerSkill } from "./install-skill.js";
|
|
25
|
+
import { checkManagedProjectSkills, updateManagedProjectSkills } from "./project-skills.js";
|
|
22
26
|
import { runMetrics } from "./metrics.js";
|
|
23
27
|
import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
|
|
24
28
|
import { purgeSessions } from "./sessions.js";
|
|
25
|
-
import { composeWizardPlan } from "./wizard/answers.js";
|
|
29
|
+
import { composeWizardPlan, referencedSetupModelRefs } from "./wizard/answers.js";
|
|
26
30
|
import { answersFromCli, isWithChannel } from "./wizard/from-flags.js";
|
|
27
31
|
import { findPreset, PRESET_CATALOG, presetAnswers, presetIds, RECIPE_TO_PRESET } from "./wizard/presets.js";
|
|
28
|
-
import { runInitWizard } from "./wizard/run.js";
|
|
29
|
-
import { executeProviderSetupPlan, planProviderSetup, providerSetupActionCommandLine } from "./provider-setup.js";
|
|
32
|
+
import { runInitWizard, runSetupRepairWizard } from "./wizard/run.js";
|
|
33
|
+
import { detectProviderCredentialStates, executeProviderSetupPlan, isProviderSetupPiApiKeyAction, planProviderSetup, providerSetupActionCommandLine, } from "./provider-setup.js";
|
|
34
|
+
import { readinessProbeTimeoutMs, runAllRouteReadinessProbe } from "./readiness-probe.js";
|
|
35
|
+
import { checkSandboxRuntime, sandboxRuntimeStatus, setupManagedSrt, } from "./sandbox-manager.js";
|
|
36
|
+
import * as p from "@clack/prompts";
|
|
30
37
|
import * as ui from "./ui.js";
|
|
31
38
|
const DEFAULT_LOG_LINES = 200;
|
|
32
39
|
// Node's maximum setInterval/setTimeout delay (2^31 - 1 ms, ~24.8 days). A
|
|
@@ -34,7 +41,7 @@ const DEFAULT_LOG_LINES = 200;
|
|
|
34
41
|
// busy-waiting; larger values silently overflow to a 1ms delay.
|
|
35
42
|
const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
|
|
36
43
|
const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
|
|
37
|
-
const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
|
|
44
|
+
const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "auth", "sandbox", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
|
|
38
45
|
export function parseCliArgs(argv) {
|
|
39
46
|
const [command, ...rest] = argv;
|
|
40
47
|
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -58,8 +65,12 @@ export function parseCliArgs(argv) {
|
|
|
58
65
|
: command);
|
|
59
66
|
const isLogs = cmd === "logs";
|
|
60
67
|
let configPath;
|
|
68
|
+
let name;
|
|
61
69
|
let model;
|
|
62
70
|
let fallbackModels;
|
|
71
|
+
const fallbacks = [];
|
|
72
|
+
let canAssignFallbackEffort = false;
|
|
73
|
+
let routeSafety;
|
|
63
74
|
let effort;
|
|
64
75
|
let memory;
|
|
65
76
|
let preset;
|
|
@@ -67,6 +78,9 @@ export function parseCliArgs(argv) {
|
|
|
67
78
|
let withChannels;
|
|
68
79
|
let yes = false;
|
|
69
80
|
let auth = false;
|
|
81
|
+
let piAuthPath;
|
|
82
|
+
let apiKeyStdin = false;
|
|
83
|
+
let codexAuthMode;
|
|
70
84
|
const positionals = [];
|
|
71
85
|
let envFile;
|
|
72
86
|
let target;
|
|
@@ -85,16 +99,25 @@ export function parseCliArgs(argv) {
|
|
|
85
99
|
let consumerPath;
|
|
86
100
|
let agent;
|
|
87
101
|
let conversation;
|
|
102
|
+
let local = false;
|
|
103
|
+
let configure = false;
|
|
104
|
+
let project = false;
|
|
105
|
+
let check = false;
|
|
106
|
+
let update = false;
|
|
88
107
|
let staleAfterMs;
|
|
89
108
|
let json = false;
|
|
109
|
+
let strict = false;
|
|
90
110
|
let limit;
|
|
91
111
|
let host;
|
|
92
112
|
let port;
|
|
93
113
|
let open;
|
|
94
114
|
let allowNonLoopback;
|
|
115
|
+
let showAuthUrl;
|
|
95
116
|
let maxRunsPerInstance;
|
|
96
117
|
for (let i = 0; i < rest.length; i += 1) {
|
|
97
118
|
const flag = rest[i];
|
|
119
|
+
if (flag !== "--fallback-effort")
|
|
120
|
+
canAssignFallbackEffort = false;
|
|
98
121
|
switch (flag) {
|
|
99
122
|
case "--config":
|
|
100
123
|
configPath = requireValue(rest, ++i, flag);
|
|
@@ -140,6 +163,21 @@ export function parseCliArgs(argv) {
|
|
|
140
163
|
case "--conversation":
|
|
141
164
|
conversation = requireValue(rest, ++i, flag);
|
|
142
165
|
break;
|
|
166
|
+
case "--local":
|
|
167
|
+
local = true;
|
|
168
|
+
break;
|
|
169
|
+
case "--configure":
|
|
170
|
+
configure = true;
|
|
171
|
+
break;
|
|
172
|
+
case "--project":
|
|
173
|
+
project = true;
|
|
174
|
+
break;
|
|
175
|
+
case "--check":
|
|
176
|
+
check = true;
|
|
177
|
+
break;
|
|
178
|
+
case "--update":
|
|
179
|
+
update = true;
|
|
180
|
+
break;
|
|
143
181
|
case "--stale-after-ms": {
|
|
144
182
|
const raw = requireValue(rest, ++i, flag);
|
|
145
183
|
const parsed = Number(raw);
|
|
@@ -152,6 +190,9 @@ export function parseCliArgs(argv) {
|
|
|
152
190
|
case "--json":
|
|
153
191
|
json = true;
|
|
154
192
|
break;
|
|
193
|
+
case "--strict":
|
|
194
|
+
strict = true;
|
|
195
|
+
break;
|
|
155
196
|
case "--limit": {
|
|
156
197
|
const raw = requireValue(rest, ++i, flag);
|
|
157
198
|
const parsed = Number(raw);
|
|
@@ -179,6 +220,9 @@ export function parseCliArgs(argv) {
|
|
|
179
220
|
case "--allow-non-loopback":
|
|
180
221
|
allowNonLoopback = true;
|
|
181
222
|
break;
|
|
223
|
+
case "--show-auth-url":
|
|
224
|
+
showAuthUrl = true;
|
|
225
|
+
break;
|
|
182
226
|
case "--max-runs": {
|
|
183
227
|
const raw = requireValue(rest, ++i, flag);
|
|
184
228
|
const parsed = Number(raw);
|
|
@@ -191,12 +235,55 @@ export function parseCliArgs(argv) {
|
|
|
191
235
|
case "--model":
|
|
192
236
|
model = requireValue(rest, ++i, flag);
|
|
193
237
|
break;
|
|
238
|
+
case "--name":
|
|
239
|
+
name = requireValue(rest, ++i, flag).trim();
|
|
240
|
+
if (Array.from(name).length === 0
|
|
241
|
+
|| Array.from(name).length > MAX_AGENT_NAME_LENGTH
|
|
242
|
+
|| /[\u0000-\u001f\u007f]/u.test(name)) {
|
|
243
|
+
throw new Error(`--name must be 1-${MAX_AGENT_NAME_LENGTH} characters on one line.`);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
194
246
|
case "--fallback-models":
|
|
195
247
|
fallbackModels = requireValue(rest, ++i, flag)
|
|
196
248
|
.split(",")
|
|
197
249
|
.map((entry) => entry.trim())
|
|
198
250
|
.filter((entry) => entry.length > 0);
|
|
199
251
|
break;
|
|
252
|
+
case "--fallback": {
|
|
253
|
+
const fallbackModel = requireValue(rest, ++i, flag).trim();
|
|
254
|
+
if (fallbacks.some((entry) => entry.model === fallbackModel)) {
|
|
255
|
+
throw new Error(`Duplicate --fallback model \`${fallbackModel}\`.`);
|
|
256
|
+
}
|
|
257
|
+
fallbacks.push({ model: fallbackModel });
|
|
258
|
+
canAssignFallbackEffort = true;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case "--fallback-effort": {
|
|
262
|
+
if (!canAssignFallbackEffort || fallbacks.length === 0) {
|
|
263
|
+
throw new Error("--fallback-effort must immediately follow the --fallback it configures.");
|
|
264
|
+
}
|
|
265
|
+
const raw = requireValue(rest, ++i, flag);
|
|
266
|
+
if (raw !== "provider-default" && !EFFORT_LEVELS.includes(raw)) {
|
|
267
|
+
throw new Error(`--fallback-effort must be provider-default or ${EFFORT_LEVELS.join(", ")}.`);
|
|
268
|
+
}
|
|
269
|
+
if (raw !== "provider-default") {
|
|
270
|
+
const current = fallbacks[fallbacks.length - 1];
|
|
271
|
+
fallbacks[fallbacks.length - 1] = {
|
|
272
|
+
...current,
|
|
273
|
+
effort: raw,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
canAssignFallbackEffort = false;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "--route-safety": {
|
|
280
|
+
const raw = requireValue(rest, ++i, flag);
|
|
281
|
+
if (raw !== "uniform" && raw !== "per-route-native") {
|
|
282
|
+
throw new Error("--route-safety must be uniform or per-route-native.");
|
|
283
|
+
}
|
|
284
|
+
routeSafety = raw;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
200
287
|
case "--effort": {
|
|
201
288
|
const raw = requireValue(rest, ++i, flag);
|
|
202
289
|
if (!EFFORT_LEVELS.includes(raw)) {
|
|
@@ -225,6 +312,20 @@ export function parseCliArgs(argv) {
|
|
|
225
312
|
case "--auth":
|
|
226
313
|
auth = true;
|
|
227
314
|
break;
|
|
315
|
+
case "--pi-auth-path":
|
|
316
|
+
piAuthPath = requireValue(rest, ++i, flag);
|
|
317
|
+
break;
|
|
318
|
+
case "--api-key-stdin":
|
|
319
|
+
apiKeyStdin = true;
|
|
320
|
+
break;
|
|
321
|
+
case "--codex-auth": {
|
|
322
|
+
const raw = requireValue(rest, ++i, flag);
|
|
323
|
+
if (raw !== "browser" && raw !== "device") {
|
|
324
|
+
throw new Error("--codex-auth must be browser or device.");
|
|
325
|
+
}
|
|
326
|
+
codexAuthMode = raw;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
228
329
|
case "--with":
|
|
229
330
|
withChannels = requireValue(rest, ++i, flag)
|
|
230
331
|
.split(",")
|
|
@@ -284,9 +385,29 @@ export function parseCliArgs(argv) {
|
|
|
284
385
|
if (consumerPath !== undefined && cmd !== "validate" && cmd !== "audit-runs") {
|
|
285
386
|
throw new Error("--consumer is only supported for `mono-agent validate` and `mono-agent audit-runs`.");
|
|
286
387
|
}
|
|
287
|
-
if ((
|
|
388
|
+
if ((local || configure) && cmd !== "tui") {
|
|
389
|
+
throw new Error("--local and --configure are only supported for `mono-agent tui`.");
|
|
390
|
+
}
|
|
391
|
+
if (configure && !local) {
|
|
392
|
+
throw new Error("--configure requires `mono-agent tui --local`.");
|
|
393
|
+
}
|
|
394
|
+
if ((project || check || update) && cmd !== "install-skill") {
|
|
395
|
+
throw new Error("--project, --check, and --update are only supported for `mono-agent install-skill`.");
|
|
396
|
+
}
|
|
397
|
+
if ((check || update) && !project) {
|
|
398
|
+
throw new Error("--check and --update require `mono-agent install-skill --project`.");
|
|
399
|
+
}
|
|
400
|
+
if (check && update) {
|
|
401
|
+
throw new Error("Choose either --check or --update for project skills.");
|
|
402
|
+
}
|
|
403
|
+
if ((host !== undefined
|
|
404
|
+
|| port !== undefined
|
|
405
|
+
|| open !== undefined
|
|
406
|
+
|| allowNonLoopback !== undefined
|
|
407
|
+
|| showAuthUrl !== undefined
|
|
408
|
+
|| maxRunsPerInstance !== undefined) &&
|
|
288
409
|
cmd !== "web") {
|
|
289
|
-
throw new Error("--host, --port, --no-open, --allow-non-loopback, and --max-runs are only supported for `mono-agent web`.");
|
|
410
|
+
throw new Error("--host, --port, --no-open, --allow-non-loopback, --show-auth-url, and --max-runs are only supported for `mono-agent web`.");
|
|
290
411
|
}
|
|
291
412
|
if (includeMemory && cmd !== "audit-runs" && cmd !== "metrics" && cmd !== "backfill" && cmd !== "web") {
|
|
292
413
|
throw new Error("--include-memory is only supported for `mono-agent audit-runs`, `mono-agent metrics`, `mono-agent backfill`, and `mono-agent web`.");
|
|
@@ -294,14 +415,39 @@ export function parseCliArgs(argv) {
|
|
|
294
415
|
if (limit !== undefined && cmd !== "memory") {
|
|
295
416
|
throw new Error("--limit is only supported for `mono-agent memory`.");
|
|
296
417
|
}
|
|
418
|
+
if (strict && (cmd !== "memory" || (positionals[0] ?? "stats") !== "audit")) {
|
|
419
|
+
throw new Error("--strict is only supported for `mono-agent memory audit`.");
|
|
420
|
+
}
|
|
297
421
|
if (auth && cmd !== "init") {
|
|
298
422
|
throw new Error("--auth is only supported for `mono-agent init`.");
|
|
299
423
|
}
|
|
424
|
+
if (piAuthPath !== undefined && cmd !== "auth") {
|
|
425
|
+
throw new Error("--pi-auth-path is only supported for `mono-agent auth`.");
|
|
426
|
+
}
|
|
427
|
+
if (apiKeyStdin && cmd !== "auth") {
|
|
428
|
+
throw new Error("--api-key-stdin is only supported for `mono-agent auth login <provider>`.");
|
|
429
|
+
}
|
|
430
|
+
if (codexAuthMode !== undefined && cmd !== "init" && cmd !== "auth") {
|
|
431
|
+
throw new Error("--codex-auth is only supported for `mono-agent init` and `mono-agent auth login codex`.");
|
|
432
|
+
}
|
|
433
|
+
if (fallbackModels !== undefined && fallbacks.length > 0) {
|
|
434
|
+
throw new Error("Use either legacy --fallback-models or repeated --fallback flags, not both.");
|
|
435
|
+
}
|
|
436
|
+
if (fallbackModels !== undefined && new Set(fallbackModels).size !== fallbackModels.length) {
|
|
437
|
+
throw new Error("--fallback-models contains a duplicate model reference.");
|
|
438
|
+
}
|
|
439
|
+
const selectedFallbackModels = fallbackModels ?? fallbacks.map((fallback) => fallback.model);
|
|
440
|
+
if (model !== undefined && selectedFallbackModels.includes(model)) {
|
|
441
|
+
throw new Error(`Primary --model \`${model}\` cannot also be a fallback.`);
|
|
442
|
+
}
|
|
300
443
|
return {
|
|
301
444
|
command: cmd,
|
|
302
445
|
...(configPath === undefined ? {} : { configPath }),
|
|
446
|
+
...(name === undefined ? {} : { name }),
|
|
303
447
|
...(model === undefined ? {} : { model }),
|
|
304
448
|
...(fallbackModels === undefined ? {} : { fallbackModels }),
|
|
449
|
+
...(fallbacks.length === 0 ? {} : { fallbacks }),
|
|
450
|
+
...(routeSafety === undefined ? {} : { routeSafety }),
|
|
305
451
|
...(effort === undefined ? {} : { effort }),
|
|
306
452
|
...(memory === undefined ? {} : { memory }),
|
|
307
453
|
...(preset === undefined ? {} : { preset }),
|
|
@@ -309,6 +455,9 @@ export function parseCliArgs(argv) {
|
|
|
309
455
|
...(withChannels === undefined ? {} : { withChannels }),
|
|
310
456
|
...(yes ? { yes } : {}),
|
|
311
457
|
...(auth ? { auth } : {}),
|
|
458
|
+
...(piAuthPath === undefined ? {} : { piAuthPath }),
|
|
459
|
+
...(apiKeyStdin ? { apiKeyStdin } : {}),
|
|
460
|
+
...(codexAuthMode === undefined ? {} : { codexAuthMode }),
|
|
312
461
|
positionals,
|
|
313
462
|
...(envFile === undefined ? {} : { envFile }),
|
|
314
463
|
...(target === undefined ? {} : { target }),
|
|
@@ -327,13 +476,20 @@ export function parseCliArgs(argv) {
|
|
|
327
476
|
...(consumerPath === undefined ? {} : { consumerPath }),
|
|
328
477
|
...(staleAfterMs === undefined ? {} : { staleAfterMs }),
|
|
329
478
|
...(json ? { json } : {}),
|
|
479
|
+
...(strict ? { strict } : {}),
|
|
330
480
|
...(limit === undefined ? {} : { limit }),
|
|
331
481
|
...(agent === undefined ? {} : { agent }),
|
|
332
482
|
...(conversation === undefined ? {} : { conversation }),
|
|
483
|
+
...(local ? { local } : {}),
|
|
484
|
+
...(configure ? { configure } : {}),
|
|
485
|
+
...(project ? { project } : {}),
|
|
486
|
+
...(check ? { check } : {}),
|
|
487
|
+
...(update ? { update } : {}),
|
|
333
488
|
...(host === undefined ? {} : { host }),
|
|
334
489
|
...(port === undefined ? {} : { port }),
|
|
335
490
|
...(open === undefined ? {} : { open }),
|
|
336
491
|
...(allowNonLoopback === undefined ? {} : { allowNonLoopback }),
|
|
492
|
+
...(showAuthUrl === undefined ? {} : { showAuthUrl }),
|
|
337
493
|
...(maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance }),
|
|
338
494
|
};
|
|
339
495
|
}
|
|
@@ -370,14 +526,18 @@ function requireValue(args, index, flag) {
|
|
|
370
526
|
const HELP_COMMANDS = [
|
|
371
527
|
{
|
|
372
528
|
signature: "mono-agent init [--preset <id>] [--with <csv>] [--yes] [--auth] [--dry-run]\n" +
|
|
373
|
-
" [--
|
|
374
|
-
" [--
|
|
529
|
+
" [--name <display-name>] [--model <ref>] [--effort <level>]\n" +
|
|
530
|
+
" [--fallback <ref> [--fallback-effort <provider-default|level>]]...\n" +
|
|
531
|
+
" [--fallback-models <csv>] [--route-safety uniform|per-route-native]\n" +
|
|
532
|
+
" [--codex-auth browser|device] [--memory lite|journal|bujo]",
|
|
375
533
|
lines: [
|
|
376
534
|
"Scaffold a mono-agent in the current folder. On a TTY with no flags, launches",
|
|
377
535
|
"the step-by-step wizard; with --yes or any flag, writes the default/preset",
|
|
378
536
|
"scaffold non-interactively. --preset seeds a blueprint, --with adds channels,",
|
|
379
|
-
|
|
380
|
-
"
|
|
537
|
+
`Effort levels: ${EFFORT_LEVELS.join(", ")}; an omitted fallback effort uses that provider's default.`,
|
|
538
|
+
"--auth runs supported provider auth/preflight before writing; --codex-auth device supports headless hosts.",
|
|
539
|
+
"--dry-run previews only. Existing scaffold/config files are not overwritten;",
|
|
540
|
+
"guided secret setup may securely update .env and .gitignore after explicit review.",
|
|
381
541
|
],
|
|
382
542
|
},
|
|
383
543
|
{
|
|
@@ -392,7 +552,7 @@ const HELP_COMMANDS = [
|
|
|
392
552
|
],
|
|
393
553
|
},
|
|
394
554
|
{
|
|
395
|
-
signature: "mono-agent validate [--preset <id>] [--consumer <path>] [--config <path>] [--env-file <path>]",
|
|
555
|
+
signature: "mono-agent validate [--preset <id>] [--consumer <path>] [--config <path>] [--env-file <path>] [--json]",
|
|
396
556
|
lines: [
|
|
397
557
|
"Load every config section and report what would run, wait, or fail.",
|
|
398
558
|
"--consumer validates another agent folder read-only, including its .env.",
|
|
@@ -400,6 +560,25 @@ const HELP_COMMANDS = [
|
|
|
400
560
|
"`mono-agent doctor` is an alias for this command.",
|
|
401
561
|
],
|
|
402
562
|
},
|
|
563
|
+
{
|
|
564
|
+
signature: "mono-agent auth login <provider|codex> [--pi-auth-path <path>] [--api-key-stdin]\n" +
|
|
565
|
+
" [--codex-auth browser|device] [--config <path>]",
|
|
566
|
+
lines: [
|
|
567
|
+
"Run a supported bundled Pi provider login, or direct Codex browser/device login.",
|
|
568
|
+
"Pi credentials are promoted with owner-only no-clobber checks.",
|
|
569
|
+
"API-key providers prompt securely on a TTY; --api-key-stdin explicitly reads a redirected secret.",
|
|
570
|
+
"Path precedence: --pi-auth-path, MONO_AGENT_PI_AUTH_PATH, providers.piAuthPath, then Pi's default.",
|
|
571
|
+
"Supported Pi targets: anthropic, github-copilot, openai-codex, and opencode-go.",
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
signature: "mono-agent sandbox status | setup | check",
|
|
576
|
+
lines: [
|
|
577
|
+
"Inspect, install, or functionally prove the pinned SRT sandbox runtime.",
|
|
578
|
+
"Managed setup is macOS-only and installs into the user's cache; it never changes PATH,",
|
|
579
|
+
"global npm packages, system packages, or another user's files.",
|
|
580
|
+
],
|
|
581
|
+
},
|
|
403
582
|
{
|
|
404
583
|
signature: "mono-agent config [--config <path>] [--env-file <path>]",
|
|
405
584
|
lines: [
|
|
@@ -437,16 +616,19 @@ const HELP_COMMANDS = [
|
|
|
437
616
|
lines: ["Print (and optionally follow) the background instance's log files."],
|
|
438
617
|
},
|
|
439
618
|
{
|
|
440
|
-
signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]"
|
|
619
|
+
signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]\n" +
|
|
620
|
+
" [--local [--configure]]",
|
|
441
621
|
lines: [
|
|
442
622
|
"Open the operator console from any directory: live chat with full",
|
|
443
623
|
"thinking/tool/telemetry insight, recorded-run replay, and config view.",
|
|
444
624
|
"Discovers running agents via the trace-source registry; one running",
|
|
445
625
|
"agent connects directly, several open a picker.",
|
|
626
|
+
"--local builds the current folder's responder in-process without a",
|
|
627
|
+
"daemon; --configure starts the recorded local configuration invitation.",
|
|
446
628
|
],
|
|
447
629
|
},
|
|
448
630
|
{
|
|
449
|
-
signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--include-memory] [--max-runs <n>]",
|
|
631
|
+
signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--show-auth-url] [--include-memory] [--max-runs <n>]",
|
|
450
632
|
lines: [
|
|
451
633
|
"Serve the read-only Session Recorder web PWA from any directory: a live",
|
|
452
634
|
"flight-recorder over every agent's runs (prompt, reasoning, tools, cost).",
|
|
@@ -455,13 +637,20 @@ const HELP_COMMANDS = [
|
|
|
455
637
|
"--include-memory also shows memory-maintenance runs. --max-runs (default",
|
|
456
638
|
"200) bounds the in-memory working set; the UI still pages the full",
|
|
457
639
|
"on-disk history via \"Load older\".",
|
|
640
|
+
"Non-loopback service use requires MONO_AGENT_WEB_AUTH_TOKEN; an",
|
|
641
|
+
"interactive run may generate one. --show-auth-url reveals a configured",
|
|
642
|
+
"token only to an interactive terminal. Authenticated URLs are not",
|
|
643
|
+
"auto-opened through process arguments.",
|
|
458
644
|
],
|
|
459
645
|
},
|
|
460
646
|
{
|
|
461
|
-
signature: "mono-agent install-skill [--target claude|codex|both] [--force]"
|
|
647
|
+
signature: "mono-agent install-skill [--target claude|codex|both] [--force]\n" +
|
|
648
|
+
" --project (--check|--update)",
|
|
462
649
|
lines: [
|
|
463
650
|
"Copy the bundled mono-agent-composer skill into ~/.claude/skills and",
|
|
464
651
|
"~/.agents/skills (default: both). Refuses to overwrite without --force.",
|
|
652
|
+
"Project mode checks or safely updates the two managed skills generated",
|
|
653
|
+
"by init; modified copies are never overwritten and updates retain backups.",
|
|
465
654
|
],
|
|
466
655
|
},
|
|
467
656
|
{
|
|
@@ -494,12 +683,15 @@ const HELP_COMMANDS = [
|
|
|
494
683
|
],
|
|
495
684
|
},
|
|
496
685
|
{
|
|
497
|
-
signature: "mono-agent memory [stats|today|show <date>|search <query>|top]\n" +
|
|
498
|
-
" [--limit <n>] [--json] [--config <path>] [--env-file <path>]",
|
|
686
|
+
signature: "mono-agent memory [stats|today|show <date>|search <query>|top|audit|inspect [id]|retry [id]|resolve <id> <reason>|rebuild|rollback|adopt-replay]\n" +
|
|
687
|
+
" [--limit <n>] [--strict] [--json] [--config <path>] [--env-file <path>]",
|
|
499
688
|
lines: [
|
|
500
689
|
"Preview the configured memory store from an agent folder. Reads the",
|
|
501
690
|
"memory block from mono-agent.config.json, not the standalone memory-bujo",
|
|
502
|
-
"env workflow. Human-first output by default; --json is
|
|
691
|
+
"env workflow. Human-first output by default; audit --strict --json is a",
|
|
692
|
+
"metadata-only health gate. Intake inspect/retry/resolve never print payload content.",
|
|
693
|
+
"adopt-replay is an explicit stopped-agent, SSH-safe BuJo trust-on-first-use",
|
|
694
|
+
"operation. It returns metadata only and requires rebuild before restart.",
|
|
503
695
|
],
|
|
504
696
|
},
|
|
505
697
|
];
|
|
@@ -509,11 +701,28 @@ const HELP_NOTES = `Background mode runs the agent under launchd, keeping it ali
|
|
|
509
701
|
commands require macOS; elsewhere use start --foreground.
|
|
510
702
|
|
|
511
703
|
Init model references look like pi:<provider>:<model>, claude:claude-sonnet-4-6,
|
|
512
|
-
codex:gpt-5.5, or opencode:<provider>:<model>. The init wizard
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
704
|
+
codex:gpt-5.6-terra, codex:gpt-5.6-sol, or opencode:<provider>:<model>. The init wizard
|
|
705
|
+
selects the live provider-declared default when available and falls back offline to
|
|
706
|
+
codex:gpt-5.6-terra. Direct and Pi OpenAI-Codex Sol choices remain selectable.
|
|
707
|
+
Direct GPT-5.6 routes require Codex CLI 0.144.0 or newer. Guided Pi authentication
|
|
708
|
+
covers Anthropic, GitHub Copilot, OpenAI Codex, and OpenCode-Go. Claude remains selectable;
|
|
709
|
+
direct opencode:<provider>:<model> refs are for
|
|
710
|
+
hand-authored runtime backend config and are rejected by guided selection/readiness.
|
|
711
|
+
|
|
712
|
+
Mixed fallback chains are allowed. runtime.routeSafety=uniform (the default)
|
|
713
|
+
requires one compatibility-preserving contract across every route;
|
|
714
|
+
per-route-native makes each route's exact safety boundary explicit (Pi SRT,
|
|
715
|
+
Claude provider-owned permissions, Codex native sandbox, or OpenCode native).
|
|
716
|
+
|
|
717
|
+
Native mono-agent srt policy is enforced by Pi-owned tools. In uniform mode,
|
|
718
|
+
Claude, direct Codex, and direct OpenCode cannot silently weaken that policy.
|
|
719
|
+
In per-route-native mode, validate reports each provider-owned safety contract
|
|
720
|
+
and rejects capabilities that the selected route cannot represent.
|
|
721
|
+
Direct OpenCode's bridge cannot enforce an explicit runtime.effort; omit effort and
|
|
722
|
+
configure runtime.permissionMode deliberately for hand-authored direct routes.
|
|
723
|
+
It is per-run/non-resumable and rejects MCP (including auto-provisioned memory
|
|
724
|
+
or send tools), positive maxTurns, index skill disclosure, structured output,
|
|
725
|
+
live input, fast mode, and native subagents instead of silently dropping them.
|
|
517
726
|
|
|
518
727
|
A .env file in the current folder is loaded automatically when present;
|
|
519
728
|
already-exported shell variables take precedence.
|
|
@@ -558,14 +767,41 @@ export async function runCli(argv) {
|
|
|
558
767
|
args = parseCliArgs(argv);
|
|
559
768
|
}
|
|
560
769
|
catch (error) {
|
|
770
|
+
if (argv[0] === "memory" && argv.includes("adopt-replay")) {
|
|
771
|
+
const { writeReplayAdoptionCliFailure } = await import("./memory-command.js");
|
|
772
|
+
writeReplayAdoptionCliFailure(argv.includes("--json"), "replay_adoption_usage");
|
|
773
|
+
return 2;
|
|
774
|
+
}
|
|
561
775
|
process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
|
|
562
776
|
process.stdout.write(`\n${renderHelp()}`);
|
|
563
777
|
return 2;
|
|
564
778
|
}
|
|
779
|
+
if (args.command === "memory"
|
|
780
|
+
&& args.positionals[0] === "adopt-replay"
|
|
781
|
+
&& hasUnsupportedReplayAdoptionFlag(argv)) {
|
|
782
|
+
const { writeReplayAdoptionCliFailure } = await import("./memory-command.js");
|
|
783
|
+
writeReplayAdoptionCliFailure(args.json === true, "replay_adoption_usage");
|
|
784
|
+
return 2;
|
|
785
|
+
}
|
|
565
786
|
const invocationCwd = process.cwd();
|
|
566
|
-
|
|
787
|
+
// Capture the exported shell before dotenv loading. Guided init retains only
|
|
788
|
+
// worker-operational values and reports shell/background credential drift;
|
|
789
|
+
// normal CLI commands still get the established shell-over-dotenv precedence.
|
|
790
|
+
const shellEnv = { ...process.env };
|
|
791
|
+
const envFilePath = args.command === "validate"
|
|
567
792
|
? resolveValidateContext(args, invocationCwd).envFilePath
|
|
568
|
-
: resolve(invocationCwd, args.envFile ?? ".env")
|
|
793
|
+
: resolve(invocationCwd, args.envFile ?? ".env");
|
|
794
|
+
let dotenvEnv = {};
|
|
795
|
+
if (args.command === "init") {
|
|
796
|
+
try {
|
|
797
|
+
dotenvEnv = await readCliDotenvFile(envFilePath);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
process.stderr.write(ui.errorLine(`Cannot read ${envFilePath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
801
|
+
return 1;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
loadCliEnvFile(envFilePath);
|
|
569
805
|
switch (args.command) {
|
|
570
806
|
case "help":
|
|
571
807
|
process.stdout.write(renderHelp());
|
|
@@ -574,9 +810,13 @@ export async function runCli(argv) {
|
|
|
574
810
|
process.stdout.write(`mono-agent ${monoAgentVersion()}\n`);
|
|
575
811
|
return 0;
|
|
576
812
|
case "init":
|
|
577
|
-
return await runInit(args);
|
|
813
|
+
return await runInit(args, { shellEnv, dotenvEnv, dotenvPath: envFilePath });
|
|
578
814
|
case "validate":
|
|
579
815
|
return await runValidate(args);
|
|
816
|
+
case "auth":
|
|
817
|
+
return await runAuth(args);
|
|
818
|
+
case "sandbox":
|
|
819
|
+
return await runSandboxCommand(args);
|
|
580
820
|
case "config":
|
|
581
821
|
return await runConfig(args);
|
|
582
822
|
case "presets":
|
|
@@ -597,6 +837,8 @@ export async function runCli(argv) {
|
|
|
597
837
|
env: process.env,
|
|
598
838
|
...(args.agent === undefined ? {} : { agent: args.agent }),
|
|
599
839
|
...(args.conversation === undefined ? {} : { conversationId: args.conversation }),
|
|
840
|
+
...(args.local === true ? { local: true } : {}),
|
|
841
|
+
...(args.configure === true ? { configure: true } : {}),
|
|
600
842
|
});
|
|
601
843
|
}
|
|
602
844
|
case "web": {
|
|
@@ -610,6 +852,7 @@ export async function runCli(argv) {
|
|
|
610
852
|
...(args.port === undefined ? {} : { port: args.port }),
|
|
611
853
|
...(args.open === undefined ? {} : { open: args.open }),
|
|
612
854
|
...(args.allowNonLoopback === undefined ? {} : { allowNonLoopback: args.allowNonLoopback }),
|
|
855
|
+
...(args.showAuthUrl === undefined ? {} : { showAuthUrl: args.showAuthUrl }),
|
|
613
856
|
includeMemory: args.includeMemory,
|
|
614
857
|
...(args.maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance: args.maxRunsPerInstance }),
|
|
615
858
|
});
|
|
@@ -654,56 +897,809 @@ export async function runCli(argv) {
|
|
|
654
897
|
...(args.configPath === undefined ? {} : { configPath: args.configPath }),
|
|
655
898
|
positionals: args.positionals,
|
|
656
899
|
json: args.json === true,
|
|
900
|
+
strict: args.strict === true,
|
|
657
901
|
...(args.limit === undefined ? {} : { limit: args.limit }),
|
|
658
902
|
});
|
|
659
903
|
}
|
|
660
904
|
}
|
|
661
905
|
}
|
|
662
|
-
|
|
906
|
+
/**
|
|
907
|
+
* `adopt-replay` is a stopped-store trust boundary, so command-global flags
|
|
908
|
+
* must never be silently ignored. In particular, `--dry-run` cannot be parsed
|
|
909
|
+
* successfully and then execute the mutation. Validate the raw invocation so
|
|
910
|
+
* future parser flags also fail closed unless they are explicitly admitted.
|
|
911
|
+
*/
|
|
912
|
+
function hasUnsupportedReplayAdoptionFlag(argv) {
|
|
913
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
914
|
+
const token = argv[index];
|
|
915
|
+
if (token === "--config" || token === "--env-file") {
|
|
916
|
+
index += 1;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (token === "--json")
|
|
920
|
+
continue;
|
|
921
|
+
if (token?.startsWith("-") === true)
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
const DEFAULT_SANDBOX_COMMAND_DEPENDENCIES = {
|
|
927
|
+
status: sandboxRuntimeStatus,
|
|
928
|
+
setup: setupManagedSrt,
|
|
929
|
+
check: checkSandboxRuntime,
|
|
930
|
+
};
|
|
931
|
+
/** App-owned sandbox lifecycle surface; safe to inject in focused CLI tests. */
|
|
932
|
+
export async function runSandboxCommand(args, dependencies = DEFAULT_SANDBOX_COMMAND_DEPENDENCIES) {
|
|
933
|
+
const [subcommand, ...extra] = args.positionals;
|
|
934
|
+
if ((subcommand !== "status" && subcommand !== "setup" && subcommand !== "check") || extra.length > 0) {
|
|
935
|
+
process.stderr.write(ui.errorLine("[sandbox_usage] Usage: mono-agent sandbox status | setup | check."));
|
|
936
|
+
return 2;
|
|
937
|
+
}
|
|
938
|
+
if (subcommand === "status") {
|
|
939
|
+
try {
|
|
940
|
+
printSandboxRuntimeStatus(await dependencies.status());
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
process.stderr.write(ui.errorLine(`[sandbox_status_failed] ${reasonOf(error)}`));
|
|
945
|
+
return 1;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return await withScopedSandboxCancellation(async (signal) => {
|
|
949
|
+
try {
|
|
950
|
+
if (subcommand === "setup") {
|
|
951
|
+
process.stdout.write(ui.heading("Sandbox setup"));
|
|
952
|
+
process.stdout.write(ui.style.dim("Installing the pinned SRT copy in the user cache; no PATH, global npm, or system-package changes will be made.\n"));
|
|
953
|
+
const result = await dependencies.setup({ signal, verify: true });
|
|
954
|
+
printSandboxSetupResult(result);
|
|
955
|
+
return 0;
|
|
956
|
+
}
|
|
957
|
+
process.stdout.write(ui.heading("Sandbox check"));
|
|
958
|
+
const result = await dependencies.check({ signal });
|
|
959
|
+
printSandboxCheckResult(result);
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (signal.aborted || isAbortLike(error)) {
|
|
964
|
+
process.stderr.write(ui.errorLine("[sandbox_interrupted] Sandbox operation was interrupted; no partial success was claimed."));
|
|
965
|
+
process.stderr.write(ui.hint(`Retry safely with \`mono-agent sandbox ${subcommand}\`.\n`));
|
|
966
|
+
return 130;
|
|
967
|
+
}
|
|
968
|
+
const code = subcommand === "setup" ? "sandbox_setup_failed" : "sandbox_check_failed";
|
|
969
|
+
process.stderr.write(ui.errorLine(`[${code}] ${reasonOf(error)}`));
|
|
970
|
+
process.stderr.write(ui.hint(`Retry with \`mono-agent sandbox ${subcommand}\` after resolving the error.\n`));
|
|
971
|
+
return 1;
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function printSandboxRuntimeStatus(status) {
|
|
976
|
+
process.stdout.write(ui.heading("Sandbox status"));
|
|
977
|
+
process.stdout.write(` State: ${status.state}\n`);
|
|
978
|
+
process.stdout.write(` Source: ${status.source}\n`);
|
|
979
|
+
process.stdout.write(` Cache: ${status.installRoot}\n`);
|
|
980
|
+
process.stdout.write(` Detail: ${status.message}\n`);
|
|
981
|
+
}
|
|
982
|
+
function printSandboxCheckResult(result) {
|
|
983
|
+
printSandboxRuntimeStatus(result.status);
|
|
984
|
+
process.stdout.write(ui.heading("Functional enforcement"));
|
|
985
|
+
for (const check of result.checks) {
|
|
986
|
+
process.stdout.write(`${check.ok ? ui.badge("ok") : ui.badge("error")}${check.id}: ${check.detail}\n`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function printSandboxSetupResult(result) {
|
|
990
|
+
printSandboxRuntimeStatus(result.status);
|
|
991
|
+
const action = result.repaired ? "repaired" : result.installed ? "installed" : "already installed";
|
|
992
|
+
process.stdout.write(`${ui.badge("ok")}Managed SRT ${action}; integrity verification passed.\n`);
|
|
993
|
+
if (result.check !== undefined)
|
|
994
|
+
printSandboxCheckResult(result.check);
|
|
995
|
+
}
|
|
996
|
+
async function withScopedSandboxCancellation(task) {
|
|
997
|
+
const controller = new AbortController();
|
|
998
|
+
let interrupts = 0;
|
|
999
|
+
const interrupt = () => {
|
|
1000
|
+
interrupts += 1;
|
|
1001
|
+
controller.abort();
|
|
1002
|
+
};
|
|
1003
|
+
const onKeypress = (_value, key) => {
|
|
1004
|
+
if (key?.name === "escape")
|
|
1005
|
+
interrupt();
|
|
1006
|
+
};
|
|
1007
|
+
process.on("SIGINT", interrupt);
|
|
1008
|
+
const restoreKeypress = attachScopedKeypress(onKeypress);
|
|
1009
|
+
try {
|
|
1010
|
+
const result = await task(controller.signal);
|
|
1011
|
+
return interrupts > 1 ? 130 : result;
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
process.off("SIGINT", interrupt);
|
|
1015
|
+
restoreKeypress();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function isAbortLike(error) {
|
|
1019
|
+
return error instanceof Error && (error.name === "AbortError" || /abort|cancel/iu.test(error.message));
|
|
1020
|
+
}
|
|
1021
|
+
function attachScopedKeypress(listener) {
|
|
1022
|
+
if (!process.stdin.isTTY)
|
|
1023
|
+
return () => undefined;
|
|
1024
|
+
emitKeypressEvents(process.stdin);
|
|
1025
|
+
const input = process.stdin;
|
|
1026
|
+
const wasRaw = input.isRaw === true;
|
|
1027
|
+
const wasFlowing = input.readableFlowing;
|
|
1028
|
+
input.setRawMode?.(true);
|
|
1029
|
+
input.resume();
|
|
1030
|
+
input.on("keypress", listener);
|
|
1031
|
+
return () => {
|
|
1032
|
+
input.off("keypress", listener);
|
|
1033
|
+
input.setRawMode?.(wasRaw);
|
|
1034
|
+
if (wasFlowing !== true)
|
|
1035
|
+
input.pause();
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function runInit(args, environment) {
|
|
1039
|
+
const cwd = process.cwd();
|
|
663
1040
|
// On an interactive TTY with no overriding flags, walk the step-by-step wizard;
|
|
664
1041
|
// any flag (or a piped/non-TTY invocation) takes the silent default/preset path.
|
|
665
|
-
const wantsWizard = process.stdin.isTTY === true
|
|
666
|
-
&& args.yes !== true && args.preset === undefined && args.recipe === undefined
|
|
667
|
-
&& args.model === undefined && args.fallbackModels === undefined
|
|
668
|
-
&& args.effort === undefined && args.memory === undefined && args.withChannels === undefined && args.dryRun !== true;
|
|
1042
|
+
const wantsWizard = shouldRunInitWizard(args, process.stdin.isTTY === true, process.stdout.isTTY === true);
|
|
669
1043
|
if (wantsWizard) {
|
|
670
1044
|
// Existing-config pre-check — don't walk the wizard into a guaranteed no-op.
|
|
671
|
-
if (await pathExists(resolve(
|
|
1045
|
+
if (await pathExists(resolve(cwd, "mono-agent.config.json"))) {
|
|
672
1046
|
process.stdout.write(ui.hint("Found an existing mono-agent.config.json — `mono-agent init` never overwrites. Run `mono-agent validate`, or start in an empty folder.\n"));
|
|
673
1047
|
return 0;
|
|
674
1048
|
}
|
|
675
|
-
|
|
676
|
-
|
|
1049
|
+
let resolvedPiAuthPath = resolveEffectivePiAuthPath({
|
|
1050
|
+
cwd,
|
|
1051
|
+
...(nonEmptyEnv(environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH)
|
|
1052
|
+
? { envPath: environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH }
|
|
1053
|
+
: {}),
|
|
1054
|
+
});
|
|
1055
|
+
const initial = await runInitWizard({
|
|
1056
|
+
cwd,
|
|
1057
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1058
|
+
persistedEnv: environment.dotenvEnv,
|
|
1059
|
+
});
|
|
1060
|
+
if (initial.status === "cancelled")
|
|
677
1061
|
return 1;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1062
|
+
let answers = initial.answers;
|
|
1063
|
+
let moduleSecrets = { ...initial.moduleSecrets };
|
|
1064
|
+
let providerEnvironmentSecrets = { ...initial.providerEnvironmentSecrets };
|
|
1065
|
+
let providerSetupSecrets = { ...initial.providerSetupSecrets };
|
|
1066
|
+
let piApiKeyPersistenceByProvider = { ...initial.piApiKeyPersistenceByProvider };
|
|
1067
|
+
let credentialStates = { ...initial.credentialStates };
|
|
1068
|
+
let pendingProviderSetup = initial.runProviderSetup;
|
|
1069
|
+
let selectedCodexAuthMode = "browser";
|
|
1070
|
+
let readinessProgress;
|
|
1071
|
+
let sandboxMutationCompleted = false;
|
|
1072
|
+
let deferredFailure;
|
|
1073
|
+
firstRun: for (;;) {
|
|
1074
|
+
let dotenvSnapshot = { env: {}, fingerprint: "unreadable" };
|
|
1075
|
+
let failure = deferredFailure;
|
|
1076
|
+
let configurationRecoveryStep;
|
|
1077
|
+
let invalidPlanStage;
|
|
1078
|
+
deferredFailure = undefined;
|
|
1079
|
+
try {
|
|
1080
|
+
dotenvSnapshot = await readCliDotenvSnapshot(environment.dotenvPath);
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
failure ??= dotenvReadinessFailure("The persisted .env could not be read safely. Fix it before retrying setup.");
|
|
1084
|
+
}
|
|
1085
|
+
const plan = composeWizardPlan(answers, {
|
|
1086
|
+
dirBasename: basename(cwd),
|
|
1087
|
+
skillsRootExists: await pathExists(resolve(cwd, "skills")),
|
|
683
1088
|
});
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
...(
|
|
690
|
-
|
|
1089
|
+
resolvedPiAuthPath = resolveEffectivePiAuthPath({
|
|
1090
|
+
cwd,
|
|
1091
|
+
...(nonEmptyEnv(dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH)
|
|
1092
|
+
? { envPath: dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH }
|
|
1093
|
+
: {}),
|
|
1094
|
+
...(nonEmptyEnv(plan.configJson.providers?.piAuthPath)
|
|
1095
|
+
? { configPath: plan.configJson.providers.piAuthPath }
|
|
1096
|
+
: {}),
|
|
691
1097
|
});
|
|
692
|
-
|
|
693
|
-
|
|
1098
|
+
const effectiveEnv = effectiveFirstRunEnvironment({
|
|
1099
|
+
shellEnv: environment.shellEnv,
|
|
1100
|
+
dotenvEnv: dotenvSnapshot.env,
|
|
1101
|
+
enteredSecrets: { ...moduleSecrets, ...providerEnvironmentSecrets },
|
|
1102
|
+
resolvedPiAuthPath,
|
|
1103
|
+
});
|
|
1104
|
+
// Re-submit every selected durable value, not only values typed during this
|
|
1105
|
+
// wizard session. That lets the secure merge tighten an existing .env to
|
|
1106
|
+
// 0600 while preserving its non-empty operator-owned values verbatim.
|
|
1107
|
+
const selectedSecrets = {
|
|
1108
|
+
...selectedSecretValues(plan, effectiveEnv),
|
|
1109
|
+
...providerEnvironmentSecrets,
|
|
1110
|
+
};
|
|
1111
|
+
const secureExistingDotenv = hasSensitivePersistedEnvironmentValue(dotenvSnapshot.env);
|
|
1112
|
+
const conflicts = selectedSecretEnvironmentConflicts(plan, environment.shellEnv, dotenvSnapshot.env, moduleSecrets);
|
|
1113
|
+
const persistedOverrides = unexpectedPersistedMonoAgentOverrides(plan, dotenvSnapshot.env);
|
|
1114
|
+
if (failure === undefined && persistedOverrides.length > 0) {
|
|
1115
|
+
failure = {
|
|
1116
|
+
ok: false,
|
|
1117
|
+
kind: "invalid_plan",
|
|
1118
|
+
message: `Persisted .env contains mono-agent config override${persistedOverrides.length === 1 ? "" : "s"}: ` +
|
|
1119
|
+
`${persistedOverrides.join(", ")}. Remove ${persistedOverrides.length === 1 ? "it" : "them"} so the ` +
|
|
1120
|
+
"generated config is the exact config validated and started.",
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (failure === undefined && conflicts.length > 0) {
|
|
1124
|
+
failure = {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
kind: "invalid_plan",
|
|
1127
|
+
message: `Selected secret${conflicts.length === 1 ? "" : "s"} ${conflicts.join(", ")} ` +
|
|
1128
|
+
"differ between the exported shell, persisted .env, or newly entered value. " +
|
|
1129
|
+
"Unset the shell value or make every source match, then retry.",
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (failure === undefined && piAuthPathBackgroundConflict({
|
|
1133
|
+
cwd,
|
|
1134
|
+
shellPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH,
|
|
1135
|
+
dotenvPath: dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH,
|
|
1136
|
+
...(nonEmptyEnv(plan.configJson.providers?.piAuthPath)
|
|
1137
|
+
? { configPath: plan.configJson.providers.piAuthPath }
|
|
1138
|
+
: {}),
|
|
1139
|
+
})) {
|
|
1140
|
+
failure = {
|
|
1141
|
+
ok: false,
|
|
1142
|
+
kind: "invalid_plan",
|
|
1143
|
+
message: "The exported MONO_AGENT_PI_AUTH_PATH selects a different credential store than a background start. " +
|
|
1144
|
+
"Persist the same path in .env or providers.piAuthPath, or unset the shell override, then retry.",
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
if (failure === undefined && pendingProviderSetup) {
|
|
1148
|
+
pendingProviderSetup = false;
|
|
1149
|
+
const modelRefs = referencedSetupModelRefs(plan);
|
|
1150
|
+
const credentialObservation = await withScopedPreflightCancellation(async (abortSignal) => ({
|
|
1151
|
+
states: await detectProviderCredentialStates({
|
|
1152
|
+
modelRefs,
|
|
1153
|
+
cwd,
|
|
1154
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1155
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1156
|
+
abortSignal,
|
|
1157
|
+
}),
|
|
1158
|
+
interrupted: abortSignal.aborted,
|
|
1159
|
+
}));
|
|
1160
|
+
if (credentialObservation.interrupted) {
|
|
1161
|
+
pendingProviderSetup = true;
|
|
1162
|
+
deferredFailure = {
|
|
1163
|
+
ok: false,
|
|
1164
|
+
kind: "cancelled",
|
|
1165
|
+
message: "Provider status detection was interrupted. No agent files were written.",
|
|
1166
|
+
interrupted: true,
|
|
1167
|
+
};
|
|
1168
|
+
continue firstRun;
|
|
1169
|
+
}
|
|
1170
|
+
credentialStates = credentialObservation.states;
|
|
1171
|
+
const plannedSetup = planProviderSetup({
|
|
1172
|
+
modelRefs,
|
|
1173
|
+
cwd,
|
|
1174
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1175
|
+
credentialStates,
|
|
1176
|
+
piApiKeyPersistenceByProvider,
|
|
1177
|
+
});
|
|
1178
|
+
if (plannedSetup.actions.some((action) => action.id === "codex-login")) {
|
|
1179
|
+
const selected = await selectCodexAuthMode(selectedCodexAuthMode);
|
|
1180
|
+
if (selected === undefined)
|
|
1181
|
+
return 1;
|
|
1182
|
+
selectedCodexAuthMode = selected;
|
|
1183
|
+
}
|
|
1184
|
+
const environmentApiKeys = environmentProviderApiKeys(plannedSetup, effectiveEnv);
|
|
1185
|
+
const missingEnvironmentKeys = plannedSetup.actions
|
|
1186
|
+
.filter(isProviderSetupPiApiKeyAction)
|
|
1187
|
+
.filter((action) => action.persistence === "environment" && environmentApiKeys[action.id] === undefined)
|
|
1188
|
+
.map((action) => action.envVar);
|
|
1189
|
+
if (missingEnvironmentKeys.length > 0) {
|
|
1190
|
+
for (const envVar of missingEnvironmentKeys) {
|
|
1191
|
+
const answer = await p.password({
|
|
1192
|
+
message: `Enter ${envVar} for the agent's owner-only .env`,
|
|
1193
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
|
|
1194
|
+
clearOnError: true,
|
|
1195
|
+
});
|
|
1196
|
+
if (p.isCancel(answer))
|
|
1197
|
+
return 1;
|
|
1198
|
+
providerEnvironmentSecrets[envVar] = answer;
|
|
1199
|
+
}
|
|
1200
|
+
pendingProviderSetup = true;
|
|
1201
|
+
continue firstRun;
|
|
1202
|
+
}
|
|
1203
|
+
const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(effectiveEnv, () => runProviderSetupBeforeInit({
|
|
1204
|
+
modelRefs,
|
|
1205
|
+
cwd,
|
|
1206
|
+
auth: true,
|
|
1207
|
+
dryRun: false,
|
|
1208
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1209
|
+
apiKeys: { ...providerSetupSecrets, ...environmentApiKeys },
|
|
1210
|
+
codexAuthMode: selectedCodexAuthMode,
|
|
1211
|
+
credentialStates,
|
|
1212
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1213
|
+
piApiKeyPersistenceByProvider,
|
|
1214
|
+
abortSignal,
|
|
1215
|
+
})), { keypress: false });
|
|
1216
|
+
if (setup === "fatal")
|
|
1217
|
+
return 130;
|
|
1218
|
+
if (setup === "interrupted") {
|
|
1219
|
+
pendingProviderSetup = true;
|
|
1220
|
+
failure = {
|
|
1221
|
+
ok: false,
|
|
1222
|
+
kind: "cancelled",
|
|
1223
|
+
message: "Provider setup was interrupted. No agent files were written.",
|
|
1224
|
+
interrupted: true,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
else if (setup === "failed") {
|
|
1228
|
+
// A plain retry must revisit provider setup rather than falling
|
|
1229
|
+
// through to a guaranteed-failing model turn.
|
|
1230
|
+
pendingProviderSetup = true;
|
|
1231
|
+
failure = {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
kind: "provider_failed",
|
|
1234
|
+
message: "Provider setup did not complete. No agent files were written.",
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (failure === undefined) {
|
|
1239
|
+
if (answers.sandbox) {
|
|
1240
|
+
const sandboxPreflight = await runGuidedSandboxPreflight(sandboxMutationCompleted);
|
|
1241
|
+
sandboxMutationCompleted = sandboxMutationCompleted || sandboxPreflight.ok;
|
|
1242
|
+
if (!sandboxPreflight.ok)
|
|
1243
|
+
failure = sandboxPreflight;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (failure === undefined) {
|
|
1247
|
+
const configurationGate = await runConfigurationPreflightWithSpinner({
|
|
1248
|
+
cwd,
|
|
1249
|
+
answers,
|
|
1250
|
+
plan,
|
|
1251
|
+
env: effectiveEnv,
|
|
1252
|
+
secretValues: selectedSecrets,
|
|
1253
|
+
secureExistingDotenv,
|
|
1254
|
+
});
|
|
1255
|
+
if (configurationGate.interrupted === true) {
|
|
1256
|
+
failure = {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
kind: "cancelled",
|
|
1259
|
+
message: "Configuration preflight was interrupted. No agent files were written.",
|
|
1260
|
+
interrupted: true,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
else if (!configurationGate.ready) {
|
|
1264
|
+
configurationRecoveryStep = focusedConfigurationRepairStep(configurationGate.failedSectionIds);
|
|
1265
|
+
invalidPlanStage = "configuration";
|
|
1266
|
+
failure = {
|
|
1267
|
+
ok: false,
|
|
1268
|
+
kind: "invalid_plan",
|
|
1269
|
+
message: `Configuration preflight did not pass: ${configurationGate.reasons.join(" ")}`,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (failure === undefined) {
|
|
1274
|
+
const readiness = await runReadinessProbeWithSpinner({
|
|
1275
|
+
plan,
|
|
1276
|
+
effectiveEnv,
|
|
1277
|
+
resolvedPiAuthPath,
|
|
1278
|
+
...(readinessProgress === undefined ? {} : {
|
|
1279
|
+
resume: {
|
|
1280
|
+
planFingerprint: readinessProgress.planFingerprint,
|
|
1281
|
+
successfulRouteKeys: readinessProgress.successfulRouteKeys,
|
|
1282
|
+
},
|
|
1283
|
+
}),
|
|
1284
|
+
});
|
|
1285
|
+
readinessProgress = mergeReadinessProgress(readinessProgress, readiness, plan);
|
|
1286
|
+
failure = readiness;
|
|
1287
|
+
}
|
|
1288
|
+
readyAttempt: if (failure.ok) {
|
|
1289
|
+
const stagedGate = await runFinalReadinessValidationWithSpinner({
|
|
1290
|
+
cwd,
|
|
1291
|
+
answers,
|
|
1292
|
+
plan,
|
|
1293
|
+
env: effectiveEnv,
|
|
1294
|
+
secretValues: selectedSecrets,
|
|
1295
|
+
secureExistingDotenv,
|
|
1296
|
+
verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
|
|
1297
|
+
});
|
|
1298
|
+
if (stagedGate.interrupted === true) {
|
|
1299
|
+
failure = {
|
|
1300
|
+
ok: false,
|
|
1301
|
+
kind: "cancelled",
|
|
1302
|
+
message: "Final readiness validation was interrupted. No agent files were written.",
|
|
1303
|
+
interrupted: true,
|
|
1304
|
+
};
|
|
1305
|
+
break readyAttempt;
|
|
1306
|
+
}
|
|
1307
|
+
if (stagedGate.ready) {
|
|
1308
|
+
const drift = await firstRunDotenvDrift(environment.dotenvPath, dotenvSnapshot);
|
|
1309
|
+
if (drift !== undefined) {
|
|
1310
|
+
failure = drift;
|
|
1311
|
+
break readyAttempt;
|
|
1312
|
+
}
|
|
1313
|
+
let result;
|
|
1314
|
+
try {
|
|
1315
|
+
result = await initMonoAgentFolder({
|
|
1316
|
+
dir: cwd,
|
|
1317
|
+
answers,
|
|
1318
|
+
env: effectiveEnv,
|
|
1319
|
+
secretValues: selectedSecrets,
|
|
1320
|
+
secureExistingDotenv,
|
|
1321
|
+
requireConfigCreation: true,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
const recovery = secretPersistenceRecoveryMessage(error);
|
|
1326
|
+
process.stderr.write(ui.errorLine(`The validated scaffold could not be committed safely. The agent was not started; inspect the destination before retrying.${recovery}`));
|
|
1327
|
+
return 1;
|
|
1328
|
+
}
|
|
1329
|
+
let committedConfigSnapshot;
|
|
1330
|
+
try {
|
|
1331
|
+
committedConfigSnapshot = await readCliConfigSnapshot(result.configPath);
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
printIncompleteSetup(["The committed config could not be read back as the regular file setup created."], result.configPath);
|
|
1335
|
+
return 1;
|
|
1336
|
+
}
|
|
1337
|
+
if (committedConfigSnapshot.contents !== `${JSON.stringify(result.plan.configJson, null, 2)}\n`) {
|
|
1338
|
+
printIncompleteSetup(["The committed config does not match the exact plan setup validated."], result.configPath);
|
|
1339
|
+
return 1;
|
|
1340
|
+
}
|
|
1341
|
+
let committedDotenvSnapshot;
|
|
1342
|
+
try {
|
|
1343
|
+
committedDotenvSnapshot = await readCliDotenvSnapshot(environment.dotenvPath);
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
printIncompleteSetup(["The committed .env could not be read back safely; no readiness claim is safe."], result.configPath);
|
|
1347
|
+
return 1;
|
|
1348
|
+
}
|
|
1349
|
+
const postWriteConflicts = selectedSecretEnvironmentConflicts(result.plan, environment.shellEnv, committedDotenvSnapshot.env, moduleSecrets);
|
|
1350
|
+
const postWriteOverrides = unexpectedPersistedMonoAgentOverrides(result.plan, committedDotenvSnapshot.env);
|
|
1351
|
+
const postWritePiAuthConflict = piAuthPathBackgroundConflict({
|
|
1352
|
+
cwd,
|
|
1353
|
+
shellPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH,
|
|
1354
|
+
dotenvPath: committedDotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH,
|
|
1355
|
+
...(nonEmptyEnv(result.plan.configJson.providers?.piAuthPath)
|
|
1356
|
+
? { configPath: result.plan.configJson.providers.piAuthPath }
|
|
1357
|
+
: {}),
|
|
1358
|
+
});
|
|
1359
|
+
if (postWriteConflicts.length > 0 || postWriteOverrides.length > 0 || postWritePiAuthConflict) {
|
|
1360
|
+
printIncompleteSetup(["The committed .env no longer matches the values and generated config that setup approved."], result.configPath);
|
|
1361
|
+
return 1;
|
|
1362
|
+
}
|
|
1363
|
+
const postWriteEnv = effectiveFirstRunEnvironment({
|
|
1364
|
+
shellEnv: environment.shellEnv,
|
|
1365
|
+
dotenvEnv: committedDotenvSnapshot.env,
|
|
1366
|
+
resolvedPiAuthPath,
|
|
1367
|
+
});
|
|
1368
|
+
if (!sameConcreteEnvironment(effectiveEnv, postWriteEnv)) {
|
|
1369
|
+
printIncompleteSetup(["The durable environment changed after the primary-model check. Retry setup before claiming readiness."], result.configPath);
|
|
1370
|
+
return 1;
|
|
1371
|
+
}
|
|
1372
|
+
printInitResult(result);
|
|
1373
|
+
let report;
|
|
1374
|
+
try {
|
|
1375
|
+
report = await validateMonoAgentFolder({
|
|
1376
|
+
env: postWriteEnv,
|
|
1377
|
+
cwd,
|
|
1378
|
+
configPath: result.configPath,
|
|
1379
|
+
liveness: true,
|
|
1380
|
+
verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
printIncompleteSetup(["Post-write validation could not complete; no readiness claim is safe."], result.configPath);
|
|
1385
|
+
return 1;
|
|
1386
|
+
}
|
|
1387
|
+
process.stdout.write("\n" + ui.heading("Validation"));
|
|
1388
|
+
for (const section of report.sections)
|
|
1389
|
+
process.stdout.write(formatSection(section));
|
|
1390
|
+
process.stdout.write(renderPlanCompleteness(result.plan.validateExpectations, "Selected capabilities", report));
|
|
1391
|
+
const configuredSecrets = configuredSecretNames(result, postWriteEnv);
|
|
1392
|
+
printSecretsChecklist(result.plan.secrets, configuredSecrets);
|
|
1393
|
+
const finalGate = evaluateFirstRunReadiness({
|
|
1394
|
+
plan: result.plan,
|
|
1395
|
+
report,
|
|
1396
|
+
secretPersistence: result.secretPersistence,
|
|
1397
|
+
verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
|
|
1398
|
+
});
|
|
1399
|
+
if (!finalGate.ready) {
|
|
1400
|
+
printIncompleteSetup(finalGate.reasons, result.configPath);
|
|
1401
|
+
return 1;
|
|
1402
|
+
}
|
|
1403
|
+
const postValidationConfigDrift = await firstRunConfigDrift(result.configPath, committedConfigSnapshot);
|
|
1404
|
+
if (postValidationConfigDrift !== undefined) {
|
|
1405
|
+
printIncompleteSetup([postValidationConfigDrift.message], result.configPath);
|
|
1406
|
+
return 1;
|
|
1407
|
+
}
|
|
1408
|
+
const postValidationDrift = await firstRunDotenvDrift(environment.dotenvPath, committedDotenvSnapshot);
|
|
1409
|
+
if (postValidationDrift !== undefined) {
|
|
1410
|
+
printIncompleteSetup([postValidationDrift.message], result.configPath);
|
|
1411
|
+
return 1;
|
|
1412
|
+
}
|
|
1413
|
+
const postValidationSecretGuard = await firstRunSecretEnvGuardFailure(environment.dotenvPath, result.secretPersistence.status === "persisted");
|
|
1414
|
+
if (postValidationSecretGuard !== undefined) {
|
|
1415
|
+
printIncompleteSetup([postValidationSecretGuard.message], result.configPath);
|
|
1416
|
+
return 1;
|
|
1417
|
+
}
|
|
1418
|
+
process.stdout.write(ui.badge("ok") + ui.style.green("All runtime route checks passed — every selected model produced a real no-tool response.\n") +
|
|
1419
|
+
ui.badge("ok") + ui.style.green("Agent ready — every selected capability passed full validation.\n"));
|
|
1420
|
+
const preTuiConfigDrift = await firstRunConfigDrift(result.configPath, committedConfigSnapshot);
|
|
1421
|
+
if (preTuiConfigDrift !== undefined) {
|
|
1422
|
+
printIncompleteSetup([preTuiConfigDrift.message], result.configPath);
|
|
1423
|
+
return 1;
|
|
1424
|
+
}
|
|
1425
|
+
const preTuiDotenvDrift = await firstRunDotenvDrift(environment.dotenvPath, committedDotenvSnapshot);
|
|
1426
|
+
if (preTuiDotenvDrift !== undefined) {
|
|
1427
|
+
printIncompleteSetup([preTuiDotenvDrift.message], result.configPath);
|
|
1428
|
+
return 1;
|
|
1429
|
+
}
|
|
1430
|
+
const preTuiSecretGuard = await firstRunSecretEnvGuardFailure(environment.dotenvPath, result.secretPersistence.status === "persisted");
|
|
1431
|
+
if (preTuiSecretGuard !== undefined) {
|
|
1432
|
+
printIncompleteSetup([preTuiSecretGuard.message], result.configPath);
|
|
1433
|
+
return 1;
|
|
1434
|
+
}
|
|
1435
|
+
process.stdout.write(ui.badge("ok") + ui.style.green("Opening the ready agent in the local configuration TUI.\n"));
|
|
1436
|
+
const { runTui } = await import("./tui-command.js");
|
|
1437
|
+
return await withExactProcessEnvironment(postWriteEnv, () => runTui({
|
|
1438
|
+
configPath: result.configPath,
|
|
1439
|
+
cwd,
|
|
1440
|
+
env: postWriteEnv,
|
|
1441
|
+
local: true,
|
|
1442
|
+
configure: true,
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
configurationRecoveryStep = focusedConfigurationRepairStep(stagedGate.failedSectionIds);
|
|
1446
|
+
invalidPlanStage = "final_readiness";
|
|
1447
|
+
failure = {
|
|
1448
|
+
ok: false,
|
|
1449
|
+
kind: "invalid_plan",
|
|
1450
|
+
message: `Runtime route checks passed, but the complete agent is not ready: ${stagedGate.reasons.join(" ")}`,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
if (failure.ok)
|
|
1454
|
+
throw new Error("First-run recovery reached without a failure.");
|
|
1455
|
+
if (failure.interrupted === true || failure.kind === "cancelled") {
|
|
1456
|
+
interruptedRecoveryMenu: for (;;) {
|
|
1457
|
+
const interruptedRecovery = await selectInterruptedFirstRunRecovery();
|
|
1458
|
+
if (interruptedRecovery === "cancel")
|
|
1459
|
+
return 1;
|
|
1460
|
+
if (interruptedRecovery === "restart") {
|
|
1461
|
+
readinessProgress = undefined;
|
|
1462
|
+
break interruptedRecoveryMenu;
|
|
1463
|
+
}
|
|
1464
|
+
if (interruptedRecovery === "edit") {
|
|
1465
|
+
const repaired = await runSetupRepairWizard({
|
|
1466
|
+
cwd,
|
|
1467
|
+
answers,
|
|
1468
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1469
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1470
|
+
moduleSecrets,
|
|
1471
|
+
providerSetupSecrets,
|
|
1472
|
+
providerEnvironmentSecrets,
|
|
1473
|
+
piApiKeyPersistenceByProvider,
|
|
1474
|
+
credentialStates,
|
|
1475
|
+
runProviderSetup: pendingProviderSetup,
|
|
1476
|
+
});
|
|
1477
|
+
if (repaired.status === "cancelled")
|
|
1478
|
+
continue interruptedRecoveryMenu;
|
|
1479
|
+
answers = repaired.answers;
|
|
1480
|
+
moduleSecrets = { ...repaired.moduleSecrets };
|
|
1481
|
+
providerSetupSecrets = { ...repaired.providerSetupSecrets };
|
|
1482
|
+
providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
|
|
1483
|
+
piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
|
|
1484
|
+
credentialStates = { ...repaired.credentialStates };
|
|
1485
|
+
pendingProviderSetup = repaired.runProviderSetup;
|
|
1486
|
+
if (pendingProviderSetup)
|
|
1487
|
+
readinessProgress = undefined;
|
|
1488
|
+
}
|
|
1489
|
+
break interruptedRecoveryMenu;
|
|
1490
|
+
}
|
|
1491
|
+
continue firstRun;
|
|
1492
|
+
}
|
|
1493
|
+
if (failure.message.startsWith("[sandbox_preflight_failed]")) {
|
|
1494
|
+
sandboxRecoveryMenu: for (;;) {
|
|
1495
|
+
const recovery = await selectSandboxPreflightRecovery();
|
|
1496
|
+
if (recovery === "cancel")
|
|
1497
|
+
return 1;
|
|
1498
|
+
if (recovery === "edit") {
|
|
1499
|
+
const edited = await runSetupRepairWizard({
|
|
1500
|
+
cwd,
|
|
1501
|
+
answers,
|
|
1502
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1503
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1504
|
+
moduleSecrets,
|
|
1505
|
+
providerSetupSecrets,
|
|
1506
|
+
providerEnvironmentSecrets,
|
|
1507
|
+
piApiKeyPersistenceByProvider,
|
|
1508
|
+
credentialStates,
|
|
1509
|
+
runProviderSetup: pendingProviderSetup,
|
|
1510
|
+
});
|
|
1511
|
+
if (edited.status === "cancelled")
|
|
1512
|
+
continue sandboxRecoveryMenu;
|
|
1513
|
+
answers = edited.answers;
|
|
1514
|
+
moduleSecrets = { ...edited.moduleSecrets };
|
|
1515
|
+
providerSetupSecrets = { ...edited.providerSetupSecrets };
|
|
1516
|
+
providerEnvironmentSecrets = { ...edited.providerEnvironmentSecrets };
|
|
1517
|
+
piApiKeyPersistenceByProvider = { ...edited.piApiKeyPersistenceByProvider };
|
|
1518
|
+
credentialStates = { ...edited.credentialStates };
|
|
1519
|
+
pendingProviderSetup = edited.runProviderSetup;
|
|
1520
|
+
if (pendingProviderSetup)
|
|
1521
|
+
readinessProgress = undefined;
|
|
1522
|
+
}
|
|
1523
|
+
break sandboxRecoveryMenu;
|
|
1524
|
+
}
|
|
1525
|
+
continue firstRun;
|
|
1526
|
+
}
|
|
1527
|
+
let recoveryFailure = failure;
|
|
1528
|
+
recoveryMenu: for (;;) {
|
|
1529
|
+
p.log.error(`[${recoveryFailure.kind}] ${recoveryFailure.message}`);
|
|
1530
|
+
const recovery = await selectFirstRunRecovery(recoveryFailure, configurationRecoveryStep, invalidPlanStage);
|
|
1531
|
+
if (recovery === "cancel")
|
|
1532
|
+
return 1;
|
|
1533
|
+
if (recovery === "save") {
|
|
1534
|
+
let saved;
|
|
1535
|
+
try {
|
|
1536
|
+
saved = await initMonoAgentFolder({
|
|
1537
|
+
dir: cwd,
|
|
1538
|
+
answers,
|
|
1539
|
+
env: effectiveEnv,
|
|
1540
|
+
secretValues: selectedSecrets,
|
|
1541
|
+
secureExistingDotenv,
|
|
1542
|
+
requireConfigCreation: true,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
const recovery = secretPersistenceRecoveryMessage(error);
|
|
1547
|
+
process.stderr.write(ui.errorLine(`The incomplete scaffold could not be committed safely; inspect the destination and retry.${recovery}`));
|
|
1548
|
+
return 1;
|
|
1549
|
+
}
|
|
1550
|
+
printInitResult(saved);
|
|
1551
|
+
let durableSavedEnv = {};
|
|
1552
|
+
if (saved.secretPersistence.status === "persisted") {
|
|
1553
|
+
try {
|
|
1554
|
+
durableSavedEnv = (await readCliDotenvSnapshot(environment.dotenvPath)).env;
|
|
1555
|
+
}
|
|
1556
|
+
catch {
|
|
1557
|
+
durableSavedEnv = {};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
printSecretsChecklist(saved.plan.secrets, configuredSecretNames(saved, durableSavedEnv));
|
|
1561
|
+
printIncompleteSetup([recoveryFailure.message], saved.configPath);
|
|
1562
|
+
return 1;
|
|
1563
|
+
}
|
|
1564
|
+
if (recovery === "edit") {
|
|
1565
|
+
const repaired = await runSetupRepairWizard({
|
|
1566
|
+
cwd,
|
|
1567
|
+
answers,
|
|
1568
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1569
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1570
|
+
...(configurationRecoveryStep === undefined ? {} : { initialStep: configurationRecoveryStep }),
|
|
1571
|
+
moduleSecrets,
|
|
1572
|
+
providerSetupSecrets,
|
|
1573
|
+
providerEnvironmentSecrets,
|
|
1574
|
+
piApiKeyPersistenceByProvider,
|
|
1575
|
+
credentialStates,
|
|
1576
|
+
runProviderSetup: pendingProviderSetup,
|
|
1577
|
+
});
|
|
1578
|
+
if (repaired.status === "cancelled")
|
|
1579
|
+
continue recoveryMenu;
|
|
1580
|
+
answers = repaired.answers;
|
|
1581
|
+
moduleSecrets = { ...repaired.moduleSecrets };
|
|
1582
|
+
providerSetupSecrets = { ...repaired.providerSetupSecrets };
|
|
1583
|
+
providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
|
|
1584
|
+
piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
|
|
1585
|
+
credentialStates = { ...repaired.credentialStates };
|
|
1586
|
+
pendingProviderSetup = repaired.runProviderSetup;
|
|
1587
|
+
if (pendingProviderSetup)
|
|
1588
|
+
readinessProgress = undefined;
|
|
1589
|
+
continue firstRun;
|
|
1590
|
+
}
|
|
1591
|
+
if (recovery === "model") {
|
|
1592
|
+
const repaired = await runSetupRepairWizard({
|
|
1593
|
+
cwd,
|
|
1594
|
+
answers,
|
|
1595
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1596
|
+
persistedEnv: dotenvSnapshot.env,
|
|
1597
|
+
initialStep: 1,
|
|
1598
|
+
moduleSecrets,
|
|
1599
|
+
providerSetupSecrets,
|
|
1600
|
+
providerEnvironmentSecrets,
|
|
1601
|
+
piApiKeyPersistenceByProvider,
|
|
1602
|
+
credentialStates,
|
|
1603
|
+
runProviderSetup: pendingProviderSetup,
|
|
1604
|
+
});
|
|
1605
|
+
if (repaired.status === "cancelled")
|
|
1606
|
+
continue recoveryMenu;
|
|
1607
|
+
answers = repaired.answers;
|
|
1608
|
+
moduleSecrets = { ...repaired.moduleSecrets };
|
|
1609
|
+
providerSetupSecrets = { ...repaired.providerSetupSecrets };
|
|
1610
|
+
providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
|
|
1611
|
+
piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
|
|
1612
|
+
credentialStates = { ...repaired.credentialStates };
|
|
1613
|
+
pendingProviderSetup = repaired.runProviderSetup;
|
|
1614
|
+
if (pendingProviderSetup)
|
|
1615
|
+
readinessProgress = undefined;
|
|
1616
|
+
continue firstRun;
|
|
1617
|
+
}
|
|
1618
|
+
if (recovery === "auth") {
|
|
1619
|
+
// Authentication can replace credential bytes without changing the
|
|
1620
|
+
// route/config fingerprint. Every route must be proven again.
|
|
1621
|
+
readinessProgress = undefined;
|
|
1622
|
+
if (referencedSetupModelRefs(plan).some((ref) => ref.startsWith("codex:"))) {
|
|
1623
|
+
const selected = await selectCodexAuthMode(selectedCodexAuthMode);
|
|
1624
|
+
if (selected === undefined)
|
|
1625
|
+
return 1;
|
|
1626
|
+
selectedCodexAuthMode = selected;
|
|
1627
|
+
}
|
|
1628
|
+
const setupPlan = planProviderSetup({
|
|
1629
|
+
modelRefs: referencedSetupModelRefs(plan),
|
|
1630
|
+
cwd,
|
|
1631
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1632
|
+
codexAuthMode: selectedCodexAuthMode,
|
|
1633
|
+
forceAuthentication: true,
|
|
1634
|
+
});
|
|
1635
|
+
const prompted = await promptProviderSetupSecrets(setupPlan, providerSetupSecrets, piApiKeyPersistenceByProvider, providerEnvironmentSecrets);
|
|
1636
|
+
if (prompted === undefined)
|
|
1637
|
+
return 1;
|
|
1638
|
+
providerSetupSecrets = prompted.apiKeys;
|
|
1639
|
+
piApiKeyPersistenceByProvider = prompted.persistenceByProvider;
|
|
1640
|
+
providerEnvironmentSecrets = prompted.environmentSecrets;
|
|
1641
|
+
readinessProgress = undefined;
|
|
1642
|
+
const selectedSetupPlan = planProviderSetup({
|
|
1643
|
+
modelRefs: referencedSetupModelRefs(plan),
|
|
1644
|
+
cwd,
|
|
1645
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1646
|
+
codexAuthMode: selectedCodexAuthMode,
|
|
1647
|
+
forceAuthentication: true,
|
|
1648
|
+
piApiKeyPersistenceByProvider,
|
|
1649
|
+
});
|
|
1650
|
+
const environmentApiKeys = environmentProviderApiKeys(selectedSetupPlan, { ...effectiveEnv, ...providerEnvironmentSecrets });
|
|
1651
|
+
const missingEnvironmentKeys = selectedSetupPlan.actions
|
|
1652
|
+
.filter(isProviderSetupPiApiKeyAction)
|
|
1653
|
+
.filter((action) => action.persistence === "environment" && environmentApiKeys[action.id] === undefined)
|
|
1654
|
+
.map((action) => action.envVar);
|
|
1655
|
+
if (missingEnvironmentKeys.length > 0) {
|
|
1656
|
+
recoveryFailure = {
|
|
1657
|
+
ok: false,
|
|
1658
|
+
kind: "provider_failed",
|
|
1659
|
+
message: `Add ${missingEnvironmentKeys.join(", ")} to the durable owner-only .env, then retry authentication. No agent files were written.`,
|
|
1660
|
+
};
|
|
1661
|
+
continue recoveryMenu;
|
|
1662
|
+
}
|
|
1663
|
+
const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(effectiveEnv, () => runProviderSetupBeforeInit({
|
|
1664
|
+
modelRefs: referencedSetupModelRefs(plan),
|
|
1665
|
+
cwd,
|
|
1666
|
+
auth: true,
|
|
1667
|
+
dryRun: false,
|
|
1668
|
+
piAuthPath: resolvedPiAuthPath,
|
|
1669
|
+
apiKeys: { ...providerSetupSecrets, ...environmentApiKeys },
|
|
1670
|
+
codexAuthMode: selectedCodexAuthMode,
|
|
1671
|
+
forceAuthentication: true,
|
|
1672
|
+
piApiKeyPersistenceByProvider,
|
|
1673
|
+
abortSignal,
|
|
1674
|
+
})), { keypress: false });
|
|
1675
|
+
if (setup === "fatal")
|
|
1676
|
+
return 130;
|
|
1677
|
+
if (setup === "interrupted") {
|
|
1678
|
+
pendingProviderSetup = true;
|
|
1679
|
+
deferredFailure = {
|
|
1680
|
+
ok: false,
|
|
1681
|
+
kind: "cancelled",
|
|
1682
|
+
message: "Provider setup was interrupted. No agent files were written.",
|
|
1683
|
+
interrupted: true,
|
|
1684
|
+
};
|
|
1685
|
+
continue firstRun;
|
|
1686
|
+
}
|
|
1687
|
+
if (setup === "failed") {
|
|
1688
|
+
pendingProviderSetup = true;
|
|
1689
|
+
recoveryFailure = {
|
|
1690
|
+
ok: false,
|
|
1691
|
+
kind: "provider_failed",
|
|
1692
|
+
message: "Provider setup still needs attention. No agent files were written.",
|
|
1693
|
+
};
|
|
1694
|
+
continue recoveryMenu;
|
|
1695
|
+
}
|
|
1696
|
+
pendingProviderSetup = false;
|
|
1697
|
+
}
|
|
1698
|
+
// "retry" deliberately reruns only the live checks. Provider setup is a
|
|
1699
|
+
// separate explicit recovery action and is never repeated automatically.
|
|
1700
|
+
continue firstRun;
|
|
694
1701
|
}
|
|
695
1702
|
}
|
|
696
|
-
const result = await initMonoAgentFolder({ dir: process.cwd(), answers: outcome.answers });
|
|
697
|
-
printInitResult(result);
|
|
698
|
-
const report = await validateMonoAgentFolder({ env: process.env, cwd: process.cwd(), configPath: result.configPath });
|
|
699
|
-
process.stdout.write("\n" + ui.heading("Validation"));
|
|
700
|
-
for (const section of report.sections) {
|
|
701
|
-
process.stdout.write(formatSection(section));
|
|
702
|
-
}
|
|
703
|
-
process.stdout.write(renderPlanCompleteness(result.plan.validateExpectations, "Selected capabilities", report));
|
|
704
|
-
printSecretsChecklist(result.plan.secrets);
|
|
705
|
-
printNextSteps(result.configPath);
|
|
706
|
-
return report.ok ? 0 : 1;
|
|
707
1703
|
}
|
|
708
1704
|
const presetId = resolveInitPresetId(args);
|
|
709
1705
|
if (presetId === "unknown") {
|
|
@@ -715,7 +1711,10 @@ async function runInit(args) {
|
|
|
715
1711
|
}
|
|
716
1712
|
const answers = answersFromCli({
|
|
717
1713
|
...(args.model === undefined ? {} : { model: args.model }),
|
|
1714
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
718
1715
|
...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
|
|
1716
|
+
...(args.fallbacks === undefined ? {} : { fallbacks: args.fallbacks }),
|
|
1717
|
+
...(args.routeSafety === undefined ? {} : { routeSafety: args.routeSafety }),
|
|
719
1718
|
...(args.effort === undefined ? {} : { effort: args.effort }),
|
|
720
1719
|
...(args.memory === undefined ? {} : { memory: args.memory }),
|
|
721
1720
|
...(withChannels === undefined ? {} : { withChannels }),
|
|
@@ -725,24 +1724,750 @@ async function runInit(args) {
|
|
|
725
1724
|
dirBasename: basename(process.cwd()),
|
|
726
1725
|
skillsRootExists: await pathExists(resolve(process.cwd(), "skills")),
|
|
727
1726
|
});
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
1727
|
+
const nonInteractivePiAuthPath = resolveEffectivePiAuthPath({
|
|
1728
|
+
cwd,
|
|
1729
|
+
...(nonEmptyEnv(environment.shellEnv.MONO_AGENT_PI_AUTH_PATH)
|
|
1730
|
+
? { envPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH }
|
|
1731
|
+
: nonEmptyEnv(environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH)
|
|
1732
|
+
? { envPath: environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH }
|
|
1733
|
+
: {}),
|
|
1734
|
+
...(nonEmptyEnv(previewPlan.configJson.providers?.piAuthPath)
|
|
1735
|
+
? { configPath: previewPlan.configJson.providers.piAuthPath }
|
|
1736
|
+
: {}),
|
|
1737
|
+
});
|
|
1738
|
+
const nonInteractiveEnvironment = effectiveFirstRunEnvironment({
|
|
1739
|
+
shellEnv: environment.shellEnv,
|
|
1740
|
+
dotenvEnv: environment.dotenvEnv,
|
|
1741
|
+
resolvedPiAuthPath: nonInteractivePiAuthPath,
|
|
1742
|
+
});
|
|
1743
|
+
const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(nonInteractiveEnvironment, () => runProviderSetupBeforeInit({
|
|
1744
|
+
modelRefs: referencedSetupModelRefs(previewPlan),
|
|
1745
|
+
cwd,
|
|
731
1746
|
auth: args.auth === true,
|
|
732
1747
|
dryRun: args.dryRun,
|
|
733
|
-
|
|
734
|
-
|
|
1748
|
+
persistedEnv: environment.dotenvEnv,
|
|
1749
|
+
piAuthPath: nonInteractivePiAuthPath,
|
|
1750
|
+
...(args.codexAuthMode === undefined ? {} : { codexAuthMode: args.codexAuthMode }),
|
|
1751
|
+
abortSignal,
|
|
1752
|
+
})), { keypress: false });
|
|
1753
|
+
if (setup === "interrupted" || setup === "fatal") {
|
|
1754
|
+
return 130;
|
|
1755
|
+
}
|
|
735
1756
|
if (setup === "failed") {
|
|
736
1757
|
return 1;
|
|
737
1758
|
}
|
|
738
|
-
const result = await initMonoAgentFolder({
|
|
1759
|
+
const result = await initMonoAgentFolder({
|
|
1760
|
+
dir: cwd,
|
|
1761
|
+
answers,
|
|
1762
|
+
dryRun: args.dryRun,
|
|
1763
|
+
// Scaffold-only init has no immediate launchd-minimal worker proof. Reject
|
|
1764
|
+
// both shell and persisted memory-identity overrides instead of creating a
|
|
1765
|
+
// generation at a different path/tier than a follow-up validate would use.
|
|
1766
|
+
env: { ...environment.shellEnv, ...environment.dotenvEnv },
|
|
1767
|
+
});
|
|
739
1768
|
printInitResult(result);
|
|
740
|
-
printSecretsChecklist(result.plan.secrets);
|
|
1769
|
+
printSecretsChecklist(result.plan.secrets, new Set());
|
|
741
1770
|
printNextSteps(result.configPath);
|
|
742
1771
|
return 0;
|
|
743
1772
|
}
|
|
1773
|
+
const FIRST_RUN_STAGING_FAILURE_MAX_LENGTH = 500;
|
|
1774
|
+
const FIRST_RUN_SENSITIVE_ENV_NAME = /(api.?key|credential|password|secret|token)/iu;
|
|
1775
|
+
function throwIfFirstRunPreflightAborted(signal) {
|
|
1776
|
+
if (signal?.aborted !== true)
|
|
1777
|
+
return;
|
|
1778
|
+
const error = new Error("Preflight was interrupted.");
|
|
1779
|
+
error.name = "AbortError";
|
|
1780
|
+
throw error;
|
|
1781
|
+
}
|
|
1782
|
+
function firstRunStagingFailureDetail(error, sensitiveValues = []) {
|
|
1783
|
+
let message = reasonOf(error);
|
|
1784
|
+
for (const value of [...new Set(sensitiveValues)].filter((candidate) => candidate.length >= 4).sort((left, right) => right.length - left.length)) {
|
|
1785
|
+
message = message.replaceAll(value, "[secret-redacted]");
|
|
1786
|
+
}
|
|
1787
|
+
const normalized = message
|
|
1788
|
+
.replace(/\bBearer\s+[^\s,;]+/giu, "Bearer [secret-redacted]")
|
|
1789
|
+
.replace(/\b(api[ _-]?key|access[ _-]?token|auth[ _-]?token|password|secret)(\s*[=:]\s*)([^\s,;]+)/giu, (_match, label, separator) => `${label}${separator}[secret-redacted]`)
|
|
1790
|
+
.replace(/\s+/gu, " ")
|
|
1791
|
+
.trim();
|
|
1792
|
+
if (normalized.length === 0)
|
|
1793
|
+
return "Unknown staging failure.";
|
|
1794
|
+
return normalized.length <= FIRST_RUN_STAGING_FAILURE_MAX_LENGTH
|
|
1795
|
+
? normalized
|
|
1796
|
+
: `${normalized.slice(0, FIRST_RUN_STAGING_FAILURE_MAX_LENGTH - 1).trimEnd()}…`;
|
|
1797
|
+
}
|
|
1798
|
+
function firstRunStagingSensitiveValues(env, explicit) {
|
|
1799
|
+
return [
|
|
1800
|
+
...Object.entries(env)
|
|
1801
|
+
.filter((entry) => FIRST_RUN_SENSITIVE_ENV_NAME.test(entry[0])
|
|
1802
|
+
&& typeof entry[1] === "string"
|
|
1803
|
+
&& entry[1].length > 0)
|
|
1804
|
+
.map(([, value]) => value),
|
|
1805
|
+
...Object.values(explicit),
|
|
1806
|
+
];
|
|
1807
|
+
}
|
|
1808
|
+
async function runConfigurationPreflightWithSpinner(options) {
|
|
1809
|
+
process.stdout.write("\n" + ui.heading("Configuration preflight"));
|
|
1810
|
+
const spinner = p.spinner();
|
|
1811
|
+
try {
|
|
1812
|
+
return await withScopedPreflightCancellation(async (abortSignal) => {
|
|
1813
|
+
spinner.start("Validating generated files and selected capabilities before runtime calls");
|
|
1814
|
+
const gate = await assessPrewriteFirstRunConfigurationReadiness({ ...options, abortSignal });
|
|
1815
|
+
if (abortSignal.aborted || spinner.isCancelled) {
|
|
1816
|
+
spinner.cancel("Configuration preflight interrupted");
|
|
1817
|
+
return interruptedConfigurationAssessment();
|
|
1818
|
+
}
|
|
1819
|
+
if (gate.ready) {
|
|
1820
|
+
spinner.stop("Selected capabilities are ready for runtime checks");
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
spinner.error("Configuration preflight needs attention");
|
|
1824
|
+
}
|
|
1825
|
+
return gate;
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
if (!isAbortLike(error))
|
|
1830
|
+
throw error;
|
|
1831
|
+
spinner.cancel("Configuration preflight interrupted");
|
|
1832
|
+
return interruptedConfigurationAssessment();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function interruptedConfigurationAssessment() {
|
|
1836
|
+
return {
|
|
1837
|
+
ready: false,
|
|
1838
|
+
reasons: ["Configuration preflight was interrupted."],
|
|
1839
|
+
failedSectionIds: [],
|
|
1840
|
+
interrupted: true,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
async function runFinalReadinessValidationWithSpinner(options) {
|
|
1844
|
+
process.stdout.write("\n" + ui.heading("Final readiness validation"));
|
|
1845
|
+
const spinner = p.spinner();
|
|
1846
|
+
try {
|
|
1847
|
+
return await withScopedPreflightCancellation(async (abortSignal) => {
|
|
1848
|
+
spinner.start("Revalidating the effective files after runtime route checks");
|
|
1849
|
+
const gate = await assessPrewriteFirstRunReadiness({ ...options, abortSignal });
|
|
1850
|
+
if (abortSignal.aborted || spinner.isCancelled) {
|
|
1851
|
+
spinner.cancel("Final readiness validation interrupted");
|
|
1852
|
+
return interruptedFinalReadinessAssessment();
|
|
1853
|
+
}
|
|
1854
|
+
if (gate.ready)
|
|
1855
|
+
spinner.stop("Effective files and runtime routes are ready");
|
|
1856
|
+
else
|
|
1857
|
+
spinner.error("Final readiness validation needs attention");
|
|
1858
|
+
return gate;
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
if (!isAbortLike(error))
|
|
1863
|
+
throw error;
|
|
1864
|
+
spinner.cancel("Final readiness validation interrupted");
|
|
1865
|
+
return interruptedFinalReadinessAssessment();
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
function interruptedFinalReadinessAssessment() {
|
|
1869
|
+
return {
|
|
1870
|
+
ready: false,
|
|
1871
|
+
reasons: ["Final readiness validation was interrupted."],
|
|
1872
|
+
failedSectionIds: [],
|
|
1873
|
+
interrupted: true,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
async function assessPrewriteFirstRunConfigurationReadiness(options) {
|
|
1877
|
+
try {
|
|
1878
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1879
|
+
const preview = await initMonoAgentFolder({
|
|
1880
|
+
dir: options.cwd,
|
|
1881
|
+
answers: options.answers,
|
|
1882
|
+
env: options.env,
|
|
1883
|
+
secretValues: options.secretValues,
|
|
1884
|
+
secureExistingDotenv: options.secureExistingDotenv,
|
|
1885
|
+
dryRun: true,
|
|
1886
|
+
});
|
|
1887
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1888
|
+
const report = await validateWizardPlanInStaging({
|
|
1889
|
+
plan: options.plan,
|
|
1890
|
+
sourceCwd: options.cwd,
|
|
1891
|
+
env: options.env,
|
|
1892
|
+
verifiedCredentialModelRefs: [],
|
|
1893
|
+
...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
|
|
1894
|
+
});
|
|
1895
|
+
const gate = evaluateFirstRunConfigurationReadiness({
|
|
1896
|
+
plan: options.plan,
|
|
1897
|
+
report,
|
|
1898
|
+
secretPersistence: preview.secretPersistence,
|
|
1899
|
+
});
|
|
1900
|
+
return {
|
|
1901
|
+
...gate,
|
|
1902
|
+
failedSectionIds: configurationFailureSectionIds(options.plan, report, true),
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
catch (error) {
|
|
1906
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1907
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
1908
|
+
throw error;
|
|
1909
|
+
return {
|
|
1910
|
+
ready: false,
|
|
1911
|
+
reasons: [
|
|
1912
|
+
`The complete generated configuration could not be validated safely in staging: ${firstRunStagingFailureDetail(error, firstRunStagingSensitiveValues(options.env, options.secretValues))}`,
|
|
1913
|
+
],
|
|
1914
|
+
failedSectionIds: [],
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
async function assessPrewriteFirstRunReadiness(options) {
|
|
1919
|
+
try {
|
|
1920
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1921
|
+
const preview = await initMonoAgentFolder({
|
|
1922
|
+
dir: options.cwd,
|
|
1923
|
+
answers: options.answers,
|
|
1924
|
+
env: options.env,
|
|
1925
|
+
secretValues: options.secretValues,
|
|
1926
|
+
secureExistingDotenv: options.secureExistingDotenv,
|
|
1927
|
+
dryRun: true,
|
|
1928
|
+
});
|
|
1929
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1930
|
+
const report = await validateWizardPlanInStaging({
|
|
1931
|
+
plan: options.plan,
|
|
1932
|
+
sourceCwd: options.cwd,
|
|
1933
|
+
env: options.env,
|
|
1934
|
+
verifiedCredentialModelRefs: options.verifiedCredentialModelRefs,
|
|
1935
|
+
...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
|
|
1936
|
+
});
|
|
1937
|
+
const gate = evaluateFirstRunReadiness({
|
|
1938
|
+
plan: options.plan,
|
|
1939
|
+
report,
|
|
1940
|
+
secretPersistence: preview.secretPersistence,
|
|
1941
|
+
verifiedCredentialModelRefs: options.verifiedCredentialModelRefs,
|
|
1942
|
+
});
|
|
1943
|
+
return {
|
|
1944
|
+
...gate,
|
|
1945
|
+
failedSectionIds: configurationFailureSectionIds(options.plan, report, false),
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
catch (error) {
|
|
1949
|
+
throwIfFirstRunPreflightAborted(options.abortSignal);
|
|
1950
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
1951
|
+
throw error;
|
|
1952
|
+
return {
|
|
1953
|
+
ready: false,
|
|
1954
|
+
reasons: [
|
|
1955
|
+
`The complete generated plan could not be validated safely in staging: ${firstRunStagingFailureDetail(error, firstRunStagingSensitiveValues(options.env, options.secretValues))}`,
|
|
1956
|
+
],
|
|
1957
|
+
failedSectionIds: [],
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function configurationFailureSectionIds(plan, report, deferWaitingCredentials) {
|
|
1962
|
+
const byId = new Map(report.sections.map((section) => [section.id, section]));
|
|
1963
|
+
const ids = new Set();
|
|
1964
|
+
for (const expectation of plan.validateExpectations) {
|
|
1965
|
+
const actual = byId.get(expectation.sectionId)?.status;
|
|
1966
|
+
if (actual === expectation.mustBe
|
|
1967
|
+
|| (deferWaitingCredentials && expectation.sectionId === "credentials" && actual === "waiting"))
|
|
1968
|
+
continue;
|
|
1969
|
+
ids.add(expectation.sectionId);
|
|
1970
|
+
}
|
|
1971
|
+
if (!report.ok) {
|
|
1972
|
+
for (const section of report.sections) {
|
|
1973
|
+
if (section.status === "error")
|
|
1974
|
+
ids.add(section.id);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return [...ids];
|
|
1978
|
+
}
|
|
1979
|
+
function nonEmptyEnv(value) {
|
|
1980
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1981
|
+
}
|
|
1982
|
+
function sameConcreteEnvironment(left, right) {
|
|
1983
|
+
const concreteEntries = (env) => Object.entries(env)
|
|
1984
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
1985
|
+
.sort(([leftName], [rightName]) => leftName.localeCompare(rightName));
|
|
1986
|
+
const leftEntries = concreteEntries(left);
|
|
1987
|
+
const rightEntries = concreteEntries(right);
|
|
1988
|
+
return leftEntries.length === rightEntries.length && leftEntries.every(([name, value], index) => rightEntries[index]?.[0] === name && rightEntries[index]?.[1] === value);
|
|
1989
|
+
}
|
|
1990
|
+
function dotenvReadinessFailure(message) {
|
|
1991
|
+
return { ok: false, kind: "invalid_plan", message };
|
|
1992
|
+
}
|
|
1993
|
+
async function firstRunDotenvDrift(path, expected) {
|
|
1994
|
+
let current;
|
|
1995
|
+
try {
|
|
1996
|
+
current = await readCliDotenvSnapshot(path);
|
|
1997
|
+
}
|
|
1998
|
+
catch {
|
|
1999
|
+
return dotenvReadinessFailure("The persisted .env became unreadable during setup. Readiness cannot be claimed.");
|
|
2000
|
+
}
|
|
2001
|
+
if (current.fingerprint === expected.fingerprint)
|
|
2002
|
+
return undefined;
|
|
2003
|
+
return dotenvReadinessFailure("The persisted .env changed while setup was validating the agent. Review the change, then retry so the exact durable values can be checked.");
|
|
2004
|
+
}
|
|
2005
|
+
async function firstRunConfigDrift(path, expected) {
|
|
2006
|
+
let current;
|
|
2007
|
+
try {
|
|
2008
|
+
current = await readCliConfigSnapshot(path);
|
|
2009
|
+
}
|
|
2010
|
+
catch {
|
|
2011
|
+
return dotenvReadinessFailure("The committed config became unreadable or unsafe during setup. Readiness cannot be claimed.");
|
|
2012
|
+
}
|
|
2013
|
+
if (current.fingerprint === expected.fingerprint)
|
|
2014
|
+
return undefined;
|
|
2015
|
+
return dotenvReadinessFailure("The committed config changed while setup was validating the agent. Review the change, then retry so the exact plan can be checked.");
|
|
2016
|
+
}
|
|
2017
|
+
async function firstRunSecretEnvGuardFailure(path, required) {
|
|
2018
|
+
if (!required)
|
|
2019
|
+
return undefined;
|
|
2020
|
+
try {
|
|
2021
|
+
if (await verifySecretEnvPersistenceGuard(path))
|
|
2022
|
+
return undefined;
|
|
2023
|
+
}
|
|
2024
|
+
catch {
|
|
2025
|
+
// Fall through to one stable, non-secret-bearing operator message.
|
|
2026
|
+
}
|
|
2027
|
+
return dotenvReadinessFailure("The committed .env is no longer owner-only, safely ignored, and untracked. Readiness cannot be claimed.");
|
|
2028
|
+
}
|
|
2029
|
+
function secretPersistenceRecoveryMessage(error) {
|
|
2030
|
+
if (!(error instanceof SecretEnvConcurrentModificationError)) {
|
|
2031
|
+
return "";
|
|
2032
|
+
}
|
|
2033
|
+
return ` ${error.message}`;
|
|
2034
|
+
}
|
|
2035
|
+
function readinessPlanIdentity(plan) {
|
|
2036
|
+
const displayed = readinessRoutesForDisplay(plan);
|
|
2037
|
+
const immutable = displayed.map((route, index) => ({
|
|
2038
|
+
index,
|
|
2039
|
+
model: route.model,
|
|
2040
|
+
effort: route.effort ?? null,
|
|
2041
|
+
}));
|
|
2042
|
+
return {
|
|
2043
|
+
fingerprint: createHash("sha256")
|
|
2044
|
+
.update(JSON.stringify({ version: 1, routes: immutable }))
|
|
2045
|
+
.digest("hex"),
|
|
2046
|
+
routes: displayed.map((route, index) => ({
|
|
2047
|
+
index,
|
|
2048
|
+
...route,
|
|
2049
|
+
key: createHash("sha256")
|
|
2050
|
+
.update(JSON.stringify({ version: 1, index, model: route.model, effort: route.effort ?? null }))
|
|
2051
|
+
.digest("hex"),
|
|
2052
|
+
})),
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function mergeReadinessProgress(previous, result, plan) {
|
|
2056
|
+
const identity = readinessPlanIdentity(plan);
|
|
2057
|
+
const fingerprint = result.planFingerprint ?? identity.fingerprint;
|
|
2058
|
+
const successfulKeys = new Set(previous?.planFingerprint === fingerprint ? previous.successfulRouteKeys : []);
|
|
2059
|
+
const verifiedRefs = new Set(previous?.planFingerprint === fingerprint ? previous.verifiedModelRefs : []);
|
|
2060
|
+
const reported = result.routes ?? (result.ok
|
|
2061
|
+
? identity.routes.map((route) => ({ ...route, status: "verified" }))
|
|
2062
|
+
: []);
|
|
2063
|
+
for (const route of reported) {
|
|
2064
|
+
if (route.status === "verified" || route.status === "skipped_verified") {
|
|
2065
|
+
successfulKeys.add(route.key);
|
|
2066
|
+
verifiedRefs.add(route.model);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
const currentRefs = new Set(identity.routes.map((route) => route.model));
|
|
2070
|
+
return {
|
|
2071
|
+
planFingerprint: fingerprint,
|
|
2072
|
+
successfulRouteKeys: [...successfulKeys],
|
|
2073
|
+
verifiedModelRefs: [...verifiedRefs].filter((ref) => currentRefs.has(ref)),
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
async function runGuidedSandboxPreflight(installedEarlier) {
|
|
2077
|
+
process.stdout.write("\n" + ui.heading("Sandbox preflight"));
|
|
2078
|
+
return await withScopedPreflightCancellation(async (signal) => {
|
|
2079
|
+
try {
|
|
2080
|
+
process.stdout.write(ui.style.dim(installedEarlier
|
|
2081
|
+
? "Rechecking the pinned managed SRT copy and its functional enforcement postcondition.\n"
|
|
2082
|
+
: "Installing the pinned managed SRT copy in the private user cache, then running the functional enforcement check.\n"));
|
|
2083
|
+
const setup = await setupManagedSrt({ signal, verify: true });
|
|
2084
|
+
if (setup.status.source !== "managed" || setup.status.state !== "ready" || setup.check === undefined) {
|
|
2085
|
+
throw new Error("Managed SRT setup did not return a ready managed functional-check result.");
|
|
2086
|
+
}
|
|
2087
|
+
process.stdout.write(`${ui.badge("ok")}Managed SRT ${setup.repaired ? "repaired" : setup.installed ? "installed" : "verified"}; functional postcondition passed.\n`);
|
|
2088
|
+
return { ok: true };
|
|
2089
|
+
}
|
|
2090
|
+
catch (error) {
|
|
2091
|
+
if (signal.aborted || isAbortLike(error)) {
|
|
2092
|
+
process.stderr.write(ui.errorLine("Preflight was interrupted."));
|
|
2093
|
+
return {
|
|
2094
|
+
ok: false,
|
|
2095
|
+
kind: "cancelled",
|
|
2096
|
+
message: "Sandbox preflight was interrupted. No agent files were written.",
|
|
2097
|
+
interrupted: true,
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
ok: false,
|
|
2102
|
+
kind: "provider_failed",
|
|
2103
|
+
message: `[sandbox_preflight_failed] ${reasonOf(error)} No agent files were written; retry setup or edit the sandbox choice.`,
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
async function runReadinessProbeWithSpinner(options) {
|
|
2109
|
+
const routes = readinessRoutesForDisplay(options.plan);
|
|
2110
|
+
process.stdout.write("\n" + ui.heading("Runtime readiness"));
|
|
2111
|
+
routes.forEach((route, index) => {
|
|
2112
|
+
const timeoutMs = readinessProbeTimeoutMs(parseMonoRuntimeModelReference(route.model));
|
|
2113
|
+
process.stdout.write(` Route ${index + 1}/${routes.length}: ${route.model} ` +
|
|
2114
|
+
ui.style.dim(`(effort: ${route.effort ?? "provider-default"}; up to ${Math.ceil(timeoutMs / 1_000)}s)`) +
|
|
2115
|
+
"\n");
|
|
2116
|
+
});
|
|
2117
|
+
process.stdout.write(ui.style.dim("Running real no-tool checks sequentially. Press Esc or Ctrl-C once to interrupt safely.\n"));
|
|
2118
|
+
return await withScopedPreflightCancellation(async (signal) => {
|
|
2119
|
+
try {
|
|
2120
|
+
const result = await runAllRouteReadinessProbe({
|
|
2121
|
+
plan: options.plan,
|
|
2122
|
+
hostEnv: options.effectiveEnv,
|
|
2123
|
+
secretValues: selectedSecretValues(options.plan, options.effectiveEnv),
|
|
2124
|
+
resolvedPiAuthPath: options.resolvedPiAuthPath,
|
|
2125
|
+
abortSignal: signal,
|
|
2126
|
+
...(options.resume === undefined ? {} : { resume: options.resume }),
|
|
2127
|
+
onRouteStart: (route) => {
|
|
2128
|
+
process.stdout.write(` Checking route ${route.index + 1}/${route.total}: ${route.model} ` +
|
|
2129
|
+
ui.style.dim(`(effort: ${route.effort ?? "provider-default"})`) +
|
|
2130
|
+
"\n");
|
|
2131
|
+
},
|
|
2132
|
+
onRouteComplete: (route) => {
|
|
2133
|
+
const ok = route.status === "verified" || route.status === "skipped_verified";
|
|
2134
|
+
process.stdout.write(`${ok ? ui.badge("ok") : route.status === "interrupted" ? ui.badge("waiting") : ui.badge("error")}` +
|
|
2135
|
+
`Route ${route.index + 1}/${routes.length} ${route.status.replaceAll("_", " ")}\n`);
|
|
2136
|
+
},
|
|
2137
|
+
});
|
|
2138
|
+
process.stdout.write(ui.heading("Readiness summary"));
|
|
2139
|
+
printReadinessRouteSummary(result, routes);
|
|
2140
|
+
return result;
|
|
2141
|
+
}
|
|
2142
|
+
catch (error) {
|
|
2143
|
+
if (signal.aborted || isAbortLike(error)) {
|
|
2144
|
+
process.stderr.write(ui.errorLine("Preflight was interrupted."));
|
|
2145
|
+
return {
|
|
2146
|
+
ok: false,
|
|
2147
|
+
kind: "cancelled",
|
|
2148
|
+
message: "Preflight was interrupted before the current route completed.",
|
|
2149
|
+
interrupted: true,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
process.stderr.write(ui.errorLine("[readiness_probe_failed] Runtime readiness could not run."));
|
|
2153
|
+
return {
|
|
2154
|
+
ok: false,
|
|
2155
|
+
kind: "probe_failed",
|
|
2156
|
+
message: "Runtime readiness could not run. Review provider authentication and retry.",
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function readinessRoutesForDisplay(plan) {
|
|
2162
|
+
const runtime = (plan.configJson.runtime ?? {});
|
|
2163
|
+
const primaryEffort = typeof runtime.effort === "string" ? runtime.effort : undefined;
|
|
2164
|
+
const routes = [];
|
|
2165
|
+
if (typeof runtime.model === "string") {
|
|
2166
|
+
routes.push({ model: runtime.model, ...(primaryEffort === undefined ? {} : { effort: primaryEffort }) });
|
|
2167
|
+
}
|
|
2168
|
+
if (Array.isArray(runtime.fallbacks) && runtime.fallbacks.length > 0) {
|
|
2169
|
+
for (const raw of runtime.fallbacks) {
|
|
2170
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
2171
|
+
continue;
|
|
2172
|
+
const entry = raw;
|
|
2173
|
+
if (typeof entry.model !== "string")
|
|
2174
|
+
continue;
|
|
2175
|
+
routes.push({
|
|
2176
|
+
model: entry.model,
|
|
2177
|
+
...(typeof entry.effort === "string" ? { effort: entry.effort } : {}),
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
else if (Array.isArray(runtime.fallbackModels)) {
|
|
2182
|
+
for (const model of runtime.fallbackModels) {
|
|
2183
|
+
if (typeof model === "string") {
|
|
2184
|
+
routes.push({ model, ...(primaryEffort === undefined ? {} : { effort: primaryEffort }) });
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return routes;
|
|
2189
|
+
}
|
|
2190
|
+
function printReadinessRouteSummary(result, planned) {
|
|
2191
|
+
const reported = result.routes ?? planned.map((route, index) => ({
|
|
2192
|
+
key: `${index}:${route.model}`,
|
|
2193
|
+
index,
|
|
2194
|
+
...route,
|
|
2195
|
+
status: result.ok ? "verified" : result.kind === "cancelled" ? "interrupted" : "failed",
|
|
2196
|
+
...(!result.ok ? { kind: result.kind, message: result.message } : {}),
|
|
2197
|
+
}));
|
|
2198
|
+
for (const route of reported) {
|
|
2199
|
+
const badge = route.status === "verified" || route.status === "skipped_verified"
|
|
2200
|
+
? ui.badge("ok")
|
|
2201
|
+
: route.status === "interrupted"
|
|
2202
|
+
? ui.badge("waiting")
|
|
2203
|
+
: ui.badge("error");
|
|
2204
|
+
const state = route.status === "skipped_verified" ? "verified earlier" : route.status.replaceAll("_", " ");
|
|
2205
|
+
process.stdout.write(`${badge}Route ${route.index + 1}/${planned.length}: ${route.model} ` +
|
|
2206
|
+
ui.style.dim(`(effort: ${route.effort ?? "provider-default"})`) +
|
|
2207
|
+
` — ${state}${route.message === undefined ? "" : `: ${route.message}`}\n`);
|
|
2208
|
+
}
|
|
2209
|
+
if (!result.ok && result.interrupted === true) {
|
|
2210
|
+
process.stderr.write(ui.errorLine("Preflight was interrupted."));
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async function withScopedPreflightCancellation(task, options = {}) {
|
|
2214
|
+
const controller = new AbortController();
|
|
2215
|
+
let interruptCount = 0;
|
|
2216
|
+
const interrupt = () => {
|
|
2217
|
+
interruptCount += 1;
|
|
2218
|
+
controller.abort();
|
|
2219
|
+
if (interruptCount > 1)
|
|
2220
|
+
process.exitCode = 130;
|
|
2221
|
+
};
|
|
2222
|
+
const onKeypress = (_value, key) => {
|
|
2223
|
+
if (key?.name === "escape" || (key?.ctrl === true && key.name === "c"))
|
|
2224
|
+
interrupt();
|
|
2225
|
+
};
|
|
2226
|
+
process.on("SIGINT", interrupt);
|
|
2227
|
+
const restoreKeypress = options.keypress === false
|
|
2228
|
+
? () => undefined
|
|
2229
|
+
: attachScopedKeypress(onKeypress);
|
|
2230
|
+
try {
|
|
2231
|
+
return await task(controller.signal);
|
|
2232
|
+
}
|
|
2233
|
+
finally {
|
|
2234
|
+
process.off("SIGINT", interrupt);
|
|
2235
|
+
restoreKeypress();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function focusedConfigurationRepairStep(sectionIds) {
|
|
2239
|
+
const mapped = new Set();
|
|
2240
|
+
for (const id of sectionIds) {
|
|
2241
|
+
if (id === "agent")
|
|
2242
|
+
mapped.add(0);
|
|
2243
|
+
else if (id === "runtime" || id === "credentials")
|
|
2244
|
+
mapped.add(1);
|
|
2245
|
+
else if (id === "memory" || id.startsWith("memory:"))
|
|
2246
|
+
mapped.add(3);
|
|
2247
|
+
else if (id === "context" || id.startsWith("channel:"))
|
|
2248
|
+
mapped.add(4);
|
|
2249
|
+
else if (id === "tools")
|
|
2250
|
+
mapped.add(5);
|
|
2251
|
+
else if (id === "sandbox")
|
|
2252
|
+
mapped.add(6);
|
|
2253
|
+
else if (id === "observability")
|
|
2254
|
+
mapped.add(7);
|
|
2255
|
+
}
|
|
2256
|
+
return mapped.size === 1 ? [...mapped][0] : undefined;
|
|
2257
|
+
}
|
|
2258
|
+
function configurationRecoveryEditLabel(step) {
|
|
2259
|
+
switch (step) {
|
|
2260
|
+
case 0: return "Edit agent name";
|
|
2261
|
+
case 1: return "Edit model routes";
|
|
2262
|
+
case 3: return "Edit memory";
|
|
2263
|
+
case 4: return "Edit capability details";
|
|
2264
|
+
case 5: return "Edit tools";
|
|
2265
|
+
case 6: return "Edit route safety and sandbox";
|
|
2266
|
+
case 7: return "Edit observability";
|
|
2267
|
+
default: return "Edit setup choices";
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async function selectSandboxPreflightRecovery() {
|
|
2271
|
+
const recovery = await p.select({
|
|
2272
|
+
message: "Sandbox preflight did not pass. How would you like to recover?",
|
|
2273
|
+
initialValue: "retry",
|
|
2274
|
+
options: [
|
|
2275
|
+
{ value: "retry", label: "Retry sandbox setup and check" },
|
|
2276
|
+
{ value: "edit", label: "Change safety or other choices" },
|
|
2277
|
+
{ value: "cancel", label: "Cancel without writing" },
|
|
2278
|
+
],
|
|
2279
|
+
});
|
|
2280
|
+
return p.isCancel(recovery) ? "cancel" : recovery;
|
|
2281
|
+
}
|
|
2282
|
+
async function selectInterruptedFirstRunRecovery() {
|
|
2283
|
+
const recovery = await p.select({
|
|
2284
|
+
message: "Preflight was interrupted. What would you like to do?",
|
|
2285
|
+
initialValue: "resume",
|
|
2286
|
+
options: [
|
|
2287
|
+
{ value: "resume", label: "Resume preflight", hint: "keeps successful auth, SRT setup, and route checks" },
|
|
2288
|
+
{ value: "restart", label: "Restart all checks", hint: "keeps successful auth and SRT installation" },
|
|
2289
|
+
{ value: "edit", label: "Edit setup choices" },
|
|
2290
|
+
{ value: "cancel", label: "Cancel without writing" },
|
|
2291
|
+
],
|
|
2292
|
+
});
|
|
2293
|
+
return p.isCancel(recovery) ? "cancel" : recovery;
|
|
2294
|
+
}
|
|
2295
|
+
async function selectFirstRunRecovery(failure, configurationRepairStep, invalidPlanStage) {
|
|
2296
|
+
const sharedTail = [
|
|
2297
|
+
{ value: "save", label: "Save incomplete", hint: "does not call the agent ready or start it" },
|
|
2298
|
+
{ value: "cancel", label: "Cancel without writing" },
|
|
2299
|
+
];
|
|
2300
|
+
const providerSetupFailed = failure.kind === "provider_failed"
|
|
2301
|
+
&& /^Provider setup (?:did not complete|still needs attention)\./u.test(failure.message);
|
|
2302
|
+
const message = failure.kind === "invalid_plan"
|
|
2303
|
+
? invalidPlanStage === "final_readiness"
|
|
2304
|
+
? "Final readiness validation did not pass. What would you like to do?"
|
|
2305
|
+
: "Configuration preflight did not pass. What would you like to do?"
|
|
2306
|
+
: providerSetupFailed
|
|
2307
|
+
? "Provider setup did not pass. What would you like to do?"
|
|
2308
|
+
: "Runtime readiness did not pass. What would you like to do?";
|
|
2309
|
+
const options = failure.kind === "invalid_plan"
|
|
2310
|
+
? [
|
|
2311
|
+
{ value: "edit", label: configurationRecoveryEditLabel(configurationRepairStep) },
|
|
2312
|
+
{
|
|
2313
|
+
value: "retry",
|
|
2314
|
+
label: invalidPlanStage === "final_readiness"
|
|
2315
|
+
? "Retry final readiness validation"
|
|
2316
|
+
: "Retry configuration preflight",
|
|
2317
|
+
},
|
|
2318
|
+
...sharedTail,
|
|
2319
|
+
]
|
|
2320
|
+
: providerSetupFailed
|
|
2321
|
+
? [
|
|
2322
|
+
{ value: "auth", label: "Repair authentication" },
|
|
2323
|
+
{ value: "retry", label: "Retry provider setup" },
|
|
2324
|
+
{ value: "model", label: "Edit model routes" },
|
|
2325
|
+
...sharedTail,
|
|
2326
|
+
]
|
|
2327
|
+
: failure.kind === "provider_failed"
|
|
2328
|
+
? [
|
|
2329
|
+
{ value: "retry", label: "Retry failed route" },
|
|
2330
|
+
{ value: "auth", label: "Repair authentication" },
|
|
2331
|
+
{ value: "model", label: "Edit model routes" },
|
|
2332
|
+
...sharedTail,
|
|
2333
|
+
]
|
|
2334
|
+
: failure.kind === "unsupported_guided_probe"
|
|
2335
|
+
? [
|
|
2336
|
+
{ value: "model", label: "Edit model routes" },
|
|
2337
|
+
...sharedTail,
|
|
2338
|
+
]
|
|
2339
|
+
: [
|
|
2340
|
+
{ value: "retry", label: "Retry runtime checks" },
|
|
2341
|
+
{ value: "model", label: "Edit model routes" },
|
|
2342
|
+
...sharedTail,
|
|
2343
|
+
];
|
|
2344
|
+
const recovery = await p.select({
|
|
2345
|
+
message,
|
|
2346
|
+
initialValue: options[0]?.value ?? "cancel",
|
|
2347
|
+
options: [...options],
|
|
2348
|
+
});
|
|
2349
|
+
return p.isCancel(recovery) ? "cancel" : recovery;
|
|
2350
|
+
}
|
|
2351
|
+
async function promptProviderSetupSecrets(plan, existing, existingPersistence = {}, existingEnvironmentSecrets = {}) {
|
|
2352
|
+
const values = { ...existing };
|
|
2353
|
+
const persistenceByProvider = { ...existingPersistence };
|
|
2354
|
+
const environmentSecrets = { ...existingEnvironmentSecrets };
|
|
2355
|
+
for (const action of plan.actions) {
|
|
2356
|
+
if (!isProviderSetupPiApiKeyAction(action))
|
|
2357
|
+
continue;
|
|
2358
|
+
const reviewedPersistence = existingPersistence[action.provider];
|
|
2359
|
+
const persistence = reviewedPersistence ?? await p.select({
|
|
2360
|
+
message: `How should ${action.label} receive ${action.envVar}?`,
|
|
2361
|
+
initialValue: "secure-store",
|
|
2362
|
+
options: [
|
|
2363
|
+
{ value: "secure-store", label: "Store securely in Pi auth.json", hint: "owner-only credential store" },
|
|
2364
|
+
{ value: "environment", label: `Use environment variable ${action.envVar}`, hint: "save it to the agent's owner-only .env" },
|
|
2365
|
+
],
|
|
2366
|
+
});
|
|
2367
|
+
if (p.isCancel(persistence))
|
|
2368
|
+
return undefined;
|
|
2369
|
+
if (persistence === "environment") {
|
|
2370
|
+
delete values[action.id];
|
|
2371
|
+
persistenceByProvider[action.provider] = "environment";
|
|
2372
|
+
const answer = await p.password({
|
|
2373
|
+
message: `Enter ${action.envVar} for the agent's owner-only .env`,
|
|
2374
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
|
|
2375
|
+
clearOnError: true,
|
|
2376
|
+
});
|
|
2377
|
+
if (p.isCancel(answer))
|
|
2378
|
+
return undefined;
|
|
2379
|
+
environmentSecrets[action.envVar] = answer;
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
persistenceByProvider[action.provider] = "secure-store";
|
|
2383
|
+
delete environmentSecrets[action.envVar];
|
|
2384
|
+
const answer = await p.password({
|
|
2385
|
+
message: `Enter ${action.label} (${action.envVar})`,
|
|
2386
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
|
|
2387
|
+
clearOnError: true,
|
|
2388
|
+
});
|
|
2389
|
+
if (p.isCancel(answer))
|
|
2390
|
+
return undefined;
|
|
2391
|
+
values[action.id] = answer;
|
|
2392
|
+
}
|
|
2393
|
+
return { apiKeys: values, persistenceByProvider, environmentSecrets };
|
|
2394
|
+
}
|
|
2395
|
+
function environmentProviderApiKeys(plan, env) {
|
|
2396
|
+
const values = {};
|
|
2397
|
+
for (const action of plan.actions) {
|
|
2398
|
+
if (!isProviderSetupPiApiKeyAction(action) || action.persistence !== "environment")
|
|
2399
|
+
continue;
|
|
2400
|
+
const value = env[action.envVar];
|
|
2401
|
+
if (nonEmptyEnv(value))
|
|
2402
|
+
values[action.id] = value;
|
|
2403
|
+
}
|
|
2404
|
+
return values;
|
|
2405
|
+
}
|
|
2406
|
+
async function selectCodexAuthMode(initialValue) {
|
|
2407
|
+
const selected = await p.select({
|
|
2408
|
+
message: "How should Codex authenticate on this machine?",
|
|
2409
|
+
initialValue,
|
|
2410
|
+
options: [
|
|
2411
|
+
{ value: "browser", label: "Browser login", hint: "opens a localhost callback server" },
|
|
2412
|
+
{ value: "device", label: "Device-code login", hint: "recommended for remote or headless machines" },
|
|
2413
|
+
],
|
|
2414
|
+
});
|
|
2415
|
+
return p.isCancel(selected) ? undefined : selected;
|
|
2416
|
+
}
|
|
2417
|
+
function configuredSecretNames(result, effectiveEnv) {
|
|
2418
|
+
const names = new Set();
|
|
2419
|
+
for (const secret of result.plan.secrets) {
|
|
2420
|
+
if (nonEmptyEnv(effectiveEnv[secret.envVar]))
|
|
2421
|
+
names.add(secret.envVar);
|
|
2422
|
+
}
|
|
2423
|
+
return names;
|
|
2424
|
+
}
|
|
2425
|
+
function printIncompleteSetup(reasons, configPath) {
|
|
2426
|
+
process.stderr.write(ui.hint("INCOMPLETE SETUP: no readiness claim was made and the agent was not started.\n"));
|
|
2427
|
+
for (const reason of reasons)
|
|
2428
|
+
process.stderr.write(ui.style.yellow(` - ${reason}\n`));
|
|
2429
|
+
process.stderr.write(ui.hint(`Review ${configPath}, run \`mono-agent validate\`, then retry the first turn.\n`));
|
|
2430
|
+
}
|
|
2431
|
+
export function shouldRunInitWizard(args, stdinIsTty, stdoutIsTty) {
|
|
2432
|
+
if (!stdinIsTty || !stdoutIsTty || args.command !== "init" || args.positionals.length > 0) {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
if (args.force || args.foreground || args.follow || args.all || args.dryRun || args.includeMemory) {
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
// A bare parsed init has only these required/default keys. Treat every
|
|
2439
|
+
// optional key—current or future—as an overriding flag so the documented
|
|
2440
|
+
// "any flag is scaffold-only" contract cannot silently drift again.
|
|
2441
|
+
const bareKeys = new Set([
|
|
2442
|
+
"command",
|
|
2443
|
+
"positionals",
|
|
2444
|
+
"force",
|
|
2445
|
+
"foreground",
|
|
2446
|
+
"follow",
|
|
2447
|
+
"all",
|
|
2448
|
+
"dryRun",
|
|
2449
|
+
"includeMemory",
|
|
2450
|
+
]);
|
|
2451
|
+
return Object.keys(args).every((key) => bareKeys.has(key));
|
|
2452
|
+
}
|
|
744
2453
|
export async function runProviderSetupBeforeInit(options) {
|
|
745
|
-
const
|
|
2454
|
+
const credentialStates = options.credentialStates !== undefined
|
|
2455
|
+
? options.credentialStates
|
|
2456
|
+
: options.forceAuthentication === true || !options.auth || options.dryRun
|
|
2457
|
+
? undefined
|
|
2458
|
+
: await detectProviderCredentialStates({
|
|
2459
|
+
modelRefs: options.modelRefs,
|
|
2460
|
+
cwd: options.cwd,
|
|
2461
|
+
...(options.piAuthPath === undefined ? {} : { piAuthPath: options.piAuthPath }),
|
|
2462
|
+
...(options.persistedEnv === undefined ? {} : { persistedEnv: options.persistedEnv }),
|
|
2463
|
+
...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
|
|
2464
|
+
});
|
|
2465
|
+
const plan = planProviderSetup({
|
|
2466
|
+
...options,
|
|
2467
|
+
...(credentialStates === undefined ? {} : { credentialStates }),
|
|
2468
|
+
...(options.codexAuthMode === undefined ? {} : { codexAuthMode: options.codexAuthMode }),
|
|
2469
|
+
...(options.forceAuthentication === undefined ? {} : { forceAuthentication: options.forceAuthentication }),
|
|
2470
|
+
});
|
|
746
2471
|
if (plan.actions.length === 0) {
|
|
747
2472
|
return "skipped";
|
|
748
2473
|
}
|
|
@@ -757,23 +2482,191 @@ export async function runProviderSetupBeforeInit(options) {
|
|
|
757
2482
|
}
|
|
758
2483
|
process.stdout.write("\n" + ui.heading("Provider setup"));
|
|
759
2484
|
printProviderSetupPlan(plan);
|
|
2485
|
+
if (options.abortSignal !== undefined) {
|
|
2486
|
+
process.stdout.write(ui.style.dim("Press Ctrl-C once to interrupt authentication safely.\n"));
|
|
2487
|
+
}
|
|
760
2488
|
const results = await (options.execute ?? ((setupPlan) => executeProviderSetupPlan(setupPlan, {
|
|
761
2489
|
...(options.apiKeys === undefined ? {} : { apiKeys: options.apiKeys }),
|
|
2490
|
+
...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
|
|
762
2491
|
})))(plan);
|
|
2492
|
+
const interrupted = options.abortSignal?.aborted === true;
|
|
763
2493
|
for (const result of results) {
|
|
764
|
-
const badge = result.status === "
|
|
765
|
-
? ui.badge("
|
|
766
|
-
: result.status === "
|
|
767
|
-
? ui.
|
|
768
|
-
:
|
|
2494
|
+
const badge = interrupted && result.status === "failed"
|
|
2495
|
+
? ui.badge("waiting")
|
|
2496
|
+
: result.status === "ok"
|
|
2497
|
+
? ui.badge("ok")
|
|
2498
|
+
: result.status === "skipped"
|
|
2499
|
+
? ui.style.dim("- ")
|
|
2500
|
+
: ui.badge("error");
|
|
769
2501
|
process.stdout.write(`${badge}${result.action.label}: ${result.detail}\n`);
|
|
770
2502
|
}
|
|
2503
|
+
if (results.some((result) => result.failureKind !== undefined)) {
|
|
2504
|
+
process.stderr.write(ui.errorLine("Provider setup ended in an unconfirmed process or credential-cleanup state. Follow the reported manual cleanup guidance before retrying; automatic recovery is disabled."));
|
|
2505
|
+
return "fatal";
|
|
2506
|
+
}
|
|
2507
|
+
if (interrupted) {
|
|
2508
|
+
process.stderr.write(ui.errorLine("Provider setup was interrupted."));
|
|
2509
|
+
return "interrupted";
|
|
2510
|
+
}
|
|
771
2511
|
if (results.some((result) => result.status === "failed")) {
|
|
772
2512
|
process.stderr.write(ui.errorLine("Provider setup failed; init stopped before writing files."));
|
|
773
2513
|
return "failed";
|
|
774
2514
|
}
|
|
775
2515
|
return "ok";
|
|
776
2516
|
}
|
|
2517
|
+
async function runAuth(args) {
|
|
2518
|
+
const [subcommand, provider, ...extra] = args.positionals;
|
|
2519
|
+
if (subcommand !== "login" || provider === undefined || extra.length > 0) {
|
|
2520
|
+
process.stderr.write(ui.errorLine("Usage: mono-agent auth login <provider|codex> [--pi-auth-path <path>] [--api-key-stdin] [--codex-auth browser|device] [--config <path>]."));
|
|
2521
|
+
return 2;
|
|
2522
|
+
}
|
|
2523
|
+
const cwd = process.cwd();
|
|
2524
|
+
const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
|
|
2525
|
+
const directCodex = provider === "codex";
|
|
2526
|
+
if (directCodex && args.piAuthPath !== undefined) {
|
|
2527
|
+
process.stderr.write(ui.errorLine("--pi-auth-path does not apply to direct Codex login."));
|
|
2528
|
+
return 2;
|
|
2529
|
+
}
|
|
2530
|
+
let configuredPiAuthPath;
|
|
2531
|
+
try {
|
|
2532
|
+
configuredPiAuthPath = directCodex ? resolve(cwd, ".pi", "auth.json") : await resolvePiAuthPathForLogin({
|
|
2533
|
+
configPath,
|
|
2534
|
+
cwd,
|
|
2535
|
+
...(process.env.MONO_AGENT_PI_AUTH_PATH === undefined
|
|
2536
|
+
? {}
|
|
2537
|
+
: { envPath: process.env.MONO_AGENT_PI_AUTH_PATH }),
|
|
2538
|
+
...(args.piAuthPath === undefined ? {} : { piAuthPath: args.piAuthPath }),
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
catch (error) {
|
|
2542
|
+
process.stderr.write(ui.errorLine(`Cannot resolve the Pi auth path from ${configPath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
2543
|
+
return 1;
|
|
2544
|
+
}
|
|
2545
|
+
const plan = planProviderSetup({
|
|
2546
|
+
modelRefs: [directCodex ? "codex:gpt-5.6-terra" : `pi:${provider}:credential-setup`],
|
|
2547
|
+
cwd,
|
|
2548
|
+
piAuthPath: configuredPiAuthPath,
|
|
2549
|
+
forceAuthentication: true,
|
|
2550
|
+
...(args.codexAuthMode === undefined ? {} : { codexAuthMode: args.codexAuthMode }),
|
|
2551
|
+
});
|
|
2552
|
+
if (plan.actions.length === 0) {
|
|
2553
|
+
process.stderr.write(ui.errorLine(`Provider \`${provider}\` has no interactive auth method in the bundled ${directCodex ? "Codex" : "Pi"} provider catalog.`));
|
|
2554
|
+
return 2;
|
|
2555
|
+
}
|
|
2556
|
+
const apiKeyActions = plan.actions.filter(isProviderSetupPiApiKeyAction);
|
|
2557
|
+
if (args.apiKeyStdin === true && apiKeyActions.length !== 1) {
|
|
2558
|
+
process.stderr.write(ui.errorLine("--api-key-stdin is only supported when the selected provider has one bundled API-key login action."));
|
|
2559
|
+
return 2;
|
|
2560
|
+
}
|
|
2561
|
+
process.stdout.write("\n" + ui.heading(directCodex ? "Codex authentication" : "Pi authentication"));
|
|
2562
|
+
printProviderSetupPlan(plan);
|
|
2563
|
+
let apiKeys;
|
|
2564
|
+
const apiKeyAction = apiKeyActions[0];
|
|
2565
|
+
if (apiKeyAction !== undefined) {
|
|
2566
|
+
let apiKey;
|
|
2567
|
+
if (args.apiKeyStdin === true) {
|
|
2568
|
+
if (process.stdin.isTTY === true) {
|
|
2569
|
+
process.stderr.write(ui.errorLine("--api-key-stdin requires redirected standard input. Omit the flag to enter the key in a masked prompt."));
|
|
2570
|
+
return 2;
|
|
2571
|
+
}
|
|
2572
|
+
try {
|
|
2573
|
+
apiKey = await readApiKeyFromStdin(process.stdin);
|
|
2574
|
+
}
|
|
2575
|
+
catch {
|
|
2576
|
+
process.stderr.write(ui.errorLine(`Could not read a valid ${apiKeyAction.envVar} value from standard input; no credentials were written.`));
|
|
2577
|
+
return 1;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
2582
|
+
process.stderr.write(ui.errorLine(`Cannot securely prompt for ${apiKeyAction.envVar} without an interactive TTY. ` +
|
|
2583
|
+
`Run this command in a terminal, or pipe the value explicitly with --api-key-stdin; no credentials were written.`));
|
|
2584
|
+
return 1;
|
|
2585
|
+
}
|
|
2586
|
+
const answer = await p.password({
|
|
2587
|
+
message: `Enter ${apiKeyAction.label} (${apiKeyAction.envVar})`,
|
|
2588
|
+
validate: (value) => apiKeyInputProblem(value ?? ""),
|
|
2589
|
+
clearOnError: true,
|
|
2590
|
+
});
|
|
2591
|
+
if (p.isCancel(answer)) {
|
|
2592
|
+
process.stderr.write(ui.errorLine("Authentication was cancelled; no credentials were written."));
|
|
2593
|
+
return 130;
|
|
2594
|
+
}
|
|
2595
|
+
apiKey = answer.trim();
|
|
2596
|
+
}
|
|
2597
|
+
apiKeys = { [apiKeyAction.id]: apiKey };
|
|
2598
|
+
}
|
|
2599
|
+
process.stdout.write(ui.style.dim("Press Ctrl-C once to interrupt authentication safely.\n"));
|
|
2600
|
+
const execution = await withScopedPreflightCancellation(async (abortSignal) => ({
|
|
2601
|
+
results: await executeProviderSetupPlan(plan, {
|
|
2602
|
+
...(apiKeys === undefined ? {} : { apiKeys }),
|
|
2603
|
+
abortSignal,
|
|
2604
|
+
}),
|
|
2605
|
+
interrupted: abortSignal.aborted,
|
|
2606
|
+
}), { keypress: false });
|
|
2607
|
+
const { results } = execution;
|
|
2608
|
+
for (const result of results) {
|
|
2609
|
+
const badge = execution.interrupted && result.status === "failed"
|
|
2610
|
+
? ui.badge("waiting")
|
|
2611
|
+
: result.status === "ok"
|
|
2612
|
+
? ui.badge("ok")
|
|
2613
|
+
: ui.badge("error");
|
|
2614
|
+
process.stdout.write(`${badge}${result.action.label}: ${result.detail}\n`);
|
|
2615
|
+
}
|
|
2616
|
+
if (results.some((result) => result.failureKind !== undefined)) {
|
|
2617
|
+
process.stderr.write(ui.errorLine("Provider setup ended in an unconfirmed process or credential-cleanup state. Follow the reported manual cleanup guidance before retrying; automatic recovery is disabled."));
|
|
2618
|
+
return 130;
|
|
2619
|
+
}
|
|
2620
|
+
if (execution.interrupted) {
|
|
2621
|
+
process.stderr.write(ui.errorLine("Authentication was interrupted; temporary credentials were cleaned up."));
|
|
2622
|
+
return 130;
|
|
2623
|
+
}
|
|
2624
|
+
return results.every((result) => result.status === "ok") ? 0 : 1;
|
|
2625
|
+
}
|
|
2626
|
+
const MAX_STANDALONE_API_KEY_BYTES = 65_536;
|
|
2627
|
+
function apiKeyInputProblem(value) {
|
|
2628
|
+
const normalized = value.trim();
|
|
2629
|
+
if (normalized.length === 0)
|
|
2630
|
+
return "API key is required.";
|
|
2631
|
+
if (normalized.includes("\0") || /[\r\n]/u.test(normalized))
|
|
2632
|
+
return "API key must be a single non-empty line.";
|
|
2633
|
+
if (Buffer.byteLength(normalized, "utf8") > MAX_STANDALONE_API_KEY_BYTES)
|
|
2634
|
+
return "API key is too large.";
|
|
2635
|
+
return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Read one explicitly redirected API key without consulting ambient provider
|
|
2639
|
+
* environment variables. A single trailing line ending from `echo` is accepted;
|
|
2640
|
+
* embedded newlines, NUL bytes, empty input, and unbounded input fail closed.
|
|
2641
|
+
*/
|
|
2642
|
+
export async function readApiKeyFromStdin(input) {
|
|
2643
|
+
const chunks = [];
|
|
2644
|
+
let size = 0;
|
|
2645
|
+
for await (const chunk of input) {
|
|
2646
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
2647
|
+
size += bytes.byteLength;
|
|
2648
|
+
if (size > MAX_STANDALONE_API_KEY_BYTES + 2)
|
|
2649
|
+
throw new Error("API key input is too large.");
|
|
2650
|
+
chunks.push(bytes);
|
|
2651
|
+
}
|
|
2652
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/u, "");
|
|
2653
|
+
const problem = apiKeyInputProblem(value);
|
|
2654
|
+
if (problem !== undefined)
|
|
2655
|
+
throw new Error(problem);
|
|
2656
|
+
return value.trim();
|
|
2657
|
+
}
|
|
2658
|
+
export async function resolvePiAuthPathForLogin(options) {
|
|
2659
|
+
// A missing config is represented by readMonoAgentConfigJson as `missing`; a
|
|
2660
|
+
// malformed or unreadable config throws and must remain visible to operators.
|
|
2661
|
+
const result = await readMonoAgentConfigJson(options.configPath);
|
|
2662
|
+
const configured = result.missing ? undefined : result.json.providers?.piAuthPath;
|
|
2663
|
+
return resolveEffectivePiAuthPath({
|
|
2664
|
+
cwd: options.cwd ?? dirname(resolve(options.configPath)),
|
|
2665
|
+
...(nonEmptyEnv(options.piAuthPath) ? { explicitPath: options.piAuthPath } : {}),
|
|
2666
|
+
...(nonEmptyEnv(options.envPath) ? { envPath: options.envPath } : {}),
|
|
2667
|
+
...(nonEmptyEnv(configured) ? { configPath: configured } : {}),
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
777
2670
|
function printProviderSetupPlan(plan) {
|
|
778
2671
|
for (const action of plan.actions) {
|
|
779
2672
|
process.stdout.write(` ${action.label}: ${providerSetupActionCommandLine(action)} ${ui.style.dim(`(cwd: ${action.cwd})`)}\n`);
|
|
@@ -820,16 +2713,41 @@ function resolveWithChannels(args) {
|
|
|
820
2713
|
}
|
|
821
2714
|
return args.withChannels.filter(isWithChannel);
|
|
822
2715
|
}
|
|
2716
|
+
/** Safe reporting rows: paths and outcomes only, never secret contents. */
|
|
2717
|
+
export function initChangeDisplayRows(result) {
|
|
2718
|
+
const labels = {
|
|
2719
|
+
created: "created",
|
|
2720
|
+
updated: "updated",
|
|
2721
|
+
unchanged: "kept",
|
|
2722
|
+
"planned-create": "would create",
|
|
2723
|
+
"planned-update": "would update",
|
|
2724
|
+
};
|
|
2725
|
+
return result.changes.map((change) => ({
|
|
2726
|
+
label: labels[change.kind],
|
|
2727
|
+
path: change.path,
|
|
2728
|
+
unchanged: change.kind === "unchanged",
|
|
2729
|
+
}));
|
|
2730
|
+
}
|
|
2731
|
+
export function secretChecklistDisplayRows(secrets, configured) {
|
|
2732
|
+
return secrets.map((secret) => ({
|
|
2733
|
+
envVar: secret.envVar,
|
|
2734
|
+
label: secret.label,
|
|
2735
|
+
description: secret.description,
|
|
2736
|
+
status: configured.has(secret.envVar) ? "configured" : secret.required ? "missing" : "optional",
|
|
2737
|
+
}));
|
|
2738
|
+
}
|
|
823
2739
|
function printInitResult(result) {
|
|
824
2740
|
if (result.dryRun) {
|
|
825
2741
|
process.stdout.write(ui.style.dim("Dry run — nothing was written.\n"));
|
|
826
2742
|
}
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
2743
|
+
for (const row of initChangeDisplayRows(result)) {
|
|
2744
|
+
const prefix = row.unchanged ? " " : ui.badge("ok");
|
|
2745
|
+
const rendered = row.unchanged
|
|
2746
|
+
? ui.style.dim(row.label.padEnd(12))
|
|
2747
|
+
: ui.style.green(row.label.padEnd(12));
|
|
2748
|
+
// Sensitive files are safe to identify by path; their contents are never
|
|
2749
|
+
// included in this result or printed here.
|
|
2750
|
+
process.stdout.write(`${prefix}${rendered} ${row.path}\n`);
|
|
833
2751
|
}
|
|
834
2752
|
if (result.knowledgeFiles.length > 0) {
|
|
835
2753
|
process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
|
|
@@ -843,19 +2761,34 @@ function printInitResult(result) {
|
|
|
843
2761
|
process.stdout.write(` ${ui.style.cyan(module.title)} ${ui.style.dim(`(risk: ${riskColor(module.riskLevel)})`)}\n`);
|
|
844
2762
|
}
|
|
845
2763
|
}
|
|
846
|
-
if (result.
|
|
847
|
-
process.stdout.write("\n" + ui.style.dim(
|
|
2764
|
+
if (result.secretPersistence.status === "persisted") {
|
|
2765
|
+
process.stdout.write("\n" + ui.style.dim(result.secretPersistence.changed
|
|
2766
|
+
? "Required secrets were securely merged into .env (mode 0600).\n"
|
|
2767
|
+
: "Required secrets were already securely configured in .env.\n"));
|
|
2768
|
+
}
|
|
2769
|
+
else if (result.secretPersistence.status === "planned") {
|
|
2770
|
+
process.stdout.write("\n" + ui.style.dim("Dry run: required secrets would be securely merged into .env.\n"));
|
|
2771
|
+
}
|
|
2772
|
+
else if (result.secretPersistence.status === "refused") {
|
|
2773
|
+
process.stderr.write(ui.hint(`Automatic secret persistence was refused${result.secretPersistence.reason === undefined ? "" : ` (${result.secretPersistence.reason})`}. No secret value was written.\n` +
|
|
2774
|
+
(result.secretPersistence.detail === undefined ? "" : `${result.secretPersistence.detail}\n`)));
|
|
2775
|
+
}
|
|
2776
|
+
else if (result.plan.envExample !== undefined) {
|
|
2777
|
+
process.stdout.write("\n" + ui.style.dim("Use .env.example as a reference and add missing values to .env; do not overwrite an existing .env.\n"));
|
|
848
2778
|
}
|
|
849
2779
|
}
|
|
850
|
-
function printSecretsChecklist(secrets) {
|
|
2780
|
+
function printSecretsChecklist(secrets, configured = new Set()) {
|
|
851
2781
|
process.stdout.write("\n" + ui.heading("Secrets checklist"));
|
|
852
2782
|
if (secrets.length === 0) {
|
|
853
2783
|
process.stdout.write(ui.style.dim("No secrets required by the selected capabilities.\n"));
|
|
854
2784
|
return;
|
|
855
2785
|
}
|
|
856
|
-
process.stdout.write(ui.style.dim("
|
|
857
|
-
for (const secret of secrets) {
|
|
858
|
-
|
|
2786
|
+
process.stdout.write(ui.style.dim("Secret values are never written to config JSON and are never printed.\n"));
|
|
2787
|
+
for (const secret of secretChecklistDisplayRows(secrets, configured)) {
|
|
2788
|
+
const status = secret.status === "configured"
|
|
2789
|
+
? ui.style.green(secret.status)
|
|
2790
|
+
: ui.style.yellow(secret.status);
|
|
2791
|
+
process.stdout.write(` ${ui.style.bold(secret.envVar)} ${ui.style.dim(`- ${secret.label}: ${secret.description}`)} ${status}\n`);
|
|
859
2792
|
}
|
|
860
2793
|
}
|
|
861
2794
|
function printNextSteps(configPath) {
|
|
@@ -863,9 +2796,39 @@ function printNextSteps(configPath) {
|
|
|
863
2796
|
ui.heading("Next steps") +
|
|
864
2797
|
` ${ui.style.bold("1.")} Edit ${configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
|
|
865
2798
|
` ${ui.style.bold("2.")} mono-agent validate\n` +
|
|
866
|
-
` ${ui.style.bold("3.")} mono-agent
|
|
2799
|
+
` ${ui.style.bold("3.")} mono-agent tui --local --configure\n` +
|
|
2800
|
+
` ${ui.style.bold("4.")} ${process.platform === "darwin" ? "mono-agent start" : "mono-agent start --foreground"} ${ui.style.dim("(optional background/long-running service)")}\n`);
|
|
867
2801
|
}
|
|
868
2802
|
async function runInstallSkill(args) {
|
|
2803
|
+
if (args.project === true) {
|
|
2804
|
+
try {
|
|
2805
|
+
if (args.update === true) {
|
|
2806
|
+
const result = await updateManagedProjectSkills(process.cwd());
|
|
2807
|
+
for (const path of result.updated) {
|
|
2808
|
+
process.stdout.write(`${ui.badge("ok")}${ui.style.green("updated")} ${path}\n`);
|
|
2809
|
+
}
|
|
2810
|
+
if (result.backupDir !== undefined) {
|
|
2811
|
+
process.stdout.write(`${ui.badge("ok")}backup ${result.backupDir}\n`);
|
|
2812
|
+
}
|
|
2813
|
+
if (result.updated.length === 0)
|
|
2814
|
+
process.stdout.write(`${ui.badge("ok")}project skills are current\n`);
|
|
2815
|
+
return 0;
|
|
2816
|
+
}
|
|
2817
|
+
const result = await checkManagedProjectSkills(process.cwd());
|
|
2818
|
+
for (const status of result.statuses) {
|
|
2819
|
+
const badge = status.status === "ready" ? ui.badge("ok") : ui.badge("error");
|
|
2820
|
+
process.stdout.write(`${badge}${status.name}: ${status.status} (${status.path})\n`);
|
|
2821
|
+
}
|
|
2822
|
+
if (!result.ok && args.check !== true) {
|
|
2823
|
+
process.stderr.write(ui.errorLine("Project skills need attention. Run `mono-agent install-skill --project --update`; modified copies require manual reconciliation."));
|
|
2824
|
+
}
|
|
2825
|
+
return result.ok ? 0 : 1;
|
|
2826
|
+
}
|
|
2827
|
+
catch (error) {
|
|
2828
|
+
process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
|
|
2829
|
+
return 1;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
869
2832
|
let result;
|
|
870
2833
|
try {
|
|
871
2834
|
result = await installComposerSkill({
|
|
@@ -890,22 +2853,58 @@ async function runValidate(args) {
|
|
|
890
2853
|
configPath: context.configPath,
|
|
891
2854
|
allowFilesystemWrites: context.allowFilesystemWrites,
|
|
892
2855
|
});
|
|
893
|
-
for (const section of report.sections) {
|
|
894
|
-
process.stdout.write(formatSection(section));
|
|
895
|
-
}
|
|
896
2856
|
const preset = resolveValidatePreset(args);
|
|
897
2857
|
if (preset === "unknown") {
|
|
2858
|
+
if (args.json === true) {
|
|
2859
|
+
process.stdout.write(`${JSON.stringify({ ok: false, sections: report.sections })}\n`);
|
|
2860
|
+
}
|
|
898
2861
|
return 1;
|
|
899
2862
|
}
|
|
2863
|
+
let presetResult;
|
|
900
2864
|
if (preset !== undefined) {
|
|
901
2865
|
const plan = composeWizardPlan(presetAnswers(preset), {
|
|
902
2866
|
dirBasename: basename(context.cwd),
|
|
903
2867
|
skillsRootExists: false,
|
|
904
2868
|
});
|
|
2869
|
+
presetResult = {
|
|
2870
|
+
id: preset.id,
|
|
2871
|
+
expectations: plan.validateExpectations.map((expectation) => {
|
|
2872
|
+
const actual = report.sections.find((entry) => entry.id === expectation.sectionId)?.status ?? "missing";
|
|
2873
|
+
return {
|
|
2874
|
+
sectionId: expectation.sectionId,
|
|
2875
|
+
expected: expectation.mustBe,
|
|
2876
|
+
actual,
|
|
2877
|
+
met: actual === expectation.mustBe,
|
|
2878
|
+
};
|
|
2879
|
+
}),
|
|
2880
|
+
};
|
|
2881
|
+
if (args.json === true) {
|
|
2882
|
+
process.stdout.write(`${JSON.stringify({
|
|
2883
|
+
ok: report.ok,
|
|
2884
|
+
sections: report.sections,
|
|
2885
|
+
preset: presetResult,
|
|
2886
|
+
})}\n`);
|
|
2887
|
+
return report.ok ? 0 : 1;
|
|
2888
|
+
}
|
|
2889
|
+
for (const section of report.sections) {
|
|
2890
|
+
process.stdout.write(formatSection(section));
|
|
2891
|
+
}
|
|
905
2892
|
process.stdout.write(renderPlanCompleteness(plan.validateExpectations, `Preset: ${preset.id}`, report));
|
|
906
2893
|
}
|
|
2894
|
+
else if (args.json === true) {
|
|
2895
|
+
process.stdout.write(`${JSON.stringify({ ok: report.ok, sections: report.sections })}\n`);
|
|
2896
|
+
return report.ok ? 0 : 1;
|
|
2897
|
+
}
|
|
2898
|
+
else {
|
|
2899
|
+
for (const section of report.sections) {
|
|
2900
|
+
process.stdout.write(formatSection(section));
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const hasWaitingSections = report.sections.some((section) => section.status === "waiting");
|
|
907
2904
|
process.stdout.write(report.ok
|
|
908
|
-
?
|
|
2905
|
+
? hasWaitingSections
|
|
2906
|
+
? `\n${ui.style.yellow("⚠ Config is structurally valid, but needs attention before start.")}\n${ui.style.dim("Review the waiting sections above, then re-run mono-agent validate.")}\n`
|
|
2907
|
+
: `\n${ui.style.green("✓ Config is ready to start.")}\n${ui.style.dim("Run `mono-agent config` for the full field-by-field view.")}\n`
|
|
909
2908
|
: `\n${ui.hint("Fix the errors above, then re-run mono-agent validate.")}`);
|
|
910
2909
|
process.stdout.write(ui.style.dim("Core sections activate by presence; channels need `enabled: true` — see docs/config (How sections activate).\n"));
|
|
911
2910
|
return report.ok ? 0 : 1;
|
|
@@ -1151,13 +3150,13 @@ function colorDetail(status, detail) {
|
|
|
1151
3150
|
* refuse on any `error` section. `waiting` (e.g. Ollama/Phoenix not up yet) is
|
|
1152
3151
|
* runtime-soft and never blocks.
|
|
1153
3152
|
*/
|
|
1154
|
-
export async function ensureStartable(args) {
|
|
3153
|
+
export async function ensureStartable(args, env = process.env) {
|
|
1155
3154
|
const cwd = process.cwd();
|
|
1156
3155
|
const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
|
|
1157
3156
|
if (!(await pathExists(configPath))) {
|
|
1158
3157
|
return { ok: false, code: 2, kind: "missing-config", configPath };
|
|
1159
3158
|
}
|
|
1160
|
-
const report = await validateMonoAgentFolder({ env
|
|
3159
|
+
const report = await validateMonoAgentFolder({ env, cwd, configPath, liveness: false });
|
|
1161
3160
|
if (!report.ok) {
|
|
1162
3161
|
return { ok: false, code: 1, kind: "validation", report };
|
|
1163
3162
|
}
|
|
@@ -1186,25 +3185,26 @@ async function pathExists(path) {
|
|
|
1186
3185
|
return false;
|
|
1187
3186
|
}
|
|
1188
3187
|
}
|
|
1189
|
-
async function runStart(args) {
|
|
3188
|
+
async function runStart(args, env) {
|
|
1190
3189
|
if (args.foreground) {
|
|
1191
|
-
return await runForeground(args);
|
|
3190
|
+
return await runForeground(args, env);
|
|
1192
3191
|
}
|
|
1193
|
-
return await runBackgroundCommand(args, "start");
|
|
3192
|
+
return await runBackgroundCommand(args, "start", env);
|
|
1194
3193
|
}
|
|
1195
3194
|
/**
|
|
1196
3195
|
* The blocking worker: builds the responder, starts every configured channel
|
|
1197
3196
|
* plus traceability, and stays alive until a signal. This is what launchd
|
|
1198
3197
|
* invokes (via `start --foreground`) and what users get with `--foreground`/`-f`.
|
|
1199
3198
|
*/
|
|
1200
|
-
async function runForeground(args) {
|
|
1201
|
-
const pre = await ensureStartable(args);
|
|
3199
|
+
async function runForeground(args, env = process.env) {
|
|
3200
|
+
const pre = await ensureStartable(args, env);
|
|
1202
3201
|
if (!pre.ok) {
|
|
1203
3202
|
printPreflightFailure(pre);
|
|
1204
3203
|
return pre.code;
|
|
1205
3204
|
}
|
|
1206
3205
|
const app = await startMonoAgentApp({
|
|
1207
3206
|
cwd: process.cwd(),
|
|
3207
|
+
env,
|
|
1208
3208
|
...(args.configPath === undefined ? {} : { configPath: args.configPath }),
|
|
1209
3209
|
logger: consoleLogger(),
|
|
1210
3210
|
});
|
|
@@ -1215,7 +3215,7 @@ async function runForeground(args) {
|
|
|
1215
3215
|
// trace heartbeat timer is unref'd.
|
|
1216
3216
|
return await waitForShutdownSignal(app);
|
|
1217
3217
|
}
|
|
1218
|
-
async function runBackgroundCommand(args, command) {
|
|
3218
|
+
async function runBackgroundCommand(args, command, env = process.env) {
|
|
1219
3219
|
const guard = requireDarwin(command);
|
|
1220
3220
|
if (guard !== undefined) {
|
|
1221
3221
|
return guard;
|
|
@@ -1225,7 +3225,7 @@ async function runBackgroundCommand(args, command) {
|
|
|
1225
3225
|
// launchd's KeepAlive would retry it forever. stop/status/logs stay ungated so
|
|
1226
3226
|
// a broken instance can still be inspected and torn down.
|
|
1227
3227
|
if (command === "start" || command === "restart") {
|
|
1228
|
-
const pre = await ensureStartable(args);
|
|
3228
|
+
const pre = await ensureStartable(args, env);
|
|
1229
3229
|
if (!pre.ok) {
|
|
1230
3230
|
printPreflightFailure(pre);
|
|
1231
3231
|
return pre.code;
|
|
@@ -1236,7 +3236,7 @@ async function runBackgroundCommand(args, command) {
|
|
|
1236
3236
|
...(args.configPath === undefined ? {} : { configPath: args.configPath }),
|
|
1237
3237
|
...(args.envFile === undefined ? {} : { envFile: args.envFile }),
|
|
1238
3238
|
},
|
|
1239
|
-
env
|
|
3239
|
+
env,
|
|
1240
3240
|
cwd: process.cwd(),
|
|
1241
3241
|
cliPath: fileURLToPath(import.meta.url),
|
|
1242
3242
|
});
|