@amalgm/shell 0.1.162 → 0.1.163

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 (89) hide show
  1. package/PURPOSE.md +98 -14
  2. package/README.md +16 -0
  3. package/dist/authorization/recovery-host.d.ts +1 -0
  4. package/dist/authorization/recovery-host.js +2 -3
  5. package/dist/authorization/recovery-host.js.map +1 -1
  6. package/dist/cli-arguments.d.ts +8 -0
  7. package/dist/cli-arguments.js +47 -0
  8. package/dist/cli-arguments.js.map +1 -0
  9. package/dist/cli.d.ts +6 -4
  10. package/dist/cli.js +85 -73
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compose.js +4 -3
  13. package/dist/compose.js.map +1 -1
  14. package/dist/development/profile.d.ts +27 -0
  15. package/dist/development/profile.js +142 -0
  16. package/dist/development/profile.js.map +1 -0
  17. package/dist/files-register-host.d.ts +1 -0
  18. package/dist/files-register-host.js +1 -0
  19. package/dist/files-register-host.js.map +1 -1
  20. package/dist/installation/environment.d.ts +30 -0
  21. package/dist/installation/environment.js +28 -0
  22. package/dist/installation/environment.js.map +1 -0
  23. package/dist/installation/lease.d.ts +6 -0
  24. package/dist/installation/lease.js +28 -0
  25. package/dist/installation/lease.js.map +1 -0
  26. package/dist/installation/ownership.d.ts +30 -0
  27. package/dist/installation/ownership.js +156 -0
  28. package/dist/installation/ownership.js.map +1 -0
  29. package/dist/installation/selection.d.ts +8 -0
  30. package/dist/installation/selection.js +29 -0
  31. package/dist/installation/selection.js.map +1 -0
  32. package/dist/machine-host.d.ts +26 -5
  33. package/dist/machine-host.js +192 -69
  34. package/dist/machine-host.js.map +1 -1
  35. package/dist/main.js +56 -2
  36. package/dist/main.js.map +1 -1
  37. package/dist/platform-product-host.d.ts +9 -9
  38. package/dist/platform-product-host.js +17 -17
  39. package/dist/platform-product-host.js.map +1 -1
  40. package/dist/platform-tools.d.ts +4 -3
  41. package/dist/platform-tools.js +34 -37
  42. package/dist/platform-tools.js.map +1 -1
  43. package/dist/process-lifecycle.d.ts +8 -0
  44. package/dist/process-lifecycle.js +32 -0
  45. package/dist/process-lifecycle.js.map +1 -0
  46. package/dist/runtime-command-host.d.ts +7 -0
  47. package/dist/runtime-command-host.js +49 -13
  48. package/dist/runtime-command-host.js.map +1 -1
  49. package/dist/service/adapter.d.ts +36 -0
  50. package/dist/service/adapter.js +24 -0
  51. package/dist/service/adapter.js.map +1 -0
  52. package/dist/service/cli.d.ts +8 -0
  53. package/dist/service/cli.js +196 -0
  54. package/dist/service/cli.js.map +1 -0
  55. package/dist/service/control.d.ts +29 -0
  56. package/dist/service/control.js +68 -0
  57. package/dist/service/control.js.map +1 -0
  58. package/dist/service/index.d.ts +9 -0
  59. package/dist/service/index.js +10 -0
  60. package/dist/service/index.js.map +1 -0
  61. package/dist/service/keepalive.d.ts +11 -0
  62. package/dist/service/keepalive.js +180 -0
  63. package/dist/service/keepalive.js.map +1 -0
  64. package/dist/service/launchd.d.ts +9 -0
  65. package/dist/service/launchd.js +105 -0
  66. package/dist/service/launchd.js.map +1 -0
  67. package/dist/service/owner.d.ts +4 -0
  68. package/dist/service/owner.js +14 -0
  69. package/dist/service/owner.js.map +1 -0
  70. package/dist/service/portable.d.ts +10 -0
  71. package/dist/service/portable.js +32 -0
  72. package/dist/service/portable.js.map +1 -0
  73. package/dist/service/registration.d.ts +82 -0
  74. package/dist/service/registration.js +114 -0
  75. package/dist/service/registration.js.map +1 -0
  76. package/dist/service/systemd.d.ts +9 -0
  77. package/dist/service/systemd.js +94 -0
  78. package/dist/service/systemd.js.map +1 -0
  79. package/dist/service/windows-task.d.ts +16 -0
  80. package/dist/service/windows-task.js +82 -0
  81. package/dist/service/windows-task.js.map +1 -0
  82. package/dist/service/windows.d.ts +2 -0
  83. package/dist/service/windows.js +65 -0
  84. package/dist/service/windows.js.map +1 -0
  85. package/dist/user-ground-host.d.ts +3 -0
  86. package/dist/user-ground-host.js +13 -0
  87. package/dist/user-ground-host.js.map +1 -1
  88. package/npm-shrinkwrap.json +49 -83
  89. package/package.json +9 -9
