amalgm 0.1.138 → 0.1.139
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/bin/amalgm.js +73 -0
- package/lib/auth-store.js +16 -2
- package/lib/cli.js +48 -7
- package/lib/identity-adopt.js +43 -0
- package/lib/layout.js +17 -10
- package/lib/migrate-layout.js +1 -1
- package/lib/state-migration.js +23 -1
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/agents/store.js +8 -2
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +116 -11
- package/runtime/scripts/amalgm-mcp/browser/backend.js +5 -2
- package/runtime/scripts/amalgm-mcp/browser/cli.js +2 -1
- package/runtime/scripts/amalgm-mcp/config.js +26 -5
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +119 -53
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +2 -2
- package/runtime/scripts/amalgm-mcp/events/npm-release-runner.js +4 -2
- package/runtime/scripts/amalgm-mcp/events/pr-check-runner.js +4 -2
- package/runtime/scripts/amalgm-mcp/fs/rest.js +20 -1
- package/runtime/scripts/amalgm-mcp/lib/layout.js +17 -10
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +2 -2
- package/runtime/scripts/amalgm-mcp/project-context/store.js +27 -5
- package/runtime/scripts/amalgm-mcp/tests/agents-source-of-truth.test.js +13 -0
- package/runtime/scripts/amalgm-mcp/tests/apps-supervisor.test.js +190 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-routing.test.js +2 -3
- package/runtime/scripts/amalgm-mcp/tests/config-migration.test.js +37 -1
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +51 -0
- package/runtime/scripts/amalgm-mcp/tests/files-resource.test.js +25 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +59 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +6 -1
- package/runtime/scripts/chat-core/contract.js +2 -2
- package/runtime/scripts/chat-core/credentials/store.js +2 -1
- package/runtime/scripts/chat-core/recorder.js +2 -1
- package/runtime/scripts/chat-core/tooling/native-binaries.js +2 -1
- package/runtime/scripts/chat-server/config.js +2 -2
- package/runtime/scripts/credential-adapter.js +2 -2
- package/runtime/scripts/lib/runtime-paths.js +61 -1
- package/runtime/scripts/local-gateway.js +2 -1
- package/runtime/scripts/proxy-token-store.js +7 -4
- package/runtime/scripts/proxy-token-store.test.js +112 -0
package/bin/amalgm.js
CHANGED
|
@@ -22,6 +22,79 @@ if (label && !process.env.AMALGM_RUNTIME_LABEL) process.env.AMALGM_RUNTIME_LABEL
|
|
|
22
22
|
const userScope = takeFlag(process.argv, ['--user', '--user-id', '--runtime-user']);
|
|
23
23
|
if (userScope && !process.env.AMALGM_RUNTIME_USER_ID) process.env.AMALGM_RUNTIME_USER_ID = userScope;
|
|
24
24
|
|
|
25
|
+
function readJson(file, fallback = null) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(require('fs').readFileSync(file, 'utf8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cleanString(value) {
|
|
34
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function userEmail(record) {
|
|
38
|
+
return cleanString(record?.user_email || record?.userEmail || record?.email).toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function identityFromDir(dir) {
|
|
42
|
+
const path = require('path');
|
|
43
|
+
const record = readJson(path.join(dir, 'computer.json'), null) || {};
|
|
44
|
+
const auth = readJson(path.join(dir, 'auth.json'), null) || {};
|
|
45
|
+
const userId = cleanString(record.user_id) || cleanString(auth.user_id);
|
|
46
|
+
const email = userEmail(record) || userEmail(auth);
|
|
47
|
+
return userId && email ? { userId, email } : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function preflightCanonicalLayout() {
|
|
51
|
+
if (process.env.AMALGM_HOME || process.env.AMALGM_DIR) return;
|
|
52
|
+
const fs = require('fs');
|
|
53
|
+
const os = require('os');
|
|
54
|
+
const path = require('path');
|
|
55
|
+
const layout = require('../lib/layout');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
require('../lib/migrate-layout').migrateLayout({ home: os.homedir() });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error && error.message ? error.message : String(error);
|
|
61
|
+
console.warn(`[layout] Could not migrate Amalgm root automatically: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const homeDir = layout.resolveAmalgmHome(process.env);
|
|
65
|
+
const identities = [];
|
|
66
|
+
const rootIdentity = identityFromDir(homeDir);
|
|
67
|
+
if (rootIdentity) identities.push(rootIdentity);
|
|
68
|
+
|
|
69
|
+
for (const entry of layout.listUserEntries(homeDir)) {
|
|
70
|
+
const entryIdentity = identityFromDir(entry.dir);
|
|
71
|
+
if (entryIdentity) identities.push(entryIdentity);
|
|
72
|
+
const runtimesDir = path.join(entry.dir, 'runtimes');
|
|
73
|
+
let runtimeNames = [];
|
|
74
|
+
try {
|
|
75
|
+
runtimeNames = fs.readdirSync(runtimesDir);
|
|
76
|
+
} catch {}
|
|
77
|
+
for (const runtimeName of runtimeNames) {
|
|
78
|
+
const runtimeIdentity = identityFromDir(path.join(runtimesDir, runtimeName));
|
|
79
|
+
if (runtimeIdentity) identities.push(runtimeIdentity);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const identity of identities) {
|
|
84
|
+
try {
|
|
85
|
+
layout.ensureUserDir(homeDir, {
|
|
86
|
+
userId: identity.userId,
|
|
87
|
+
email: identity.email,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error && error.message ? error.message : String(error);
|
|
91
|
+
console.warn(`[layout] Could not adopt email folder for ${identity.userId}: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
preflightCanonicalLayout();
|
|
97
|
+
|
|
25
98
|
require('../lib/cli').main(process.argv).catch((error) => {
|
|
26
99
|
const message = error && error.message ? error.message : String(error);
|
|
27
100
|
console.error(`Error: ${message}`);
|
package/lib/auth-store.js
CHANGED
|
@@ -67,6 +67,14 @@ function cleanString(value) {
|
|
|
67
67
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function normalizeUserEmail(value) {
|
|
71
|
+
return cleanString(value).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function userEmailFromRecord(record) {
|
|
75
|
+
return normalizeUserEmail(record?.user_email || record?.userEmail || record?.email);
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
function hasComputerSecrets(record) {
|
|
71
79
|
if (!record || typeof record !== 'object') return false;
|
|
72
80
|
return Array.from(SECRET_COMPUTER_KEYS).some((key) => cleanString(record[key]));
|
|
@@ -77,6 +85,8 @@ function publicComputerRecord(record, authFile = AUTH_FILE) {
|
|
|
77
85
|
...(record || {}),
|
|
78
86
|
auth_file: path.basename(authFile),
|
|
79
87
|
};
|
|
88
|
+
const email = userEmailFromRecord(out);
|
|
89
|
+
if (email) out.user_email = email;
|
|
80
90
|
for (const key of SECRET_COMPUTER_KEYS) delete out[key];
|
|
81
91
|
return out;
|
|
82
92
|
}
|
|
@@ -89,8 +99,10 @@ function authFromRecord(record, existing = null) {
|
|
|
89
99
|
updated_at: now,
|
|
90
100
|
};
|
|
91
101
|
|
|
92
|
-
for (const key of ['computer_id', 'device_id', 'user_id', 'app_url']) {
|
|
93
|
-
const value =
|
|
102
|
+
for (const key of ['computer_id', 'device_id', 'user_id', 'user_email', 'app_url']) {
|
|
103
|
+
const value = key === 'user_email'
|
|
104
|
+
? userEmailFromRecord(record)
|
|
105
|
+
: cleanString(record?.[key]);
|
|
94
106
|
if (value) auth[key] = value;
|
|
95
107
|
}
|
|
96
108
|
|
|
@@ -171,6 +183,7 @@ function mergeRecordAuth(record, auth) {
|
|
|
171
183
|
merged.proxy_token_refreshed_at = cleanString(auth.proxy?.refreshed_at);
|
|
172
184
|
}
|
|
173
185
|
if (!merged.user_id) merged.user_id = cleanString(auth.user_id);
|
|
186
|
+
if (!merged.user_email) merged.user_email = normalizeUserEmail(auth.user_email);
|
|
174
187
|
if (!merged.app_url) merged.app_url = cleanString(auth.app_url);
|
|
175
188
|
return merged;
|
|
176
189
|
}
|
|
@@ -310,4 +323,5 @@ module.exports = {
|
|
|
310
323
|
RUNTIME_AUTH_FILE,
|
|
311
324
|
RUNTIME_COMPUTER_FILE,
|
|
312
325
|
saveComputerRecord,
|
|
326
|
+
userEmailFromRecord,
|
|
313
327
|
};
|
package/lib/cli.js
CHANGED
|
@@ -58,6 +58,9 @@ const {
|
|
|
58
58
|
proxyTokenNeedsRefresh,
|
|
59
59
|
saveComputerRecord,
|
|
60
60
|
} = require('./auth-store');
|
|
61
|
+
const {
|
|
62
|
+
adoptIdentityFromRecord,
|
|
63
|
+
} = require('./identity-adopt');
|
|
61
64
|
const {
|
|
62
65
|
agentCommandShimStatus,
|
|
63
66
|
binaryStatus,
|
|
@@ -72,6 +75,7 @@ const {
|
|
|
72
75
|
|
|
73
76
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
74
77
|
const SETUP_CODE_LENGTH = 16;
|
|
78
|
+
const EXPLICIT_AMALGM_DIR = Boolean(process.env.AMALGM_DIR);
|
|
75
79
|
|
|
76
80
|
function parseArgs(argv) {
|
|
77
81
|
const args = argv.slice(2);
|
|
@@ -126,7 +130,7 @@ function usage() {
|
|
|
126
130
|
' AMALGM_AUTO_UPDATE Auto-update before start unless set to 0/false/off',
|
|
127
131
|
' AMALGM_DISABLE_AUTO_UPDATE Set to 1 to disable startup/background auto-updates',
|
|
128
132
|
' AMALGM_DIR Explicit runtime state dir override',
|
|
129
|
-
' AMALGM_HOME Runtime state root (default
|
|
133
|
+
' AMALGM_HOME Runtime state root (default ~/amalgm)',
|
|
130
134
|
' AMALGM_RUNTIME_LABEL Runtime label: main, preview, or local',
|
|
131
135
|
' AMALGM_RUNTIME_USER_ID Runtime user scope',
|
|
132
136
|
' AMALGM_PROXY_TOKEN Optional short-lived proxy token override',
|
|
@@ -486,15 +490,16 @@ async function registerComputer(appUrl, registrationToken, device, name) {
|
|
|
486
490
|
app_version: PACKAGE_VERSION,
|
|
487
491
|
registered_at: new Date().toISOString(),
|
|
488
492
|
};
|
|
489
|
-
saveComputerRecord(record);
|
|
493
|
+
const saved = saveComputerRecord(record);
|
|
494
|
+
adoptIdentityFromRecord(saved, { logger: console });
|
|
490
495
|
|
|
491
|
-
console.log(`Registered computer: ${
|
|
492
|
-
console.log(`Events URL: ${
|
|
493
|
-
if (!proxyTokenFromRecord(
|
|
496
|
+
console.log(`Registered computer: ${saved.computer_id}`);
|
|
497
|
+
console.log(`Events URL: ${saved.webhook_url}`);
|
|
498
|
+
if (!proxyTokenFromRecord(saved)) {
|
|
494
499
|
console.log('HMAC proxy: not minted yet; Amalgm-managed egress will refresh after registration is complete.');
|
|
495
500
|
}
|
|
496
501
|
console.log('Run `amalgm start` to connect this machine.');
|
|
497
|
-
return
|
|
502
|
+
return saved;
|
|
498
503
|
}
|
|
499
504
|
|
|
500
505
|
async function refreshStoredProxyToken(record) {
|
|
@@ -515,12 +520,15 @@ async function refreshStoredProxyToken(record) {
|
|
|
515
520
|
|
|
516
521
|
const next = {
|
|
517
522
|
...record,
|
|
523
|
+
user_email: refreshed.user_email || record.user_email,
|
|
518
524
|
proxy_token: refreshed.proxy_token || record.proxy_token,
|
|
519
525
|
proxy_token_expires_in: refreshed.proxy_token_expires_in || record.proxy_token_expires_in,
|
|
520
526
|
proxy_token_expires_at: refreshed.proxy_token_expires_at || record.proxy_token_expires_at,
|
|
521
527
|
proxy_token_refreshed_at: new Date().toISOString(),
|
|
522
528
|
};
|
|
523
|
-
|
|
529
|
+
const saved = saveComputerRecord(next);
|
|
530
|
+
adoptIdentityFromRecord(saved, { logger: console });
|
|
531
|
+
return saved;
|
|
524
532
|
}
|
|
525
533
|
|
|
526
534
|
async function loginWithSetupCode(appUrl, setupCode, device, name) {
|
|
@@ -811,6 +819,31 @@ function relaunchCurrentCommandAfterUpdate() {
|
|
|
811
819
|
return Number.isInteger(result.status) ? result.status : 0;
|
|
812
820
|
}
|
|
813
821
|
|
|
822
|
+
function relaunchCurrentCommandAfterLayoutChange() {
|
|
823
|
+
const result = spawnSync(process.execPath, process.argv.slice(1), {
|
|
824
|
+
stdio: 'inherit',
|
|
825
|
+
env: {
|
|
826
|
+
...process.env,
|
|
827
|
+
AMALGM_LAYOUT_RELAUNCHED: '1',
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
if (result.error) throw result.error;
|
|
831
|
+
if (result.signal) {
|
|
832
|
+
process.kill(process.pid, result.signal);
|
|
833
|
+
return 1;
|
|
834
|
+
}
|
|
835
|
+
return Number.isInteger(result.status) ? result.status : 0;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function shouldRelaunchForAdoptedDir(adoption) {
|
|
839
|
+
return Boolean(
|
|
840
|
+
adoption?.dir
|
|
841
|
+
&& !EXPLICIT_AMALGM_DIR
|
|
842
|
+
&& process.env.AMALGM_LAYOUT_RELAUNCHED !== '1'
|
|
843
|
+
&& path.resolve(adoption.dir) !== path.resolve(AMALGM_DIR)
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
814
847
|
async function start(options) {
|
|
815
848
|
ensureBaseDirs();
|
|
816
849
|
|
|
@@ -822,6 +855,7 @@ async function start(options) {
|
|
|
822
855
|
assertRuntimePresent();
|
|
823
856
|
|
|
824
857
|
let record = loadComputerRecord({ migrate: true });
|
|
858
|
+
let adoption = adoptIdentityFromRecord(record, { logger: console });
|
|
825
859
|
const localOnly = !!options['local-only'];
|
|
826
860
|
if (!localOnly && !hasRequiredComputerFields(record)) {
|
|
827
861
|
throw new Error('This machine is not logged in. Run `amalgm login` to authorize it in your browser, or use `amalgm start --local-only` for local-only development.');
|
|
@@ -829,11 +863,18 @@ async function start(options) {
|
|
|
829
863
|
if (!localOnly) {
|
|
830
864
|
try {
|
|
831
865
|
record = await refreshStoredProxyToken(record);
|
|
866
|
+
adoption = adoptIdentityFromRecord(record, { logger: console });
|
|
832
867
|
} catch (error) {
|
|
833
868
|
console.warn(`Could not refresh proxy token before start: ${error.message}`);
|
|
834
869
|
}
|
|
835
870
|
}
|
|
836
871
|
|
|
872
|
+
if (shouldRelaunchForAdoptedDir(adoption)) {
|
|
873
|
+
console.log(`Adopted Amalgm user folder ${adoption.dir}; restarting start command with canonical paths.`);
|
|
874
|
+
process.exitCode = relaunchCurrentCommandAfterLayoutChange();
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
837
878
|
ensureAgentRuntimeCommands({ logger: console });
|
|
838
879
|
|
|
839
880
|
const pid = readPid();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
AMALGM_HOME,
|
|
5
|
+
} = require('./paths');
|
|
6
|
+
const layout = require('./layout');
|
|
7
|
+
|
|
8
|
+
function cleanString(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeUserEmail(value) {
|
|
13
|
+
return cleanString(value).toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function userEmailFromRecord(record) {
|
|
17
|
+
return normalizeUserEmail(record?.user_email || record?.userEmail || record?.email);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function adoptIdentityFromRecord(record, options = {}) {
|
|
21
|
+
const userId = cleanString(record?.user_id);
|
|
22
|
+
const email = userEmailFromRecord(record);
|
|
23
|
+
if (!userId || !email) {
|
|
24
|
+
return { dir: null, adopted: false, reason: !userId ? 'missing-user-id' : 'missing-email' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const homeDir = options.homeDir || AMALGM_HOME;
|
|
28
|
+
const logger = options.logger || console;
|
|
29
|
+
try {
|
|
30
|
+
const dir = layout.ensureUserDir(homeDir, { userId, email });
|
|
31
|
+
return { dir, adopted: true };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
logger.warn?.(`[identity] email folder adoption deferred: ${message}`);
|
|
35
|
+
return { dir: null, adopted: false, reason: 'error', error: message };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
adoptIdentityFromRecord,
|
|
41
|
+
normalizeUserEmail,
|
|
42
|
+
userEmailFromRecord,
|
|
43
|
+
};
|
package/lib/layout.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* Rules:
|
|
17
17
|
* - The folder name is presentation; `.amalgm-user.json` is identity. If a
|
|
18
18
|
* folder is renamed, resolution recovers by scanning user manifests.
|
|
19
|
-
* -
|
|
20
|
-
* resolution
|
|
21
|
-
*
|
|
19
|
+
* - Hidden-root layouts are migrated to the visible root by startup preflight;
|
|
20
|
+
* resolution itself does not silently stay on ~/.amalgm.
|
|
21
|
+
* - UUID-named user folders are compatibility aliases only after email
|
|
22
|
+
* adoption. The runtime identity remains the userId in the manifest.
|
|
22
23
|
* - Explicit env (AMALGM_HOME / AMALGM_DIR) always wins — isolated runtimes
|
|
23
24
|
* and tests depend on that.
|
|
24
25
|
*/
|
|
@@ -101,16 +102,14 @@ function isAmalgmRoot(dir) {
|
|
|
101
102
|
/**
|
|
102
103
|
* Where Amalgm state lives on this machine.
|
|
103
104
|
*
|
|
104
|
-
* Order: explicit AMALGM_HOME →
|
|
105
|
-
*
|
|
106
|
-
*
|
|
105
|
+
* Order: explicit AMALGM_HOME → visible root. A migrated ~/.amalgm symlink is
|
|
106
|
+
* allowed only as a pointer back to the visible root; an old hidden directory
|
|
107
|
+
* is intentionally ignored so new process trees don't keep depending on it.
|
|
107
108
|
*/
|
|
108
109
|
function resolveAmalgmHome(env = process.env) {
|
|
109
110
|
if (env.AMALGM_HOME) return path.resolve(env.AMALGM_HOME);
|
|
110
111
|
|
|
111
112
|
const visible = visibleRootPath();
|
|
112
|
-
if (isAmalgmRoot(visible)) return visible;
|
|
113
|
-
|
|
114
113
|
const hidden = hiddenRootPath();
|
|
115
114
|
const hiddenStat = lstatOrNull(hidden);
|
|
116
115
|
if (hiddenStat?.isSymbolicLink()) {
|
|
@@ -121,7 +120,6 @@ function resolveAmalgmHome(env = process.env) {
|
|
|
121
120
|
return visible;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
if (hiddenStat) return hidden;
|
|
125
123
|
return visible;
|
|
126
124
|
}
|
|
127
125
|
|
|
@@ -186,6 +184,14 @@ function writeUserManifest(userDir, { userId, email }) {
|
|
|
186
184
|
return manifest;
|
|
187
185
|
}
|
|
188
186
|
|
|
187
|
+
function createCompatLink(linkPath, targetPath) {
|
|
188
|
+
if (process.platform === 'win32') {
|
|
189
|
+
fs.symlinkSync(targetPath, linkPath, 'junction');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
fs.symlinkSync(path.basename(targetPath), linkPath);
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
/** All user folders under <homeDir>/users. Real directories first, compat
|
|
190
196
|
* symlinks after, both sorted — so scans prefer canonical folders. */
|
|
191
197
|
function listUserEntries(homeDir) {
|
|
@@ -299,7 +305,7 @@ function adoptUserEmail(homeDir, { userId, email }) {
|
|
|
299
305
|
}
|
|
300
306
|
|
|
301
307
|
fs.renameSync(current, target);
|
|
302
|
-
|
|
308
|
+
createCompatLink(current, target);
|
|
303
309
|
indexUserFolder(homeDir, folderName, { userId: cleanId, email });
|
|
304
310
|
return { dir: target, renamed: true, from: current };
|
|
305
311
|
}
|
|
@@ -321,6 +327,7 @@ module.exports = {
|
|
|
321
327
|
emailFolderName,
|
|
322
328
|
ensureRootManifest,
|
|
323
329
|
ensureUserDir,
|
|
330
|
+
createCompatLink,
|
|
324
331
|
findUserDirByUserId,
|
|
325
332
|
hiddenRootPath,
|
|
326
333
|
isAmalgmRoot,
|
package/lib/migrate-layout.js
CHANGED
|
@@ -101,7 +101,7 @@ function migrateRoot({ hiddenRoot, visibleRoot, dryRun, actions }) {
|
|
|
101
101
|
actions.push(`symlink ${hiddenRoot} -> ${path.basename(visibleRoot)}`);
|
|
102
102
|
if (!dryRun) {
|
|
103
103
|
fs.renameSync(hiddenRoot, visibleRoot);
|
|
104
|
-
|
|
104
|
+
layout.createCompatLink(hiddenRoot, visibleRoot);
|
|
105
105
|
}
|
|
106
106
|
return { moved: true };
|
|
107
107
|
}
|
package/lib/state-migration.js
CHANGED
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
|
|
12
12
|
const MARKER_FILE = path.join(AMALGM_DIR, '.user-state-migrated.json');
|
|
13
13
|
const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
|
|
14
|
+
const WORKSPACES_BACKFILL_ID = 'workspaces-merge-v1';
|
|
15
|
+
const CONTEXT_BACKFILL_ID = 'context-merge-v1';
|
|
14
16
|
|
|
15
17
|
const DATA_FILES = [
|
|
16
18
|
'amalgm.db',
|
|
@@ -30,6 +32,7 @@ const DATA_DIRS = [
|
|
|
30
32
|
'apps',
|
|
31
33
|
'browser',
|
|
32
34
|
'browser-sessions',
|
|
35
|
+
'context',
|
|
33
36
|
'contexts',
|
|
34
37
|
'harness-configs',
|
|
35
38
|
'memories',
|
|
@@ -37,6 +40,7 @@ const DATA_DIRS = [
|
|
|
37
40
|
'task-runs',
|
|
38
41
|
'toolbox',
|
|
39
42
|
'uploads',
|
|
43
|
+
'workspaces',
|
|
40
44
|
];
|
|
41
45
|
|
|
42
46
|
function readJson(file, fallback = null) {
|
|
@@ -132,6 +136,8 @@ function migrateLegacyUserState() {
|
|
|
132
136
|
const hadMarker = Boolean(marker);
|
|
133
137
|
const completedMigrations = markerCompletedMigrations(marker);
|
|
134
138
|
const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
|
|
139
|
+
const needsWorkspacesBackfill = !completedMigrations.has(WORKSPACES_BACKFILL_ID);
|
|
140
|
+
const needsContextBackfill = !completedMigrations.has(CONTEXT_BACKFILL_ID);
|
|
135
141
|
|
|
136
142
|
fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
|
|
137
143
|
|
|
@@ -157,7 +163,23 @@ function migrateLegacyUserState() {
|
|
|
157
163
|
completedMigrations.add(CLI_HOMES_BACKFILL_ID);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
if (
|
|
166
|
+
if (needsWorkspacesBackfill) {
|
|
167
|
+
const workspacesSource = path.join(AMALGM_HOME, 'workspaces');
|
|
168
|
+
const workspacesTarget = path.join(AMALGM_DIR, 'workspaces');
|
|
169
|
+
if (mergeDirContents(workspacesSource, workspacesTarget)) copied.push('workspaces/');
|
|
170
|
+
completedMigrations.add(WORKSPACES_BACKFILL_ID);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (needsContextBackfill) {
|
|
174
|
+
// System context (user-authored system-instructions.md) lives in the
|
|
175
|
+
// singular context/ dir; earlier migrations only covered plural contexts/.
|
|
176
|
+
const contextSource = path.join(AMALGM_HOME, 'context');
|
|
177
|
+
const contextTarget = path.join(AMALGM_DIR, 'context');
|
|
178
|
+
if (mergeDirContents(contextSource, contextTarget)) copied.push('context/');
|
|
179
|
+
completedMigrations.add(CONTEXT_BACKFILL_ID);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!hadMarker || copied.length > 0 || needsCliHomesBackfill || needsWorkspacesBackfill || needsContextBackfill) {
|
|
161
183
|
writeJson(MARKER_FILE, {
|
|
162
184
|
migrated_at: marker?.migrated_at || new Date().toISOString(),
|
|
163
185
|
source: AMALGM_HOME,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.139",
|
|
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/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/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/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -86,13 +86,18 @@ const BUILTIN_AGENT_BLUEPRINTS = [
|
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
88
|
|
|
89
|
+
// Every builtin agent starts with the core platform surface. `memories` must
|
|
90
|
+
// be here: project context injected into the prompt names the memory tools,
|
|
91
|
+
// so they have to actually be callable.
|
|
92
|
+
const DEFAULT_BUILTIN_TOOL_IDS = ['agents', 'apps', 'automations', 'browser', 'memories', 'notifications'];
|
|
93
|
+
|
|
89
94
|
const BUILTIN_AGENTS = BUILTIN_AGENT_BLUEPRINTS.map((agent) => ({
|
|
90
95
|
...agent,
|
|
91
96
|
systemPrompt: '',
|
|
92
97
|
files: [],
|
|
93
98
|
skills: [],
|
|
94
99
|
mcpAppIds: [],
|
|
95
|
-
loadout: { toolIds: [] },
|
|
100
|
+
loadout: { toolIds: [...DEFAULT_BUILTIN_TOOL_IDS] },
|
|
96
101
|
mcp: { inheritAll: false, customServers: [], appIds: [] },
|
|
97
102
|
builtin: false,
|
|
98
103
|
deletable: true,
|
|
@@ -462,7 +467,7 @@ function seedBuiltinAgents(options = {}) {
|
|
|
462
467
|
files: existing?.files || [],
|
|
463
468
|
skills: existing?.skills || [],
|
|
464
469
|
mcpAppIds: existing?.mcpAppIds || [],
|
|
465
|
-
loadout: existing?.loadout || existing?.tools || { toolIds: [] },
|
|
470
|
+
loadout: existing?.loadout || existing?.tools || { toolIds: [...DEFAULT_BUILTIN_TOOL_IDS] },
|
|
466
471
|
mcp: {
|
|
467
472
|
inheritAll: false,
|
|
468
473
|
customServers: [],
|
|
@@ -755,6 +760,7 @@ function readAgentConvoLog(sessionId, limit = 50) {
|
|
|
755
760
|
module.exports = {
|
|
756
761
|
BUILTIN_AGENTS,
|
|
757
762
|
BUILTIN_AGENT_IDS,
|
|
763
|
+
DEFAULT_BUILTIN_TOOL_IDS,
|
|
758
764
|
ensureAgentsDirs,
|
|
759
765
|
loadAgents,
|
|
760
766
|
saveAgents,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { spawn } = require('child_process');
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
11
11
|
const { DEFAULT_CWD } = require('../config');
|
|
12
12
|
const { runtimePort } = require('../../../lib/runtime-manifest');
|
|
13
13
|
const { syncAppRoutesToGateway } = require('./advertise');
|
|
@@ -53,6 +53,26 @@ const APP_ENV_ALLOWLIST = [
|
|
|
53
53
|
'PLAYWRIGHT_BROWSERS_PATH',
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
+
function runtimeEnvFingerprint() {
|
|
57
|
+
// The part of the inherited env that must match for a saved PID to be
|
|
58
|
+
// trusted: where the platform stores state. If this moved, the process
|
|
59
|
+
// predates a layout migration and must be respawned.
|
|
60
|
+
return process.env.AMALGM_DIR || '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function killProcessTree(pid) {
|
|
64
|
+
try { process.kill(-pid, 'SIGTERM'); } catch {}
|
|
65
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
66
|
+
const deadline = Date.now() + 3000;
|
|
67
|
+
while (isProcessAlive(pid) && Date.now() < deadline) {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
69
|
+
}
|
|
70
|
+
if (isProcessAlive(pid)) {
|
|
71
|
+
try { process.kill(-pid, 'SIGKILL'); } catch {}
|
|
72
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
function isProcessAlive(pid) {
|
|
57
77
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
58
78
|
try {
|
|
@@ -63,6 +83,79 @@ function isProcessAlive(pid) {
|
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
function parsePids(output) {
|
|
87
|
+
const pids = new Set();
|
|
88
|
+
String(output || '').split(/\s+/).forEach((value) => {
|
|
89
|
+
const pid = Number(value);
|
|
90
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
91
|
+
});
|
|
92
|
+
return Array.from(pids);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function listeningPidsForPort(port) {
|
|
96
|
+
const parsed = Number(port);
|
|
97
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return [];
|
|
98
|
+
const result = spawnSync('lsof', ['-nP', `-iTCP:${parsed}`, '-sTCP:LISTEN', '-t'], {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
});
|
|
101
|
+
if (result.error || !result.stdout) return [];
|
|
102
|
+
return parsePids(result.stdout);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pidCommand(pid) {
|
|
106
|
+
const result = spawnSync('ps', ['eww', '-p', String(pid), '-o', 'command='], {
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
});
|
|
109
|
+
if (result.error || result.status !== 0) return '';
|
|
110
|
+
return String(result.stdout || '');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pidCwd(pid) {
|
|
114
|
+
const result = spawnSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
});
|
|
117
|
+
if (result.error || result.status !== 0) return '';
|
|
118
|
+
const line = String(result.stdout || '').split('\n').find((item) => item.startsWith('n'));
|
|
119
|
+
return line ? line.slice(1) : '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPathInside(parent, maybeChild) {
|
|
123
|
+
if (!parent || !maybeChild) return false;
|
|
124
|
+
const relative = path.relative(path.resolve(parent), path.resolve(maybeChild));
|
|
125
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isAppOwnedPid(app, pid) {
|
|
129
|
+
if (app.pid === pid) return true;
|
|
130
|
+
const command = pidCommand(pid);
|
|
131
|
+
if (command.includes(`AMALGM_APP_ID=${app.id}`) || command.includes(`AMALGM_APP_ID='${app.id}'`)) return true;
|
|
132
|
+
if (app.appRef && (command.includes(`AMALGM_APP_REF=${app.appRef}`) || command.includes(`AMALGM_APP_REF='${app.appRef}'`))) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (app.cwd && command.includes(path.resolve(app.cwd))) return true;
|
|
136
|
+
return isPathInside(app.cwd, pidCwd(pid));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function killOwnedPortListeners(app, options = {}) {
|
|
140
|
+
const pids = listeningPidsForPort(app.port).filter((pid) => pid !== process.pid);
|
|
141
|
+
if (pids.length === 0) return [];
|
|
142
|
+
|
|
143
|
+
const owned = [];
|
|
144
|
+
const unknown = [];
|
|
145
|
+
for (const pid of pids) {
|
|
146
|
+
(isAppOwnedPid(app, pid) ? owned : unknown).push(pid);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (unknown.length > 0 && options.failOnUnknown !== false) {
|
|
150
|
+
throw new Error(`Port ${app.port} is already in use by pid(s) ${unknown.join(', ')}; refusing to kill non-app process`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const pid of owned) {
|
|
154
|
+
await killProcessTree(pid);
|
|
155
|
+
}
|
|
156
|
+
return owned;
|
|
157
|
+
}
|
|
158
|
+
|
|
66
159
|
async function syncRoutesBestEffort(reason) {
|
|
67
160
|
try {
|
|
68
161
|
await syncAppRoutesToGateway(reason);
|
|
@@ -215,24 +308,34 @@ async function startApp(appId) {
|
|
|
215
308
|
}
|
|
216
309
|
|
|
217
310
|
if (app.pid && isProcessAlive(app.pid)) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
311
|
+
if ((app.envFingerprint || '') === runtimeEnvFingerprint()) {
|
|
312
|
+
const current = updateAppMeta(app.id, {
|
|
313
|
+
desiredState: 'running',
|
|
314
|
+
status: 'running',
|
|
315
|
+
error: null,
|
|
316
|
+
});
|
|
317
|
+
await syncRoutesBestEffort(`start:${appId}`);
|
|
318
|
+
return current;
|
|
319
|
+
}
|
|
320
|
+
// The saved process was spawned under a different runtime env (e.g. the
|
|
321
|
+
// storage layout moved); adopting it would keep an app alive against
|
|
322
|
+
// stale AMALGM_* paths. Replace it.
|
|
323
|
+
await killProcessTree(app.pid);
|
|
324
|
+
updateAppMeta(app.id, { pid: null });
|
|
225
325
|
}
|
|
226
326
|
|
|
227
327
|
if (app.pid && !isProcessAlive(app.pid)) {
|
|
228
328
|
updateAppMeta(app.id, { pid: null });
|
|
229
329
|
}
|
|
230
330
|
|
|
331
|
+
await killOwnedPortListeners(app);
|
|
332
|
+
|
|
231
333
|
const child = startAppProcess(app);
|
|
232
334
|
const current = updateAppMeta(app.id, {
|
|
233
335
|
desiredState: 'running',
|
|
234
336
|
status: 'running',
|
|
235
337
|
pid: child.pid,
|
|
338
|
+
envFingerprint: runtimeEnvFingerprint(),
|
|
236
339
|
error: null,
|
|
237
340
|
lastStartedAt: new Date().toISOString(),
|
|
238
341
|
});
|
|
@@ -251,11 +354,13 @@ async function stopApp(appId, options = {}) {
|
|
|
251
354
|
|
|
252
355
|
const child = running.get(appId);
|
|
253
356
|
if (child?.pid) {
|
|
254
|
-
|
|
255
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
357
|
+
await killProcessTree(child.pid);
|
|
256
358
|
} else if (app.pid) {
|
|
257
|
-
|
|
359
|
+
await killProcessTree(app.pid);
|
|
258
360
|
}
|
|
361
|
+
await killOwnedPortListeners(app, { failOnUnknown: false }).catch((err) => {
|
|
362
|
+
console.warn(`[Apps] Could not clean listeners for ${appId} on port ${app.port}: ${err.message}`);
|
|
363
|
+
});
|
|
259
364
|
running.delete(appId);
|
|
260
365
|
|
|
261
366
|
setTimeout(() => stopping.delete(appId), 1000);
|