@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.
Files changed (151) hide show
  1. package/README.md +32 -9
  2. package/bin/a1-guardian.js +22 -0
  3. package/bin/a1-ui.js +7 -4
  4. package/dist/native/darwin-arm64/a1-process-guardian +0 -0
  5. package/dist/native/darwin-arm64/manifest.json +20 -0
  6. package/dist/native/linux-x64/a1-process-guardian +0 -0
  7. package/dist/native/linux-x64/manifest.json +20 -0
  8. package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
  9. package/dist/native/win32-x64/manifest.json +20 -0
  10. package/dist/src/composition/index.d.ts +26 -0
  11. package/dist/src/composition/index.js +88 -2
  12. package/dist/src/features/launch/runtime-selection.d.ts +2 -1
  13. package/dist/src/features/launch/runtime-selection.js +2 -2
  14. package/dist/src/features/owned-ui/index.d.ts +1 -0
  15. package/dist/src/features/owned-ui/index.js +1 -0
  16. package/dist/src/features/owned-ui/run.d.ts +6 -0
  17. package/dist/src/features/owned-ui/run.js +3 -1
  18. package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
  19. package/dist/src/features/owned-ui/settings-app.js +631 -0
  20. package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
  21. package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
  22. package/dist/src/foundation/launch-guardian/index.js +1 -0
  23. package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
  24. package/dist/src/foundation/launch-guardian/main.js +165 -0
  25. package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
  26. package/dist/src/foundation/lifecycle/commands.js +1 -0
  27. package/dist/src/foundation/lifecycle/index.d.ts +2 -0
  28. package/dist/src/foundation/lifecycle/index.js +2 -0
  29. package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
  30. package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
  31. package/dist/src/foundation/lifecycle/model.d.ts +6 -42
  32. package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
  33. package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
  34. package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
  35. package/dist/src/foundation/owned-ui-settings/index.js +6 -0
  36. package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
  37. package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
  38. package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
  39. package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
  40. package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
  41. package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
  42. package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
  43. package/dist/src/foundation/owned-ui-settings/session.js +145 -0
  44. package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
  45. package/dist/src/foundation/owned-ui-settings/store.js +90 -0
  46. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
  47. package/dist/src/foundation/pi-component-adapter/index.js +1 -0
  48. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
  49. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
  50. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +12 -3
  51. package/dist/src/foundation/pi-engine-adapter/adapter.js +73 -7
  52. package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
  53. package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
  54. package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +42 -2
  55. package/dist/src/foundation/pi-engine-adapter/settings-integration.js +185 -24
  56. package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
  57. package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
  58. package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
  59. package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
  60. package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
  61. package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
  62. package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
  63. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
  64. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
  65. package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
  66. package/dist/src/foundation/process-containment/artifact.js +34 -0
  67. package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
  68. package/dist/src/foundation/process-containment/contracts.js +35 -0
  69. package/dist/src/foundation/process-containment/index.d.ts +5 -0
  70. package/dist/src/foundation/process-containment/index.js +5 -0
  71. package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
  72. package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
  73. package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
  74. package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
  75. package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
  76. package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
  77. package/dist/src/foundation/protocol/client.d.ts +2 -1
  78. package/dist/src/foundation/protocol/client.js +3 -0
  79. package/dist/src/foundation/protocol/messages.d.ts +5 -5
  80. package/dist/src/foundation/protocol/messages.js +1 -1
  81. package/dist/src/foundation/release/bootstrap.js +29 -10
  82. package/dist/src/foundation/release/cohort-selection.js +3 -3
  83. package/dist/src/foundation/release/cohort-state.d.ts +3 -2
  84. package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
  85. package/dist/src/foundation/release/process-cleanup.js +2 -2
  86. package/dist/src/foundation/release/update.js +2 -2
  87. package/dist/src/foundation/storage/control-store.d.ts +8 -7
  88. package/dist/src/foundation/storage/control-store.js +105 -55
  89. package/dist/src/foundation/supervision/server.d.ts +4 -2
  90. package/dist/src/foundation/supervision/server.js +254 -87
  91. package/dist/src/foundation/transparent-terminal/command-resolution.js +6 -3
  92. package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +5 -13
  93. package/dist/src/foundation/transparent-terminal/foreground-broker.js +11 -68
  94. package/dist/src/foundation/transparent-terminal/main.js +1 -8
  95. package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
  96. package/dist/src/foundation/ui-apps/contracts.js +12 -0
  97. package/dist/src/foundation/ui-apps/host.d.ts +42 -0
  98. package/dist/src/foundation/ui-apps/host.js +152 -0
  99. package/dist/src/foundation/ui-apps/index.d.ts +3 -0
  100. package/dist/src/foundation/ui-apps/index.js +3 -0
  101. package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
  102. package/dist/src/foundation/ui-apps/registry.js +32 -0
  103. package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
  104. package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
  105. package/dist/src/foundation/ui-components/frame.d.ts +21 -0
  106. package/dist/src/foundation/ui-components/frame.js +54 -0
  107. package/dist/src/foundation/ui-components/index.d.ts +18 -0
  108. package/dist/src/foundation/ui-components/index.js +18 -0
  109. package/dist/src/foundation/ui-components/label.d.ts +8 -0
  110. package/dist/src/foundation/ui-components/label.js +36 -0
  111. package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
  112. package/dist/src/foundation/ui-components/line-input.js +245 -0
  113. package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
  114. package/dist/src/foundation/ui-components/list-block.js +176 -0
  115. package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
  116. package/dist/src/foundation/ui-components/list-view.js +76 -0
  117. package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
  118. package/dist/src/foundation/ui-components/mouse.js +46 -0
  119. package/dist/src/foundation/ui-components/pane.d.ts +33 -0
  120. package/dist/src/foundation/ui-components/pane.js +7 -0
  121. package/dist/src/foundation/ui-components/revision.d.ts +35 -0
  122. package/dist/src/foundation/ui-components/revision.js +79 -0
  123. package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
  124. package/dist/src/foundation/ui-components/scrollbar.js +102 -0
  125. package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
  126. package/dist/src/foundation/ui-components/shortcuts.js +81 -0
  127. package/dist/src/foundation/ui-components/spans.d.ts +7 -0
  128. package/dist/src/foundation/ui-components/spans.js +59 -0
  129. package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
  130. package/dist/src/foundation/ui-components/status-line.js +14 -0
  131. package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
  132. package/dist/src/foundation/ui-components/stepper.js +33 -0
  133. package/dist/src/foundation/ui-components/surface.d.ts +20 -0
  134. package/dist/src/foundation/ui-components/surface.js +38 -0
  135. package/dist/src/foundation/ui-components/text.d.ts +24 -0
  136. package/dist/src/foundation/ui-components/text.js +123 -0
  137. package/dist/src/foundation/ui-components/theme.d.ts +17 -0
  138. package/dist/src/foundation/ui-components/theme.js +12 -0
  139. package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
  140. package/dist/src/foundation/ui-components/value-menu.js +33 -0
  141. package/dist/src/product-identity.d.ts +2 -2
  142. package/dist/src/product-identity.js +2 -1
  143. package/dist/src/product-identity.json +3 -0
  144. package/docs/architecture/boundaries.md +31 -13
  145. package/docs/architecture/process-guardian-provenance.md +26 -0
  146. package/docs/architecture/toolchain.md +6 -0
  147. package/docs/architecture/ui-reference-provenance.md +50 -0
  148. package/docs/ci-release-runbook.md +38 -42
  149. package/docs/features/launch-profiles.md +7 -1
  150. package/docs/manual-launch-instance-acceptance.md +66 -0
  151. 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, assertTransparentTerminalLaunchProfile, } from "../lifecycle/index.js";
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
- #foregroundLease;
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
- constructor(store, paths = resolveProductPaths(), release, bootNonce = randomUUID(), terminateProcess = code => process.exit(code)) {
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
- this.#foregroundLease = store.loadLiveForegroundTerminalLease();
42
+ for (const instance of store.loadActiveLaunchInstances())
43
+ this.#instances.set(instance.id, instance);
32
44
  }
