@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259
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/bin/guardian.js +4 -2
- package/bin/supervisor.js +1 -1
- package/bin/ui.js +44 -36
- package/bin/warmup.js +18 -9
- package/dist/cli/dispatch.d.ts +9 -1
- package/dist/cli/dispatch.js +73 -14
- package/dist/cli/package-messages.d.ts +26 -0
- package/dist/cli/package-messages.js +71 -0
- package/dist/cli/packages.d.ts +2 -1
- package/dist/cli/packages.js +12 -19
- package/dist/composition/owned-ui.d.ts +5 -2
- package/dist/composition/owned-ui.js +3 -1
- package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
- package/dist/features/launch/intent.d.ts +5 -3
- package/dist/features/launch/intent.js +7 -2
- package/dist/features/owned-ui/index.d.ts +1 -0
- package/dist/features/owned-ui/index.js +1 -0
- package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
- package/dist/features/owned-ui/session-fork-prompt.js +34 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -0
- package/dist/foundation/launch-guardian/main.js +8 -3
- package/dist/foundation/lifecycle/index.d.ts +2 -0
- package/dist/foundation/lifecycle/index.js +2 -0
- package/dist/foundation/lifecycle/paths.d.ts +2 -0
- package/dist/foundation/lifecycle/paths.js +9 -2
- package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
- package/dist/foundation/lifecycle/session-selection.js +39 -0
- package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
- package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
- package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
- package/dist/foundation/process-containment/index.d.ts +1 -0
- package/dist/foundation/process-containment/index.js +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
- package/dist/foundation/release/bootstrap.d.ts +10 -3
- package/dist/foundation/release/bootstrap.js +58 -25
- package/dist/foundation/release/dependency-layer.js +11 -10
- package/dist/foundation/release/immutable-platform.d.ts +6 -0
- package/dist/foundation/release/immutable-platform.js +13 -0
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-store.d.ts +1 -1
- package/dist/foundation/release/release-store.js +4 -2
- package/dist/foundation/release/restart-certification.d.ts +67 -0
- package/dist/foundation/release/restart-certification.js +211 -0
- package/dist/foundation/release/update.js +4 -4
- package/dist/foundation/release/warmup.js +1 -1
- package/dist/foundation/startup/startup-runtime.d.ts +2 -2
- package/dist/foundation/startup/startup-runtime.js +1 -1
- package/dist/foundation/supervision/main.d.ts +1 -1
- package/dist/foundation/supervision/main.js +60 -30
- package/dist/foundation/supervision/server.js +14 -1
- package/dist/integrations/pi/components/shell-footer-status.js +28 -12
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +6 -0
- package/dist/integrations/pi/engine/adapter.js +354 -53
- package/dist/integrations/pi/engine/index.d.ts +1 -0
- package/dist/integrations/pi/engine/index.js +1 -0
- package/dist/integrations/pi/engine/package-integration.js +31 -25
- package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
- package/dist/integrations/pi/engine/runtime-integration.js +11 -4
- package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
- package/dist/integrations/pi/engine/session-selection.js +90 -0
- package/dist/integrations/pi/engine/workflows.d.ts +18 -1
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +46 -14
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
- package/dist/native/darwin-arm64/manifest.json +4 -4
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/text-selection.js +8 -35
- package/dist/ui/components/transcript-viewport.d.ts +8 -2
- package/dist/ui/components/transcript-viewport.js +8 -6
- package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
- package/dist/ui/components/visible-hyperlinks.js +64 -0
- package/docs/architecture/process-guardian-provenance.md +3 -1
- package/docs/architecture/ui-reference-provenance.md +2 -0
- package/docs/ci-release-runbook.md +1 -1
- package/docs/features/launch-profiles.md +17 -1
- package/docs/manual-ghost-link-baseline.md +89 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
5
|
+
import { assertImmutableFileMode, immutablePlatformPolicy } from "./immutable-platform.js";
|
|
5
6
|
import { digestManifestFiles, releaseFileIdentity, resolveWithin } from "./release.js";
|
|
6
7
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
7
8
|
const LAYER_IO_CONCURRENCY = 32;
|
|
@@ -278,7 +279,8 @@ export async function readCertifiedDependencyLayer(dataDir, layerId, expected) {
|
|
|
278
279
|
const manifest = JSON.parse(await readFile(resolve(layerRoot, DEPENDENCY_LAYER_MANIFEST), "utf8"));
|
|
279
280
|
validateLayerManifest(manifest);
|
|
280
281
|
const certification = JSON.parse(await readFile(certificationPath(dataDir, layerId), "utf8"));
|
|
281
|
-
if (certification.schema !== PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema || certification.layerId !== manifest.layerId || certification.contentDigest !== manifest.contentDigest
|
|
282
|
+
if (certification.schema !== PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema || certification.layerId !== manifest.layerId || certification.contentDigest !== manifest.contentDigest
|
|
283
|
+
|| certification.platform !== process.platform || certification.platformPolicy !== immutablePlatformPolicy()) {
|
|
282
284
|
throw new Error(`dependency layer certification differs from manifest: ${layerId}`);
|
|
283
285
|
}
|
|
284
286
|
if (expected && (expected.layerId !== manifest.layerId || expected.contentDigest !== manifest.contentDigest)) {
|
|
@@ -300,6 +302,7 @@ export async function verifyDependencyLayer(dataDir, reference, onOperation) {
|
|
|
300
302
|
const metadata = await lstat(path);
|
|
301
303
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== file.bytes)
|
|
302
304
|
throw new Error(`dependency layer file mismatch: ${file.path}`);
|
|
305
|
+
assertImmutableFileMode(metadata, path);
|
|
303
306
|
const bytes = await readFile(path);
|
|
304
307
|
onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
|
|
305
308
|
const actual = releaseFileIdentity(file.path, bytes, file.executable);
|
|
@@ -338,16 +341,14 @@ async function writeLayerCertification(dataDir, identity) {
|
|
|
338
341
|
schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
|
|
339
342
|
layerId: identity.layerId,
|
|
340
343
|
contentDigest: identity.contentDigest,
|
|
344
|
+
platform: process.platform,
|
|
345
|
+
platformPolicy: immutablePlatformPolicy(),
|
|
341
346
|
certifiedAt: new Date().toISOString(),
|
|
342
|
-
}, null, 2), { flag: "wx", mode:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!await lstat(path).catch(() => null))
|
|
348
|
-
throw error;
|
|
349
|
-
await rm(temporary, { force: true });
|
|
350
|
-
}
|
|
347
|
+
}, null, 2), { flag: "wx", mode: 0o400 });
|
|
348
|
+
await chmod(path, 0o600).catch(() => { });
|
|
349
|
+
await rm(path, { force: true });
|
|
350
|
+
await rename(temporary, path);
|
|
351
|
+
await chmod(path, 0o400);
|
|
351
352
|
}
|
|
352
353
|
function validateLayerManifest(value) {
|
|
353
354
|
if (value.schema !== DEPENDENCY_LAYER_SCHEMA || !/^dependencies-[a-f0-9]{32}$/.test(value.layerId)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
export type ImmutablePlatformPolicy = "windows-readonly-content-v1" | "posix-readonly-content-v1";
|
|
3
|
+
/** Name the platform protection established while complete payload verification owns every file. */
|
|
4
|
+
export declare function immutablePlatformPolicy(platform?: NodeJS.Platform): ImmutablePlatformPolicy | null;
|
|
5
|
+
/** Refuse certification when a payload file is writable after immutable materialization. */
|
|
6
|
+
export declare function assertImmutableFileMode(metadata: Pick<Stats, "mode">, path: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Name the platform protection established while complete payload verification owns every file. */
|
|
2
|
+
export function immutablePlatformPolicy(platform = process.platform) {
|
|
3
|
+
if (platform === "win32")
|
|
4
|
+
return "windows-readonly-content-v1";
|
|
5
|
+
if (platform === "linux" || platform === "darwin")
|
|
6
|
+
return "posix-readonly-content-v1";
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
/** Refuse certification when a payload file is writable after immutable materialization. */
|
|
10
|
+
export function assertImmutableFileMode(metadata, path) {
|
|
11
|
+
if ((metadata.mode & 0o222) !== 0)
|
|
12
|
+
throw new Error(`immutable payload file is writable: ${path}`);
|
|
13
|
+
}
|
|
@@ -7,6 +7,7 @@ export * from "./process-cleanup.js";
|
|
|
7
7
|
export * from "./release.js";
|
|
8
8
|
export * from "./release-gc.js";
|
|
9
9
|
export * from "./release-store.js";
|
|
10
|
+
export * from "./restart-certification.js";
|
|
10
11
|
export * from "./stable-release.js";
|
|
11
12
|
export * from "./update.js";
|
|
12
13
|
export * from "./update-transaction.js";
|
|
@@ -7,6 +7,7 @@ export * from "./process-cleanup.js";
|
|
|
7
7
|
export * from "./release.js";
|
|
8
8
|
export * from "./release-gc.js";
|
|
9
9
|
export * from "./release-store.js";
|
|
10
|
+
export * from "./restart-certification.js";
|
|
10
11
|
export * from "./stable-release.js";
|
|
11
12
|
export * from "./update.js";
|
|
12
13
|
export * from "./update-transaction.js";
|
|
@@ -32,7 +32,7 @@ export interface CertifiedReleaseRecord {
|
|
|
32
32
|
export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
|
|
33
33
|
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
34
34
|
export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
|
|
35
|
-
export declare function readMaterializedRelease(releaseRoot: string): Promise<MaterializedRelease>;
|
|
35
|
+
export declare function readMaterializedRelease(releaseRoot: string, selectedStoreRoot?: string): Promise<MaterializedRelease>;
|
|
36
36
|
/**
|
|
37
37
|
* Load metadata for a release whose bytes were already certified by the
|
|
38
38
|
* current parent process or an authenticated live supervisor. Callers must
|
|
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
5
5
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
6
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
7
|
+
import { assertImmutableFileMode } from "./immutable-platform.js";
|
|
7
8
|
import { dependencyReference, materializeDependencyLayer, readCertifiedDependencyLayer, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
|
|
8
9
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
9
10
|
const certificationReady = new WeakSet();
|
|
@@ -94,10 +95,10 @@ export function consumeMaterializationProof(release) {
|
|
|
94
95
|
certificationReady.delete(release);
|
|
95
96
|
return true;
|
|
96
97
|
}
|
|
97
|
-
export async function readMaterializedRelease(releaseRoot) {
|
|
98
|
+
export async function readMaterializedRelease(releaseRoot, selectedStoreRoot) {
|
|
98
99
|
const canonical = await realpath(releaseRoot);
|
|
99
100
|
const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
100
|
-
return await verifyMaterializedRelease(canonical, manifest);
|
|
101
|
+
return certificationReadyRelease(await verifyMaterializedRelease(canonical, manifest, selectedStoreRoot));
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
104
|
* Load metadata for a release whose bytes were already certified by the
|
|
@@ -187,6 +188,7 @@ async function verifyFile(root, file, options) {
|
|
|
187
188
|
throw new Error(`release candidate is incomplete: ${file.path}`);
|
|
188
189
|
if (metadata.size !== file.bytes)
|
|
189
190
|
throw new Error(`release file size mismatch: ${file.path}`);
|
|
191
|
+
assertImmutableFileMode(metadata, path);
|
|
190
192
|
const bytes = await readFile(path);
|
|
191
193
|
options.onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
|
|
192
194
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
2
|
+
import { type DependencyLayerReference } from "./dependency-layer.js";
|
|
3
|
+
import { type ImmutablePlatformPolicy } from "./immutable-platform.js";
|
|
4
|
+
import { type ReleaseFileIdentity } from "./release.js";
|
|
5
|
+
import { type CertifiedReleaseRecord, type MaterializedRelease } from "./release-store.js";
|
|
6
|
+
declare const RESTART_SEAL_VERSION = 1;
|
|
7
|
+
type EvidenceKind = "directory" | "file" | "binding";
|
|
8
|
+
interface PathEvidence {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly kind: EvidenceKind;
|
|
11
|
+
readonly device: string;
|
|
12
|
+
readonly inode: string;
|
|
13
|
+
readonly mode: number;
|
|
14
|
+
readonly size: string;
|
|
15
|
+
readonly modifiedNs: string;
|
|
16
|
+
readonly changedNs: string;
|
|
17
|
+
readonly bornNs: string;
|
|
18
|
+
}
|
|
19
|
+
interface LayerRestartEvidence extends DependencyLayerReference {
|
|
20
|
+
readonly layerRoot: PathEvidence;
|
|
21
|
+
readonly manifest: PathEvidence;
|
|
22
|
+
readonly certification: PathEvidence;
|
|
23
|
+
readonly bindingEvidence: PathEvidence;
|
|
24
|
+
readonly bindingTarget: string;
|
|
25
|
+
}
|
|
26
|
+
interface RestartSealCore {
|
|
27
|
+
readonly version: typeof RESTART_SEAL_VERSION;
|
|
28
|
+
readonly platform: NodeJS.Platform;
|
|
29
|
+
readonly platformPolicy: ImmutablePlatformPolicy;
|
|
30
|
+
readonly releaseId: string;
|
|
31
|
+
readonly packageVersion: string;
|
|
32
|
+
readonly contentDigest: string;
|
|
33
|
+
readonly packageRoot: string;
|
|
34
|
+
readonly releaseRoot: PathEvidence;
|
|
35
|
+
readonly manifest: PathEvidence;
|
|
36
|
+
readonly launchEntries: readonly ReleaseFileIdentity[];
|
|
37
|
+
readonly launchEntryEvidence: readonly PathEvidence[];
|
|
38
|
+
readonly dependencyLayers: readonly LayerRestartEvidence[];
|
|
39
|
+
readonly certifiedAt: string;
|
|
40
|
+
}
|
|
41
|
+
export interface RestartSeal extends RestartSealCore {
|
|
42
|
+
readonly sealDigest: string;
|
|
43
|
+
}
|
|
44
|
+
interface ReleaseCertificationDocument {
|
|
45
|
+
readonly schema: typeof PRODUCT_IDENTITY.evidence.releaseCertificationSchema;
|
|
46
|
+
readonly releaseId: string;
|
|
47
|
+
readonly contentDigest: string;
|
|
48
|
+
readonly verifiedAt: string;
|
|
49
|
+
readonly checks: readonly {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly passed: boolean;
|
|
52
|
+
}[];
|
|
53
|
+
readonly restartSeal: RestartSeal | null;
|
|
54
|
+
}
|
|
55
|
+
export interface RestartValidationEvent {
|
|
56
|
+
readonly operation: "restart-evidence-read";
|
|
57
|
+
readonly path: string;
|
|
58
|
+
}
|
|
59
|
+
/** Build compact restart authority after complete content verification established read-only payload files. */
|
|
60
|
+
export declare function createRestartSeal(release: MaterializedRelease, dataDir: string): Promise<RestartSeal | null>;
|
|
61
|
+
/** Recover an approved release from durable bounded evidence without traversing its payload. */
|
|
62
|
+
export declare function readRestartCertifiedRelease(record: CertifiedReleaseRecord & {
|
|
63
|
+
readonly diagnosticsPath?: string | null;
|
|
64
|
+
}, dataDir: string, onEvent?: (event: RestartValidationEvent) => void): Promise<MaterializedRelease>;
|
|
65
|
+
export declare function releaseCertificationDocument(release: MaterializedRelease, restartSeal: RestartSeal | null, verifiedAt?: string): ReleaseCertificationDocument;
|
|
66
|
+
export declare function restartSealDigest(core: RestartSealCore): string;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
+
import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
|
|
6
|
+
import { immutablePlatformPolicy } from "./immutable-platform.js";
|
|
7
|
+
import { PRODUCT_PACKAGE_NAME } from "./release.js";
|
|
8
|
+
import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
|
|
9
|
+
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
10
|
+
const RESTART_SEAL_VERSION = 1;
|
|
11
|
+
const REQUIRED_LAUNCH_ENTRIES = ["bin/supervisor.js", "bin/guardian.js"];
|
|
12
|
+
/** Build compact restart authority after complete content verification established read-only payload files. */
|
|
13
|
+
export async function createRestartSeal(release, dataDir) {
|
|
14
|
+
const policy = immutablePlatformPolicy();
|
|
15
|
+
if (policy === null)
|
|
16
|
+
return null;
|
|
17
|
+
const canonicalData = await realpath(dataDir);
|
|
18
|
+
const releasesRoot = await realpath(resolve(canonicalData, "releases"));
|
|
19
|
+
const releaseRoot = await realpath(release.releaseRoot);
|
|
20
|
+
assertDirectChild(releasesRoot, releaseRoot, release.releaseId, "release");
|
|
21
|
+
const manifestPath = resolve(releaseRoot, RELEASE_MANIFEST_FILENAME);
|
|
22
|
+
const launchEntries = REQUIRED_LAUNCH_ENTRIES.map(path => release.files.find(candidate => candidate.path === path));
|
|
23
|
+
if (launchEntries.some(file => file === undefined))
|
|
24
|
+
return null;
|
|
25
|
+
const certifiedLaunchEntries = launchEntries;
|
|
26
|
+
const layersRoot = release.dependencyLayers?.length ? await realpath(resolve(canonicalData, "dependency-layers")) : null;
|
|
27
|
+
const dependencyLayers = await Promise.all((release.dependencyLayers ?? []).map(async (reference) => {
|
|
28
|
+
if (layersRoot === null)
|
|
29
|
+
throw new Error("restart certification has no dependency-layer root");
|
|
30
|
+
const layerRoot = await realpath(resolve(layersRoot, reference.layerId));
|
|
31
|
+
assertDirectChild(layersRoot, layerRoot, reference.layerId, "dependency layer");
|
|
32
|
+
const bindingPath = resolve(releaseRoot, reference.binding);
|
|
33
|
+
const bindingTarget = await realpath(bindingPath);
|
|
34
|
+
const expectedTarget = await realpath(resolve(layerRoot, "node_modules"));
|
|
35
|
+
if (bindingTarget !== expectedTarget)
|
|
36
|
+
throw new Error(`restart certification dependency binding targets unexpected content: ${bindingPath}`);
|
|
37
|
+
return {
|
|
38
|
+
...reference,
|
|
39
|
+
layerRoot: await pathEvidence(layerRoot, "directory"),
|
|
40
|
+
manifest: await pathEvidence(resolve(layerRoot, DEPENDENCY_LAYER_MANIFEST), "file"),
|
|
41
|
+
certification: await pathEvidence(dependencyLayerCertificationPath(canonicalData, reference.layerId), "file"),
|
|
42
|
+
bindingEvidence: await pathEvidence(bindingPath, "binding", true),
|
|
43
|
+
bindingTarget,
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
const core = {
|
|
47
|
+
version: RESTART_SEAL_VERSION,
|
|
48
|
+
platform: process.platform,
|
|
49
|
+
platformPolicy: policy,
|
|
50
|
+
releaseId: release.releaseId,
|
|
51
|
+
packageVersion: release.packageVersion,
|
|
52
|
+
contentDigest: release.contentDigest,
|
|
53
|
+
packageRoot: release.packageRoot,
|
|
54
|
+
releaseRoot: await pathEvidence(releaseRoot, "directory"),
|
|
55
|
+
manifest: await pathEvidence(manifestPath, "file"),
|
|
56
|
+
launchEntries: certifiedLaunchEntries,
|
|
57
|
+
launchEntryEvidence: await Promise.all(certifiedLaunchEntries.map(file => pathEvidence(resolve(releaseRoot, file.path), "file"))),
|
|
58
|
+
dependencyLayers,
|
|
59
|
+
certifiedAt: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
return { ...core, sealDigest: restartSealDigest(core) };
|
|
62
|
+
}
|
|
63
|
+
/** Recover an approved release from durable bounded evidence without traversing its payload. */
|
|
64
|
+
export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
65
|
+
const transaction = await new UpdateTransactionStore(dataDir).read();
|
|
66
|
+
if (transaction?.status === "active")
|
|
67
|
+
throw new Error("restart certification is unavailable during an active update transaction");
|
|
68
|
+
const certificationPath = resolve(dataDir, `certification-${record.releaseId}.json`);
|
|
69
|
+
if (record.diagnosticsPath !== undefined && record.diagnosticsPath !== certificationPath) {
|
|
70
|
+
throw new Error("restart certification path differs from the approved release record");
|
|
71
|
+
}
|
|
72
|
+
const certificationMetadata = await lstat(certificationPath);
|
|
73
|
+
onEvent?.({ operation: "restart-evidence-read", path: certificationPath });
|
|
74
|
+
if (!certificationMetadata.isFile() || certificationMetadata.isSymbolicLink() || (certificationMetadata.mode & 0o222) !== 0) {
|
|
75
|
+
throw new Error("restart certification is not an immutable regular file");
|
|
76
|
+
}
|
|
77
|
+
const certification = await readJson(certificationPath, onEvent);
|
|
78
|
+
if (certification.schema !== PRODUCT_IDENTITY.evidence.releaseCertificationSchema
|
|
79
|
+
|| certification.releaseId !== record.releaseId || certification.contentDigest !== record.contentDigest
|
|
80
|
+
|| !certification.checks.some(check => check.id === "immutable-content" && check.passed)) {
|
|
81
|
+
throw new Error("restart certification differs from the approved release record");
|
|
82
|
+
}
|
|
83
|
+
const seal = normalizeRestartSeal(certification.restartSeal);
|
|
84
|
+
if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
|
|
85
|
+
throw new Error("restart certification platform immutability evidence is unsupported");
|
|
86
|
+
}
|
|
87
|
+
if (seal.releaseId !== record.releaseId || seal.contentDigest !== record.contentDigest
|
|
88
|
+
|| resolve(seal.releaseRoot.path) !== resolve(record.releaseRoot)
|
|
89
|
+
|| (record.packageVersion !== undefined && seal.packageVersion !== record.packageVersion)) {
|
|
90
|
+
throw new Error("restart seal differs from the approved release identity");
|
|
91
|
+
}
|
|
92
|
+
const { sealDigest, ...core } = seal;
|
|
93
|
+
if (restartSealDigest(core) !== sealDigest)
|
|
94
|
+
throw new Error("restart seal digest is invalid");
|
|
95
|
+
const canonicalData = await observedRealpath(dataDir, onEvent);
|
|
96
|
+
const releasesRoot = await observedRealpath(resolve(canonicalData, "releases"), onEvent);
|
|
97
|
+
const releaseRoot = await observedRealpath(seal.releaseRoot.path, onEvent);
|
|
98
|
+
assertDirectChild(releasesRoot, releaseRoot, seal.releaseId, "release");
|
|
99
|
+
await assertPathEvidence(seal.releaseRoot, onEvent);
|
|
100
|
+
await assertPathEvidence(seal.manifest, onEvent);
|
|
101
|
+
for (let index = 0; index < seal.launchEntries.length; index += 1) {
|
|
102
|
+
const file = seal.launchEntries[index];
|
|
103
|
+
if (!REQUIRED_LAUNCH_ENTRIES.includes(file.path)) {
|
|
104
|
+
throw new Error(`restart seal contains an unexpected launch entry: ${file.path}`);
|
|
105
|
+
}
|
|
106
|
+
const evidence = seal.launchEntryEvidence[index];
|
|
107
|
+
if (!evidence || evidence.path !== resolve(releaseRoot, file.path) || file.bytes !== Number(evidence.size)) {
|
|
108
|
+
throw new Error(`restart launch entry evidence differs: ${file.path}`);
|
|
109
|
+
}
|
|
110
|
+
await assertPathEvidence(evidence, onEvent);
|
|
111
|
+
}
|
|
112
|
+
if (seal.launchEntries.length !== REQUIRED_LAUNCH_ENTRIES.length)
|
|
113
|
+
throw new Error("restart seal launch entries are incomplete");
|
|
114
|
+
const layersRoot = seal.dependencyLayers.length ? await observedRealpath(resolve(canonicalData, "dependency-layers"), onEvent) : null;
|
|
115
|
+
for (const layer of seal.dependencyLayers) {
|
|
116
|
+
if (layersRoot === null)
|
|
117
|
+
throw new Error("restart seal has no dependency-layer root");
|
|
118
|
+
const layerRoot = await observedRealpath(layer.layerRoot.path, onEvent);
|
|
119
|
+
assertDirectChild(layersRoot, layerRoot, layer.layerId, "dependency layer");
|
|
120
|
+
await assertPathEvidence(layer.layerRoot, onEvent);
|
|
121
|
+
await assertPathEvidence(layer.manifest, onEvent);
|
|
122
|
+
await assertPathEvidence(layer.certification, onEvent);
|
|
123
|
+
await assertPathEvidence(layer.bindingEvidence, onEvent, true);
|
|
124
|
+
const bindingTarget = await observedRealpath(layer.bindingEvidence.path, onEvent);
|
|
125
|
+
const expectedTarget = await observedRealpath(resolve(layerRoot, "node_modules"), onEvent);
|
|
126
|
+
if (bindingTarget !== layer.bindingTarget || bindingTarget !== expectedTarget) {
|
|
127
|
+
throw new Error(`restart dependency binding targets unexpected content: ${layer.bindingEvidence.path}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
packageName: PRODUCT_PACKAGE_NAME,
|
|
132
|
+
packageVersion: seal.packageVersion,
|
|
133
|
+
contentDigest: seal.contentDigest,
|
|
134
|
+
releaseId: seal.releaseId,
|
|
135
|
+
packageRoot: seal.packageRoot,
|
|
136
|
+
files: seal.launchEntries,
|
|
137
|
+
...(seal.dependencyLayers.length === 0 ? {} : {
|
|
138
|
+
dependencyLayers: seal.dependencyLayers.map(({ layerId, contentDigest, binding }) => ({ layerId, contentDigest, binding })),
|
|
139
|
+
}),
|
|
140
|
+
releaseRoot,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function releaseCertificationDocument(release, restartSeal, verifiedAt = new Date().toISOString()) {
|
|
144
|
+
return {
|
|
145
|
+
schema: PRODUCT_IDENTITY.evidence.releaseCertificationSchema,
|
|
146
|
+
releaseId: release.releaseId,
|
|
147
|
+
contentDigest: release.contentDigest,
|
|
148
|
+
verifiedAt,
|
|
149
|
+
checks: [{ id: "immutable-content", passed: true }],
|
|
150
|
+
restartSeal,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function restartSealDigest(core) {
|
|
154
|
+
return createHash("sha256").update(JSON.stringify(core)).digest("hex");
|
|
155
|
+
}
|
|
156
|
+
async function assertPathEvidence(evidence, onEvent, symbolic = false) {
|
|
157
|
+
const actual = await pathEvidence(evidence.path, evidence.kind, symbolic, onEvent);
|
|
158
|
+
if (JSON.stringify(actual) !== JSON.stringify(evidence))
|
|
159
|
+
throw new Error(`restart path evidence changed: ${evidence.path}`);
|
|
160
|
+
}
|
|
161
|
+
async function pathEvidence(path, kind, symbolic = false, onEvent) {
|
|
162
|
+
onEvent?.({ operation: "restart-evidence-read", path });
|
|
163
|
+
const metadata = symbolic ? await lstat(path, { bigint: true }) : await stat(path, { bigint: true });
|
|
164
|
+
if ((kind === "directory") !== metadata.isDirectory())
|
|
165
|
+
throw new Error(`restart evidence kind changed: ${path}`);
|
|
166
|
+
if (kind === "file" && !metadata.isFile())
|
|
167
|
+
throw new Error(`restart evidence file changed: ${path}`);
|
|
168
|
+
if (kind === "binding" && !metadata.isSymbolicLink())
|
|
169
|
+
throw new Error(`restart evidence binding changed: ${path}`);
|
|
170
|
+
if (kind === "file" && (metadata.mode & 146n) !== 0n)
|
|
171
|
+
throw new Error(`restart evidence file is writable: ${path}`);
|
|
172
|
+
return {
|
|
173
|
+
path: resolve(path),
|
|
174
|
+
kind,
|
|
175
|
+
device: metadata.dev.toString(),
|
|
176
|
+
inode: metadata.ino.toString(),
|
|
177
|
+
mode: Number(metadata.mode),
|
|
178
|
+
size: metadata.size.toString(),
|
|
179
|
+
modifiedNs: metadata.mtimeNs.toString(),
|
|
180
|
+
changedNs: metadata.ctimeNs.toString(),
|
|
181
|
+
bornNs: metadata.birthtimeNs.toString(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function normalizeRestartSeal(value) {
|
|
185
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
186
|
+
throw new Error("release certification has no durable restart seal");
|
|
187
|
+
const seal = value;
|
|
188
|
+
if (seal.version !== RESTART_SEAL_VERSION || typeof seal.platform !== "string" || typeof seal.platformPolicy !== "string"
|
|
189
|
+
|| typeof seal.releaseId !== "string" || typeof seal.packageVersion !== "string" || typeof seal.contentDigest !== "string"
|
|
190
|
+
|| typeof seal.packageRoot !== "string" || typeof seal.certifiedAt !== "string" || typeof seal.sealDigest !== "string"
|
|
191
|
+
|| !seal.releaseRoot || !seal.manifest || !Array.isArray(seal.launchEntries) || !Array.isArray(seal.launchEntryEvidence)
|
|
192
|
+
|| !Array.isArray(seal.dependencyLayers)) {
|
|
193
|
+
throw new Error("durable restart seal is invalid");
|
|
194
|
+
}
|
|
195
|
+
return seal;
|
|
196
|
+
}
|
|
197
|
+
async function readJson(path, onEvent) {
|
|
198
|
+
onEvent?.({ operation: "restart-evidence-read", path });
|
|
199
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
200
|
+
}
|
|
201
|
+
async function observedRealpath(path, onEvent) {
|
|
202
|
+
onEvent?.({ operation: "restart-evidence-read", path });
|
|
203
|
+
return await realpath(path);
|
|
204
|
+
}
|
|
205
|
+
function assertDirectChild(parent, child, expectedName, kind) {
|
|
206
|
+
const fromParent = relative(parent, child);
|
|
207
|
+
if (isAbsolute(fromParent) || fromParent === ".." || fromParent.startsWith(`..${sep}`) || fromParent.includes(sep)
|
|
208
|
+
|| basename(child) !== expectedName) {
|
|
209
|
+
throw new Error(`restart ${kind} is outside its managed store: ${child}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -167,8 +167,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
167
167
|
onWarmup?.("started");
|
|
168
168
|
await warmMaterializedRelease(candidate, environment);
|
|
169
169
|
onWarmup?.("completed");
|
|
170
|
-
await startSupervisor(candidate, environment);
|
|
171
|
-
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000);
|
|
170
|
+
const startup = await startSupervisor(candidate, environment);
|
|
171
|
+
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
|
|
172
172
|
},
|
|
173
173
|
};
|
|
174
174
|
}
|
|
@@ -555,8 +555,8 @@ async function rollbackPriorCohort(dataDir, environment, priorReleaseId) {
|
|
|
555
555
|
const paths = resolveProductPaths(environment);
|
|
556
556
|
// Invariant: rollback re-points the active reference and starts the prior cohort on its own endpoint;
|
|
557
557
|
// a cohort that survived the update keeps serving the work it already had.
|
|
558
|
-
await startSupervisor(release, environment);
|
|
559
|
-
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, release.releaseId, environment).endpointMetadataPath, release, 8_000);
|
|
558
|
+
const startup = await startSupervisor(release, environment);
|
|
559
|
+
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, release.releaseId, environment).endpointMetadataPath, release, 8_000, startup);
|
|
560
560
|
return "rolled back";
|
|
561
561
|
}
|
|
562
562
|
function phaseBefore(current, target) {
|
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { releaseEnvironment } from "./bootstrap.js";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
5
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
6
|
-
export async function warmMaterializedRelease(release, environment, timeoutMs =
|
|
6
|
+
export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
|
|
7
7
|
const entry = resolve(release.releaseRoot, "bin", "warmup.js");
|
|
8
8
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
9
9
|
const child = spawn(process.execPath, [entry], {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
2
|
-
export type StartupPhase = "command-invoked" | "bootstrap-start" | "bootstrap-selected" | "guardian-start" | "guardian-connected" | "ui-entry" | "ui-modules-loaded" | "pi-services" | "resource-discovery" | "session-created" | "settings-loaded" | "first-input-ready-render";
|
|
2
|
+
export type StartupPhase = "command-invoked" | "bootstrap-start" | "bootstrap-selected" | "durable-validation-start" | "durable-validation-complete" | "replacement-supervisor-start" | "replacement-supervisor-ready" | "guardian-start" | "guardian-connected" | "ui-entry" | "ui-modules-loaded" | "pi-services" | "resource-discovery" | "session-created" | "settings-loaded" | "first-input-ready-render";
|
|
3
3
|
export interface StartupPerformanceEvidence {
|
|
4
4
|
readonly profileId: "a1" | "pi";
|
|
5
|
-
readonly launchKind: "post-update" | "warm";
|
|
5
|
+
readonly launchKind: "post-update" | "no-live-supervisor" | "warm";
|
|
6
6
|
readonly events: readonly StartupTraceEvent[];
|
|
7
7
|
}
|
|
8
8
|
export interface StartupTraceEvent {
|
|
@@ -100,7 +100,7 @@ export function assertStartupPerformanceBudget(evidence, budgets = { postUpdateM
|
|
|
100
100
|
const ready = events.findLast(event => event.phase === "first-input-ready-render");
|
|
101
101
|
if (!ready)
|
|
102
102
|
throw new Error(`startup budget failed for ${evidence.profileId}: first input-ready render was not recorded`);
|
|
103
|
-
const budget = evidence.launchKind === "
|
|
103
|
+
const budget = evidence.launchKind === "warm" ? budgets.warmMs : budgets.postUpdateMs;
|
|
104
104
|
if (ready.elapsedMs <= budget)
|
|
105
105
|
return;
|
|
106
106
|
const intervals = events.map((event, index) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runSupervisor(): Promise<void>;
|
|
1
|
+
export declare function runSupervisor(arguments_?: readonly string[]): Promise<void>;
|
|
@@ -2,11 +2,12 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { assertImmutableExecutionRoot, CohortStateStore, readCertifiedReleaseManifest } from "../release/index.js";
|
|
5
|
+
import { publishSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath } from "../lifecycle/index.js";
|
|
5
6
|
import { ControlStore } from "../storage/index.js";
|
|
6
7
|
import { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
|
|
7
8
|
import { SupervisorServer } from "./server.js";
|
|
8
9
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
9
|
-
export async function runSupervisor() {
|
|
10
|
+
export async function runSupervisor(arguments_ = []) {
|
|
10
11
|
const productPaths = resolveProductPaths();
|
|
11
12
|
mkdirSync(productPaths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
12
13
|
mkdirSync(productPaths.endpointsDir, { recursive: true, mode: 0o700 });
|
|
@@ -15,33 +16,62 @@ export async function runSupervisor() {
|
|
|
15
16
|
const releaseRoot = process.env[PRODUCT_IDENTITY.environment.releaseRoot];
|
|
16
17
|
const releaseId = process.env[PRODUCT_IDENTITY.environment.releaseId];
|
|
17
18
|
const contentDigest = process.env[PRODUCT_IDENTITY.environment.releaseDigest];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
19
|
+
const attemptId = startupAttempt(arguments_);
|
|
20
|
+
const resultPath = attemptId ? supervisorStartupResultPath(productPaths.runtimeDir, attemptId) : null;
|
|
21
|
+
let stage = "release-environment";
|
|
22
|
+
let server = null;
|
|
23
|
+
try {
|
|
24
|
+
if (!releaseRoot || !releaseId || !contentDigest)
|
|
25
|
+
throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
|
|
26
|
+
stage = "release-certification";
|
|
27
|
+
const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
|
|
28
|
+
stage = "immutable-root";
|
|
29
|
+
await assertImmutableExecutionRoot(release, paths.dataDir);
|
|
30
|
+
// Protocol: one endpoint per cohort: a superseded cohort keeps serving what it already has while the
|
|
31
|
+
// installed release listens on its own address.
|
|
32
|
+
paths = { ...productPaths, ...resolveCohortEndpoint(productPaths, release.releaseId) };
|
|
33
|
+
stage = "storage";
|
|
34
|
+
const bootNonce = randomUUID();
|
|
35
|
+
const store = new ControlStore(paths.databasePath, bootNonce);
|
|
36
|
+
const cohortState = new CohortStateStore(paths.dataDir);
|
|
37
|
+
server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => (await cohortState.read()).references.active);
|
|
38
|
+
stage = "endpoint-listen";
|
|
39
|
+
await server.listen();
|
|
40
|
+
if (resultPath && attemptId)
|
|
41
|
+
await publishSupervisorStartupResult(resultPath, supervisorStartupReady(attemptId, release.releaseId));
|
|
42
|
+
log(`supervisor ${server.id} listening at ${paths.endpoint} (pid ${process.pid})`);
|
|
43
|
+
let closing = false;
|
|
44
|
+
const close = async (signal) => {
|
|
45
|
+
if (closing)
|
|
46
|
+
return;
|
|
47
|
+
closing = true;
|
|
48
|
+
log(`received ${signal}; stopping supervised process tree`);
|
|
49
|
+
await server?.close(true);
|
|
50
|
+
process.exitCode = 0;
|
|
51
|
+
};
|
|
52
|
+
process.on("SIGINT", () => void close("SIGINT"));
|
|
53
|
+
process.on("SIGTERM", () => void close("SIGTERM"));
|
|
54
|
+
process.on("uncaughtException", error => {
|
|
55
|
+
log(`uncaught exception: ${error.stack ?? error.message}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
});
|
|
58
|
+
process.on("unhandledRejection", error => log(`unhandled rejection: ${String(error)}`));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await server?.close(true).catch(() => undefined);
|
|
62
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
63
|
+
log(`startup failure at ${stage}: ${message}`);
|
|
64
|
+
if (resultPath && attemptId && releaseId) {
|
|
65
|
+
await publishSupervisorStartupResult(resultPath, supervisorStartupFailure(error, attemptId, releaseId, stage)).catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function startupAttempt(arguments_) {
|
|
71
|
+
if (arguments_.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
if (arguments_.length !== 2 || arguments_[0] !== "--startup-attempt" || !arguments_[1]) {
|
|
74
|
+
throw new Error(PRODUCT_TEXT.diagnostic("supervisor startup arguments are invalid"));
|
|
75
|
+
}
|
|
76
|
+
return arguments_[1];
|
|
47
77
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createConnection, createServer } from "node:net";
|
|
4
4
|
import { platform } from "node:os";
|
|
5
|
+
import { dirname } from "node:path";
|
|
5
6
|
import { assertLaunchInstanceOutcome, assertNativeProcessIdentity, assertProcessContainmentIdentity, } from "../lifecycle/index.js";
|
|
6
7
|
import { encodeFrame, isCommandMessage, isControlHello, LineFrameDecoder, localControlHello, MAX_CONTROL_FRAME_BYTES, negotiateControlFeatures } from "../protocol/index.js";
|
|
7
8
|
import { processIsAlive } from "../release/index.js";
|
|
@@ -71,6 +72,8 @@ export class SupervisorServer {
|
|
|
71
72
|
await mkdir(this.paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
72
73
|
await mkdir(this.paths.endpointsDir, { recursive: true, mode: 0o700 });
|
|
73
74
|
if (platform() !== "win32") {
|
|
75
|
+
if (this.paths.endpointDirectory)
|
|
76
|
+
await ensureManagedEndpointDirectory(dirname(this.paths.endpoint));
|
|
74
77
|
if (await endpointIsLive(this.paths.endpoint))
|
|
75
78
|
throw new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`));
|
|
76
79
|
await rm(this.paths.endpoint, { force: true });
|
|
@@ -489,6 +492,16 @@ function sameContainmentIdentity(left, right) {
|
|
|
489
492
|
function isMessageType(value, type) {
|
|
490
493
|
return typeof value === "object" && value !== null && "type" in value && value.type === type;
|
|
491
494
|
}
|
|
495
|
+
async function ensureManagedEndpointDirectory(path) {
|
|
496
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
497
|
+
const metadata = await lstat(path);
|
|
498
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
499
|
+
throw new Error(`managed endpoint path is not a directory: ${path}`);
|
|
500
|
+
const currentUser = process.getuid?.();
|
|
501
|
+
if (currentUser !== undefined && metadata.uid !== currentUser)
|
|
502
|
+
throw new Error(`managed endpoint directory has another owner: ${path}`);
|
|
503
|
+
await chmod(path, 0o700);
|
|
504
|
+
}
|
|
492
505
|
async function endpointIsLive(endpoint) {
|
|
493
506
|
return await new Promise(resolve => {
|
|
494
507
|
const socket = createConnection(endpoint);
|