@timurproko/a1 0.2.7-dev.792 → 0.2.7-dev.794
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/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell.d.ts +1 -0
- package/dist/app/session-shell/session-shell.js +10 -0
- package/dist/cli/tabs.js +1 -1
- package/dist/composition/owned-ui.d.ts +1 -0
- package/dist/composition/owned-ui.js +16 -3
- package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
- package/dist/features/resident-tab/shortcut-declarations.js +1 -0
- package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
- package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -1
- package/dist/foundation/launch-guardian/main.js +58 -6
- package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
- package/dist/foundation/process-containment/addon-containment.js +139 -0
- package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
- package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
- package/dist/foundation/process-containment/addon.d.ts +42 -0
- package/dist/foundation/process-containment/addon.js +57 -0
- package/dist/foundation/process-containment/artifact.d.ts +1 -1
- package/dist/foundation/process-containment/artifact.js +6 -5
- package/dist/foundation/process-containment/contracts.d.ts +2 -0
- package/dist/foundation/process-containment/index.d.ts +4 -4
- package/dist/foundation/process-containment/index.js +3 -3
- package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/native-process-inspector.js +10 -10
- package/dist/foundation/release/bootstrap.js +53 -16
- package/dist/foundation/release/index.d.ts +1 -1
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/release-store.d.ts +12 -0
- package/dist/foundation/release/release-store.js +124 -2
- package/dist/foundation/resident-tabs/bounded-host-command.d.ts +3 -1
- package/dist/foundation/resident-tabs/bounded-host-command.js +6 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +2 -0
- package/dist/foundation/resident-tabs/bridge-client.js +13 -0
- package/dist/foundation/resident-tabs/doctor.d.ts +0 -1
- package/dist/foundation/resident-tabs/doctor.js +4 -4
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +7 -2
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +15 -11
- package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
- package/dist/foundation/resident-tabs/host-entry.js +17 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -2
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/maintenance.d.ts +5 -4
- package/dist/foundation/resident-tabs/maintenance.js +32 -17
- package/dist/foundation/resident-tabs/release-holds.d.ts +3 -2
- package/dist/foundation/resident-tabs/release-holds.js +16 -7
- package/dist/foundation/resident-tabs/settings.d.ts +0 -1
- package/dist/foundation/resident-tabs/settings.js +1 -7
- package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +11 -15
- package/dist/foundation/resident-tabs/terminal-host-artifact.js +57 -32
- package/dist/foundation/session-writer-lock/index.d.ts +1 -3
- package/dist/foundation/session-writer-lock/index.js +8 -6
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/native/darwin-arm64/manifest.json +8 -6
- package/dist/native/darwin-arm64/process-guardian.node +0 -0
- package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +14 -13
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +9 -7
- package/dist/native/darwin-x64/process-guardian.node +0 -0
- package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +15 -14
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +8 -6
- package/dist/native/linux-x64/process-guardian.node +0 -0
- package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +14 -13
- package/dist/native/linux-x64/{terminal-host → terminal-host.node} +0 -0
- package/dist/native/win32-x64/manifest.json +8 -6
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/win32-x64/terminal-host.manifest.json +14 -13
- package/dist/native/win32-x64/{terminal-host.exe → terminal-host.node} +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +3 -2
- package/dist/runtime-payload-inventory.json +1 -0
- package/dist/ui/settings/declarations.d.ts +0 -12
- package/dist/ui/settings/declarations.js +0 -10
- package/docs/architecture/boundaries.md +10 -9
- package/docs/architecture/process-guardian-provenance.md +17 -11
- package/docs/architecture/resident-tabs.md +36 -34
- package/docs/architecture/terminal-host-provenance.md +5 -3
- package/docs/architecture/toolchain.md +6 -6
- package/docs/openspec-archive-automation.md +3 -3
- package/docs/validation.md +5 -1
- package/package.json +1 -1
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
- package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
- package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
- package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
- package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/darwin-arm64/terminal-host +0 -0
- package/dist/native/darwin-x64/a1_session_lock.node +0 -0
- package/dist/native/darwin-x64/process-guardian +0 -0
- package/dist/native/darwin-x64/terminal-host +0 -0
- package/dist/native/linux-x64/a1_session_lock.node +0 -0
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/a1_session_lock.node +0 -0
- package/dist/native/win32-x64/process-guardian.exe +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ProcessGuardianVersion {
|
|
2
|
+
readonly crateVersion: string;
|
|
3
|
+
readonly protocolVersion: 2;
|
|
4
|
+
readonly target: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProcessGuardianRuntime {
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly startIdentity: string;
|
|
9
|
+
readonly containmentProvider: string;
|
|
10
|
+
readonly containmentToken: string;
|
|
11
|
+
readonly processGroup: number | null;
|
|
12
|
+
readonly foregroundGroup: number | null;
|
|
13
|
+
wait(): Promise<number>;
|
|
14
|
+
terminate(code: number): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
export interface SpawnContainedOptions {
|
|
19
|
+
readonly executable: string;
|
|
20
|
+
readonly arguments: readonly string[];
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
23
|
+
readonly instanceId: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProcessGuardianAddon {
|
|
26
|
+
version(): ProcessGuardianVersion;
|
|
27
|
+
inspectProcessStart(pid: number): string | null;
|
|
28
|
+
captureTerminalModes(): string;
|
|
29
|
+
restoreTerminalModes(encoded: string): void;
|
|
30
|
+
deliverExitNotice(path: string, encodedModes: string): void;
|
|
31
|
+
spawnContained(options: SpawnContainedOptions): ProcessGuardianRuntime;
|
|
32
|
+
spawnWatcher(options: {
|
|
33
|
+
readonly executable: string;
|
|
34
|
+
readonly arguments: readonly string[];
|
|
35
|
+
}): number;
|
|
36
|
+
waitForProcessExit(pid: number): Promise<void>;
|
|
37
|
+
stopProcessGroup(processGroup: number): void;
|
|
38
|
+
restoreForegroundGroup(processGroup: number): void;
|
|
39
|
+
ignoreConsoleControl(): void;
|
|
40
|
+
}
|
|
41
|
+
/** Loads one verified Node-API guardian instance per exact addon path. */
|
|
42
|
+
export declare function loadProcessGuardianAddon(path: string): ProcessGuardianAddon;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
/** Loads one verified Node-API guardian instance per exact addon path. */
|
|
6
|
+
export function loadProcessGuardianAddon(path) {
|
|
7
|
+
if (!path || path.includes("\0"))
|
|
8
|
+
throw new TypeError("process guardian addon path is invalid");
|
|
9
|
+
const exactPath = resolve(path);
|
|
10
|
+
const cached = cache.get(exactPath);
|
|
11
|
+
if (cached)
|
|
12
|
+
return cached;
|
|
13
|
+
let value;
|
|
14
|
+
try {
|
|
15
|
+
value = require(exactPath);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const code = errorCode(error) === "MODULE_NOT_FOUND" || errorCode(error) === "ENOENT"
|
|
19
|
+
? "CONTAINMENT_ARTIFACT_MISSING"
|
|
20
|
+
: "CONTAINMENT_ARTIFACT_INCOMPATIBLE";
|
|
21
|
+
throw diagnosticError(`process guardian addon could not be loaded: ${errorMessage(error)}`, code);
|
|
22
|
+
}
|
|
23
|
+
if (!isProcessGuardianAddon(value)) {
|
|
24
|
+
throw diagnosticError("process guardian addon exports are incompatible", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
|
|
25
|
+
}
|
|
26
|
+
let report;
|
|
27
|
+
try {
|
|
28
|
+
report = value.version();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw diagnosticError(`process guardian addon identity failed: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
|
|
32
|
+
}
|
|
33
|
+
if (report.protocolVersion !== 2 || typeof report.crateVersion !== "string" || report.crateVersion.length === 0 || typeof report.target !== "string") {
|
|
34
|
+
throw diagnosticError("process guardian addon protocol is incompatible", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
|
|
35
|
+
}
|
|
36
|
+
cache.set(exactPath, value);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function isProcessGuardianAddon(value) {
|
|
40
|
+
if (typeof value !== "object" || value === null)
|
|
41
|
+
return false;
|
|
42
|
+
const candidate = value;
|
|
43
|
+
return [
|
|
44
|
+
"version", "inspectProcessStart", "captureTerminalModes", "restoreTerminalModes",
|
|
45
|
+
"deliverExitNotice", "spawnContained", "spawnWatcher", "waitForProcessExit",
|
|
46
|
+
"stopProcessGroup", "restoreForegroundGroup", "ignoreConsoleControl",
|
|
47
|
+
].every(name => typeof candidate[name] === "function");
|
|
48
|
+
}
|
|
49
|
+
function diagnosticError(message, code) {
|
|
50
|
+
return Object.assign(new Error(message), { code });
|
|
51
|
+
}
|
|
52
|
+
function errorCode(error) {
|
|
53
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
54
|
+
}
|
|
55
|
+
function errorMessage(error) {
|
|
56
|
+
return error instanceof Error ? error.message : String(error);
|
|
57
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function verifyProcessGuardianArtifact(
|
|
1
|
+
export declare function verifyProcessGuardianArtifact(addonPath: string, platform?: NodeJS.Platform, architecture?: string): Promise<void>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
|
-
export async function verifyProcessGuardianArtifact(
|
|
5
|
-
const manifestPath = resolve(dirname(
|
|
4
|
+
export async function verifyProcessGuardianArtifact(addonPath, platform = process.platform, architecture = process.arch) {
|
|
5
|
+
const manifestPath = resolve(dirname(addonPath), "manifest.json");
|
|
6
6
|
let manifest;
|
|
7
7
|
try {
|
|
8
8
|
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
@@ -10,15 +10,16 @@ export async function verifyProcessGuardianArtifact(helperPath, platform = proce
|
|
|
10
10
|
catch (error) {
|
|
11
11
|
throw artifactError(`process guardian manifest is missing or malformed: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_MISSING");
|
|
12
12
|
}
|
|
13
|
-
if (manifest.schema !== "a1-process-guardian-artifact-
|
|
13
|
+
if (manifest.schema !== "a1-process-guardian-artifact-v2" || manifest.kind !== "node-addon"
|
|
14
|
+
|| manifest.nodeApi !== 9 || manifest.protocolVersion !== 2
|
|
14
15
|
|| manifest.platform !== platform || manifest.architecture !== architecture
|
|
15
|
-
|| manifest.artifact?.filename !== basename(
|
|
16
|
+
|| manifest.artifact?.filename !== basename(addonPath)) {
|
|
16
17
|
throw artifactError("process guardian manifest is incompatible with this platform or protocol", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
|
|
17
18
|
}
|
|
18
19
|
if (manifest.capability !== "supported") {
|
|
19
20
|
throw artifactError(`process containment is not certified for ${platform}-${architecture}`, "CONTAINMENT_UNSUPPORTED");
|
|
20
21
|
}
|
|
21
|
-
const [bytes, metadata] = await Promise.all([readFile(
|
|
22
|
+
const [bytes, metadata] = await Promise.all([readFile(addonPath), stat(addonPath)]).catch(error => {
|
|
22
23
|
throw artifactError(`process guardian artifact is unavailable: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_MISSING");
|
|
23
24
|
});
|
|
24
25
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
@@ -7,6 +7,8 @@ export interface ContainedSpawnOptions {
|
|
|
7
7
|
export interface ContainedProcessHandle {
|
|
8
8
|
readonly identity: NativeProcessIdentity;
|
|
9
9
|
readonly outcome: Promise<LaunchInstanceOutcome>;
|
|
10
|
+
readonly processGroup?: number;
|
|
11
|
+
readonly foregroundGroup?: number;
|
|
10
12
|
}
|
|
11
13
|
export interface NativeProcessInspector {
|
|
12
14
|
observe(pid: number): Promise<NativeProcessIdentity | null>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
export { loadProcessGuardianAddon } from "./addon.js";
|
|
2
|
+
export type { ProcessGuardianAddon, ProcessGuardianRuntime, ProcessGuardianVersion } from "./addon.js";
|
|
3
|
+
export { AddonContainment, resolveProcessGuardianPath } from "./addon-containment.js";
|
|
4
|
+
export { AddonProcessInspector } from "./addon-process-inspector.js";
|
|
1
5
|
export { verifyProcessGuardianArtifact } from "./artifact.js";
|
|
2
6
|
export { closeVerifiedContainment } from "./contracts.js";
|
|
3
7
|
export type { ContainedProcessHandle, ContainedSpawnOptions, ContainmentCloseResult, NativeProcessInspector, ProcessContainment, ProcessContainmentFactory, } from "./contracts.js";
|
|
4
|
-
export { DarwinNativeProcessInspector } from "./darwin-process-inspector.js";
|
|
5
8
|
export { LinuxNativeProcessInspector } from "./linux-process-inspector.js";
|
|
6
|
-
export { NativeGuardianContainment, resolveProcessGuardianPath } from "./native-guardian-containment.js";
|
|
7
9
|
export { createNativeProcessInspector } from "./native-process-inspector.js";
|
|
8
|
-
export { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
|
|
9
|
-
export type { InspectorCommandResult, InspectorCommandRunner } from "./windows-process-inspector.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
export { loadProcessGuardianAddon } from "./addon.js";
|
|
2
|
+
export { AddonContainment, resolveProcessGuardianPath } from "./addon-containment.js";
|
|
3
|
+
export { AddonProcessInspector } from "./addon-process-inspector.js";
|
|
1
4
|
export { verifyProcessGuardianArtifact } from "./artifact.js";
|
|
2
5
|
export { closeVerifiedContainment } from "./contracts.js";
|
|
3
|
-
export { DarwinNativeProcessInspector } from "./darwin-process-inspector.js";
|
|
4
6
|
export { LinuxNativeProcessInspector } from "./linux-process-inspector.js";
|
|
5
|
-
export { NativeGuardianContainment, resolveProcessGuardianPath } from "./native-guardian-containment.js";
|
|
6
7
|
export { createNativeProcessInspector } from "./native-process-inspector.js";
|
|
7
|
-
export { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProcessGuardianAddon } from "./addon.js";
|
|
1
2
|
import type { NativeProcessInspector } from "./contracts.js";
|
|
2
3
|
/** Resolve only the platform's exact-process inspector used by cooperative session ownership. */
|
|
3
|
-
export declare function createNativeProcessInspector(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, releaseRoot?: string): Promise<NativeProcessInspector>;
|
|
4
|
+
export declare function createNativeProcessInspector(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, releaseRoot?: string, loadAddon?: (path: string) => ProcessGuardianAddon): Promise<NativeProcessInspector>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
2
2
|
/** Resolve only the platform's exact-process inspector used by cooperative session ownership. */
|
|
3
|
-
export async function createNativeProcessInspector(environment = process.env, platform = process.platform, releaseRoot) {
|
|
3
|
+
export async function createNativeProcessInspector(environment = process.env, platform = process.platform, releaseRoot, loadAddon) {
|
|
4
4
|
if (platform === "linux") {
|
|
5
5
|
const { LinuxNativeProcessInspector } = await import("./linux-process-inspector.js");
|
|
6
6
|
return new LinuxNativeProcessInspector();
|
|
@@ -8,14 +8,14 @@ export async function createNativeProcessInspector(environment = process.env, pl
|
|
|
8
8
|
if (platform !== "win32" && platform !== "darwin")
|
|
9
9
|
throw new Error(`native process identity is unsupported on ${platform}`);
|
|
10
10
|
if (releaseRoot === undefined && !environment[PRODUCT_IDENTITY.environment.processGuardianPath]) {
|
|
11
|
-
throw new Error("native process identity
|
|
11
|
+
throw new Error("native process identity addon is unavailable");
|
|
12
12
|
}
|
|
13
|
-
const { resolveProcessGuardianPath } = await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
return new
|
|
13
|
+
const [{ resolveProcessGuardianPath }, { loadProcessGuardianAddon }, { AddonProcessInspector }] = await Promise.all([
|
|
14
|
+
import("./addon-containment.js"),
|
|
15
|
+
import("./addon.js"),
|
|
16
|
+
import("./addon-process-inspector.js"),
|
|
17
|
+
]);
|
|
18
|
+
const addonPath = resolveProcessGuardianPath(releaseRoot ?? process.cwd(), environment, platform);
|
|
19
|
+
const addon = (loadAddon ?? loadProcessGuardianAddon)(addonPath);
|
|
20
|
+
return new AddonProcessInspector(addon, platform === "win32" ? "windows-filetime:" : "darwin-proc-start:");
|
|
21
21
|
}
|
|
@@ -12,7 +12,7 @@ import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
|
|
|
12
12
|
import { probeOwnership, readEndpointMetadata, removeEndpointArtifacts, sweepDeadEndpoints } from "./endpoints.js";
|
|
13
13
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
14
14
|
import { selectUpdateLaunchRelease } from "./update-launch.js";
|
|
15
|
-
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
15
|
+
import { assertCertifiedFilesPresent, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
16
16
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
17
17
|
import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
|
|
18
18
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
@@ -61,10 +61,10 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
61
61
|
});
|
|
62
62
|
if (resident !== null) {
|
|
63
63
|
residentContext = resident.status === "ready" ? {
|
|
64
|
-
residentRootExecutable: resident.executable,
|
|
64
|
+
residentRootExecutable: resident.executable.executable,
|
|
65
65
|
residentRootArguments: JSON.stringify([
|
|
66
|
+
resident.executable.entry,
|
|
66
67
|
"attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
|
|
67
|
-
"--tabs-max", String((await residentLaunchSettings).tabsMax),
|
|
68
68
|
"--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
69
69
|
"--settings-file", resolve(agentProfileDir, "settings.json"),
|
|
70
70
|
"--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
|
|
@@ -126,7 +126,23 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
126
126
|
const installedVersion = await readInstalledVersion(options.packageRoot);
|
|
127
127
|
const activeId = state.references.active;
|
|
128
128
|
const active = activeId === null ? undefined : state.releases[activeId];
|
|
129
|
-
|
|
129
|
+
let activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
|
|
130
|
+
let excludedRetainedReleaseId;
|
|
131
|
+
if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
|
|
132
|
+
try {
|
|
133
|
+
await assertCertifiedFilesPresent(active.releaseRoot);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
try {
|
|
137
|
+
await repairApprovedRelease(options.packageRoot, active, paths.dataDir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Security: a damaged certified release is no longer a fallback when package bytes cannot restore it.
|
|
141
|
+
activeIsLaunchable = false;
|
|
142
|
+
excludedRetainedReleaseId = active.releaseId;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
130
146
|
if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
|
|
131
147
|
const endpointMatches = endpoint?.releaseId === active.releaseId
|
|
132
148
|
&& endpoint.releaseRoot === active.releaseRoot
|
|
@@ -140,18 +156,27 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
140
156
|
await removeEndpointArtifacts(endpointPaths.endpointMetadataPath, endpointPaths.endpoint);
|
|
141
157
|
await markStartupPhase(environment, "durable-validation-start");
|
|
142
158
|
const retained = await readRestartCertifiedRelease(active, paths.dataDir).catch(async () => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
let verified;
|
|
160
|
+
try {
|
|
161
|
+
verified = await readMaterializedRelease(active.releaseRoot, resolve(paths.dataDir, "releases"));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
verified = await repairApprovedRelease(options.packageRoot, active, paths.dataDir);
|
|
165
|
+
return verified;
|
|
146
166
|
}
|
|
167
|
+
assertApprovedReleaseMatches(verified, active);
|
|
147
168
|
await certifyMaterializedRelease(verified, paths.dataDir);
|
|
148
169
|
return verified;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
}).catch(() => null);
|
|
171
|
+
if (retained !== null) {
|
|
172
|
+
await markStartupPhase(environment, "durable-validation-complete");
|
|
173
|
+
await markStartupPhase(environment, "replacement-supervisor-start");
|
|
174
|
+
await ensureSupervisor(retained, environment);
|
|
175
|
+
await markStartupPhase(environment, "replacement-supervisor-ready");
|
|
176
|
+
return await launchSelected(retained);
|
|
177
|
+
}
|
|
178
|
+
activeIsLaunchable = false;
|
|
179
|
+
excludedRetainedReleaseId = active.releaseId;
|
|
155
180
|
}
|
|
156
181
|
}
|
|
157
182
|
// Concurrency: an installation being replaced is not a reason to refuse a launch. While
|
|
@@ -163,7 +188,7 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
163
188
|
candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
164
189
|
}
|
|
165
190
|
catch (error) {
|
|
166
|
-
const fallback = await launchRetainedActive(state, paths, environment, launchSelected);
|
|
191
|
+
const fallback = await launchRetainedActive(state, paths, environment, launchSelected, excludedRetainedReleaseId);
|
|
167
192
|
if (fallback === null)
|
|
168
193
|
throw error;
|
|
169
194
|
return fallback;
|
|
@@ -290,10 +315,10 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
290
315
|
* certifying anything new, and reports no selection when there is nothing safe
|
|
291
316
|
* to fall back to.
|
|
292
317
|
*/
|
|
293
|
-
async function launchRetainedActive(state, paths, environment, launch) {
|
|
318
|
+
async function launchRetainedActive(state, paths, environment, launch, excludedReleaseId) {
|
|
294
319
|
const activeId = state.references.active;
|
|
295
320
|
const active = activeId === null ? undefined : state.releases[activeId];
|
|
296
|
-
if (!active || active.approval !== "approved")
|
|
321
|
+
if (!active || active.approval !== "approved" || active.releaseId === excludedReleaseId)
|
|
297
322
|
return null;
|
|
298
323
|
try {
|
|
299
324
|
const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
|
|
@@ -304,6 +329,18 @@ async function launchRetainedActive(state, paths, environment, launch) {
|
|
|
304
329
|
return null;
|
|
305
330
|
}
|
|
306
331
|
}
|
|
332
|
+
async function repairApprovedRelease(packageRoot, active, dataDir) {
|
|
333
|
+
const repaired = await repairMaterializedRelease(packageRoot, active.releaseRoot);
|
|
334
|
+
assertApprovedReleaseMatches(repaired, active);
|
|
335
|
+
await recordParentCertifiedRelease(repaired, dataDir);
|
|
336
|
+
return repaired;
|
|
337
|
+
}
|
|
338
|
+
function assertApprovedReleaseMatches(release, active) {
|
|
339
|
+
if (release.releaseId !== active.releaseId || release.contentDigest !== active.contentDigest
|
|
340
|
+
|| (active.packageVersion !== undefined && release.packageVersion !== active.packageVersion)) {
|
|
341
|
+
throw new Error("completely verified release differs from the approved active record");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
307
344
|
async function readInstalledVersion(packageRoot) {
|
|
308
345
|
const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
309
346
|
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
@@ -15,7 +15,7 @@ export { PRODUCT_PACKAGE_NAME, createReleaseIdentity, deriveReleaseIdentity, dig
|
|
|
15
15
|
export type { DiscoverReleasePayloadOptions, DiscoveredReleasePayload, ReleaseFileIdentity, ReleaseIdentity } from "./release.js";
|
|
16
16
|
export { collectRelease, prepareReleaseCleanup, runBoundedReleaseCleanup, runReleaseCleanupWorker, scheduleReleaseCleanup } from "./release-gc.js";
|
|
17
17
|
export type { ReleaseCleanupLimits, ReleaseCleanupOperations, ReleaseCleanupOptions, ReleaseCleanupResult, ReleaseCleanupWorkerOptions } from "./release-gc.js";
|
|
18
|
-
export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
|
|
18
|
+
export { RELEASE_MANIFEST_FILENAME, ReleaseRepairUnavailableError, assertCertifiedFilesPresent, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
|
|
19
19
|
export type { CertifiedReleaseRecord, MaterializeReleaseOptions, MaterializedRelease, ReadCertifiedReleaseManifestOptions, ReleaseContentOperation, ReleaseContentOperationEvent, VerifyMaterializedReleaseOptions, } from "./release-store.js";
|
|
20
20
|
export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
|
|
21
21
|
export type { RestartSeal, RestartValidationEvent } from "./restart-certification.js";
|
|
@@ -7,7 +7,7 @@ export { listRecordedEndpoints, liveReleaseIds, probeOwnership, readEndpointMeta
|
|
|
7
7
|
export { cleanupProvenIdleOwner, cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
|
|
8
8
|
export { PRODUCT_PACKAGE_NAME, createReleaseIdentity, deriveReleaseIdentity, digestManifestFiles, discoverReleasePayload, packageRootFromModule, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
9
9
|
export { collectRelease, prepareReleaseCleanup, runBoundedReleaseCleanup, runReleaseCleanupWorker, scheduleReleaseCleanup } from "./release-gc.js";
|
|
10
|
-
export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
|
|
10
|
+
export { RELEASE_MANIFEST_FILENAME, ReleaseRepairUnavailableError, assertCertifiedFilesPresent, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
|
|
11
11
|
export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
|
|
12
12
|
export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
|
|
13
13
|
export { PACKAGE_UNLOCK_PATIENCE_MS, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
@@ -31,12 +31,24 @@ export interface CertifiedReleaseRecord {
|
|
|
31
31
|
/** Absent on records written before releases declared a handoff contract. */
|
|
32
32
|
readonly launchContract?: string;
|
|
33
33
|
}
|
|
34
|
+
/** Reports that installed package bytes cannot safely reproduce an approved release. */
|
|
35
|
+
export declare class ReleaseRepairUnavailableError extends Error {
|
|
36
|
+
readonly code: "RELEASE_REPAIR_UNAVAILABLE";
|
|
37
|
+
constructor(message: string, options?: ErrorOptions);
|
|
38
|
+
}
|
|
34
39
|
/** Process-authority compatibility controls for metadata-only release loading. */
|
|
35
40
|
export type ReadCertifiedReleaseManifestOptions = ReadCertifiedDependencyLayerOptions;
|
|
36
41
|
export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
|
|
37
42
|
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
38
43
|
export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
|
|
39
44
|
export declare function readMaterializedRelease(releaseRoot: string, selectedStoreRoot?: string): Promise<MaterializedRelease>;
|
|
45
|
+
/** Fast presence check before trusted certification permits a release to skip full verification. */
|
|
46
|
+
export declare function assertCertifiedFilesPresent(releaseRoot: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Restore changed or missing product payload files from the installed package without replacing the
|
|
49
|
+
* immutable release directory. Dependency layers retain their own verification and repair policy.
|
|
50
|
+
*/
|
|
51
|
+
export declare function repairMaterializedRelease(packageRoot: string, releaseRoot: string): Promise<MaterializedRelease>;
|
|
40
52
|
/**
|
|
41
53
|
* Load metadata for a release whose bytes were already certified by the
|
|
42
54
|
* current parent process or an authenticated live supervisor. Callers must
|
|
@@ -10,6 +10,14 @@ import { dependencyReference, materializeDependencyLayer, readCertifiedDependenc
|
|
|
10
10
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
11
11
|
const certificationReady = new WeakSet();
|
|
12
12
|
export const RELEASE_MANIFEST_FILENAME = PRODUCT_IDENTITY.manifest.releaseFilename;
|
|
13
|
+
/** Reports that installed package bytes cannot safely reproduce an approved release. */
|
|
14
|
+
export class ReleaseRepairUnavailableError extends Error {
|
|
15
|
+
code = "RELEASE_REPAIR_UNAVAILABLE";
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "ReleaseRepairUnavailableError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
14
22
|
const payload = await discoverReleasePayload(packageRoot, {
|
|
15
23
|
onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
|
|
@@ -71,7 +79,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
71
79
|
const releaseRoot = resolveWithin(storeRoot, identity.releaseId);
|
|
72
80
|
if (await lstat(releaseRoot).catch(() => null)) {
|
|
73
81
|
await rm(candidate, { recursive: true, force: true });
|
|
74
|
-
return certificationReadyRelease(await
|
|
82
|
+
return certificationReadyRelease(await verifyOrRepairMaterializedRelease(payload.packageRoot, releaseRoot, identity));
|
|
75
83
|
}
|
|
76
84
|
await writeFile(resolve(candidate, RELEASE_MANIFEST_FILENAME), JSON.stringify(identity, null, 2), { mode: 0o400, flag: "wx" });
|
|
77
85
|
try {
|
|
@@ -81,7 +89,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
81
89
|
if (!await lstat(releaseRoot).catch(() => null))
|
|
82
90
|
throw error;
|
|
83
91
|
await rm(candidate, { recursive: true, force: true });
|
|
84
|
-
return certificationReadyRelease(await
|
|
92
|
+
return certificationReadyRelease(await verifyOrRepairMaterializedRelease(payload.packageRoot, releaseRoot, identity));
|
|
85
93
|
}
|
|
86
94
|
return certificationReadyRelease({ ...identity, releaseRoot: await realpath(releaseRoot) });
|
|
87
95
|
}
|
|
@@ -102,6 +110,61 @@ export async function readMaterializedRelease(releaseRoot, selectedStoreRoot) {
|
|
|
102
110
|
const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
103
111
|
return certificationReadyRelease(await verifyMaterializedRelease(canonical, manifest, selectedStoreRoot));
|
|
104
112
|
}
|
|
113
|
+
/** Fast presence check before trusted certification permits a release to skip full verification. */
|
|
114
|
+
export async function assertCertifiedFilesPresent(releaseRoot) {
|
|
115
|
+
const canonical = await realpath(releaseRoot);
|
|
116
|
+
const manifest = await readReleaseManifest(canonical);
|
|
117
|
+
await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
|
|
118
|
+
const metadata = await lstat(resolveWithin(canonical, file.path));
|
|
119
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
120
|
+
throw new Error(`certified release file is missing: ${file.path}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Restore changed or missing product payload files from the installed package without replacing the
|
|
125
|
+
* immutable release directory. Dependency layers retain their own verification and repair policy.
|
|
126
|
+
*/
|
|
127
|
+
export async function repairMaterializedRelease(packageRoot, releaseRoot) {
|
|
128
|
+
const canonical = await realpath(releaseRoot);
|
|
129
|
+
let manifest;
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
manifest = await readReleaseManifest(canonical);
|
|
133
|
+
payload = await discoverReleasePayload(packageRoot);
|
|
134
|
+
assertCurrentLaunchContract(payload);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
throw repairUnavailable("release repair metadata is unavailable", error);
|
|
138
|
+
}
|
|
139
|
+
if (payload.packageVersion !== manifest.packageVersion) {
|
|
140
|
+
throw repairUnavailable(`installed package version ${payload.packageVersion} does not match release ${manifest.packageVersion}`);
|
|
141
|
+
}
|
|
142
|
+
const payloadPaths = new Set(payload.paths.filter(path => !path.startsWith("node_modules/")));
|
|
143
|
+
try {
|
|
144
|
+
await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
|
|
145
|
+
const destination = resolveWithin(canonical, file.path);
|
|
146
|
+
if (!await certifiedFileDiffers(destination, file))
|
|
147
|
+
return;
|
|
148
|
+
if (!payloadPaths.has(file.path))
|
|
149
|
+
throw repairUnavailable(`installed package does not carry ${file.path}`);
|
|
150
|
+
const source = resolveWithin(payload.packageRoot, file.path);
|
|
151
|
+
const sourceMetadata = await lstat(source);
|
|
152
|
+
if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink())
|
|
153
|
+
throw repairUnavailable(`installed package file is not regular: ${file.path}`);
|
|
154
|
+
const bytes = payload.cachedFiles.get(file.path) ?? await readFile(source);
|
|
155
|
+
if (bytes.length !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.sha256) {
|
|
156
|
+
throw repairUnavailable(`installed package bytes do not match release file ${file.path}`);
|
|
157
|
+
}
|
|
158
|
+
await replaceReleaseFile(destination, bytes, file);
|
|
159
|
+
});
|
|
160
|
+
return await verifyMaterializedRelease(canonical, manifest);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof ReleaseRepairUnavailableError)
|
|
164
|
+
throw error;
|
|
165
|
+
throw repairUnavailable("release repair could not restore the certified payload", error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
105
168
|
/**
|
|
106
169
|
* Load metadata for a release whose bytes were already certified by the
|
|
107
170
|
* current parent process or an authenticated live supervisor. Callers must
|
|
@@ -197,6 +260,65 @@ async function verifyFile(root, file, options) {
|
|
|
197
260
|
if (digest !== file.sha256)
|
|
198
261
|
throw new Error(`release file digest mismatch: ${file.path}`);
|
|
199
262
|
}
|
|
263
|
+
async function verifyOrRepairMaterializedRelease(packageRoot, releaseRoot, expected) {
|
|
264
|
+
try {
|
|
265
|
+
return await verifyMaterializedRelease(releaseRoot, expected);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
await repairMaterializedRelease(packageRoot, releaseRoot);
|
|
269
|
+
return await verifyMaterializedRelease(releaseRoot, expected);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function readReleaseManifest(releaseRoot) {
|
|
273
|
+
const manifest = JSON.parse(await readFile(resolveWithin(releaseRoot, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
274
|
+
validateManifest(manifest);
|
|
275
|
+
if (releaseRoot.split(sep).at(-1) !== manifest.releaseId)
|
|
276
|
+
throw new Error(`release directory does not match identity ${manifest.releaseId}`);
|
|
277
|
+
return manifest;
|
|
278
|
+
}
|
|
279
|
+
async function certifiedFileDiffers(path, file) {
|
|
280
|
+
const metadata = await lstat(path).catch((error) => {
|
|
281
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
282
|
+
return null;
|
|
283
|
+
throw error;
|
|
284
|
+
});
|
|
285
|
+
if (metadata === null || !metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== file.bytes)
|
|
286
|
+
return true;
|
|
287
|
+
if ((metadata.mode & 0o222) !== 0 || ((metadata.mode & 0o111) !== 0) !== file.executable)
|
|
288
|
+
return true;
|
|
289
|
+
return createHash("sha256").update(await readFile(path)).digest("hex") !== file.sha256;
|
|
290
|
+
}
|
|
291
|
+
async function replaceReleaseFile(path, bytes, file) {
|
|
292
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
293
|
+
const temporary = resolve(dirname(path), `.a1-repair-${randomUUID()}`);
|
|
294
|
+
const mode = file.executable ? 0o500 : 0o400;
|
|
295
|
+
let existing = null;
|
|
296
|
+
try {
|
|
297
|
+
await writeFile(temporary, bytes, { flag: "wx", mode: 0o600 });
|
|
298
|
+
existing = await lstat(path).catch((error) => {
|
|
299
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
300
|
+
return null;
|
|
301
|
+
throw error;
|
|
302
|
+
});
|
|
303
|
+
if (existing !== null) {
|
|
304
|
+
if (!existing.isFile() || existing.isSymbolicLink())
|
|
305
|
+
throw repairUnavailable(`release file cannot be safely replaced: ${file.path}`);
|
|
306
|
+
// Platform: Windows refuses rename-over for a read-only destination.
|
|
307
|
+
await chmod(path, 0o600);
|
|
308
|
+
}
|
|
309
|
+
await rename(temporary, path);
|
|
310
|
+
await chmod(path, mode);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
314
|
+
if (existing !== null)
|
|
315
|
+
await chmod(path, mode).catch(() => { });
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function repairUnavailable(message, cause) {
|
|
320
|
+
return new ReleaseRepairUnavailableError(message, cause === undefined ? {} : { cause });
|
|
321
|
+
}
|
|
200
322
|
function certificationReadyRelease(release) {
|
|
201
323
|
certificationReady.add(release);
|
|
202
324
|
return release;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TerminalHostCommand } from "./ensure-terminal-host.js";
|
|
1
2
|
export interface BoundedHostCommandResult {
|
|
2
3
|
readonly code: number;
|
|
3
4
|
readonly stdout: string;
|
|
@@ -8,9 +9,10 @@ export interface BoundedHostCommandLimits {
|
|
|
8
9
|
readonly timeoutMs: number;
|
|
9
10
|
/** Names the exchange in the timeout error. */
|
|
10
11
|
readonly label: string;
|
|
12
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Runs one terminal-host command whose report is small. An oversized report kills the command and
|
|
14
16
|
* reports a failed exchange; a command that outlives its deadline is killed and rejected.
|
|
15
17
|
*/
|
|
16
|
-
export declare function runBoundedHostCommand(
|
|
18
|
+
export declare function runBoundedHostCommand(command: TerminalHostCommand, arguments_: readonly string[], limits: BoundedHostCommandLimits): Promise<BoundedHostCommandResult>;
|
|
@@ -3,9 +3,13 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
* Runs one terminal-host command whose report is small. An oversized report kills the command and
|
|
4
4
|
* reports a failed exchange; a command that outlives its deadline is killed and rejected.
|
|
5
5
|
*/
|
|
6
|
-
export async function runBoundedHostCommand(
|
|
6
|
+
export async function runBoundedHostCommand(command, arguments_, limits) {
|
|
7
7
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
8
|
-
const child = spawn(executable, [...arguments_], {
|
|
8
|
+
const child = spawn(command.executable, [command.entry, ...arguments_], {
|
|
9
|
+
env: limits.environment,
|
|
10
|
+
windowsHide: true,
|
|
11
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
12
|
+
});
|
|
9
13
|
const stdout = [];
|
|
10
14
|
let size = 0;
|
|
11
15
|
let overflowed = false;
|
|
@@ -42,6 +42,7 @@ export interface TabBridgeOptions {
|
|
|
42
42
|
export declare class TabBridgeClient {
|
|
43
43
|
#private;
|
|
44
44
|
constructor(options: TabBridgeOptions);
|
|
45
|
+
get tabId(): string;
|
|
45
46
|
get available(): boolean;
|
|
46
47
|
captureAttachment(): TabAttachment | null;
|
|
47
48
|
onCommand(listener: (command: TabBridgeCommand) => void): () => void;
|
|
@@ -55,6 +56,7 @@ export declare class TabBridgeClient {
|
|
|
55
56
|
shortcuts(assignments: Readonly<Record<string, string | null>>): void;
|
|
56
57
|
requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
57
58
|
requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
59
|
+
requestClose(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
58
60
|
consumeControllerBoundaryInput(data: string): string;
|
|
59
61
|
close(): void;
|
|
60
62
|
}
|
|
@@ -20,12 +20,16 @@ export class TabBridgeClient {
|
|
|
20
20
|
#pending = new Map();
|
|
21
21
|
#startWaiter = null;
|
|
22
22
|
#commandListeners = new Set();
|
|
23
|
+
#pendingCommands = [];
|
|
23
24
|
#boundaryInputRemainder = "";
|
|
24
25
|
constructor(options) { this.#options = options; }
|
|
26
|
+
get tabId() { return this.#options.credentials.tabId; }
|
|
25
27
|
get available() { return this.#available && this.#connected; }
|
|
26
28
|
captureAttachment() { return this.#attachment === null ? null : { ...this.#attachment }; }
|
|
27
29
|
onCommand(listener) {
|
|
28
30
|
this.#commandListeners.add(listener);
|
|
31
|
+
for (const command of this.#pendingCommands.splice(0))
|
|
32
|
+
listener(command);
|
|
29
33
|
return () => this.#commandListeners.delete(listener);
|
|
30
34
|
}
|
|
31
35
|
async start(timeoutMs = 4_000) {
|
|
@@ -119,6 +123,9 @@ export class TabBridgeClient {
|
|
|
119
123
|
requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
120
124
|
return this.#requestAction("restart", origin, timeoutMs);
|
|
121
125
|
}
|
|
126
|
+
requestClose(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
127
|
+
return this.#requestAction("close", origin, timeoutMs);
|
|
128
|
+
}
|
|
122
129
|
consumeControllerBoundaryInput(data) {
|
|
123
130
|
const prefix = "\x1bP+a1;controller=";
|
|
124
131
|
const suffix = "\x1b\\";
|
|
@@ -176,6 +183,12 @@ export class TabBridgeClient {
|
|
|
176
183
|
this.#fail(false);
|
|
177
184
|
}
|
|
178
185
|
#emitCommand(command) {
|
|
186
|
+
if (this.#commandListeners.size === 0) {
|
|
187
|
+
if (this.#pendingCommands.length >= 16)
|
|
188
|
+
this.#pendingCommands.shift();
|
|
189
|
+
this.#pendingCommands.push(command);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
179
192
|
for (const listener of this.#commandListeners)
|
|
180
193
|
listener(command);
|
|
181
194
|
}
|