@amalgm/shell 0.1.162 → 0.1.164-main.34182640231

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 (144) hide show
  1. package/PURPOSE.md +163 -17
  2. package/README.md +74 -11
  3. package/config/releases/trust/runtime/main.root.json +84 -0
  4. package/config/releases/trust/runtime/preview.root.json +84 -0
  5. package/dist/authorization/recovery-host.d.ts +1 -0
  6. package/dist/authorization/recovery-host.js +2 -3
  7. package/dist/authorization/recovery-host.js.map +1 -1
  8. package/dist/boot.js +28 -20
  9. package/dist/boot.js.map +1 -1
  10. package/dist/cli-arguments.d.ts +8 -0
  11. package/dist/cli-arguments.js +47 -0
  12. package/dist/cli-arguments.js.map +1 -0
  13. package/dist/cli.d.ts +6 -4
  14. package/dist/cli.js +91 -73
  15. package/dist/cli.js.map +1 -1
  16. package/dist/compose.js +4 -3
  17. package/dist/compose.js.map +1 -1
  18. package/dist/content-cache-index.js +2 -2
  19. package/dist/content-cache-index.js.map +1 -1
  20. package/dist/detection/runtime.js +2 -2
  21. package/dist/detection/runtime.js.map +1 -1
  22. package/dist/development/profile.d.ts +27 -0
  23. package/dist/development/profile.js +142 -0
  24. package/dist/development/profile.js.map +1 -0
  25. package/dist/entity-apply-host.js +2 -2
  26. package/dist/entity-apply-host.js.map +1 -1
  27. package/dist/entity-apply-store.d.ts +2 -1
  28. package/dist/entity-apply-store.js +52 -37
  29. package/dist/entity-apply-store.js.map +1 -1
  30. package/dist/files-register-host.d.ts +1 -0
  31. package/dist/files-register-host.js +1 -0
  32. package/dist/files-register-host.js.map +1 -1
  33. package/dist/installation/environment.d.ts +30 -0
  34. package/dist/installation/environment.js +28 -0
  35. package/dist/installation/environment.js.map +1 -0
  36. package/dist/installation/lease.d.ts +6 -0
  37. package/dist/installation/lease.js +28 -0
  38. package/dist/installation/lease.js.map +1 -0
  39. package/dist/installation/ownership.d.ts +30 -0
  40. package/dist/installation/ownership.js +156 -0
  41. package/dist/installation/ownership.js.map +1 -0
  42. package/dist/installation/selection.d.ts +8 -0
  43. package/dist/installation/selection.js +29 -0
  44. package/dist/installation/selection.js.map +1 -0
  45. package/dist/machine-host.d.ts +27 -5
  46. package/dist/machine-host.js +244 -96
  47. package/dist/machine-host.js.map +1 -1
  48. package/dist/main.js +56 -2
  49. package/dist/main.js.map +1 -1
  50. package/dist/platform-product-host.d.ts +9 -9
  51. package/dist/platform-product-host.js +17 -17
  52. package/dist/platform-product-host.js.map +1 -1
  53. package/dist/platform-tools.d.ts +4 -3
  54. package/dist/platform-tools.js +34 -37
  55. package/dist/platform-tools.js.map +1 -1
  56. package/dist/process-lifecycle.d.ts +6 -0
  57. package/dist/process-lifecycle.js +42 -0
  58. package/dist/process-lifecycle.js.map +1 -0
  59. package/dist/pty-host.d.ts +1 -1
  60. package/dist/pty-host.js +70 -21
  61. package/dist/pty-host.js.map +1 -1
  62. package/dist/runtime-command-host.d.ts +7 -0
  63. package/dist/runtime-command-host.js +49 -13
  64. package/dist/runtime-command-host.js.map +1 -1
  65. package/dist/service/adapter.d.ts +36 -0
  66. package/dist/service/adapter.js +24 -0
  67. package/dist/service/adapter.js.map +1 -0
  68. package/dist/service/cli.d.ts +8 -0
  69. package/dist/service/cli.js +237 -0
  70. package/dist/service/cli.js.map +1 -0
  71. package/dist/service/control.d.ts +38 -0
  72. package/dist/service/control.js +68 -0
  73. package/dist/service/control.js.map +1 -0
  74. package/dist/service/index.d.ts +9 -0
  75. package/dist/service/index.js +10 -0
  76. package/dist/service/index.js.map +1 -0
  77. package/dist/service/keepalive.d.ts +18 -0
  78. package/dist/service/keepalive.js +418 -0
  79. package/dist/service/keepalive.js.map +1 -0
  80. package/dist/service/launchd.d.ts +9 -0
  81. package/dist/service/launchd.js +105 -0
  82. package/dist/service/launchd.js.map +1 -0
  83. package/dist/service/owner.d.ts +4 -0
  84. package/dist/service/owner.js +14 -0
  85. package/dist/service/owner.js.map +1 -0
  86. package/dist/service/portable.d.ts +10 -0
  87. package/dist/service/portable.js +32 -0
  88. package/dist/service/portable.js.map +1 -0
  89. package/dist/service/recovery.d.ts +27 -0
  90. package/dist/service/recovery.js +58 -0
  91. package/dist/service/recovery.js.map +1 -0
  92. package/dist/service/registration.d.ts +82 -0
  93. package/dist/service/registration.js +114 -0
  94. package/dist/service/registration.js.map +1 -0
  95. package/dist/service/systemd-unit.d.ts +7 -0
  96. package/dist/service/systemd-unit.js +43 -0
  97. package/dist/service/systemd-unit.js.map +1 -0
  98. package/dist/service/systemd.d.ts +5 -0
  99. package/dist/service/systemd.js +72 -0
  100. package/dist/service/systemd.js.map +1 -0
  101. package/dist/service/windows-action.d.ts +9 -0
  102. package/dist/service/windows-action.js +55 -0
  103. package/dist/service/windows-action.js.map +1 -0
  104. package/dist/service/windows-task.d.ts +9 -0
  105. package/dist/service/windows-task.js +46 -0
  106. package/dist/service/windows-task.js.map +1 -0
  107. package/dist/service/windows.d.ts +2 -0
  108. package/dist/service/windows.js +65 -0
  109. package/dist/service/windows.js.map +1 -0
  110. package/dist/sqlite.d.ts +4 -0
  111. package/dist/sqlite.js +16 -0
  112. package/dist/sqlite.js.map +1 -0
  113. package/dist/updates/artifacts.d.ts +40 -0
  114. package/dist/updates/artifacts.js +299 -0
  115. package/dist/updates/artifacts.js.map +1 -0
  116. package/dist/updates/bootstrap.d.ts +48 -0
  117. package/dist/updates/bootstrap.js +384 -0
  118. package/dist/updates/bootstrap.js.map +1 -0
  119. package/dist/updates/controller.d.ts +66 -0
  120. package/dist/updates/controller.js +322 -0
  121. package/dist/updates/controller.js.map +1 -0
  122. package/dist/updates/isolated-command-lifetime.d.ts +1 -0
  123. package/dist/updates/isolated-command-lifetime.js +63 -0
  124. package/dist/updates/isolated-command-lifetime.js.map +1 -0
  125. package/dist/updates/store.d.ts +41 -0
  126. package/dist/updates/store.js +80 -0
  127. package/dist/updates/store.js.map +1 -0
  128. package/dist/updates/trust.d.ts +20 -0
  129. package/dist/updates/trust.js +138 -0
  130. package/dist/updates/trust.js.map +1 -0
  131. package/dist/user-ground-host.d.ts +4 -1
  132. package/dist/user-ground-host.js +16 -3
  133. package/dist/user-ground-host.js.map +1 -1
  134. package/dist/watching/host.js +4 -2
  135. package/dist/watching/host.js.map +1 -1
  136. package/dist/workspace-add-staging.js +2 -2
  137. package/dist/workspace-add-staging.js.map +1 -1
  138. package/examinations/fixtures/machine-control-plane.js +57 -0
  139. package/examinations/fixtures/private-wire-authority.js +1065 -0
  140. package/examinations/native-service.mjs +92 -0
  141. package/examinations/network-view.mjs +202 -0
  142. package/examinations/runtime-upgrade.native-test.mjs +198 -0
  143. package/npm-shrinkwrap.json +2097 -218
  144. package/package.json +95 -14
