amalgm 0.1.255 → 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 +1 -1
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +27 -10
- package/runtime/scripts/amalgm-mcp/registration/service.js +6 -9
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +7 -4
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +21 -14
- package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +36 -2
package/package.json
CHANGED
|
@@ -424,14 +424,23 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
519
|
-
throw new Error('entity bootstrap reconciliation
|
|
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
|
|
551
|
-
|
|
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
|
|
556
|
-
|
|
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
|
}
|
|
@@ -381,10 +381,10 @@ function createRegistrationService({
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
/** The only local graph
|
|
385
|
-
* the exact home that the user-home handshake has already verified.
|
|
386
|
-
*
|
|
387
|
-
*
|
|
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. */
|
|
388
388
|
async function exactUserHomeCloudRecords() {
|
|
389
389
|
const home = await bootstrapUserHome();
|
|
390
390
|
const expected = new Map();
|
|
@@ -395,12 +395,9 @@ function createRegistrationService({
|
|
|
395
395
|
// containers, with the registry's sync representation.
|
|
396
396
|
expected.set(entry.record.uuid, boundary.current.registry.syncRecord(entry.record.uuid));
|
|
397
397
|
}
|
|
398
|
-
const records =
|
|
399
|
-
if (records.length !== expected.size) {
|
|
400
|
-
throw new Error('local entity graph contains ground outside the declared user home');
|
|
401
|
-
}
|
|
398
|
+
const records = [...expected.values()];
|
|
402
399
|
for (const record of records) {
|
|
403
|
-
const declared =
|
|
400
|
+
const declared = boundary.current.registry.syncRecord(record.uuid);
|
|
404
401
|
if (!declared || ![
|
|
405
402
|
'uuid', 'type', 'parentUUID', 'name', 'status', 'version', 'payloadVersion',
|
|
406
403
|
].every((field) => record[field] === declared[field])) {
|
|
@@ -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
|
-
|
|
140
|
-
|
|
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 === '
|
|
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('
|
|
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
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const safeRoot =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
188
|
-
})
|
|
189
|
-
|
|
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'),
|
|
@@ -326,7 +360,7 @@ test('a signed-in register call writes every entity delivery intent in identity.
|
|
|
326
360
|
cloudDeviceId: 'computer-cloud-register',
|
|
327
361
|
});
|
|
328
362
|
const home = await service.bootstrapUserHome();
|
|
329
|
-
const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
|
|
363
|
+
const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
|
|
330
364
|
service.entityCloud.install({
|
|
331
365
|
registry: service.registration.registry,
|
|
332
366
|
resourceId: bootstrap.resourceId,
|
|
@@ -366,7 +400,7 @@ test('a large signed-in register call publishes one complete cloud graph before
|
|
|
366
400
|
cloudUserId: 'user-cloud-register-large',
|
|
367
401
|
cloudDeviceId: 'computer-cloud-register-large',
|
|
368
402
|
});
|
|
369
|
-
const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
|
|
403
|
+
const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
|
|
370
404
|
service.entityCloud.install({
|
|
371
405
|
registry: service.registration.registry,
|
|
372
406
|
resourceId: bootstrap.resourceId,
|