amalgm 0.1.257 → 0.1.259
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/lib/cli.js +207 -3
- package/lib/layout.js +30 -9
- package/lib/shared-realtime-tunnel.js +12 -14
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/lib/layout.js +32 -9
- package/runtime/scripts/amalgm-mcp/observer/index.js +65 -4
- package/runtime/scripts/amalgm-mcp/observer/scan.js +60 -1
- package/runtime/scripts/amalgm-mcp/observer/store.js +43 -8
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +186 -32
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +33 -6
- package/runtime/scripts/amalgm-mcp/registration/index.js +21 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +117 -11
- package/runtime/scripts/amalgm-mcp/registry/index.js +1 -1
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +16 -8
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +6 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +67 -8
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +85 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +95 -3
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +21 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +3 -6
- package/runtime/scripts/amalgm-mcp/tests/toolbox-artifacts.test.js +88 -0
- package/runtime/scripts/amalgm-mcp/tests/toolbox-cloud-registration.test.js +91 -0
- package/runtime/scripts/amalgm-mcp/toolbox/artifacts.js +225 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +59 -10
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +14 -5
package/lib/cli.js
CHANGED
|
@@ -120,7 +120,10 @@ function usage() {
|
|
|
120
120
|
' amalgm stop',
|
|
121
121
|
' amalgm register [path ...] Register local ground (default: current directory)',
|
|
122
122
|
' amalgm add <entity-uuid> [directory] Add a cloud-backed tree to this machine',
|
|
123
|
-
' amalgm
|
|
123
|
+
' amalgm ls [entity] List the verified cloud workspace',
|
|
124
|
+
' amalgm cd <entity> Resolve a materialized cloud directory',
|
|
125
|
+
' amalgm status [path-or-uuid] Show cloud-backed entity and watcher state',
|
|
126
|
+
' amalgm shell init zsh Enable `amalgm cd` in the current shell',
|
|
124
127
|
' amalgm doctor',
|
|
125
128
|
' amalgm service [install|start|stop|status|uninstall] [--mode auto|systemd|launchd|windows|portable]',
|
|
126
129
|
' amalgm update [--tag latest|canary]',
|
|
@@ -1112,7 +1115,7 @@ async function waitForServices(timeoutMs) {
|
|
|
1112
1115
|
return latest;
|
|
1113
1116
|
}
|
|
1114
1117
|
|
|
1115
|
-
async function
|
|
1118
|
+
async function runtimeStatus() {
|
|
1116
1119
|
const pid = readPid();
|
|
1117
1120
|
const record = loadComputerRecord({ migrate: true });
|
|
1118
1121
|
const runtimeState = readJson(RUNTIME_STATE_FILE, null);
|
|
@@ -1177,6 +1180,203 @@ async function status() {
|
|
|
1177
1180
|
}
|
|
1178
1181
|
}
|
|
1179
1182
|
|
|
1183
|
+
/** One runtime read for every navigation command. The endpoint owns facts;
|
|
1184
|
+
* these commands only select and render them, so navigating never becomes a
|
|
1185
|
+
* backdoor mutation or cloud refresh. */
|
|
1186
|
+
async function readEntityStatus() {
|
|
1187
|
+
const port = servicePorts().find(([name]) => name === 'amalgm-mcp')?.[1];
|
|
1188
|
+
let answer = null;
|
|
1189
|
+
if (port) {
|
|
1190
|
+
try {
|
|
1191
|
+
answer = await runtimeGet(port, '/entities/status');
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
if (error.code !== 'ECONNREFUSED') throw error;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (!answer) {
|
|
1197
|
+
console.error('Amalgm runtime is not running. Start it with `amalgm start`.');
|
|
1198
|
+
process.exitCode = 1;
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
if (answer.status !== 200 || !answer.payload) {
|
|
1202
|
+
console.error(`Error: ${answer.payload?.error || `runtime responded ${answer.status}`}`);
|
|
1203
|
+
process.exitCode = 1;
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
return answer.payload;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** A selector is deliberately exact: full UUID, local address, canonical
|
|
1210
|
+
* cloud path, or an unambiguous entity name. A guessed fuzzy match would
|
|
1211
|
+
* make a cloud navigation command choose the wrong ground. */
|
|
1212
|
+
function selectEntity(status, selector) {
|
|
1213
|
+
const entities = status.entities || [];
|
|
1214
|
+
if (!selector) return null;
|
|
1215
|
+
const uuid = selector && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(selector)
|
|
1216
|
+
? selector.toLowerCase()
|
|
1217
|
+
: null;
|
|
1218
|
+
const localPath = selector && !uuid ? path.resolve(selector) : null;
|
|
1219
|
+
const cloudPath = uuid ? null : String(selector).replace(/^\/+|\/+$/g, '');
|
|
1220
|
+
const matches = entities.filter((entity) => entity.uuid === uuid
|
|
1221
|
+
|| entity.localPath === localPath
|
|
1222
|
+
|| entity.cloudPath === cloudPath
|
|
1223
|
+
|| entity.name === selector);
|
|
1224
|
+
if (matches.length === 0) throw new Error(`No cloud-backed entity matches ${selector}.`);
|
|
1225
|
+
if (matches.length > 1) {
|
|
1226
|
+
throw new Error(`Entity selector ${selector} is ambiguous: ${matches.map((entity) => entity.cloudPath || entity.uuid).join(', ')}. Use a UUID or canonical cloud path.`);
|
|
1227
|
+
}
|
|
1228
|
+
return matches[0];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function requireCurrentCloudWorkspace(status) {
|
|
1232
|
+
const cloud = status.cloud || {};
|
|
1233
|
+
if (!cloud.replica) {
|
|
1234
|
+
throw new Error(cloud.identity?.enabled
|
|
1235
|
+
? 'Cloud workspace is initializing; wait for the signed-in runtime to finish its handshake.'
|
|
1236
|
+
: 'Cloud workspace is unavailable because this machine is not signed in.');
|
|
1237
|
+
}
|
|
1238
|
+
if (!cloud.catalog?.current) {
|
|
1239
|
+
throw new Error('Cloud workspace is catching up; wait for the active verified cloud head before listing it.');
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function printEntityLine(entity) {
|
|
1244
|
+
const where = entity.localPath ? 'local' : 'cloud only';
|
|
1245
|
+
console.log(` ${entity.name}\t${entity.type}\t${entity.status}\t${where}\t${entity.uuid}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** `amalgm status [path-or-uuid]` is a read-only health view. Daemon status
|
|
1249
|
+
* remains available as `amalgm service status`. */
|
|
1250
|
+
async function entityStatus(positionals) {
|
|
1251
|
+
if (positionals.length > 1) throw new Error('Usage: amalgm status [path-or-uuid]');
|
|
1252
|
+
const status = await readEntityStatus();
|
|
1253
|
+
if (!status) return;
|
|
1254
|
+
const selector = positionals[0] || null;
|
|
1255
|
+
let entities = status.entities || [];
|
|
1256
|
+
if (selector) {
|
|
1257
|
+
try {
|
|
1258
|
+
entities = [selectEntity(status, selector)];
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
console.error(error.message);
|
|
1261
|
+
process.exitCode = 1;
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const cloud = status.cloud || {};
|
|
1267
|
+
const replica = cloud.replica;
|
|
1268
|
+
if (!replica) {
|
|
1269
|
+
console.log(`Cloud: ${cloud.identity?.enabled ? 'initializing' : 'not signed in'}`);
|
|
1270
|
+
} else {
|
|
1271
|
+
console.log(`Cloud: ${cloud.catalog?.current ? 'current' : 'catching up'} (head ${replica.headVersion})`);
|
|
1272
|
+
}
|
|
1273
|
+
const outbox = cloud.outbox || {};
|
|
1274
|
+
console.log(`Delivery: ${outbox.pending || 0} pending, ${outbox.syncing || 0} sending, ${outbox.rejected || 0} rejected`);
|
|
1275
|
+
const jobs = cloud.materializations || {};
|
|
1276
|
+
if ((jobs.waitingForContent || 0) + (jobs.materializing || 0) + (jobs.failed || 0) > 0) {
|
|
1277
|
+
console.log(`Materializations: ${jobs.waitingForContent || 0} waiting, ${jobs.materializing || 0} applying, ${jobs.failed || 0} failed`);
|
|
1278
|
+
}
|
|
1279
|
+
if (entities.length === 0) {
|
|
1280
|
+
console.log('Entities: none on this cloud replica.');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
console.log('Entities:');
|
|
1284
|
+
for (const entity of entities) {
|
|
1285
|
+
console.log(` ${entity.uuid} ${entity.type} ${entity.status}`);
|
|
1286
|
+
if (entity.localPath) console.log(` Local: ${entity.localPath}`);
|
|
1287
|
+
else console.log(' Local: not materialized on this machine');
|
|
1288
|
+
if (entity.root) {
|
|
1289
|
+
const watcher = entity.root.watching && !entity.root.degraded ? 'on' : 'unavailable';
|
|
1290
|
+
console.log(` Watching: ${watcher}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** `amalgm ls [entity]` lists the canonical cloud graph, never this
|
|
1296
|
+
* machine's incidental directory. A stale catalog is useful in `status`,
|
|
1297
|
+
* but is never presented as a navigable workspace here. */
|
|
1298
|
+
async function entityList(positionals) {
|
|
1299
|
+
if (positionals.length > 1) throw new Error('Usage: amalgm ls [entity]');
|
|
1300
|
+
const status = await readEntityStatus();
|
|
1301
|
+
if (!status) return;
|
|
1302
|
+
try {
|
|
1303
|
+
requireCurrentCloudWorkspace(status);
|
|
1304
|
+
const selected = positionals[0] ? selectEntity(status, positionals[0]) : null;
|
|
1305
|
+
if (selected && !selected.catalogued) {
|
|
1306
|
+
throw new Error(`Entity ${selected.uuid} is not in the verified cloud workspace yet.`);
|
|
1307
|
+
}
|
|
1308
|
+
const entities = (status.entities || []).filter((entity) => entity.catalogued
|
|
1309
|
+
&& entity.status === 'active'
|
|
1310
|
+
&& (selected ? entity.parentUUID === selected.uuid : entity.parentUUID === null))
|
|
1311
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
1312
|
+
if (selected) console.log(`${selected.cloudPath || selected.uuid}:`);
|
|
1313
|
+
console.log('NAME\tTYPE\tSTATE\tLOCATION\tUUID');
|
|
1314
|
+
for (const entity of entities) printEntityLine(entity);
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
console.error(error.message);
|
|
1317
|
+
process.exitCode = 1;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function resolveEntityDirectory(positionals) {
|
|
1322
|
+
if (positionals.length !== 1) throw new Error('Usage: amalgm cd <entity>');
|
|
1323
|
+
const status = await readEntityStatus();
|
|
1324
|
+
if (!status) return null;
|
|
1325
|
+
const entity = selectEntity(status, positionals[0]);
|
|
1326
|
+
if (!entity.localPath) {
|
|
1327
|
+
throw new Error(`Cloud entity ${entity.cloudPath || entity.uuid} is not materialized on this machine. Run \`amalgm add ${entity.uuid} <directory>\` first.`);
|
|
1328
|
+
}
|
|
1329
|
+
let stat;
|
|
1330
|
+
try { stat = fs.statSync(entity.localPath); } catch {
|
|
1331
|
+
throw new Error(`Local binding for ${entity.cloudPath || entity.uuid} is unavailable at ${entity.localPath}. Run \`amalgm status ${entity.uuid}\` for details.`);
|
|
1332
|
+
}
|
|
1333
|
+
if (!stat.isDirectory()) {
|
|
1334
|
+
throw new Error(`${entity.cloudPath || entity.uuid} is a ${entity.type}, not a directory.`);
|
|
1335
|
+
}
|
|
1336
|
+
return entity.localPath;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** A standalone process cannot change the invoking shell. This direct form
|
|
1340
|
+
* resolves a directory honestly and points to the opt-in shell integration;
|
|
1341
|
+
* the shell function below intercepts the same spelling and performs cd. */
|
|
1342
|
+
async function entityCd(positionals) {
|
|
1343
|
+
try {
|
|
1344
|
+
const target = await resolveEntityDirectory(positionals);
|
|
1345
|
+
if (!target) return;
|
|
1346
|
+
console.log(target);
|
|
1347
|
+
console.log('Run `eval "$(amalgm shell init zsh)"` once to make `amalgm cd <entity>` change this shell.');
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
console.error(error.message);
|
|
1350
|
+
process.exitCode = 1;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function resolveEntityDirectoryMachine(positionals) {
|
|
1355
|
+
try {
|
|
1356
|
+
const target = await resolveEntityDirectory(positionals);
|
|
1357
|
+
if (target) console.log(target);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
console.error(error.message);
|
|
1360
|
+
process.exitCode = 1;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function shellCommand(positionals) {
|
|
1365
|
+
if (positionals[0] !== 'init' || !['zsh', 'bash'].includes(positionals[1] || 'zsh') || positionals.length > 2) {
|
|
1366
|
+
throw new Error('Usage: amalgm shell init [zsh|bash]');
|
|
1367
|
+
}
|
|
1368
|
+
console.log(`amalgm() {
|
|
1369
|
+
if [ "$1" = cd ]; then
|
|
1370
|
+
shift
|
|
1371
|
+
local _amalgm_destination
|
|
1372
|
+
_amalgm_destination="$(command amalgm __resolve-directory "$@")" || return $?
|
|
1373
|
+
builtin cd "$_amalgm_destination"
|
|
1374
|
+
return $?
|
|
1375
|
+
fi
|
|
1376
|
+
command amalgm "$@"
|
|
1377
|
+
}`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1180
1380
|
async function doctor() {
|
|
1181
1381
|
ensureBaseDirs();
|
|
1182
1382
|
|
|
@@ -1649,7 +1849,11 @@ async function main(argv) {
|
|
|
1649
1849
|
if (command === 'stop') return stop();
|
|
1650
1850
|
if (command === 'register') return register(positionals);
|
|
1651
1851
|
if (command === 'add') return add(positionals);
|
|
1652
|
-
if (command === '
|
|
1852
|
+
if (command === 'ls') return entityList(positionals);
|
|
1853
|
+
if (command === 'cd') return entityCd(positionals);
|
|
1854
|
+
if (command === 'status') return entityStatus(positionals);
|
|
1855
|
+
if (command === 'shell') return shellCommand(positionals);
|
|
1856
|
+
if (command === '__resolve-directory') return resolveEntityDirectoryMachine(positionals);
|
|
1653
1857
|
if (command === 'doctor') return doctor();
|
|
1654
1858
|
if (command === 'service') return serviceCommand(options, positionals);
|
|
1655
1859
|
if (command === 'update') return update(options);
|
package/lib/layout.js
CHANGED
|
@@ -33,7 +33,9 @@ const ROOT_MANIFEST_NAME = '.amalgm-root.json';
|
|
|
33
33
|
const USER_MANIFEST_NAME = '.amalgm-user.json';
|
|
34
34
|
const LAYOUT_VERSION = 1;
|
|
35
35
|
const USER_HOME_MANIFEST_NAME = 'home.json';
|
|
36
|
-
|
|
36
|
+
// Changing the declared exact graph changes the identity contract. A client
|
|
37
|
+
// must never accept a prior graph and silently invent a new managed child.
|
|
38
|
+
const USER_HOME_SCHEMA_VERSION = 2;
|
|
37
39
|
|
|
38
40
|
// Stable per-user product namespaces. Their authority is NOT inferred from
|
|
39
41
|
// their path: user content and exact platform-managed targets are declared
|
|
@@ -59,6 +61,19 @@ const USER_HOME_ROOTS = Object.freeze([
|
|
|
59
61
|
// Platform management is an exact-node fact. It deliberately does not
|
|
60
62
|
// cascade from `system/`: human permissions may still inherit normally, while
|
|
61
63
|
// only these particular entities are product-managed.
|
|
64
|
+
//
|
|
65
|
+
// A Toolbox tool is one complete entity: its definition and actions travel in
|
|
66
|
+
// the same file. These names are deliberately stable and human-readable.
|
|
67
|
+
const SYSTEM_TOOL_ARTIFACTS = Object.freeze([
|
|
68
|
+
{ key: 'amalgm-automations', toolId: 'automations', relativePath: 'toolbox/amalgm-automations.json', rootKey: 'toolbox' },
|
|
69
|
+
{ key: 'amalgm-notifications', toolId: 'notifications', relativePath: 'toolbox/amalgm-notifications.json', rootKey: 'toolbox' },
|
|
70
|
+
{ key: 'amalgm-memories', toolId: 'memories', relativePath: 'toolbox/amalgm-memories.json', rootKey: 'toolbox' },
|
|
71
|
+
{ key: 'amalgm-agents', toolId: 'agents', relativePath: 'toolbox/amalgm-agents.json', rootKey: 'toolbox' },
|
|
72
|
+
{ key: 'amalgm-apps', toolId: 'apps', relativePath: 'toolbox/amalgm-apps.json', rootKey: 'toolbox' },
|
|
73
|
+
{ key: 'amalgm-cua', toolId: 'computer-use', relativePath: 'toolbox/amalgm-cua.json', rootKey: 'toolbox' },
|
|
74
|
+
{ key: 'amalgm-browser', toolId: 'browser', relativePath: 'toolbox/amalgm-browser.json', rootKey: 'toolbox' },
|
|
75
|
+
]);
|
|
76
|
+
|
|
62
77
|
const USER_HOME_EXACT_MANAGED = Object.freeze([
|
|
63
78
|
...USER_HOME_ROOTS.map(({ key, relativePath }) => ({
|
|
64
79
|
key: `${key}-root`, relativePath, rootKey: key,
|
|
@@ -66,6 +81,7 @@ const USER_HOME_EXACT_MANAGED = Object.freeze([
|
|
|
66
81
|
{ key: 'home-manifest', relativePath: 'system/home.json', rootKey: 'system' },
|
|
67
82
|
{ key: 'defaults', relativePath: 'system/defaults', rootKey: 'system' },
|
|
68
83
|
{ key: 'policy', relativePath: 'system/policy', rootKey: 'system' },
|
|
84
|
+
...SYSTEM_TOOL_ARTIFACTS,
|
|
69
85
|
]);
|
|
70
86
|
|
|
71
87
|
function readJson(file, fallback = null) {
|
|
@@ -106,6 +122,7 @@ function declaredUserHome(userDir) {
|
|
|
106
122
|
*/
|
|
107
123
|
function ensureUserHome(userDir) {
|
|
108
124
|
const declaration = declaredUserHome(userDir);
|
|
125
|
+
const materializedExactPaths = [];
|
|
109
126
|
for (const root of declaration.roots) {
|
|
110
127
|
fs.mkdirSync(root.path, { recursive: true, mode: 0o700 });
|
|
111
128
|
}
|
|
@@ -115,15 +132,18 @@ function ensureUserHome(userDir) {
|
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
const manifest = declaration.exactManaged.find((entry) => entry.key === 'home-manifest');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
const manifestContents = {
|
|
136
|
+
schemaVersion: USER_HOME_SCHEMA_VERSION,
|
|
137
|
+
kind: 'amalgm-user-home',
|
|
138
|
+
roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
|
|
139
|
+
exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
|
|
140
|
+
};
|
|
141
|
+
if (manifest && JSON.stringify(readJson(manifest.path, null)) !== JSON.stringify(manifestContents)) {
|
|
142
|
+
writeJson(manifest.path, manifestContents);
|
|
143
|
+
materializedExactPaths.push(manifest.relativePath);
|
|
125
144
|
}
|
|
126
|
-
|
|
145
|
+
|
|
146
|
+
return { ...declaration, materializedExactPaths };
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
function lstatOrNull(target) {
|
|
@@ -401,6 +421,7 @@ module.exports = {
|
|
|
401
421
|
LAYOUT_VERSION,
|
|
402
422
|
PRIMITIVE_DIRS,
|
|
403
423
|
ROOT_MANIFEST_NAME,
|
|
424
|
+
SYSTEM_TOOL_ARTIFACTS,
|
|
404
425
|
USER_MANIFEST_NAME,
|
|
405
426
|
USER_HOME_EXACT_MANAGED,
|
|
406
427
|
USER_HOME_MANIFEST_NAME,
|
|
@@ -19,8 +19,8 @@ const RETRY_MS = 1_000;
|
|
|
19
19
|
const REAUTHORIZE_MS = 30_000;
|
|
20
20
|
const REPO_OBJECT_REQUEST_TIMEOUT_MS = 15_000;
|
|
21
21
|
const COOKIE_PRIVATE_CONTRACT = 'browser-cookie-jar@1';
|
|
22
|
-
const USER_HOME_PRIVATE_CONTRACT = 'amalgm-user-home@
|
|
23
|
-
const ENTITY_REGISTRY_PRIVATE_CONTRACT = 'amalgm-entity-registry@
|
|
22
|
+
const USER_HOME_PRIVATE_CONTRACT = 'amalgm-user-home@2';
|
|
23
|
+
const ENTITY_REGISTRY_PRIVATE_CONTRACT = 'amalgm-entity-registry@3';
|
|
24
24
|
|
|
25
25
|
function createSharedRealtimeTunnel({
|
|
26
26
|
send,
|
|
@@ -938,13 +938,10 @@ function createSharedRealtimeTunnel({
|
|
|
938
938
|
committedAt: frame.committed_at,
|
|
939
939
|
}).then((ack) => {
|
|
940
940
|
entityMutationInflight.delete(frame.request_id);
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
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.
|
|
941
|
+
// The runtime reports whether it converged its catalog locally from
|
|
942
|
+
// this acknowledgement. Only when it could not (a version gap, a
|
|
943
|
+
// reconciliation handshake, a stale local view) is the compacted
|
|
944
|
+
// snapshot re-downloaded; the ordinary in-order ack costs nothing.
|
|
948
945
|
if (ack?.requiresSnapshot !== false) void ensurePrivateEntityRegistry();
|
|
949
946
|
scheduleEntityMutationDrain();
|
|
950
947
|
}).catch(() => {
|
|
@@ -1109,11 +1106,12 @@ function createSharedRealtimeTunnel({
|
|
|
1109
1106
|
version: frame.version,
|
|
1110
1107
|
committedAt: frame.committed_at,
|
|
1111
1108
|
actorUserId: frame.actor_user_id,
|
|
1112
|
-
})).then(() => {
|
|
1113
|
-
if (entityRegistryMutation) {
|
|
1114
|
-
// The
|
|
1115
|
-
//
|
|
1116
|
-
// compact and return its active snapshot
|
|
1109
|
+
})).then((result) => {
|
|
1110
|
+
if (entityRegistryMutation && result?.requiresSnapshot !== false) {
|
|
1111
|
+
// The runtime could not converge its catalog from this frame alone
|
|
1112
|
+
// (a gap, a snapshot replacement, or a stale local view). Ask the
|
|
1113
|
+
// authority to compact and return its active snapshot; an in-order
|
|
1114
|
+
// frame needs no re-download.
|
|
1117
1115
|
void ensurePrivateEntityRegistry();
|
|
1118
1116
|
}
|
|
1119
1117
|
}).catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.259",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/app-chat.js && node --check lib/react.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/identity-adopt.js && node --check lib/layout.js && node --check lib/migrate-layout.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check lib/updater.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/amalgm-mcp/events/pr-check-runner.js && node --check runtime/scripts/amalgm-mcp/registration/entity-cloud.js && node --check runtime/scripts/amalgm-mcp/registration/entity-content.js && node --check runtime/scripts/amalgm-mcp/registration/service.js && node --check runtime/scripts/amalgm-mcp/server/routes/entities.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/app-chat.js && node --check lib/react.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/identity-adopt.js && node --check lib/layout.js && node --check lib/migrate-layout.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check lib/updater.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/amalgm-mcp/events/pr-check-runner.js && node --check runtime/scripts/amalgm-mcp/registration/entity-cloud.js && node --check runtime/scripts/amalgm-mcp/registration/entity-content.js && node --check runtime/scripts/amalgm-mcp/registration/service.js && node --check runtime/scripts/amalgm-mcp/toolbox/artifacts.js && node --check runtime/scripts/amalgm-mcp/server/routes/entities.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -33,7 +33,9 @@ const ROOT_MANIFEST_NAME = '.amalgm-root.json';
|
|
|
33
33
|
const USER_MANIFEST_NAME = '.amalgm-user.json';
|
|
34
34
|
const LAYOUT_VERSION = 1;
|
|
35
35
|
const USER_HOME_MANIFEST_NAME = 'home.json';
|
|
36
|
-
|
|
36
|
+
// Changing the declared exact graph changes the identity contract. A client
|
|
37
|
+
// must never accept a prior graph and silently invent a new managed child.
|
|
38
|
+
const USER_HOME_SCHEMA_VERSION = 2;
|
|
37
39
|
|
|
38
40
|
// Stable per-user product namespaces. Their authority is NOT inferred from
|
|
39
41
|
// their path: user content and exact platform-managed targets are declared
|
|
@@ -59,6 +61,21 @@ const USER_HOME_ROOTS = Object.freeze([
|
|
|
59
61
|
// Platform management is an exact-node fact. It deliberately does not
|
|
60
62
|
// cascade from `system/`: human permissions may still inherit normally, while
|
|
61
63
|
// only these particular entities are product-managed.
|
|
64
|
+
//
|
|
65
|
+
// A Toolbox tool is one complete entity: its definition and actions travel in
|
|
66
|
+
// the same file. These names are deliberately stable and human-readable. They
|
|
67
|
+
// are not an `amalgm/` subtree because that would falsely imply ownership
|
|
68
|
+
// cascades to every future child of that directory.
|
|
69
|
+
const SYSTEM_TOOL_ARTIFACTS = Object.freeze([
|
|
70
|
+
{ key: 'amalgm-automations', toolId: 'automations', relativePath: 'toolbox/amalgm-automations.json', rootKey: 'toolbox' },
|
|
71
|
+
{ key: 'amalgm-notifications', toolId: 'notifications', relativePath: 'toolbox/amalgm-notifications.json', rootKey: 'toolbox' },
|
|
72
|
+
{ key: 'amalgm-memories', toolId: 'memories', relativePath: 'toolbox/amalgm-memories.json', rootKey: 'toolbox' },
|
|
73
|
+
{ key: 'amalgm-agents', toolId: 'agents', relativePath: 'toolbox/amalgm-agents.json', rootKey: 'toolbox' },
|
|
74
|
+
{ key: 'amalgm-apps', toolId: 'apps', relativePath: 'toolbox/amalgm-apps.json', rootKey: 'toolbox' },
|
|
75
|
+
{ key: 'amalgm-cua', toolId: 'computer-use', relativePath: 'toolbox/amalgm-cua.json', rootKey: 'toolbox' },
|
|
76
|
+
{ key: 'amalgm-browser', toolId: 'browser', relativePath: 'toolbox/amalgm-browser.json', rootKey: 'toolbox' },
|
|
77
|
+
]);
|
|
78
|
+
|
|
62
79
|
const USER_HOME_EXACT_MANAGED = Object.freeze([
|
|
63
80
|
...USER_HOME_ROOTS.map(({ key, relativePath }) => ({
|
|
64
81
|
key: `${key}-root`, relativePath, rootKey: key,
|
|
@@ -66,6 +83,7 @@ const USER_HOME_EXACT_MANAGED = Object.freeze([
|
|
|
66
83
|
{ key: 'home-manifest', relativePath: 'system/home.json', rootKey: 'system' },
|
|
67
84
|
{ key: 'defaults', relativePath: 'system/defaults', rootKey: 'system' },
|
|
68
85
|
{ key: 'policy', relativePath: 'system/policy', rootKey: 'system' },
|
|
86
|
+
...SYSTEM_TOOL_ARTIFACTS,
|
|
69
87
|
]);
|
|
70
88
|
|
|
71
89
|
function readJson(file, fallback = null) {
|
|
@@ -106,6 +124,7 @@ function declaredUserHome(userDir) {
|
|
|
106
124
|
*/
|
|
107
125
|
function ensureUserHome(userDir) {
|
|
108
126
|
const declaration = declaredUserHome(userDir);
|
|
127
|
+
const materializedExactPaths = [];
|
|
109
128
|
for (const root of declaration.roots) {
|
|
110
129
|
fs.mkdirSync(root.path, { recursive: true, mode: 0o700 });
|
|
111
130
|
}
|
|
@@ -115,15 +134,18 @@ function ensureUserHome(userDir) {
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
const manifest = declaration.exactManaged.find((entry) => entry.key === 'home-manifest');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
const manifestContents = {
|
|
138
|
+
schemaVersion: USER_HOME_SCHEMA_VERSION,
|
|
139
|
+
kind: 'amalgm-user-home',
|
|
140
|
+
roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
|
|
141
|
+
exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
|
|
142
|
+
};
|
|
143
|
+
if (manifest && JSON.stringify(readJson(manifest.path, null)) !== JSON.stringify(manifestContents)) {
|
|
144
|
+
writeJson(manifest.path, manifestContents);
|
|
145
|
+
materializedExactPaths.push(manifest.relativePath);
|
|
125
146
|
}
|
|
126
|
-
|
|
147
|
+
|
|
148
|
+
return { ...declaration, materializedExactPaths };
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
function lstatOrNull(target) {
|
|
@@ -401,6 +423,7 @@ module.exports = {
|
|
|
401
423
|
LAYOUT_VERSION,
|
|
402
424
|
PRIMITIVE_DIRS,
|
|
403
425
|
ROOT_MANIFEST_NAME,
|
|
426
|
+
SYSTEM_TOOL_ARTIFACTS,
|
|
404
427
|
USER_MANIFEST_NAME,
|
|
405
428
|
USER_HOME_EXACT_MANAGED,
|
|
406
429
|
USER_HOME_MANIFEST_NAME,
|
|
@@ -40,7 +40,7 @@ const path = require('path');
|
|
|
40
40
|
const { createApply } = require('./apply');
|
|
41
41
|
const { createContinuity } = require('./continuity');
|
|
42
42
|
const { createStore } = require('./store');
|
|
43
|
-
const { censusBoundaries, censusLinks, diffDirState, diffLinkState, diffRootState, gitDirTruth, walkRoot } = require('./scan');
|
|
43
|
+
const { censusBoundaries, censusLinks, diffDirState, diffLinkState, diffRootState, gitDirTruth, listDir, walkRoot } = require('./scan');
|
|
44
44
|
const { createVerify } = require('./verify');
|
|
45
45
|
const { watchDir, watchRecursive } = require('./watch');
|
|
46
46
|
const { adapterFor } = require('../adapters');
|
|
@@ -343,18 +343,75 @@ function createObserver(options) {
|
|
|
343
343
|
}
|
|
344
344
|
if (parent) {
|
|
345
345
|
// Born from a files root, returned to it: the parent's next scan
|
|
346
|
-
// reclaims the files as creates.
|
|
346
|
+
// reclaims the files as creates. No doorbell will ever name the
|
|
347
|
+
// reclaimed ground, so every file is suspect — the scan must walk.
|
|
347
348
|
detach(root.rootId);
|
|
348
349
|
store.removeRoot(root.rootId);
|
|
349
|
-
live.get(parent.rootId)
|
|
350
|
+
const parentState = live.get(parent.rootId);
|
|
351
|
+
parentState?.territories.delete(root.path.slice(parent.path.length + 1));
|
|
352
|
+
if (parentState) parentState.checkAll = true;
|
|
350
353
|
scanFilesRoot(parent);
|
|
351
354
|
return;
|
|
352
355
|
}
|
|
353
356
|
store.setRootKind(root.rootId, 'files');
|
|
354
|
-
if (state)
|
|
357
|
+
if (state) {
|
|
358
|
+
state.kind = 'files';
|
|
359
|
+
state.checkAll = true; // conversion ground has no doorbells: walk it
|
|
360
|
+
}
|
|
355
361
|
scanFilesRoot({ ...root, kind: 'files' });
|
|
356
362
|
}
|
|
357
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The scoped scan a NAMED doorbell earns: read only the directories
|
|
366
|
+
* the rung paths live in, compare only those directories' rows, and
|
|
367
|
+
* conclude only what is channel-local — an existing file's bytes
|
|
368
|
+
* changed, an existing link's written target changed. Anything
|
|
369
|
+
* structural (a create, a delete, a rename, a retag, a `.git`, an
|
|
370
|
+
* unreadable or absent directory) may be half of a story another
|
|
371
|
+
* directory holds, so it concludes nothing here and escalates to the
|
|
372
|
+
* full walk, which owns those laws unchanged. Returns true when the
|
|
373
|
+
* scoped conclusion was complete; false means walk.
|
|
374
|
+
*/
|
|
375
|
+
function scanRungDirs(root, state, rung) {
|
|
376
|
+
const scopeDirs = new Set();
|
|
377
|
+
for (const relPath of rung) {
|
|
378
|
+
scopeDirs.add(relPath.includes('/') ? relPath.slice(0, relPath.lastIndexOf('/')) : '');
|
|
379
|
+
// A rung directory is also looked at directly: its own children
|
|
380
|
+
// may be what actually changed (the OS may name only the folder).
|
|
381
|
+
if (isDirectory(path.join(root.path, relPath))) scopeDirs.add(relPath);
|
|
382
|
+
}
|
|
383
|
+
const entries = { files: [], dirs: [], links: [] };
|
|
384
|
+
for (const relDir of scopeDirs) {
|
|
385
|
+
if (relDir !== '' && underTerritory(state, relDir)) return false; // territory ground answers to its own root
|
|
386
|
+
const look = listDir(root.path, relDir, shouldEnroll);
|
|
387
|
+
if (look.absent || look.unsettled || look.hasGit) return false;
|
|
388
|
+
for (const dir of look.dirs) {
|
|
389
|
+
// A nested repo is territory, never a dir channel — walkRoot
|
|
390
|
+
// reports it to repoDirs; the scoped look just leaves it alone.
|
|
391
|
+
if (state.territories.has(dir.relPath)) continue;
|
|
392
|
+
entries.dirs.push(dir);
|
|
393
|
+
}
|
|
394
|
+
entries.files.push(...look.files);
|
|
395
|
+
entries.links.push(...look.links);
|
|
396
|
+
}
|
|
397
|
+
const dirs = [...scopeDirs];
|
|
398
|
+
const rows = {
|
|
399
|
+
files: dirs.flatMap((relDir) => store.filesInDir(root.rootId, relDir)),
|
|
400
|
+
dirs: dirs.flatMap((relDir) => store.dirsInDir(root.rootId, relDir)),
|
|
401
|
+
links: dirs.flatMap((relDir) => store.linksInDir(root.rootId, relDir)),
|
|
402
|
+
};
|
|
403
|
+
const dirDiff = diffDirState(rows.dirs, entries.dirs);
|
|
404
|
+
const linkDiff = diffLinkState(rows.links, entries.links);
|
|
405
|
+
const fileDiff = diffRootState(rows.files, entries.files, rung);
|
|
406
|
+
const structural = dirDiff.renames.length || dirDiff.retags.length || dirDiff.creates.length || dirDiff.deletes.length
|
|
407
|
+
|| linkDiff.renames.length || linkDiff.creates.length || linkDiff.deletes.length
|
|
408
|
+
|| fileDiff.renames.length || fileDiff.creates.length || fileDiff.deletes.length;
|
|
409
|
+
if (structural) return false;
|
|
410
|
+
applyLinkDiff(root, linkDiff); // changes and retags: payload-local facts
|
|
411
|
+
applyDiff(root, fileDiff); // checks only: content truth on existing channels
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
358
415
|
function scanFilesRoot(root, { allowRootRepository = false } = {}) {
|
|
359
416
|
// Drain the doorbell's named hints first: anything that rings during the
|
|
360
417
|
// walk below belongs to the next scan, not this one.
|
|
@@ -366,6 +423,10 @@ function createObserver(options) {
|
|
|
366
423
|
state.checkAll = false;
|
|
367
424
|
}
|
|
368
425
|
|
|
426
|
+
// A named doorbell earns a scoped look; only a nameless one (or a
|
|
427
|
+
// scoped look that met structural news) pays for the whole walk.
|
|
428
|
+
if (!checkAll && rung.size > 0 && state && scanRungDirs(root, state, rung)) return;
|
|
429
|
+
|
|
369
430
|
const { files: entries, dirs, links, repoDirs, unsettled } = walkRoot(
|
|
370
431
|
root.path,
|
|
371
432
|
shouldEnroll,
|
|
@@ -126,6 +126,65 @@ function walkRoot(rootPath, shouldEnroll, { allowRootRepository = false } = {})
|
|
|
126
126
|
return { files, dirs, links, repoDirs, unsettled };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* One directory's immediate truth — the scoped look a NAMED doorbell
|
|
131
|
+
* earns (walkRoot is the nameless doorbell's). Same laws, one level
|
|
132
|
+
* deep: entries classified exactly as walkRoot classifies them,
|
|
133
|
+
* symlinks read but never followed. The caller decides what a scoped
|
|
134
|
+
* look may conclude; this function only refuses to pretend it saw more
|
|
135
|
+
* than one directory: a `.git` here is territory news (`hasGit`), a
|
|
136
|
+
* listing that failed is `unsettled`, a directory that is gone is
|
|
137
|
+
* `absent` — all three send the caller to the full walk, which owns
|
|
138
|
+
* those conclusions.
|
|
139
|
+
*/
|
|
140
|
+
function listDir(rootPath, relDir, shouldEnroll) {
|
|
141
|
+
const absDir = relDir ? path.join(rootPath, relDir) : rootPath;
|
|
142
|
+
const out = { files: [], dirs: [], links: [], hasGit: false, absent: false, unsettled: false };
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (lookFailed(error).truth === 'absent') out.absent = true;
|
|
148
|
+
else out.unsettled = true;
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
if (entries.some((entry) => entry.name === '.git')) {
|
|
152
|
+
out.hasGit = true;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
157
|
+
if (!shouldEnroll(relPath)) continue;
|
|
158
|
+
let stat;
|
|
159
|
+
try {
|
|
160
|
+
stat = fs.lstatSync(path.join(rootPath, relPath));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Vanished mid-look is honest absence (simply not listed); a
|
|
163
|
+
// failed look concludes nothing — the full walk reads again.
|
|
164
|
+
if (lookFailed(error).truth === 'unsettled') out.unsettled = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (stat.isDirectory()) {
|
|
168
|
+
out.dirs.push({ relPath, device: stat.dev, inode: stat.ino });
|
|
169
|
+
} else if (stat.isSymbolicLink()) {
|
|
170
|
+
try {
|
|
171
|
+
out.links.push({ relPath, device: stat.dev, inode: stat.ino, target: fs.readlinkSync(path.join(rootPath, relPath)) });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (lookFailed(error).truth === 'unsettled') out.unsettled = true;
|
|
174
|
+
}
|
|
175
|
+
} else if (stat.isFile()) {
|
|
176
|
+
out.files.push({
|
|
177
|
+
relPath,
|
|
178
|
+
device: stat.dev,
|
|
179
|
+
inode: stat.ino,
|
|
180
|
+
size: stat.size,
|
|
181
|
+
mtimeMs: Math.floor(stat.mtimeMs),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
129
188
|
function defaultShouldEnroll(relPath) {
|
|
130
189
|
return require('../workspace/tree/inclusion').shouldEnroll(relPath);
|
|
131
190
|
}
|
|
@@ -384,4 +443,4 @@ function diffRootState(rows, entries, mustCheck = null) {
|
|
|
384
443
|
return actions;
|
|
385
444
|
}
|
|
386
445
|
|
|
387
|
-
module.exports = { censusBoundaries, censusLinks, diffDirState, diffLinkState, diffRootState, gitDirTruth, walkRoot };
|
|
446
|
+
module.exports = { censusBoundaries, censusLinks, diffDirState, diffLinkState, diffRootState, gitDirTruth, listDir, walkRoot };
|