amalgm 0.1.256 → 0.1.258

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.
@@ -15,7 +15,10 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
 
17
17
  const CONTENT_CONTRACT = 'amalgm-entity-content@1';
18
- const CHUNK_BYTES = 256 * 1024;
18
+ // One chunk is one WAN round trip. The gateway accepts parts up to this same
19
+ // bound (ENTITY_CONTENT_PART_BYTES); older 256KB chunks remain valid because
20
+ // the manifest rule is `bytes <= CHUNK_BYTES`, never an exact size.
21
+ const CHUNK_BYTES = 4 * 1024 * 1024;
19
22
  const HASH = /^[0-9a-f]{64}$/;
20
23
 
21
24
  function digest(bytes) {
@@ -41,6 +44,10 @@ function createEntityContent({ dir }) {
41
44
  const chunksDir = path.join(root, 'chunks');
42
45
  const manifestsDir = path.join(root, 'manifests');
43
46
  const uploadsDir = path.join(root, 'uploads');
47
+ // Unfinished uploads per resource, so claim() does not re-read the whole
48
+ // ledger directory for every part it sends. The on-disk ledger stays the
49
+ // durable truth; this set is rebuilt from it once per process.
50
+ const pendingUploads = new Map();
44
51
 
45
52
  function ensureDirs() {
46
53
  for (const target of [chunksDir, manifestsDir, uploadsDir]) {
@@ -236,14 +243,27 @@ function createEntityContent({ dir }) {
236
243
  }
237
244
  }
238
245
 
246
+ function pendingSet(resourceId) {
247
+ let pending = pendingUploads.get(resourceId);
248
+ if (!pending) {
249
+ pending = new Set(uploadEntries(resourceId));
250
+ pendingUploads.set(resourceId, pending);
251
+ }
252
+ return pending;
253
+ }
254
+
239
255
  function ensureUpload({ resourceId, contentHash }) {
240
256
  if (typeof resourceId !== 'string' || !resourceId) throw new Error('entity content upload requires a resource id');
241
257
  const content = manifest(contentHash);
242
258
  if (!content) throw new Error(`content ${contentHash} was not captured before its entity record committed`);
243
259
  const current = readUpload(resourceId, contentHash, content);
244
- if (current) return current;
260
+ if (current) {
261
+ if (!current.manifestUploaded) pendingSet(resourceId).add(contentHash);
262
+ return current;
263
+ }
245
264
  const created = initialUpload(resourceId, contentHash);
246
265
  saveUpload(created);
266
+ pendingSet(resourceId).add(contentHash);
247
267
  return created;
248
268
  }
249
269
 
@@ -269,11 +289,15 @@ function createEntityContent({ dir }) {
269
289
  function claim({ resourceId, leaseMs = 30_000 }) {
270
290
  ensureDirs();
271
291
  const at = Date.now();
272
- for (const contentHash of uploadEntries(resourceId)) {
292
+ const pending = pendingSet(resourceId);
293
+ for (const contentHash of pending) {
273
294
  const content = manifest(contentHash);
274
295
  if (!content) throw new Error(`entity content ${contentHash} disappeared from the local cache`);
275
296
  const upload = readUpload(resourceId, contentHash, content);
276
- if (!upload || upload.manifestUploaded) continue;
297
+ if (!upload || upload.manifestUploaded) {
298
+ pending.delete(contentHash);
299
+ continue;
300
+ }
277
301
  if (upload.leaseUntil && Date.parse(upload.leaseUntil) > at) continue;
278
302
  const next = { ...upload, leaseUntil: new Date(at + Math.max(1_000, Number(leaseMs) || 30_000)).toISOString(), updatedAt: now() };
279
303
  if (next.nextPart < content.chunks.length) {
@@ -333,6 +357,7 @@ function createEntityContent({ dir }) {
333
357
  current.leaseUntil = null;
334
358
  current.updatedAt = now();
335
359
  saveUpload(current);
360
+ if (current.manifestUploaded) pendingUploads.get(resourceId)?.delete(contentHash);
336
361
  return { duplicate: false, uploaded: current.manifestUploaded };
337
362
  }
338
363
 
@@ -412,9 +437,11 @@ function createEntityContent({ dir }) {
412
437
  if (!current) return { kind: 'manifest', partIndex: 0, sha256: null };
413
438
  for (let partIndex = 0; partIndex < current.chunks.length; partIndex += 1) {
414
439
  const chunk = current.chunks[partIndex];
440
+ // Presence + size only: every chunk file was hash-verified before it
441
+ // was written (capture or receive), writes are write-once, and final
442
+ // assembly (`bytes`) re-verifies the whole content head.
415
443
  try {
416
- const bytes = fs.readFileSync(chunkFile(chunk.sha256));
417
- if (bytes.length === chunk.bytes && digest(bytes) === chunk.sha256) continue;
444
+ if (fs.statSync(chunkFile(chunk.sha256)).size === chunk.bytes) continue;
418
445
  } catch (error) {
419
446
  if (error.code !== 'ENOENT') throw error;
420
447
  }
@@ -596,6 +596,26 @@ function createRegistration(options) {
596
596
  return normalized.map(({ targetPath, identityAt }) => add(targetPath, { identityAt }));
597
597
  }
598
598
 
599
+ /** Reconcile one already-watched tree after the platform has materialized
600
+ * a newly declared exact default inside it. This is deliberately not part
601
+ * of ordinary re-registration: the caller must have already named the
602
+ * exact platform node it needs, and the normal Detect → Record path still
603
+ * decides the resulting record. */
604
+ function reconcileDeclared(targetPath) {
605
+ let ground;
606
+ try {
607
+ ground = fs.lstatSync(targetPath).isSymbolicLink() ? path.resolve(targetPath) : fs.realpathSync(targetPath);
608
+ } catch {
609
+ throw new Error(`declared user-home root is unavailable: ${targetPath}`);
610
+ }
611
+ const covering = treeRootCovering(ground);
612
+ if (!covering) throw new Error(`declared user-home root was not registered: ${targetPath}`);
613
+ observer.scanNow(covering.rootId);
614
+ flushIfDirty();
615
+ requireLiveWatchers(covering.path);
616
+ return { rootPath: covering.path, record: rootRecordOf(covering.rootId) };
617
+ }
618
+
599
619
  /** The boundary's whole convergence pass, one act: loss convicted
600
620
  * first (loudly — a conviction is never a retry), edges followed,
601
621
  * lenses reconciled, renderings projected. Startup, the explicit
@@ -626,6 +646,7 @@ function createRegistration(options) {
626
646
  registry,
627
647
  add,
628
648
  addMany,
649
+ reconcileDeclared,
629
650
 
630
651
  /**
631
652
  * `amalgm register <existing-path> --as <address>` — the managed
@@ -47,6 +47,10 @@ const Database = require('better-sqlite3');
47
47
  const { STORAGE_DIR } = require('../config');
48
48
  const { ensureDir } = require('../lib/storage');
49
49
  const { ensureUserHome } = require('../lib/layout');
50
+ const {
51
+ materializeSystemToolArtifacts,
52
+ migrateLegacyToolboxCatalog,
53
+ } = require('../toolbox/artifacts');
50
54
  const { createObserver } = require('../observer');
51
55
  const { shouldEnroll } = require('../workspace/tree/inclusion');
52
56
  const { classifyFile } = require('./classify');
@@ -58,8 +62,8 @@ const { transportBytes, localState, unpack } = require('./repo-states');
58
62
  const { machineId, userId } = require('../workspace/identity');
59
63
 
60
64
  const ENTITIES_DIR = path.join(STORAGE_DIR, 'entities');
61
- const USER_HOME_CLOUD_CONTRACT = 'amalgm-user-home@1';
62
- const USER_HOME_CLOUD_SCHEMA_VERSION = 1;
65
+ const USER_HOME_CLOUD_CONTRACT = 'amalgm-user-home@2';
66
+ const USER_HOME_CLOUD_SCHEMA_VERSION = 2;
63
67
  const ATOMIC_REGISTRATION_THRESHOLD = 500;
64
68
 
65
69
  let live = null;
@@ -260,6 +264,19 @@ function createRegistrationService({
260
264
  */
261
265
  async function bootstrapUserHome({ identitySnapshot = null } = {}) {
262
266
  const declaration = ensureUserHome(homeDir);
267
+ // Materialize every built-in tool before the shared registration boundary
268
+ // first sees `toolbox/`. Each file is an exact managed child; user tools
269
+ // remain ordinary sibling files. A legacy aggregate is moved outside the
270
+ // watched tree only after per-tool replacements exist.
271
+ const toolbox = declaration.roots.find((entry) => entry.key === 'toolbox');
272
+ if (!toolbox) throw new Error('declared user-home is missing its toolbox root');
273
+ const migrated = migrateLegacyToolboxCatalog({ toolboxDir: toolbox.path });
274
+ const systemTools = materializeSystemToolArtifacts({ toolboxDir: toolbox.path });
275
+ declaration.materializedExactPaths = [
276
+ ...(declaration.materializedExactPaths || []),
277
+ ...(migrated.artifactPaths || []).map((target) => path.relative(homeDir, target).split(path.sep).join('/')),
278
+ ...(systemTools.changedRelativePaths || []),
279
+ ];
263
280
  const seeds = identitySeeds(identitySnapshot, declaration);
264
281
  const addedRoots = boundary.current.addMany(declaration.roots.map((entry) => ({
265
282
  targetPath: entry.path,
@@ -274,25 +291,50 @@ function createRegistrationService({
274
291
  };
275
292
  });
276
293
 
277
- const system = roots.find((entry) => entry.key === 'system');
278
- if (!system) throw new Error('declared user home has no system root');
279
- const tree = boundary.current.tree(system.path);
280
- if (!tree?.root()) throw new Error(`declared system root was not registered: ${system.path}`);
281
-
282
- const recordAt = (relativePath) => {
294
+ const recordAt = (rootKey, relativePath, { required = true } = {}) => {
295
+ const root = roots.find((entry) => entry.key === rootKey);
296
+ if (!root) throw new Error(`declared user-home root was not registered: ${rootKey}`);
297
+ const tree = boundary.current.tree(root.path);
298
+ if (!tree?.root()) throw new Error(`declared user-home root was not registered: ${root.path}`);
283
299
  let record = tree.root();
284
300
  for (const name of relativePath.split('/')) {
285
301
  record = tree.registry.activeAt(record.uuid, name);
286
- if (!record) throw new Error(`declared user-home target was not registered: ${relativePath}`);
302
+ if (!record) {
303
+ if (!required) return null;
304
+ throw new Error(`declared user-home target was not registered: ${rootKey}/${relativePath}`);
305
+ }
287
306
  }
288
307
  return record;
289
308
  };
290
309
 
310
+ // An upgraded installation can already watch an older declared root.
311
+ // Re-registering that root must remain only an availability check, so the
312
+ // narrow exception is a root holding an exact node that the layout just
313
+ // materialized or that the registry proves missing. Detect and Record
314
+ // still create/update its identity through the shared boundary.
315
+ const rootsToReconcile = new Set((declaration.materializedExactPaths || [])
316
+ .map((relativePath) => relativePath.split('/')[0]));
317
+ for (const entry of declaration.exactManaged) {
318
+ if (entry.relativePath === entry.rootKey) continue;
319
+ const relativePath = entry.relativePath.slice(entry.rootKey.length + 1);
320
+ if (!recordAt(entry.rootKey, relativePath, { required: false })) {
321
+ rootsToReconcile.add(entry.rootKey);
322
+ }
323
+ }
324
+ for (const rootKey of rootsToReconcile) {
325
+ const root = roots.find((candidate) => candidate.key === rootKey);
326
+ if (!root) throw new Error(`declared user-home root was not registered: ${rootKey}`);
327
+ boundary.current.reconcileDeclared(root.path);
328
+ roots = roots.map((candidate) => candidate.key === rootKey
329
+ ? { ...candidate, record: boundary.current.tree(candidate.path).root() }
330
+ : candidate);
331
+ }
332
+
291
333
  let exactManaged = declaration.exactManaged.map((entry) => {
292
334
  const root = roots.find((candidate) => candidate.key === entry.rootKey);
293
335
  const isRoot = entry.relativePath === entry.rootKey;
294
336
  const relativePath = isRoot ? '' : entry.relativePath.slice(entry.rootKey.length + 1);
295
- const record = isRoot ? root?.record : recordAt(relativePath);
337
+ const record = isRoot ? root?.record : recordAt(entry.rootKey, relativePath);
296
338
  if (!record) throw new Error(`declared user-home managed target was not registered: ${entry.relativePath}`);
297
339
  return {
298
340
  key: entry.key,
@@ -767,6 +809,51 @@ function createRegistrationService({
767
809
  }
768
810
  }
769
811
 
812
+ /** The status surface composes each layer's own facts without creating a
813
+ * second registry or treating local paths as cloud metadata. A cloud-only
814
+ * catalog record is intentionally visible with `localPath: null`; only
815
+ * `amalgm add` creates a watcher binding for it. */
816
+ function status() {
817
+ const cloud = entityCloud.status();
818
+ const localRecords = boundary.current.registry.syncRecords();
819
+ const localByUuid = new Map(localRecords.map((record) => [record.uuid, record]));
820
+ const cloudByUuid = new Map(cloud.catalog.records.map((record) => [record.uuid, record]));
821
+ const roots = observer.status().map((root) => {
822
+ const tree = boundary.current.tree(root.path);
823
+ const record = tree ? tree.root() : null;
824
+ return {
825
+ uuid: record?.uuid || null,
826
+ path: root.path,
827
+ kind: root.kind,
828
+ watching: root.watching,
829
+ degraded: root.degraded,
830
+ addressWatching: root.addressWatching,
831
+ addressDegraded: root.addressDegraded,
832
+ };
833
+ });
834
+ const rootByUuid = new Map(roots.filter((root) => root.uuid !== null).map((root) => [root.uuid, root]));
835
+ const uuids = new Set([...localByUuid.keys(), ...cloudByUuid.keys()]);
836
+ const entities = [...uuids].sort().map((uuid) => {
837
+ const local = localByUuid.get(uuid) || null;
838
+ const record = cloudByUuid.get(uuid) || local;
839
+ let localPath = null;
840
+ if (local) {
841
+ try { localPath = boundary.current.addressOf(uuid); } catch {}
842
+ }
843
+ return {
844
+ ...record,
845
+ // A local journal projects its intent into the catalog before the
846
+ // gateway acknowledges it. Call that fact `catalogued`, not
847
+ // `cloudBacked`: the enclosing cloud summary says whether the
848
+ // catalog is actually pinned to an authoritative current head.
849
+ catalogued: cloudByUuid.has(uuid),
850
+ localPath,
851
+ root: rootByUuid.get(uuid) || null,
852
+ };
853
+ });
854
+ return { cloud, roots, entities };
855
+ }
856
+
770
857
  return {
771
858
  dir,
772
859
  homeDir,
@@ -820,9 +907,13 @@ function createRegistrationService({
820
907
  const received = entityCloud.receiveDownload({
821
908
  resourceId, contentHash, kind, partIndex, sha256, dataBase64,
822
909
  });
823
- advanceMaterializations();
910
+ // A mid-file part cannot complete any waiting tree; only a finished
911
+ // content head can. Advancing on every part re-walks every waiting
912
+ // job once per part, which is quadratic over a large download.
913
+ if (received?.complete) advanceMaterializations();
824
914
  return received;
825
915
  },
916
+ status,
826
917
  advanceMaterializations,
827
918
  bootstrapUserHome,
828
919
  exactUserHomeCloudRecords,
@@ -686,7 +686,7 @@ function createRegistry(options) {
686
686
 
687
687
  /**
688
688
  * First cloud bootstrap may meet a pre-cloud local safe home. Its six
689
- * roots and three exact managed nodes are platform truth, so they may
689
+ * roots and ten exact managed nodes are platform truth, so they may
690
690
  * adopt the authenticated cloud UUIDs without changing any user-owned
691
691
  * descendant identity. This is one atomic graph rekey, never a delete or
692
692
  * local-wins merge, and callers must prove that no ordinary entity-cloud
@@ -316,6 +316,12 @@ async function handleEntityRoutes(ctx) {
316
316
  return true;
317
317
  }
318
318
 
319
+ if (ctx.pathname === '/entities/status' && ctx.method === 'GET') {
320
+ ctx.res.setHeader('Cache-Control', 'no-store');
321
+ ctx.sendJson(200, registrationService().status());
322
+ return true;
323
+ }
324
+
319
325
  const materializationMatch = /^\/entities\/add\/(mat_[A-Za-z0-9_-]+)$/.exec(ctx.pathname);
320
326
  if (materializationMatch && ctx.method === 'GET') {
321
327
  const service = registrationService();
@@ -76,6 +76,37 @@ test('a signed-in registration is refused until the cloud entity resource is act
76
76
  assert.doesNotThrow(() => cloud.assertRegistrationReady());
77
77
  });
78
78
 
79
+ test('entity-cloud status reports an unsettled catalog as catching up without repairing it', () => {
80
+ const db = new Database(':memory:');
81
+ const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
82
+ const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
83
+ const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
84
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
85
+ cloud.install({
86
+ registry,
87
+ resourceId: bootstrap.resourceId,
88
+ authorityEpoch: 1,
89
+ headVersion: 0,
90
+ snapshotBase64: bootstrap.snapshotBase64,
91
+ snapshotChecksum: bootstrap.snapshotChecksum,
92
+ });
93
+
94
+ const current = cloud.status();
95
+ assert.equal(current.catalog.current, true);
96
+ assert.equal(current.catalog.records[0].uuid, root.uuid);
97
+ assert.equal(current.outbox.pending, 0);
98
+
99
+ const child = registry.create({ type: 'folder', parentUUID: root.uuid, name: 'child' });
100
+ const catchingUp = cloud.status();
101
+ assert.equal(catchingUp.catalog.current, false, 'an unacknowledged local mutation never masquerades as an active cloud head');
102
+ assert.equal(catchingUp.outbox.pending, 1);
103
+ assert.equal(catchingUp.catalog.records.some((record) => record.uuid === child.uuid), true,
104
+ 'status may show the local catalog projection while naming it catching up');
105
+
106
+ const after = cloud.status();
107
+ assert.deepEqual(after, catchingUp, 'status is read-only: it does not claim, retry, or repair delivery');
108
+ });
109
+
79
110
  test('a large registration replaces the complete graph in one cloud operation and a recipient waits for its compacted snapshot', () => {
80
111
  const db = new Database(':memory:');
81
112
  const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
@@ -244,7 +275,7 @@ test('a compacted remote snapshot enters the cloud catalog, not locally watched
244
275
  fs.rmSync(targetDir, { recursive: true, force: true });
245
276
  });
246
277
 
247
- test('add accepts only a catalog installed by the active compacted snapshot, never a snapshot plus a mutation tail', () => {
278
+ test('add follows in-order mirrored mutations and refuses after a version gap until the compacted snapshot returns', () => {
248
279
  const sourceDb = new Database(':memory:');
249
280
  const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
250
281
  const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
@@ -277,7 +308,7 @@ test('add accepts only a catalog installed by the active compacted snapshot, nev
277
308
  envelope: {
278
309
  resourceId,
279
310
  mutationId: 'ent_remote_change',
280
- contract: 'amalgm-entity-registry@1',
311
+ contract: 'amalgm-entity-registry@3',
281
312
  schemaVersion: 1,
282
313
  operationKind: 'entity.upsert',
283
314
  operation: { kind: 'entity.upsert', record: changed },
@@ -286,25 +317,53 @@ test('add accepts only a catalog installed by the active compacted snapshot, nev
286
317
  committedAt: '2026-08-04T00:00:00.000Z',
287
318
  });
288
319
 
320
+ // Version 13 arrived in official order on a snapshot-consistent catalog:
321
+ // the catalog IS the compacted head, so add needs no re-download.
322
+ const inOrder = path.join(targetDir, 'in-order');
323
+ fs.mkdirSync(inOrder);
324
+ assert.equal(
325
+ targetCloud.startMaterialization({ uuid: root.uuid, targetPath: inOrder }).headVersion,
326
+ 13,
327
+ 'an in-order mirrored mutation keeps the catalog materializable at the new head',
328
+ );
329
+
330
+ // Version 15 skips 14: the catalog can no longer prove it equals the
331
+ // head, so add refuses until a fresh compacted snapshot is installed.
332
+ const gapped = { ...changed, name: 'gapped.md', version: 'd'.repeat(64) };
333
+ targetCloud.ingest({
334
+ registry: targetRegistry,
335
+ envelope: {
336
+ resourceId,
337
+ mutationId: 'ent_remote_gap',
338
+ contract: 'amalgm-entity-registry@3',
339
+ schemaVersion: 1,
340
+ operationKind: 'entity.upsert',
341
+ operation: { kind: 'entity.upsert', record: gapped },
342
+ },
343
+ version: 15,
344
+ committedAt: '2026-08-04T00:00:01.000Z',
345
+ });
346
+ const afterGap = path.join(targetDir, 'after-gap');
347
+ fs.mkdirSync(afterGap);
289
348
  assert.throws(
290
- () => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }),
349
+ () => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: afterGap }),
291
350
  /catalog is not pinned to the active cloud head/,
292
- 'the mutation updated live catalog facts, but not the snapshot barrier',
351
+ 'a version gap drops the snapshot barrier until compaction returns',
293
352
  );
294
353
 
295
354
  const cloudRoot = initialSnapshot.records.find((record) => record.uuid === root.uuid);
296
- const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, changed])), 'utf8');
355
+ const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, gapped])), 'utf8');
297
356
  targetCloud.install({
298
357
  registry: targetRegistry,
299
358
  resourceId,
300
359
  authorityEpoch: 3,
301
- headVersion: 13,
360
+ headVersion: 15,
302
361
  snapshotBase64: compactedBytes.toString('base64'),
303
362
  snapshotChecksum: require('crypto').createHash('sha256').update(compactedBytes).digest('hex'),
304
363
  });
305
364
  assert.equal(
306
- targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }).headVersion,
307
- 13,
365
+ targetCloud.startMaterialization({ uuid: root.uuid, targetPath: afterGap }).headVersion,
366
+ 15,
308
367
  'the fresh compacted snapshot—not a history replay—makes add available again',
309
368
  );
310
369
  } finally {
@@ -189,7 +189,7 @@ test('an added ordinary workspace stays live: a later cloud file head replaces l
189
189
  envelope: {
190
190
  resourceId: sourceCloud.identity().resourceId,
191
191
  mutationId: 'ent_remote_file_change',
192
- contract: 'amalgm-entity-registry@1',
192
+ contract: 'amalgm-entity-registry@3',
193
193
  schemaVersion: 1,
194
194
  operationKind: 'entity.upsert',
195
195
  operation: { kind: 'entity.upsert', record: remoteRecord },
@@ -296,7 +296,7 @@ test('add preserves a symlink UUID and later retargets the same literal link wit
296
296
  const ingested = target.ingestCloudMutation({
297
297
  envelope: {
298
298
  resourceId: sourceCloud.identity().resourceId, mutationId: 'ent_remote_link_change',
299
- contract: 'amalgm-entity-registry@1', schemaVersion: 1,
299
+ contract: 'amalgm-entity-registry@3', schemaVersion: 1,
300
300
  operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: sourceRegistry.syncRecord(changed.uuid) },
301
301
  },
302
302
  version: 42, committedAt: '2026-08-05T00:00:00.000Z',
@@ -420,7 +420,7 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
420
420
  const ingested = target.ingestCloudMutation({
421
421
  envelope: {
422
422
  resourceId: bootstrap.resourceId, mutationId: 'ent_remote_repo_change',
423
- contract: 'amalgm-entity-registry@1', schemaVersion: 1,
423
+ contract: 'amalgm-entity-registry@3', schemaVersion: 1,
424
424
  operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: remoteRecord },
425
425
  },
426
426
  version: 2, committedAt: '2026-08-05T00:00:00.000Z',
@@ -54,6 +54,7 @@ const { ENTITIES_DIR, createRegistrationService, registrationService } = require
54
54
  const { adapterFor } = require('../adapters');
55
55
  const { createRuntimeHttpServer } = require('../server/http-service');
56
56
  const { createLocalServiceRouter } = require('../server/local-service-router');
57
+ const { SYSTEM_TOOL_ARTIFACTS } = require('../lib/layout');
57
58
 
58
59
  const services = [];
59
60
  let server;
@@ -203,6 +204,7 @@ test('user-home bootstrap creates only declared roots and registers them through
203
204
  ['agents-root', 'exact'], ['apps-root', 'exact'], ['automations-root', 'exact'],
204
205
  ['toolbox-root', 'exact'], ['workspaces-root', 'exact'], ['system-root', 'exact'],
205
206
  ['home-manifest', 'exact'], ['defaults', 'exact'], ['policy', 'exact'],
207
+ ...SYSTEM_TOOL_ARTIFACTS.map((entry) => [entry.key, 'exact']),
206
208
  ],
207
209
  );
208
210
  for (const entry of bootstrapped.exactManaged) {
@@ -218,7 +220,7 @@ test('user-home bootstrap creates only declared roots and registers them through
218
220
  }
219
221
 
220
222
  const cloudRecords = await service.exactUserHomeCloudRecords();
221
- assert.equal(cloudRecords.length, 9,
223
+ assert.equal(cloudRecords.length, 16,
222
224
  'the declared home is the entire graph eligible to repair a legacy empty cloud registry');
223
225
  assert.deepEqual(
224
226
  new Set(cloudRecords.map((record) => record.uuid)),
@@ -233,6 +235,46 @@ test('user-home bootstrap creates only declared roots and registers them through
233
235
  );
234
236
  });
235
237
 
238
+ test('an upgraded declared home registers every built-in tool through Detect and Record', async () => {
239
+ const home = path.join(tempRoot, 'user-home-upgrade');
240
+ const service = createRegistrationService({
241
+ dir: path.join(tempRoot, 'state-user-home-upgrade'),
242
+ homeDir: home,
243
+ });
244
+ services.push(service);
245
+
246
+ for (const root of ['agents', 'apps', 'automations', 'toolbox', 'workspaces', 'system']) {
247
+ fs.mkdirSync(path.join(home, root), { recursive: true });
248
+ }
249
+ fs.mkdirSync(path.join(home, 'system', 'defaults'));
250
+ fs.mkdirSync(path.join(home, 'system', 'policy'));
251
+ fs.writeFileSync(path.join(home, 'system', 'home.json'), JSON.stringify({
252
+ schemaVersion: 1,
253
+ kind: 'amalgm-user-home',
254
+ roots: [],
255
+ exactManaged: [],
256
+ }));
257
+ service.registration.addMany(['agents', 'apps', 'automations', 'toolbox', 'workspaces', 'system']
258
+ .map((root) => path.join(home, root)));
259
+
260
+ const bootstrapped = await service.bootstrapUserHome();
261
+ const toolbox = bootstrapped.roots.find((entry) => entry.key === 'toolbox');
262
+ const browser = bootstrapped.exactManaged.find((entry) => entry.key === 'amalgm-browser');
263
+ const manifest = bootstrapped.exactManaged.find((entry) => entry.key === 'home-manifest');
264
+ assert.equal(browser.record.parentUUID, toolbox.record.uuid,
265
+ 'the declared browser tool is a real child of the existing toolbox root');
266
+ assert.equal(browser.record.type, 'file.text');
267
+ assert.equal(service.registration.registry.syncRecord(manifest.record.uuid).payloadVersion, require('crypto')
268
+ .createHash('sha256')
269
+ .update(fs.readFileSync(path.join(home, 'system', 'home.json')))
270
+ .digest('hex'), 'the rewritten exact manifest is observed before its cloud identity is returned');
271
+ for (const artifact of SYSTEM_TOOL_ARTIFACTS) {
272
+ assert.ok(fs.existsSync(path.join(home, artifact.relativePath)), `${artifact.key} is materialized`);
273
+ assert.ok(bootstrapped.exactManaged.some((entry) => entry.key === artifact.key),
274
+ `${artifact.key} is an exact managed entity`);
275
+ }
276
+ });
277
+
236
278
  test('entity-cloud bootstrap publishes only the verified user home when an old local repo exists', async () => {
237
279
  const service = createRegistrationService({
238
280
  dir: path.join(tempRoot, 'state-user-home-cloud-boundary'),
@@ -248,13 +290,13 @@ test('entity-cloud bootstrap publishes only the verified user home when an old l
248
290
  });
249
291
 
250
292
  const records = await service.exactUserHomeCloudRecords();
251
- assert.equal(records.length, 9);
293
+ assert.equal(records.length, 16);
252
294
  assert.equal(records.some((record) => record.uuid === oldRepo.uuid), false,
253
295
  'login never turns existing user-selected ground into an implicit cloud registration');
254
296
 
255
297
  const bootstrap = service.entityCloud.bootstrap(records);
256
298
  const snapshot = JSON.parse(Buffer.from(bootstrap.snapshotBase64, 'base64').toString('utf8'));
257
- assert.equal(snapshot.records.length, 9);
299
+ assert.equal(snapshot.records.length, 16);
258
300
  service.entityCloud.install({
259
301
  registry: service.registration.registry,
260
302
  resourceId: bootstrap.resourceId,
@@ -35,7 +35,7 @@ test('system toolbox catalog exposes first-party default tools', () => {
35
35
  );
36
36
  });
37
37
 
38
- test('system toolbox catalog merge preserves persisted records', () => {
38
+ test('system toolbox catalog merge preserves user records but keeps built-ins authoritative', () => {
39
39
  const merged = mergeSystemToolboxCatalog({
40
40
  version: 1,
41
41
  tools: {
@@ -59,12 +59,9 @@ test('system toolbox catalog merge preserves persisted records', () => {
59
59
  },
60
60
  });
61
61
 
62
- assert.equal(merged.tools.browser.name, 'Persisted Browser');
62
+ assert.equal(merged.tools.browser.name, 'Browser');
63
63
  assert.equal(merged.tools['custom.cli'].name, 'Custom CLI');
64
- assert.equal(
65
- merged.toolActions['browser.browser_open'].name,
66
- 'persisted_navigate',
67
- );
64
+ assert.equal(merged.toolActions['browser.browser_open'], undefined);
68
65
  assert.equal(merged.toolActions['apps.apps_list'].name, 'apps_list');
69
66
  });
70
67
 
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const test = require('node:test');
8
+
9
+ const {
10
+ SYSTEM_TOOL_ARTIFACTS,
11
+ artifactPathForTool,
12
+ isSystemToolId,
13
+ materializeSystemToolArtifacts,
14
+ migrateLegacyToolboxCatalog,
15
+ readToolArtifacts,
16
+ writeToolArtifact,
17
+ } = require('../toolbox/artifacts');
18
+
19
+ function temporaryToolbox() {
20
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-toolbox-artifacts-'));
21
+ const toolboxDir = path.join(home, 'toolbox');
22
+ fs.mkdirSync(toolboxDir);
23
+ return { home, toolboxDir };
24
+ }
25
+
26
+ test('each built-in tool materializes as one distinct managed artifact', () => {
27
+ const { home, toolboxDir } = temporaryToolbox();
28
+ try {
29
+ const first = materializeSystemToolArtifacts({ toolboxDir });
30
+ assert.deepEqual(first.changedRelativePaths, SYSTEM_TOOL_ARTIFACTS.map((entry) => entry.relativePath));
31
+ const catalog = readToolArtifacts({ toolboxDir });
32
+ assert.deepEqual(Object.keys(catalog.tools).sort(), SYSTEM_TOOL_ARTIFACTS.map((entry) => entry.toolId).sort());
33
+ for (const entry of SYSTEM_TOOL_ARTIFACTS) {
34
+ const tool = catalog.tools[entry.toolId];
35
+ assert.equal(tool.origin, 'system');
36
+ assert.equal(tool.owner, 'amalgm');
37
+ assert.deepEqual(tool.source, { transport: 'amalgm-runtime', service: 'mcp' },
38
+ 'the cloud artifact has no machine-local port');
39
+ assert.equal(path.basename(artifactPathForTool(tool, { toolboxDir })), path.basename(entry.relativePath));
40
+ assert.equal(isSystemToolId(tool.id), true);
41
+ }
42
+ assert.deepEqual(materializeSystemToolArtifacts({ toolboxDir }).changedRelativePaths, [],
43
+ 'rerunning login does not rewrite an unchanged built-in tool');
44
+ } finally {
45
+ fs.rmSync(home, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test('a user tool is a sibling artifact and remains independent of built-in ownership', () => {
50
+ const { home, toolboxDir } = temporaryToolbox();
51
+ try {
52
+ materializeSystemToolArtifacts({ toolboxDir });
53
+ const tool = { id: 'aayush.stock-price', name: 'Stock Price', origin: 'user', owner: 'aayush' };
54
+ const actions = [{ id: 'aayush.stock-price.lookup', toolId: tool.id, name: 'lookup' }];
55
+ const target = writeToolArtifact({ tool, actions }, { toolboxDir });
56
+ assert.equal(path.basename(target), 'tool-aayush.stock-price.json');
57
+ const catalog = readToolArtifacts({ toolboxDir });
58
+ assert.equal(catalog.tools[tool.id].origin, 'user');
59
+ assert.equal(catalog.toolActions[actions[0].id].toolId, tool.id);
60
+ assert.equal(isSystemToolId(tool.id), false);
61
+ } finally {
62
+ fs.rmSync(home, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ test('a legacy aggregate catalog becomes user tool artifacts before leaving the registered toolbox tree', () => {
67
+ const { home, toolboxDir } = temporaryToolbox();
68
+ try {
69
+ fs.writeFileSync(path.join(toolboxDir, 'toolbox.json'), JSON.stringify({
70
+ version: 1,
71
+ tools: {
72
+ 'user.example': { id: 'user.example', name: 'Example', origin: 'user', owner: 'user' },
73
+ },
74
+ toolActions: {
75
+ 'user.example.run': { id: 'user.example.run', toolId: 'user.example', name: 'run' },
76
+ },
77
+ }));
78
+ const migrated = migrateLegacyToolboxCatalog({ toolboxDir });
79
+ assert.equal(migrated.migrated, true);
80
+ assert.equal(fs.existsSync(path.join(toolboxDir, 'toolbox.json')), false);
81
+ assert.equal(fs.existsSync(path.join(home, 'toolbox.legacy.json')), true);
82
+ const catalog = readToolArtifacts({ toolboxDir });
83
+ assert.equal(catalog.tools['user.example'].name, 'Example');
84
+ assert.equal(catalog.toolActions['user.example.run'].name, 'run');
85
+ } finally {
86
+ fs.rmSync(home, { recursive: true, force: true });
87
+ }
88
+ });