amalgm 0.1.253 → 0.1.255

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.253",
3
+ "version": "0.1.255",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -505,8 +505,12 @@ function createRegistration(options) {
505
505
  }
506
506
  const covering = treeRootCovering(ground);
507
507
  if (covering) {
508
- ensureTrees();
509
- tryFollow();
508
+ // This address is already registered ground. Its prior Register →
509
+ // Watch transition remains the proof of its identity; repeating a
510
+ // harmless "ensure the safe home exists" request must not rescan every
511
+ // edge and repository in every tree. Doorbells own later convergence.
512
+ // We still prove the one property this caller may rely on now: that
513
+ // the existing watcher is live (and retry its own failed birth once).
510
514
  requireLiveWatchers(covering.path);
511
515
  return { rootPath: covering.path, record: rootRecordOf(covering.rootId) };
512
516
  }
@@ -230,6 +230,24 @@ function createRegistrationService({
230
230
  };
231
231
  }
232
232
 
233
+ /** A bootstrap rekey is safe only before this installation has accepted
234
+ * ordinary entity-cloud state. After that point a UUID graph has an
235
+ * independent authority and an operator must resolve any disagreement
236
+ * explicitly; a login handshake never chooses a winner for it. */
237
+ function assertBootstrapRekeyAvailable() {
238
+ const state = identityDb.prepare(`
239
+ SELECT
240
+ (SELECT COUNT(*) FROM entity_cloud_replica) AS replicas,
241
+ (SELECT COUNT(*) FROM entity_cloud_mutations) AS mutations,
242
+ (SELECT COUNT(*) FROM entity_cloud_catalog_state) AS catalog_states,
243
+ (SELECT COUNT(*) FROM entity_cloud_catalog) AS catalog_records,
244
+ (SELECT COUNT(*) FROM entity_materializations) AS materializations
245
+ `).get();
246
+ if (Object.values(state).some((count) => Number(count) !== 0)) {
247
+ throw new Error('cannot adopt cloud user-home UUIDs after ordinary entity-cloud state exists');
248
+ }
249
+ }
250
+
233
251
  /**
234
252
  * Bootstrap a logged-in person's declared Amalgm home through the SAME
235
253
  * operation as `amalgm register`. `ensureUserHome` only creates the safe shape;
@@ -243,23 +261,18 @@ function createRegistrationService({
243
261
  async function bootstrapUserHome({ identitySnapshot = null } = {}) {
244
262
  const declaration = ensureUserHome(homeDir);
245
263
  const seeds = identitySeeds(identitySnapshot, declaration);
246
- const roots = [];
247
264
  const addedRoots = boundary.current.addMany(declaration.roots.map((entry) => ({
248
265
  targetPath: entry.path,
249
266
  identityAt: seeds?.forRoot(entry.key) ?? null,
250
267
  })));
251
- for (let index = 0; index < declaration.roots.length; index += 1) {
252
- const entry = declaration.roots[index];
268
+ let roots = declaration.roots.map((entry, index) => {
253
269
  const added = addedRoots[index];
254
- if (seeds && added.record.uuid !== seeds.root(entry.key).uuid) {
255
- throw new Error(`local user-home root ${entry.key} does not match its cloud UUID`);
256
- }
257
- roots.push({
270
+ return {
258
271
  key: entry.key,
259
272
  path: added.rootPath,
260
273
  record: added.record,
261
- });
262
- }
274
+ };
275
+ });
263
276
 
264
277
  const system = roots.find((entry) => entry.key === 'system');
265
278
  if (!system) throw new Error('declared user home has no system root');
@@ -275,15 +288,12 @@ function createRegistrationService({
275
288
  return record;
276
289
  };
277
290
 
278
- const exactManaged = declaration.exactManaged.map((entry) => {
291
+ let exactManaged = declaration.exactManaged.map((entry) => {
279
292
  const root = roots.find((candidate) => candidate.key === entry.rootKey);
280
293
  const isRoot = entry.relativePath === entry.rootKey;
281
294
  const relativePath = isRoot ? '' : entry.relativePath.slice(entry.rootKey.length + 1);
282
295
  const record = isRoot ? root?.record : recordAt(relativePath);
283
296
  if (!record) throw new Error(`declared user-home managed target was not registered: ${entry.relativePath}`);
284
- if (seeds && record.uuid !== seeds.managed(entry.key).uuid) {
285
- throw new Error(`local user-home managed target ${entry.key} does not match its cloud UUID`);
286
- }
287
297
  return {
288
298
  key: entry.key,
289
299
  rootKey: entry.rootKey,
@@ -294,6 +304,65 @@ function createRegistrationService({
294
304
  };
295
305
  });
296
306
 
307
+ if (seeds) {
308
+ const bindings = [
309
+ ...roots.map((entry) => ({
310
+ key: entry.key,
311
+ local: entry.record,
312
+ authoritative: seeds.root(entry.key),
313
+ })),
314
+ ...exactManaged.map((entry) => ({
315
+ key: entry.key,
316
+ local: entry.record,
317
+ authoritative: seeds.managed(entry.key),
318
+ })),
319
+ ];
320
+ for (const binding of bindings) {
321
+ const remote = binding.authoritative;
322
+ const local = binding.local;
323
+ if (!remote || remote.type !== local.type || remote.name !== local.name || remote.status !== local.status) {
324
+ throw new Error(`cloud user-home node ${binding.key} does not match its declared local shape`);
325
+ }
326
+ }
327
+ if (bindings.some(({ local, authoritative }) => local.uuid !== authoritative.uuid)) {
328
+ assertBootstrapRekeyAvailable();
329
+ // The six roots appear once more in `exactManaged` to name their
330
+ // governance. They are the same nodes, not a second rekey.
331
+ const uniqueBindings = [...new Map(bindings.map((binding) => [binding.local.uuid, binding])).values()];
332
+ boundary.current.registry.rebindBootstrapIdentities(uniqueBindings.map(({ local, authoritative }) => ({
333
+ oldUUID: local.uuid,
334
+ newUUID: authoritative.uuid,
335
+ newParentUUID: authoritative.parentUUID,
336
+ type: authoritative.type,
337
+ name: authoritative.name,
338
+ })));
339
+ roots = roots.map((entry) => ({
340
+ ...entry,
341
+ record: boundary.current.registry.syncRecord(seeds.root(entry.key).uuid),
342
+ }));
343
+ const reboundSystem = roots.find((entry) => entry.key === 'system');
344
+ const reboundTree = boundary.current.tree(reboundSystem.path);
345
+ if (!reboundTree?.root()) throw new Error('cloud user-home rekey lost the declared system root');
346
+ exactManaged = exactManaged.map((entry) => {
347
+ const remote = seeds.managed(entry.key);
348
+ return {
349
+ ...entry,
350
+ record: boundary.current.registry.syncRecord(remote.uuid),
351
+ };
352
+ });
353
+ }
354
+ for (const entry of roots) {
355
+ if (entry.record.uuid !== seeds.root(entry.key).uuid) {
356
+ throw new Error(`local user-home root ${entry.key} does not match its cloud UUID`);
357
+ }
358
+ }
359
+ for (const entry of exactManaged) {
360
+ if (entry.record.uuid !== seeds.managed(entry.key).uuid) {
361
+ throw new Error(`local user-home managed target ${entry.key} does not match its cloud UUID`);
362
+ }
363
+ }
364
+ }
365
+
297
366
  const cloudSnapshot = {
298
367
  schemaVersion: USER_HOME_CLOUD_SCHEMA_VERSION,
299
368
  kind: 'amalgm-user-home',
@@ -684,6 +684,67 @@ function createRegistry(options) {
684
684
  });
685
685
  },
686
686
 
687
+ /**
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
690
+ * adopt the authenticated cloud UUIDs without changing any user-owned
691
+ * descendant identity. This is one atomic graph rekey, never a delete or
692
+ * local-wins merge, and callers must prove that no ordinary entity-cloud
693
+ * history has begun before asking for it.
694
+ */
695
+ rebindBootstrapIdentities(bindings) {
696
+ if (!Array.isArray(bindings) || bindings.length === 0) {
697
+ throw new Error('bootstrap identity rebind requires one or more bindings');
698
+ }
699
+ return transaction(() => {
700
+ const byOld = new Map();
701
+ const byNew = new Map();
702
+ const mappings = bindings.map((binding) => {
703
+ if (!binding || typeof binding.oldUUID !== 'string' || typeof binding.newUUID !== 'string'
704
+ || !UUID.test(binding.oldUUID) || !UUID.test(binding.newUUID)
705
+ || (binding.newParentUUID !== null && (!UUID.test(binding.newParentUUID)))) {
706
+ throw new Error('bootstrap identity rebind has an invalid UUID binding');
707
+ }
708
+ const oldUuid = binding.oldUUID.toLowerCase();
709
+ const newUuid = binding.newUUID.toLowerCase();
710
+ if (byOld.has(oldUuid) || byNew.has(newUuid)) {
711
+ throw new Error('bootstrap identity rebind repeats an identity');
712
+ }
713
+ const row = mustGet(oldUuid);
714
+ if (row.status !== 'active' || row.type !== binding.type || row.name !== binding.name) {
715
+ throw new Error(`bootstrap identity ${oldUuid} does not match its declared local node`);
716
+ }
717
+ if (newUuid !== oldUuid && store.byUuid(newUuid)) {
718
+ throw new Error(`bootstrap identity ${newUuid} is already registered locally`);
719
+ }
720
+ const mapping = {
721
+ oldUuid,
722
+ newUuid,
723
+ newParentUuid: binding.newParentUUID === null ? null : binding.newParentUUID.toLowerCase(),
724
+ type: row.type,
725
+ name: row.name,
726
+ status: row.status,
727
+ payloadVersion: row.payloadVersion,
728
+ oldParentUuid: row.parentUuid,
729
+ };
730
+ byOld.set(oldUuid, mapping);
731
+ byNew.set(newUuid, mapping);
732
+ return mapping;
733
+ });
734
+ for (const mapping of mappings) {
735
+ const translatedParent = mapping.oldParentUuid === null
736
+ ? null
737
+ : (byOld.get(mapping.oldParentUuid)?.newUuid ?? mapping.oldParentUuid);
738
+ if (translatedParent !== mapping.newParentUuid) {
739
+ throw new Error(`bootstrap identity ${mapping.oldUuid} has a different authoritative parent`);
740
+ }
741
+ }
742
+ store.rebindIdentities(mappings);
743
+ membershipHashes.clear();
744
+ return mappings.map((mapping) => syncRecord(mustGet(mapping.newUuid)));
745
+ });
746
+ },
747
+
687
748
  record: (uuid) => record(mustGet(uuid)),
