amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
package/README.md CHANGED
@@ -14,6 +14,18 @@ To move an existing install to the newest stable published build immediately, ru
14
14
 
15
15
  `amalgm login` opens a browser approval page, then registers the machine and stores its local tunnel/computer record in `~/.amalgm/computer.json`.
16
16
 
17
+ After login, register local ground with the cloud-backed entity rail:
18
+
19
+ ```sh
20
+ amalgm register path/to/file path/to/folder
21
+ ```
22
+
23
+ `amalgm register` watches the selected local ground and saves its entity
24
+ records and file contents to the signed-in user's private Amalgm cloud
25
+ resource. `amalgm add` is reserved for the inverse operation—bringing an
26
+ existing cloud entity onto a machine—and is not available until
27
+ materialization ships.
28
+
17
29
  If you start from the Amalgm web app or are setting up a remote/headless machine, create a setup code from the web app and run:
18
30
 
19
31
  ```sh
package/lib/cli.js CHANGED
@@ -114,10 +114,12 @@ function usage() {
114
114
  'amalgm - local computer runtime',
115
115
  '',
116
116
  'Usage:',
117
- ' amalgm login [--app-url https://amalgm.ai] [--name "My Mac"] [--setup-code ABCD-EFGH-JKLM-NPQR] [--no-open] [--no-poll]',
117
+ ' amalgm login [--app-url https://amalgm.ai] [--name "My Mac"] [--setup-code ABCD-EFGH-JKLM-NPQR] [--no-open] [--no-poll] [--no-start]',
118
118
  ' amalgm start [--foreground] [--local-only] [--no-auto-update] [--branch main|preview] [--user <user-id>]',
119
119
  ' amalgm run [--local-only]',
120
120
  ' amalgm stop',
121
+ ' amalgm register [path ...] Register local ground (default: current directory)',
122
+ ' amalgm add <entity-uuid> [path] Add a cloud-backed entity to this machine (coming with materialization)',
121
123
  ' amalgm status',
122
124
  ' amalgm doctor',
123
125
  ' amalgm service [install|start|stop|status|uninstall] [--mode auto|systemd|launchd|windows|portable]',
@@ -466,7 +468,7 @@ function openBrowser(url) {
466
468
  }
467
469
  }
468
470
 
469
- async function registerComputer(appUrl, registrationToken, device, name) {
471
+ async function registerComputer(appUrl, registrationToken, device, name, options = {}) {
470
472
  const registered = await requestJson(`${appUrl}/api/computers/register`, {
471
473
  method: 'POST',
472
474
  headers: {
@@ -498,7 +500,12 @@ async function registerComputer(appUrl, registrationToken, device, name) {
498
500
  if (!proxyTokenFromRecord(saved)) {
499
501
  console.log('HMAC proxy: not minted yet; Amalgm-managed egress will refresh after registration is complete.');
500
502
  }
501
- console.log('Run `amalgm start` to connect this machine.');
503
+ if (options['no-start']) {
504
+ console.log('Runtime start skipped by --no-start. Run `amalgm start` to connect this machine.');
505
+ return saved;
506
+ }
507
+ console.log('Starting the canonical user runtime to complete local and cloud setup...');
508
+ await start({ user: saved.user_id });
502
509
  return saved;
503
510
  }
504
511
 
@@ -531,7 +538,7 @@ async function refreshStoredProxyToken(record) {
531
538
  return saved;
532
539
  }
533
540
 
534
- async function loginWithSetupCode(appUrl, setupCode, device, name) {
541
+ async function loginWithSetupCode(appUrl, setupCode, device, name, options = {}) {
535
542
  const normalizedCode = normalizeSetupCode(setupCode);
536
543
  if (normalizedCode.length !== SETUP_CODE_LENGTH) {
537
544
  throw new Error('Invalid setup code. It should look like ABCD-EFGH-JKLM-NPQR.');
@@ -555,7 +562,7 @@ async function loginWithSetupCode(appUrl, setupCode, device, name) {
555
562
  throw new Error('Setup code was accepted, but no registration token was returned.');
556
563
  }
557
564
 
558
- return registerComputer(appUrl, redeemed.computer_registration_token, device, name);
565
+ return registerComputer(appUrl, redeemed.computer_registration_token, device, name, options);
559
566
  }
560
567
 
561
568
  function appUrlFlag(appUrl) {
@@ -665,7 +672,7 @@ async function startCliLogin(appUrl, device, name, options) {
665
672
  console.log(`Approved by: ${polled.approved_by.label}`);
666
673
  }
667
674
  console.log('Browser approved. Registering this computer...');
668
- return registerComputer(appUrl, polled.computer_registration_token, device, name);
675
+ return registerComputer(appUrl, polled.computer_registration_token, device, name, options);
669
676
  }
670
677
 
671
678
  if (polled.status === 'registered') {
@@ -692,7 +699,7 @@ async function login(options) {
692
699
  const setupCode = options['setup-code'] || options.code;
693
700
 
694
701
  if (setupCode) {
695
- return loginWithSetupCode(appUrl, setupCode, device, name);
702
+ return loginWithSetupCode(appUrl, setupCode, device, name, options);
696
703
  }
697
704
 
698
705
  return startCliLogin(appUrl, device, name, options);
@@ -835,6 +842,45 @@ function relaunchCurrentCommandAfterLayoutChange() {
835
842
  return Number.isInteger(result.status) ? result.status : 0;
836
843
  }
837
844
 
845
+ /** A login process begins before it knows the account scope. The registered
846
+ * runtime must not inherit that provisional `local` address or a caller's
847
+ * explicit AMALGM_DIR; launch the canonical process with the stable user id
848
+ * and let paths.js derive its user folder afresh. */
849
+ function canonicalStartLaunch(userId, options = {}, inheritedEnv = process.env) {
850
+ const userScope = sanitizeScopeSegment(userId, '');
851
+ if (!userScope) throw new Error('cannot start the canonical runtime without a user id');
852
+ const env = {
853
+ ...inheritedEnv,
854
+ AMALGM_RUNTIME_USER_ID: userScope,
855
+ AMALGM_USER_SCOPE: userScope,
856
+ AMALGM_LAYOUT_RELAUNCHED: '1',
857
+ };
858
+ delete env.AMALGM_DIR;
859
+ delete env.AMALGM_RUNTIME_STATE_DIR;
860
+ delete env.AMALGM_RUNTIME_INSTANCE_ID;
861
+ const args = [path.join(__dirname, '..', 'bin', 'amalgm.js'), 'start', '--user', userScope];
862
+ if (options.foreground) args.push('--foreground');
863
+ if (options['local-only']) args.push('--local-only');
864
+ if (options['no-auto-update']) args.push('--no-auto-update');
865
+ if (options['no-service']) args.push('--no-service');
866
+ if (options.mode) args.push('--mode', String(options.mode));
867
+ return { args, env };
868
+ }
869
+
870
+ function relaunchStartForUser(userId, options = {}) {
871
+ const launch = canonicalStartLaunch(userId, options);
872
+ const result = spawnSync(process.execPath, launch.args, {
873
+ stdio: 'inherit',
874
+ env: launch.env,
875
+ });
876
+ if (result.error) throw result.error;
877
+ if (result.signal) {
878
+ process.kill(process.pid, result.signal);
879
+ return 1;
880
+ }
881
+ return Number.isInteger(result.status) ? result.status : 0;
882
+ }
883
+
838
884
  function shouldRelaunchForAdoptedDir(adoption) {
839
885
  return Boolean(
840
886
  adoption?.dir
@@ -845,6 +891,13 @@ function shouldRelaunchForAdoptedDir(adoption) {
845
891
  }
846
892
 
847
893
  async function start(options) {
894
+ const requestedUser = typeof options.user === 'string'
895
+ ? sanitizeScopeSegment(options.user, '')
896
+ : '';
897
+ if (requestedUser && requestedUser !== AMALGM_RUNTIME_USER_SCOPE) {
898
+ process.exitCode = relaunchStartForUser(requestedUser, options);
899
+ return;
900
+ }
848
901
  ensureBaseDirs();
849
902
 
850
903
  if (await updateBeforeStartIfRequested(options)) {
@@ -1391,6 +1444,87 @@ async function logout() {
1391
1444
  console.log('Logged out locally. The device id remains so re-login updates the same computer.');
1392
1445
  }
1393
1446
 
1447
+ /**
1448
+ * `amalgm register [path ...]` — register selected local ground with the running runtime.
1449
+ *
1450
+ * The CLI is a human interface over the runtime's operation, never a
1451
+ * second implementation (docs/realtime-rails.md §3): it resolves the
1452
+ * path, authenticates, and prints what came back. Every rule about what
1453
+ * may be registered lives behind the endpoint.
1454
+ */
1455
+ async function register(positionals) {
1456
+ const targets = (positionals.length > 0 ? positionals : [process.cwd()])
1457
+ .map((target) => path.resolve(target));
1458
+
1459
+ // A resolved port is not a running runtime — the manifest supplies a
1460
+ // default whether or not anything listens — so both "no port for this
1461
+ // scope" and "nothing answered" mean the same thing to the user.
1462
+ const port = servicePorts().find(([name]) => name === 'amalgm-mcp')?.[1];
1463
+ let answer = null;
1464
+ if (port) {
1465
+ try {
1466
+ answer = await runtimePost(port, '/entities/register', { paths: targets });
1467
+ } catch (error) {
1468
+ if (error.code !== 'ECONNREFUSED') throw error;
1469
+ }
1470
+ }
1471
+ if (!answer) {
1472
+ console.error('Amalgm runtime is not running. Start it with `amalgm start`.');
1473
+ process.exitCode = 1;
1474
+ return;
1475
+ }
1476
+
1477
+ if (answer.status !== 200) {
1478
+ console.error(`Error: ${answer.payload?.error || `runtime responded ${answer.status}`}`);
1479
+ process.exitCode = 1;
1480
+ return;
1481
+ }
1482
+ const results = Array.isArray(answer.payload?.results)
1483
+ ? answer.payload.results
1484
+ : [{ rootPath: answer.payload?.rootPath, record: answer.payload?.record }];
1485
+ for (let index = 0; index < results.length; index += 1) {
1486
+ const { rootPath, record } = results[index];
1487
+ const target = targets[index];
1488
+ console.log(`Registered ${rootPath}`);
1489
+ console.log(` ${record.type} ${record.uuid}`);
1490
+ if (rootPath !== target) console.log(` (${target} is already covered by this tree)`);
1491
+ }
1492
+ }
1493
+
1494
+ /** POST JSON to a local runtime service, carrying the runtime token. */
1495
+ function runtimePost(port, requestPath, body) {
1496
+ const payload = JSON.stringify(body);
1497
+ const token = readJson(RUNTIME_TOKEN_FILE, null)?.token || '';
1498
+ return new Promise((resolve, reject) => {
1499
+ const req = http.request({
1500
+ host: '127.0.0.1',
1501
+ port,
1502
+ path: requestPath,
1503
+ method: 'POST',
1504
+ headers: {
1505
+ 'content-type': 'application/json',
1506
+ 'content-length': Buffer.byteLength(payload),
1507
+ ...(token ? { 'x-amalgm-runtime-token': token } : {}),
1508
+ },
1509
+ }, (res) => {
1510
+ let text = '';
1511
+ res.setEncoding('utf8');
1512
+ res.on('data', (chunk) => { text += chunk; });
1513
+ res.on('end', () => {
1514
+ let parsed = null;
1515
+ try {
1516
+ parsed = JSON.parse(text);
1517
+ } catch {
1518
+ parsed = null;
1519
+ }
1520
+ resolve({ status: res.statusCode, payload: parsed });
1521
+ });
1522
+ });
1523
+ req.on('error', reject);
1524
+ req.end(payload);
1525
+ });
1526
+ }
1527
+
1394
1528
  async function main(argv) {
1395
1529
  const { command, options, positionals } = parseArgs(argv);
1396
1530
 
@@ -1413,6 +1547,10 @@ async function main(argv) {
1413
1547
  });
1414
1548
  }
1415
1549
  if (command === 'stop') return stop();
1550
+ if (command === 'register') return register(positionals);
1551
+ if (command === 'add') {
1552
+ throw new Error('`amalgm add` is reserved for cloud-to-local materialization, which is not available yet. Use `amalgm register <path>` for local ground.');
1553
+ }
1416
1554
  if (command === 'status') return status();
1417
1555
  if (command === 'doctor') return doctor();
1418
1556
  if (command === 'service') return serviceCommand(options, positionals);
@@ -1426,4 +1564,4 @@ async function main(argv) {
1426
1564
  process.exitCode = 1;
1427
1565
  }
1428
1566
 
1429
- module.exports = { main };
1567
+ module.exports = { canonicalStartLaunch, main };
package/lib/layout.js CHANGED
@@ -32,15 +32,40 @@ const path = require('path');
32
32
  const ROOT_MANIFEST_NAME = '.amalgm-root.json';
33
33
  const USER_MANIFEST_NAME = '.amalgm-user.json';
34
34
  const LAYOUT_VERSION = 1;
35
+ const USER_HOME_MANIFEST_NAME = 'home.json';
36
+ const USER_HOME_SCHEMA_VERSION = 1;
35
37
 
36
- // Per-user roots for platform primitives. Everything Amalgm owns for a user
37
- // lives under one of these; anything else on disk is the user's own project.
38
+ // Stable per-user product namespaces. Their authority is NOT inferred from
39
+ // their path: user content and exact platform-managed targets are declared
40
+ // separately below (docs/user-cloud-bootstrap.md).
38
41
  const PRIMITIVE_DIRS = Object.freeze([
39
42
  'agents',
40
43
  'apps',
41
44
  'automations',
42
45
  'toolbox',
43
46
  'workspaces',
47
+ 'system',
48
+ ]);
49
+
50
+ const USER_HOME_ROOTS = Object.freeze([
51
+ { key: 'agents', relativePath: 'agents' },
52
+ { key: 'apps', relativePath: 'apps' },
53
+ { key: 'automations', relativePath: 'automations' },
54
+ { key: 'toolbox', relativePath: 'toolbox' },
55
+ { key: 'workspaces', relativePath: 'workspaces' },
56
+ { key: 'system', relativePath: 'system' },
57
+ ]);
58
+
59
+ // Platform management is an exact-node fact. It deliberately does not
60
+ // cascade from `system/`: human permissions may still inherit normally, while
61
+ // only these particular entities are product-managed.
62
+ const USER_HOME_EXACT_MANAGED = Object.freeze([
63
+ ...USER_HOME_ROOTS.map(({ key, relativePath }) => ({
64
+ key: `${key}-root`, relativePath, rootKey: key,
65
+ })),
66
+ { key: 'home-manifest', relativePath: 'system/home.json', rootKey: 'system' },
67
+ { key: 'defaults', relativePath: 'system/defaults', rootKey: 'system' },
68
+ { key: 'policy', relativePath: 'system/policy', rootKey: 'system' },
44
69
  ]);
45
70
 
46
71
  function readJson(file, fallback = null) {
@@ -56,6 +81,51 @@ function writeJson(file, data) {
56
81
  fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
57
82
  }
58
83
 
84
+ function declaredUserHome(userDir) {
85
+ const root = path.resolve(userDir);
86
+ return {
87
+ schemaVersion: USER_HOME_SCHEMA_VERSION,
88
+ userDir: root,
89
+ roots: USER_HOME_ROOTS.map((entry) => ({
90
+ ...entry,
91
+ path: path.join(root, entry.relativePath),
92
+ })),
93
+ exactManaged: USER_HOME_EXACT_MANAGED.map((entry) => ({
94
+ ...entry,
95
+ path: path.join(root, entry.relativePath),
96
+ scope: 'exact',
97
+ })),
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Materialize the fixed, safe bootstrap surface. This does not register or
103
+ * synchronize anything; runtime bootstrap calls the shared `amalgm register`
104
+ * boundary for that. Keeping those acts separate prevents login from growing
105
+ * a second identity registrar or silently enrolling machine-local state.
106
+ */
107
+ function ensureUserHome(userDir) {
108
+ const declaration = declaredUserHome(userDir);
109
+ for (const root of declaration.roots) {
110
+ fs.mkdirSync(root.path, { recursive: true, mode: 0o700 });
111
+ }
112
+ for (const managed of declaration.exactManaged) {
113
+ if (path.extname(managed.path)) continue;
114
+ fs.mkdirSync(managed.path, { recursive: true, mode: 0o700 });
115
+ }
116
+
117
+ const manifest = declaration.exactManaged.find((entry) => entry.key === 'home-manifest');
118
+ if (manifest && !fs.existsSync(manifest.path)) {
119
+ writeJson(manifest.path, {
120
+ schemaVersion: USER_HOME_SCHEMA_VERSION,
121
+ kind: 'amalgm-user-home',
122
+ roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
123
+ exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
124
+ });
125
+ }
126
+ return declaration;
127
+ }
128
+
59
129
  function lstatOrNull(target) {
60
130
  try {
61
131
  return fs.lstatSync(target);
@@ -249,17 +319,25 @@ function ensureUserDir(homeDir, { userId, email }) {
249
319
  const existing = findUserDirByUserId(homeDir, cleanId);
250
320
  if (existing) {
251
321
  if (cleanEmail && readUserManifest(existing)?.canonicalEmail !== cleanEmail) {
252
- return adoptUserEmail(homeDir, { userId: cleanId, email: cleanEmail }).dir;
322
+ const adopted = adoptUserEmail(homeDir, { userId: cleanId, email: cleanEmail }).dir;
323
+ ensureUserHome(adopted);
324
+ return adopted;
253
325
  }
254
326
  indexUserFolder(homeDir, path.basename(existing), { userId: cleanId, email: cleanEmail });
327
+ ensureUserHome(existing);
255
328
  return existing;
256
329
  }
257
330
 
258
331
  const legacyDir = path.join(homeDir, 'users', sanitizeFolderSegment(cleanId));
259
332
  if (lstatOrNull(legacyDir)?.isDirectory()) {
260
333
  writeUserManifest(legacyDir, { userId: cleanId, email: cleanEmail });
261
- if (cleanEmail) return adoptUserEmail(homeDir, { userId: cleanId, email: cleanEmail }).dir;
334
+ if (cleanEmail) {
335
+ const adopted = adoptUserEmail(homeDir, { userId: cleanId, email: cleanEmail }).dir;
336
+ ensureUserHome(adopted);
337
+ return adopted;
338
+ }
262
339
  indexUserFolder(homeDir, path.basename(legacyDir), { userId: cleanId, email: '' });
340
+ ensureUserHome(legacyDir);
263
341
  return legacyDir;
264
342
  }
265
343
 
@@ -268,6 +346,7 @@ function ensureUserDir(homeDir, { userId, email }) {
268
346
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
269
347
  writeUserManifest(dir, { userId: cleanId, email: cleanEmail });
270
348
  indexUserFolder(homeDir, folderName, { userId: cleanId, email: cleanEmail });
349
+ ensureUserHome(dir);
271
350
  return dir;
272
351
  }
273
352
 
@@ -323,8 +402,14 @@ module.exports = {
323
402
  PRIMITIVE_DIRS,
324
403
  ROOT_MANIFEST_NAME,
325
404
  USER_MANIFEST_NAME,
405
+ USER_HOME_EXACT_MANAGED,
406
+ USER_HOME_MANIFEST_NAME,
407
+ USER_HOME_ROOTS,
408
+ USER_HOME_SCHEMA_VERSION,
326
409
  adoptUserEmail,
410
+ declaredUserHome,
327
411
  emailFolderName,
412
+ ensureUserHome,
328
413
  ensureRootManifest,
329
414
  ensureUserDir,
330
415
  createCompatLink,