@timurproko/a1 0.1.1-dev.6 → 0.1.1-dev.7
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 +32 -9
- package/bin/a1-guardian.js +22 -0
- package/bin/a1-ui.js +7 -4
- package/dist/native/darwin-arm64/a1-process-guardian +0 -0
- package/dist/native/darwin-arm64/manifest.json +20 -0
- package/dist/native/linux-x64/a1-process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +20 -0
- package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
- package/dist/native/win32-x64/manifest.json +20 -0
- package/dist/src/composition/index.d.ts +26 -0
- package/dist/src/composition/index.js +88 -2
- package/dist/src/features/launch/runtime-selection.d.ts +2 -1
- package/dist/src/features/launch/runtime-selection.js +2 -2
- package/dist/src/features/owned-ui/index.d.ts +1 -0
- package/dist/src/features/owned-ui/index.js +1 -0
- package/dist/src/features/owned-ui/run.d.ts +6 -0
- package/dist/src/features/owned-ui/run.js +3 -1
- package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
- package/dist/src/features/owned-ui/settings-app.js +631 -0
- package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
- package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
- package/dist/src/foundation/launch-guardian/index.js +1 -0
- package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
- package/dist/src/foundation/launch-guardian/main.js +165 -0
- package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
- package/dist/src/foundation/lifecycle/commands.js +1 -0
- package/dist/src/foundation/lifecycle/index.d.ts +2 -0
- package/dist/src/foundation/lifecycle/index.js +2 -0
- package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
- package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
- package/dist/src/foundation/lifecycle/model.d.ts +6 -42
- package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
- package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
- package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
- package/dist/src/foundation/owned-ui-settings/index.js +6 -0
- package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
- package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
- package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
- package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
- package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
- package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
- package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
- package/dist/src/foundation/owned-ui-settings/session.js +145 -0
- package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
- package/dist/src/foundation/owned-ui-settings/store.js +90 -0
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
- package/dist/src/foundation/pi-component-adapter/index.js +1 -0
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +12 -3
- package/dist/src/foundation/pi-engine-adapter/adapter.js +73 -7
- package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
- package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
- package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +42 -2
- package/dist/src/foundation/pi-engine-adapter/settings-integration.js +185 -24
- package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
- package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
- package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
- package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
- package/dist/src/foundation/process-containment/artifact.js +34 -0
- package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
- package/dist/src/foundation/process-containment/contracts.js +35 -0
- package/dist/src/foundation/process-containment/index.d.ts +5 -0
- package/dist/src/foundation/process-containment/index.js +5 -0
- package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
- package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
- package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
- package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
- package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
- package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
- package/dist/src/foundation/protocol/client.d.ts +2 -1
- package/dist/src/foundation/protocol/client.js +3 -0
- package/dist/src/foundation/protocol/messages.d.ts +5 -5
- package/dist/src/foundation/protocol/messages.js +1 -1
- package/dist/src/foundation/release/bootstrap.js +29 -10
- package/dist/src/foundation/release/cohort-selection.js +3 -3
- package/dist/src/foundation/release/cohort-state.d.ts +3 -2
- package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
- package/dist/src/foundation/release/process-cleanup.js +2 -2
- package/dist/src/foundation/release/update.js +2 -2
- package/dist/src/foundation/storage/control-store.d.ts +8 -7
- package/dist/src/foundation/storage/control-store.js +105 -55
- package/dist/src/foundation/supervision/server.d.ts +4 -2
- package/dist/src/foundation/supervision/server.js +254 -87
- package/dist/src/foundation/transparent-terminal/command-resolution.js +6 -3
- package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +5 -13
- package/dist/src/foundation/transparent-terminal/foreground-broker.js +11 -68
- package/dist/src/foundation/transparent-terminal/main.js +1 -8
- package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
- package/dist/src/foundation/ui-apps/contracts.js +12 -0
- package/dist/src/foundation/ui-apps/host.d.ts +42 -0
- package/dist/src/foundation/ui-apps/host.js +152 -0
- package/dist/src/foundation/ui-apps/index.d.ts +3 -0
- package/dist/src/foundation/ui-apps/index.js +3 -0
- package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
- package/dist/src/foundation/ui-apps/registry.js +32 -0
- package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
- package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
- package/dist/src/foundation/ui-components/frame.d.ts +21 -0
- package/dist/src/foundation/ui-components/frame.js +54 -0
- package/dist/src/foundation/ui-components/index.d.ts +18 -0
- package/dist/src/foundation/ui-components/index.js +18 -0
- package/dist/src/foundation/ui-components/label.d.ts +8 -0
- package/dist/src/foundation/ui-components/label.js +36 -0
- package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
- package/dist/src/foundation/ui-components/line-input.js +245 -0
- package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
- package/dist/src/foundation/ui-components/list-block.js +176 -0
- package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
- package/dist/src/foundation/ui-components/list-view.js +76 -0
- package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
- package/dist/src/foundation/ui-components/mouse.js +46 -0
- package/dist/src/foundation/ui-components/pane.d.ts +33 -0
- package/dist/src/foundation/ui-components/pane.js +7 -0
- package/dist/src/foundation/ui-components/revision.d.ts +35 -0
- package/dist/src/foundation/ui-components/revision.js +79 -0
- package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
- package/dist/src/foundation/ui-components/scrollbar.js +102 -0
- package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
- package/dist/src/foundation/ui-components/shortcuts.js +81 -0
- package/dist/src/foundation/ui-components/spans.d.ts +7 -0
- package/dist/src/foundation/ui-components/spans.js +59 -0
- package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
- package/dist/src/foundation/ui-components/status-line.js +14 -0
- package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
- package/dist/src/foundation/ui-components/stepper.js +33 -0
- package/dist/src/foundation/ui-components/surface.d.ts +20 -0
- package/dist/src/foundation/ui-components/surface.js +38 -0
- package/dist/src/foundation/ui-components/text.d.ts +24 -0
- package/dist/src/foundation/ui-components/text.js +123 -0
- package/dist/src/foundation/ui-components/theme.d.ts +17 -0
- package/dist/src/foundation/ui-components/theme.js +12 -0
- package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
- package/dist/src/foundation/ui-components/value-menu.js +33 -0
- package/dist/src/product-identity.d.ts +2 -2
- package/dist/src/product-identity.js +2 -1
- package/dist/src/product-identity.json +3 -0
- package/docs/architecture/boundaries.md +31 -13
- package/docs/architecture/process-guardian-provenance.md +26 -0
- package/docs/architecture/toolchain.md +6 -0
- package/docs/architecture/ui-reference-provenance.md +50 -0
- package/docs/ci-release-runbook.md +38 -42
- package/docs/features/launch-profiles.md +7 -1
- package/docs/manual-launch-instance-acceptance.md +66 -0
- package/package.json +11 -12
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LaunchInstanceStopIntent, LaunchProfileId, SupervisorCommand } from "../lifecycle/index.js";
|
|
2
|
+
import { type NativeProcessInspector, type ProcessContainment } from "../process-containment/index.js";
|
|
3
|
+
interface GuardianControl {
|
|
4
|
+
connect(endpoint: string, timeoutMs?: number): Promise<unknown>;
|
|
5
|
+
command(command: SupervisorCommand): Promise<{
|
|
6
|
+
readonly ok: boolean;
|
|
7
|
+
readonly error?: {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
};
|
|
11
|
+
}>;
|
|
12
|
+
on(event: "stopIntent", listener: (intent: LaunchInstanceStopIntent) => void): unknown;
|
|
13
|
+
on(event: "disconnect", listener: () => void): unknown;
|
|
14
|
+
off(event: "stopIntent", listener: (intent: LaunchInstanceStopIntent) => void): unknown;
|
|
15
|
+
off(event: "disconnect", listener: () => void): unknown;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
export interface LaunchGuardianOptions {
|
|
19
|
+
readonly profileId: LaunchProfileId;
|
|
20
|
+
readonly releaseRoot: string;
|
|
21
|
+
readonly uiEntry: string;
|
|
22
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
23
|
+
readonly cwd?: string;
|
|
24
|
+
readonly helperPath?: string;
|
|
25
|
+
readonly inspector?: NativeProcessInspector;
|
|
26
|
+
readonly control?: GuardianControl;
|
|
27
|
+
readonly containment?: ProcessContainment;
|
|
28
|
+
readonly ensureHelper?: (path: string) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare function runLaunchGuardian(options: LaunchGuardianOptions): Promise<number>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { assertLaunchProfileId } from "../lifecycle/index.js";
|
|
5
|
+
import { closeVerifiedContainment, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
|
|
6
|
+
import { SupervisorClient } from "../protocol/index.js";
|
|
7
|
+
import { resolveProductPaths } from "../supervision/index.js";
|
|
8
|
+
export async function runLaunchGuardian(options) {
|
|
9
|
+
assertLaunchProfileId(options.profileId);
|
|
10
|
+
const environment = { ...(options.environment ?? process.env) };
|
|
11
|
+
const paths = resolveProductPaths(environment);
|
|
12
|
+
const instanceId = randomUUID();
|
|
13
|
+
const helperPath = options.helperPath ?? resolveProcessGuardianPath(options.releaseRoot, environment);
|
|
14
|
+
const inspector = options.inspector ?? platformInspector(helperPath);
|
|
15
|
+
await (options.ensureHelper ?? verifyProcessGuardianArtifact)(helperPath);
|
|
16
|
+
const guardianIdentity = await inspector.observe(process.pid);
|
|
17
|
+
if (!guardianIdentity)
|
|
18
|
+
throw diagnosticError("launch guardian cannot verify its own native process identity", "PROCESS_IDENTITY_UNAVAILABLE");
|
|
19
|
+
const client = options.control ?? new SupervisorClient(environment[PRODUCT_IDENTITY.environment.releaseId]);
|
|
20
|
+
await client.connect(paths.endpoint);
|
|
21
|
+
const containment = options.containment ?? new NativeGuardianContainment(instanceId, helperPath, resolve(paths.runtimeDir, "launch-instances", `${instanceId}.ready.json`));
|
|
22
|
+
const stop = stopRequests(client, instanceId);
|
|
23
|
+
let rootIdentity = null;
|
|
24
|
+
try {
|
|
25
|
+
await requireAccepted(client, {
|
|
26
|
+
type: "create-launch-instance",
|
|
27
|
+
requestId: randomUUID(),
|
|
28
|
+
instanceId,
|
|
29
|
+
profileId: options.profileId,
|
|
30
|
+
shutdownPolicy: "terminate-tree-on-close",
|
|
31
|
+
guardianIdentity,
|
|
32
|
+
});
|
|
33
|
+
let handle;
|
|
34
|
+
try {
|
|
35
|
+
handle = await containment.spawn(process.execPath, [options.uiEntry], {
|
|
36
|
+
cwd: options.cwd ?? process.cwd(),
|
|
37
|
+
environment: environmentEntries(environment),
|
|
38
|
+
...(environment.TERM ? { terminalType: environment.TERM } : {}),
|
|
39
|
+
});
|
|
40
|
+
rootIdentity = handle.identity;
|
|
41
|
+
await requireAccepted(client, {
|
|
42
|
+
type: "activate-launch-instance",
|
|
43
|
+
requestId: randomUUID(),
|
|
44
|
+
instanceId,
|
|
45
|
+
rootIdentity,
|
|
46
|
+
containmentIdentity: containment.identity,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const outcome = failureOutcome(error, "guardian-error");
|
|
51
|
+
await completeBestEffort(client, instanceId, "completed", outcome);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const outcome = await Promise.race([
|
|
55
|
+
handle.outcome,
|
|
56
|
+
stop.requested.then(async (reason) => {
|
|
57
|
+
if (reason !== "supervisor-disconnect") {
|
|
58
|
+
await requireAccepted(client, {
|
|
59
|
+
type: "begin-launch-instance-stop",
|
|
60
|
+
requestId: randomUUID(),
|
|
61
|
+
instanceId,
|
|
62
|
+
reason,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!rootIdentity)
|
|
66
|
+
return failureOutcome(new Error("launch root identity is unavailable"), "cleanup-error");
|
|
67
|
+
const result = await closeVerifiedContainment(containment, inspector, rootIdentity, reason);
|
|
68
|
+
await containment.close();
|
|
69
|
+
if (reason === "supervisor-disconnect" && result.outcome.kind === "stopped") {
|
|
70
|
+
return {
|
|
71
|
+
kind: "interrupted",
|
|
72
|
+
reason,
|
|
73
|
+
message: "supervisor disconnected after local launch-instance containment closed",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return result.outcome;
|
|
77
|
+
}),
|
|
78
|
+
]);
|
|
79
|
+
await containment.close();
|
|
80
|
+
if (outcome.kind !== "interrupted" || outcome.reason !== "supervisor-disconnect") {
|
|
81
|
+
await completeBestEffort(client, instanceId, outcome.kind === "interrupted" || outcome.kind === "cleanup-error" ? "interrupted" : "completed", outcome);
|
|
82
|
+
}
|
|
83
|
+
return exitCode(outcome);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await containment.close().catch(() => undefined);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
stop.dispose();
|
|
91
|
+
client.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function platformInspector(helperPath) {
|
|
95
|
+
if (process.platform === "win32")
|
|
96
|
+
return new WindowsNativeProcessInspector(helperPath);
|
|
97
|
+
if (process.platform === "linux")
|
|
98
|
+
return new LinuxNativeProcessInspector();
|
|
99
|
+
throw diagnosticError(`launch-instance process containment is unsupported on ${process.platform}`, "CONTAINMENT_UNSUPPORTED");
|
|
100
|
+
}
|
|
101
|
+
function stopRequests(client, instanceId) {
|
|
102
|
+
let resolveStop;
|
|
103
|
+
let settled = false;
|
|
104
|
+
const requested = new Promise(resolvePromise => { resolveStop = resolvePromise; });
|
|
105
|
+
const request = (reason) => {
|
|
106
|
+
if (settled)
|
|
107
|
+
return;
|
|
108
|
+
settled = true;
|
|
109
|
+
resolveStop(reason);
|
|
110
|
+
};
|
|
111
|
+
const onStopIntent = (intent) => {
|
|
112
|
+
if (intent.instanceId === instanceId)
|
|
113
|
+
request(intent.reason);
|
|
114
|
+
};
|
|
115
|
+
const onDisconnect = () => request("supervisor-disconnect");
|
|
116
|
+
const onSigterm = () => request("user-request");
|
|
117
|
+
const onSigint = () => undefined;
|
|
118
|
+
client.on("stopIntent", onStopIntent);
|
|
119
|
+
client.on("disconnect", onDisconnect);
|
|
120
|
+
process.once("SIGTERM", onSigterm);
|
|
121
|
+
process.on("SIGINT", onSigint);
|
|
122
|
+
return {
|
|
123
|
+
requested,
|
|
124
|
+
dispose() {
|
|
125
|
+
client.off("stopIntent", onStopIntent);
|
|
126
|
+
client.off("disconnect", onDisconnect);
|
|
127
|
+
process.off("SIGTERM", onSigterm);
|
|
128
|
+
process.off("SIGINT", onSigint);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function requireAccepted(client, command) {
|
|
133
|
+
const result = await client.command(command);
|
|
134
|
+
if (!result.ok)
|
|
135
|
+
throw diagnosticError(result.error?.message ?? `supervisor rejected ${command.type}`, result.error?.code ?? "SUPERVISOR_REJECTED");
|
|
136
|
+
}
|
|
137
|
+
async function completeBestEffort(client, instanceId, terminalState, outcome) {
|
|
138
|
+
await requireAccepted(client, {
|
|
139
|
+
type: "complete-launch-instance",
|
|
140
|
+
requestId: randomUUID(),
|
|
141
|
+
instanceId,
|
|
142
|
+
terminalState,
|
|
143
|
+
outcome,
|
|
144
|
+
}).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
function environmentEntries(environment) {
|
|
147
|
+
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined));
|
|
148
|
+
}
|
|
149
|
+
function failureOutcome(error, kind) {
|
|
150
|
+
return {
|
|
151
|
+
kind,
|
|
152
|
+
message: error instanceof Error ? error.message : String(error),
|
|
153
|
+
code: error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function exitCode(outcome) {
|
|
157
|
+
if (outcome.kind === "exited")
|
|
158
|
+
return outcome.exitCode;
|
|
159
|
+
if (outcome.kind === "stopped")
|
|
160
|
+
return 0;
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
function diagnosticError(message, code) {
|
|
164
|
+
return Object.assign(new Error(`A1: ${message}`), { code });
|
|
165
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LaunchInstanceId, LaunchInstanceOutcome, LaunchInstanceShutdownPolicy, LaunchInstanceStopReason, ProcessContainmentIdentity } from "./launch-instance.js";
|
|
2
|
+
import type { LaunchProfileId, NativeProcessIdentity, RequestId } from "./model.js";
|
|
3
|
+
export type SupervisorCommand = {
|
|
4
|
+
readonly type: "create-launch-instance";
|
|
5
|
+
readonly requestId: RequestId;
|
|
6
|
+
readonly instanceId: LaunchInstanceId;
|
|
7
|
+
readonly profileId: LaunchProfileId;
|
|
8
|
+
readonly shutdownPolicy: LaunchInstanceShutdownPolicy;
|
|
9
|
+
readonly guardianIdentity: NativeProcessIdentity;
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: "activate-launch-instance";
|
|
12
|
+
readonly requestId: RequestId;
|
|
13
|
+
readonly instanceId: LaunchInstanceId;
|
|
14
|
+
readonly rootIdentity: NativeProcessIdentity;
|
|
15
|
+
readonly containmentIdentity: ProcessContainmentIdentity;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "begin-launch-instance-stop";
|
|
18
|
+
readonly requestId: RequestId;
|
|
19
|
+
readonly instanceId: LaunchInstanceId;
|
|
20
|
+
readonly reason: LaunchInstanceStopReason;
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: "complete-launch-instance";
|
|
23
|
+
readonly requestId: RequestId;
|
|
24
|
+
readonly instanceId: LaunchInstanceId;
|
|
25
|
+
readonly terminalState: "completed" | "interrupted";
|
|
26
|
+
readonly outcome: LaunchInstanceOutcome;
|
|
27
|
+
} | {
|
|
28
|
+
readonly type: "reconcile-launch-instance";
|
|
29
|
+
readonly requestId: RequestId;
|
|
30
|
+
readonly instanceId: LaunchInstanceId;
|
|
31
|
+
} | {
|
|
32
|
+
readonly type: "resynchronize";
|
|
33
|
+
readonly requestId: RequestId;
|
|
34
|
+
};
|
|
35
|
+
export interface LaunchInstanceStopIntent {
|
|
36
|
+
readonly type: "stop-launch-instance";
|
|
37
|
+
readonly requestId: RequestId;
|
|
38
|
+
readonly instanceId: LaunchInstanceId;
|
|
39
|
+
readonly reason: LaunchInstanceStopReason;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LaunchProfileId, NativeProcessIdentity } from "./model.js";
|
|
2
|
+
export type LaunchInstanceId = string;
|
|
3
|
+
export type LaunchInstanceState = "requested" | "active" | "stopping" | "completed" | "interrupted";
|
|
4
|
+
export type LaunchInstanceShutdownPolicy = "terminate-tree-on-close";
|
|
5
|
+
export type LaunchInstanceStopReason = "owner-disconnect" | "supervisor-disconnect" | "user-request" | "update";
|
|
6
|
+
export interface ProcessContainmentIdentity {
|
|
7
|
+
readonly provider: string;
|
|
8
|
+
readonly token: string;
|
|
9
|
+
}
|
|
10
|
+
export type LaunchInstanceOutcome = {
|
|
11
|
+
readonly kind: "exited";
|
|
12
|
+
readonly exitCode: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "signaled";
|
|
15
|
+
readonly signal: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "stopped";
|
|
18
|
+
readonly reason: LaunchInstanceStopReason;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "spawn-error" | "guardian-error" | "cleanup-error";
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly code: string | null;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "interrupted";
|
|
25
|
+
readonly reason: "owner-disconnect" | "supervisor-disconnect" | "guardian-exit" | "legacy-migration";
|
|
26
|
+
readonly message: string;
|
|
27
|
+
};
|
|
28
|
+
export interface LaunchInstance {
|
|
29
|
+
readonly id: LaunchInstanceId;
|
|
30
|
+
readonly ownerClientId: string;
|
|
31
|
+
readonly profileId: LaunchProfileId;
|
|
32
|
+
readonly state: LaunchInstanceState;
|
|
33
|
+
readonly shutdownPolicy: LaunchInstanceShutdownPolicy;
|
|
34
|
+
readonly guardianIdentity: NativeProcessIdentity;
|
|
35
|
+
readonly rootIdentity: NativeProcessIdentity | null;
|
|
36
|
+
readonly containmentIdentity: ProcessContainmentIdentity | null;
|
|
37
|
+
readonly createdAt: string;
|
|
38
|
+
readonly activatedAt: string | null;
|
|
39
|
+
readonly stoppingAt: string | null;
|
|
40
|
+
readonly completedAt: string | null;
|
|
41
|
+
readonly outcome: LaunchInstanceOutcome | null;
|
|
42
|
+
}
|
|
43
|
+
export declare function assertLaunchInstance(instance: LaunchInstance): void;
|
|
44
|
+
export declare function assertLaunchInstanceTransition(previous: LaunchInstance, next: LaunchInstance): void;
|
|
45
|
+
export declare function assertProcessContainmentIdentity(identity: ProcessContainmentIdentity): void;
|
|
46
|
+
export declare function assertLaunchInstanceOutcome(outcome: LaunchInstanceOutcome): void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { assertLaunchProfileId, assertNativeProcessIdentity } from "./model.js";
|
|
2
|
+
const ALLOWED_TRANSITIONS = {
|
|
3
|
+
requested: ["active", "completed", "interrupted"],
|
|
4
|
+
active: ["stopping", "completed", "interrupted"],
|
|
5
|
+
stopping: ["completed", "interrupted"],
|
|
6
|
+
completed: [],
|
|
7
|
+
interrupted: [],
|
|
8
|
+
};
|
|
9
|
+
export function assertLaunchInstance(instance) {
|
|
10
|
+
assertIdentifier(instance.id, "launch instance identity");
|
|
11
|
+
assertIdentifier(instance.ownerClientId, "launch instance owner identity");
|
|
12
|
+
assertLaunchProfileId(instance.profileId);
|
|
13
|
+
if (!(instance.state in ALLOWED_TRANSITIONS))
|
|
14
|
+
throw new TypeError(`invalid launch instance state: ${String(instance.state)}`);
|
|
15
|
+
if (instance.shutdownPolicy !== "terminate-tree-on-close")
|
|
16
|
+
throw new TypeError("launch instance shutdown policy must terminate its process tree on close");
|
|
17
|
+
assertNativeProcessIdentity(instance.guardianIdentity);
|
|
18
|
+
if (instance.rootIdentity !== null)
|
|
19
|
+
assertNativeProcessIdentity(instance.rootIdentity);
|
|
20
|
+
if (instance.containmentIdentity !== null)
|
|
21
|
+
assertProcessContainmentIdentity(instance.containmentIdentity);
|
|
22
|
+
assertTimestamp(instance.createdAt, "launch instance creation time");
|
|
23
|
+
assertOptionalTimestamp(instance.activatedAt, "launch instance activation time");
|
|
24
|
+
assertOptionalTimestamp(instance.stoppingAt, "launch instance stopping time");
|
|
25
|
+
assertOptionalTimestamp(instance.completedAt, "launch instance completion time");
|
|
26
|
+
if (instance.outcome !== null)
|
|
27
|
+
assertLaunchInstanceOutcome(instance.outcome);
|
|
28
|
+
if (instance.state === "requested") {
|
|
29
|
+
if (instance.rootIdentity !== null || instance.containmentIdentity !== null || instance.activatedAt !== null) {
|
|
30
|
+
throw new TypeError("requested launch instance cannot have active runtime ownership");
|
|
31
|
+
}
|
|
32
|
+
if (instance.stoppingAt !== null || instance.completedAt !== null || instance.outcome !== null) {
|
|
33
|
+
throw new TypeError("requested launch instance cannot have stopping or terminal state");
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (instance.state === "active" || instance.state === "stopping") {
|
|
38
|
+
if (instance.rootIdentity === null || instance.containmentIdentity === null || instance.activatedAt === null) {
|
|
39
|
+
throw new TypeError(`${instance.state} launch instance requires root and containment ownership`);
|
|
40
|
+
}
|
|
41
|
+
if (instance.completedAt !== null || instance.outcome !== null)
|
|
42
|
+
throw new TypeError(`${instance.state} launch instance cannot have a terminal outcome`);
|
|
43
|
+
if (instance.state === "active" && instance.stoppingAt !== null)
|
|
44
|
+
throw new TypeError("active launch instance cannot have a stopping time");
|
|
45
|
+
if (instance.state === "stopping" && instance.stoppingAt === null)
|
|
46
|
+
throw new TypeError("stopping launch instance requires a stopping time");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (instance.completedAt === null || instance.outcome === null)
|
|
50
|
+
throw new TypeError("terminal launch instance requires completion time and outcome");
|
|
51
|
+
if (instance.state === "completed" && instance.outcome.kind === "interrupted")
|
|
52
|
+
throw new TypeError("completed launch instance cannot carry an interrupted outcome");
|
|
53
|
+
if (instance.state === "interrupted" && instance.outcome.kind !== "interrupted" && instance.outcome.kind !== "cleanup-error") {
|
|
54
|
+
throw new TypeError("interrupted launch instance requires an interrupted or cleanup-error outcome");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function assertLaunchInstanceTransition(previous, next) {
|
|
58
|
+
assertLaunchInstance(previous);
|
|
59
|
+
assertLaunchInstance(next);
|
|
60
|
+
if (previous.id !== next.id || previous.ownerClientId !== next.ownerClientId || previous.profileId !== next.profileId) {
|
|
61
|
+
throw new TypeError("launch instance identity and ownership are immutable");
|
|
62
|
+
}
|
|
63
|
+
if (previous.shutdownPolicy !== next.shutdownPolicy
|
|
64
|
+
|| !sameProcessIdentity(previous.guardianIdentity, next.guardianIdentity)
|
|
65
|
+
|| previous.createdAt !== next.createdAt) {
|
|
66
|
+
throw new TypeError("launch instance guardian, policy, and creation identity are immutable");
|
|
67
|
+
}
|
|
68
|
+
const allowedTransitions = ALLOWED_TRANSITIONS[previous.state];
|
|
69
|
+
if (!allowedTransitions.includes(next.state)) {
|
|
70
|
+
throw new TypeError(`invalid launch instance transition: ${previous.state} -> ${next.state}`);
|
|
71
|
+
}
|
|
72
|
+
if (previous.rootIdentity !== null && !sameProcessIdentity(previous.rootIdentity, next.rootIdentity)) {
|
|
73
|
+
throw new TypeError("launch instance root identity is immutable after activation");
|
|
74
|
+
}
|
|
75
|
+
if (previous.containmentIdentity !== null && !sameContainmentIdentity(previous.containmentIdentity, next.containmentIdentity)) {
|
|
76
|
+
throw new TypeError("launch instance containment identity is immutable after activation");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function assertProcessContainmentIdentity(identity) {
|
|
80
|
+
assertIdentifier(identity.provider, "process containment provider");
|
|
81
|
+
assertIdentifier(identity.token, "process containment token");
|
|
82
|
+
}
|
|
83
|
+
export function assertLaunchInstanceOutcome(outcome) {
|
|
84
|
+
if (outcome.kind === "exited") {
|
|
85
|
+
if (!Number.isSafeInteger(outcome.exitCode))
|
|
86
|
+
throw new TypeError("launch instance exit code must be a safe integer");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (outcome.kind === "signaled") {
|
|
90
|
+
assertIdentifier(outcome.signal, "launch instance signal");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (outcome.kind === "stopped") {
|
|
94
|
+
if (!["owner-disconnect", "supervisor-disconnect", "user-request", "update"].includes(outcome.reason)) {
|
|
95
|
+
throw new TypeError("invalid launch instance stop reason");
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (outcome.kind === "interrupted") {
|
|
100
|
+
if (!["owner-disconnect", "supervisor-disconnect", "guardian-exit", "legacy-migration"].includes(outcome.reason)) {
|
|
101
|
+
throw new TypeError("invalid launch instance interruption reason");
|
|
102
|
+
}
|
|
103
|
+
assertDiagnostic(outcome.message, "launch instance interruption message");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (outcome.kind !== "spawn-error" && outcome.kind !== "guardian-error" && outcome.kind !== "cleanup-error") {
|
|
107
|
+
throw new TypeError(`invalid launch instance outcome: ${String(outcome.kind)}`);
|
|
108
|
+
}
|
|
109
|
+
assertDiagnostic(outcome.message, "launch instance failure message");
|
|
110
|
+
if (outcome.code !== null)
|
|
111
|
+
assertIdentifier(outcome.code, "launch instance failure code");
|
|
112
|
+
}
|
|
113
|
+
function sameProcessIdentity(left, right) {
|
|
114
|
+
return right !== null && left.pid === right.pid && left.startIdentity === right.startIdentity;
|
|
115
|
+
}
|
|
116
|
+
function sameContainmentIdentity(left, right) {
|
|
117
|
+
return right !== null && left.provider === right.provider && left.token === right.token;
|
|
118
|
+
}
|
|
119
|
+
function assertIdentifier(value, name) {
|
|
120
|
+
if (!value || value.length > 512 || value.includes("\0"))
|
|
121
|
+
throw new TypeError(`${name} must be non-empty, bounded, and contain no null byte`);
|
|
122
|
+
}
|
|
123
|
+
function assertDiagnostic(value, name) {
|
|
124
|
+
if (!value || value.length > 4_096 || value.includes("\0"))
|
|
125
|
+
throw new TypeError(`${name} must be non-empty, bounded, and contain no null byte`);
|
|
126
|
+
}
|
|
127
|
+
function assertTimestamp(value, name) {
|
|
128
|
+
if (!value || !Number.isFinite(Date.parse(value)))
|
|
129
|
+
throw new TypeError(`${name} must be a valid timestamp`);
|
|
130
|
+
}
|
|
131
|
+
function assertOptionalTimestamp(value, name) {
|
|
132
|
+
if (value !== null)
|
|
133
|
+
assertTimestamp(value, name);
|
|
134
|
+
}
|
|
@@ -2,7 +2,6 @@ export type LaunchProfileId = "a1" | "pi" | "sandbox";
|
|
|
2
2
|
export type GenerationId = string;
|
|
3
3
|
export type DriverProfileId = string;
|
|
4
4
|
export type RequestId = string;
|
|
5
|
-
export type ForegroundTerminalLeaseId = string;
|
|
6
5
|
export interface TerminalDimensions {
|
|
7
6
|
readonly columns: number;
|
|
8
7
|
readonly rows: number;
|
|
@@ -42,55 +41,20 @@ export type TransparentTerminalLifecycleOutcome = {
|
|
|
42
41
|
readonly message: string;
|
|
43
42
|
readonly code: string | null;
|
|
44
43
|
};
|
|
45
|
-
export type ForegroundTerminalLeaseState = "requested" | "active" | "released";
|
|
46
|
-
export interface ForegroundTerminalLease {
|
|
47
|
-
readonly id: ForegroundTerminalLeaseId;
|
|
48
|
-
readonly ownerId: string;
|
|
49
|
-
readonly profile: TransparentTerminalLaunchProfile;
|
|
50
|
-
readonly state: ForegroundTerminalLeaseState;
|
|
51
|
-
readonly generationId: GenerationId | null;
|
|
52
|
-
readonly processIdentity: NativeProcessIdentity | null;
|
|
53
|
-
readonly acquiredAt: string;
|
|
54
|
-
readonly heartbeatAt: string | null;
|
|
55
|
-
readonly releasedAt: string | null;
|
|
56
|
-
readonly outcome: TransparentTerminalLifecycleOutcome | null;
|
|
57
|
-
}
|
|
58
44
|
export interface SupervisorSnapshot {
|
|
59
45
|
readonly revision: number;
|
|
46
|
+
readonly activeInstances: readonly {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly profileId: LaunchProfileId;
|
|
49
|
+
readonly state: "requested" | "active" | "stopping";
|
|
50
|
+
}[];
|
|
60
51
|
}
|
|
61
|
-
export type SupervisorCommand = {
|
|
62
|
-
readonly type: "acquire-foreground-terminal-lease";
|
|
63
|
-
readonly requestId: RequestId;
|
|
64
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
65
|
-
readonly ownerId: string;
|
|
66
|
-
readonly profile: TransparentTerminalLaunchProfile;
|
|
67
|
-
} | {
|
|
68
|
-
readonly type: "activate-foreground-terminal-lease";
|
|
69
|
-
readonly requestId: RequestId;
|
|
70
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
71
|
-
readonly generationId: GenerationId;
|
|
72
|
-
readonly processIdentity: NativeProcessIdentity;
|
|
73
|
-
} | {
|
|
74
|
-
readonly type: "heartbeat-foreground-terminal-lease";
|
|
75
|
-
readonly requestId: RequestId;
|
|
76
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
77
|
-
readonly processIdentity: NativeProcessIdentity;
|
|
78
|
-
} | {
|
|
79
|
-
readonly type: "release-foreground-terminal-lease";
|
|
80
|
-
readonly requestId: RequestId;
|
|
81
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
82
|
-
readonly processIdentity: NativeProcessIdentity | null;
|
|
83
|
-
readonly outcome: TransparentTerminalLifecycleOutcome;
|
|
84
|
-
} | {
|
|
85
|
-
readonly type: "resynchronize";
|
|
86
|
-
readonly requestId: RequestId;
|
|
87
|
-
};
|
|
88
52
|
export interface CommandResult {
|
|
89
53
|
readonly requestId: RequestId;
|
|
90
54
|
readonly ok: boolean;
|
|
91
55
|
readonly revision: number;
|
|
92
56
|
readonly error?: {
|
|
93
|
-
readonly code: "invalid-command" | "not-found" | "stale-generation" | "capability-error" | "driver-error";
|
|
57
|
+
readonly code: "invalid-command" | "not-found" | "stale-generation" | "capability-error" | "containment-unsupported" | "ownership-error" | "driver-error";
|
|
94
58
|
readonly message: string;
|
|
95
59
|
};
|
|
96
60
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const OWNED_UI_SETTINGS_VERSION = 1;
|
|
2
|
+
export type OwnedUiSettingValue = string | number | boolean;
|
|
3
|
+
export type OwnedUiSettingApplication = "live" | "restart";
|
|
4
|
+
export interface OwnedUiSettingDeclaration {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly description: string;
|
|
7
|
+
readonly application: OwnedUiSettingApplication;
|
|
8
|
+
readonly defaultValue: OwnedUiSettingValue;
|
|
9
|
+
readonly allowedValues: readonly OwnedUiSettingValue[];
|
|
10
|
+
}
|
|
11
|
+
export declare const OWNED_UI_SETTING_DECLARATIONS: readonly OwnedUiSettingDeclaration[];
|
|
12
|
+
export declare function assertOwnedUiSettingDeclarations(declarations: readonly OwnedUiSettingDeclaration[]): void;
|
|
13
|
+
export declare function findOwnedUiSettingDeclaration(declarations: readonly OwnedUiSettingDeclaration[], id: string): OwnedUiSettingDeclaration | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const OWNED_UI_SETTINGS_VERSION = 1;
|
|
2
|
+
const MAX_ID_LENGTH = 64;
|
|
3
|
+
const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
4
|
+
export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
|
|
5
|
+
Object.freeze({
|
|
6
|
+
id: "transcriptDensity",
|
|
7
|
+
description: "Vertical density of transcript content. Reserved for the custom-viewport milestone; no visible effect yet.",
|
|
8
|
+
application: "live",
|
|
9
|
+
defaultValue: "comfortable",
|
|
10
|
+
allowedValues: Object.freeze(["comfortable", "compact"]),
|
|
11
|
+
}),
|
|
12
|
+
]);
|
|
13
|
+
export function assertOwnedUiSettingDeclarations(declarations) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const declaration of declarations) {
|
|
16
|
+
if (!ID_PATTERN.test(declaration.id) || declaration.id.length > MAX_ID_LENGTH) {
|
|
17
|
+
throw new Error(`owned UI setting id is not a bounded camelCase identifier: ${declaration.id}`);
|
|
18
|
+
}
|
|
19
|
+
if (seen.has(declaration.id))
|
|
20
|
+
throw new Error(`duplicate owned UI setting id: ${declaration.id}`);
|
|
21
|
+
seen.add(declaration.id);
|
|
22
|
+
if (declaration.description.trim().length === 0) {
|
|
23
|
+
throw new Error(`owned UI setting has no description: ${declaration.id}`);
|
|
24
|
+
}
|
|
25
|
+
if (declaration.allowedValues.length === 0) {
|
|
26
|
+
throw new Error(`owned UI setting declares no allowed values: ${declaration.id}`);
|
|
27
|
+
}
|
|
28
|
+
const types = new Set(declaration.allowedValues.map(value => typeof value));
|
|
29
|
+
if (types.size !== 1) {
|
|
30
|
+
throw new Error(`owned UI setting mixes allowed value types: ${declaration.id}`);
|
|
31
|
+
}
|
|
32
|
+
if (!declaration.allowedValues.includes(declaration.defaultValue)) {
|
|
33
|
+
throw new Error(`owned UI setting default is not an allowed value: ${declaration.id}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function findOwnedUiSettingDeclaration(declarations, id) {
|
|
38
|
+
return declarations.find(declaration => declaration.id === id) ?? null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OwnedUiSettingsMigration {
|
|
2
|
+
/** Version this migration produces. A migration from N-1 declares `to: N`. */
|
|
3
|
+
readonly to: number;
|
|
4
|
+
readonly description: string;
|
|
5
|
+
migrate(values: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Ordered, contiguous migrations ending at OWNED_UI_SETTINGS_VERSION. Version 1
|
|
9
|
+
* is the first stored shape, so nothing precedes it and the list is empty.
|
|
10
|
+
*/
|
|
11
|
+
export declare const OWNED_UI_SETTINGS_MIGRATIONS: readonly OwnedUiSettingsMigration[];
|
|
12
|
+
export declare function assertOwnedUiSettingsMigrations(migrations: readonly OwnedUiSettingsMigration[], currentVersion?: number): void;
|
|
13
|
+
export declare function migrationsFrom(migrations: readonly OwnedUiSettingsMigration[], storedVersion: number): readonly OwnedUiSettingsMigration[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { OWNED_UI_SETTINGS_VERSION } from "./declarations.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ordered, contiguous migrations ending at OWNED_UI_SETTINGS_VERSION. Version 1
|
|
4
|
+
* is the first stored shape, so nothing precedes it and the list is empty.
|
|
5
|
+
*/
|
|
6
|
+
export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([]);
|
|
7
|
+
export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
|
|
8
|
+
const firstProduced = currentVersion - migrations.length + 1;
|
|
9
|
+
if (migrations.length > 0 && firstProduced < 2) {
|
|
10
|
+
throw new Error("owned UI settings migrations cannot produce a version below 2");
|
|
11
|
+
}
|
|
12
|
+
migrations.forEach((migration, index) => {
|
|
13
|
+
const expected = firstProduced + index;
|
|
14
|
+
if (migration.to !== expected) {
|
|
15
|
+
throw new Error(`owned UI settings migration ${index} produces version ${migration.to}, expected ${expected}`);
|
|
16
|
+
}
|
|
17
|
+
if (migration.description.trim().length === 0) {
|
|
18
|
+
throw new Error(`owned UI settings migration to version ${migration.to} has no description`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const last = migrations.at(-1);
|
|
22
|
+
if (last && last.to !== currentVersion) {
|
|
23
|
+
throw new Error(`owned UI settings migrations end at version ${last.to}, expected ${currentVersion}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function migrationsFrom(migrations, storedVersion) {
|
|
27
|
+
return migrations.filter(migration => migration.to > storedVersion);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type OwnedUiSettingDeclaration, type OwnedUiSettingValue } from "./declarations.js";
|
|
2
|
+
import { type OwnedUiSettingsMigration } from "./migrations.js";
|
|
3
|
+
export type OwnedUiSettingSource = "default" | "stored";
|
|
4
|
+
export interface OwnedUiResolvedSetting {
|
|
5
|
+
readonly declaration: OwnedUiSettingDeclaration;
|
|
6
|
+
readonly value: OwnedUiSettingValue;
|
|
7
|
+
readonly source: OwnedUiSettingSource;
|
|
8
|
+
}
|
|
9
|
+
export type OwnedUiSettingsNoticeCode = "document-unreadable" | "version-newer-than-supported" | "migration-unavailable" | "value-rejected";
|
|
10
|
+
export interface OwnedUiSettingsNotice {
|
|
11
|
+
readonly code: OwnedUiSettingsNoticeCode;
|
|
12
|
+
/** Setting id for a rejected value; null for document-level notices. */
|
|
13
|
+
readonly settingId: string | null;
|
|
14
|
+
readonly detail: string;
|
|
15
|
+
}
|
|
16
|
+
export interface OwnedUiSettingsResolution {
|
|
17
|
+
readonly version: number;
|
|
18
|
+
readonly settings: readonly OwnedUiResolvedSetting[];
|
|
19
|
+
/** Keys that match no declaration, preserved verbatim so a downgrade cannot destroy them. */
|
|
20
|
+
readonly preserved: Readonly<Record<string, unknown>>;
|
|
21
|
+
readonly notices: readonly OwnedUiSettingsNotice[];
|
|
22
|
+
/** True when the resolved form differs from what was stored and should be written on the next change. */
|
|
23
|
+
readonly migrated: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface OwnedUiSettingsDocument {
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseOwnedUiSettingsDocument(raw: string): OwnedUiSettingsDocument | null;
|
|
30
|
+
export interface ResolveOwnedUiSettingsInput {
|
|
31
|
+
readonly declarations: readonly OwnedUiSettingDeclaration[];
|
|
32
|
+
readonly migrations: readonly OwnedUiSettingsMigration[];
|
|
33
|
+
/** Parsed document, or null when the file is absent, unreadable, or unparseable. */
|
|
34
|
+
readonly document: OwnedUiSettingsDocument | null;
|
|
35
|
+
/** Set when the document existed but could not be read or parsed. */
|
|
36
|
+
readonly unreadableDetail?: string;
|
|
37
|
+
readonly currentVersion?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pure resolution: declarations plus a parsed document produce a complete value
|
|
41
|
+
* set, the keys to preserve, and every notice worth reporting. Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveOwnedUiSettings(input: ResolveOwnedUiSettingsInput): OwnedUiSettingsResolution;
|
|
44
|
+
export declare function settingValue(resolution: OwnedUiSettingsResolution, id: string): OwnedUiSettingValue | null;
|
|
45
|
+
export declare function documentFrom(resolution: OwnedUiSettingsResolution): OwnedUiSettingsDocument;
|