688
749
  syncRecord: (uuid) => syncRecord(mustGet(uuid)),
689
750
  /** Full logical graph for cloud snapshots. Machine bindings and evidence
@@ -300,6 +300,63 @@ function createStore(db) {
300
300
  WHERE uuid = ?
301
301
  `).run(type, parentUuid, name, status, payloadVersion, uuid);
302
302
  },
303
+ /**
304
+ * Rekey a small, already-validated identity set in one invisible SQLite
305
+ * transition. This is deliberately a store primitive, not a generic
306
+ * public CRUD verb: trusted bootstrap migration is the only caller.
307
+ *
308
+ * The old row is retired just long enough to free its active slot, a new
309
+ * row is born with the authoritative UUID, every direct child follows the
310
+ * new parent, then the retired row is removed. Nothing outside the
311
+ * transaction can observe the temporary state. UUID references held by
312
+ * local evidence, Git state, and managed references follow as well.
313
+ */
314
+ rebindIdentities(mappings) {
315
+ const retired = mappings.filter((mapping) => mapping.oldUuid !== mapping.newUuid);
316
+ const retire = db.prepare("UPDATE entities SET status = 'trashed' WHERE uuid = ?");
317
+ const insert = db.prepare(`
318
+ INSERT INTO entities (uuid, type, parent_uuid, name, status, payload_version)
319
+ VALUES (?, ?, ?, ?, ?, ?)
320
+ `);
321
+ const reparent = db.prepare('UPDATE entities SET parent_uuid = ? WHERE parent_uuid = ?');
322
+ const rebindChannels = db.prepare('UPDATE channel_bindings SET entity_uuid = ? WHERE entity_uuid = ?');
323
+ const rebindClaims = db.prepare('UPDATE contested_ground SET claimant_uuid = ? WHERE claimant_uuid = ?');
324
+ const rebindRepoState = db.prepare('UPDATE repo_states SET entity_uuid = ? WHERE entity_uuid = ?');
325
+ const rebindReference = db.prepare("UPDATE entities SET payload_version = ? WHERE type = 'reference' AND payload_version = ?");
326
+ const remove = db.prepare('DELETE FROM entities WHERE uuid = ?');
327
+
328
+ db.transaction(() => {
329
+ for (const mapping of retired) retire.run(mapping.oldUuid);
330
+
331
+ const remaining = new Map(retired.map((mapping) => [mapping.newUuid, mapping]));
332
+ while (remaining.size > 0) {
333
+ let inserted = false;
334
+ for (const mapping of [...remaining.values()]) {
335
+ if (remaining.has(mapping.newParentUuid)) continue;
336
+ insert.run(
337
+ mapping.newUuid,
338
+ mapping.type,
339
+ mapping.newParentUuid,
340
+ mapping.name,
341
+ mapping.status,
342
+ mapping.payloadVersion,
343
+ );
344
+ remaining.delete(mapping.newUuid);
345
+ inserted = true;
346
+ }
347
+ if (!inserted) throw new Error('identity rebind has a cyclic authoritative parent map');
348
+ }
349
+
350
+ for (const mapping of retired) reparent.run(mapping.newUuid, mapping.oldUuid);
351
+ for (const mapping of retired) {
352
+ rebindChannels.run(mapping.newUuid, mapping.oldUuid);
353
+ rebindClaims.run(mapping.newUuid, mapping.oldUuid);
354
+ rebindRepoState.run(mapping.newUuid, mapping.oldUuid);
355
+ rebindReference.run(mapping.newUuid, mapping.oldUuid);
356
+ }
357
+ for (const mapping of retired) remove.run(mapping.oldUuid);
358
+ })();
359
+ },
303
360
  /**
304
361
  * One address permutation as one write. Movers may hold each other's
305
362
  * slots, and the live unique index cannot be threaded one row at a
@@ -262,6 +262,62 @@ test('a fresh machine binds existing user-home UUIDs before it starts watching',
262
262
  'only the bound identities enter Watch');
263
263
  });
264
264
 
265
+ test('an older pre-cloud home adopts authenticated platform UUIDs without changing user descendants', async () => {
266
+ const authority = createRegistrationService({
267
+ dir: path.join(tempRoot, 'state-user-home-authority'),
268
+ homeDir: path.join(tempRoot, 'user-home-authority'),
269
+ });
270
+ const local = createRegistrationService({
271
+ dir: path.join(tempRoot, 'state-user-home-pre-cloud'),
272
+ homeDir: path.join(tempRoot, 'user-home-pre-cloud'),
273
+ });
274
+ services.push(authority, local);
275
+ const authoritative = await authority.bootstrapUserHome();
276
+ const before = await local.bootstrapUserHome();
277
+ const oldWorkspaces = before.roots.find((entry) => entry.key === 'workspaces').record;
278
+ const userProject = local.registration.registry.create({
279
+ type: 'folder', parentUUID: oldWorkspaces.uuid, name: 'user-project',
280
+ });
281
+
282
+ const snapshot = JSON.parse(JSON.stringify(authoritative.cloudSnapshot));
283
+ const rootIds = new Map();
284
+ for (const root of snapshot.roots) {
285
+ root.record.uuid = crypto.randomUUID();
286
+ rootIds.set(root.key, root.record.uuid);
287
+ }
288
+ for (const managed of snapshot.exactManaged) {
289
+ if (managed.relativePath === managed.key.replace(/-root$/, '')) {
290
+ managed.record.uuid = rootIds.get(managed.key.replace(/-root$/, ''));
291
+ managed.record.parentUUID = null;
292
+ } else {
293
+ managed.record.uuid = crypto.randomUUID();
294
+ managed.record.parentUUID = rootIds.get(managed.relativePath.split('/')[0]);
295
+ }
296
+ }
297
+
298
+ const rebound = await local.bootstrapUserHome({ identitySnapshot: snapshot });
299
+ const reboundWorkspaces = rebound.roots.find((entry) => entry.key === 'workspaces').record;
300
+ assert.equal(reboundWorkspaces.uuid, rootIds.get('workspaces'));
301
+ assert.equal(local.registration.registry.syncRecord(userProject.uuid).uuid, userProject.uuid,
302
+ 'the user project keeps its permanent identity');
303
+ assert.equal(local.registration.registry.syncRecord(userProject.uuid).parentUUID, reboundWorkspaces.uuid,
304
+ 'only its parent now names the cloud-authoritative platform root');
305
+ assert.deepEqual(
306
+ rebound.exactManaged.map((entry) => entry.record.uuid),
307
+ snapshot.exactManaged.map((entry) => entry.record.uuid),
308
+ 'the three exact system nodes and six roots now use the signed cloud identities',
309
+ );
310
+ const db = new Database(path.join(local.dir, 'identity.db'), { readonly: true });
311
+ try {
312
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entities WHERE uuid = ?').get(oldWorkspaces.uuid).count, 0,
313
+ 'the superseded platform root is not retained as a second active identity');
314
+ } finally {
315
+ db.close();
316
+ }
317
+ assert.equal(local.observer.status().every((root) => root.watching && !root.degraded), true,
318
+ 'rekeying identity preserves every existing watcher');
319
+ });
320
+
265
321
  test('a signed-in register call writes every entity delivery intent in identity.db before Watch begins', async () => {
266
322
  const service = createRegistrationService({
267
323
  dir: path.join(tempRoot, 'state-cloud-register'),