@mono-agent/agent-app 0.6.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -19
- package/dist/account-home.d.ts +6 -0
- package/dist/account-home.d.ts.map +1 -0
- package/dist/account-home.js +9 -0
- package/dist/account-home.js.map +1 -0
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +33 -6
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +485 -87
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts +3 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +16 -5
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +27 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +761 -147
- package/dist/app.js.map +1 -1
- package/dist/background-environment.d.ts +16 -0
- package/dist/background-environment.d.ts.map +1 -0
- package/dist/background-environment.js +68 -0
- package/dist/background-environment.js.map +1 -0
- package/dist/background-runtime.d.ts +62 -0
- package/dist/background-runtime.d.ts.map +1 -0
- package/dist/background-runtime.js +1115 -0
- package/dist/background-runtime.js.map +1 -0
- package/dist/background-snapshot-key.d.ts +13 -0
- package/dist/background-snapshot-key.d.ts.map +1 -0
- package/dist/background-snapshot-key.js +194 -0
- package/dist/background-snapshot-key.js.map +1 -0
- package/dist/background-snapshot.d.ts +96 -0
- package/dist/background-snapshot.d.ts.map +1 -0
- package/dist/background-snapshot.js +450 -0
- package/dist/background-snapshot.js.map +1 -0
- package/dist/background-worker-lease.d.ts +39 -0
- package/dist/background-worker-lease.d.ts.map +1 -0
- package/dist/background-worker-lease.js +356 -0
- package/dist/background-worker-lease.js.map +1 -0
- package/dist/background.d.ts +74 -2
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +815 -73
- package/dist/background.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channel-plugins.d.ts +2 -0
- package/dist/channel-plugins.d.ts.map +1 -1
- package/dist/channel-plugins.js +8 -0
- package/dist/channel-plugins.js.map +1 -1
- package/dist/channels.d.ts +10 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +87 -9
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +86 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2421 -143
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +448 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +33 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +188 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +16 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +165 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/continuation-command.d.ts +15 -0
- package/dist/continuation-command.d.ts.map +1 -0
- package/dist/continuation-command.js +170 -0
- package/dist/continuation-command.js.map +1 -0
- package/dist/continuation-config.d.ts +25 -0
- package/dist/continuation-config.d.ts.map +1 -0
- package/dist/continuation-config.js +236 -0
- package/dist/continuation-config.js.map +1 -0
- package/dist/continuation-service.d.ts +99 -0
- package/dist/continuation-service.d.ts.map +1 -0
- package/dist/continuation-service.js +1313 -0
- package/dist/continuation-service.js.map +1 -0
- package/dist/continuation-store.d.ts +104 -0
- package/dist/continuation-store.d.ts.map +1 -0
- package/dist/continuation-store.js +688 -0
- package/dist/continuation-store.js.map +1 -0
- package/dist/continuations.d.ts +167 -0
- package/dist/continuations.d.ts.map +1 -0
- package/dist/continuations.js +71 -0
- package/dist/continuations.js.map +1 -0
- package/dist/doctor.d.ts +29 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1611 -116
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +584 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +728 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +95 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1218 -24
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +25 -1
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +281 -19
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd.d.ts +16 -6
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +30 -15
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts +99 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +1299 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/managed-runtime-packages.d.ts +9 -0
- package/dist/managed-runtime-packages.d.ts.map +1 -0
- package/dist/managed-runtime-packages.js +56 -0
- package/dist/managed-runtime-packages.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-embedding-service.d.ts +33 -0
- package/dist/memory-embedding-service.d.ts.map +1 -0
- package/dist/memory-embedding-service.js +207 -0
- package/dist/memory-embedding-service.js.map +1 -0
- package/dist/memory-recall.d.ts +43 -14
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +103 -33
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +141 -25
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/notify-runtime.d.ts +26 -0
- package/dist/notify-runtime.d.ts.map +1 -0
- package/dist/notify-runtime.js +27 -0
- package/dist/notify-runtime.js.map +1 -0
- package/dist/notify-tool.d.ts +51 -0
- package/dist/notify-tool.d.ts.map +1 -0
- package/dist/notify-tool.js +182 -0
- package/dist/notify-tool.js.map +1 -0
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/proactive-notify.d.ts +2 -0
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +1 -0
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.d.ts.map +1 -0
- package/dist/process-incarnation.js +208 -0
- package/dist/process-incarnation.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +117 -2
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1505 -49
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/recipes/base.d.ts +15 -0
- package/dist/recipes/base.d.ts.map +1 -0
- package/dist/recipes/base.js +51 -0
- package/dist/recipes/base.js.map +1 -0
- package/dist/recipes/catalog.d.ts +4 -0
- package/dist/recipes/catalog.d.ts.map +1 -0
- package/dist/recipes/catalog.js +525 -0
- package/dist/recipes/catalog.js.map +1 -0
- package/dist/recipes/index.d.ts +11 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +14 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/types.d.ts +70 -0
- package/dist/recipes/types.d.ts.map +1 -0
- package/dist/recipes/types.js +15 -0
- package/dist/recipes/types.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +86 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +1402 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/setup.d.ts +29 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +97 -0
- package/dist/setup.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +56 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +142 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +56 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +305 -31
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +141 -29
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +33 -2
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +826 -108
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +56 -9
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +172 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +59 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1290 -191
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +587 -20
- package/skills/mono-agent-composer/SKILL.md +6 -6
- package/skills/mono-agent-composer/references/config-blueprint.md +11 -10
- package/skills/mono-agent-composer/references/discovery-questions.md +33 -18
- package/skills/mono-agent-composer/references/feature-coverage.md +14 -12
- package/skills/mono-agent-composer/references/package-map.md +2 -2
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +3 -3
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, open, opendir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { parseEnv } from "node:util";
|
|
7
|
+
import { validateMonoAgentFolder } from "./doctor.js";
|
|
8
|
+
import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
|
|
9
|
+
import { initializeFirstRunManagedMemory } from "./first-run-managed-memory.js";
|
|
10
|
+
import { piAuthPathForSetup } from "./provider-setup.js";
|
|
11
|
+
let exactProcessEnvironmentTail = Promise.resolve();
|
|
12
|
+
function replaceProcessEnvironment(env) {
|
|
13
|
+
const next = new Map(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
14
|
+
for (const name of Object.keys(process.env)) {
|
|
15
|
+
if (!next.has(name))
|
|
16
|
+
delete process.env[name];
|
|
17
|
+
}
|
|
18
|
+
for (const [name, value] of next)
|
|
19
|
+
process.env[name] = value;
|
|
20
|
+
}
|
|
21
|
+
function escapesRoot(relativePath) {
|
|
22
|
+
return relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_CRON_DIRECTORY = "cron";
|
|
25
|
+
const MAX_STAGED_FILES = 256;
|
|
26
|
+
const MAX_STAGED_DIRECTORY_ENTRIES = 4_096;
|
|
27
|
+
const MAX_STAGED_FILE_BYTES = 1_048_576;
|
|
28
|
+
const MAX_STAGED_TOTAL_BYTES = 8 * 1_048_576;
|
|
29
|
+
function throwIfStagingAborted(signal) {
|
|
30
|
+
signal?.throwIfAborted();
|
|
31
|
+
}
|
|
32
|
+
function stagingRelativePath(root, candidate, label, allowRoot = false, rootDescription = "its source root") {
|
|
33
|
+
const pathRelative = relative(root, candidate);
|
|
34
|
+
if ((!allowRoot && pathRelative.length === 0) || escapesRoot(pathRelative)) {
|
|
35
|
+
throw new Error(`Refusing to stage ${label} outside ${rootDescription}.`);
|
|
36
|
+
}
|
|
37
|
+
return pathRelative;
|
|
38
|
+
}
|
|
39
|
+
function consumeStagingBudget(budget, path, bytes) {
|
|
40
|
+
if (bytes > MAX_STAGED_FILE_BYTES) {
|
|
41
|
+
throw new Error(`Refusing to stage ${path} because it exceeds ${MAX_STAGED_FILE_BYTES} bytes.`);
|
|
42
|
+
}
|
|
43
|
+
if (budget.count >= MAX_STAGED_FILES) {
|
|
44
|
+
throw new Error(`Refusing to stage more than ${MAX_STAGED_FILES} existing/generated files.`);
|
|
45
|
+
}
|
|
46
|
+
if (budget.bytes + bytes > MAX_STAGED_TOTAL_BYTES) {
|
|
47
|
+
throw new Error(`Refusing to stage more than ${MAX_STAGED_TOTAL_BYTES} total file bytes.`);
|
|
48
|
+
}
|
|
49
|
+
budget.count += 1;
|
|
50
|
+
budget.bytes += bytes;
|
|
51
|
+
}
|
|
52
|
+
function pathSegments(pathRelative) {
|
|
53
|
+
return pathRelative.length === 0 ? [] : pathRelative.split(sep).filter((part) => part.length > 0);
|
|
54
|
+
}
|
|
55
|
+
async function existingDirectoryWithoutSymlinks(canonicalRoot, pathRelative, label, signal) {
|
|
56
|
+
let current = canonicalRoot;
|
|
57
|
+
for (const segment of pathSegments(pathRelative)) {
|
|
58
|
+
throwIfStagingAborted(signal);
|
|
59
|
+
const next = join(current, segment);
|
|
60
|
+
let pathStat;
|
|
61
|
+
try {
|
|
62
|
+
pathStat = await lstat(next);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code === "ENOENT")
|
|
66
|
+
return undefined;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (pathStat.isSymbolicLink()) {
|
|
70
|
+
throw new Error(`Refusing to stage symbolic-link ${label}: ${next}`);
|
|
71
|
+
}
|
|
72
|
+
if (!pathStat.isDirectory()) {
|
|
73
|
+
throw new Error(`Refusing to stage non-directory ${label}: ${next}`);
|
|
74
|
+
}
|
|
75
|
+
current = next;
|
|
76
|
+
}
|
|
77
|
+
const canonical = await realpath(current);
|
|
78
|
+
if (escapesRoot(relative(canonicalRoot, canonical))) {
|
|
79
|
+
throw new Error(`Refusing to stage ${label} outside its source root: ${current}`);
|
|
80
|
+
}
|
|
81
|
+
return canonical;
|
|
82
|
+
}
|
|
83
|
+
async function readExistingStagingFile(options) {
|
|
84
|
+
const segments = pathSegments(options.pathRelative);
|
|
85
|
+
const fileName = segments.pop();
|
|
86
|
+
if (fileName === undefined) {
|
|
87
|
+
throw new Error(`Refusing to stage ${options.label} without a file name.`);
|
|
88
|
+
}
|
|
89
|
+
const parent = await existingDirectoryWithoutSymlinks(options.canonicalRoot, segments.join(sep), `${options.label} parent`, options.signal);
|
|
90
|
+
if (parent === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
throwIfStagingAborted(options.signal);
|
|
93
|
+
const path = join(parent, fileName);
|
|
94
|
+
let pathStat;
|
|
95
|
+
try {
|
|
96
|
+
pathStat = await lstat(path);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error.code === "ENOENT")
|
|
100
|
+
return undefined;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
if (pathStat.isSymbolicLink()) {
|
|
104
|
+
throw new Error(`Refusing to stage symbolic-link ${options.label}: ${path}`);
|
|
105
|
+
}
|
|
106
|
+
if (!pathStat.isFile()) {
|
|
107
|
+
throw new Error(`Refusing to stage non-regular ${options.label}: ${path}`);
|
|
108
|
+
}
|
|
109
|
+
let handle;
|
|
110
|
+
try {
|
|
111
|
+
handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (["ELOOP", "EMLINK"].includes(error.code ?? "")) {
|
|
115
|
+
throw new Error(`Refusing to stage symbolic-link ${options.label}: ${path}`);
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const openedStat = await handle.stat();
|
|
121
|
+
if (!openedStat.isFile()) {
|
|
122
|
+
throw new Error(`Refusing to stage non-regular ${options.label}: ${path}`);
|
|
123
|
+
}
|
|
124
|
+
if (openedStat.size > MAX_STAGED_FILE_BYTES) {
|
|
125
|
+
throw new Error(`Refusing to stage ${options.label} because it exceeds ${MAX_STAGED_FILE_BYTES} bytes: ${path}`);
|
|
126
|
+
}
|
|
127
|
+
const contents = await handle.readFile();
|
|
128
|
+
throwIfStagingAborted(options.signal);
|
|
129
|
+
if (contents.length > MAX_STAGED_FILE_BYTES) {
|
|
130
|
+
throw new Error(`Refusing to stage ${options.label} because it exceeds ${MAX_STAGED_FILE_BYTES} bytes: ${path}`);
|
|
131
|
+
}
|
|
132
|
+
const currentStat = await lstat(path);
|
|
133
|
+
if (currentStat.isSymbolicLink() ||
|
|
134
|
+
!currentStat.isFile() ||
|
|
135
|
+
currentStat.dev !== openedStat.dev ||
|
|
136
|
+
currentStat.ino !== openedStat.ino) {
|
|
137
|
+
throw new Error(`Refusing to stage ${options.label} because it changed during preflight: ${path}`);
|
|
138
|
+
}
|
|
139
|
+
return contents;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await handle.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function writeStagedFile(options) {
|
|
146
|
+
if (options.stagedPaths.has(options.pathRelative))
|
|
147
|
+
return;
|
|
148
|
+
throwIfStagingAborted(options.signal);
|
|
149
|
+
consumeStagingBudget(options.budget, options.pathRelative, options.contents.length);
|
|
150
|
+
const path = resolve(options.stagingRoot, options.pathRelative);
|
|
151
|
+
stagingRelativePath(options.stagingRoot, path, `file ${options.pathRelative}`, false, "the disposable agent folder");
|
|
152
|
+
await mkdir(dirname(path), { recursive: true });
|
|
153
|
+
throwIfStagingAborted(options.signal);
|
|
154
|
+
await writeFile(path, options.contents, { flag: "wx", mode: 0o600 });
|
|
155
|
+
options.stagedPaths.add(options.pathRelative);
|
|
156
|
+
}
|
|
157
|
+
function effectiveCronDirectory(plan, env) {
|
|
158
|
+
const envDirectory = env.MONO_AGENT_CRON_DIR?.trim();
|
|
159
|
+
if (envDirectory !== undefined && envDirectory.length > 0)
|
|
160
|
+
return envDirectory;
|
|
161
|
+
const cron = plan.configJson.cron;
|
|
162
|
+
if (typeof cron === "object" && cron !== null && !Array.isArray(cron)) {
|
|
163
|
+
const configured = cron.dir;
|
|
164
|
+
if (typeof configured === "string" && configured.trim().length > 0)
|
|
165
|
+
return configured.trim();
|
|
166
|
+
}
|
|
167
|
+
return DEFAULT_CRON_DIRECTORY;
|
|
168
|
+
}
|
|
169
|
+
async function stageExistingCronFiles(options) {
|
|
170
|
+
const configuredDirectory = effectiveCronDirectory(options.plan, options.env);
|
|
171
|
+
if (isAbsolute(configuredDirectory)) {
|
|
172
|
+
throw new Error(`Refusing to stage absolute cron directory ${configuredDirectory}.`);
|
|
173
|
+
}
|
|
174
|
+
const cronPath = resolve(options.sourceRoot, configuredDirectory);
|
|
175
|
+
const cronRelative = stagingRelativePath(options.sourceRoot, cronPath, `cron directory ${configuredDirectory}`, true);
|
|
176
|
+
const sourceCronDirectory = await existingDirectoryWithoutSymlinks(options.sourceRoot, cronRelative, "cron directory", options.signal);
|
|
177
|
+
if (sourceCronDirectory === undefined)
|
|
178
|
+
return;
|
|
179
|
+
const names = [];
|
|
180
|
+
let scannedEntries = 0;
|
|
181
|
+
const directory = await opendir(sourceCronDirectory);
|
|
182
|
+
for await (const entry of directory) {
|
|
183
|
+
throwIfStagingAborted(options.signal);
|
|
184
|
+
scannedEntries += 1;
|
|
185
|
+
if (scannedEntries > MAX_STAGED_DIRECTORY_ENTRIES) {
|
|
186
|
+
throw new Error(`Refusing to scan more than ${MAX_STAGED_DIRECTORY_ENTRIES} cron directory entries.`);
|
|
187
|
+
}
|
|
188
|
+
if (!entry.name.toLowerCase().endsWith(".md"))
|
|
189
|
+
continue;
|
|
190
|
+
if (!entry.isFile()) {
|
|
191
|
+
const kind = entry.isSymbolicLink() ? "symbolic-link" : "non-regular";
|
|
192
|
+
throw new Error(`Refusing to stage ${kind} cron job: ${join(sourceCronDirectory, entry.name)}`);
|
|
193
|
+
}
|
|
194
|
+
names.push(entry.name);
|
|
195
|
+
if (names.length > MAX_STAGED_FILES) {
|
|
196
|
+
throw new Error(`Refusing to stage more than ${MAX_STAGED_FILES} cron job files.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const name of names.sort()) {
|
|
200
|
+
throwIfStagingAborted(options.signal);
|
|
201
|
+
const pathRelative = cronRelative.length === 0 ? name : join(cronRelative, name);
|
|
202
|
+
const contents = await readExistingStagingFile({
|
|
203
|
+
canonicalRoot: options.sourceRoot,
|
|
204
|
+
pathRelative,
|
|
205
|
+
label: `cron job ${name}`,
|
|
206
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
207
|
+
});
|
|
208
|
+
if (contents === undefined) {
|
|
209
|
+
throw new Error(`Cron job disappeared during staging: ${join(options.sourceRoot, pathRelative)}`);
|
|
210
|
+
}
|
|
211
|
+
await writeStagedFile({
|
|
212
|
+
stagingRoot: options.stagingRoot,
|
|
213
|
+
pathRelative,
|
|
214
|
+
contents,
|
|
215
|
+
budget: options.budget,
|
|
216
|
+
stagedPaths: options.stagedPaths,
|
|
217
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Run one guided-init operation with exactly the durable worker environment.
|
|
223
|
+
*
|
|
224
|
+
* Provider SDKs and CLI bridges still consult `process.env` internally, so an
|
|
225
|
+
* `env` argument used only for config loading is not enough. Guided init is a
|
|
226
|
+
* single-purpose CLI path; serialize these temporary global swaps, hold the
|
|
227
|
+
* selected environment for the whole async operation, and restore the complete
|
|
228
|
+
* caller snapshot on every exit path.
|
|
229
|
+
*/
|
|
230
|
+
export async function withExactProcessEnvironment(env, task) {
|
|
231
|
+
const predecessor = exactProcessEnvironmentTail;
|
|
232
|
+
let release;
|
|
233
|
+
exactProcessEnvironmentTail = new Promise((resolveTail) => {
|
|
234
|
+
release = resolveTail;
|
|
235
|
+
});
|
|
236
|
+
await predecessor;
|
|
237
|
+
const original = { ...process.env };
|
|
238
|
+
try {
|
|
239
|
+
replaceProcessEnvironment(env);
|
|
240
|
+
return await task();
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
try {
|
|
244
|
+
replaceProcessEnvironment(original);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
release();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Host variables required for the CLI and its child processes to operate. The
|
|
253
|
+
* guided path deliberately does not inherit provider credentials or mono-agent
|
|
254
|
+
* config from the invoking shell: a launchd worker cannot reproduce either.
|
|
255
|
+
*/
|
|
256
|
+
const MONO_AGENT_SECRET_ENV_NAME = /(?:^|_)(?:API_KEY|CREDENTIAL|CREDENTIALS|PASSWORD|SECRET|TOKEN)$/u;
|
|
257
|
+
const SENSITIVE_DOTENV_NAME = /(api.?key|credential|password|secret|token)/iu;
|
|
258
|
+
/** Whether guided setup consumed any durable dotenv value that should be owner-only and untracked. */
|
|
259
|
+
export function hasSensitivePersistedEnvironmentValue(env) {
|
|
260
|
+
return Object.entries(env).some(([name, value]) => SENSITIVE_DOTENV_NAME.test(name) && nonEmpty(value));
|
|
261
|
+
}
|
|
262
|
+
/** Read and fingerprint dotenv without changing process.env. A missing file is empty. */
|
|
263
|
+
export async function readCliDotenvSnapshot(path) {
|
|
264
|
+
let handle;
|
|
265
|
+
try {
|
|
266
|
+
// O_NONBLOCK matters before fstat: opening a FIFO read-only can otherwise
|
|
267
|
+
// wait forever for a writer, preventing us from reaching the regular-file
|
|
268
|
+
// check. It is a no-op for ordinary files.
|
|
269
|
+
handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (error.code === "ENOENT") {
|
|
273
|
+
return { env: {}, fingerprint: "missing" };
|
|
274
|
+
}
|
|
275
|
+
if (["ELOOP", "EMLINK"].includes(error.code ?? "")) {
|
|
276
|
+
throw new Error(`Refusing to read dotenv path ${path} because it is a symbolic link.`);
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const fileStat = await handle.stat();
|
|
282
|
+
if (!fileStat.isFile()) {
|
|
283
|
+
throw new Error(`Refusing to read dotenv path ${path} because it is not a regular file.`);
|
|
284
|
+
}
|
|
285
|
+
const contents = await handle.readFile({ encoding: "utf8" });
|
|
286
|
+
const mode = fileStat.mode & 0o777;
|
|
287
|
+
const env = Object.fromEntries(Object.entries(parseEnv(contents)).filter((entry) => entry[1] !== undefined));
|
|
288
|
+
const digest = createHash("sha256").update(contents).digest("hex");
|
|
289
|
+
return { env, fingerprint: `file:${mode.toString(8)}:${digest}` };
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
await handle.close();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Read a dotenv file without changing process.env. A missing file is empty. */
|
|
296
|
+
export async function readCliDotenvFile(path) {
|
|
297
|
+
return { ...(await readCliDotenvSnapshot(path)).env };
|
|
298
|
+
}
|
|
299
|
+
/** Read one exact config snapshot without following the final path component. */
|
|
300
|
+
export async function readCliConfigSnapshot(path) {
|
|
301
|
+
let handle;
|
|
302
|
+
try {
|
|
303
|
+
handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (["ELOOP", "EMLINK"].includes(error.code ?? "")) {
|
|
307
|
+
throw new Error(`Refusing to read config path ${path} because it is a symbolic link.`);
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const fileStat = await handle.stat();
|
|
313
|
+
if (!fileStat.isFile()) {
|
|
314
|
+
throw new Error(`Refusing to read config path ${path} because it is not a regular file.`);
|
|
315
|
+
}
|
|
316
|
+
const bytes = await handle.readFile();
|
|
317
|
+
const pathStat = await lstat(path);
|
|
318
|
+
if (pathStat.isSymbolicLink() ||
|
|
319
|
+
!pathStat.isFile() ||
|
|
320
|
+
pathStat.dev !== fileStat.dev ||
|
|
321
|
+
pathStat.ino !== fileStat.ino) {
|
|
322
|
+
throw new Error(`Refusing to read config path ${path} because it changed during setup.`);
|
|
323
|
+
}
|
|
324
|
+
let contents;
|
|
325
|
+
try {
|
|
326
|
+
contents = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
throw new Error(`Refusing to read config path ${path} because it is not valid UTF-8.`);
|
|
330
|
+
}
|
|
331
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
332
|
+
const mode = fileStat.mode & 0o777;
|
|
333
|
+
return {
|
|
334
|
+
contents,
|
|
335
|
+
digest,
|
|
336
|
+
fingerprint: [
|
|
337
|
+
"file",
|
|
338
|
+
fileStat.dev,
|
|
339
|
+
fileStat.ino,
|
|
340
|
+
fileStat.size,
|
|
341
|
+
fileStat.mtimeMs,
|
|
342
|
+
fileStat.ctimeMs,
|
|
343
|
+
mode.toString(8),
|
|
344
|
+
digest,
|
|
345
|
+
].join(":"),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
await handle.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Build the durable environment proven by guided init and later used by its
|
|
354
|
+
* immediate start. Shell-only provider credentials and MONO_AGENT_* overrides
|
|
355
|
+
* are intentionally absent; only worker-operational host values survive.
|
|
356
|
+
* Operational values win over dotenv exactly as `process.loadEnvFile` and the
|
|
357
|
+
* launchd plist do, while entered secrets and resolved Pi auth remain explicit.
|
|
358
|
+
*/
|
|
359
|
+
export function effectiveFirstRunEnvironment(options) {
|
|
360
|
+
const operationalEnv = selectBackgroundOperationalEnvironment(options.shellEnv);
|
|
361
|
+
return {
|
|
362
|
+
...options.dotenvEnv,
|
|
363
|
+
...operationalEnv,
|
|
364
|
+
...(options.enteredSecrets ?? {}),
|
|
365
|
+
...(options.resolvedPiAuthPath === undefined
|
|
366
|
+
? {}
|
|
367
|
+
: { MONO_AGENT_PI_AUTH_PATH: options.resolvedPiAuthPath }),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Persisted mono-agent config overrides that could make the generated JSON say
|
|
372
|
+
* something different from the runtime. Secret values and the selected Pi
|
|
373
|
+
* credential-store path are data inputs, not config substitutions, so remain
|
|
374
|
+
* allowed. Returned names are sorted for deterministic operator output.
|
|
375
|
+
*/
|
|
376
|
+
export function unexpectedPersistedMonoAgentOverrides(plan, dotenvEnv) {
|
|
377
|
+
const selectedSecrets = new Set(plan.secrets.map((secret) => secret.envVar));
|
|
378
|
+
return Object.keys(dotenvEnv)
|
|
379
|
+
.filter((name) => name.startsWith("MONO_AGENT_") &&
|
|
380
|
+
name !== "MONO_AGENT_PI_AUTH_PATH" &&
|
|
381
|
+
!selectedSecrets.has(name) &&
|
|
382
|
+
!MONO_AGENT_SECRET_ENV_NAME.test(name))
|
|
383
|
+
.sort();
|
|
384
|
+
}
|
|
385
|
+
function nonEmpty(value) {
|
|
386
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
387
|
+
}
|
|
388
|
+
/** Selected secret names whose shell and persisted values would diverge. */
|
|
389
|
+
export function selectedSecretEnvironmentConflicts(plan, shellEnv, dotenvEnv, enteredSecrets = {}) {
|
|
390
|
+
return [...new Set(plan.secrets.map((secret) => secret.envVar))].filter((name) => {
|
|
391
|
+
const values = [shellEnv[name], dotenvEnv[name], enteredSecrets[name]].filter(nonEmpty);
|
|
392
|
+
return new Set(values).size > 1;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** All selected secret values used by the live probe, including persisted ones. */
|
|
396
|
+
export function selectedSecretValues(plan, env) {
|
|
397
|
+
const values = {};
|
|
398
|
+
for (const secret of plan.secrets) {
|
|
399
|
+
const value = env[secret.envVar];
|
|
400
|
+
if (nonEmpty(value))
|
|
401
|
+
values[secret.envVar] = value;
|
|
402
|
+
}
|
|
403
|
+
return values;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Resolve the one Pi credential store used by discovery, setup, validation and
|
|
407
|
+
* runtime. Inputs are already ordered by their documented precedence.
|
|
408
|
+
*/
|
|
409
|
+
export function resolveEffectivePiAuthPath(options) {
|
|
410
|
+
const selected = [options.explicitPath, options.envPath, options.configPath]
|
|
411
|
+
.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
412
|
+
return piAuthPathForSetup(selected, options.cwd);
|
|
413
|
+
}
|
|
414
|
+
/** Whether an exported shell override would select a different background store. */
|
|
415
|
+
export function piAuthPathBackgroundConflict(options) {
|
|
416
|
+
if (!nonEmpty(options.shellPath))
|
|
417
|
+
return false;
|
|
418
|
+
const interactive = resolveEffectivePiAuthPath({
|
|
419
|
+
cwd: options.cwd,
|
|
420
|
+
envPath: options.shellPath,
|
|
421
|
+
...(options.configPath === undefined ? {} : { configPath: options.configPath }),
|
|
422
|
+
});
|
|
423
|
+
const background = resolveEffectivePiAuthPath({
|
|
424
|
+
cwd: options.cwd,
|
|
425
|
+
...(nonEmpty(options.dotenvPath) ? { envPath: options.dotenvPath } : {}),
|
|
426
|
+
...(options.configPath === undefined ? {} : { configPath: options.configPath }),
|
|
427
|
+
});
|
|
428
|
+
return interactive !== background;
|
|
429
|
+
}
|
|
430
|
+
const FIRST_RUN_DOCTOR_DETAIL_MAX_LENGTH = 300;
|
|
431
|
+
function firstActionableDoctorDetail(section) {
|
|
432
|
+
const details = (section?.details ?? [])
|
|
433
|
+
.map((rawDetail) => rawDetail.replace(/\s+/gu, " ").trim())
|
|
434
|
+
.filter((detail) => detail.length > 0);
|
|
435
|
+
// Section summaries usually lead with neutral context (for example the
|
|
436
|
+
// memory mode/path), while the real recovery action is a later [WARN]/[ERROR]
|
|
437
|
+
// line. Preserve that doctor distinction in the wizard's one-line failure.
|
|
438
|
+
const detail = details.find((candidate) => /^\[(?:WARN|ERROR)\]/u.test(candidate)) ?? details[0];
|
|
439
|
+
if (detail === undefined)
|
|
440
|
+
return undefined;
|
|
441
|
+
if (detail.length <= FIRST_RUN_DOCTOR_DETAIL_MAX_LENGTH)
|
|
442
|
+
return detail;
|
|
443
|
+
return `${detail.slice(0, FIRST_RUN_DOCTOR_DETAIL_MAX_LENGTH - 1).trimEnd()}…`;
|
|
444
|
+
}
|
|
445
|
+
function expectationMismatchReason(expectation, section) {
|
|
446
|
+
const actual = section?.status ?? "missing";
|
|
447
|
+
const label = section?.label.trim() || "Missing validation section";
|
|
448
|
+
const detail = firstActionableDoctorDetail(section);
|
|
449
|
+
return [
|
|
450
|
+
`${expectation.sectionId} must be ${expectation.mustBe}, but is ${actual} (${label} [${expectation.sectionId}]).`,
|
|
451
|
+
detail,
|
|
452
|
+
expectation.note?.trim(),
|
|
453
|
+
].filter((part) => part !== undefined && part.length > 0).join(" ");
|
|
454
|
+
}
|
|
455
|
+
function firstRunConfigurationReasons(options) {
|
|
456
|
+
const reasons = [];
|
|
457
|
+
const seenReasons = new Set();
|
|
458
|
+
const mismatchedSectionIds = new Set();
|
|
459
|
+
const addReason = (reason) => {
|
|
460
|
+
if (seenReasons.has(reason))
|
|
461
|
+
return;
|
|
462
|
+
seenReasons.add(reason);
|
|
463
|
+
reasons.push(reason);
|
|
464
|
+
};
|
|
465
|
+
const byId = new Map(options.report.sections.map((section) => [section.id, section]));
|
|
466
|
+
for (const expectation of options.plan.validateExpectations) {
|
|
467
|
+
const section = byId.get(expectation.sectionId);
|
|
468
|
+
const actual = section?.status;
|
|
469
|
+
if (actual === expectation.mustBe ||
|
|
470
|
+
(options.deferWaitingCredentials && expectation.sectionId === "credentials" && actual === "waiting")) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
mismatchedSectionIds.add(expectation.sectionId);
|
|
474
|
+
addReason(expectationMismatchReason(expectation, section));
|
|
475
|
+
}
|
|
476
|
+
if (!options.report.ok) {
|
|
477
|
+
const errorSections = options.report.sections.filter((section) => section.status === "error");
|
|
478
|
+
for (const section of errorSections) {
|
|
479
|
+
if (mismatchedSectionIds.has(section.id))
|
|
480
|
+
continue;
|
|
481
|
+
const detail = firstActionableDoctorDetail(section);
|
|
482
|
+
addReason(`Validation error in ${section.label} [${section.id}].` +
|
|
483
|
+
(detail === undefined ? " Doctor reported an error." : ` ${detail}`));
|
|
484
|
+
}
|
|
485
|
+
if (errorSections.length === 0) {
|
|
486
|
+
addReason("The complete generated configuration has validation errors.");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (options.secretPersistence.status === "refused") {
|
|
490
|
+
addReason(`Secure secret persistence was refused${options.secretPersistence.reason === undefined ? "" : ` (${options.secretPersistence.reason})`}.` +
|
|
491
|
+
(options.secretPersistence.detail === undefined ? "" : ` ${options.secretPersistence.detail}`));
|
|
492
|
+
}
|
|
493
|
+
return reasons;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Gate the generated capability configuration before any paid or slow model
|
|
497
|
+
* checks. A waiting credential section is deliberately deferred to the exact
|
|
498
|
+
* live route proof; every other selected expectation must already be ready.
|
|
499
|
+
*/
|
|
500
|
+
export function evaluateFirstRunConfigurationReadiness(options) {
|
|
501
|
+
const reasons = firstRunConfigurationReasons({
|
|
502
|
+
...options,
|
|
503
|
+
deferWaitingCredentials: true,
|
|
504
|
+
});
|
|
505
|
+
return { ready: reasons.length === 0, reasons };
|
|
506
|
+
}
|
|
507
|
+
/** A readiness claim is stricter than ValidationReport.ok (which permits waiting). */
|
|
508
|
+
export function evaluateFirstRunReadiness(options) {
|
|
509
|
+
const reasons = firstRunConfigurationReasons({
|
|
510
|
+
plan: options.plan,
|
|
511
|
+
report: options.report,
|
|
512
|
+
secretPersistence: options.secretPersistence,
|
|
513
|
+
deferWaitingCredentials: false,
|
|
514
|
+
});
|
|
515
|
+
const verified = new Set(options.verifiedCredentialModelRefs ?? []);
|
|
516
|
+
for (const modelRef of selectedPersistentRuntimeModelRefs(options.plan)) {
|
|
517
|
+
if (!verified.has(modelRef)) {
|
|
518
|
+
reasons.push(`Runtime route ${modelRef} has not completed its exact live readiness check.`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { ready: reasons.length === 0, reasons };
|
|
522
|
+
}
|
|
523
|
+
function selectedPersistentRuntimeModelRefs(plan) {
|
|
524
|
+
const runtime = (plan.configJson.runtime ?? {});
|
|
525
|
+
const refs = [];
|
|
526
|
+
if (typeof runtime.model === "string" && runtime.model.length > 0)
|
|
527
|
+
refs.push(runtime.model);
|
|
528
|
+
if (Array.isArray(runtime.fallbacks)) {
|
|
529
|
+
for (const raw of runtime.fallbacks) {
|
|
530
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
531
|
+
continue;
|
|
532
|
+
const model = raw.model;
|
|
533
|
+
if (typeof model === "string" && model.length > 0)
|
|
534
|
+
refs.push(model);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else if (Array.isArray(runtime.fallbackModels)) {
|
|
538
|
+
for (const model of runtime.fallbackModels) {
|
|
539
|
+
if (typeof model === "string" && model.length > 0)
|
|
540
|
+
refs.push(model);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return [...new Set(refs)];
|
|
544
|
+
}
|
|
545
|
+
/** Validate the full plan in a disposable folder before touching the target. */
|
|
546
|
+
export async function validateWizardPlanInStaging(options) {
|
|
547
|
+
throwIfStagingAborted(options.abortSignal);
|
|
548
|
+
const dir = await mkdtemp(join(tmpdir(), "mono-agent-first-run-"));
|
|
549
|
+
const configPath = join(dir, "mono-agent.config.json");
|
|
550
|
+
const budget = { count: 0, bytes: 0 };
|
|
551
|
+
const stagedPaths = new Set();
|
|
552
|
+
try {
|
|
553
|
+
throwIfStagingAborted(options.abortSignal);
|
|
554
|
+
const canonicalSourceCwd = options.sourceCwd === undefined
|
|
555
|
+
? undefined
|
|
556
|
+
: await realpath(options.sourceCwd);
|
|
557
|
+
if (canonicalSourceCwd !== undefined && !(await stat(canonicalSourceCwd)).isDirectory()) {
|
|
558
|
+
throw new Error(`Staging source is not a directory: ${options.sourceCwd}`);
|
|
559
|
+
}
|
|
560
|
+
throwIfStagingAborted(options.abortSignal);
|
|
561
|
+
await writeFile(configPath, `${JSON.stringify(options.plan.configJson, null, 2)}\n`, { mode: 0o600 });
|
|
562
|
+
const existingIdentity = canonicalSourceCwd === undefined
|
|
563
|
+
? undefined
|
|
564
|
+
: await readExistingStagingFile({
|
|
565
|
+
canonicalRoot: canonicalSourceCwd,
|
|
566
|
+
pathRelative: "IDENTITY.md",
|
|
567
|
+
label: "existing identity",
|
|
568
|
+
...(options.abortSignal === undefined ? {} : { signal: options.abortSignal }),
|
|
569
|
+
});
|
|
570
|
+
await writeStagedFile({
|
|
571
|
+
stagingRoot: dir,
|
|
572
|
+
pathRelative: "IDENTITY.md",
|
|
573
|
+
contents: existingIdentity ?? Buffer.from("# First-run validation identity\n\nTemporary identity used only for setup validation.\n", "utf8"),
|
|
574
|
+
budget,
|
|
575
|
+
stagedPaths,
|
|
576
|
+
...(options.abortSignal === undefined ? {} : { signal: options.abortSignal }),
|
|
577
|
+
});
|
|
578
|
+
throwIfStagingAborted(options.abortSignal);
|
|
579
|
+
await mkdir(join(dir, ".mono-agent", "workspace"), { recursive: true });
|
|
580
|
+
await mkdir(join(dir, ".mono-agent", "artifacts"), { recursive: true });
|
|
581
|
+
const skillsRoot = options.plan.configJson.context?.skillsRoot;
|
|
582
|
+
if (skillsRoot !== undefined) {
|
|
583
|
+
const sourceSkillsRoot = canonicalSourceCwd === undefined ? undefined : resolve(canonicalSourceCwd, skillsRoot);
|
|
584
|
+
let canonicalSourceSkillsRoot;
|
|
585
|
+
if (sourceSkillsRoot !== undefined && canonicalSourceCwd !== undefined) {
|
|
586
|
+
try {
|
|
587
|
+
if (!(await stat(sourceSkillsRoot)).isDirectory()) {
|
|
588
|
+
throw new Error(`Configured skills root is not a directory: ${sourceSkillsRoot}`);
|
|
589
|
+
}
|
|
590
|
+
canonicalSourceSkillsRoot = await realpath(sourceSkillsRoot);
|
|
591
|
+
if (escapesRoot(relative(canonicalSourceCwd, canonicalSourceSkillsRoot))) {
|
|
592
|
+
throw new Error(`Refusing to stage skills root outside the source agent folder: ${skillsRoot}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
if (error.code !== "ENOENT")
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const stagedSkillsRoot = resolve(dir, skillsRoot);
|
|
601
|
+
const stagedRelative = relative(dir, stagedSkillsRoot);
|
|
602
|
+
if (stagedRelative.length === 0 || escapesRoot(stagedRelative)) {
|
|
603
|
+
throw new Error(`Refusing to stage skills root outside the disposable agent folder: ${skillsRoot}`);
|
|
604
|
+
}
|
|
605
|
+
await mkdir(stagedSkillsRoot, { recursive: true });
|
|
606
|
+
for (const skill of options.plan.configJson.context?.selectedSkills ?? []) {
|
|
607
|
+
throwIfStagingAborted(options.abortSignal);
|
|
608
|
+
const sourceSkillPath = sourceSkillsRoot === undefined ? undefined : resolve(sourceSkillsRoot, skill, "SKILL.md");
|
|
609
|
+
const sourceRelative = sourceSkillsRoot === undefined || sourceSkillPath === undefined
|
|
610
|
+
? undefined
|
|
611
|
+
: relative(sourceSkillsRoot, sourceSkillPath);
|
|
612
|
+
const stagedSkillPath = resolve(stagedSkillsRoot, skill, "SKILL.md");
|
|
613
|
+
const stagedSkillRelative = relative(stagedSkillsRoot, stagedSkillPath);
|
|
614
|
+
if ((sourceRelative !== undefined && escapesRoot(sourceRelative)) || escapesRoot(stagedSkillRelative)) {
|
|
615
|
+
throw new Error(`Refusing to stage a skill outside its configured root: ${skill}`);
|
|
616
|
+
}
|
|
617
|
+
let contents;
|
|
618
|
+
if (canonicalSourceSkillsRoot !== undefined && sourceSkillPath !== undefined) {
|
|
619
|
+
try {
|
|
620
|
+
contents = await readSelectedSkillManifest(sourceSkillPath, canonicalSourceSkillsRoot);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (error.code !== "ENOENT")
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (contents === undefined) {
|
|
628
|
+
const stagedSkillRelativeFromRoot = relative(dir, stagedSkillPath);
|
|
629
|
+
const generated = options.plan.files.find((file) => relative(dir, resolve(dir, file.path)) === stagedSkillRelativeFromRoot);
|
|
630
|
+
if (generated === undefined) {
|
|
631
|
+
throw new Error(`Configured skill is neither present nor generated by the approved plan: ${skill}`);
|
|
632
|
+
}
|
|
633
|
+
contents = Buffer.from(generated.contents, "utf8");
|
|
634
|
+
}
|
|
635
|
+
await mkdir(dirname(stagedSkillPath), { recursive: true });
|
|
636
|
+
// Materialize a fresh regular file. Copying a symlink here would let a
|
|
637
|
+
// later generated-file write follow it outside the disposable tree.
|
|
638
|
+
await writeFile(stagedSkillPath, contents, { flag: "wx", mode: 0o600 });
|
|
639
|
+
stagedPaths.add(relative(dir, stagedSkillPath));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (canonicalSourceCwd !== undefined &&
|
|
643
|
+
options.plan.validateExpectations.some((expectation) => expectation.sectionId === "channel:cron")) {
|
|
644
|
+
await stageExistingCronFiles({
|
|
645
|
+
sourceRoot: canonicalSourceCwd,
|
|
646
|
+
stagingRoot: dir,
|
|
647
|
+
plan: options.plan,
|
|
648
|
+
env: options.env,
|
|
649
|
+
budget,
|
|
650
|
+
stagedPaths,
|
|
651
|
+
...(options.abortSignal === undefined ? {} : { signal: options.abortSignal }),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
for (const file of options.plan.files) {
|
|
655
|
+
throwIfStagingAborted(options.abortSignal);
|
|
656
|
+
const pathRelative = stagingRelativePath(dir, resolve(dir, file.path), `generated file ${file.path}`, false, "the disposable agent folder");
|
|
657
|
+
if (stagedPaths.has(pathRelative))
|
|
658
|
+
continue;
|
|
659
|
+
const existingContents = canonicalSourceCwd === undefined
|
|
660
|
+
? undefined
|
|
661
|
+
: await readExistingStagingFile({
|
|
662
|
+
canonicalRoot: canonicalSourceCwd,
|
|
663
|
+
pathRelative,
|
|
664
|
+
label: `existing plan file ${file.path}`,
|
|
665
|
+
...(options.abortSignal === undefined ? {} : { signal: options.abortSignal }),
|
|
666
|
+
});
|
|
667
|
+
const contents = existingContents ?? Buffer.from(file.contents, "utf8");
|
|
668
|
+
await writeStagedFile({
|
|
669
|
+
stagingRoot: dir,
|
|
670
|
+
pathRelative,
|
|
671
|
+
contents,
|
|
672
|
+
budget,
|
|
673
|
+
stagedPaths,
|
|
674
|
+
...(options.abortSignal === undefined ? {} : { signal: options.abortSignal }),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
throwIfStagingAborted(options.abortSignal);
|
|
678
|
+
await initializeFirstRunManagedMemory({
|
|
679
|
+
agentRoot: dir,
|
|
680
|
+
plan: options.plan,
|
|
681
|
+
env: options.env,
|
|
682
|
+
...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
|
|
683
|
+
});
|
|
684
|
+
throwIfStagingAborted(options.abortSignal);
|
|
685
|
+
const validation = await (options.validate ?? validateMonoAgentFolder)({
|
|
686
|
+
cwd: dir,
|
|
687
|
+
configPath,
|
|
688
|
+
env: options.env,
|
|
689
|
+
// Writes are confined to this disposable directory. Memory/sandbox
|
|
690
|
+
// capabilities need their normal initialization path to prove readiness.
|
|
691
|
+
allowFilesystemWrites: true,
|
|
692
|
+
liveness: true,
|
|
693
|
+
verifiedCredentialModelRefs: options.verifiedCredentialModelRefs,
|
|
694
|
+
});
|
|
695
|
+
throwIfStagingAborted(options.abortSignal);
|
|
696
|
+
return validation;
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
await rm(dir, { recursive: true, force: true });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function readSelectedSkillManifest(path, canonicalSkillsRoot) {
|
|
703
|
+
const canonicalParent = await realpath(dirname(path));
|
|
704
|
+
if (escapesRoot(relative(canonicalSkillsRoot, canonicalParent))) {
|
|
705
|
+
throw new Error(`Refusing to stage a skill manifest outside its configured root: ${path}`);
|
|
706
|
+
}
|
|
707
|
+
const canonicalPath = join(canonicalParent, "SKILL.md");
|
|
708
|
+
let handle;
|
|
709
|
+
try {
|
|
710
|
+
handle = await open(canonicalPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
if (["ELOOP", "EMLINK"].includes(error.code ?? "")) {
|
|
714
|
+
throw new Error(`Refusing to stage symbolic-link skill manifest: ${path}`);
|
|
715
|
+
}
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
if (!(await handle.stat()).isFile()) {
|
|
720
|
+
throw new Error(`Refusing to stage non-regular skill manifest: ${path}`);
|
|
721
|
+
}
|
|
722
|
+
return await handle.readFile();
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
await handle.close();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
//# sourceMappingURL=first-run-readiness.js.map
|