amalgm 0.1.247 → 0.1.248

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.
@@ -936,13 +936,16 @@ function createSharedRealtimeTunnel({
936
936
  mutationId: entityIdentity.mutationId,
937
937
  version: frame.version,
938
938
  committedAt: frame.committed_at,
939
- }).then(() => {
939
+ }).then((ack) => {
940
940
  entityMutationInflight.delete(frame.request_id);
941
941
  // An acknowledgement advances the official head. Re-read the
942
942
  // private resource immediately: the gateway compacts that head into
943
943
  // one immutable snapshot before replying, so this device never has
944
- // to use its mutation tail as an `amalgm add` starting point.
945
- void ensurePrivateEntityRegistry();
944
+ // to use its mutation tail as an `amalgm add` starting point. A
945
+ // legacy empty entity resource is seeded in ordered records first;
946
+ // intermediate heads are intentionally not imported as a usable
947
+ // snapshot, because they are not the complete declared home yet.
948
+ if (ack?.requiresSnapshot !== false) void ensurePrivateEntityRegistry();
946
949
  scheduleEntityMutationDrain();
947
950
  }).catch(() => {
948
951
  setTimeout(() => enabled && handleAck(frame), RETRY_MS).unref?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.247",
3
+ "version": "0.1.248",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -13,6 +13,7 @@ const crypto = require('crypto');
13
13
 
14
14
  const ENTITY_CLOUD_CONTRACT = 'amalgm-entity-registry@1';
15
15
  const ENTITY_CLOUD_SCHEMA_VERSION = 1;
16
+ const BOOTSTRAP_RECONCILIATION_STATE = 'reconciling-bootstrap';
16
17
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
17
18
  const TYPES = new Set([
18
19
  'workspace', 'folder', 'repo.git', 'file.text', 'file.binary', 'link', 'reference',
@@ -40,6 +41,15 @@ function sameRecord(left, right) {
40
41
  return stableJson(left) === stableJson(right);
41
42
  }
42
43
 
44
+ function sameRecords(left, right) {
45
+ return sameRecord(snapshotFromRecords(left), snapshotFromRecords(right));
46
+ }
47
+
48
+ function recordsAreSubset(records, expected) {
49
+ const expectedByUuid = new Map(snapshotFromRecords(expected).records.map((record) => [record.uuid, record]));
50
+ return snapshotFromRecords(records).records.every((record) => sameRecord(record, expectedByUuid.get(record.uuid)));
51
+ }
52
+
43
53
  function assertRecord(value) {
44
54
  if (!value || typeof value !== 'object' || typeof value.uuid !== 'string' || !UUID.test(value.uuid)
45
55
  || typeof value.type !== 'string' || !TYPES.has(value.type)
@@ -309,6 +319,26 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
309
319
  `).run(current.resourceId, mutationId, stableJson(operation), createdAt, createdAt);
310
320
  }
311
321
 
322
+ function enqueueBootstrapReconciliation(records, current) {
323
+ const pending = db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations WHERE resource_id = ? AND mutation_state <> \'synced\'')
324
+ .get(current.resourceId);
325
+ if (Number(pending.count) !== 0) {
326
+ throw new Error('entity bootstrap reconciliation already has an unfinished outbox');
327
+ }
328
+ const insert = db.prepare('INSERT INTO entity_cloud_mutations (resource_id, mutation_id, operation_json, mutation_state, created_at, updated_at) VALUES (?, ?, ?, \'pending\', ?, ?)');
329
+ const createdAt = now();
330
+ for (const record of snapshotFromRecords(records).records) {
331
+ queueContent(record, current.resourceId, { queue: true });
332
+ insert.run(
333
+ current.resourceId,
334
+ 'ent_' + crypto.randomBytes(18).toString('base64url'),
335
+ stableJson({ kind: 'entity.upsert', record }),
336
+ createdAt,
337
+ createdAt,
338
+ );
339
+ }
340
+ }
341
+
312
342
  function assertRegistrationReady() {
313
343
  if (!ownerUserId) return;
314
344
  const current = replica();
@@ -346,7 +376,15 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
346
376
  };
347
377
  }
348
378
 
349
- function install({ registry, resourceId, authorityEpoch, headVersion, snapshotBase64, snapshotChecksum }) {
379
+ function install({
380
+ registry,
381
+ resourceId,
382
+ authorityEpoch,
383
+ headVersion,
384
+ snapshotBase64,
385
+ snapshotChecksum,
386
+ bootstrapRecords = null,
387
+ }) {
350
388
  const currentIdentity = identity();
351
389
  if (!currentIdentity.enabled || resourceId !== currentIdentity.resourceId) {
352
390
  throw new Error('cloud entity registry identity does not belong to this runtime user');
@@ -355,8 +393,86 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
355
393
  const checksum = crypto.createHash('sha256').update(bytes).digest('hex');
356
394
  if (checksum !== snapshotChecksum) throw new Error('cloud entity snapshot checksum mismatch');
357
395
  const snapshot = parseSnapshot(bytes);
396
+ if (!Number.isInteger(Number(authorityEpoch)) || Number(authorityEpoch) < 1
397
+ || !Number.isInteger(Number(headVersion)) || Number(headVersion) < 0) {
398
+ throw new Error('entity cloud registry authority is invalid');
399
+ }
358
400
  const local = snapshotFromRecords(registry.syncRecords());
359
- const bootstrapping = replica()?.state === 'bootstrapping';
401
+ const current = replica();
402
+ const bootstrapping = current?.state === 'bootstrapping';
403
+ const reconciling = current?.state === BOOTSTRAP_RECONCILIATION_STATE;
404
+ const trustedBootstrap = Array.isArray(bootstrapRecords)
405
+ ? snapshotFromRecords(bootstrapRecords).records
406
+ : null;
407
+ const emptyCloudGraph = snapshot.records.length === 0 && Number(headVersion) === 0;
408
+
409
+ // A pre-release client could create the automatic entity resource before
410
+ // the user-home handshake was complete, leaving a real private resource
411
+ // with an empty v0 graph. It is safe to repair only if the local graph is
412
+ // exactly the already-verified declared user home. This is not a general
413
+ // local-wins merge: user-selected ground remains refused until an active
414
+ // authoritative snapshot exists.
415
+ if (!current && emptyCloudGraph && trustedBootstrap && sameRecords(local.records, trustedBootstrap)) {
416
+ const next = {
417
+ resourceId,
418
+ deviceId: currentIdentity.deviceId,
419
+ authorityEpoch: Number(authorityEpoch),
420
+ headVersion: Number(headVersion),
421
+ state: BOOTSTRAP_RECONCILIATION_STATE,
422
+ snapshotCutoff: null,
423
+ };
424
+ db.transaction(() => {
425
+ saveReplica(next);
426
+ replaceCatalog({
427
+ resourceId,
428
+ authorityEpoch: Number(authorityEpoch),
429
+ headVersion: Number(headVersion),
430
+ records: snapshot.records,
431
+ });
432
+ enqueueBootstrapReconciliation(trustedBootstrap, next);
433
+ })();
434
+ return {
435
+ resourceId,
436
+ records: snapshot.records.length,
437
+ headVersion: Number(headVersion),
438
+ snapshotVersion: Number(headVersion),
439
+ reconciling: true,
440
+ };
441
+ }
442
+
443
+ if (reconciling) {
444
+ if (!trustedBootstrap || !sameRecords(local.records, trustedBootstrap)) {
445
+ throw new Error('entity bootstrap reconciliation no longer matches the verified user-home graph');
446
+ }
447
+ if (current.resourceId !== resourceId || Number(authorityEpoch) !== current.authorityEpoch
448
+ || Number(headVersion) < current.headVersion || !recordsAreSubset(snapshot.records, trustedBootstrap)) {
449
+ throw new Error('entity bootstrap reconciliation received an invalid cloud head');
450
+ }
451
+ const unsettled = db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations WHERE resource_id = ? AND mutation_state <> \'synced\'')
452
+ .get(resourceId);
453
+ const complete = Number(unsettled.count) === 0 && sameRecords(snapshot.records, trustedBootstrap);
454
+ replaceCatalog({
455
+ resourceId,
456
+ authorityEpoch: Number(authorityEpoch),
457
+ headVersion: Number(headVersion),
458
+ records: snapshot.records,
459
+ });
460
+ saveReplica({
461
+ resourceId,
462
+ deviceId: currentIdentity.deviceId,
463
+ authorityEpoch: Number(authorityEpoch),
464
+ headVersion: Number(headVersion),
465
+ state: complete ? 'active' : BOOTSTRAP_RECONCILIATION_STATE,
466
+ snapshotCutoff: null,
467
+ });
468
+ return {
469
+ resourceId,
470
+ records: snapshot.records.length,
471
+ headVersion: Number(headVersion),
472
+ snapshotVersion: Number(headVersion),
473
+ reconciling: !complete,
474
+ };
475
+ }
360
476
  if (bootstrapping && !sameRecord(local, snapshot)) {
361
477
  throw new Error('first cloud entity snapshot does not match the locally bootstrapped identity graph');
362
478
  }
@@ -499,7 +615,7 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
499
615
 
500
616
  function claim({ limit = 16, leaseMs = 30_000 } = {}) {
501
617
  const current = replica();
502
- if (!current || current.state !== 'active') return [];
618
+ if (!current || !['active', BOOTSTRAP_RECONCILIATION_STATE].includes(current.state)) return [];
503
619
  const boundedLimit = Math.max(1, Math.min(Number(limit) || 16, 100));
504
620
  const leaseUntil = new Date(Date.now() + Math.max(1_000, Number(leaseMs) || 30_000)).toISOString();
505
621
  const started = now();
@@ -547,7 +663,7 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
547
663
 
548
664
  function claimContent({ leaseMs = 30_000 } = {}) {
549
665
  const current = replica();
550
- if (!current || current.state !== 'active' || !content) return null;
666
+ if (!current || !['active', BOOTSTRAP_RECONCILIATION_STATE].includes(current.state) || !content) return null;
551
667
  // Later registrations journal their record transactionally in SQLite.
552
668
  // Derive its content-upload cursor only after that commit exists, so a
553
669
  // failed transaction cannot publish a private object no entity names.
@@ -587,7 +703,15 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
587
703
  const row = db.prepare('SELECT * FROM entity_cloud_mutations WHERE resource_id = ? AND mutation_id = ?')
588
704
  .get(resourceId, mutationId);
589
705
  if (!row) throw new Error(`unknown entity cloud mutation ${mutationId}`);
590
- if (row.mutation_state === 'synced') return { duplicate: true, version: Number(row.official_version) };
706
+ if (row.mutation_state === 'synced') {
707
+ const unsettled = db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations WHERE resource_id = ? AND mutation_state <> \'synced\'')
708
+ .get(resourceId);
709
+ return {
710
+ duplicate: true,
711
+ version: Number(row.official_version),
712
+ requiresSnapshot: current.state !== BOOTSTRAP_RECONCILIATION_STATE || Number(unsettled.count) === 0,
713
+ };
714
+ }
591
715
  if (!['pending', 'syncing'].includes(row.mutation_state)) throw new Error(`entity cloud mutation ${mutationId} cannot sync from ${row.mutation_state}`);
592
716
  db.prepare(`
593
717
  UPDATE entity_cloud_mutations
@@ -598,7 +722,13 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
598
722
  if (Number(version) > current.headVersion) {
599
723
  saveReplica({ ...current, headVersion: Number(version) });
600
724
  }
601
- return { duplicate: false, version: Number(version) };
725
+ const unsettled = db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations WHERE resource_id = ? AND mutation_state <> \'synced\'')
726
+ .get(resourceId);
727
+ return {
728
+ duplicate: false,
729
+ version: Number(version),
730
+ requiresSnapshot: current.state !== BOOTSTRAP_RECONCILIATION_STATE || Number(unsettled.count) === 0,
731
+ };
602
732
  })();
603
733
  }
604
734
 
@@ -625,7 +755,9 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
625
755
 
626
756
  function ingest({ registry, envelope, version, committedAt }) {
627
757
  const current = replica();
628
- if (!current || current.state !== 'active') throw new Error('entity cloud registry is not active');
758
+ if (!current || !['active', BOOTSTRAP_RECONCILIATION_STATE].includes(current.state)) {
759
+ throw new Error('entity cloud registry is not active');
760
+ }
629
761
  if (envelope?.resourceId !== current.resourceId
630
762
  || envelope?.contract !== ENTITY_CLOUD_CONTRACT
631
763
  || Number(envelope?.schemaVersion) !== ENTITY_CLOUD_SCHEMA_VERSION
@@ -644,6 +776,9 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
644
776
  committedAt,
645
777
  });
646
778
  }
779
+ if (current.state === BOOTSTRAP_RECONCILIATION_STATE) {
780
+ throw new Error('entity bootstrap reconciliation cannot accept an unrelated remote mutation');
781
+ }
647
782
  // Remote mutations update the cloud-only catalog. They never create a
648
783
  // local watcher; `amalgm add` alone turns catalog identity into ground.
649
784
  let local;
@@ -259,6 +259,31 @@ function createRegistrationService({
259
259
  };
260
260
  }
261
261
 
262
+ /** The only local graph allowed to repair an old empty entity resource is
263
+ * the exact home that the user-home handshake has already verified. Any
264
+ * extra local entity would be user-selected ground and must wait for an
265
+ * active entity-cloud authority instead. */
266
+ async function exactUserHomeCloudRecords() {
267
+ const home = await bootstrapUserHome();
268
+ const expected = new Map();
269
+ for (const entry of [...home.roots, ...home.exactManaged]) {
270
+ expected.set(entry.record.uuid, entry.record);
271
+ }
272
+ const records = boundary.current.registry.syncRecords();
273
+ if (records.length !== expected.size) {
274
+ throw new Error('local entity graph contains ground outside the declared user home');
275
+ }
276
+ for (const record of records) {
277
+ const declared = expected.get(record.uuid);
278
+ if (!declared || ![
279
+ 'uuid', 'type', 'parentUUID', 'name', 'status', 'version', 'payloadVersion',
280
+ ].every((field) => record[field] === declared[field])) {
281
+ throw new Error('local entity graph does not exactly match the declared user home');
282
+ }
283
+ }
284
+ return records;
285
+ }
286
+
262
287
  /** Materialize one already-pinned cloud tree into a fresh destination, then
263
288
  * bind its cloud UUID graph to Watch. The destination is never merged with
264
289
  * user ground: bytes are assembled in a sibling staging path and renamed
@@ -375,6 +400,7 @@ function createRegistrationService({
375
400
  },
376
401
  advanceMaterializations,
377
402
  bootstrapUserHome,
403
+ exactUserHomeCloudRecords,
378
404
  entityCloud,
379
405
  entityContent,
380
406
  /**
@@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS entity_cloud_replica (
69
69
  device_id TEXT NOT NULL,
70
70
  authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
71
71
  head_version INTEGER NOT NULL CHECK (head_version >= 0),
72
- state TEXT NOT NULL CHECK (state IN ('active', 'bootstrapping')),
72
+ state TEXT NOT NULL CHECK (state IN ('active', 'bootstrapping', 'reconciling-bootstrap')),
73
73
  snapshot_cutoff INTEGER,
74
74
  updated_at TEXT NOT NULL
75
75
  );
@@ -140,12 +140,17 @@ const TABLES = [...REPO_STATE_TABLES, ...CLOUD_TABLES, ...MATERIALIZATION_TABLES
140
140
  // be known locally before any observer is bound, so they cannot masquerade
141
141
  // as local registered ground. Generation 6 records the one snapshot version
142
142
  // that populated that catalog; a later mutation tail cannot pose as a fresh
143
- // active head for `amalgm add`. Generations 2 and 3 are
143
+ // active head for `amalgm add`. Generation 7 adds the explicit
144
+ // `reconciling-bootstrap` state: an old, empty entity resource may be
145
+ // populated only from the already-verified exact user-home graph, and cannot
146
+ // admit ordinary registration or materialization until its ordered outbox
147
+ // becomes an active cloud snapshot. Generations 2 and 3 are
144
148
  // additive predecessors: migrations only add tables beside existing identity
145
149
  // records, never rewrite UUIDs or evidence bindings. The per-tree era never
146
150
  // stamped a version and reads 0 — an older generation, refused distinctly
147
151
  // from loss until an explicit migration exists.
148
- const GENERATION = 6;
152
+ const GENERATION = 7;
153
+ const BOOTSTRAP_RECONCILIATION_GENERATION = 6;
149
154
  const MATERIALIZATION_GENERATION = 5;
150
155
  const CLOUD_GENERATION = 4;
151
156
  const REPO_STATE_GENERATION = 3;
@@ -173,6 +178,9 @@ function inspect(db) {
173
178
 
174
179
  function isMigratableGenerationShape(db, inspection) {
175
180
  const generation = db.pragma('user_version', { simple: true });
181
+ if (generation === BOOTSTRAP_RECONCILIATION_GENERATION) {
182
+ return inspection.missing.length === 0;
183
+ }
176
184
  if (generation === MATERIALIZATION_GENERATION) {
177
185
  return inspection.missing.length === 1
178
186
  && inspection.missing[0] === 'entity_cloud_catalog_state'
@@ -196,12 +204,45 @@ function isMigratableGenerationShape(db, inspection) {
196
204
  && REPO_STATE_TABLES.every((name) => !inspection.missing.includes(name));
197
205
  }
198
206
 
207
+ /** SQLite cannot widen a CHECK constraint in place. This is a schema
208
+ * migration—not a state rewrite: every old row is copied byte-for-byte into
209
+ * the wider replica table in one transaction. */
210
+ function migrateBootstrapReconciliationState(db) {
211
+ db.transaction(() => {
212
+ db.exec(`
213
+ ALTER TABLE entity_cloud_replica RENAME TO entity_cloud_replica_v6;
214
+ CREATE TABLE entity_cloud_replica (
215
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
216
+ resource_id TEXT NOT NULL,
217
+ device_id TEXT NOT NULL,
218
+ authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
219
+ head_version INTEGER NOT NULL CHECK (head_version >= 0),
220
+ state TEXT NOT NULL CHECK (state IN ('active', 'bootstrapping', 'reconciling-bootstrap')),
221
+ snapshot_cutoff INTEGER,
222
+ updated_at TEXT NOT NULL
223
+ );
224
+ INSERT INTO entity_cloud_replica (
225
+ singleton, resource_id, device_id, authority_epoch, head_version,
226
+ state, snapshot_cutoff, updated_at
227
+ )
228
+ SELECT
229
+ singleton, resource_id, device_id, authority_epoch, head_version,
230
+ state, snapshot_cutoff, updated_at
231
+ FROM entity_cloud_replica_v6;
232
+ DROP TABLE entity_cloud_replica_v6;
233
+ `);
234
+ })();
235
+ }
236
+
199
237
  function createStore(db) {
200
238
  const generation = db.pragma('user_version', { simple: true });
201
239
  const inspection = inspect(db);
202
240
  if (!inspection.fresh && generation !== GENERATION && !isMigratableGenerationShape(db, inspection)) {
203
241
  throw new Error(`identity registry is schema generation ${generation}, this code speaks ${GENERATION} — migration is an explicit act, never a silent table creation`);
204
242
  }
243
+ if (generation === BOOTSTRAP_RECONCILIATION_GENERATION) {
244
+ migrateBootstrapReconciliationState(db);
245
+ }
205
246
  db.exec(SCHEMA);
206
247
  db.pragma(`user_version = ${GENERATION}`);
207
248
 
@@ -146,6 +146,20 @@ async function handleEntityRoutes(ctx) {
146
146
  const body = await ctx.readJsonBody();
147
147
  const service = registrationService();
148
148
  try {
149
+ const existing = service.entityCloud.replica();
150
+ let bootstrapRecords = null;
151
+ try {
152
+ const snapshot = JSON.parse(Buffer.from(String(body?.snapshotBase64 || ''), 'base64').toString('utf8'));
153
+ if (
154
+ existing?.state === 'reconciling-bootstrap'
155
+ || (Number(body?.headVersion) === 0 && Array.isArray(snapshot?.records) && snapshot.records.length === 0)
156
+ ) {
157
+ bootstrapRecords = await service.exactUserHomeCloudRecords();
158
+ }
159
+ } catch {
160
+ // install performs the authoritative checksum and shape validation;
161
+ // this lookahead only decides whether exact bootstrap proof is needed.
162
+ }
149
163
  ctx.sendJson(200, service.entityCloud.install({
150
164
  registry: service.registration.registry,
151
165
  resourceId: body?.resourceId,
@@ -153,6 +167,7 @@ async function handleEntityRoutes(ctx) {
153
167
  headVersion: body?.headVersion,
154
168
  snapshotBase64: body?.snapshotBase64,
155
169
  snapshotChecksum: body?.snapshotChecksum,
170
+ bootstrapRecords,
156
171
  }));
157
172
  } catch (error) {
158
173
  ctx.sendJson(409, { error: error.message });
@@ -76,6 +76,69 @@ 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('only an exact verified bootstrap graph can repair a legacy empty entity resource', () => {
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: 'agents' });
84
+ const resourceId = cloud.identity().resourceId;
85
+ const empty = Buffer.from(JSON.stringify(snapshotFromRecords([])), 'utf8');
86
+
87
+ const first = cloud.install({
88
+ registry,
89
+ resourceId,
90
+ authorityEpoch: 1,
91
+ headVersion: 0,
92
+ snapshotBase64: empty.toString('base64'),
93
+ snapshotChecksum: require('crypto').createHash('sha256').update(empty).digest('hex'),
94
+ bootstrapRecords: registry.syncRecords(),
95
+ });
96
+ assert.equal(first.reconciling, true);
97
+ assert.equal(cloud.replica().state, 'reconciling-bootstrap');
98
+ assert.throws(() => cloud.assertRegistrationReady(), /not ready/,
99
+ 'ordinary register stays closed until the ordered repair reaches a full cloud head');
100
+
101
+ const claimed = cloud.claim();
102
+ assert.equal(claimed.length, 1);
103
+ assert.equal(claimed[0].baseVersion, 0);
104
+ assert.equal(cloud.acknowledge({
105
+ resourceId,
106
+ mutationId: claimed[0].mutationId,
107
+ version: 1,
108
+ committedAt: '2026-08-05T00:00:00.000Z',
109
+ }).requiresSnapshot, true, 'only the final ordered acknowledgement asks for a usable snapshot');
110
+
111
+ const completed = snapshotFromRecords(registry.syncRecords());
112
+ const final = cloud.install({
113
+ registry,
114
+ resourceId,
115
+ authorityEpoch: 1,
116
+ headVersion: 1,
117
+ snapshotBase64: Buffer.from(JSON.stringify(completed), 'utf8').toString('base64'),
118
+ snapshotChecksum: require('crypto').createHash('sha256').update(JSON.stringify(completed)).digest('hex'),
119
+ bootstrapRecords: registry.syncRecords(),
120
+ });
121
+ assert.equal(final.reconciling, false);
122
+ assert.equal(cloud.replica().state, 'active');
123
+ assert.doesNotThrow(() => cloud.assertRegistrationReady());
124
+
125
+ const unsafeDb = new Database(':memory:');
126
+ const unsafeCloud = createEntityCloud({ db: unsafeDb, userId: 'user-1', deviceId: 'unsafe-device' });
127
+ const unsafeRegistry = createRegistry({ db: unsafeDb, ephemeral: true, emit: () => {}, journal: unsafeCloud.journal });
128
+ const safeRoot = unsafeRegistry.enrollRoot({ type: 'workspace', name: 'agents' });
129
+ unsafeRegistry.create({ type: 'file.text', parentUUID: safeRoot.uuid, name: 'not-a-default.md', payloadVersion: 'a'.repeat(64) });
130
+ assert.throws(() => unsafeCloud.install({
131
+ registry: unsafeRegistry,
132
+ resourceId,
133
+ authorityEpoch: 1,
134
+ headVersion: 0,
135
+ snapshotBase64: empty.toString('base64'),
136
+ snapshotChecksum: require('crypto').createHash('sha256').update(empty).digest('hex'),
137
+ bootstrapRecords: unsafeRegistry.syncRecords().filter((record) => record.uuid === safeRoot.uuid),
138
+ }), /conflicts with locally bound ground/,
139
+ 'an empty cloud resource never authorizes arbitrary local ground');
140
+ });
141
+
79
142
  test('a compacted remote snapshot enters the cloud catalog, not locally watched identity, and add claims its immutable content', () => {
80
143
  const sourceDb = new Database(':memory:');
81
144
  const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
@@ -97,7 +97,7 @@ test('the additive entity-cloud migration preserves a generation-2 registry', ()
97
97
 
98
98
  const registry = createRegistry({ db, ephemeral: true, emit: () => {} });
99
99
  assert.equal(registry.epoch(), 'legacy-epoch', 'the existing identity epoch survives');
100
- assert.equal(db.pragma('user_version', { simple: true }), 6);
100
+ assert.equal(db.pragma('user_version', { simple: true }), 7);
101
101
  assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'repo_states'").get(),
102
102
  'the repo-state document table is added without replacing identity');
103
103
  assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'entity_cloud_mutations'").get(),
@@ -114,11 +114,45 @@ test('the active-snapshot marker migrates beside an existing materialization cat
114
114
 
115
115
  const reopened = createRegistry({ db, ephemeral: true, emit: () => {} });
116
116
  assert.ok(reopened.epoch(), 'the previous identity database remains usable');
117
- assert.equal(db.pragma('user_version', { simple: true }), 6);
117
+ assert.equal(db.pragma('user_version', { simple: true }), 7);
118
118
  assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'entity_cloud_catalog_state'").get(),
119
119
  'the separate pinned-snapshot marker is created without rewriting identities');
120
120
  });
121
121
 
122
+ test('the bootstrap-reconciliation migration preserves a generation-6 cloud replica', () => {
123
+ const db = new Database(':memory:');
124
+ createRegistry({ db, ephemeral: true, emit: () => {} });
125
+ db.exec([
126
+ 'ALTER TABLE entity_cloud_replica RENAME TO entity_cloud_replica_v7;',
127
+ 'CREATE TABLE entity_cloud_replica (',
128
+ ' singleton INTEGER PRIMARY KEY CHECK (singleton = 1),',
129
+ ' resource_id TEXT NOT NULL,',
130
+ ' device_id TEXT NOT NULL,',
131
+ ' authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),',
132
+ ' head_version INTEGER NOT NULL CHECK (head_version >= 0),',
133
+ " state TEXT NOT NULL CHECK (state IN ('active', 'bootstrapping')),",
134
+ ' snapshot_cutoff INTEGER,',
135
+ ' updated_at TEXT NOT NULL',
136
+ ');',
137
+ 'INSERT INTO entity_cloud_replica SELECT * FROM entity_cloud_replica_v7;',
138
+ 'DROP TABLE entity_cloud_replica_v7;',
139
+ ].join('\n'));
140
+ db.prepare([
141
+ 'INSERT INTO entity_cloud_replica (',
142
+ ' singleton, resource_id, device_id, authority_epoch, head_version,',
143
+ ' state, snapshot_cutoff, updated_at',
144
+ ") VALUES (1, 'pvr_legacy', 'device', 1, 4, 'active', NULL, 'now')",
145
+ ].join('\n')).run();
146
+ db.pragma('user_version = 6');
147
+
148
+ createRegistry({ db, ephemeral: true, emit: () => {} });
149
+ assert.equal(db.pragma('user_version', { simple: true }), 7);
150
+ assert.equal(db.prepare('SELECT state FROM entity_cloud_replica WHERE singleton = 1').get().state, 'active');
151
+ assert.doesNotThrow(() => db.prepare(
152
+ "UPDATE entity_cloud_replica SET state = 'reconciling-bootstrap' WHERE singleton = 1",
153
+ ).run(), 'the widened state is durable after migration');
154
+ });
155
+
122
156
  test('an entity commit and its cloud delivery intent are one SQLite transaction', () => {
123
157
  const db = new Database(':memory:');
124
158
  let nextMutation = 0;