@@ -0,0 +1,29 @@
1
+ import { resolveRuntimeBinding } from '@amalgm/core/identity';
2
+ /** Public command prefixes select the environment, never ambient env vars. */
3
+ export function selectRuntimeInvocation(args) {
4
+ if (args[0] === 'preview')
5
+ return { runtimeLabel: 'preview', args: args.slice(1) };
6
+ if (args[0] === 'dev')
7
+ return { runtimeLabel: 'local', args: args.slice(1) };
8
+ return { runtimeLabel: 'main', args };
9
+ }
10
+ export function resolveInvocationBinding(runtimeLabel, parsed) {
11
+ const explicitLabel = stringOption(parsed, 'runtime-label');
12
+ if (explicitLabel !== undefined && explicitLabel !== runtimeLabel) {
13
+ throw new Error('The runtime label conflicts with the command prefix. Use amalgm preview … or amalgm dev … to select that environment.');
14
+ }
15
+ return resolveRuntimeBinding({
16
+ runtimeLabel,
17
+ branch: stringOption(parsed, 'branch'),
18
+ localSession: stringOption(parsed, 'local-session'),
19
+ });
20
+ }
21
+ function stringOption(parsed, name) {
22
+ const value = parsed.options[name];
23
+ if (value === undefined)
24
+ return undefined;
25
+ if (typeof value !== 'string' || value.length === 0)
26
+ throw new Error(`--${name} requires a value.`);
27
+ return value;
28
+ }
29
+ //# sourceMappingURL=selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.js","sourceRoot":"","sources":["../../src/installation/selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAA0C,MAAM,uBAAuB,CAAC;AAGtG,8EAA8E;AAC9E,MAAM,UAAU,uBAAuB,CAAC,IAAuB;IAC7D,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK;QAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,YAA0B,EAAE,MAAuB;IAC1F,MAAM,aAAa,GAAG,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC5D,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,KAAK,YAAY,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uHAAuH,CAAC,CAAC;IAC3I,CAAC;IACD,OAAO,qBAAqB,CAAC;QAC3B,YAAY;QACZ,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC;QACtC,YAAY,EAAE,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC;KACpD,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,MAAuB,EAAE,IAAY;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,KAAK,IAAI,oBAAoB,CAAC,CAAC;IACpG,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1,14 +1,14 @@
1
1
  import type { ChannelsCommands } from "@amalgm/channels";
2
2
  import type { ChatRuntimePort } from "@amalgm/chat";
3
- import { type JoinMachineInput, type MachineSetupPorts, type ReadyMachine } from "@amalgm/core/identity";
4
- import { type FilesAddResult } from "@amalgm/live/files";
3
+ import { type JoinMachineInput, type MachineSetupPorts, type ReadyMachine, type RuntimeBinding } from "@amalgm/core/identity";
4
+ import type { FilesAddResult } from "@amalgm/live/files";
5
5
  import { type FilesStageObserver } from "./files-observability.js";
6
6
  import type { EntityApplyStageEvidence } from "./entity-apply-host.js";
7
7
  import { type DeviceKeyStore } from "./authorization/index.js";
8
8
  import { type AuthorizationKeyRecoveryResult } from "./authorization/recovery-host.js";
9
- import { type PlatformProductHostOptions } from "./platform-product-host.js";
9
+ import type { PlatformProductHostOptions } from "./platform-product-host.js";
10
10
  import { type RuntimeCommandConnection } from "./runtime-command-host.js";
11
- import { UserGroundHost, type DetectScanEvidence, type UserGroundCloseOptions, type WorkspaceAddStage } from "./user-ground-host.js";
11
+ import type { UserGroundHost, DetectScanEvidence, UserGroundCloseOptions, WorkspaceAddStage } from "./user-ground-host.js";
12
12
  import type { OpenNativeWatch, WatchCoverageEvidence, WatchHealth } from "./watching/index.js";