@@ -2,40 +2,25 @@ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
3
  import { homedir as osHomedir, hostname, platform, release } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
- import { Agents } from "@amalgm/agents";
6
- import { createRestServer as createAgentsRest } from "@amalgm/agents/http";
7
- import { createMcpServer as createAgentsMcp, createMcpTools as createAgentsMcpTools } from "@amalgm/agents/mcp";
8
- import { createMcpServer as createAppsMcp, createMcpTools as createAppsMcpTools } from "@amalgm/apps";
9
- import { createBrowser } from "@amalgm/browser";
10
- import { createBrowserMcpServer, createBrowserMcpTools } from "@amalgm/browser/mcp";
11
- import { createNodeAutomationProcessHost } from "@amalgm/automations";
12
- import { createAutomationMcpServer } from "@amalgm/automations/mcp";
13
- import { createShellChannelsCommands } from "@amalgm/channels/cli";
14
- import { createChannelsMcpServer } from "@amalgm/channels/mcp";
15
- import { createChatMcpServer } from "@amalgm/chat/mcp";
16
- import { buildRootManifest, joinMachine, mergeRecordAuth, resumeMachine, scopedAmalgmDir, userMachineDir, } from "@amalgm/core/identity";
17
- import { addFilesWorkspace, registerFilesWorkspace, } from "@amalgm/live/files";
18
- import { createMcpServer as createToolsMcp, } from "@amalgm/tools";
19
- import { createToolboxHttpServer } from "@amalgm/tools/http";
5
+ import { admitRuntimeFormat } from './updates/store.js';
6
+ import { serviceIdentity } from './service/registration.js';
7
+ import { buildRootManifest, approveMachineSetup, adoptMachineSetup, joinApprovedMachine, mergeRecordAuth, resumeMachine, resolveRuntimeBinding, runtimeBindingsMatch, runtimeBindingFromRecord, RUNTIME_IDENTITY_PATH, createRuntimeIdentity, runtimeRootSegments, resolveRuntimeEndpoint, scopedAmalgmDir, userMachineDir, } from "@amalgm/core/identity";
20
8
  import { atomicJson, atomicJsonSet, ensurePrivateDir, readJson, } from "./filesystem.js";
21
9
  import { emitFilesStage, } from "./files-observability.js";
22
10
  import { FilesStageRecorder, serializeStages } from "./files-trace.js";
23
- import { createShellChatComposition } from "./chat-composition.js";
24
11
  import { createShellAuthorization, loadInstallationDeviceKey, } from "./authorization/index.js";
25
12
  import { recoverMachineAuthorization, } from "./authorization/recovery-host.js";
