amalgm 0.1.254 → 0.1.256

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.254",
3
+ "version": "0.1.256",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -424,14 +424,23 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
424
424
  }
425
425
  }
426
426
 
427
- function bootstrap(registry) {
427
+ /**
428
+ * Create the first private entity-resource snapshot from the *already
429
+ * verified* bootstrap graph. This deliberately accepts records rather than
430
+ * a registry: login owns the declared user-home graph, but an older device
431
+ * can also hold unrelated local registrations which have never had cloud
432
+ * authority. Publishing those as an implicit local-wins merge would make
433
+ * sign-in a hidden `register` command.
434
+ */
435
+ function bootstrap(records) {
428
436
  const currentIdentity = identity();
429
437
  if (!currentIdentity.enabled) return currentIdentity;
430
438
  const existing = replica();
431
439
  if (existing?.state === 'active') {
432
440
  throw new Error('entity cloud registry already exists locally; cloud lookup must not create it again');
433
441
  }
434
- const snapshot = cloudSnapshot(registry.syncRecords());
442
+ if (!Array.isArray(records)) throw new Error('entity cloud bootstrap requires verified records');
443
+ const snapshot = cloudSnapshot(records);
435
444
  const cutoff = db.prepare('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM entity_cloud_mutations').get().sequence;
436
445
  saveReplica({
437
446
  resourceId: currentIdentity.resourceId,
@@ -471,7 +480,6 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
471
480
  || !Number.isInteger(Number(headVersion)) || Number(headVersion) < 0) {
472
481
  throw new Error('entity cloud registry authority is invalid');
473
482
  }
474
- const local = cloudSnapshot(registry.syncRecords());
475
483
  const current = replica();
476
484
  const bootstrapping = current?.state === 'bootstrapping';
477
485
  const reconciling = current?.state === BOOTSTRAP_RECONCILIATION_STATE;
@@ -486,7 +494,12 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
486
494
  // exactly the already-verified declared user home. This is not a general
487
495
  // local-wins merge: user-selected ground remains refused until an active
488
496
  // authoritative snapshot exists.
489
- if (!current && emptyCloudGraph && trustedBootstrap && sameRecords(local.records, trustedBootstrap)) {
497
+ // The first entity resource contains only declared user-home truth. An
498
+ // old installation may also have local, pre-cloud user ground; that is
499
+ // deliberately excluded here and will join only through an explicit
500
+ // later `amalgm register`. The empty cloud resource is therefore repaired
501
+ // from the trusted bootstrap records, never from the whole local registry.
502
+ if (emptyCloudGraph && trustedBootstrap && (!current || bootstrapping)) {
490
503
  const next = {
491
504
  resourceId,
492
505
  deviceId: currentIdentity.deviceId,
@@ -515,8 +528,8 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
515
528
  }
516
529
 
517
530
  if (reconciling) {
518
- if (!trustedBootstrap || !sameRecords(local.records, trustedBootstrap)) {
519
- throw new Error('entity bootstrap reconciliation no longer matches the verified user-home graph');
531
+ if (!trustedBootstrap) {
532
+ throw new Error('entity bootstrap reconciliation requires the verified user-home graph');
520
533
  }
521
534
  if (current.resourceId !== resourceId || Number(authorityEpoch) !== current.authorityEpoch
522
535
  || Number(headVersion) < current.headVersion || !recordsAreSubset(snapshot.records, trustedBootstrap)) {
@@ -547,13 +560,17 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
547
560
  reconciling: !complete,
548
561
  };
549
562
  }
550
- if (bootstrapping && !sameRecord(local, snapshot)) {
551
- throw new Error('first cloud entity snapshot does not match the locally bootstrapped identity graph');
563
+ if (bootstrapping) {
564
+ const expected = trustedBootstrap ?? cloudSnapshot(registry.syncRecords()).records;
565
+ if (!sameRecords(expected, snapshot.records)) {
566
+ throw new Error('first cloud entity snapshot does not match the locally bootstrapped identity graph');
567
+ }
552
568
  }
553
569
  if (!bootstrapping) {
554
570
  const cloudByUuid = new Map(snapshot.records.map((record) => [record.uuid, record]));
555
- for (const record of local.records) {
556
- if (!sameRecord(record, cloudByUuid.get(record.uuid))) {
571
+ for (const local of registry.syncRecords()) {
572
+ const remote = cloudByUuid.get(local.uuid);
573
+ if (remote && !sameRecord(cloudRecord(local), remote)) {
557
574
  throw new Error('cloud entity graph conflicts with locally bound ground; resolve the local mutation before importing a new cloud head');
558
575
  }
559
576
  }
@@ -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',
@@ -312,10 +381,10 @@ function createRegistrationService({
312
381
  };
313
382
  }
314
383
 
315
- /** The only local graph allowed to repair an old empty entity resource is
316
- * the exact home that the user-home handshake has already verified. Any
317
- * extra local entity would be user-selected ground and must wait for an
318
- * active entity-cloud authority instead. */
384
+ /** The only local graph eligible to repair an old empty entity resource is
385
+ * the exact home that the user-home handshake has already verified. Extra
386
+ * local entities are deliberately excluded rather than treated as a login
387
+ * merge: they become cloud-backed only through a later explicit register. */
319
388
  async function exactUserHomeCloudRecords() {
320
389
  const home = await bootstrapUserHome();
321
390
  const expected = new Map();
@@ -326,12 +395,9 @@ function createRegistrationService({
326
395
  // containers, with the registry's sync representation.
327
396
  expected.set(entry.record.uuid, boundary.current.registry.syncRecord(entry.record.uuid));
328
397
  }
329
- const records = boundary.current.registry.syncRecords();
330
- if (records.length !== expected.size) {
331
- throw new Error('local entity graph contains ground outside the declared user home');
332
- }
398
+ const records = [...expected.values()];
333
399
  for (const record of records) {
334
- const declared = expected.get(record.uuid);
400
+ const declared = boundary.current.registry.syncRecord(record.uuid);
335
401
  if (!declared || ![
336
402
  'uuid', 'type', 'parentUUID', 'name', 'status', 'version', 'payloadVersion',
337
403
  ].every((field) => record[field] === declared[field])) {
@@ -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
@@ -135,9 +135,11 @@ async function handleEntityRoutes(ctx) {
135
135
  return true;
136
136
  }
137
137
  // A missing entity resource is first created from the same declared
138
- // user-home graph that login owns. This is not an alternate registrar.
139
- await service.bootstrapUserHome();
140
- ctx.sendJson(200, service.entityCloud.bootstrap(service.registration.registry));
138
+ // user-home graph that login owns. This is not an alternate registrar:
139
+ // pre-cloud user registrations are deliberately excluded until their
140
+ // caller explicitly registers them after the bootstrap head is active.
141
+ const bootstrapRecords = await service.exactUserHomeCloudRecords();
142
+ ctx.sendJson(200, service.entityCloud.bootstrap(bootstrapRecords));
141
143
  return true;
142
144
  }
143
145
 
@@ -147,7 +149,8 @@ async function handleEntityRoutes(ctx) {
147
149
  const service = registrationService();
148
150
  try {
149
151
  const existing = service.entityCloud.replica();
150
- let requiresBootstrapProof = existing?.state === 'reconciling-bootstrap';
152
+ let requiresBootstrapProof = existing?.state === 'bootstrapping'
153
+ || existing?.state === 'reconciling-bootstrap';
151
154
  try {
152
155
  const snapshot = JSON.parse(Buffer.from(String(body?.snapshotBase64 || ''), 'base64').toString('utf8'));
153
156
  requiresBootstrapProof ||= Number(body?.headVersion) === 0
@@ -25,7 +25,7 @@ test('the entity cloud snapshot establishes identity, then each later entity com
25
25
  assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
26
26
  'initial bootstrap identity is carried by the first immutable snapshot, not an unscoped tail');
27
27
 
28
- const bootstrap = cloud.bootstrap(registry);
28
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
29
29
  const snapshot = parseSnapshot(Buffer.from(bootstrap.snapshotBase64, 'base64'));
30
30
  assert.deepEqual(snapshot.records.map((record) => record.uuid), [root.uuid]);
31
31
  cloud.install({
@@ -64,7 +64,7 @@ test('a signed-in registration is refused until the cloud entity resource is act
64
64
  const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
65
65
  assert.throws(() => cloud.assertRegistrationReady(), /entity cloud registry is not ready/);
66
66
  registry.enrollRoot({ type: 'workspace', name: 'bootstrap-only' });
67
- const bootstrap = cloud.bootstrap(registry);
67
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
68
68
  cloud.install({
69
69
  registry,
70
70
  resourceId: bootstrap.resourceId,
@@ -81,7 +81,7 @@ test('a large registration replaces the complete graph in one cloud operation an
81
81
  const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
82
82
  const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
83
83
  const root = registry.enrollRoot({ type: 'workspace', name: 'large-ground' });
84
- const bootstrap = cloud.bootstrap(registry);
84
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
85
85
  cloud.install({
86
86
  registry,
87
87
  resourceId: bootstrap.resourceId,
@@ -126,7 +126,7 @@ test('a large registration replaces the complete graph in one cloud operation an
126
126
  recipientDb.close();
127
127
  });
128
128
 
129
- test('only an exact verified bootstrap graph can repair a legacy empty entity resource', () => {
129
+ test('a legacy empty entity resource repairs only the verified home and excludes unrelated local ground', () => {
130
130
  const db = new Database(':memory:');
131
131
  const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
132
132
  const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
@@ -172,21 +172,28 @@ test('only an exact verified bootstrap graph can repair a legacy empty entity re
172
172
  assert.equal(cloud.replica().state, 'active');
173
173
  assert.doesNotThrow(() => cloud.assertRegistrationReady());
174
174
 
175
- const unsafeDb = new Database(':memory:');
176
- const unsafeCloud = createEntityCloud({ db: unsafeDb, userId: 'user-1', deviceId: 'unsafe-device' });
177
- const unsafeRegistry = createRegistry({ db: unsafeDb, ephemeral: true, emit: () => {}, journal: unsafeCloud.journal });
178
- const safeRoot = unsafeRegistry.enrollRoot({ type: 'workspace', name: 'agents' });
179
- unsafeRegistry.create({ type: 'file.text', parentUUID: safeRoot.uuid, name: 'not-a-default.md', payloadVersion: 'a'.repeat(64) });
180
- assert.throws(() => unsafeCloud.install({
181
- registry: unsafeRegistry,
175
+ const legacyDb = new Database(':memory:');
176
+ const legacyCloud = createEntityCloud({ db: legacyDb, userId: 'user-1', deviceId: 'legacy-device' });
177
+ const legacyRegistry = createRegistry({ db: legacyDb, ephemeral: true, emit: () => {}, journal: legacyCloud.journal });
178
+ const safeRoot = legacyRegistry.enrollRoot({ type: 'workspace', name: 'agents' });
179
+ const legacyRepo = legacyRegistry.create({ type: 'repo.git', parentUUID: safeRoot.uuid, name: 'old-local-repo' });
180
+ const verifiedHome = legacyRegistry.syncRecords().filter((record) => record.uuid === safeRoot.uuid);
181
+ const repaired = legacyCloud.install({
182
+ registry: legacyRegistry,
182
183
  resourceId,
183
184
  authorityEpoch: 1,
184
185
  headVersion: 0,
185
186
  snapshotBase64: empty.toString('base64'),
186
187
  snapshotChecksum: require('crypto').createHash('sha256').update(empty).digest('hex'),
187
- bootstrapRecords: unsafeRegistry.syncRecords().filter((record) => record.uuid === safeRoot.uuid),
188
- }), /conflicts with locally bound ground/,
189
- 'an empty cloud resource never authorizes arbitrary local ground');
188
+ bootstrapRecords: verifiedHome,
189
+ });
190
+ assert.equal(repaired.reconciling, true);
191
+ const repair = legacyCloud.claim();
192
+ assert.equal(repair.length, 1);
193
+ assert.equal(repair[0].operation.record.uuid, safeRoot.uuid,
194
+ 'only the verified home node is queued into the automatic cloud graph');
195
+ assert.equal(legacyRegistry.syncRecord(legacyRepo.uuid).uuid, legacyRepo.uuid,
196
+ 'unrelated local ground remains local until its caller explicitly registers it');
190
197
  });
191
198
 
192
199
  test('a compacted remote snapshot enters the cloud catalog, not locally watched identity, and add claims its immutable content', () => {
@@ -32,7 +32,7 @@ test('file bytes are captured as bounded immutable chunks, then acknowledged bef
32
32
  const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1', content });
33
33
  const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
34
34
  const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
35
- const bootstrap = cloud.bootstrap(registry);
35
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
36
36
  cloud.install({
37
37
  registry,
38
38
  resourceId: bootstrap.resourceId,
@@ -344,7 +344,7 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
344
344
  cloudUserId: 'user-1', cloudDeviceId: 'target-device',
345
345
  });
346
346
  try {
347
- const bootstrap = source.entityCloud.bootstrap(source.registration.registry);
347
+ const bootstrap = source.entityCloud.bootstrap(source.registration.registry.syncRecords());
348
348
  source.entityCloud.install({
349
349
  registry: source.registration.registry, resourceId: bootstrap.resourceId,
350
350
  authorityEpoch: 1, headVersion: 0, snapshotBase64: bootstrap.snapshotBase64,
@@ -233,6 +233,40 @@ test('user-home bootstrap creates only declared roots and registers them through
233
233
  );
234
234
  });
235
235
 
236
+ test('entity-cloud bootstrap publishes only the verified user home when an old local repo exists', async () => {
237
+ const service = createRegistrationService({
238
+ dir: path.join(tempRoot, 'state-user-home-cloud-boundary'),
239
+ homeDir: path.join(tempRoot, 'user-home-cloud-boundary'),
240
+ cloudUserId: 'user-home-cloud-boundary',
241
+ cloudDeviceId: 'computer-user-home-cloud-boundary',
242
+ });
243
+ services.push(service);
244
+ const home = await service.bootstrapUserHome();
245
+ const workspaces = home.roots.find((entry) => entry.key === 'workspaces').record;
246
+ const oldRepo = service.registration.registry.create({
247
+ type: 'repo.git', parentUUID: workspaces.uuid, name: 'old-local-repo', payloadVersion: null,
248
+ });
249
+
250
+ const records = await service.exactUserHomeCloudRecords();
251
+ assert.equal(records.length, 9);
252
+ assert.equal(records.some((record) => record.uuid === oldRepo.uuid), false,
253
+ 'login never turns existing user-selected ground into an implicit cloud registration');
254
+
255
+ const bootstrap = service.entityCloud.bootstrap(records);
256
+ const snapshot = JSON.parse(Buffer.from(bootstrap.snapshotBase64, 'base64').toString('utf8'));
257
+ assert.equal(snapshot.records.length, 9);
258
+ service.entityCloud.install({
259
+ registry: service.registration.registry,
260
+ resourceId: bootstrap.resourceId,
261
+ authorityEpoch: 1,
262
+ headVersion: 1,
263
+ snapshotBase64: bootstrap.snapshotBase64,
264
+ snapshotChecksum: bootstrap.snapshotChecksum,
265
+ bootstrapRecords: records,
266
+ });
267
+ assert.doesNotThrow(() => service.entityCloud.assertRegistrationReady());
268
+ });
269
+
236
270
  test('a fresh machine binds existing user-home UUIDs before it starts watching', async () => {
237
271
  const first = createRegistrationService({
238
272
  dir: path.join(tempRoot, 'state-user-home-origin'),
@@ -262,6 +296,62 @@ test('a fresh machine binds existing user-home UUIDs before it starts watching',
262
296
  'only the bound identities enter Watch');
263
297
  });
264
298
 
299
+ test('an older pre-cloud home adopts authenticated platform UUIDs without changing user descendants', async () => {
300
+ const authority = createRegistrationService({
301
+ dir: path.join(tempRoot, 'state-user-home-authority'),
302
+ homeDir: path.join(tempRoot, 'user-home-authority'),
303
+ });
304
+ const local = createRegistrationService({
305
+ dir: path.join(tempRoot, 'state-user-home-pre-cloud'),
306
+ homeDir: path.join(tempRoot, 'user-home-pre-cloud'),
307
+ });
308
+ services.push(authority, local);
309
+ const authoritative = await authority.bootstrapUserHome();
310
+ const before = await local.bootstrapUserHome();
311
+ const oldWorkspaces = before.roots.find((entry) => entry.key === 'workspaces').record;
312
+ const userProject = local.registration.registry.create({
313
+ type: 'folder', parentUUID: oldWorkspaces.uuid, name: 'user-project',
314
+ });
315
+
316
+ const snapshot = JSON.parse(JSON.stringify(authoritative.cloudSnapshot));
317
+ const rootIds = new Map();
318
+ for (const root of snapshot.roots) {
319
+ root.record.uuid = crypto.randomUUID();
320
+ rootIds.set(root.key, root.record.uuid);
321
+ }
322
+ for (const managed of snapshot.exactManaged) {
323
+ if (managed.relativePath === managed.key.replace(/-root$/, '')) {
324
+ managed.record.uuid = rootIds.get(managed.key.replace(/-root$/, ''));
325
+ managed.record.parentUUID = null;
326
+ } else {
327
+ managed.record.uuid = crypto.randomUUID();
328
+ managed.record.parentUUID = rootIds.get(managed.relativePath.split('/')[0]);
329
+ }
330
+ }
331
+
332
+ const rebound = await local.bootstrapUserHome({ identitySnapshot: snapshot });
333
+ const reboundWorkspaces = rebound.roots.find((entry) => entry.key === 'workspaces').record;
334
+ assert.equal(reboundWorkspaces.uuid, rootIds.get('workspaces'));
335
+ assert.equal(local.registration.registry.syncRecord(userProject.uuid).uuid, userProject.uuid,
336
+ 'the user project keeps its permanent identity');
337
+ assert.equal(local.registration.registry.syncRecord(userProject.uuid).parentUUID, reboundWorkspaces.uuid,
338
+ 'only its parent now names the cloud-authoritative platform root');
339
+ assert.deepEqual(
340
+ rebound.exactManaged.map((entry) => entry.record.uuid),
341
+ snapshot.exactManaged.map((entry) => entry.record.uuid),
342
+ 'the three exact system nodes and six roots now use the signed cloud identities',
343
+ );
344
+ const db = new Database(path.join(local.dir, 'identity.db'), { readonly: true });
345
+ try {
346
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entities WHERE uuid = ?').get(oldWorkspaces.uuid).count, 0,
347
+ 'the superseded platform root is not retained as a second active identity');
348
+ } finally {
349
+ db.close();
350
+ }
351
+ assert.equal(local.observer.status().every((root) => root.watching && !root.degraded), true,
352
+ 'rekeying identity preserves every existing watcher');
353
+ });
354
+
265
355
  test('a signed-in register call writes every entity delivery intent in identity.db before Watch begins', async () => {
266
356
  const service = createRegistrationService({
267
357
  dir: path.join(tempRoot, 'state-cloud-register'),
@@ -270,7 +360,7 @@ test('a signed-in register call writes every entity delivery intent in identity.
270
360
  cloudDeviceId: 'computer-cloud-register',
271
361
  });
272
362
  const home = await service.bootstrapUserHome();
273
- const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
363
+ const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
274
364
  service.entityCloud.install({
275
365
  registry: service.registration.registry,
276
366
  resourceId: bootstrap.resourceId,
@@ -310,7 +400,7 @@ test('a large signed-in register call publishes one complete cloud graph before
310
400
  cloudUserId: 'user-cloud-register-large',
311
401
  cloudDeviceId: 'computer-cloud-register-large',
312
402
  });
313
- const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
403
+ const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
314
404
  service.entityCloud.install({
315
405
  registry: service.registration.registry,
316
406
  resourceId: bootstrap.resourceId,