13
13
  export interface MachineHostOptions {
14
14
  readonly homedir?: string;
@@ -19,7 +19,11 @@ export interface MachineHostOptions {
19
19
  readonly runtimeVersion: string;
20
20
  readonly runtimeLabel?: string;
21
21
  readonly branch?: string;
22
+ readonly localSession?: string;
23
+ /** Explicit local availability; managed installations retain their release selection. */
24
+ readonly services?: readonly string[];
22
25
  readonly proxyUrl?: string;
26
+ readonly wireUrl?: string;
23
27
  /** Hosted product endpoints. Omit in isolated hosts that do not compose them. */
24
28
  readonly platformProducts?: Omit<PlatformProductHostOptions, 'authorization'>;
25
29
  /** Test/adapter seam; production defaults to the OS-backed device key store. */
@@ -63,6 +67,7 @@ interface MachinePaths {
63
67
  readonly status: string;
64
68
  }
65
69
  export interface MachineStatus {
70
+ readonly binding: RuntimeBinding;
66
71
  readonly user: string;
67
72
  readonly deviceId: string | null;
68
73
  readonly registration: "absent" | "incomplete" | "ready";
@@ -74,6 +79,14 @@ export interface MachineStatus {
74
79
  readonly machineDir: string | null;
75
80
  readonly database: string | null;
76
81
  }
82
+ type HostJoinInput = Omit<JoinMachineInput, 'appUrl' | 'platform' | 'appVersion' | 'runtimeLabel' | 'branch' | 'localSession'>;
83
+ export interface MachineSetupResult {
84
+ readonly disposition: 'enrolled' | 'adopted';
85
+ readonly userEmail: string;
86
+ readonly userId: string;
87
+ readonly computerId: string;
88
+ readonly deviceId: string;
89
+ }
77
90
  /** The production effects around Core's machine-setup operation. It does not
78
91
  * decide ordering, readiness, or installation selection; those remain Core. */
79
92
  export declare class MachineHost {
@@ -82,13 +95,19 @@ export declare class MachineHost {
82
95
  readonly amalgmRoot: string;
83
96
  readonly deviceFile: string;
84
97
  readonly ports: MachineSetupPorts;
98
+ readonly binding: RuntimeBinding;
99
+ private readonly ownership;
85
100
  private selectedUserEmail;
86
101
  private groundHost;
87
102
  private running;
88
103
  private readonly stages;
89
104
  constructor(options: MachineHostOptions);
90
- join(input: Omit<JoinMachineInput, "appUrl" | "platform" | "appVersion" | "runtimeLabel" | "branch">): Promise<ReadyMachine>;
105
+ private approveSetup;
106
+ join(input: HostJoinInput): Promise<ReadyMachine>;
107
+ /** Shared, finite UI setup. An existing owner is never disturbed by adoption. */
108
+ setup(input: HostJoinInput): Promise<MachineSetupResult>;
91
109
  resume(): Promise<ReadyMachine>;
110
+ assertRuntimeCanStart(): void;
92
111
  recoverAuthorizationKey(input?: Readonly<{
93
112
  setupCode?: string;
94
113
  }>): Promise<AuthorizationKeyRecoveryResult>;
@@ -116,6 +135,7 @@ export declare class MachineHost {
116
135
  * loopback MCP route. Shell alone reads the local admission capability. */
117
136
  channelsCommands(): ChannelsCommands;
118
137
  close(options?: UserGroundCloseOptions): Promise<void>;
138
+ private drainRuntime;
119
139
  private ensureInstallationRoot;
120
140
  private openChannelsCommands;
121
141
  private runtimeCommandAddress;
@@ -124,6 +144,7 @@ export declare class MachineHost {
124
144
  private paths;
125
145
  private requireSelectedUser;
126
146
  private resolveUserForResume;
147
+ private assertRecordBinding;
127
148
  private loadMachine;
128
149
  private saveMachine;
129
150
  private startRuntime;
@@ -2,40 +2,23 @@ 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 { buildRootManifest, approveMachineSetup, adoptMachineSetup, joinApprovedMachine, mergeRecordAuth, resumeMachine, resolveRuntimeBinding, runtimeBindingsMatch, runtimeBindingFromRecord, RUNTIME_IDENTITY_PATH, createRuntimeIdentity, runtimeRootSegments, resolveRuntimeEndpoint, scopedAmalgmDir, userMachineDir, } from "@amalgm/core/identity";
20
6
  import { atomicJson, atomicJsonSet, ensurePrivateDir, readJson, } from "./filesystem.js";
21
7
  import { emitFilesStage, } from "./files-observability.js";
22
8
  import { FilesStageRecorder, serializeStages } from "./files-trace.js";
23
- import { createShellChatComposition } from "./chat-composition.js";
24
9
  import { createShellAuthorization, loadInstallationDeviceKey, } from "./authorization/index.js";
25
10
  import { recoverMachineAuthorization, } from "./authorization/recovery-host.js";
26
11
  import { postRegistration } from "./authorization/registration-http.js";
27
12
  import { postSetupJson, setupOrigin } from "./authorization/setup-http.js";
28
13
  import { installBuiltinAgents } from "./builtin-agents.js";
14
+ import { InstallationOwnership } from "./installation/ownership.js";
15
+ import { runtimeAppOrigin, runtimeProxyOrigin, runtimeWireUrl } from "./installation/environment.js";
29
16
  import { forwardMount } from "./forward.js";
30
17
  import { createGateway } from "./gateway.js";
31
18
  import { localMount } from "./local-host.js";
32
- import { connectOfficialMcpServer } from "./official-mcp-host.js";
33
- import { createPlatformProductHost, } from "./platform-product-host.js";
34
19
  import { createPtyHost } from "./pty-host.js";
35
- import { callRuntimeCommand, inspectRuntimeCommands, publishRuntimeCommands, runtimeCommandConnection, unpublishRuntimeCommands, } from "./runtime-command-host.js";
20
+ import { callRuntimeCommand, assertRuntimeCanStart, inspectRuntimeCommands, publishRuntimeCommands, runtimeCommandConnection, unpublishRuntimeCommands, } from "./runtime-command-host.js";
36
21
  import { shellReleaseCatalog, shellReleaseVerifier } from "./release-catalog.js";
37
- import { platformToolDefinitions } from "./platform-tools.js";
38
- import { UserGroundHost, } from "./user-ground-host.js";
39
22
  /** The production effects around Core's machine-setup operation. It does not
40
23
  * decide ordering, readiness, or installation selection; those remain Core. */
41
24
  export class MachineHost {
@@ -44,19 +27,41 @@ export class MachineHost {
44
27
  amalgmRoot;
45
28
  deviceFile;
46
29
  ports;
30
+ binding;
31
+ ownership;
47
32
  selectedUserEmail;
48
33
  groundHost = null;
49
34
  running = new Set();
50
35
  stages;
51
36
  constructor(options) {
52
37
  setupOrigin(options.appUrl);
53
- this.options = options;
38
+ this.binding = resolveRuntimeBinding(options);
39
+ if (options.services && this.binding.runtimeLabel !== 'local')
40
+ throw new Error('Only a local profile may override runtime service selection.');
41
+ runtimeAppOrigin(this.binding, options.appUrl);
42
+ for (const [role, endpoint] of [['proxy', options.proxyUrl], ['wire', options.wireUrl],
43
+ ['automations', options.platformProducts?.automationsBaseUrl], ['channels', options.platformProducts?.channelsBaseUrl]]) {
44
+ if (endpoint)
45
+ resolveRuntimeEndpoint(this.binding, role, endpoint, options.appUrl);
46
+ }
47
+ this.options = { ...options, ...this.binding };
54
48
  this.stages = new FilesStageRecorder(options.onFilesStage);
55
49
  this.homedir = options.homedir ?? osHomedir();
56
- this.amalgmRoot = options.amalgmRoot ?? join(this.homedir, "amalgm");
50
+ this.amalgmRoot = resolve(options.amalgmRoot ?? join(this.homedir, ...runtimeRootSegments(this.binding)));
51
+ this.ownership = new InstallationOwnership({
52
+ home: this.homedir,
53
+ installationRoot: this.amalgmRoot,
54
+ binding: this.binding,
55
+ });
57
56
  this.deviceFile = join(this.amalgmRoot, ".amalgm", "device.json");
58
57
  this.selectedUserEmail = normalizeEmail(options.userEmail);
59
- this.ensureInstallationRoot();
58
+ try {
59
+ this.ownership.initialize(() => this.ensureInstallationRoot());
60
+ }
61
+ catch (error) {
62
+ this.ownership.close();
63
+ throw error;
64
+ }
60
65
  const deviceStore = {
61
66
  load: async () => readJson(this.deviceFile),
62
67
  save: async (device) => atomicJson(this.deviceFile, device),
@@ -96,17 +101,17 @@ export class MachineHost {
96
101
  : {}),
97
102
  ...(this.options.authorizationPlatform
98
103
  ? { platform: this.options.authorizationPlatform }
99
- : this.options.homedir ? { platform: "linux" } : {}),
104
+ : {}),
100
105
  });
101
106
  const response = await postRegistration(globalThis.fetch, this.options.appUrl, {
102
107
  ...body, authorization_key_thumbprint: signer.keyThumbprint,
103
108
  }, token, signer);
109
+ runtimeWireUrl(this.binding, this.options.appUrl, text(response.tunnel_url), this.options.wireUrl);
104
110
  const email = normalizeEmail(response.user_email);
105
111
  if (!email)
106
112
  throw Object.assign(new Error("registration did not return a user email"), {
107
113
  code: "missing_user_email",
108
114
  });
109
- this.selectedUserEmail = email;
110
115
  return response;
111
116
  },
112
117
  };
@@ -138,12 +143,14 @@ export class MachineHost {
138
143
  throw Object.assign(new Error("machine tunnel credentials are missing"), { code: "tunnel_credentials_missing" });
139
144
  }
140
145
  await this.groundHost?.close();
146
+ const { UserGroundHost } = await import("./user-ground-host.js");
141
147
  this.groundHost = new UserGroundHost({
142
148
  amalgmRoot: this.amalgmRoot,
143
149
  deviceId: identity.deviceId,
144
150
  tunnelUrl,
145
151
  tunnelToken,
146
152
  runtimeVersion: this.options.runtimeVersion,
153
+ reserveWritableRoot: (path) => { this.ownership.reserve(path, identity.userEmail); },
147
154
  openWatch: this.options.openWatch,
148
155
  onWatchHealth: this.options.onWatchHealth,
149
156
  onDetectScan: this.options.onDetectScan,
@@ -179,19 +186,79 @@ export class MachineHost {
179
186
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
180
187
  };
181
188
  }
182
- join(input) {
183
- return joinMachine({
189
+ async approveSetup(input) {
190
+ const approved = await approveMachineSetup({
184
191
  ...input,
185
192
  appUrl: this.options.appUrl,
186
193
  platform: `${platform()}-${release()}`,
187
194
  appVersion: this.options.runtimeVersion,
188
- runtimeLabel: this.options.runtimeLabel ?? "main",
195
+ ...this.binding,
189
196
  branch: this.options.branch ?? "main",
197
+ ...(this.binding.localSession ? { localSession: this.binding.localSession } : {}),
190
198
  }, this.ports);
199
+ const email = approved.grant.approved_by.email;
200
+ if (this.options.userEmail && normalizeEmail(this.options.userEmail) !== email) {
201
+ throw Object.assign(new Error('Setup was approved by a different account than the selected user.'), { code: 'approved_user_mismatch' });
202
+ }
203
+ this.selectedUserEmail = email;
204
+ return approved;
205
+ }
206
+ async join(input) {
207
+ await this.ownership.waitForRuntime('setup');
208
+ try {
209
+ const approved = await this.approveSetup(input);
210
+ this.ownership.acquireRuntime(this.requireSelectedUser());
211
+ this.assertRuntimeCanStart();
212
+ return await joinApprovedMachine(approved, this.ports);
213
+ }
214
+ finally {
215
+ this.ownership.releaseRuntime('setup');
216
+ }
217
+ }
218
+ /** Shared, finite UI setup. An existing owner is never disturbed by adoption. */
219
+ async setup(input) {
220
+ await this.ownership.waitForRuntime('setup');
221
+ let enrolling = false;
222
+ try {
223
+ const approved = await this.approveSetup(input);
224
+ const machine = await this.loadMachine();
225
+ if (machine?.ready) {
226
+ await adoptMachineSetup(approved, machine, this.ports);
227
+ return { disposition: 'adopted', userEmail: this.requireSelectedUser(),
228
+ userId: approved.grant.approved_by.user_id, computerId: text(machine.publicRecord.computer_id),
229
+ deviceId: approved.device.device_id };
230
+ }
231
+ this.ownership.acquireRuntime(this.requireSelectedUser());
232
+ this.assertRuntimeCanStart();
233
+ enrolling = true;
234
+ const ready = await joinApprovedMachine(approved, this.ports);
235
+ return { disposition: 'enrolled', userEmail: this.requireSelectedUser(),
236
+ userId: ready.userId, computerId: ready.computerId, deviceId: ready.deviceId };
237
+ }
238
+ finally {
239
+ try {
240
+ if (enrolling) {
241
+ await this.drainRuntime();
242
+ this.ownership.releaseRuntime(this.requireSelectedUser());
243
+ }
244
+ }
245
+ finally {
246
+ this.ownership.releaseRuntime('setup');
247
+ }
248
+ }
191
249
  }
192
250
  resume() {
193
251
  this.resolveUserForResume();
194
- return resumeMachine(this.ports);
252
+ this.ownership.acquireRuntime(this.requireSelectedUser());
253
+ this.assertRuntimeCanStart();
254
+ return resumeMachine(this.ports, { services: this.options.services });
255
+ }
256
+ assertRuntimeCanStart() {
257
+ const deviceId = text(readJson(this.deviceFile)?.device_id);
258
+ if (!deviceId)
259
+ return;
260
+ assertRuntimeCanStart({ userRoot: this.paths().userRoot, deviceId,
261
+ ...this.binding, userScope: this.requireSelectedUser() });
195
262
  }
196
263
  async recoverAuthorizationKey(input = {}) {
197
264
  this.resolveUserForResume();
@@ -204,7 +271,7 @@ export class MachineHost {
204
271
  machine,
205
272
  appUrl: this.options.appUrl,
206
273
  runtimeVersion: this.options.runtimeVersion,
207
- runtimeLabel: this.options.runtimeLabel ?? "main",
274
+ ...this.binding,
208
275
  branch: this.options.branch ?? "main",
209
276
  fallbackName: defaultMachineName(),
210
277
  fallbackPlatform: `${platform()}-${release()}`,
@@ -226,6 +293,7 @@ export class MachineHost {
226
293
  const deviceId = text(device?.device_id) || null;
227
294
  if (!deviceId) {
228
295
  return {
296
+ binding: this.binding,
229
297
  user,
230
298
  deviceId: null,
231
299
  registration: "absent",
@@ -240,6 +308,8 @@ export class MachineHost {
240
308
  }
241
309
  const paths = this.paths(user);
242
310
  const publicRecord = readJson(paths.publicRecord);
311
+ if (publicRecord)
312
+ this.assertRecordBinding(publicRecord);
243
313
  const secretAuth = readJson(paths.secretAuth);
244
314
  const installation = readJson(paths.installation);
245
315
  const storedStatus = readJson(paths.status);
@@ -251,11 +321,12 @@ export class MachineHost {
251
321
  ? await inspectRuntimeCommands({
252
322
  userRoot: paths.userRoot,
253
323
  deviceId,
254
- runtimeLabel: this.options.runtimeLabel ?? "main",
324
+ ...this.binding,
255
325
  userScope: user,
256
326
  })
257
327
  : { published: false, running: false, pid: null, port: null };
258
328
  return {
329
+ binding: this.binding,
259
330
  user,
260
331
  deviceId,
261
332
  registration,
@@ -282,6 +353,7 @@ export class MachineHost {
282
353
  primitive: "download", stage: "files-add", status: "started", workspaceId,
283
354
  });
284
355
  try {
356
+ const { addFilesWorkspace } = await import("@amalgm/live/files");
285
357
  const added = await addFilesWorkspace(workspaceId, destinationParent, this.groundHost.filesCommandPorts().add);
286
358
  const result = { ...added, watch: this.groundHost.addedWorkspaceEvidence(added.workspaceId) };
287
359
  emitFilesStage(this.stages.observe, {
@@ -318,7 +390,7 @@ export class MachineHost {
318
390
  primitive: "register", stage: "durable-declaration", status: "started", path: requestedPath,
319
391
  });
320
392
  try {
321
- const declared = await registerFilesWorkspace(requestedPath, this.groundHost.filesCommandPorts().register);
393
+ const declared = await (await import("@amalgm/live/files")).registerFilesWorkspace(requestedPath, this.groundHost.filesCommandPorts().register);
322
394
  emitFilesStage(this.stages.observe, {
323
395
  primitive: "register",
324
396
  stage: "durable-declaration",
@@ -383,6 +455,10 @@ export class MachineHost {
383
455
  return Object.freeze(adapter);
384
456
  }
385
457
  async close(options = {}) {
458
+ await this.drainRuntime(options);
459
+ this.ownership.close();
460
+ }
461
+ async drainRuntime(options = {}) {
386
462
  const runtimes = [...this.running];
387
463
  this.running.clear();
388
464
  await this.groundHost?.close(options);
@@ -398,11 +474,20 @@ export class MachineHost {
398
474
  nowIso: () => new Date().toISOString(),
399
475
  randomHex: (bytes) => randomBytes(bytes).toString("hex"),
400
476
  });
401
- if (!existing?.installationId)
402
- atomicJson(manifestFile, manifest);
477
+ const previousBinding = existing?.runtimeBinding;
478
+ if (previousBinding && !runtimeBindingsMatch(previousBinding, this.binding)) {
479
+ throw new Error('This installation root belongs to another runtime binding.');
480
+ }
481
+ if (existing?.installationId && !previousBinding && this.binding.runtimeLabel !== 'main') {
482
+ throw new Error('An unbound existing installation must be adopted explicitly; use a fresh root for preview/local.');
483
+ }
484
+ if (!existing?.installationId || !previousBinding) {
485
+ atomicJson(manifestFile, { ...manifest, runtimeBinding: this.binding });
486
+ }
403
487
  }
404
488
  async openChannelsCommands() {
405
489
  const access = runtimeCommandConnection(await this.runtimeCommandAddress());
490
+ const { createShellChannelsCommands } = await import("@amalgm/channels/cli");
406
491
  return createShellChannelsCommands(access);
407
492
  }
408
493
  async runtimeCommandAddress() {
@@ -419,7 +504,7 @@ export class MachineHost {
419
504
  return {
420
505
  userRoot: this.paths().userRoot,
421
506
  deviceId,
422
- runtimeLabel: this.options.runtimeLabel ?? "main",
507
+ ...this.binding,
423
508
  userScope: userEmail,
424
509
  };
425
510
  }
@@ -440,7 +525,7 @@ export class MachineHost {
440
525
  : {}),
441
526
  ...(this.options.authorizationPlatform
442
527
  ? { platform: this.options.authorizationPlatform }
443
- : this.options.homedir ? { platform: "linux" } : {}),
528
+ : {}),
444
529
  };
445
530
  }
446
531
  paths(email = this.requireSelectedUser()) {
@@ -494,6 +579,17 @@ export class MachineHost {
494
579
  });
495
580
  }
496
581
  }
582
+ assertRecordBinding(record) {
583
+ const stored = runtimeBindingFromRecord(record);
584
+ if (!runtimeBindingsMatch(stored, this.binding)
585
+ || (typeof record.app_url === 'string' && setupOrigin(record.app_url) !== setupOrigin(this.options.appUrl))) {
586
+ throw Object.assign(new Error('Stored machine belongs to another runtime binding or control plane.'), {
587
+ code: 'machine_binding_mismatch',
588
+ });
589
+ }
590
+ if (record.tunnel_url !== undefined)
591
+ runtimeWireUrl(this.binding, this.options.appUrl, text(record.tunnel_url), this.options.wireUrl);
592
+ }
497
593
  async loadMachine() {
498
594
  if (!this.selectedUserEmail)
499
595
  this.resolveUserForResume();
@@ -506,12 +602,15 @@ export class MachineHost {
506
602
  const status = readJson(paths.status);
507
603
  if (!publicRecord || !secretAuth || !installation || !status)
508
604
  return null;
605
+ this.assertRecordBinding(publicRecord);
509
606
  return { publicRecord, secretAuth, installation, ready: status.ready === true };
510
607
  }
511
608
  async saveMachine(machine) {
609
+ this.assertRecordBinding(machine.publicRecord);
512
610
  const recordEmail = normalizeEmail(machine.publicRecord.user_email);
513
611
  if (recordEmail)
514
612
  this.selectedUserEmail = recordEmail;
613
+ this.ownership.acquireRuntime(this.requireSelectedUser());
515
614
  const paths = this.paths();
516
615
  atomicJsonSet([
517
616
  [paths.publicRecord, machine.publicRecord],
@@ -535,6 +634,9 @@ export class MachineHost {
535
634
  const userRoot = this.paths(userEmail).userRoot;
536
635
  const workspaceRoot = join(userRoot, "workspaces");
537
636
  const runtimeToken = randomBytes(32).toString("base64url");
637
+ const runtimeIdentity = createRuntimeIdentity({
638
+ binding: this.binding, userScope: userEmail, deviceId, instanceId: randomUUID(),
639
+ });
538
640
  const env = {
539
641
  ...process.env,
540
642
  AMALGM_ROOT: this.amalgmRoot,
@@ -545,7 +647,8 @@ export class MachineHost {
545
647
  AMALGM_RUNTIME_LABEL: this.options.runtimeLabel ?? "main",
546
648
  AMALGM_BRANCH: this.options.branch ?? "main",
547
649
  AMALGM_RUNTIME_TOKEN: runtimeToken,
548
- AMALGM_PROXY_URL: this.options.proxyUrl ?? "https://amalgm-api-proxy-v2.fly.dev",
650
+ AMALGM_LOCAL_SESSION: this.binding.localSession,
651
+ AMALGM_PROXY_URL: runtimeProxyOrigin(this.binding, this.options.appUrl, this.options.proxyUrl),
549
652
  AMALGM_WORKSPACES_DIR: workspaceRoot,
550
653
  };
551
654
  const pty = createPtyHost({
@@ -554,37 +657,48 @@ export class MachineHost {
554
657
  });
555
658
  const services = new Set(machine.installation.services);
556
659
  const serviceStateRoot = join(this.paths(userEmail).machineDir, "services");
557
- const agentsService = services.has("agents")
558
- ? new Agents({ stateDir: join(serviceStateRoot, "agents") })
660
+ const agentsSdk = services.has("agents") ? await import("@amalgm/agents") : null;
661
+ const agentsMcpSdk = agentsSdk ? await import("@amalgm/agents/mcp") : null;
662
+ const agentsHttpSdk = agentsSdk ? await import("@amalgm/agents/http") : null;
663
+ const toolsSdk = services.has("tools") ? await import("@amalgm/tools") : null;
664
+ const toolsHttpSdk = toolsSdk ? await import("@amalgm/tools/http") : null;
665
+ const includeApps = services.has("apps") || machine.installation.selection === "all";
666
+ const includeBrowser = services.has("browser") || machine.installation.selection === "all";
667
+ const appsSdk = toolsSdk && includeApps ? await import("@amalgm/apps") : null;
668
+ const browserSdk = toolsSdk && includeBrowser ? await import("@amalgm/browser") : null;
669
+ const browserMcpSdk = browserSdk ? await import("@amalgm/browser/mcp") : null;
670
+ const agentsService = agentsSdk
671
+ ? new agentsSdk.Agents({ stateDir: join(serviceStateRoot, "agents") })
559
672
  : null;
560
673
  if (agentsService)
561
674
  installBuiltinAgents(agentsService);
562
- const agentsMcp = agentsService ? createAgentsMcp({ agents: agentsService }) : null;
563
- const agentsRest = agentsService ? createAgentsRest({ agents: agentsService }) : null;
675
+ const agentsMcp = agentsService ? agentsMcpSdk.createMcpServer({ agents: agentsService }) : null;
676
+ const agentsRest = agentsService ? agentsHttpSdk.createRestServer({ agents: agentsService }) : null;
564
677
  const agentsAddress = agentsRest ? await agentsRest.listen(0, "127.0.0.1") : null;
565
678
  const platformTools = services.has("tools")
566
- ? await platformToolDefinitions({
567
- agents: agentsMcp ?? { tools: createAgentsMcpTools({}) },
568
- apps: { tools: createAppsMcpTools({}) },
569
- browser: { tools: createBrowserMcpTools({}) },
679
+ ? await (await import("./platform-tools.js")).platformToolDefinitions({
680
+ ...(agentsMcp ? { agents: agentsMcp } : {}),
681
+ ...(appsSdk ? { apps: { tools: appsSdk.createMcpTools({}) } } : {}),
682
+ ...(browserMcpSdk ? { browser: { tools: browserMcpSdk.createBrowserMcpTools({}) } } : {}),
683
+ chat: services.has("chat"),
570
684
  products: {
571
- automations: Boolean(this.options.platformProducts),
572
- channels: Boolean(this.options.platformProducts),
685
+ automations: Boolean(this.options.platformProducts?.automationsBaseUrl),
686
+ channels: Boolean(this.options.platformProducts?.channelsBaseUrl),
573
687
  },
574
688
  })
575
689
  : [];
576
690
  const toolsMcp = services.has("tools")
577
- ? createToolsMcp({
691
+ ? toolsSdk.createMcpServer({
578
692
  databaseFile: join(serviceStateRoot, "tools", "tools.db"),
579
693
  deploymentDir: join(userRoot, "toolbox"),
580
694
  systemTools: [...platformTools],
581
695
  })
582
696
  : null;
583
- const toolsManagement = toolsMcp ? createToolsMcp({
697
+ const toolsManagement = toolsMcp ? toolsSdk.createMcpServer({
584
698
  toolbox: toolsMcp.toolbox,
585
699
  mcp: { loadout: [], includeManagement: true },
586
700
  }) : null;
587
- const toolsHttp = toolsMcp ? createToolboxHttpServer({ toolbox: toolsMcp.toolbox }) : null;
701
+ const toolsHttp = toolsMcp ? toolsHttpSdk.createToolboxHttpServer({ toolbox: toolsMcp.toolbox }) : null;
588
702
  const toolsAddress = toolsHttp ? await toolsHttp.listen(0, "127.0.0.1") : null;
589
703
  if (services.has("chat") && (!agentsService || !toolsMcp)) {
590
704
  throw new Error("The Chat release requires Agents and Tools");
@@ -600,7 +714,7 @@ export class MachineHost {
600
714
  : {}),
601
715
  ...(this.options.authorizationPlatform
602
716
  ? { platform: this.options.authorizationPlatform }
603
- : this.options.homedir ? { platform: "linux" } : {}),
717
+ : {}),
604
718
  });
605
719
  let gateway = null;
606
720
  const gatewayPort = () => {
@@ -610,7 +724,7 @@ export class MachineHost {
610
724
  return address.port;
611
725
  };
612
726
  const chat = services.has("chat") && agentsService && toolsMcp
613
- ? await createShellChatComposition({
727
+ ? await (await import("./chat-composition.js")).createShellChatComposition({
614
728
  ownerUserId: userId,
615
729
  computerId,
616
730
  stateDir: join(serviceStateRoot, "chat"),
@@ -625,13 +739,15 @@ export class MachineHost {
625
739
  })
626
740
  : null;
627
741
  const products = this.options.platformProducts
628
- ? createPlatformProductHost({
742
+ ? await (await import("./platform-product-host.js")).createPlatformProductHost({
629
743
  ...this.options.platformProducts,
630
744
  authorization,
631
- processes: createNodeAutomationProcessHost({
632
- defaultCwd: this.homedir,
633
- environment: { ...process.env, HOME: this.homedir },
634
- }),
745
+ ...(this.options.platformProducts.automationsBaseUrl ? {
746
+ processes: (await import("@amalgm/automations")).createNodeAutomationProcessHost({
747
+ defaultCwd: this.homedir,
748
+ environment: { ...process.env, HOME: this.homedir },
749
+ }),
750
+ } : {}),
635
751
  ...(toolsMcp ? { toolbox: toolsMcp.toolbox } : {}),
636
752
  ...(chat ? { actions: { 'chat.chat_agent_run': chatAgentAction(chat.agentRunner) } } : {}),
637
753
  ...(chat ? { inboundExecutor: {
@@ -647,23 +763,27 @@ export class MachineHost {
647
763
  } } : {}),
648
764
  })
649
765
  : null;
650
- const automationsMcp = products
651
- ? await connectOfficialMcpServer(createAutomationMcpServer(products.automations), { name: 'amalgm-automations', version: '0.2.5' })
766
+ const automationsMcp = products?.automations
767
+ ? await (await import("./official-mcp-host.js")).connectOfficialMcpServer((await import("@amalgm/automations/mcp")).createAutomationMcpServer(products.automations), { name: 'amalgm-automations', version: '0.2.5' })
652
768
  : null;
653
- const channelsMcp = products
654
- ? await connectOfficialMcpServer(createChannelsMcpServer(products.channels), { name: 'amalgm-channels', version: '0.1.0' })
769
+ const channelsMcp = products?.channels
770
+ ? await (await import("./official-mcp-host.js")).connectOfficialMcpServer((await import("@amalgm/channels/mcp")).createChannelsMcpServer(products.channels), { name: 'amalgm-channels', version: '0.1.0' })
655
771
  : null;
656
- const chatMcp = chat ? createChatMcpServer(chat.chat, { agentRunner: chat.agentRunner }) : null;
772
+ const chatMcp = chat ? (await import("@amalgm/chat/mcp")).createChatMcpServer(chat.chat, { agentRunner: chat.agentRunner }) : null;
657
773
  let appsMcp = null;
658
774
  const selectedAppsMcp = () => {
659
- appsMcp ??= createAppsMcp({ stateDir: join(serviceStateRoot, "apps") });
775
+ if (!appsSdk)
776
+ throw new Error("Apps is not selected in this installation.");
777
+ appsMcp ??= appsSdk.createMcpServer({ stateDir: join(serviceStateRoot, "apps") });
660
778
  return appsMcp;
661
779
  };
662
780
  let browserMcp = null;
663
781
  let browserService = null;
664
782
  const selectedBrowserMcp = () => {
665
- browserService ??= createBrowser({ root: join(serviceStateRoot, "browser") });
666
- browserMcp ??= createBrowserMcpServer(browserService);
783
+ if (!browserSdk || !browserMcpSdk)
784
+ throw new Error("Browser is not selected in this installation.");
785
+ browserService ??= browserSdk.createBrowser({ root: join(serviceStateRoot, "browser") });
786
+ browserMcp ??= browserMcpSdk.createBrowserMcpServer(browserService);
667
787
  return browserMcp;
668
788
  };
669
789
  const closeSelectedProducts = async () => {
@@ -678,6 +798,7 @@ export class MachineHost {
678
798
  env,
679
799
  exemptPrefixes: ["/chat/egress/"],
680
800
  mounts: [
801
+ { prefix: RUNTIME_IDENTITY_PATH, handle: async () => runtimeIdentity },
681
802
  {
682
803
  prefix: "/entities/register",
683
804
  handle: (body) => this.journey(body, () => this.registerWorkspace(bodyText(body, "path"))),
@@ -696,11 +817,12 @@ export class MachineHost {
696
817
  protocol: "mcp-json-rpc",
697
818
  handle: (body) => toolsMcp.handle(body),
698
819
  }] : []),
699
- ...(toolsMcp ? [{
820
+ ...(appsSdk ? [{
700
821
  prefix: "/mcp/apps",
701
822
  protocol: "mcp-json-rpc",
702
823
  handle: (body) => selectedAppsMcp().handle(body),
703
- }, {
824
+ }] : []),
825
+ ...(browserSdk ? [{
704
826
  prefix: "/mcp/browser",
705
827
  protocol: "mcp-json-rpc",
706
828
  handle: (body) => selectedBrowserMcp().handle(body),
@@ -757,9 +879,10 @@ export class MachineHost {
757
879
  await this.groundHost.activateRuntimeTunnel(address.port, runtimeToken);
758
880
  products?.runner.start();
759
881
  commandPublication = {
882
+ instanceId: runtimeIdentity.instanceId,
760
883
  userRoot: this.paths(userEmail).userRoot,
761
884
  deviceId,
762
- runtimeLabel: this.options.runtimeLabel ?? "main",
885
+ ...this.binding,
763
886
  userScope: userEmail,
764
887
  port: address.port,
765
888
  token: runtimeToken,