33
45
  snapshot() {
34
- return { revision: this.#revision };
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.#foregroundLease)
58
- this.#releaseForegroundLeaseForUpdate();
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.#foregroundLease === null;
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 ? "foreground terminal lease is still live" : "boot nonce or candidate release mismatch",
95
- liveGenerationIds: this.#liveGenerationIds(),
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
- const accepted = request.bootNonce === this.bootNonce && typeof request.targetVersion === "string";
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.#clients.delete(socket));
141
- socket.on("error", () => this.#clients.delete(socket));
155
+ socket.on("close", () => this.#onSocketClosed(socket));
156
+ socket.on("error", () => this.#onSocketClosed(socket));
142
157
  }
143
158
  async #execute(command, socket) {
144
- const recorded = this.#results.get(command.requestId);
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 === "acquire-foreground-terminal-lease")
152
- this.#acquireForegroundLease(command);
153
- if (command.type === "activate-foreground-terminal-lease")
154
- this.#activateForegroundLease(command);
155
- if (command.type === "heartbeat-foreground-terminal-lease")
156
- this.#heartbeatForegroundLease(command);
157
- if (command.type === "release-foreground-terminal-lease")
158
- this.#releaseForegroundLease(command);
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(command.requestId, result);
194
+ this.#results.set(resultKey, result);
170
195
  this.#send(socket, { type: "command-result", result });
171
196
  }
172
- #acquireForegroundLease(command) {
173
- if (!command.leaseId || !command.ownerId)
174
- throw new Error("foreground lease and owner identities are required");
175
- assertTransparentTerminalLaunchProfile(command.profile);
176
- const lease = {
177
- id: command.leaseId,
178
- ownerId: command.ownerId,
179
- profile: command.profile,
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
- generationId: null,
182
- processIdentity: null,
183
- acquiredAt: new Date().toISOString(),
184
- heartbeatAt: null,
185
- releasedAt: null,
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.acquireForegroundTerminalLease(lease);
189
- this.#foregroundLease = lease;
190
- void this.#writeEndpointMetadata();
228
+ this.store.createLaunchInstance(instance);
229
+ this.#instances.set(instance.id, instance);
230
+ this.#bindOwner(instance.id, socket);
231
+ this.#mutated();
191
232
  }
192
- #activateForegroundLease(command) {
193
- assertNativeProcessIdentity(command.processIdentity);
194
- if (!this.#foregroundLease || this.#foregroundLease.id !== command.leaseId || this.#foregroundLease.state !== "requested")
195
- throw new Error("foreground terminal lease not found or is not requested");
196
- const heartbeatAt = new Date().toISOString();
197
- if (!this.store.activateForegroundTerminalLease(command.leaseId, command.generationId, command.processIdentity, heartbeatAt))
198
- throw new Error("foreground terminal lease activation was rejected");
199
- this.#foregroundLease = { ...this.#foregroundLease, state: "active", generationId: command.generationId, processIdentity: command.processIdentity, heartbeatAt };
200
- void this.#writeEndpointMetadata();
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
- #heartbeatForegroundLease(command) {
203
- assertNativeProcessIdentity(command.processIdentity);
204
- if (!this.#foregroundLease || this.#foregroundLease.id !== command.leaseId || this.#foregroundLease.state !== "active")
205
- throw new Error("active foreground terminal lease not found");
206
- const heartbeatAt = new Date().toISOString();
207
- if (!this.store.heartbeatForegroundTerminalLease(command.leaseId, command.processIdentity, heartbeatAt))
208
- throw new Error("foreground terminal lease ownership mismatch");
209
- this.#foregroundLease = { ...this.#foregroundLease, heartbeatAt };
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
- #releaseForegroundLease(command) {
212
- if (command.processIdentity)
213
- assertNativeProcessIdentity(command.processIdentity);
214
- if (!this.#foregroundLease || this.#foregroundLease.id !== command.leaseId)
215
- throw new Error("foreground terminal lease not found");
216
- const releasedAt = new Date().toISOString();
217
- if (!this.store.releaseForegroundTerminalLease(command.leaseId, command.processIdentity, command.outcome, releasedAt))
218
- throw new Error("foreground terminal lease ownership mismatch");
219
- this.#foregroundLease = null;
220
- void this.#writeEndpointMetadata();
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
- #releaseForegroundLeaseForUpdate() {
223
- const lease = this.#foregroundLease;
224
- if (!lease)
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
- const outcome = { kind: "stopped", reason: "update" };
227
- if (!this.store.releaseForegroundTerminalLease(lease.id, lease.processIdentity, outcome, new Date().toISOString()))
228
- throw new Error("foreground terminal lease update shutdown was rejected");
229
- this.#foregroundLease = null;
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
- #liveGenerationIds() {
232
- return this.#foregroundLease?.generationId ? [this.#foregroundLease.generationId] : [];
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.#foregroundLease ? "busy" : "idle",
250
- liveGenerationIds: this.#liveGenerationIds(),
251
- nonResumableGenerationIds: this.#liveGenerationIds(),
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 { delimiter, extname, isAbsolute, resolve } from "node:path";
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(delimiter).filter(Boolean);
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).map(value => value.startsWith(".") ? value : `.${value}`);
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 { CommandResult, ForegroundTerminalLeaseId, GenerationId, NativeProcessIdentity, RequestId, SupervisorCommand, TransparentTerminalLaunchProfile, TransparentTerminalLifecycleOutcome } from "../lifecycle/index.js";
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
- * Coordinates ownership around one natively attached child. Ordinary terminal
29
- * data never enters this object: the launcher owns native attachment and this
30
- * broker observes only process identity and the final lifecycle outcome.
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, control: ForegroundLeaseControl, launcher: TransparentNativeLauncher, createRequestId: () => RequestId): Promise<ForegroundBrokerResult>;
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
- * Coordinates ownership around one natively attached child. Ordinary terminal
4
- * data never enters this object: the launcher owns native attachment and this
5
- * broker observes only process identity and the final lifecycle outcome.
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, control, launcher, createRequestId) {
8
- assertRequest(request);
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
- const outcome = {
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
- function assertRequest(request) {
73
- if (!request.leaseId || !request.generationId || !request.ownerId)
74
- throw new TypeError("foreground broker identities are required");
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({ leaseId: randomUUID(), generationId: randomUUID(), ownerId, profile, stopRequested: stop.requested }, client, createPlatformTransparentLauncher(), randomUUID);
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) {