26
13
  import { postRegistration } from "./authorization/registration-http.js";
27
14
  import { postSetupJson, setupOrigin } from "./authorization/setup-http.js";
28
15
  import { installBuiltinAgents } from "./builtin-agents.js";
16
+ import { InstallationOwnership } from "./installation/ownership.js";
17
+ import { runtimeAppOrigin, runtimeProxyOrigin, runtimeWireUrl } from "./installation/environment.js";
29
18
  import { forwardMount } from "./forward.js";
30
19
  import { createGateway } from "./gateway.js";
31
20
  import { localMount } from "./local-host.js";
32
- import { connectOfficialMcpServer } from "./official-mcp-host.js";
33
- import { createPlatformProductHost, } from "./platform-product-host.js";
34
21
  import { createPtyHost } from "./pty-host.js";
35
- import { callRuntimeCommand, inspectRuntimeCommands, publishRuntimeCommands, runtimeCommandConnection, unpublishRuntimeCommands, } from "./runtime-command-host.js";
22
+ import { callRuntimeCommand, assertRuntimeCanStart, inspectRuntimeCommands, publishRuntimeCommands, runtimeCommandConnection, unpublishRuntimeCommands, } from "./runtime-command-host.js";
36
23
  import { shellReleaseCatalog, shellReleaseVerifier } from "./release-catalog.js";
37
- import { platformToolDefinitions } from "./platform-tools.js";
38
- import { UserGroundHost, } from "./user-ground-host.js";
39
24
  /** The production effects around Core's machine-setup operation. It does not
40
25
  * decide ordering, readiness, or installation selection; those remain Core. */
41
26
  export class MachineHost {
@@ -44,19 +29,41 @@ export class MachineHost {
44
29
  amalgmRoot;
45
30
  deviceFile;
46
31
  ports;
32
+ binding;
33
+ ownership;
47
34
  selectedUserEmail;
48
35
  groundHost = null;
49
36
  running = new Set();
50
37
  stages;
51
38
  constructor(options) {
52
39
  setupOrigin(options.appUrl);
53
- this.options = options;
40
+ this.binding = resolveRuntimeBinding(options);
41
+ if (options.services && this.binding.runtimeLabel !== 'local')
42
+ throw new Error('Only a local profile may override runtime service selection.');
43
+ runtimeAppOrigin(this.binding, options.appUrl);
44
+ for (const [role, endpoint] of [['proxy', options.proxyUrl], ['wire', options.wireUrl],
45
+ ['automations', options.platformProducts?.automationsBaseUrl], ['channels', options.platformProducts?.channelsBaseUrl]]) {
46
+ if (endpoint)
47
+ resolveRuntimeEndpoint(this.binding, role, endpoint, options.appUrl);
48
+ }
49
+ this.options = { ...options, ...this.binding };
54
50
  this.stages = new FilesStageRecorder(options.onFilesStage);
55
51
  this.homedir = options.homedir ?? osHomedir();
56
- this.amalgmRoot = options.amalgmRoot ?? join(this.homedir, "amalgm");
52
+ this.amalgmRoot = resolve(options.amalgmRoot ?? join(this.homedir, ...runtimeRootSegments(this.binding)));
53
+ this.ownership = new InstallationOwnership({
54
+ home: this.homedir,
55
+ installationRoot: this.amalgmRoot,
56
+ binding: this.binding,
57
+ });
57
58
  this.deviceFile = join(this.amalgmRoot, ".amalgm", "device.json");
58
59
  this.selectedUserEmail = normalizeEmail(options.userEmail);
59
- this.ensureInstallationRoot();
60
+ try {
61
+ this.ownership.initialize(() => this.ensureInstallationRoot());
62
+ }
63
+ catch (error) {
64
+ this.ownership.close();
65
+ throw error;
66
+ }
60
67
  const deviceStore = {
61
68
  load: async () => readJson(this.deviceFile),
62
69
  save: async (device) => atomicJson(this.deviceFile, device),
@@ -96,17 +103,17 @@ export class MachineHost {
96
103
  : {}),
97
104
  ...(this.options.authorizationPlatform
98
105
  ? { platform: this.options.authorizationPlatform }
99
- : this.options.homedir ? { platform: "linux" } : {}),
106
+ : {}),
100
107
  });
101
108
  const response = await postRegistration(globalThis.fetch, this.options.appUrl, {
102
109
  ...body, authorization_key_thumbprint: signer.keyThumbprint,
103
110
  }, token, signer);
111
+ runtimeWireUrl(this.binding, this.options.appUrl, text(response.tunnel_url), this.options.wireUrl);
104
112
  const email = normalizeEmail(response.user_email);
105
113
  if (!email)
106
114
  throw Object.assign(new Error("registration did not return a user email"), {
107
115
  code: "missing_user_email",
108
116
  });
109
- this.selectedUserEmail = email;
110
117
  return response;
111
118
  },
112
119
  };
@@ -138,12 +145,14 @@ export class MachineHost {
138
145
  throw Object.assign(new Error("machine tunnel credentials are missing"), { code: "tunnel_credentials_missing" });
139
146
  }
140
147
  await this.groundHost?.close();
