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 +1 -1
- package/runtime/scripts/amalgm-mcp/registration/index.js +6 -2
- package/runtime/scripts/amalgm-mcp/registration/service.js +82 -13
- package/runtime/scripts/amalgm-mcp/registry/index.js +61 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +57 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +56 -0
package/package.json
CHANGED
|
@@ -505,8 +505,12 @@ function createRegistration(options) {
|
|
|
505
505
|
}
|
|
506
506
|
const covering = treeRootCovering(ground);
|
|
507
507
|
if (covering) {
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
252
|
-
const entry = declaration.roots[index];
|
|
268
|
+
let roots = declaration.roots.map((entry, index) => {
|
|
253
269
|
const added = addedRoots[index];
|
|
254
|
-
|
|
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
|
-
|
|
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'),
|