@mono-agent/agent-app 0.8.0 → 0.9.1
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 +71 -22
- 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/app-config.d.ts +3 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -5
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +25 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +237 -16
- 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/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 +71 -3
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +12 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +341 -63
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +253 -0
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-tool.d.ts +7 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -1
- package/dist/configuration-proposal-tool.js +55 -6
- package/dist/configuration-proposal-tool.js.map +1 -1
- package/dist/configured-agent.d.ts +4 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +15 -0
- package/dist/configured-agent.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 +2 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +426 -27
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts.map +1 -1
- package/dist/first-run-managed-memory.js +66 -3
- package/dist/first-run-managed-memory.js.map +1 -1
- package/dist/first-run-readiness.d.ts.map +1 -1
- package/dist/first-run-readiness.js +2 -31
- package/dist/first-run-readiness.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +7 -2
- package/dist/init.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 +61 -18
- package/dist/local-configuration.d.ts.map +1 -1
- package/dist/local-configuration.js +530 -145
- package/dist/local-configuration.js.map +1 -1
- 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-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 +5 -4
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +21 -8
- package/dist/memory-recall.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +91 -9
- package/dist/modules/catalog.js.map +1 -1
- 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 +1 -1
- package/dist/project-skills.js +6 -6
- package/dist/sandbox-manager.d.ts +17 -0
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox-manager.js +673 -127
- package/dist/sandbox-manager.js.map +1 -1
- package/dist/supermemory-plugin.d.ts +5 -3
- package/dist/supermemory-plugin.d.ts.map +1 -1
- package/dist/supermemory-plugin.js +11 -5
- package/dist/supermemory-plugin.js.map +1 -1
- package/dist/tui-command.d.ts +43 -2
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +261 -27
- package/dist/tui-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +4 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +4 -21
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +162 -5
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +206 -3
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +13 -7
- package/skills/mono-agent-composer/references/discovery-questions.md +26 -11
- package/skills/mono-agent-composer/references/feature-coverage.md +9 -9
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +2 -2
package/dist/sandbox-manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { constants as fsConstants } from "node:fs";
|
|
4
|
-
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, realpath, rename, rm, stat,
|
|
4
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, realpath, rename, rm, stat, } from "node:fs/promises";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { homedir, tmpdir } from "node:os";
|
|
7
7
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
@@ -10,14 +10,21 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { CodedError } from "@mono-agent/agent-contracts";
|
|
12
12
|
import { createSandboxPolicy, createSrtSandboxEngine, MANAGED_SRT_TREE_SHA256, } from "@mono-agent/runtime-adapter";
|
|
13
|
+
import { currentProcessIncarnation, isSameProcessIncarnation as matchesProcessIncarnation, processIncarnationFromJson, } from "./process-incarnation.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
export const MANAGED_SRT_PACKAGE = "@anthropic-ai/sandbox-runtime";
|
|
15
16
|
export const MANAGED_SRT_VERSION = "0.0.64";
|
|
16
17
|
export const MANAGED_SRT_LOCK_SHA256 = "0f34561ddc700ad43c31f254beb0e1e4b87bae61795acac5d7fa890dd5890924";
|
|
17
18
|
export const MANAGED_SRT_MARKER = ".mono-agent-srt.json";
|
|
18
19
|
const MANAGED_SRT_RESOURCE_ROOT = fileURLToPath(new URL("../resources/srt", import.meta.url));
|
|
20
|
+
const INSTALL_GUARD_NAME = ".install.guard";
|
|
19
21
|
const INSTALL_LOCK_NAME = ".install.lock";
|
|
22
|
+
const INSTALL_LOCK_OWNER_NAME = "owner.json";
|
|
20
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;
|
|
21
28
|
export class SandboxManagerError extends CodedError {
|
|
22
29
|
}
|
|
23
30
|
export function managedSrtInstallRoot(options = {}) {
|
|
@@ -98,76 +105,102 @@ export async function setupManagedSrt(options = {}) {
|
|
|
98
105
|
throw new SandboxManagerError("managed_srt_corrupt", `Managed SRT cache or install resources are unsafe: ${errorMessage(error)}`, { cause: errorMessage(error) });
|
|
99
106
|
}
|
|
100
107
|
const installRoot = managedSrtInstallRoot(options);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
104
|
-
return { installed: false, repaired: false, status: initial, ...(check === undefined ? {} : { check }) };
|
|
105
|
-
}
|
|
106
|
-
const heldLock = await acquireInstallLock(options);
|
|
108
|
+
const heldGuard = await acquireInstallGuard(options);
|
|
109
|
+
let heldLock;
|
|
107
110
|
let stagingRoot;
|
|
108
|
-
let
|
|
111
|
+
let result;
|
|
112
|
+
let primaryError;
|
|
109
113
|
try {
|
|
110
|
-
|
|
111
|
-
await cleanupStaleStaging(managedSrtVersionRoot(options));
|
|
112
|
-
initial = await inspectManagedOnly(options);
|
|
114
|
+
let initial = await inspectManagedOnly(options);
|
|
113
115
|
if (initial.state === "ready") {
|
|
114
116
|
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (initial.state === "corrupt") {
|
|
118
|
-
await quarantineManagedInstall(installRoot);
|
|
119
|
-
repaired = true;
|
|
120
|
-
}
|
|
121
|
-
stagingRoot = resolve(managedSrtVersionRoot(options), `.${MANAGED_SRT_LOCK_SHA256}.staging.${process.pid}.${randomUUID()}`);
|
|
122
|
-
await mkdir(stagingRoot, { mode: 0o700 });
|
|
123
|
-
await stageInstallResources(options.resourceRoot ?? MANAGED_SRT_RESOURCE_ROOT, stagingRoot);
|
|
124
|
-
throwIfAborted(options.signal);
|
|
125
|
-
if (options.hooks?.installDependencies !== undefined) {
|
|
126
|
-
await options.hooks.installDependencies(stagingRoot, options.signal);
|
|
117
|
+
result = { installed: false, repaired: false, status: initial, ...(check === undefined ? {} : { check }) };
|
|
127
118
|
}
|
|
128
119
|
else {
|
|
129
|
-
await
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 }) };
|
|
143
167
|
}
|
|
144
|
-
// Another actor can only win this race by bypassing our lock. Accept its
|
|
145
|
-
// result solely if it independently passes the full integrity check.
|
|
146
|
-
await verifyManagedInstall(installRoot, options);
|
|
147
|
-
}
|
|
148
|
-
const status = await inspectManagedOnly(options);
|
|
149
|
-
if (status.state !== "ready") {
|
|
150
|
-
throw new SandboxManagerError("managed_srt_install_failed", `Managed SRT did not validate after atomic installation: ${status.message}`, { installRoot });
|
|
151
168
|
}
|
|
152
|
-
const check = options.verify === false ? undefined : await checkSandboxRuntime(options);
|
|
153
|
-
return { installed: true, repaired, status, ...(check === undefined ? {} : { check }) };
|
|
154
169
|
}
|
|
155
170
|
catch (error) {
|
|
156
|
-
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
throw new SandboxManagerError("managed_srt_install_failed", `Managed SRT installation failed without changing PATH or system packages: ${errorMessage(error)}`, { installRoot, cause: errorMessage(error) });
|
|
171
|
+
primaryError = managedSrtSetupFailure(error, installRoot);
|
|
160
172
|
}
|
|
161
|
-
|
|
173
|
+
const completionErrors = [];
|
|
174
|
+
if (primaryError !== undefined)
|
|
175
|
+
completionErrors.push(primaryError);
|
|
176
|
+
if (stagingRoot !== undefined) {
|
|
162
177
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
178
|
+
await options.hooks?.beforeInstallStagingCleanup?.(stagingRoot);
|
|
179
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
166
180
|
}
|
|
167
|
-
|
|
181
|
+
catch (error) {
|
|
182
|
+
completionErrors.push(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (heldLock !== undefined) {
|
|
186
|
+
try {
|
|
168
187
|
await releaseInstallLock(heldLock);
|
|
169
188
|
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
completionErrors.push(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await closeInstallGuard(heldGuard);
|
|
170
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;
|
|
171
204
|
}
|
|
172
205
|
export async function checkSandboxRuntime(options = {}) {
|
|
173
206
|
const status = await sandboxRuntimeStatus(options);
|
|
@@ -542,128 +575,641 @@ async function cleanupStaleStaging(versionRoot) {
|
|
|
542
575
|
await rm(path, { recursive: true, force: true });
|
|
543
576
|
}
|
|
544
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
|
+
}
|
|
545
730
|
async function acquireInstallLock(options) {
|
|
546
731
|
const lockPath = resolve(managedSrtVersionRoot(options), INSTALL_LOCK_NAME);
|
|
547
|
-
const
|
|
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;
|
|
548
740
|
while (true) {
|
|
549
741
|
throwIfAborted(options.signal);
|
|
550
742
|
const token = randomUUID();
|
|
551
|
-
let
|
|
743
|
+
let createdIdentity;
|
|
552
744
|
try {
|
|
553
|
-
|
|
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);
|
|
554
750
|
const record = {
|
|
555
|
-
schemaVersion:
|
|
751
|
+
schemaVersion: 2,
|
|
556
752
|
pid: process.pid,
|
|
557
753
|
uid: process.getuid?.() ?? null,
|
|
558
754
|
token,
|
|
559
|
-
startedAt: new Date().toISOString(),
|
|
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 }),
|
|
560
768
|
};
|
|
561
|
-
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
|
|
562
|
-
await handle.sync();
|
|
563
|
-
const lockStat = await handle.stat({ bigint: true });
|
|
564
|
-
await handle.close();
|
|
565
|
-
return { path: lockPath, token, dev: lockStat.dev, ino: lockStat.ino };
|
|
566
769
|
}
|
|
567
770
|
catch (error) {
|
|
568
|
-
|
|
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
|
+
}
|
|
569
779
|
if (!isNodeError(error, "EEXIST")) {
|
|
570
780
|
throw error;
|
|
571
781
|
}
|
|
572
782
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (
|
|
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);
|
|
579
790
|
continue;
|
|
580
791
|
}
|
|
581
|
-
|
|
792
|
+
if (now() >= deadline) {
|
|
793
|
+
throw installLockUnsafe(lockPath, "Another installer left a fresh ownerless SRT install lock in its bounded publication window.");
|
|
794
|
+
}
|
|
582
795
|
}
|
|
583
|
-
|
|
584
|
-
|
|
796
|
+
else if (existing.kind === "owned") {
|
|
797
|
+
let sameOwner;
|
|
585
798
|
try {
|
|
586
|
-
await
|
|
799
|
+
sameOwner = await isSameProcess(existing.owner.record.pid, existing.owner.record.incarnation);
|
|
587
800
|
}
|
|
588
801
|
catch (error) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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 });
|
|
592
810
|
}
|
|
593
|
-
continue;
|
|
594
811
|
}
|
|
595
|
-
if (
|
|
596
|
-
|
|
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
|
+
}
|
|
597
816
|
}
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
}
|
|
600
830
|
}
|
|
601
|
-
await (options.hooks?.sleep ?? abortableSleep)(
|
|
831
|
+
await (options.hooks?.sleep ?? abortableSleep)(INSTALL_LOCK_POLL_MS, options.signal);
|
|
602
832
|
}
|
|
603
833
|
}
|
|
604
|
-
async function
|
|
605
|
-
let handle;
|
|
834
|
+
async function inspectExistingInstallLock(lockPath) {
|
|
606
835
|
try {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
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");
|
|
628
860
|
}
|
|
629
861
|
catch (error) {
|
|
630
|
-
if (isNodeError(error, "ENOENT"))
|
|
862
|
+
if (isNodeError(error, "ENOENT"))
|
|
863
|
+
return undefined;
|
|
864
|
+
if (error instanceof SandboxManagerError)
|
|
631
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);
|
|
632
969
|
}
|
|
633
|
-
|
|
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
|
+
};
|
|
634
991
|
}
|
|
635
992
|
finally {
|
|
636
993
|
await handle?.close().catch(() => undefined);
|
|
637
994
|
}
|
|
638
995
|
}
|
|
639
|
-
async function
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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);
|
|
646
1033
|
}
|
|
647
|
-
|
|
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;
|
|
648
1045
|
}
|
|
649
1046
|
async function releaseInstallLock(lock) {
|
|
650
|
-
|
|
651
|
-
|
|
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.");
|
|
652
1055
|
}
|
|
653
|
-
|
|
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()}`;
|
|
654
1062
|
try {
|
|
655
|
-
|
|
1063
|
+
await rename(lock.path, releasedPath);
|
|
656
1064
|
}
|
|
657
1065
|
catch (error) {
|
|
658
1066
|
if (isNodeError(error, "ENOENT")) {
|
|
659
|
-
|
|
1067
|
+
throw installLockUnsafe(lock.path, "SRT install lock disappeared during release.");
|
|
660
1068
|
}
|
|
661
1069
|
throw error;
|
|
662
1070
|
}
|
|
663
|
-
|
|
664
|
-
|
|
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)");
|
|
665
1107
|
}
|
|
666
|
-
|
|
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}`);
|
|
667
1213
|
}
|
|
668
1214
|
function processLiveness(pid) {
|
|
669
1215
|
try {
|