@timurproko/a1 0.1.1-dev.5 → 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
|
@@ -2,8 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { 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 { assertNativeProcessIdentity,
|
|
5
|
+
import { assertLaunchInstanceOutcome, assertNativeProcessIdentity, assertProcessContainmentIdentity, } from "../lifecycle/index.js";
|
|
6
6
|
import { encodeFrame, isCommandMessage, isControlHello, LineFrameDecoder, localControlHello, MAX_CONTROL_FRAME_BYTES, negotiateControlFeatures } from "../protocol/index.js";
|
|
7
|
+
import { processIsAlive } from "../release/index.js";
|
|
7
8
|
import { ControlStore } from "../storage/index.js";
|
|
8
9
|
import { resolveProductPaths } from "./paths.js";
|
|
9
10
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
@@ -11,27 +12,45 @@ export class SupervisorServer {
|
|
|
11
12
|
store;
|
|
12
13
|
release;
|
|
13
14
|
terminateProcess;
|
|
15
|
+
reconciliationDeadlineMs;
|
|
16
|
+
shutdownDeadlineMs;
|
|
14
17
|
id = randomUUID();
|
|
15
18
|
bootNonce;
|
|
16
19
|
pidStartIdentity = `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1_000)}`;
|
|
17
20
|
startedAt = new Date().toISOString();
|
|
18
21
|
paths;
|
|
19
22
|
#server = null;
|
|
20
|
-
#
|
|
23
|
+
#instances = new Map();
|
|
24
|
+
#instanceOwners = new Map();
|
|
25
|
+
#clientIds = new Map();
|
|
26
|
+
#clientInstances = new Map();
|
|
27
|
+
#uncertainInstances = new Set();
|
|
28
|
+
#reconciliations = new Map();
|
|
21
29
|
#revision = 0;
|
|
22
30
|
#clients = new Set();
|
|
23
31
|
#results = new Map();
|
|
24
32
|
#metadataWrites = Promise.resolve();
|
|
25
|
-
|
|
33
|
+
#closing = false;
|
|
34
|
+
constructor(store, paths = resolveProductPaths(), release, bootNonce = randomUUID(), terminateProcess = code => process.exit(code), reconciliationDeadlineMs = 3_000, shutdownDeadlineMs = 1_500) {
|
|
26
35
|
this.store = store;
|
|
27
36
|
this.release = release;
|
|
28
37
|
this.terminateProcess = terminateProcess;
|
|
38
|
+
this.reconciliationDeadlineMs = reconciliationDeadlineMs;
|
|
39
|
+
this.shutdownDeadlineMs = shutdownDeadlineMs;
|
|
29
40
|
this.bootNonce = bootNonce;
|
|
30
41
|
this.paths = paths;
|
|
31
|
-
|
|
42
|
+
for (const instance of store.loadActiveLaunchInstances())
|
|
43
|
+
this.#instances.set(instance.id, instance);
|
|
32
44
|
}
|
|
33
45
|
snapshot() {
|
|
34
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
revision: this.#revision,
|
|
48
|
+
activeInstances: [...this.#instances.values()].flatMap(instance => {
|
|
49
|
+
if (instance.state !== "requested" && instance.state !== "active" && instance.state !== "stopping")
|
|
50
|
+
return [];
|
|
51
|
+
return [{ id: instance.id, profileId: instance.profileId, state: instance.state }];
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
35
54
|
}
|
|
36
55
|
async listen() {
|
|
37
56
|
if (this.#server)
|
|
@@ -54,8 +73,10 @@ export class SupervisorServer {
|
|
|
54
73
|
await this.#writeEndpointMetadata();
|
|
55
74
|
}
|
|
56
75
|
async close(stopAgents = false) {
|
|
57
|
-
if (stopAgents && this.#
|
|
58
|
-
|
|
76
|
+
if (stopAgents && !await this.#drainInstances("update", this.shutdownDeadlineMs)) {
|
|
77
|
+
throw new Error("active launch instances did not release ownership within the shutdown deadline");
|
|
78
|
+
}
|
|
79
|
+
this.#closing = true;
|
|
59
80
|
for (const client of this.#clients)
|
|
60
81
|
client.destroy();
|
|
61
82
|
if (this.#server)
|
|
@@ -86,13 +107,13 @@ export class SupervisorServer {
|
|
|
86
107
|
if (isMessageType(value, "release-idle-ownership")) {
|
|
87
108
|
const request = value;
|
|
88
109
|
const identityMatches = request.bootNonce === this.bootNonce && typeof request.candidateReleaseId === "string";
|
|
89
|
-
const released = identityMatches && this.#
|
|
110
|
+
const released = identityMatches && this.#instances.size === 0;
|
|
90
111
|
this.#send(socket, {
|
|
91
112
|
type: "release-ownership-result",
|
|
92
113
|
released,
|
|
93
114
|
reason: released ? "idle cohort released ownership"
|
|
94
|
-
: identityMatches ? "
|
|
95
|
-
|
|
115
|
+
: identityMatches ? "launch instances are still active" : "boot nonce or candidate release mismatch",
|
|
116
|
+
liveInstanceIds: this.#liveInstanceIds(),
|
|
96
117
|
});
|
|
97
118
|
if (released)
|
|
98
119
|
setTimeout(() => void this.closeForReleaseReplacement(false), 25);
|
|
@@ -100,15 +121,7 @@ export class SupervisorServer {
|
|
|
100
121
|
}
|
|
101
122
|
if (isMessageType(value, "release-update-ownership")) {
|
|
102
123
|
const request = value;
|
|
103
|
-
|
|
104
|
-
this.#send(socket, {
|
|
105
|
-
type: "release-update-result",
|
|
106
|
-
accepted,
|
|
107
|
-
reason: accepted ? `verified ${PRODUCT_TEXT.displayName} owner accepted immediate update shutdown` : "boot nonce or target version mismatch",
|
|
108
|
-
liveGenerationIds: this.#liveGenerationIds(),
|
|
109
|
-
});
|
|
110
|
-
if (accepted)
|
|
111
|
-
setTimeout(() => void this.closeForReleaseReplacement(true), 25);
|
|
124
|
+
void this.#handleUpdateRelease(socket, request);
|
|
112
125
|
continue;
|
|
113
126
|
}
|
|
114
127
|
if (!isControlHello(value)) {
|
|
@@ -125,6 +138,8 @@ export class SupervisorServer {
|
|
|
125
138
|
}
|
|
126
139
|
welcomed = true;
|
|
127
140
|
this.#clients.add(socket);
|
|
141
|
+
this.#clientIds.set(socket, value.clientId);
|
|
142
|
+
this.#clientInstances.set(socket, new Set());
|
|
128
143
|
this.#send(socket, { type: "server-hello", ...serverHello, releaseId: this.release.releaseId, supervisorId: this.id, bootNonce: this.bootNonce, pidStartIdentity: this.pidStartIdentity, negotiatedFeatures: negotiation.negotiatedFeatures, snapshot: this.snapshot() });
|
|
129
144
|
}
|
|
130
145
|
else if (isCommandMessage(value)) {
|
|
@@ -137,25 +152,35 @@ export class SupervisorServer {
|
|
|
137
152
|
socket.destroy();
|
|
138
153
|
}
|
|
139
154
|
});
|
|
140
|
-
socket.on("close", () => this.#
|
|
141
|
-
socket.on("error", () => this.#
|
|
155
|
+
socket.on("close", () => this.#onSocketClosed(socket));
|
|
156
|
+
socket.on("error", () => this.#onSocketClosed(socket));
|
|
142
157
|
}
|
|
143
158
|
async #execute(command, socket) {
|
|
144
|
-
const
|
|
159
|
+
const clientId = this.#clientIds.get(socket);
|
|
160
|
+
if (!clientId) {
|
|
161
|
+
this.#send(socket, { type: "protocol-error", code: "unauthenticated-command", message: "launch-instance commands require an authenticated client" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const resultKey = `${clientId}:${command.requestId}`;
|
|
165
|
+
const recorded = this.#results.get(resultKey);
|
|
145
166
|
if (recorded) {
|
|
146
167
|
this.#send(socket, { type: "command-result", result: recorded });
|
|
147
168
|
return;
|
|
148
169
|
}
|
|
149
170
|
let result;
|
|
150
171
|
try {
|
|
151
|
-
if (command.type === "
|
|
152
|
-
this.#
|
|
153
|
-
if (command.type === "activate-
|
|
154
|
-
this.#
|
|
155
|
-
if (command.type === "
|
|
156
|
-
this.#
|
|
157
|
-
if (command.type === "
|
|
158
|
-
this.#
|
|
172
|
+
if (command.type === "create-launch-instance")
|
|
173
|
+
this.#createLaunchInstance(command, socket, clientId);
|
|
174
|
+
if (command.type === "activate-launch-instance")
|
|
175
|
+
this.#activateLaunchInstance(command, socket, clientId);
|
|
176
|
+
if (command.type === "begin-launch-instance-stop")
|
|
177
|
+
this.#beginLaunchInstanceStop(command, socket, clientId);
|
|
178
|
+
if (command.type === "complete-launch-instance")
|
|
179
|
+
this.#completeLaunchInstance(command, socket, clientId);
|
|
180
|
+
if (command.type === "reconcile-launch-instance") {
|
|
181
|
+
this.#ownedInstance(command.instanceId, socket, clientId);
|
|
182
|
+
await this.#reconcileLaunchInstance(command.instanceId);
|
|
183
|
+
}
|
|
159
184
|
if (command.type === "resynchronize")
|
|
160
185
|
this.#send(socket, { type: "snapshot", snapshot: this.snapshot() });
|
|
161
186
|
await this.#metadataWrites;
|
|
@@ -163,75 +188,210 @@ export class SupervisorServer {
|
|
|
163
188
|
}
|
|
164
189
|
catch (error) {
|
|
165
190
|
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
-
const code = /not found/i.test(message) ? "not-found" : "driver-error";
|
|
191
|
+
const code = /not found/i.test(message) ? "not-found" : /owner|ownership|authenticated/i.test(message) ? "ownership-error" : "driver-error";
|
|
167
192
|
result = { requestId: command.requestId, ok: false, revision: this.#revision, error: { code, message } };
|
|
168
193
|
}
|
|
169
|
-
this.#results.set(
|
|
194
|
+
this.#results.set(resultKey, result);
|
|
170
195
|
this.#send(socket, { type: "command-result", result });
|
|
171
196
|
}
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
#createLaunchInstance(command, socket, clientId) {
|
|
198
|
+
assertNativeProcessIdentity(command.guardianIdentity);
|
|
199
|
+
const existing = this.store.loadLaunchInstance(command.instanceId);
|
|
200
|
+
if (existing) {
|
|
201
|
+
if (existing.ownerClientId !== clientId
|
|
202
|
+
|| existing.profileId !== command.profileId
|
|
203
|
+
|| existing.shutdownPolicy !== command.shutdownPolicy
|
|
204
|
+
|| !sameProcessIdentity(existing.guardianIdentity, command.guardianIdentity)) {
|
|
205
|
+
throw new Error("launch instance already exists with different authenticated ownership");
|
|
206
|
+
}
|
|
207
|
+
if (existing.state === "completed" || existing.state === "interrupted")
|
|
208
|
+
throw new Error("launch instance already has a terminal outcome");
|
|
209
|
+
this.#instances.set(existing.id, existing);
|
|
210
|
+
this.#bindOwner(existing.id, socket);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const instance = {
|
|
214
|
+
id: command.instanceId,
|
|
215
|
+
ownerClientId: clientId,
|
|
216
|
+
profileId: command.profileId,
|
|
180
217
|
state: "requested",
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
shutdownPolicy: command.shutdownPolicy,
|
|
219
|
+
guardianIdentity: command.guardianIdentity,
|
|
220
|
+
rootIdentity: null,
|
|
221
|
+
containmentIdentity: null,
|
|
222
|
+
createdAt: new Date().toISOString(),
|
|
223
|
+
activatedAt: null,
|
|
224
|
+
stoppingAt: null,
|
|
225
|
+
completedAt: null,
|
|
186
226
|
outcome: null,
|
|
187
227
|
};
|
|
188
|
-
this.store.
|
|
189
|
-
this.#
|
|
190
|
-
|
|
228
|
+
this.store.createLaunchInstance(instance);
|
|
229
|
+
this.#instances.set(instance.id, instance);
|
|
230
|
+
this.#bindOwner(instance.id, socket);
|
|
231
|
+
this.#mutated();
|
|
191
232
|
}
|
|
192
|
-
#
|
|
193
|
-
assertNativeProcessIdentity(command.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
233
|
+
#activateLaunchInstance(command, socket, clientId) {
|
|
234
|
+
assertNativeProcessIdentity(command.rootIdentity);
|
|
235
|
+
assertProcessContainmentIdentity(command.containmentIdentity);
|
|
236
|
+
const instance = this.#ownedInstance(command.instanceId, socket, clientId);
|
|
237
|
+
if (instance.state === "active"
|
|
238
|
+
&& sameProcessIdentity(instance.rootIdentity, command.rootIdentity)
|
|
239
|
+
&& sameContainmentIdentity(instance.containmentIdentity, command.containmentIdentity))
|
|
240
|
+
return;
|
|
241
|
+
if (instance.state !== "requested")
|
|
242
|
+
throw new Error("launch instance is not requested");
|
|
243
|
+
const activatedAt = new Date().toISOString();
|
|
244
|
+
if (!this.store.activateLaunchInstance(instance.id, clientId, command.rootIdentity, command.containmentIdentity, activatedAt)) {
|
|
245
|
+
throw new Error("launch instance activation ownership mismatch");
|
|
246
|
+
}
|
|
247
|
+
this.#instances.set(instance.id, { ...instance, state: "active", rootIdentity: command.rootIdentity, containmentIdentity: command.containmentIdentity, activatedAt });
|
|
248
|
+
this.#mutated();
|
|
201
249
|
}
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
250
|
+
#beginLaunchInstanceStop(command, socket, clientId) {
|
|
251
|
+
const instance = this.#ownedInstance(command.instanceId, socket, clientId);
|
|
252
|
+
if (instance.state === "stopping")
|
|
253
|
+
return;
|
|
254
|
+
if (instance.state !== "active")
|
|
255
|
+
throw new Error("active launch instance not found");
|
|
256
|
+
const stoppingAt = new Date().toISOString();
|
|
257
|
+
if (!this.store.beginLaunchInstanceStop(instance.id, clientId, stoppingAt))
|
|
258
|
+
throw new Error("launch instance stop ownership mismatch");
|
|
259
|
+
this.#instances.set(instance.id, { ...instance, state: "stopping", stoppingAt });
|
|
260
|
+
this.#mutated();
|
|
210
261
|
}
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!
|
|
215
|
-
throw new Error("
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
262
|
+
#completeLaunchInstance(command, socket, clientId) {
|
|
263
|
+
assertLaunchInstanceOutcome(command.outcome);
|
|
264
|
+
const stored = this.store.loadLaunchInstance(command.instanceId);
|
|
265
|
+
if (!stored)
|
|
266
|
+
throw new Error("launch instance not found");
|
|
267
|
+
if (stored.ownerClientId !== clientId || this.#instanceOwners.get(stored.id) !== socket)
|
|
268
|
+
throw new Error("launch instance ownership mismatch");
|
|
269
|
+
if (stored.state === "completed" || stored.state === "interrupted") {
|
|
270
|
+
if (stored.state === command.terminalState && JSON.stringify(stored.outcome) === JSON.stringify(command.outcome))
|
|
271
|
+
return;
|
|
272
|
+
throw new Error("launch instance already has a different terminal outcome");
|
|
273
|
+
}
|
|
274
|
+
const completedAt = new Date().toISOString();
|
|
275
|
+
if (!this.store.completeLaunchInstance(stored.id, clientId, command.terminalState, command.outcome, completedAt)) {
|
|
276
|
+
throw new Error("launch instance completion ownership mismatch");
|
|
277
|
+
}
|
|
278
|
+
this.#instances.delete(stored.id);
|
|
279
|
+
this.#uncertainInstances.delete(stored.id);
|
|
280
|
+
this.#unbindOwner(stored.id, socket);
|
|
281
|
+
this.#mutated();
|
|
282
|
+
}
|
|
283
|
+
#ownedInstance(instanceId, socket, clientId) {
|
|
284
|
+
const instance = this.#instances.get(instanceId) ?? this.store.loadLaunchInstance(instanceId);
|
|
285
|
+
if (!instance || instance.state === "completed" || instance.state === "interrupted")
|
|
286
|
+
throw new Error("active launch instance not found");
|
|
287
|
+
if (instance.ownerClientId !== clientId || this.#instanceOwners.get(instanceId) !== socket)
|
|
288
|
+
throw new Error("launch instance ownership mismatch");
|
|
289
|
+
return instance;
|
|
290
|
+
}
|
|
291
|
+
#bindOwner(instanceId, socket) {
|
|
292
|
+
const current = this.#instanceOwners.get(instanceId);
|
|
293
|
+
if (current && current !== socket)
|
|
294
|
+
throw new Error("launch instance already belongs to another authenticated connection");
|
|
295
|
+
this.#instanceOwners.set(instanceId, socket);
|
|
296
|
+
this.#clientInstances.get(socket)?.add(instanceId);
|
|
297
|
+
}
|
|
298
|
+
#unbindOwner(instanceId, socket) {
|
|
299
|
+
this.#instanceOwners.delete(instanceId);
|
|
300
|
+
this.#clientInstances.get(socket)?.delete(instanceId);
|
|
301
|
+
}
|
|
302
|
+
#onSocketClosed(socket) {
|
|
303
|
+
if (!this.#clients.delete(socket) && !this.#clientIds.has(socket))
|
|
304
|
+
return;
|
|
305
|
+
const instanceIds = [...(this.#clientInstances.get(socket) ?? [])];
|
|
306
|
+
this.#clientInstances.delete(socket);
|
|
307
|
+
this.#clientIds.delete(socket);
|
|
308
|
+
for (const instanceId of instanceIds) {
|
|
309
|
+
if (this.#instanceOwners.get(instanceId) === socket)
|
|
310
|
+
this.#instanceOwners.delete(instanceId);
|
|
311
|
+
if (!this.#closing)
|
|
312
|
+
void this.#reconcileLaunchInstance(instanceId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
#reconcileLaunchInstance(instanceId) {
|
|
316
|
+
const existing = this.#reconciliations.get(instanceId);
|
|
317
|
+
if (existing)
|
|
318
|
+
return existing;
|
|
319
|
+
const reconciliation = this.#performReconciliation(instanceId).finally(() => this.#reconciliations.delete(instanceId));
|
|
320
|
+
this.#reconciliations.set(instanceId, reconciliation);
|
|
321
|
+
return reconciliation;
|
|
221
322
|
}
|
|
222
|
-
#
|
|
223
|
-
const
|
|
224
|
-
if (!
|
|
323
|
+
async #performReconciliation(instanceId) {
|
|
324
|
+
const instance = this.#instances.get(instanceId);
|
|
325
|
+
if (!instance)
|
|
326
|
+
return;
|
|
327
|
+
const deadline = Date.now() + this.reconciliationDeadlineMs;
|
|
328
|
+
while (instance.rootIdentity && processIsAlive(instance.rootIdentity.pid) && Date.now() < deadline) {
|
|
329
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 25));
|
|
330
|
+
}
|
|
331
|
+
if (instance.state === "requested" || (instance.rootIdentity && !processIsAlive(instance.rootIdentity.pid))) {
|
|
332
|
+
const outcome = { kind: "interrupted", reason: "owner-disconnect", message: "launch instance owner disconnected before reporting a terminal outcome" };
|
|
333
|
+
if (this.store.completeLaunchInstance(instance.id, instance.ownerClientId, "interrupted", outcome, new Date().toISOString())) {
|
|
334
|
+
this.#instances.delete(instance.id);
|
|
335
|
+
this.#uncertainInstances.delete(instance.id);
|
|
336
|
+
this.#mutated();
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this.#uncertainInstances.add(instance.id);
|
|
341
|
+
await this.#writeEndpointMetadata();
|
|
342
|
+
}
|
|
343
|
+
async #handleUpdateRelease(socket, request) {
|
|
344
|
+
const identityMatches = request.bootNonce === this.bootNonce && typeof request.targetVersion === "string";
|
|
345
|
+
if (!identityMatches) {
|
|
346
|
+
this.#send(socket, {
|
|
347
|
+
type: "release-update-result",
|
|
348
|
+
accepted: false,
|
|
349
|
+
reason: "boot nonce or target version mismatch",
|
|
350
|
+
liveInstanceIds: this.#liveInstanceIds(),
|
|
351
|
+
});
|
|
225
352
|
return;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
353
|
+
}
|
|
354
|
+
const released = await this.#drainInstances("update", this.shutdownDeadlineMs);
|
|
355
|
+
this.#send(socket, {
|
|
356
|
+
type: "release-update-result",
|
|
357
|
+
accepted: released,
|
|
358
|
+
reason: released
|
|
359
|
+
? `verified ${PRODUCT_TEXT.displayName} owner drained all launch instances for immediate update shutdown`
|
|
360
|
+
: "one or more launch instances did not release verified ownership within the update deadline",
|
|
361
|
+
liveInstanceIds: this.#liveInstanceIds(),
|
|
362
|
+
});
|
|
363
|
+
if (released)
|
|
364
|
+
setTimeout(() => void this.closeForReleaseReplacement(false), 25);
|
|
365
|
+
}
|
|
366
|
+
async #drainInstances(reason, timeoutMs) {
|
|
367
|
+
if (this.#instances.size === 0)
|
|
368
|
+
return true;
|
|
369
|
+
this.#requestStopAll(reason);
|
|
370
|
+
const deadline = Date.now() + timeoutMs;
|
|
371
|
+
while (Date.now() < deadline) {
|
|
372
|
+
if (this.#instances.size === 0) {
|
|
373
|
+
// Let the terminal command result flush before closing owner sockets.
|
|
374
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 25));
|
|
375
|
+
return this.#instances.size === 0;
|
|
376
|
+
}
|
|
377
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 25));
|
|
378
|
+
}
|
|
379
|
+
return this.#instances.size === 0;
|
|
230
380
|
}
|
|
231
|
-
#
|
|
232
|
-
|
|
381
|
+
#requestStopAll(reason) {
|
|
382
|
+
for (const [instanceId, socket] of this.#instanceOwners) {
|
|
383
|
+
this.#send(socket, { type: "stop-launch-instance", requestId: randomUUID(), instanceId, reason });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
#mutated() {
|
|
387
|
+
this.#revision += 1;
|
|
388
|
+
void this.#writeEndpointMetadata();
|
|
389
|
+
}
|
|
390
|
+
#liveInstanceIds() {
|
|
391
|
+
return [...this.#instances.keys()];
|
|
233
392
|
}
|
|
234
393
|
#writeEndpointMetadata() {
|
|
394
|
+
const liveInstanceIds = this.#liveInstanceIds();
|
|
235
395
|
const metadata = {
|
|
236
396
|
schema: PRODUCT_IDENTITY.protocol.supervisorSchema,
|
|
237
397
|
...localControlHello(this.release.releaseId),
|
|
@@ -246,9 +406,10 @@ export class SupervisorServer {
|
|
|
246
406
|
releaseRoot: this.release.releaseRoot,
|
|
247
407
|
contentDigest: this.release.contentDigest,
|
|
248
408
|
ownership: {
|
|
249
|
-
state: this.#
|
|
250
|
-
|
|
251
|
-
|
|
409
|
+
state: this.#uncertainInstances.size > 0 ? "blocked" : liveInstanceIds.length > 0 ? "busy" : "idle",
|
|
410
|
+
liveInstanceIds,
|
|
411
|
+
nonResumableInstanceIds: liveInstanceIds,
|
|
412
|
+
uncertainInstanceIds: [...this.#uncertainInstances],
|
|
252
413
|
},
|
|
253
414
|
};
|
|
254
415
|
const temporary = `${this.paths.endpointMetadataPath}.${process.pid}.tmp`;
|
|
@@ -270,6 +431,12 @@ export class SupervisorServer {
|
|
|
270
431
|
socket.write(frame);
|
|
271
432
|
}
|
|
272
433
|
}
|
|
434
|
+
function sameProcessIdentity(left, right) {
|
|
435
|
+
return left !== null && left.pid === right.pid && left.startIdentity === right.startIdentity;
|
|
436
|
+
}
|
|
437
|
+
function sameContainmentIdentity(left, right) {
|
|
438
|
+
return left !== null && left.provider === right.provider && left.token === right.token;
|
|
439
|
+
}
|
|
273
440
|
function isMessageType(value, type) {
|
|
274
441
|
return typeof value === "object" && value !== null && "type" in value && value.type === type;
|
|
275
442
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { extname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
/** Resolve a command without invoking a command interpreter. */
|
|
4
4
|
export async function resolveTransparentCommand(executable, arguments_, options) {
|
|
5
5
|
const platform = options.platform ?? process.platform;
|
|
@@ -17,7 +17,7 @@ async function resolveWindowsCommand(command, commandArguments, options) {
|
|
|
17
17
|
}
|
|
18
18
|
async function resolveWindowsPathCommand(executable, cwd, environment) {
|
|
19
19
|
const hasPath = isAbsolute(executable) || executable.includes("/") || executable.includes("\\");
|
|
20
|
-
const roots = hasPath ? [cwd] : windowsPath(environment).split(
|
|
20
|
+
const roots = hasPath ? [cwd] : windowsPath(environment).split(";").filter(Boolean);
|
|
21
21
|
const extension = extname(executable);
|
|
22
22
|
const extensions = extension ? [""] : windowsExecutableExtensions(environment);
|
|
23
23
|
for (const root of roots) {
|
|
@@ -56,7 +56,10 @@ function windowsPath(environment) {
|
|
|
56
56
|
}
|
|
57
57
|
function windowsExecutableExtensions(environment) {
|
|
58
58
|
const source = environment.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
59
|
-
return source.split(";").filter(Boolean).
|
|
59
|
+
return [...new Set(source.split(";").filter(Boolean).flatMap(value => {
|
|
60
|
+
const extension = value.startsWith(".") ? value : `.${value}`;
|
|
61
|
+
return [extension, extension.toLowerCase()];
|
|
62
|
+
}))];
|
|
60
63
|
}
|
|
61
64
|
async function isFile(path) {
|
|
62
65
|
try {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface ForegroundLeaseControl {
|
|
3
|
-
command(command: SupervisorCommand): Promise<CommandResult>;
|
|
4
|
-
}
|
|
1
|
+
import type { NativeProcessIdentity, TransparentTerminalLaunchProfile, TransparentTerminalLifecycleOutcome } from "../lifecycle/index.js";
|
|
5
2
|
export type TransparentStopReason = "owner-disconnect" | "user-request" | "update";
|
|
6
3
|
export interface TransparentChildHandle {
|
|
7
4
|
readonly processIdentity: NativeProcessIdentity;
|
|
@@ -12,21 +9,16 @@ export interface TransparentNativeLauncher {
|
|
|
12
9
|
launch(profile: TransparentTerminalLaunchProfile): Promise<TransparentChildHandle>;
|
|
13
10
|
}
|
|
14
11
|
export interface ForegroundBrokerRequest {
|
|
15
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
16
|
-
readonly generationId: GenerationId;
|
|
17
|
-
readonly ownerId: string;
|
|
18
12
|
readonly profile: TransparentTerminalLaunchProfile;
|
|
19
13
|
readonly stopRequested?: Promise<TransparentStopReason>;
|
|
20
14
|
}
|
|
21
15
|
export interface ForegroundBrokerResult {
|
|
22
|
-
readonly leaseId: ForegroundTerminalLeaseId;
|
|
23
|
-
readonly generationId: GenerationId;
|
|
24
16
|
readonly processIdentity: NativeProcessIdentity | null;
|
|
25
17
|
readonly outcome: TransparentTerminalLifecycleOutcome;
|
|
26
18
|
}
|
|
27
19
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
20
|
+
* Waits for one directly attached child. Launch-instance registration and
|
|
21
|
+
* process-tree containment belong to the outer profile-neutral guardian.
|
|
22
|
+
* Ordinary terminal data never enters this object.
|
|
31
23
|
*/
|
|
32
|
-
export declare function runForegroundBroker(request: ForegroundBrokerRequest,
|
|
24
|
+
export declare function runForegroundBroker(request: ForegroundBrokerRequest, launcher: TransparentNativeLauncher): Promise<ForegroundBrokerResult>;
|
|
@@ -1,83 +1,26 @@
|
|
|
1
1
|
import { assertNativeProcessIdentity, assertTransparentTerminalLaunchProfile } from "../lifecycle/index.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Waits for one directly attached child. Launch-instance registration and
|
|
4
|
+
* process-tree containment belong to the outer profile-neutral guardian.
|
|
5
|
+
* Ordinary terminal data never enters this object.
|
|
6
6
|
*/
|
|
7
|
-
export async function runForegroundBroker(request,
|
|
8
|
-
|
|
9
|
-
await requireAccepted(control, {
|
|
10
|
-
type: "acquire-foreground-terminal-lease",
|
|
11
|
-
requestId: createRequestId(),
|
|
12
|
-
leaseId: request.leaseId,
|
|
13
|
-
ownerId: request.ownerId,
|
|
14
|
-
profile: request.profile,
|
|
15
|
-
});
|
|
7
|
+
export async function runForegroundBroker(request, launcher) {
|
|
8
|
+
assertTransparentTerminalLaunchProfile(request.profile);
|
|
16
9
|
let handle;
|
|
17
10
|
try {
|
|
18
11
|
handle = await launcher.launch(request.profile);
|
|
19
12
|
assertNativeProcessIdentity(handle.processIdentity);
|
|
20
13
|
}
|
|
21
14
|
catch (error) {
|
|
22
|
-
|
|
23
|
-
kind: "spawn-error",
|
|
24
|
-
message: errorMessage(error),
|
|
25
|
-
code: errorCode(error),
|
|
26
|
-
};
|
|
27
|
-
await requireAccepted(control, {
|
|
28
|
-
type: "release-foreground-terminal-lease",
|
|
29
|
-
requestId: createRequestId(),
|
|
30
|
-
leaseId: request.leaseId,
|
|
15
|
+
return {
|
|
31
16
|
processIdentity: null,
|
|
32
|
-
outcome,
|
|
33
|
-
});
|
|
34
|
-
return { leaseId: request.leaseId, generationId: request.generationId, processIdentity: null, outcome };
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
await requireAccepted(control, {
|
|
38
|
-
type: "activate-foreground-terminal-lease",
|
|
39
|
-
requestId: createRequestId(),
|
|
40
|
-
leaseId: request.leaseId,
|
|
41
|
-
generationId: request.generationId,
|
|
42
|
-
processIdentity: handle.processIdentity,
|
|
43
|
-
});
|
|
44
|
-
const outcome = request.stopRequested
|
|
45
|
-
? await Promise.race([handle.outcome, request.stopRequested.then(async (reason) => await handle.stop(reason))])
|
|
46
|
-
: await handle.outcome;
|
|
47
|
-
await requireAccepted(control, {
|
|
48
|
-
type: "release-foreground-terminal-lease",
|
|
49
|
-
requestId: createRequestId(),
|
|
50
|
-
leaseId: request.leaseId,
|
|
51
|
-
processIdentity: handle.processIdentity,
|
|
52
|
-
outcome,
|
|
53
|
-
});
|
|
54
|
-
return { leaseId: request.leaseId, generationId: request.generationId, processIdentity: handle.processIdentity, outcome };
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const outcome = {
|
|
58
|
-
kind: "broker-error",
|
|
59
|
-
message: errorMessage(error),
|
|
60
|
-
code: errorCode(error),
|
|
17
|
+
outcome: { kind: "spawn-error", message: errorMessage(error), code: errorCode(error) },
|
|
61
18
|
};
|
|
62
|
-
await control.command({
|
|
63
|
-
type: "release-foreground-terminal-lease",
|
|
64
|
-
requestId: createRequestId(),
|
|
65
|
-
leaseId: request.leaseId,
|
|
66
|
-
processIdentity: handle.processIdentity,
|
|
67
|
-
outcome,
|
|
68
|
-
}).catch(() => undefined);
|
|
69
|
-
throw error;
|
|
70
19
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assertTransparentTerminalLaunchProfile(request.profile);
|
|
76
|
-
}
|
|
77
|
-
async function requireAccepted(control, command) {
|
|
78
|
-
const result = await control.command(command);
|
|
79
|
-
if (!result.ok)
|
|
80
|
-
throw new Error(result.error?.message ?? `supervisor rejected ${command.type}`);
|
|
20
|
+
const outcome = request.stopRequested
|
|
21
|
+
? await Promise.race([handle.outcome, request.stopRequested.then(async (reason) => await handle.stop(reason))])
|
|
22
|
+
: await handle.outcome;
|
|
23
|
+
return { processIdentity: handle.processIdentity, outcome };
|
|
81
24
|
}
|
|
82
25
|
function errorMessage(error) {
|
|
83
26
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2,22 +2,16 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { assertLaunchProfileId } from "../lifecycle/index.js";
|
|
5
|
-
import { SupervisorClient } from "../protocol/index.js";
|
|
6
|
-
import { resolveProductPaths } from "../supervision/index.js";
|
|
7
5
|
import { runForegroundBroker } from "./foreground-broker.js";
|
|
8
6
|
import { createPlatformTransparentLauncher } from "./native-launcher.js";
|
|
9
7
|
export async function runTransparentForeground(options) {
|
|
10
8
|
const environment = { ...(options.environment ?? process.env) };
|
|
11
9
|
const profileId = options.profileId ?? environment.A1_LAUNCH_PROFILE ?? "a1";
|
|
12
10
|
assertLaunchProfileId(profileId);
|
|
13
|
-
const paths = resolveProductPaths(environment);
|
|
14
|
-
const client = new SupervisorClient(environment.A1_RELEASE_ID);
|
|
15
|
-
await client.connect(paths.endpoint);
|
|
16
|
-
const ownerId = `foreground-broker-${randomUUID()}`;
|
|
17
11
|
const profile = await launchProfile(environment, options, profileId);
|
|
18
12
|
const stop = stopSignal();
|
|
19
13
|
try {
|
|
20
|
-
const result = await runForegroundBroker({
|
|
14
|
+
const result = await runForegroundBroker({ profile, stopRequested: stop.requested }, createPlatformTransparentLauncher());
|
|
21
15
|
if (result.outcome.kind === "exited")
|
|
22
16
|
return result.outcome.exitCode;
|
|
23
17
|
if (result.outcome.kind === "signaled")
|
|
@@ -28,7 +22,6 @@ export async function runTransparentForeground(options) {
|
|
|
28
22
|
}
|
|
29
23
|
finally {
|
|
30
24
|
stop.dispose();
|
|
31
|
-
client.close();
|
|
32
25
|
}
|
|
33
26
|
}
|
|
34
27
|
async function launchProfile(environment, options, profileId) {
|