148
+ const { UserGroundHost } = await import("./user-ground-host.js");
141
149
  this.groundHost = new UserGroundHost({
142
150
  amalgmRoot: this.amalgmRoot,
143
151
  deviceId: identity.deviceId,
144
152
  tunnelUrl,
145
153
  tunnelToken,
146
154
  runtimeVersion: this.options.runtimeVersion,
155
+ reserveWritableRoot: (path) => { this.ownership.reserve(path, identity.userEmail); },
147
156
  openWatch: this.options.openWatch,
148
157
  onWatchHealth: this.options.onWatchHealth,
149
158
  onDetectScan: this.options.onDetectScan,
@@ -179,19 +188,79 @@ export class MachineHost {
179
188
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
180
189
  };
181
190
  }
182
- join(input) {
183
- return joinMachine({
191
+ async approveSetup(input) {
192
+ const approved = await approveMachineSetup({
184
193
  ...input,
185
194
  appUrl: this.options.appUrl,
186
195
  platform: `${platform()}-${release()}`,
187
196
  appVersion: this.options.runtimeVersion,
188
- runtimeLabel: this.options.runtimeLabel ?? "main",
197
+ ...this.binding,
189
198
  branch: this.options.branch ?? "main",
199
+ ...(this.binding.localSession ? { localSession: this.binding.localSession } : {}),
190
200
  }, this.ports);
201
+ const email = approved.grant.approved_by.email;
202
+ if (this.options.userEmail && normalizeEmail(this.options.userEmail) !== email) {
203
+ throw Object.assign(new Error('Setup was approved by a different account than the selected user.'), { code: 'approved_user_mismatch' });
204
+ }
205
+ this.selectedUserEmail = email;
206
+ return approved;
207
+ }
208
+ async join(input) {
209
+ await this.ownership.waitForRuntime('setup');
210
+ try {
211
+ const approved = await this.approveSetup(input);
212
+ this.acquireRuntimeOwnership();
213
+ this.assertRuntimeCanStart();
214
+ return await joinApprovedMachine(approved, this.ports);
215
+ }
216
+ finally {
217
+ this.ownership.releaseRuntime('setup');
218
+ }
219
+ }
220
+ /** Shared, finite UI setup. An existing owner is never disturbed by adoption. */
221
+ async setup(input) {
222
+ await this.ownership.waitForRuntime('setup');
223
+ let enrolling = false;
224
+ try {
225
+ const approved = await this.approveSetup(input);
226
+ const machine = await this.loadMachine();
227
+ if (machine?.ready) {
228
+ await adoptMachineSetup(approved, machine, this.ports);
229
+ return { disposition: 'adopted', userEmail: this.requireSelectedUser(),
230
+ userId: approved.grant.approved_by.user_id, computerId: text(machine.publicRecord.computer_id),
231
+ deviceId: approved.device.device_id };
232
+ }
233
+ this.acquireRuntimeOwnership();
234
+ this.assertRuntimeCanStart();
235
+ enrolling = true;
236
+ const ready = await joinApprovedMachine(approved, this.ports);
237
+ return { disposition: 'enrolled', userEmail: this.requireSelectedUser(),
238
+ userId: ready.userId, computerId: ready.computerId, deviceId: ready.deviceId };
239
+ }
240
+ finally {
241
+ try {
242
+ if (enrolling) {
243
+ await this.drainRuntime();
244
+ this.ownership.releaseRuntime(this.requireSelectedUser());
245
+ }
246
+ }
247
+ finally {
248
+ this.ownership.releaseRuntime('setup');
249
+ }
250
+ }
191
251
  }
192
252
  resume() {
193
253
  this.resolveUserForResume();
194
- return resumeMachine(this.ports);
254
+ this.acquireRuntimeOwnership();
255
+ this.assertRuntimeCanStart();
256
+ return resumeMachine(this.ports, { services: this.options.services });
257
+ }
258
+ assertRuntimeCanStart() {
259
+ const deviceId = text(readJson(this.deviceFile)?.device_id);
260
+ if (!deviceId)
261
+ return;
262
+ assertRuntimeCanStart({ userRoot: this.paths().userRoot, deviceId,
263
+ ...this.binding, userScope: this.requireSelectedUser() });
195
264
  }
196
265
  async recoverAuthorizationKey(input = {}) {
197
266
  this.resolveUserForResume();
@@ -204,7 +273,7 @@ export class MachineHost {
204
273
  machine,
205
274
  appUrl: this.options.appUrl,
206
275
  runtimeVersion: this.options.runtimeVersion,
207
- runtimeLabel: this.options.runtimeLabel ?? "main",
276
+ ...this.binding,
208
277
  branch: this.options.branch ?? "main",
209
278
  fallbackName: defaultMachineName(),
210
279
  fallbackPlatform: `${platform()}-${release()}`,
@@ -226,6 +295,7 @@ export class MachineHost {
226
295
  const deviceId = text(device?.device_id) || null;
227
296
  if (!deviceId) {
228
297
  return {
298
+ binding: this.binding,
229
299
  user,
230
300
  deviceId: null,
231
301
  registration: "absent",
@@ -240,6 +310,8 @@ export class MachineHost {
240
310
  }
241
311
  const paths = this.paths(user);
242
312
  const publicRecord = readJson(paths.publicRecord);
313
+ if (publicRecord)
314
+ this.assertRecordBinding(publicRecord);
243
315
  const secretAuth = readJson(paths.secretAuth);
244
316
  const installation = readJson(paths.installation);
245
317
  const storedStatus = readJson(paths.status);
@@ -251,11 +323,12 @@ export class MachineHost {
251
323
  ? await inspectRuntimeCommands({
252
324
  userRoot: paths.userRoot,
253
325
  deviceId,
254
- runtimeLabel: this.options.runtimeLabel ?? "main",
326
+ ...this.binding,
255
327
  userScope: user,
256
328
  })
257
329
  : { published: false, running: false, pid: null, port: null };
258
330
  return {
331
+ binding: this.binding,
259
332
  user,
260
333
  deviceId,
261
334
  registration,
@@ -282,6 +355,7 @@ export class MachineHost {
282
355
  primitive: "download", stage: "files-add", status: "started", workspaceId,
283
356
  });
284
357
  try {
358
+ const { addFilesWorkspace } = await import("@amalgm/live/files");
285
359
  const added = await addFilesWorkspace(workspaceId, destinationParent, this.groundHost.filesCommandPorts().add);
286
360
  const result = { ...added, watch: this.groundHost.addedWorkspaceEvidence(added.workspaceId) };
287
361
  emitFilesStage(this.stages.observe, {
@@ -318,7 +392,7 @@ export class MachineHost {
318
392
  primitive: "register", stage: "durable-declaration", status: "started", path: requestedPath,
319
393
  });
320
394
  try {
321
- const declared = await registerFilesWorkspace(requestedPath, this.groundHost.filesCommandPorts().register);
395
+ const declared = await (await import("@amalgm/live/files")).registerFilesWorkspace(requestedPath, this.groundHost.filesCommandPorts().register);
322
396
  emitFilesStage(this.stages.observe, {
323
397
  primitive: "register",
324
398
  stage: "durable-declaration",
@@ -383,6 +457,10 @@ export class MachineHost {
383
457
  return Object.freeze(adapter);
384
458
  }
385
459
  async close(options = {}) {
460
+ await this.drainRuntime(options);
461
+ this.ownership.close();
462
+ }
463
+ async drainRuntime(options = {}) {
386
464
  const runtimes = [...this.running];
387
465
  this.running.clear();
388
466
  await this.groundHost?.close(options);
@@ -398,11 +476,20 @@ export class MachineHost {
398
476
  nowIso: () => new Date().toISOString(),
399
477
  randomHex: (bytes) => randomBytes(bytes).toString("hex"),
400
478
  });
401
- if (!existing?.installationId)
402
- atomicJson(manifestFile, manifest);
479
+ const previousBinding = existing?.runtimeBinding;
480
+ if (previousBinding && !runtimeBindingsMatch(previousBinding, this.binding)) {
481
+ throw new Error('This installation root belongs to another runtime binding.');
482
+ }
483
+ if (existing?.installationId && !previousBinding && this.binding.runtimeLabel !== 'main') {
484
+ throw new Error('An unbound existing installation must be adopted explicitly; use a fresh root for preview/local.');
485
+ }
486
+ if (!existing?.installationId || !previousBinding) {
487
+ atomicJson(manifestFile, { ...manifest, runtimeBinding: this.binding });
488
+ }
403
489
  }
404
490
  async openChannelsCommands() {
405
491
  const access = runtimeCommandConnection(await this.runtimeCommandAddress());
492
+ const { createShellChannelsCommands } = await import("@amalgm/channels/cli");
406
493
  return createShellChannelsCommands(access);
407
494
  }
408
495
  async runtimeCommandAddress() {
@@ -419,7 +506,7 @@ export class MachineHost {
419
506
  return {
420
507
  userRoot: this.paths().userRoot,
421
508
  deviceId,
422
- runtimeLabel: this.options.runtimeLabel ?? "main",
509
+ ...this.binding,
423
510
  userScope: userEmail,
424
511
  };
425
512
  }
@@ -440,7 +527,7 @@ export class MachineHost {
440
527
  : {}),
441
528
  ...(this.options.authorizationPlatform
442
529
  ? { platform: this.options.authorizationPlatform }
443
- : this.options.homedir ? { platform: "linux" } : {}),
530
+ : {}),
444
531
  };
445
532
  }
446
533
  paths(email = this.requireSelectedUser()) {
@@ -494,6 +581,17 @@ export class MachineHost {
494
581
  });
495
582
  }
496
583
  }
584
+ assertRecordBinding(record) {
585
+ const stored = runtimeBindingFromRecord(record);
586
+ if (!runtimeBindingsMatch(stored, this.binding)
587
+ || (typeof record.app_url === 'string' && setupOrigin(record.app_url) !== setupOrigin(this.options.appUrl))) {
588
+ throw Object.assign(new Error('Stored machine belongs to another runtime binding or control plane.'), {
589
+ code: 'machine_binding_mismatch',
590
+ });
591
+ }
592
+ if (record.tunnel_url !== undefined)
593
+ runtimeWireUrl(this.binding, this.options.appUrl, text(record.tunnel_url), this.options.wireUrl);
594
+ }
497
595
  async loadMachine() {
498
596
  if (!this.selectedUserEmail)
499
597
  this.resolveUserForResume();
@@ -506,12 +604,15 @@ export class MachineHost {
506
604
  const status = readJson(paths.status);
507
605
  if (!publicRecord || !secretAuth || !installation || !status)
508
606
  return null;
607
+ this.assertRecordBinding(publicRecord);
509
608
  return { publicRecord, secretAuth, installation, ready: status.ready === true };
510
609
  }
511
610
  async saveMachine(machine) {
611
+ this.assertRecordBinding(machine.publicRecord);
512
612
  const recordEmail = normalizeEmail(machine.publicRecord.user_email);
513
613
  if (recordEmail)
514
614
  this.selectedUserEmail = recordEmail;
615
+ this.acquireRuntimeOwnership();
515
616
  const paths = this.paths();
516
617
  atomicJsonSet([
517
618
  [paths.publicRecord, machine.publicRecord],
@@ -520,6 +621,11 @@ export class MachineHost {
520
621
  [paths.status, { ready: machine.ready, updatedAt: new Date().toISOString() }],
521
622
  ]);
522
623
  }
624
+ acquireRuntimeOwnership() {
625
+ const user = this.requireSelectedUser();
626
+ this.ownership.acquireRuntime(user);
627
+ admitRuntimeFormat(serviceIdentity({ ...this.binding, user, home: this.homedir, amalgmRoot: this.amalgmRoot }).serviceDir);
628
+ }
523
629
  async startRuntime(machine) {
524
630
  const record = mergeRecordAuth(machine.publicRecord, machine.secretAuth) ?? {};
525
631
  const userId = text(record.user_id);
@@ -535,6 +641,9 @@ export class MachineHost {
535
641
  const userRoot = this.paths(userEmail).userRoot;
536
642
  const workspaceRoot = join(userRoot, "workspaces");
537
643
  const runtimeToken = randomBytes(32).toString("base64url");
644
+ const runtimeIdentity = createRuntimeIdentity({
645
+ binding: this.binding, userScope: userEmail, deviceId, instanceId: randomUUID(),
646
+ });
538
647
  const env = {
539
648
  ...process.env,
540
649
  AMALGM_ROOT: this.amalgmRoot,
@@ -545,7 +654,8 @@ export class MachineHost {
545
654
  AMALGM_RUNTIME_LABEL: this.options.runtimeLabel ?? "main",
546
655
  AMALGM_BRANCH: this.options.branch ?? "main",
547
656
  AMALGM_RUNTIME_TOKEN: runtimeToken,
548
- AMALGM_PROXY_URL: this.options.proxyUrl ?? "https://amalgm-api-proxy-v2.fly.dev",
657
+ AMALGM_LOCAL_SESSION: this.binding.localSession,
658
+ AMALGM_PROXY_URL: runtimeProxyOrigin(this.binding, this.options.appUrl, this.options.proxyUrl),
549
659
  AMALGM_WORKSPACES_DIR: workspaceRoot,
550
660
  };
551
661
  const pty = createPtyHost({
@@ -554,37 +664,48 @@ export class MachineHost {
554
664
  });
555
665
  const services = new Set(machine.installation.services);
556
666
  const serviceStateRoot = join(this.paths(userEmail).machineDir, "services");
557
- const agentsService = services.has("agents")
558
- ? new Agents({ stateDir: join(serviceStateRoot, "agents") })
667
+ const agentsSdk = services.has("agents") ? await import("@amalgm/agents") : null;
668
+ const agentsMcpSdk = agentsSdk ? await import("@amalgm/agents/mcp") : null;
669
+ const agentsHttpSdk = agentsSdk ? await import("@amalgm/agents/http") : null;
670
+ const toolsSdk = services.has("tools") ? await import("@amalgm/tools") : null;
671
+ const toolsHttpSdk = toolsSdk ? await import("@amalgm/tools/http") : null;
672
+ const includeApps = services.has("apps") || machine.installation.selection === "all";
673
+ const includeBrowser = services.has("browser") || machine.installation.selection === "all";
674
+ const appsSdk = toolsSdk && includeApps ? await import("@amalgm/apps") : null;
675
+ const browserSdk = toolsSdk && includeBrowser ? await import("@amalgm/browser") : null;
676
+ const browserMcpSdk = browserSdk ? await import("@amalgm/browser/mcp") : null;
677
+ const agentsService = agentsSdk
678
+ ? new agentsSdk.Agents({ stateDir: join(serviceStateRoot, "agents") })
559
679
  : null;
560
680
  if (agentsService)
561
681
  installBuiltinAgents(agentsService);
562
- const agentsMcp = agentsService ? createAgentsMcp({ agents: agentsService }) : null;
563
- const agentsRest = agentsService ? createAgentsRest({ agents: agentsService }) : null;
682
+ const agentsMcp = agentsService ? agentsMcpSdk.createMcpServer({ agents: agentsService }) : null;
683
+ const agentsRest = agentsService ? agentsHttpSdk.createRestServer({ agents: agentsService }) : null;
564
684
  const agentsAddress = agentsRest ? await agentsRest.listen(0, "127.0.0.1") : null;
565
685
  const platformTools = services.has("tools")
566
- ? await platformToolDefinitions({
567
- agents: agentsMcp ?? { tools: createAgentsMcpTools({}) },
568
- apps: { tools: createAppsMcpTools({}) },
569
- browser: { tools: createBrowserMcpTools({}) },
686
+ ? await (await import("./platform-tools.js")).platformToolDefinitions({
687
+ ...(agentsMcp ? { agents: agentsMcp } : {}),
688
+ ...(appsSdk ? { apps: { tools: appsSdk.createMcpTools({}) } } : {}),
689
+ ...(browserMcpSdk ? { browser: { tools: browserMcpSdk.createBrowserMcpTools({}) } } : {}),
690
+ chat: services.has("chat"),
570
691
  products: {
571
- automations: Boolean(this.options.platformProducts),
572
- channels: Boolean(this.options.platformProducts),
692
+ automations: Boolean(this.options.platformProducts?.automationsBaseUrl),
693
+ channels: Boolean(this.options.platformProducts?.channelsBaseUrl),
573
694
  },
574
695
  })
575
696
  : [];
576
697
  const toolsMcp = services.has("tools")
577
- ? createToolsMcp({
698
+ ? toolsSdk.createMcpServer({
578
699
  databaseFile: join(serviceStateRoot, "tools", "tools.db"),
579
700
  deploymentDir: join(userRoot, "toolbox"),
580
701
  systemTools: [...platformTools],
581
702
  })
582
703
  : null;
583
- const toolsManagement = toolsMcp ? createToolsMcp({
704
+ const toolsManagement = toolsMcp ? toolsSdk.createMcpServer({
584
705
  toolbox: toolsMcp.toolbox,
585
706
  mcp: { loadout: [], includeManagement: true },
586
707
  }) : null;
587
- const toolsHttp = toolsMcp ? createToolboxHttpServer({ toolbox: toolsMcp.toolbox }) : null;
708
+ const toolsHttp = toolsMcp ? toolsHttpSdk.createToolboxHttpServer({ toolbox: toolsMcp.toolbox }) : null;
588
709
  const toolsAddress = toolsHttp ? await toolsHttp.listen(0, "127.0.0.1") : null;
589
710
  if (services.has("chat") && (!agentsService || !toolsMcp)) {
590
711
  throw new Error("The Chat release requires Agents and Tools");
@@ -600,7 +721,7 @@ export class MachineHost {
600
721
  : {}),
601
722
  ...(this.options.authorizationPlatform
602
723
  ? { platform: this.options.authorizationPlatform }
603
- : this.options.homedir ? { platform: "linux" } : {}),
724
+ : {}),
604
725
  });
605
726
  let gateway = null;
606
727
  const gatewayPort = () => {
@@ -610,7 +731,7 @@ export class MachineHost {
610
731
  return address.port;
611
732
  };
612
733
  const chat = services.has("chat") && agentsService && toolsMcp
613
- ? await createShellChatComposition({
734
+ ? await (await import("./chat-composition.js")).createShellChatComposition({
614
735
  ownerUserId: userId,
615
736
  computerId,
616
737
  stateDir: join(serviceStateRoot, "chat"),
@@ -625,13 +746,15 @@ export class MachineHost {
625
746
  })
626
747
  : null;
627
748
  const products = this.options.platformProducts
628
- ? createPlatformProductHost({
749
+ ? await (await import("./platform-product-host.js")).createPlatformProductHost({
629
750
  ...this.options.platformProducts,
630
751
  authorization,
631
- processes: createNodeAutomationProcessHost({
632
- defaultCwd: this.homedir,
633
- environment: { ...process.env, HOME: this.homedir },
634
- }),
752
+ ...(this.options.platformProducts.automationsBaseUrl ? {
753
+ processes: (await import("@amalgm/automations")).createNodeAutomationProcessHost({
754
+ defaultCwd: this.homedir,
755
+ environment: { ...process.env, HOME: this.homedir },
756
+ }),
757
+ } : {}),
635
758
  ...(toolsMcp ? { toolbox: toolsMcp.toolbox } : {}),
636
759
  ...(chat ? { actions: { 'chat.chat_agent_run': chatAgentAction(chat.agentRunner) } } : {}),
637
760
  ...(chat ? { inboundExecutor: {
@@ -647,23 +770,27 @@ export class MachineHost {
647
770
  } } : {}),
648
771
  })
649
772
  : null;
650
- const automationsMcp = products
651
- ? await connectOfficialMcpServer(createAutomationMcpServer(products.automations), { name: 'amalgm-automations', version: '0.2.5' })
773
+ const automationsMcp = products?.automations
774
+ ? await (await import("./official-mcp-host.js")).connectOfficialMcpServer((await import("@amalgm/automations/mcp")).createAutomationMcpServer(products.automations), { name: 'amalgm-automations', version: '0.2.5' })
652
775
  : null;
653
- const channelsMcp = products
654
- ? await connectOfficialMcpServer(createChannelsMcpServer(products.channels), { name: 'amalgm-channels', version: '0.1.0' })
776
+ const channelsMcp = products?.channels
777
+ ? await (await import("./official-mcp-host.js")).connectOfficialMcpServer((await import("@amalgm/channels/mcp")).createChannelsMcpServer(products.channels), { name: 'amalgm-channels', version: '0.1.0' })
655
778
  : null;
656
- const chatMcp = chat ? createChatMcpServer(chat.chat, { agentRunner: chat.agentRunner }) : null;
779
+ const chatMcp = chat ? (await import("@amalgm/chat/mcp")).createChatMcpServer(chat.chat, { agentRunner: chat.agentRunner }) : null;
657
780
  let appsMcp = null;
658
781
  const selectedAppsMcp = () => {
659
- appsMcp ??= createAppsMcp({ stateDir: join(serviceStateRoot, "apps") });
782
+ if (!appsSdk)
783
+ throw new Error("Apps is not selected in this installation.");
784
+ appsMcp ??= appsSdk.createMcpServer({ stateDir: join(serviceStateRoot, "apps") });
660
785
  return appsMcp;
661
786
  };
662
787
  let browserMcp = null;
663
788
  let browserService = null;
664
789
  const selectedBrowserMcp = () => {
665
- browserService ??= createBrowser({ root: join(serviceStateRoot, "browser") });
666
- browserMcp ??= createBrowserMcpServer(browserService);
790
+ if (!browserSdk || !browserMcpSdk)
791
+ throw new Error("Browser is not selected in this installation.");
792
+ browserService ??= browserSdk.createBrowser({ root: join(serviceStateRoot, "browser") });
793
+ browserMcp ??= browserMcpSdk.createBrowserMcpServer(browserService);
667
794
  return browserMcp;
668
795
  };
669
796
  const closeSelectedProducts = async () => {
@@ -678,6 +805,7 @@ export class MachineHost {
678
805
  env,
679
806
  exemptPrefixes: ["/chat/egress/"],
680
807
  mounts: [
808
+ { prefix: RUNTIME_IDENTITY_PATH, handle: async () => runtimeIdentity },
681
809
  {
682
810
  prefix: "/entities/register",
683
811
  handle: (body) => this.journey(body, () => this.registerWorkspace(bodyText(body, "path"))),
@@ -696,11 +824,12 @@ export class MachineHost {
696
824
  protocol: "mcp-json-rpc",
697
825
  handle: (body) => toolsMcp.handle(body),
698
826
  }] : []),
699
- ...(toolsMcp ? [{
827
+ ...(appsSdk ? [{
700
828
  prefix: "/mcp/apps",
701
829
  protocol: "mcp-json-rpc",
702
830
  handle: (body) => selectedAppsMcp().handle(body),
703
- }, {
831
+ }] : []),
832
+ ...(browserSdk ? [{
704
833
  prefix: "/mcp/browser",
705
834
  protocol: "mcp-json-rpc",
706
835
  handle: (body) => selectedBrowserMcp().handle(body),
@@ -757,9 +886,10 @@ export class MachineHost {
757
886
  await this.groundHost.activateRuntimeTunnel(address.port, runtimeToken);
758
887
  products?.runner.start();
759
888
  commandPublication = {
889
+ instanceId: runtimeIdentity.instanceId,
760
890
  userRoot: this.paths(userEmail).userRoot,
761
891
  deviceId,
762
- runtimeLabel: this.options.runtimeLabel ?? "main",
892
+ ...this.binding,
763
893
  userScope: userEmail,
764
894
  port: address.port,
765
895
  token: runtimeToken,
@@ -779,19 +909,27 @@ export class MachineHost {
779
909
  if (commandPublication)
780
910
  unpublishRuntimeCommands(commandPublication);
781
911
  await this.groundHost?.stopRuntimeTunnel();
782
- pty.close();
783
- const closed = new Promise((resolve) => gateway.close(() => resolve()));
784
- gateway.closeAllConnections();
785
- await closed;
786
- await chat?.close();
787
- await products?.runner.close();
788
- await automationsMcp?.close();
789
- await channelsMcp?.close();
790
- await agentsRest?.close();
791
- await toolsHttp?.close();
792
- await closeSelectedProducts();
793
- agentsService?.close();
794
- toolsMcp?.toolbox.close();
912
+ const terminalsClosed = Promise.allSettled([pty.close()]);
913
+ try {
914
+ const closed = new Promise((resolve) => gateway.close(() => resolve()));
915
+ gateway.closeAllConnections();
916
+ await closed;
917
+ await chat?.close();
918
+ await products?.runner.close();
919
+ await automationsMcp?.close();
920
+ await channelsMcp?.close();
921
+ await agentsRest?.close();
922
+ await toolsHttp?.close();
923
+ await closeSelectedProducts();
924
+ agentsService?.close();
925
+ toolsMcp?.toolbox.close();
926
+ }
927
+ finally {
928
+ await terminalsClosed;
929
+ }
930
+ const [terminal] = await terminalsClosed;
931
+ if (terminal.status === "rejected")
932
+ throw terminal.reason;
795
933
  },
796
934
  };
797
935
  this.running.add(running);
@@ -801,21 +939,31 @@ export class MachineHost {
801
939
  if (commandPublication)
802
940
  unpublishRuntimeCommands(commandPublication);
803
941
  await this.groundHost?.stopRuntimeTunnel().catch(() => undefined);
804
- pty.close();
805
- if (gateway.listening) {
806
- const closed = new Promise((resolve) => gateway.close(() => resolve()));
807
- gateway.closeAllConnections();
808
- await closed;
942
+ const terminalsClosed = Promise.allSettled([pty.close()]);
943
+ for (const close of [
944
+ async () => {
945
+ if (!gateway.listening)
946
+ return;
947
+ const closed = new Promise((resolve) => gateway.close(() => resolve()));
948
+ gateway.closeAllConnections();
949
+ await closed;
950
+ },
951
+ () => chat?.close(),
952
+ () => products?.runner.close(),
953
+ () => automationsMcp?.close(),
954
+ () => channelsMcp?.close(),
955
+ () => agentsRest?.close(),
956
+ () => toolsHttp?.close(),
957
+ closeSelectedProducts,
958
+ () => agentsService?.close(),
959
+ () => toolsMcp?.toolbox.close(),
960
+ ]) {
961
+ try {
962
+ await close();
963
+ }
964
+ catch { /* Preserve the startup failure after attempting every owner. */ }
809
965
  }
810
- await chat?.close().catch(() => undefined);
811
- await products?.runner.close().catch(() => undefined);
812
- await automationsMcp?.close().catch(() => undefined);
813
- await channelsMcp?.close().catch(() => undefined);
814
- await agentsRest?.close().catch(() => undefined);
815
- await toolsHttp?.close().catch(() => undefined);
816
- await closeSelectedProducts().catch(() => undefined);
817
- agentsService?.close();
818
- toolsMcp?.toolbox.close();
966
+ await terminalsClosed;
819
967
  throw error;
820
968
  }
821
969
  }