@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,1402 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, realpath, rename, rm, stat, } from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { CodedError } from "@mono-agent/agent-contracts";
|
|
12
|
+
import { createSandboxPolicy, createSrtSandboxEngine, MANAGED_SRT_TREE_SHA256, } from "@mono-agent/runtime-adapter";
|
|
13
|
+
import { currentProcessIncarnation, isSameProcessIncarnation as matchesProcessIncarnation, processIncarnationFromJson, } from "./process-incarnation.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
export const MANAGED_SRT_PACKAGE = "@anthropic-ai/sandbox-runtime";
|
|
16
|
+
export const MANAGED_SRT_VERSION = "0.0.64";
|
|
17
|
+
export const MANAGED_SRT_LOCK_SHA256 = "0f34561ddc700ad43c31f254beb0e1e4b87bae61795acac5d7fa890dd5890924";
|
|
18
|
+
export const MANAGED_SRT_MARKER = ".mono-agent-srt.json";
|
|
19
|
+
const MANAGED_SRT_RESOURCE_ROOT = fileURLToPath(new URL("../resources/srt", import.meta.url));
|
|
20
|
+
const INSTALL_GUARD_NAME = ".install.guard";
|
|
21
|
+
const INSTALL_LOCK_NAME = ".install.lock";
|
|
22
|
+
const INSTALL_LOCK_OWNER_NAME = "owner.json";
|
|
23
|
+
const INSTALL_LOCK_WAIT_MS = 150_000;
|
|
24
|
+
const INSTALL_LOCK_POLL_MS = 100;
|
|
25
|
+
const INSTALL_LOCK_OWNERLESS_GRACE_MS = 5 * 60_000;
|
|
26
|
+
const INSTALL_LOCK_RECORD_MAX_BYTES = 4 * 1024;
|
|
27
|
+
const INSTALL_GUARD_TIMEOUT_EXIT = 75;
|
|
28
|
+
export class SandboxManagerError extends CodedError {
|
|
29
|
+
}
|
|
30
|
+
export function managedSrtInstallRoot(options = {}) {
|
|
31
|
+
return resolve(managedSrtVersionRoot(options), MANAGED_SRT_LOCK_SHA256);
|
|
32
|
+
}
|
|
33
|
+
export async function sandboxRuntimeStatus(options = {}) {
|
|
34
|
+
const installRoot = managedSrtInstallRoot(options);
|
|
35
|
+
const platform = options.platform ?? process.platform;
|
|
36
|
+
if (platform === "darwin" && await pathExists(installRoot)) {
|
|
37
|
+
try {
|
|
38
|
+
await assertTrustedNodeExecutable(resolveNodePath(options));
|
|
39
|
+
const cliPath = await verifyManagedInstall(installRoot, options);
|
|
40
|
+
return {
|
|
41
|
+
state: "ready",
|
|
42
|
+
source: "managed",
|
|
43
|
+
version: MANAGED_SRT_VERSION,
|
|
44
|
+
installRoot,
|
|
45
|
+
nodePath: resolveNodePath(options),
|
|
46
|
+
cliPath,
|
|
47
|
+
message: `Managed SRT ${MANAGED_SRT_VERSION} is installed and integrity-verified.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
state: "corrupt",
|
|
53
|
+
source: "managed",
|
|
54
|
+
version: MANAGED_SRT_VERSION,
|
|
55
|
+
installRoot,
|
|
56
|
+
message: errorMessage(error),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const externalCommand = options.externalCommand === undefined ? "srt" : options.externalCommand;
|
|
61
|
+
if (externalCommand !== false) {
|
|
62
|
+
const available = await createSrtSandboxEngine({ command: externalCommand }).isAvailable();
|
|
63
|
+
if (available) {
|
|
64
|
+
return {
|
|
65
|
+
state: "ready",
|
|
66
|
+
source: "external",
|
|
67
|
+
version: MANAGED_SRT_VERSION,
|
|
68
|
+
installRoot,
|
|
69
|
+
message: `External SRT at ${externalCommand} passed a functional enforcement proof.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return platform === "darwin"
|
|
74
|
+
? {
|
|
75
|
+
state: "absent",
|
|
76
|
+
source: "none",
|
|
77
|
+
version: MANAGED_SRT_VERSION,
|
|
78
|
+
installRoot,
|
|
79
|
+
message: "SRT is not installed. Run `mono-agent sandbox setup` to install the pinned user-cache copy.",
|
|
80
|
+
}
|
|
81
|
+
: {
|
|
82
|
+
state: "unsupported",
|
|
83
|
+
source: "none",
|
|
84
|
+
version: MANAGED_SRT_VERSION,
|
|
85
|
+
installRoot,
|
|
86
|
+
message: `Managed SRT setup is currently supported on macOS only (${platform} detected). Install SRT and its platform prerequisites yourself, then rerun the check.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function setupManagedSrt(options = {}) {
|
|
90
|
+
const platform = options.platform ?? process.platform;
|
|
91
|
+
if (platform !== "darwin") {
|
|
92
|
+
throw new SandboxManagerError("managed_srt_unsupported", `Managed SRT setup is supported on macOS only (${platform} detected). No system packages were changed.`, { platform });
|
|
93
|
+
}
|
|
94
|
+
throwIfAborted(options.signal);
|
|
95
|
+
try {
|
|
96
|
+
assertSupportedNodeVersion(process.versions.node);
|
|
97
|
+
await assertTrustedNodeExecutable(resolveNodePath(options));
|
|
98
|
+
await verifyInstallResources(options.resourceRoot ?? MANAGED_SRT_RESOURCE_ROOT);
|
|
99
|
+
await ensurePrivateVersionRoot(options);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof SandboxManagerError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Managed SRT cache or install resources are unsafe: ${errorMessage(error)}`, { cause: errorMessage(error) });
|
|
106
|
+
}
|
|
107
|
+
const installRoot = managedSrtInstallRoot(options);
|
|
108
|
+
const heldGuard = await acquireInstallGuard(options);
|
|
109
|
+
let heldLock;
|
|
110
|
+
let stagingRoot;
|
|
111
|
+
let result;
|
|
112
|
+
let primaryError;
|
|
113
|
+
try {
|
|
114
|
+
let initial = await inspectManagedOnly(options);
|
|
115
|
+
if (initial.state === "ready") {
|
|
116
|
+
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
117
|
+
result = { installed: false, repaired: false, status: initial, ...(check === undefined ? {} : { check }) };
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
heldLock = await acquireInstallLock(options);
|
|
121
|
+
let repaired = false;
|
|
122
|
+
throwIfAborted(options.signal);
|
|
123
|
+
await cleanupStaleStaging(managedSrtVersionRoot(options));
|
|
124
|
+
initial = await inspectManagedOnly(options);
|
|
125
|
+
if (initial.state === "ready") {
|
|
126
|
+
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
127
|
+
result = { installed: false, repaired: false, status: initial, ...(check === undefined ? {} : { check }) };
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (initial.state === "corrupt") {
|
|
131
|
+
await quarantineManagedInstall(installRoot);
|
|
132
|
+
repaired = true;
|
|
133
|
+
}
|
|
134
|
+
stagingRoot = resolve(managedSrtVersionRoot(options), `.${MANAGED_SRT_LOCK_SHA256}.staging.${process.pid}.${randomUUID()}`);
|
|
135
|
+
await mkdir(stagingRoot, { mode: 0o700 });
|
|
136
|
+
await stageInstallResources(options.resourceRoot ?? MANAGED_SRT_RESOURCE_ROOT, stagingRoot);
|
|
137
|
+
throwIfAborted(options.signal);
|
|
138
|
+
if (options.hooks?.installDependencies !== undefined) {
|
|
139
|
+
await options.hooks.installDependencies(stagingRoot, options.signal);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
await runNpmCi(stagingRoot, options);
|
|
143
|
+
}
|
|
144
|
+
throwIfAborted(options.signal);
|
|
145
|
+
await rm(resolve(stagingRoot, "node_modules", ".bin"), { recursive: true, force: true });
|
|
146
|
+
await makeTreePrivateAndRejectLinks(stagingRoot);
|
|
147
|
+
await writeInstallMarker(stagingRoot, options);
|
|
148
|
+
await verifyManagedInstall(stagingRoot, options);
|
|
149
|
+
try {
|
|
150
|
+
await rename(stagingRoot, installRoot);
|
|
151
|
+
stagingRoot = undefined;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!isNodeError(error, "EEXIST") && !isNodeError(error, "ENOTEMPTY")) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
// A legacy actor can only win this race by bypassing our v0.9 guard.
|
|
158
|
+
// Accept its result solely if it independently passes the integrity check.
|
|
159
|
+
await verifyManagedInstall(installRoot, options);
|
|
160
|
+
}
|
|
161
|
+
const status = await inspectManagedOnly(options);
|
|
162
|
+
if (status.state !== "ready") {
|
|
163
|
+
throw new SandboxManagerError("managed_srt_install_failed", `Managed SRT did not validate after atomic installation: ${status.message}`, { installRoot });
|
|
164
|
+
}
|
|
165
|
+
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
166
|
+
result = { installed: true, repaired, status, ...(check === undefined ? {} : { check }) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
primaryError = managedSrtSetupFailure(error, installRoot);
|
|
172
|
+
}
|
|
173
|
+
const completionErrors = [];
|
|
174
|
+
if (primaryError !== undefined)
|
|
175
|
+
completionErrors.push(primaryError);
|
|
176
|
+
if (stagingRoot !== undefined) {
|
|
177
|
+
try {
|
|
178
|
+
await options.hooks?.beforeInstallStagingCleanup?.(stagingRoot);
|
|
179
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
completionErrors.push(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (heldLock !== undefined) {
|
|
186
|
+
try {
|
|
187
|
+
await releaseInstallLock(heldLock);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
completionErrors.push(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await closeInstallGuard(heldGuard);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
completionErrors.push(error);
|
|
198
|
+
}
|
|
199
|
+
throwCompletionErrors(completionErrors);
|
|
200
|
+
if (result === undefined) {
|
|
201
|
+
throw new Error("Managed SRT setup completed without a result.");
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
export async function checkSandboxRuntime(options = {}) {
|
|
206
|
+
const status = await sandboxRuntimeStatus(options);
|
|
207
|
+
if (status.state !== "ready") {
|
|
208
|
+
throw new SandboxManagerError(status.state === "corrupt" ? "managed_srt_corrupt" : "sandbox_check_failed", status.message, { status });
|
|
209
|
+
}
|
|
210
|
+
throwIfAborted(options.signal);
|
|
211
|
+
let engine;
|
|
212
|
+
if (status.source === "managed") {
|
|
213
|
+
const { nodePath, cliPath } = status;
|
|
214
|
+
if (nodePath === undefined || cliPath === undefined) {
|
|
215
|
+
throw new SandboxManagerError("managed_srt_corrupt", "Managed SRT status omitted its absolute Node or CLI path.", { status });
|
|
216
|
+
}
|
|
217
|
+
engine = createSrtSandboxEngine({
|
|
218
|
+
platform: options.platform ?? process.platform,
|
|
219
|
+
...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),
|
|
220
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
221
|
+
...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
|
|
222
|
+
managedNodePath: nodePath,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
engine = createSrtSandboxEngine({ command: options.externalCommand === undefined ? "srt" : options.externalCommand || "srt" });
|
|
227
|
+
}
|
|
228
|
+
const checks = [];
|
|
229
|
+
if (!(await engine.isAvailable())) {
|
|
230
|
+
throw new SandboxManagerError("sandbox_check_failed", "SRT could be resolved but did not pass its initialization and filesystem enforcement proof.", { source: status.source });
|
|
231
|
+
}
|
|
232
|
+
checks.push({ id: "engine", ok: true, detail: "SRT initialized and enforced a deny boundary." });
|
|
233
|
+
const fixtureRoot = await mkdtemp(resolve(tmpdir(), "mono-agent-srt-check-"));
|
|
234
|
+
const workspace = resolve(fixtureRoot, "workspace");
|
|
235
|
+
const siblingSecret = resolve(fixtureRoot, "sibling-secret.txt");
|
|
236
|
+
const outsideWrite = resolve(fixtureRoot, "outside-write.txt");
|
|
237
|
+
const envPath = resolve(workspace, ".env");
|
|
238
|
+
const allowedPath = resolve(workspace, "allowed.txt");
|
|
239
|
+
const outputPath = resolve(workspace, "output.txt");
|
|
240
|
+
const server = createServer((_request, response) => {
|
|
241
|
+
response.writeHead(200, { "content-type": "text/plain" });
|
|
242
|
+
response.end("mono-agent-srt-ok");
|
|
243
|
+
});
|
|
244
|
+
let requestCount = 0;
|
|
245
|
+
server.on("request", () => {
|
|
246
|
+
requestCount += 1;
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
await mkdir(workspace, { recursive: true });
|
|
250
|
+
await Promise.all([
|
|
251
|
+
writePrivateFile(allowedPath, "allowed\n"),
|
|
252
|
+
writePrivateFile(siblingSecret, "secret\n"),
|
|
253
|
+
writePrivateFile(envPath, "KEEP=true\n"),
|
|
254
|
+
]);
|
|
255
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
256
|
+
server.once("error", rejectListen);
|
|
257
|
+
server.listen(0, "127.0.0.1", () => {
|
|
258
|
+
server.off("error", rejectListen);
|
|
259
|
+
resolveListen();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
const address = server.address();
|
|
263
|
+
if (address === null || typeof address === "string") {
|
|
264
|
+
throw new Error("local functional-check server did not expose a TCP port");
|
|
265
|
+
}
|
|
266
|
+
const policy = createSandboxPolicy({
|
|
267
|
+
root: workspace,
|
|
268
|
+
readableRoots: [workspace],
|
|
269
|
+
writableRoots: [workspace],
|
|
270
|
+
denyWrite: [".env", ".env.*"],
|
|
271
|
+
network: { mode: "localhost" },
|
|
272
|
+
fallback: "fail-closed",
|
|
273
|
+
});
|
|
274
|
+
await runPrepared(engine, policy, {
|
|
275
|
+
command: process.execPath,
|
|
276
|
+
args: [
|
|
277
|
+
"-e",
|
|
278
|
+
"const fs=require('node:fs');const [a,o]=process.argv.slice(1);if(fs.readFileSync(a,'utf8').trim()!=='allowed')process.exit(2);fs.writeFileSync(o,'written');",
|
|
279
|
+
allowedPath,
|
|
280
|
+
outputPath,
|
|
281
|
+
],
|
|
282
|
+
cwd: workspace,
|
|
283
|
+
}, options.signal);
|
|
284
|
+
if (await readFile(outputPath, "utf8") !== "written") {
|
|
285
|
+
throw new Error("allowed write did not persist");
|
|
286
|
+
}
|
|
287
|
+
checks.push({ id: "allowed-filesystem", ok: true, detail: "Allowed workspace read and write succeeded." });
|
|
288
|
+
await expectPreparedDenied(engine, policy, {
|
|
289
|
+
command: process.execPath,
|
|
290
|
+
args: ["-e", "require('node:fs').readFileSync(process.argv[1])", siblingSecret],
|
|
291
|
+
cwd: workspace,
|
|
292
|
+
}, options.signal);
|
|
293
|
+
checks.push({ id: "sibling-read-denied", ok: true, detail: "A sibling secret outside readableRoots was denied." });
|
|
294
|
+
await expectPreparedDenied(engine, policy, {
|
|
295
|
+
command: process.execPath,
|
|
296
|
+
args: ["-e", "require('node:fs').writeFileSync(process.argv[1],'changed')", envPath],
|
|
297
|
+
cwd: workspace,
|
|
298
|
+
}, options.signal);
|
|
299
|
+
if (await readFile(envPath, "utf8") !== "KEEP=true\n") {
|
|
300
|
+
throw new Error(".env content changed despite denyWrite");
|
|
301
|
+
}
|
|
302
|
+
checks.push({ id: "env-write-denied", ok: true, detail: ".env mutation was denied and content stayed intact." });
|
|
303
|
+
await expectPreparedDenied(engine, policy, {
|
|
304
|
+
command: process.execPath,
|
|
305
|
+
args: ["-e", "require('node:fs').writeFileSync(process.argv[1],'bad')", outsideWrite],
|
|
306
|
+
cwd: workspace,
|
|
307
|
+
}, options.signal);
|
|
308
|
+
checks.push({ id: "outside-write-denied", ok: true, detail: "A write outside writableRoots was denied." });
|
|
309
|
+
const allowedUrl = `http://127.0.0.1:${address.port}/allowed`;
|
|
310
|
+
const allowedNetwork = await runPrepared(engine, policy, {
|
|
311
|
+
command: "/usr/bin/curl",
|
|
312
|
+
args: ["--fail", "--silent", "--show-error", "--max-time", "5", allowedUrl],
|
|
313
|
+
cwd: workspace,
|
|
314
|
+
}, options.signal);
|
|
315
|
+
if (allowedNetwork.stdout.trim() !== "mono-agent-srt-ok" || requestCount !== 1) {
|
|
316
|
+
throw new Error("localhost request did not reach the deterministic check server exactly once");
|
|
317
|
+
}
|
|
318
|
+
checks.push({ id: "localhost-allowed", ok: true, detail: "127.0.0.1 reached the local check server." });
|
|
319
|
+
await expectPreparedDenied(engine, policy, {
|
|
320
|
+
command: "/usr/bin/curl",
|
|
321
|
+
args: [
|
|
322
|
+
"--fail",
|
|
323
|
+
"--silent",
|
|
324
|
+
"--show-error",
|
|
325
|
+
"--max-time",
|
|
326
|
+
"5",
|
|
327
|
+
"--resolve",
|
|
328
|
+
`blocked.test:${address.port}:127.0.0.1`,
|
|
329
|
+
`http://blocked.test:${address.port}/blocked`,
|
|
330
|
+
],
|
|
331
|
+
cwd: workspace,
|
|
332
|
+
}, options.signal);
|
|
333
|
+
if (requestCount !== 1) {
|
|
334
|
+
throw new Error("blocked.test bypassed domain filtering and reached the local server");
|
|
335
|
+
}
|
|
336
|
+
checks.push({ id: "domain-denied", ok: true, detail: "blocked.test was denied before reaching the same local server." });
|
|
337
|
+
return { status, checks };
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
throw new SandboxManagerError("sandbox_check_failed", `SRT functional enforcement check failed: ${errorMessage(error)}`, { status, completedChecks: checks.map((check) => check.id), cause: errorMessage(error) });
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
344
|
+
await rm(fixtureRoot, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function inspectManagedOnly(options) {
|
|
348
|
+
return sandboxRuntimeStatus({ ...options, externalCommand: false });
|
|
349
|
+
}
|
|
350
|
+
function managedSrtVersionRoot(options) {
|
|
351
|
+
const cacheRoot = options.cacheRoot ?? defaultCacheRoot(options);
|
|
352
|
+
return resolve(cacheRoot, "mono-agent", "tools", "srt", MANAGED_SRT_VERSION);
|
|
353
|
+
}
|
|
354
|
+
function defaultCacheRoot(options) {
|
|
355
|
+
const env = options.env ?? process.env;
|
|
356
|
+
const xdg = env.XDG_CACHE_HOME?.trim();
|
|
357
|
+
if (xdg !== undefined && xdg.length > 0 && isAbsolute(xdg)) {
|
|
358
|
+
return resolve(xdg);
|
|
359
|
+
}
|
|
360
|
+
const home = options.homeDir ?? homedir();
|
|
361
|
+
return (options.platform ?? process.platform) === "darwin"
|
|
362
|
+
? resolve(home, "Library", "Caches")
|
|
363
|
+
: resolve(home, ".cache");
|
|
364
|
+
}
|
|
365
|
+
async function ensurePrivateVersionRoot(options) {
|
|
366
|
+
const cacheRoot = options.cacheRoot ?? defaultCacheRoot(options);
|
|
367
|
+
const roots = [
|
|
368
|
+
resolve(cacheRoot, "mono-agent"),
|
|
369
|
+
resolve(cacheRoot, "mono-agent", "tools"),
|
|
370
|
+
resolve(cacheRoot, "mono-agent", "tools", "srt"),
|
|
371
|
+
managedSrtVersionRoot(options),
|
|
372
|
+
];
|
|
373
|
+
for (const [index, root] of roots.entries()) {
|
|
374
|
+
try {
|
|
375
|
+
await mkdir(root, { mode: 0o700, recursive: index === 0 });
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (!isNodeError(error, "EEXIST")) {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
await assertPrivateDirectory(root);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function verifyInstallResources(resourceRoot) {
|
|
386
|
+
const lockPath = resolve(resourceRoot, "package-lock.json");
|
|
387
|
+
const packagePath = resolve(resourceRoot, "package.json");
|
|
388
|
+
await assertSingleRegularFile(lockPath, false, false);
|
|
389
|
+
await assertSingleRegularFile(packagePath, false, false);
|
|
390
|
+
const [lockContent, packageContent] = await Promise.all([readFile(lockPath), readFile(packagePath, "utf8")]);
|
|
391
|
+
if (sha256(lockContent) !== MANAGED_SRT_LOCK_SHA256) {
|
|
392
|
+
throw new SandboxManagerError("managed_srt_corrupt", "Bundled SRT package-lock.json failed its pinned SHA-256 check; refusing network installation.", { resourceRoot });
|
|
393
|
+
}
|
|
394
|
+
const manifest = JSON.parse(packageContent);
|
|
395
|
+
if (manifest.dependencies?.[MANAGED_SRT_PACKAGE] !== MANAGED_SRT_VERSION) {
|
|
396
|
+
throw new SandboxManagerError("managed_srt_corrupt", "Bundled SRT package.json does not pin the expected exact version.", { resourceRoot });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function stageInstallResources(resourceRoot, stagingRoot) {
|
|
400
|
+
for (const name of ["package.json", "package-lock.json"]) {
|
|
401
|
+
const source = resolve(resourceRoot, name);
|
|
402
|
+
const target = resolve(stagingRoot, name);
|
|
403
|
+
await copyFile(source, target, fsConstants.COPYFILE_EXCL);
|
|
404
|
+
await chmod(target, 0o600);
|
|
405
|
+
}
|
|
406
|
+
await verifyInstallResources(stagingRoot);
|
|
407
|
+
}
|
|
408
|
+
async function runNpmCi(stagingRoot, options) {
|
|
409
|
+
const nodePath = resolveNodePath(options);
|
|
410
|
+
const npmCliPath = await resolveNpmCliPath(options);
|
|
411
|
+
const env = options.env ?? process.env;
|
|
412
|
+
await execFileAsync(nodePath, [npmCliPath, "ci", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
413
|
+
cwd: stagingRoot,
|
|
414
|
+
env: {
|
|
415
|
+
...env,
|
|
416
|
+
npm_config_audit: "false",
|
|
417
|
+
npm_config_fund: "false",
|
|
418
|
+
npm_config_ignore_scripts: "true",
|
|
419
|
+
npm_config_package_lock: "true",
|
|
420
|
+
},
|
|
421
|
+
timeout: options.installTimeoutMs ?? 120_000,
|
|
422
|
+
signal: options.signal,
|
|
423
|
+
maxBuffer: 4_000_000,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function resolveNodePath(options) {
|
|
427
|
+
const nodePath = options.nodePath ?? process.execPath;
|
|
428
|
+
if (!isAbsolute(nodePath)) {
|
|
429
|
+
throw new SandboxManagerError("managed_srt_install_failed", "Node executable path is not absolute.", { nodePath });
|
|
430
|
+
}
|
|
431
|
+
return nodePath;
|
|
432
|
+
}
|
|
433
|
+
async function resolveNpmCliPath(options) {
|
|
434
|
+
const candidates = [
|
|
435
|
+
options.npmCliPath,
|
|
436
|
+
options.env?.npm_execpath,
|
|
437
|
+
process.env.npm_execpath,
|
|
438
|
+
resolve(dirname(resolveNodePath(options)), "npm"),
|
|
439
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
440
|
+
for (const candidate of candidates) {
|
|
441
|
+
if (!isAbsolute(candidate)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const real = await realpath(candidate);
|
|
446
|
+
const candidateStat = await stat(real);
|
|
447
|
+
if (candidateStat.isFile()
|
|
448
|
+
&& (candidateStat.mode & 0o022) === 0
|
|
449
|
+
&& (process.getuid === undefined || candidateStat.uid === process.getuid() || candidateStat.uid === 0)) {
|
|
450
|
+
return real;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// Try the next deterministic absolute candidate.
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
throw new SandboxManagerError("managed_srt_install_failed", "Could not resolve a trusted absolute npm CLI next to the running Node installation.", { nodePath: resolveNodePath(options) });
|
|
458
|
+
}
|
|
459
|
+
async function assertTrustedNodeExecutable(nodePath) {
|
|
460
|
+
const nodeStat = await lstat(nodePath);
|
|
461
|
+
if (nodeStat.isSymbolicLink() || !nodeStat.isFile()) {
|
|
462
|
+
throw new Error(`Node executable is not a regular non-symbolic file: ${nodePath}`);
|
|
463
|
+
}
|
|
464
|
+
if (nodeStat.nlink !== 1) {
|
|
465
|
+
throw new Error(`Node executable has ${nodeStat.nlink} hard links instead of one: ${nodePath}`);
|
|
466
|
+
}
|
|
467
|
+
if ((nodeStat.mode & 0o022) !== 0) {
|
|
468
|
+
throw new Error(`Node executable is writable by group or other users: ${nodePath}`);
|
|
469
|
+
}
|
|
470
|
+
if (process.getuid !== undefined && nodeStat.uid !== process.getuid() && nodeStat.uid !== 0) {
|
|
471
|
+
throw new Error(`Node executable is not owned by the current user or root: ${nodePath}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function writeInstallMarker(stagingRoot, options) {
|
|
475
|
+
const cliPath = managedCliPath(stagingRoot);
|
|
476
|
+
const packageJsonPath = managedPackageJsonPath(stagingRoot);
|
|
477
|
+
const treeSha256 = await hashPrivateManagedTree(stagingRoot);
|
|
478
|
+
const expectedTreeSha256 = await trustedTreeSha256(stagingRoot, options);
|
|
479
|
+
if (treeSha256 !== expectedTreeSha256) {
|
|
480
|
+
throw new SandboxManagerError("managed_srt_corrupt", "Installed SRT dependency tree does not match the independently pinned release manifest.", { expectedTreeSha256, treeSha256 });
|
|
481
|
+
}
|
|
482
|
+
const marker = {
|
|
483
|
+
schemaVersion: 2,
|
|
484
|
+
package: MANAGED_SRT_PACKAGE,
|
|
485
|
+
version: MANAGED_SRT_VERSION,
|
|
486
|
+
lockSha256: MANAGED_SRT_LOCK_SHA256,
|
|
487
|
+
cliSha256: sha256(await readFile(cliPath)),
|
|
488
|
+
packageJsonSha256: sha256(await readFile(packageJsonPath)),
|
|
489
|
+
treeSha256: expectedTreeSha256,
|
|
490
|
+
};
|
|
491
|
+
const markerPath = resolve(stagingRoot, MANAGED_SRT_MARKER);
|
|
492
|
+
const handle = await open(markerPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
493
|
+
try {
|
|
494
|
+
await handle.writeFile(`${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
495
|
+
await handle.sync();
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await handle.close();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function verifyManagedInstall(installRoot, options) {
|
|
502
|
+
try {
|
|
503
|
+
await assertPrivateDirectory(installRoot);
|
|
504
|
+
let ancestor = dirname(installRoot);
|
|
505
|
+
for (let index = 0; index < 4; index += 1) {
|
|
506
|
+
await assertPrivateDirectory(ancestor);
|
|
507
|
+
ancestor = dirname(ancestor);
|
|
508
|
+
}
|
|
509
|
+
const markerPath = resolve(installRoot, MANAGED_SRT_MARKER);
|
|
510
|
+
const lockPath = resolve(installRoot, "package-lock.json");
|
|
511
|
+
const cliPath = managedCliPath(installRoot);
|
|
512
|
+
const packageJsonPath = managedPackageJsonPath(installRoot);
|
|
513
|
+
for (const path of [markerPath, lockPath, cliPath, packageJsonPath]) {
|
|
514
|
+
await assertSingleRegularFile(path, true);
|
|
515
|
+
}
|
|
516
|
+
const marker = JSON.parse(await readFile(markerPath, "utf8"));
|
|
517
|
+
if (marker.schemaVersion !== 2
|
|
518
|
+
|| marker.package !== MANAGED_SRT_PACKAGE
|
|
519
|
+
|| marker.version !== MANAGED_SRT_VERSION
|
|
520
|
+
|| marker.lockSha256 !== MANAGED_SRT_LOCK_SHA256
|
|
521
|
+
|| !isSha256(marker.cliSha256)
|
|
522
|
+
|| !isSha256(marker.packageJsonSha256)
|
|
523
|
+
|| marker.treeSha256 !== await trustedTreeSha256(installRoot, options)) {
|
|
524
|
+
throw new Error("install marker identity is invalid");
|
|
525
|
+
}
|
|
526
|
+
const [lockHash, cliHash, packageJsonHash, treeHash] = await Promise.all([
|
|
527
|
+
hashFile(lockPath),
|
|
528
|
+
hashFile(cliPath),
|
|
529
|
+
hashFile(packageJsonPath),
|
|
530
|
+
hashPrivateManagedTree(installRoot),
|
|
531
|
+
]);
|
|
532
|
+
if (lockHash !== MANAGED_SRT_LOCK_SHA256) {
|
|
533
|
+
throw new Error("installed package-lock.json differs from the pinned lock");
|
|
534
|
+
}
|
|
535
|
+
if (cliHash !== marker.cliSha256 || packageJsonHash !== marker.packageJsonSha256) {
|
|
536
|
+
throw new Error("installed SRT content differs from its marker");
|
|
537
|
+
}
|
|
538
|
+
if (treeHash !== marker.treeSha256 || treeHash !== await trustedTreeSha256(installRoot, options)) {
|
|
539
|
+
throw new Error("installed SRT dependency tree differs from the independently pinned release manifest");
|
|
540
|
+
}
|
|
541
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
542
|
+
if (packageJson.name !== MANAGED_SRT_PACKAGE || packageJson.version !== MANAGED_SRT_VERSION) {
|
|
543
|
+
throw new Error("installed package name or version is not pinned");
|
|
544
|
+
}
|
|
545
|
+
return cliPath;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Managed SRT install at ${installRoot} is unsafe or corrupt: ${errorMessage(error)}`, { installRoot, cause: errorMessage(error) });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function quarantineManagedInstall(installRoot) {
|
|
552
|
+
const quarantine = `${installRoot}.corrupt.${Date.now()}.${randomUUID()}`;
|
|
553
|
+
try {
|
|
554
|
+
await rename(installRoot, quarantine);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Could not quarantine corrupt managed SRT safely: ${errorMessage(error)}`, { installRoot, quarantine, cause: errorMessage(error) });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function cleanupStaleStaging(versionRoot) {
|
|
561
|
+
const directory = await opendir(versionRoot);
|
|
562
|
+
const prefix = `.${MANAGED_SRT_LOCK_SHA256}.staging.`;
|
|
563
|
+
for await (const entry of directory) {
|
|
564
|
+
if (!entry.name.startsWith(prefix)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const path = resolve(versionRoot, entry.name);
|
|
568
|
+
const entryStat = await lstat(path);
|
|
569
|
+
if (entryStat.isSymbolicLink()
|
|
570
|
+
|| !entryStat.isDirectory()
|
|
571
|
+
|| (entryStat.mode & 0o077) !== 0
|
|
572
|
+
|| (process.getuid !== undefined && entryStat.uid !== process.getuid())) {
|
|
573
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Stale SRT staging entry is unsafe and was left untouched: ${path}`, { path });
|
|
574
|
+
}
|
|
575
|
+
await rm(path, { recursive: true, force: true });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function acquireInstallGuard(options) {
|
|
579
|
+
const guardPath = resolve(managedSrtVersionRoot(options), INSTALL_GUARD_NAME);
|
|
580
|
+
const timeoutMs = options.hooks?.installGuardTimeoutMs ?? INSTALL_LOCK_WAIT_MS;
|
|
581
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
582
|
+
throw installLockUnsafe(guardPath, "Install-guard timeout must be a non-negative finite duration.");
|
|
583
|
+
}
|
|
584
|
+
let handle;
|
|
585
|
+
try {
|
|
586
|
+
handle = await open(guardPath, fsConstants.O_RDWR | fsConstants.O_CREAT | (fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
587
|
+
const beforeHandle = await handle.stat({ bigint: true });
|
|
588
|
+
assertPrivateInstallGuardDetails(beforeHandle, guardPath);
|
|
589
|
+
const identity = installLockIdentity(beforeHandle);
|
|
590
|
+
const beforePath = await lstat(guardPath, { bigint: true });
|
|
591
|
+
assertPrivateInstallGuardDetails(beforePath, guardPath);
|
|
592
|
+
if (!sameInstallLockIdentity(beforePath, identity)) {
|
|
593
|
+
throw installLockUnsafe(guardPath, "Install-guard path did not identify the opened file before locking.");
|
|
594
|
+
}
|
|
595
|
+
await options.hooks?.beforeInstallGuardAcquire?.(guardPath);
|
|
596
|
+
await acquireInstallGuardOsLock(handle, guardPath, timeoutMs, options);
|
|
597
|
+
const afterHandle = await handle.stat({ bigint: true });
|
|
598
|
+
assertPrivateInstallGuardDetails(afterHandle, guardPath);
|
|
599
|
+
if (!sameInstallLockIdentity(afterHandle, identity)) {
|
|
600
|
+
throw installLockUnsafe(guardPath, "Opened install-guard identity changed while acquiring its OS lock.");
|
|
601
|
+
}
|
|
602
|
+
const afterPath = await lstat(guardPath, { bigint: true });
|
|
603
|
+
assertPrivateInstallGuardDetails(afterPath, guardPath);
|
|
604
|
+
if (!sameInstallLockIdentity(afterPath, identity)) {
|
|
605
|
+
throw installLockUnsafe(guardPath, "Install-guard path changed while its OS lock was acquired.");
|
|
606
|
+
}
|
|
607
|
+
await options.hooks?.afterInstallGuardAcquired?.(guardPath);
|
|
608
|
+
return {
|
|
609
|
+
path: guardPath,
|
|
610
|
+
handle,
|
|
611
|
+
identity,
|
|
612
|
+
...(options.hooks?.beforeInstallGuardClose === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: { beforeClose: options.hooks.beforeInstallGuardClose }),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const primary = error instanceof SandboxManagerError || error instanceof AggregateError
|
|
619
|
+
? error
|
|
620
|
+
: installLockUnsafe(guardPath, errorMessage(error));
|
|
621
|
+
if (handle === undefined) {
|
|
622
|
+
throw primary;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
await handle.close();
|
|
626
|
+
}
|
|
627
|
+
catch (closeError) {
|
|
628
|
+
throw aggregateFailures([primary, stageFailure("Install-guard handle close failed", closeError)], "Managed SRT install-guard acquisition and handle close both failed.");
|
|
629
|
+
}
|
|
630
|
+
throw primary;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function acquireInstallGuardOsLock(handle, guardPath, timeoutMs, options) {
|
|
634
|
+
const startedAt = process.hrtime.bigint();
|
|
635
|
+
const elapsedMs = () => Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
636
|
+
// These helpers provide advisory descriptor locks for the local user cache.
|
|
637
|
+
// Network filesystems with non-local flock semantics are deliberately unsupported.
|
|
638
|
+
const helper = process.platform === "darwin"
|
|
639
|
+
? { command: "/usr/bin/lockf", args: ["-s", "-t", "0", "3"] }
|
|
640
|
+
: process.platform === "linux"
|
|
641
|
+
? { command: "/usr/bin/flock", args: ["-E", String(INSTALL_GUARD_TIMEOUT_EXIT), "-x", "-n", "3"] }
|
|
642
|
+
: undefined;
|
|
643
|
+
if (helper === undefined) {
|
|
644
|
+
throw installLockUnsafe(guardPath, `No supported install-guard helper exists on ${process.platform}.`);
|
|
645
|
+
}
|
|
646
|
+
while (true) {
|
|
647
|
+
throwIfAborted(options.signal);
|
|
648
|
+
const outcome = await runInstallGuardHelperAttempt(handle, helper);
|
|
649
|
+
if (outcome.code === 0) {
|
|
650
|
+
throwIfAborted(options.signal);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (outcome.code !== INSTALL_GUARD_TIMEOUT_EXIT) {
|
|
654
|
+
throw installLockUnsafe(guardPath, `Install-guard helper failed (${outcome.signal ?? `exit ${String(outcome.code)}`}): ${outcome.stderr.trim() || "no diagnostic output"}`, { helper: helper.command, exitCode: outcome.code, signal: outcome.signal });
|
|
655
|
+
}
|
|
656
|
+
throwIfAborted(options.signal);
|
|
657
|
+
const elapsed = elapsedMs();
|
|
658
|
+
if (elapsed >= timeoutMs) {
|
|
659
|
+
throw installLockUnsafe(guardPath, `Timed out waiting ${timeoutMs} milliseconds for the managed SRT install guard.`, { helper: helper.command, exitCode: outcome.code });
|
|
660
|
+
}
|
|
661
|
+
await abortableSleep(Math.min(INSTALL_LOCK_POLL_MS, Math.max(0, timeoutMs - elapsed)), options.signal);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function runInstallGuardHelperAttempt(handle, helper) {
|
|
665
|
+
return await new Promise((resolvePromise, reject) => {
|
|
666
|
+
const child = spawn(helper.command, helper.args, {
|
|
667
|
+
stdio: ["ignore", "ignore", "pipe", handle.fd],
|
|
668
|
+
});
|
|
669
|
+
let stderr = "";
|
|
670
|
+
let spawnError;
|
|
671
|
+
child.stderr?.setEncoding("utf8");
|
|
672
|
+
child.stderr?.on("data", (chunk) => {
|
|
673
|
+
if (stderr.length < INSTALL_LOCK_RECORD_MAX_BYTES) {
|
|
674
|
+
stderr += chunk.slice(0, INSTALL_LOCK_RECORD_MAX_BYTES - stderr.length);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
child.once("error", (error) => { spawnError = error; });
|
|
678
|
+
child.once("close", (code, childSignal) => {
|
|
679
|
+
if (spawnError !== undefined) {
|
|
680
|
+
reject(spawnError);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
resolvePromise({ code, signal: childSignal, stderr });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
async function closeInstallGuard(guard) {
|
|
689
|
+
const errors = [];
|
|
690
|
+
try {
|
|
691
|
+
await guard.beforeClose?.(guard.path);
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
errors.push(error);
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
const handleDetails = await guard.handle.stat({ bigint: true });
|
|
698
|
+
assertPrivateInstallGuardDetails(handleDetails, guard.path);
|
|
699
|
+
const pathDetails = await lstat(guard.path, { bigint: true });
|
|
700
|
+
assertPrivateInstallGuardDetails(pathDetails, guard.path);
|
|
701
|
+
if (!sameInstallLockIdentity(handleDetails, guard.identity)
|
|
702
|
+
|| !sameInstallLockIdentity(pathDetails, guard.identity)) {
|
|
703
|
+
throw installLockUnsafe(guard.path, "Install-guard identity changed while the guarded operation was active.");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
errors.push(error instanceof SandboxManagerError
|
|
708
|
+
? error
|
|
709
|
+
: installLockUnsafe(guard.path, errorMessage(error)));
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
await guard.handle.close();
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
errors.push(error);
|
|
716
|
+
}
|
|
717
|
+
throwCompletionErrors(errors, "Managed SRT install-guard validation or close failed.");
|
|
718
|
+
}
|
|
719
|
+
function assertPrivateInstallGuardDetails(details, path) {
|
|
720
|
+
if (!details.isFile()
|
|
721
|
+
|| details.isSymbolicLink()
|
|
722
|
+
|| details.nlink !== 1n
|
|
723
|
+
|| (details.mode & 63n) !== 0n) {
|
|
724
|
+
throw new Error(`install guard ${path} must be an owner-only single-link regular file`);
|
|
725
|
+
}
|
|
726
|
+
if (process.getuid !== undefined && details.uid !== BigInt(process.getuid())) {
|
|
727
|
+
throw new Error(`install guard ${path} is not owned by the current user`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function acquireInstallLock(options) {
|
|
731
|
+
const lockPath = resolve(managedSrtVersionRoot(options), INSTALL_LOCK_NAME);
|
|
732
|
+
const now = options.hooks?.now ?? Date.now;
|
|
733
|
+
const ownerlessGraceMs = options.hooks?.ownerlessGraceMs ?? INSTALL_LOCK_OWNERLESS_GRACE_MS;
|
|
734
|
+
if (!Number.isFinite(ownerlessGraceMs) || ownerlessGraceMs < 0) {
|
|
735
|
+
throw new SandboxManagerError("managed_srt_lock_unsafe", "Managed SRT install-lock ownerless grace must be a non-negative finite duration.", { ownerlessGraceMs });
|
|
736
|
+
}
|
|
737
|
+
const deadline = now() + INSTALL_LOCK_WAIT_MS;
|
|
738
|
+
const incarnation = await (options.hooks?.currentProcessIncarnation ?? currentProcessIncarnation)();
|
|
739
|
+
const isSameProcess = options.hooks?.isSameProcessIncarnation ?? matchesProcessIncarnation;
|
|
740
|
+
while (true) {
|
|
741
|
+
throwIfAborted(options.signal);
|
|
742
|
+
const token = randomUUID();
|
|
743
|
+
let createdIdentity;
|
|
744
|
+
try {
|
|
745
|
+
await mkdir(lockPath, { mode: 0o700 });
|
|
746
|
+
const created = await assertPrivateInstallLockDirectory(lockPath);
|
|
747
|
+
createdIdentity = installLockIdentity(created);
|
|
748
|
+
await options.hooks?.afterInstallLockDirectoryCreated?.(lockPath);
|
|
749
|
+
await assertSamePrivateInstallLockDirectory(lockPath, createdIdentity);
|
|
750
|
+
const record = {
|
|
751
|
+
schemaVersion: 2,
|
|
752
|
+
pid: process.pid,
|
|
753
|
+
uid: process.getuid?.() ?? null,
|
|
754
|
+
token,
|
|
755
|
+
startedAt: new Date(now()).toISOString(),
|
|
756
|
+
incarnation,
|
|
757
|
+
};
|
|
758
|
+
const owner = await publishInstallLockOwner(lockPath, createdIdentity, record);
|
|
759
|
+
await syncPrivateInstallLockDirectory(lockPath, createdIdentity);
|
|
760
|
+
await assertSamePrivateInstallLockDirectory(lockPath, createdIdentity);
|
|
761
|
+
return {
|
|
762
|
+
path: lockPath,
|
|
763
|
+
identity: createdIdentity,
|
|
764
|
+
owner,
|
|
765
|
+
...(options.hooks?.beforeInstallLockReleaseRename === undefined
|
|
766
|
+
? {}
|
|
767
|
+
: { beforeRelease: options.hooks.beforeInstallLockReleaseRename }),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
if (createdIdentity !== undefined) {
|
|
772
|
+
try {
|
|
773
|
+
await abandonCreatedInstallLock(lockPath, createdIdentity);
|
|
774
|
+
}
|
|
775
|
+
catch (cleanupError) {
|
|
776
|
+
throw aggregateFailures([error, stageFailure("Failed install-lock acquisition cleanup", cleanupError)], `Managed SRT install-lock acquisition and cleanup both failed at ${lockPath}.`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (!isNodeError(error, "EEXIST")) {
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const existing = await inspectExistingInstallLock(lockPath);
|
|
784
|
+
if (existing === undefined)
|
|
785
|
+
continue;
|
|
786
|
+
await options.hooks?.afterInstallLockInspected?.(lockPath, existing.kind);
|
|
787
|
+
if (existing.kind === "ownerless") {
|
|
788
|
+
if (now() - existing.mtimeMs >= ownerlessGraceMs) {
|
|
789
|
+
await quarantineStaleInstallLock(lockPath, existing, options);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (now() >= deadline) {
|
|
793
|
+
throw installLockUnsafe(lockPath, "Another installer left a fresh ownerless SRT install lock in its bounded publication window.");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else if (existing.kind === "owned") {
|
|
797
|
+
let sameOwner;
|
|
798
|
+
try {
|
|
799
|
+
sameOwner = await isSameProcess(existing.owner.record.pid, existing.owner.record.incarnation);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
throw installLockUnsafe(lockPath, `Cannot prove the SRT install lock owner's process incarnation: ${errorMessage(error)}`);
|
|
803
|
+
}
|
|
804
|
+
if (!sameOwner) {
|
|
805
|
+
await quarantineStaleInstallLock(lockPath, existing, options);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (now() >= deadline) {
|
|
809
|
+
throw installLockUnsafe(lockPath, `Another live process incarnation (PID ${existing.owner.record.pid}) still owns the SRT install lock.`, { pid: existing.owner.record.pid });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else if (existing.kind === "legacy-publishing") {
|
|
813
|
+
if (now() >= deadline) {
|
|
814
|
+
throw installLockUnsafe(lockPath, "A legacy SRT installer did not finish publishing its lock record; the incomplete lock was left untouched.");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const liveness = options.hooks?.processIsAlive?.(existing.record.pid)
|
|
819
|
+
?? processLiveness(existing.record.pid);
|
|
820
|
+
if (liveness === "dead") {
|
|
821
|
+
await quarantineStaleInstallLock(lockPath, existing, options);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (liveness === "unknown") {
|
|
825
|
+
throw installLockUnsafe(lockPath, `Cannot prove whether legacy SRT install lock owner PID ${existing.record.pid} is alive; refusing to remove it.`, { pid: existing.record.pid });
|
|
826
|
+
}
|
|
827
|
+
if (now() >= deadline) {
|
|
828
|
+
throw installLockUnsafe(lockPath, `Another live legacy process (PID ${existing.record.pid}) still owns the SRT install lock.`, { pid: existing.record.pid });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
await (options.hooks?.sleep ?? abortableSleep)(INSTALL_LOCK_POLL_MS, options.signal);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function inspectExistingInstallLock(lockPath) {
|
|
835
|
+
try {
|
|
836
|
+
const details = await lstat(lockPath, { bigint: true });
|
|
837
|
+
if (details.isSymbolicLink()) {
|
|
838
|
+
throw new Error("lock path must not be a symbolic link");
|
|
839
|
+
}
|
|
840
|
+
assertInstallLockOwned(details, lockPath);
|
|
841
|
+
const identity = installLockIdentity(details);
|
|
842
|
+
if (details.isDirectory()) {
|
|
843
|
+
assertPrivateInstallLockDirectoryDetails(details, lockPath);
|
|
844
|
+
const owner = await readSecureInstallLockOwner(resolve(lockPath, INSTALL_LOCK_OWNER_NAME));
|
|
845
|
+
if (owner === "detached")
|
|
846
|
+
return undefined;
|
|
847
|
+
const current = await samePrivateInstallLockDirectory(lockPath, identity);
|
|
848
|
+
if (!current)
|
|
849
|
+
return undefined;
|
|
850
|
+
if (owner === undefined) {
|
|
851
|
+
const ownerlessDetails = await assertSamePrivateInstallLockDirectory(lockPath, identity);
|
|
852
|
+
return { kind: "ownerless", identity, mtimeMs: Number(ownerlessDetails.mtimeMs) };
|
|
853
|
+
}
|
|
854
|
+
return { kind: "owned", identity, owner };
|
|
855
|
+
}
|
|
856
|
+
if (details.isFile()) {
|
|
857
|
+
return await readSecureLegacyInstallLock(lockPath, identity);
|
|
858
|
+
}
|
|
859
|
+
throw new Error("lock path must be a private directory or a legacy regular file");
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
if (isNodeError(error, "ENOENT"))
|
|
863
|
+
return undefined;
|
|
864
|
+
if (error instanceof SandboxManagerError)
|
|
865
|
+
throw error;
|
|
866
|
+
throw installLockUnsafe(lockPath, errorMessage(error));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function publishInstallLockOwner(lockPath, identity, record) {
|
|
870
|
+
const ownerPath = resolve(lockPath, INSTALL_LOCK_OWNER_NAME);
|
|
871
|
+
const temporaryPath = resolve(lockPath, `.owner.${process.pid}.${record.token}.tmp`);
|
|
872
|
+
const handle = await open(temporaryPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
873
|
+
try {
|
|
874
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
|
|
875
|
+
await handle.sync();
|
|
876
|
+
assertPrivateInstallLockOwnerDetails(await handle.stat({ bigint: true }), temporaryPath);
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
await handle.close();
|
|
880
|
+
}
|
|
881
|
+
await assertSamePrivateInstallLockDirectory(lockPath, identity);
|
|
882
|
+
await rename(temporaryPath, ownerPath);
|
|
883
|
+
await assertSamePrivateInstallLockDirectory(lockPath, identity);
|
|
884
|
+
const owner = await readSecureInstallLockOwner(ownerPath);
|
|
885
|
+
if (owner === undefined || owner === "detached" || owner.record.token !== record.token) {
|
|
886
|
+
throw installLockUnsafe(lockPath, "The atomically published owner record could not be verified.");
|
|
887
|
+
}
|
|
888
|
+
return owner;
|
|
889
|
+
}
|
|
890
|
+
async function readSecureInstallLockOwner(ownerPath) {
|
|
891
|
+
let handle;
|
|
892
|
+
try {
|
|
893
|
+
handle = await open(ownerPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
if (isNodeError(error, "ENOENT"))
|
|
897
|
+
return undefined;
|
|
898
|
+
throw error;
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const before = await handle.stat({ bigint: true });
|
|
902
|
+
if (before.nlink === 0n)
|
|
903
|
+
return "detached";
|
|
904
|
+
assertPrivateInstallLockOwnerDetails(before, ownerPath);
|
|
905
|
+
const content = await readBoundedInstallLockRecord(handle, ownerPath);
|
|
906
|
+
const after = await handle.stat({ bigint: true });
|
|
907
|
+
if (after.nlink === 0n)
|
|
908
|
+
return "detached";
|
|
909
|
+
assertPrivateInstallLockOwnerDetails(after, ownerPath);
|
|
910
|
+
if (!sameInstallLockIdentity(after, installLockIdentity(before))) {
|
|
911
|
+
throw new Error("owner record identity changed while it was read");
|
|
912
|
+
}
|
|
913
|
+
let parsed;
|
|
914
|
+
try {
|
|
915
|
+
parsed = JSON.parse(content);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
throw new Error(`owner record JSON is malformed: ${errorMessage(error)}`);
|
|
919
|
+
}
|
|
920
|
+
const incarnation = processIncarnationFromJson(parsed.incarnation);
|
|
921
|
+
if (!validInstallLockRecordFields(parsed, 2) || incarnation === undefined) {
|
|
922
|
+
throw new Error("owner record is malformed or belongs to another user");
|
|
923
|
+
}
|
|
924
|
+
const valid = parsed;
|
|
925
|
+
return {
|
|
926
|
+
record: {
|
|
927
|
+
schemaVersion: 2,
|
|
928
|
+
pid: valid.pid,
|
|
929
|
+
uid: valid.uid,
|
|
930
|
+
token: valid.token,
|
|
931
|
+
startedAt: valid.startedAt,
|
|
932
|
+
incarnation,
|
|
933
|
+
},
|
|
934
|
+
content,
|
|
935
|
+
identity: installLockIdentity(after),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
await handle.close();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function readSecureLegacyInstallLock(lockPath, observedIdentity) {
|
|
943
|
+
let handle;
|
|
944
|
+
try {
|
|
945
|
+
handle = await open(lockPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
946
|
+
const before = await handle.stat({ bigint: true });
|
|
947
|
+
if (!sameInstallLockIdentity(before, observedIdentity) || before.nlink === 0n)
|
|
948
|
+
return undefined;
|
|
949
|
+
assertPrivateLegacyInstallLockDetails(before, lockPath);
|
|
950
|
+
const content = await readBoundedInstallLockRecord(handle, lockPath);
|
|
951
|
+
const after = await handle.stat({ bigint: true });
|
|
952
|
+
if (after.nlink === 0n)
|
|
953
|
+
return undefined;
|
|
954
|
+
assertPrivateLegacyInstallLockDetails(after, lockPath);
|
|
955
|
+
if (!sameInstallLockIdentity(after, observedIdentity))
|
|
956
|
+
return undefined;
|
|
957
|
+
if (!content.endsWith("\n")) {
|
|
958
|
+
if (!(await sameInstallLockPathIdentity(lockPath, observedIdentity)))
|
|
959
|
+
return undefined;
|
|
960
|
+
return {
|
|
961
|
+
kind: "legacy-publishing",
|
|
962
|
+
identity: observedIdentity,
|
|
963
|
+
content,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
let parsed;
|
|
967
|
+
try {
|
|
968
|
+
parsed = JSON.parse(content);
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
throw new Error(`legacy lock JSON is incomplete or malformed: ${errorMessage(error)}`);
|
|
972
|
+
}
|
|
973
|
+
if (!validInstallLockRecordFields(parsed, 1)) {
|
|
974
|
+
throw new Error("legacy lock record is malformed or belongs to another user");
|
|
975
|
+
}
|
|
976
|
+
const valid = parsed;
|
|
977
|
+
if (!(await sameInstallLockPathIdentity(lockPath, observedIdentity)))
|
|
978
|
+
return undefined;
|
|
979
|
+
return {
|
|
980
|
+
kind: "legacy",
|
|
981
|
+
identity: observedIdentity,
|
|
982
|
+
record: {
|
|
983
|
+
schemaVersion: 1,
|
|
984
|
+
pid: valid.pid,
|
|
985
|
+
uid: valid.uid,
|
|
986
|
+
token: valid.token,
|
|
987
|
+
startedAt: valid.startedAt,
|
|
988
|
+
},
|
|
989
|
+
content,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
finally {
|
|
993
|
+
await handle?.close().catch(() => undefined);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function readBoundedInstallLockRecord(handle, path) {
|
|
997
|
+
const buffer = Buffer.alloc(INSTALL_LOCK_RECORD_MAX_BYTES + 1);
|
|
998
|
+
let offset = 0;
|
|
999
|
+
while (offset < buffer.length) {
|
|
1000
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
1001
|
+
if (bytesRead === 0)
|
|
1002
|
+
break;
|
|
1003
|
+
offset += bytesRead;
|
|
1004
|
+
}
|
|
1005
|
+
if (offset > INSTALL_LOCK_RECORD_MAX_BYTES) {
|
|
1006
|
+
throw new Error(`lock record ${path} exceeds ${INSTALL_LOCK_RECORD_MAX_BYTES} bytes`);
|
|
1007
|
+
}
|
|
1008
|
+
return buffer.subarray(0, offset).toString("utf8");
|
|
1009
|
+
}
|
|
1010
|
+
function validInstallLockRecordFields(record, schemaVersion) {
|
|
1011
|
+
return record.schemaVersion === schemaVersion
|
|
1012
|
+
&& typeof record.pid === "number"
|
|
1013
|
+
&& Number.isSafeInteger(record.pid)
|
|
1014
|
+
&& record.pid > 0
|
|
1015
|
+
&& typeof record.token === "string"
|
|
1016
|
+
&& /^[a-f0-9-]{36}$/iu.test(record.token)
|
|
1017
|
+
&& typeof record.startedAt === "string"
|
|
1018
|
+
&& Number.isFinite(Date.parse(record.startedAt))
|
|
1019
|
+
&& (record.uid === null || (typeof record.uid === "number" && Number.isSafeInteger(record.uid)))
|
|
1020
|
+
&& (process.getuid === undefined || record.uid === process.getuid());
|
|
1021
|
+
}
|
|
1022
|
+
async function quarantineStaleInstallLock(lockPath, expected, options) {
|
|
1023
|
+
await options.hooks?.beforeStaleInstallLockRename?.(lockPath);
|
|
1024
|
+
const current = await inspectExistingInstallLock(lockPath);
|
|
1025
|
+
if (current === undefined)
|
|
1026
|
+
return false;
|
|
1027
|
+
if (!sameExistingInstallLock(current, expected)) {
|
|
1028
|
+
throw installLockUnsafe(lockPath, "SRT install lock changed during stale-lock verification; the replacement was left untouched.");
|
|
1029
|
+
}
|
|
1030
|
+
const quarantinePath = `${lockPath}.stale.${process.pid}.${randomUUID()}`;
|
|
1031
|
+
try {
|
|
1032
|
+
await rename(lockPath, quarantinePath);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
if (isNodeError(error, "ENOENT"))
|
|
1036
|
+
return false;
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
const moved = await inspectExistingInstallLock(quarantinePath);
|
|
1040
|
+
if (moved === undefined || !sameExistingInstallLock(moved, expected)) {
|
|
1041
|
+
throw installLockUnsafe(lockPath, `The stale lock changed across quarantine and was retained at ${quarantinePath}.`, { quarantinePath });
|
|
1042
|
+
}
|
|
1043
|
+
await rm(quarantinePath, { recursive: true, force: true });
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
async function releaseInstallLock(lock) {
|
|
1047
|
+
const expected = {
|
|
1048
|
+
kind: "owned",
|
|
1049
|
+
identity: lock.identity,
|
|
1050
|
+
owner: lock.owner,
|
|
1051
|
+
};
|
|
1052
|
+
const observed = await inspectExistingInstallLock(lock.path);
|
|
1053
|
+
if (observed === undefined || !sameExistingInstallLock(observed, expected)) {
|
|
1054
|
+
throw installLockUnsafe(lock.path, "SRT install lock identity or owner changed before release; another lock was left untouched.");
|
|
1055
|
+
}
|
|
1056
|
+
await lock.beforeRelease?.(lock.path);
|
|
1057
|
+
const current = await inspectExistingInstallLock(lock.path);
|
|
1058
|
+
if (current === undefined || !sameExistingInstallLock(current, expected)) {
|
|
1059
|
+
throw installLockUnsafe(lock.path, "SRT install lock changed at the release boundary; the replacement was left untouched.");
|
|
1060
|
+
}
|
|
1061
|
+
const releasedPath = `${lock.path}.released.${process.pid}.${randomUUID()}`;
|
|
1062
|
+
try {
|
|
1063
|
+
await rename(lock.path, releasedPath);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1067
|
+
throw installLockUnsafe(lock.path, "SRT install lock disappeared during release.");
|
|
1068
|
+
}
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
const moved = await inspectExistingInstallLock(releasedPath);
|
|
1072
|
+
if (moved === undefined || !sameExistingInstallLock(moved, expected)) {
|
|
1073
|
+
throw installLockUnsafe(lock.path, `SRT install lock changed across release and was retained at ${releasedPath}.`, { releasedPath });
|
|
1074
|
+
}
|
|
1075
|
+
await rm(releasedPath, { recursive: true, force: true });
|
|
1076
|
+
}
|
|
1077
|
+
async function abandonCreatedInstallLock(lockPath, identity) {
|
|
1078
|
+
if (!(await samePrivateInstallLockDirectory(lockPath, identity)))
|
|
1079
|
+
return;
|
|
1080
|
+
const abandonedPath = `${lockPath}.abandoned.${process.pid}.${randomUUID()}`;
|
|
1081
|
+
try {
|
|
1082
|
+
await rename(lockPath, abandonedPath);
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
if (isNodeError(error, "ENOENT"))
|
|
1086
|
+
return;
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
const moved = await lstat(abandonedPath, { bigint: true });
|
|
1090
|
+
if (!sameInstallLockIdentity(moved, identity)) {
|
|
1091
|
+
throw installLockUnsafe(lockPath, `A failed acquisition changed across quarantine and was retained at ${abandonedPath}.`, { abandonedPath });
|
|
1092
|
+
}
|
|
1093
|
+
await rm(abandonedPath, { recursive: true, force: true });
|
|
1094
|
+
}
|
|
1095
|
+
async function assertPrivateInstallLockDirectory(path) {
|
|
1096
|
+
const details = await lstat(path, { bigint: true });
|
|
1097
|
+
assertPrivateInstallLockDirectoryDetails(details, path);
|
|
1098
|
+
return details;
|
|
1099
|
+
}
|
|
1100
|
+
function assertPrivateInstallLockDirectoryDetails(details, path) {
|
|
1101
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
1102
|
+
throw new Error("lock path must be a real directory");
|
|
1103
|
+
}
|
|
1104
|
+
assertInstallLockOwned(details, path);
|
|
1105
|
+
if ((details.mode & 63n) !== 0n) {
|
|
1106
|
+
throw new Error("lock directory must be owner-only (mode 0700)");
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function assertPrivateInstallLockOwnerDetails(details, path) {
|
|
1110
|
+
if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1n || (details.mode & 63n) !== 0n) {
|
|
1111
|
+
throw new Error(`lock owner ${path} must be an owner-only single-link regular file`);
|
|
1112
|
+
}
|
|
1113
|
+
assertInstallLockOwned(details, path);
|
|
1114
|
+
}
|
|
1115
|
+
function assertPrivateLegacyInstallLockDetails(details, path) {
|
|
1116
|
+
if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1n || (details.mode & 63n) !== 0n) {
|
|
1117
|
+
throw new Error(`legacy lock ${path} must be an owner-only single-link regular file`);
|
|
1118
|
+
}
|
|
1119
|
+
assertInstallLockOwned(details, path);
|
|
1120
|
+
}
|
|
1121
|
+
function assertInstallLockOwned(details, path) {
|
|
1122
|
+
if (process.getuid !== undefined && details.uid !== BigInt(process.getuid())) {
|
|
1123
|
+
throw new Error(`lock path ${path} is not owned by the current user`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async function assertSamePrivateInstallLockDirectory(path, identity) {
|
|
1127
|
+
const details = await assertPrivateInstallLockDirectory(path);
|
|
1128
|
+
if (!sameInstallLockIdentity(details, identity)) {
|
|
1129
|
+
throw installLockUnsafe(path, "SRT install lock directory identity changed.");
|
|
1130
|
+
}
|
|
1131
|
+
return details;
|
|
1132
|
+
}
|
|
1133
|
+
async function samePrivateInstallLockDirectory(path, identity) {
|
|
1134
|
+
try {
|
|
1135
|
+
return sameInstallLockIdentity(await assertPrivateInstallLockDirectory(path), identity);
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
if (isNodeError(error, "ENOENT"))
|
|
1139
|
+
return false;
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function sameInstallLockPathIdentity(path, identity) {
|
|
1144
|
+
try {
|
|
1145
|
+
return sameInstallLockIdentity(await lstat(path, { bigint: true }), identity);
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
if (isNodeError(error, "ENOENT"))
|
|
1149
|
+
return false;
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
async function syncPrivateInstallLockDirectory(path, identity) {
|
|
1154
|
+
const handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0) | (fsConstants.O_NOFOLLOW ?? 0));
|
|
1155
|
+
try {
|
|
1156
|
+
const details = await handle.stat({ bigint: true });
|
|
1157
|
+
assertPrivateInstallLockDirectoryDetails(details, path);
|
|
1158
|
+
if (!sameInstallLockIdentity(details, identity)) {
|
|
1159
|
+
throw installLockUnsafe(path, "SRT install lock changed before its owner publication was synced.");
|
|
1160
|
+
}
|
|
1161
|
+
await handle.sync();
|
|
1162
|
+
}
|
|
1163
|
+
finally {
|
|
1164
|
+
await handle.close();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function installLockIdentity(details) {
|
|
1168
|
+
return { dev: details.dev, ino: details.ino };
|
|
1169
|
+
}
|
|
1170
|
+
function sameInstallLockIdentity(details, identity) {
|
|
1171
|
+
return details.dev === identity.dev && details.ino === identity.ino;
|
|
1172
|
+
}
|
|
1173
|
+
function sameExistingInstallLock(left, right) {
|
|
1174
|
+
if (left.kind !== right.kind || !sameInstallLockIdentity(left.identity, right.identity))
|
|
1175
|
+
return false;
|
|
1176
|
+
if (left.kind === "ownerless" && right.kind === "ownerless")
|
|
1177
|
+
return true;
|
|
1178
|
+
if (left.kind === "owned" && right.kind === "owned") {
|
|
1179
|
+
return left.owner.record.token === right.owner.record.token
|
|
1180
|
+
&& left.owner.content === right.owner.content
|
|
1181
|
+
&& sameInstallLockIdentity(left.owner.identity, right.owner.identity);
|
|
1182
|
+
}
|
|
1183
|
+
if (left.kind === "legacy" && right.kind === "legacy") {
|
|
1184
|
+
return left.record.token === right.record.token && left.content === right.content;
|
|
1185
|
+
}
|
|
1186
|
+
return left.kind === "legacy-publishing"
|
|
1187
|
+
&& right.kind === "legacy-publishing"
|
|
1188
|
+
&& left.content === right.content;
|
|
1189
|
+
}
|
|
1190
|
+
function installLockUnsafe(lockPath, cause, details = {}) {
|
|
1191
|
+
return new SandboxManagerError("managed_srt_lock_unsafe", `SRT install lock is unsafe and was left untouched: ${cause}`, { lockPath, cause, ...details });
|
|
1192
|
+
}
|
|
1193
|
+
function managedSrtSetupFailure(error, installRoot) {
|
|
1194
|
+
if (error instanceof SandboxManagerError || error instanceof AggregateError)
|
|
1195
|
+
return error;
|
|
1196
|
+
return new SandboxManagerError("managed_srt_install_failed", `Managed SRT installation failed without changing PATH or system packages: ${errorMessage(error)}`, { installRoot, cause: errorMessage(error) });
|
|
1197
|
+
}
|
|
1198
|
+
function stageFailure(stage, error) {
|
|
1199
|
+
return new Error(`${stage}: ${errorMessage(error)}`, { cause: error });
|
|
1200
|
+
}
|
|
1201
|
+
function throwCompletionErrors(errors, message = "Managed SRT setup and one or more cleanup or release stages failed.") {
|
|
1202
|
+
if (errors.length === 0)
|
|
1203
|
+
return;
|
|
1204
|
+
if (errors.length === 1)
|
|
1205
|
+
throw errors[0];
|
|
1206
|
+
throw aggregateFailures(errors, message);
|
|
1207
|
+
}
|
|
1208
|
+
function aggregateFailures(errors, message) {
|
|
1209
|
+
const summary = errors
|
|
1210
|
+
.map((error) => errorMessage(error).replace(/\s+/gu, " ").slice(0, 500))
|
|
1211
|
+
.join(" | ");
|
|
1212
|
+
return new AggregateError(errors, `${message} Failures: ${summary}`);
|
|
1213
|
+
}
|
|
1214
|
+
function processLiveness(pid) {
|
|
1215
|
+
try {
|
|
1216
|
+
process.kill(pid, 0);
|
|
1217
|
+
return "alive";
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
if (isNodeError(error, "ESRCH")) {
|
|
1221
|
+
return "dead";
|
|
1222
|
+
}
|
|
1223
|
+
if (isNodeError(error, "EPERM")) {
|
|
1224
|
+
return "alive";
|
|
1225
|
+
}
|
|
1226
|
+
return "unknown";
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async function makeTreePrivateAndRejectLinks(root) {
|
|
1230
|
+
const directory = await opendir(root);
|
|
1231
|
+
for await (const entry of directory) {
|
|
1232
|
+
const path = resolve(root, entry.name);
|
|
1233
|
+
const entryStat = await lstat(path);
|
|
1234
|
+
if (entryStat.isSymbolicLink()) {
|
|
1235
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Managed SRT staging tree contains an unexpected symbolic link: ${path}`, { path });
|
|
1236
|
+
}
|
|
1237
|
+
if (entryStat.isDirectory()) {
|
|
1238
|
+
await chmod(path, 0o700);
|
|
1239
|
+
await makeTreePrivateAndRejectLinks(path);
|
|
1240
|
+
}
|
|
1241
|
+
else if (entryStat.isFile() && entryStat.nlink === 1) {
|
|
1242
|
+
await chmod(path, 0o600);
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
throw new SandboxManagerError("managed_srt_corrupt", `Managed SRT staging tree contains an unsupported filesystem entry: ${path}`, { path });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
await chmod(root, 0o700);
|
|
1249
|
+
}
|
|
1250
|
+
async function hashPrivateManagedTree(installRoot) {
|
|
1251
|
+
const hash = createHash("sha256");
|
|
1252
|
+
async function walk(directoryPath) {
|
|
1253
|
+
const directory = await opendir(directoryPath);
|
|
1254
|
+
const entries = [];
|
|
1255
|
+
for await (const entry of directory) {
|
|
1256
|
+
entries.push(entry);
|
|
1257
|
+
}
|
|
1258
|
+
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1259
|
+
for (const entry of entries) {
|
|
1260
|
+
const path = resolve(directoryPath, entry.name);
|
|
1261
|
+
const relativePath = relative(installRoot, path);
|
|
1262
|
+
if (relativePath === MANAGED_SRT_MARKER || relativePath === "node_modules/.package-lock.json") {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const entryStat = await lstat(path);
|
|
1266
|
+
if (entryStat.isSymbolicLink()
|
|
1267
|
+
|| (process.getuid !== undefined && entryStat.uid !== process.getuid())
|
|
1268
|
+
|| (entryStat.mode & 0o077) !== 0) {
|
|
1269
|
+
throw new Error(`unsafe managed dependency tree entry: ${relativePath}`);
|
|
1270
|
+
}
|
|
1271
|
+
if (entryStat.isDirectory()) {
|
|
1272
|
+
hash.update(`D\0${relativePath}\0`);
|
|
1273
|
+
await walk(path);
|
|
1274
|
+
}
|
|
1275
|
+
else if (entryStat.isFile() && entryStat.nlink === 1) {
|
|
1276
|
+
hash.update(`F\0${relativePath}\0${entryStat.size}\0`);
|
|
1277
|
+
hash.update(await readFile(path));
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
throw new Error(`unsupported managed dependency tree entry: ${relativePath}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
await walk(installRoot);
|
|
1285
|
+
return hash.digest("hex");
|
|
1286
|
+
}
|
|
1287
|
+
async function trustedTreeSha256(installRoot, options) {
|
|
1288
|
+
return options.hooks?.expectedTreeSha256 === undefined
|
|
1289
|
+
? MANAGED_SRT_TREE_SHA256
|
|
1290
|
+
: await options.hooks.expectedTreeSha256(installRoot);
|
|
1291
|
+
}
|
|
1292
|
+
async function assertPrivateDirectory(path) {
|
|
1293
|
+
const pathStat = await lstat(path);
|
|
1294
|
+
if (pathStat.isSymbolicLink()
|
|
1295
|
+
|| !pathStat.isDirectory()
|
|
1296
|
+
|| (pathStat.mode & 0o077) !== 0
|
|
1297
|
+
|| (process.getuid !== undefined && pathStat.uid !== process.getuid())) {
|
|
1298
|
+
throw new Error(`${path} must be a real owner-only directory owned by the current user`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function assertSingleRegularFile(path, privateFile, requireSingleLink = true) {
|
|
1302
|
+
const pathStat = await lstat(path);
|
|
1303
|
+
if (pathStat.isSymbolicLink()
|
|
1304
|
+
|| !pathStat.isFile()
|
|
1305
|
+
|| (requireSingleLink && pathStat.nlink !== 1)
|
|
1306
|
+
|| (privateFile && (pathStat.mode & 0o077) !== 0)) {
|
|
1307
|
+
throw new Error(`${path} must be a ${privateFile ? "private " : ""}${requireSingleLink ? "single-link " : ""}regular file`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function runPrepared(engine, policy, spec, signal) {
|
|
1311
|
+
let prepared;
|
|
1312
|
+
try {
|
|
1313
|
+
prepared = await engine.prepareCommand(spec, policy);
|
|
1314
|
+
const result = await execFileAsync(prepared.command, [...prepared.args], {
|
|
1315
|
+
cwd: prepared.cwd,
|
|
1316
|
+
env: prepared.env === undefined ? process.env : { ...process.env, ...prepared.env },
|
|
1317
|
+
timeout: 15_000,
|
|
1318
|
+
signal,
|
|
1319
|
+
maxBuffer: 1_000_000,
|
|
1320
|
+
});
|
|
1321
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
await prepared?.cleanup?.().catch(() => undefined);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function expectPreparedDenied(engine, policy, spec, signal) {
|
|
1328
|
+
try {
|
|
1329
|
+
await runPrepared(engine, policy, spec, signal);
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
const candidate = error;
|
|
1333
|
+
const evidence = [candidate.message, candidate.stdout, candidate.stderr]
|
|
1334
|
+
.filter((value) => typeof value === "string")
|
|
1335
|
+
.join("\n");
|
|
1336
|
+
if (/operation not permitted|permission denied|eacces|eperm|not allowed|denied|http[^\n]*403|requested url returned error:\s*403/iu.test(evidence)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
throw new Error(`Command failed without sandbox-denial evidence: ${evidence || errorMessage(error)}`);
|
|
1340
|
+
}
|
|
1341
|
+
throw new Error(`Sandbox unexpectedly allowed: ${spec.command} ${spec.args.join(" ")}`);
|
|
1342
|
+
}
|
|
1343
|
+
async function writePrivateFile(path, content) {
|
|
1344
|
+
const handle = await open(path, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
1345
|
+
try {
|
|
1346
|
+
await handle.writeFile(content, "utf8");
|
|
1347
|
+
}
|
|
1348
|
+
finally {
|
|
1349
|
+
await handle.close();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function managedCliPath(installRoot) {
|
|
1353
|
+
return resolve(installRoot, "node_modules", "@anthropic-ai", "sandbox-runtime", "dist", "cli.js");
|
|
1354
|
+
}
|
|
1355
|
+
function managedPackageJsonPath(installRoot) {
|
|
1356
|
+
return resolve(installRoot, "node_modules", "@anthropic-ai", "sandbox-runtime", "package.json");
|
|
1357
|
+
}
|
|
1358
|
+
async function hashFile(path) {
|
|
1359
|
+
return sha256(await readFile(path));
|
|
1360
|
+
}
|
|
1361
|
+
function sha256(content) {
|
|
1362
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1363
|
+
}
|
|
1364
|
+
function isSha256(value) {
|
|
1365
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
1366
|
+
}
|
|
1367
|
+
function assertSupportedNodeVersion(version) {
|
|
1368
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/u.exec(version);
|
|
1369
|
+
if (match === null) {
|
|
1370
|
+
throw new SandboxManagerError("managed_srt_install_failed", `Could not parse Node version ${version}.`);
|
|
1371
|
+
}
|
|
1372
|
+
const major = Number(match[1]);
|
|
1373
|
+
const minor = Number(match[2]);
|
|
1374
|
+
if (major < 20 || (major === 20 && minor < 11)) {
|
|
1375
|
+
throw new SandboxManagerError("managed_srt_install_failed", `Managed SRT requires Node >=20.11.0; current Node is ${version}.`, { version });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async function pathExists(path) {
|
|
1379
|
+
try {
|
|
1380
|
+
await lstat(path);
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
throw error;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async function abortableSleep(milliseconds, signal) {
|
|
1391
|
+
await delay(milliseconds, undefined, signal === undefined ? undefined : { signal });
|
|
1392
|
+
}
|
|
1393
|
+
function throwIfAborted(signal) {
|
|
1394
|
+
signal?.throwIfAborted();
|
|
1395
|
+
}
|
|
1396
|
+
function errorMessage(error) {
|
|
1397
|
+
return error instanceof Error ? error.message : String(error);
|
|
1398
|
+
}
|
|
1399
|
+
function isNodeError(error, code) {
|
|
1400
|
+
return error instanceof Error && error.code === code;
|
|
1401
|
+
}
|
|
1402
|
+
//# sourceMappingURL=sandbox-manager.js.map
|