agentlas 1.0.65 → 1.0.67
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/CHANGELOG.md +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
|
@@ -11,6 +11,7 @@ const fs = require("node:fs");
|
|
|
11
11
|
const os = require("node:os");
|
|
12
12
|
const path = require("node:path");
|
|
13
13
|
const readline = require("node:readline");
|
|
14
|
+
const { randomUUID } = require("node:crypto");
|
|
14
15
|
const i18n = require("./agentlas-i18n.cjs");
|
|
15
16
|
|
|
16
17
|
function userDataDir() {
|
|
@@ -24,8 +25,13 @@ function userDataDir() {
|
|
|
24
25
|
|
|
25
26
|
const HISTORY_MAX = 500;
|
|
26
27
|
const HISTORY_SCHEMA_VERSION = 2;
|
|
28
|
+
const HISTORY_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
const HISTORY_MAX_ENTRY_BYTES = 64 * 1024;
|
|
30
|
+
const HISTORY_MAX_SCOPE_BYTES = 4 * 1024;
|
|
27
31
|
const HISTORY_LOCK_WAIT_MS = 2_000;
|
|
28
32
|
const HISTORY_LOCK_STALE_MS = 30_000;
|
|
33
|
+
const HISTORY_LOCK_OWNER_MAX_BYTES = 1024;
|
|
34
|
+
const HISTORY_LOCK_OWNER_RE = /^\.owner-(\d+)-([0-9a-f-]{36})\.json$/;
|
|
29
35
|
function historyPath() {
|
|
30
36
|
return path.join(userDataDir(), "cli-history.json");
|
|
31
37
|
}
|
|
@@ -34,6 +40,14 @@ function historyBackupPath() {
|
|
|
34
40
|
return historyPath() + ".bak";
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function validHistoryText(value) {
|
|
44
|
+
return typeof value === "string" && value.trim() && Buffer.byteLength(value, "utf8") <= HISTORY_MAX_ENTRY_BYTES;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validHistoryScope(value) {
|
|
48
|
+
return typeof value === "string" && value && Buffer.byteLength(value, "utf8") <= HISTORY_MAX_SCOPE_BYTES && !/[\u0000-\u001f\u007f]/.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
function normalizeHistoryDocument(raw) {
|
|
38
52
|
if (Array.isArray(raw)) {
|
|
39
53
|
// Legacy history had no project identity. Keep it quarantined on disk so
|
|
@@ -41,24 +55,51 @@ function normalizeHistoryDocument(raw) {
|
|
|
41
55
|
return {
|
|
42
56
|
version: HISTORY_SCHEMA_VERSION,
|
|
43
57
|
entries: [],
|
|
44
|
-
legacyUnscoped: raw.filter(
|
|
58
|
+
legacyUnscoped: raw.filter(validHistoryText).slice(0, HISTORY_MAX),
|
|
45
59
|
};
|
|
46
60
|
}
|
|
47
61
|
if (raw && raw.version === HISTORY_SCHEMA_VERSION && Array.isArray(raw.entries)) {
|
|
48
62
|
return {
|
|
49
63
|
version: HISTORY_SCHEMA_VERSION,
|
|
50
|
-
entries: raw.entries
|
|
51
|
-
|
|
64
|
+
entries: raw.entries.filter((entry) => (
|
|
65
|
+
entry && typeof entry === "object" && !Array.isArray(entry) &&
|
|
66
|
+
validHistoryText(entry.text) && validHistoryScope(entry.cwd) &&
|
|
67
|
+
Number.isFinite(Number(entry.ts)) && Number(entry.ts) >= 0
|
|
68
|
+
)).slice(0, HISTORY_MAX),
|
|
69
|
+
legacyUnscoped: Array.isArray(raw.legacyUnscoped) ? raw.legacyUnscoped.filter(validHistoryText).slice(0, HISTORY_MAX) : [],
|
|
52
70
|
};
|
|
53
71
|
}
|
|
54
72
|
return null;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
function readHistoryFile(file) {
|
|
76
|
+
let fd = null;
|
|
58
77
|
try {
|
|
59
|
-
|
|
78
|
+
const before = fs.lstatSync(file);
|
|
79
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > HISTORY_MAX_FILE_BYTES) return null;
|
|
80
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
81
|
+
const opened = fs.fstatSync(fd);
|
|
82
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.size > HISTORY_MAX_FILE_BYTES || !sameFileIdentity(before, opened)) return null;
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let total = 0;
|
|
85
|
+
while (total <= HISTORY_MAX_FILE_BYTES) {
|
|
86
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, HISTORY_MAX_FILE_BYTES + 1 - total));
|
|
87
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
88
|
+
if (!count) break;
|
|
89
|
+
chunks.push(buffer.subarray(0, count));
|
|
90
|
+
total += count;
|
|
91
|
+
}
|
|
92
|
+
const after = fs.fstatSync(fd);
|
|
93
|
+
if (
|
|
94
|
+
total > HISTORY_MAX_FILE_BYTES || !after.isFile() || after.nlink !== 1 || !sameFileIdentity(opened, after) ||
|
|
95
|
+
after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || total !== after.size
|
|
96
|
+
) return null;
|
|
97
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
|
|
98
|
+
return normalizeHistoryDocument(JSON.parse(raw));
|
|
60
99
|
} catch {
|
|
61
100
|
return null;
|
|
101
|
+
} finally {
|
|
102
|
+
if (fd !== null) try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
62
103
|
}
|
|
63
104
|
}
|
|
64
105
|
|
|
@@ -83,23 +124,226 @@ function sleepSync(ms) {
|
|
|
83
124
|
}
|
|
84
125
|
}
|
|
85
126
|
|
|
127
|
+
function sameFileIdentity(a, b) {
|
|
128
|
+
return Boolean(a && b && a.dev === b.dev && a.ino === b.ino);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ensureHistoryDirectory({ create = false } = {}) {
|
|
132
|
+
const directory = path.resolve(userDataDir());
|
|
133
|
+
if (create) fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
134
|
+
const stat = fs.lstatSync(directory);
|
|
135
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("history directory must be a real directory");
|
|
136
|
+
try { fs.chmodSync(directory, 0o700); } catch { /* win32 */ }
|
|
137
|
+
return { path: directory, stat };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isProcessAlive(pid) {
|
|
141
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
142
|
+
try {
|
|
143
|
+
process.kill(pid, 0);
|
|
144
|
+
return true;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return error && error.code === "EPERM";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readHistoryLockOwner(lock) {
|
|
151
|
+
let lockStat;
|
|
152
|
+
try {
|
|
153
|
+
lockStat = fs.lstatSync(lock);
|
|
154
|
+
if (!lockStat.isDirectory()) return null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
let names;
|
|
159
|
+
try {
|
|
160
|
+
names = fs.readdirSync(lock);
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const ownerNames = names.filter((name) => HISTORY_LOCK_OWNER_RE.test(name));
|
|
165
|
+
if (ownerNames.length !== 1) return null;
|
|
166
|
+
const ownerName = ownerNames[0];
|
|
167
|
+
const match = HISTORY_LOCK_OWNER_RE.exec(ownerName);
|
|
168
|
+
const pid = Number(match[1]);
|
|
169
|
+
const nonce = match[2];
|
|
170
|
+
const ownerPath = path.join(lock, ownerName);
|
|
171
|
+
let ownerStat;
|
|
172
|
+
let ownerIdentity;
|
|
173
|
+
try {
|
|
174
|
+
ownerStat = fs.lstatSync(ownerPath);
|
|
175
|
+
if (!ownerStat.isFile() || ownerStat.nlink !== 1 || ownerStat.size <= 0 || ownerStat.size > HISTORY_LOCK_OWNER_MAX_BYTES) return null;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
let payload = null;
|
|
180
|
+
try {
|
|
181
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
182
|
+
const fd = fs.openSync(ownerPath, flags);
|
|
183
|
+
try {
|
|
184
|
+
const current = fs.fstatSync(fd);
|
|
185
|
+
if (
|
|
186
|
+
!current.isFile() || current.nlink !== 1 || current.size <= 0 || current.size > HISTORY_LOCK_OWNER_MAX_BYTES ||
|
|
187
|
+
!sameFileIdentity(ownerStat, current)
|
|
188
|
+
) return null;
|
|
189
|
+
ownerIdentity = current;
|
|
190
|
+
const buffer = Buffer.allocUnsafe(current.size);
|
|
191
|
+
let offset = 0;
|
|
192
|
+
while (offset < buffer.length) {
|
|
193
|
+
const count = fs.readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
194
|
+
if (!count) break;
|
|
195
|
+
offset += count;
|
|
196
|
+
}
|
|
197
|
+
const after = fs.fstatSync(fd);
|
|
198
|
+
if (
|
|
199
|
+
offset !== buffer.length || !sameFileIdentity(current, after) || after.size !== current.size ||
|
|
200
|
+
after.mtimeMs !== current.mtimeMs || after.ctimeMs !== current.ctimeMs
|
|
201
|
+
) return null;
|
|
202
|
+
payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer));
|
|
203
|
+
} finally {
|
|
204
|
+
fs.closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// The filename still carries the owner PID, so a crashed process with a
|
|
208
|
+
// partially-written marker can be reaped once that PID is gone.
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
pid,
|
|
212
|
+
nonce,
|
|
213
|
+
ownerPath,
|
|
214
|
+
identity: ownerIdentity || ownerStat,
|
|
215
|
+
valid: Boolean(
|
|
216
|
+
payload
|
|
217
|
+
&& payload.version === 1
|
|
218
|
+
&& Number(payload.pid) === pid
|
|
219
|
+
&& payload.nonce === nonce,
|
|
220
|
+
),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function removeHistoryLockMarker(lockState) {
|
|
225
|
+
try {
|
|
226
|
+
const current = fs.lstatSync(lockState.lock);
|
|
227
|
+
if (!sameFileIdentity(current, lockState.identity)) return false;
|
|
228
|
+
const marker = fs.lstatSync(lockState.ownerPath);
|
|
229
|
+
if (!marker.isFile() || marker.nlink !== 1) return false;
|
|
230
|
+
if (lockState.ownerIdentity && !sameFileIdentity(marker, lockState.ownerIdentity)) return false;
|
|
231
|
+
fs.unlinkSync(lockState.ownerPath);
|
|
232
|
+
fs.rmdirSync(lockState.lock);
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeClaimedHistoryLock(lockState) {
|
|
240
|
+
try {
|
|
241
|
+
const current = fs.lstatSync(lockState.lock);
|
|
242
|
+
if (!sameFileIdentity(current, lockState.identity)) return false;
|
|
243
|
+
try {
|
|
244
|
+
const marker = fs.lstatSync(lockState.ownerPath);
|
|
245
|
+
if (!marker.isFile() || marker.nlink !== 1) return false;
|
|
246
|
+
fs.unlinkSync(lockState.ownerPath);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (!error || error.code !== "ENOENT") return false;
|
|
249
|
+
}
|
|
250
|
+
fs.rmdirSync(lockState.lock);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function releaseHistoryLock(lockState) {
|
|
258
|
+
if (!lockState) return false;
|
|
259
|
+
const owner = readHistoryLockOwner(lockState.lock);
|
|
260
|
+
if (!owner || !owner.valid || owner.pid !== lockState.pid || owner.nonce !== lockState.nonce) return false;
|
|
261
|
+
return removeHistoryLockMarker({ ...lockState, ownerIdentity: owner.identity });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function reapStaleHistoryLock(lock, stat, owner) {
|
|
265
|
+
if (!owner) {
|
|
266
|
+
try {
|
|
267
|
+
const current = fs.lstatSync(lock);
|
|
268
|
+
if (!sameFileIdentity(current, stat) || !current.isDirectory()) return false;
|
|
269
|
+
// A process can die after mkdir and before writing its owner marker. Once
|
|
270
|
+
// that marker-less directory is stale, removing only the still-empty,
|
|
271
|
+
// identity-matched directory is safe; unknown contents remain fail-closed.
|
|
272
|
+
if (fs.readdirSync(lock).length !== 0) return false;
|
|
273
|
+
fs.rmdirSync(lock);
|
|
274
|
+
return true;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (isProcessAlive(owner.pid)) return false;
|
|
280
|
+
return removeHistoryLockMarker({
|
|
281
|
+
lock,
|
|
282
|
+
identity: stat,
|
|
283
|
+
ownerPath: owner.ownerPath,
|
|
284
|
+
ownerIdentity: owner.identity,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function claimHistoryLock(lock) {
|
|
289
|
+
const identity = fs.lstatSync(lock);
|
|
290
|
+
if (!identity.isDirectory()) throw new Error("history lock is not a directory");
|
|
291
|
+
const pid = process.pid;
|
|
292
|
+
const nonce = randomUUID();
|
|
293
|
+
const ownerPath = path.join(lock, `.owner-${pid}-${nonce}.json`);
|
|
294
|
+
const lockState = { lock, identity, ownerPath, pid, nonce };
|
|
295
|
+
let fd = null;
|
|
296
|
+
try {
|
|
297
|
+
fd = fs.openSync(ownerPath, "wx", 0o600);
|
|
298
|
+
fs.writeFileSync(fd, JSON.stringify({ version: 1, pid, nonce, createdAt: Date.now() }) + "\n", "utf8");
|
|
299
|
+
fs.fsyncSync(fd);
|
|
300
|
+
fs.closeSync(fd);
|
|
301
|
+
fd = null;
|
|
302
|
+
const owner = readHistoryLockOwner(lock);
|
|
303
|
+
const verifiedIdentity = fs.lstatSync(lock);
|
|
304
|
+
if (!owner || !owner.valid || owner.pid !== pid || owner.nonce !== nonce || owner.ownerPath !== ownerPath) {
|
|
305
|
+
throw new Error("history lock owner verification failed");
|
|
306
|
+
}
|
|
307
|
+
if (!sameFileIdentity(identity, verifiedIdentity)) throw new Error("history lock identity changed");
|
|
308
|
+
return lockState;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (fd != null) {
|
|
311
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
312
|
+
fd = null;
|
|
313
|
+
}
|
|
314
|
+
removeClaimedHistoryLock(lockState);
|
|
315
|
+
throw error;
|
|
316
|
+
} finally {
|
|
317
|
+
if (fd != null) {
|
|
318
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
86
323
|
function withHistoryLock(callback) {
|
|
87
324
|
const lock = historyPath() + ".lock";
|
|
88
325
|
const deadline = Date.now() + HISTORY_LOCK_WAIT_MS;
|
|
326
|
+
let lockState = null;
|
|
89
327
|
while (true) {
|
|
328
|
+
let created = false;
|
|
90
329
|
try {
|
|
91
330
|
fs.mkdirSync(lock, { mode: 0o700 });
|
|
331
|
+
created = true;
|
|
332
|
+
lockState = claimHistoryLock(lock);
|
|
92
333
|
break;
|
|
93
334
|
} catch (error) {
|
|
335
|
+
if (created) throw error;
|
|
94
336
|
if (!error || error.code !== "EEXIST") throw error;
|
|
95
337
|
try {
|
|
96
|
-
const stat = fs.
|
|
97
|
-
if (Date.now() - stat.mtimeMs > HISTORY_LOCK_STALE_MS) {
|
|
98
|
-
|
|
99
|
-
continue;
|
|
338
|
+
const stat = fs.lstatSync(lock);
|
|
339
|
+
if (stat.isDirectory() && Date.now() - stat.mtimeMs > HISTORY_LOCK_STALE_MS) {
|
|
340
|
+
const owner = readHistoryLockOwner(lock);
|
|
341
|
+
if (reapStaleHistoryLock(lock, stat, owner)) continue;
|
|
100
342
|
}
|
|
101
|
-
} catch {
|
|
102
|
-
|
|
343
|
+
} catch (statError) {
|
|
344
|
+
// If the contender disappeared, retry immediately. Permission and I/O
|
|
345
|
+
// failures must still respect the bounded wait instead of busy-spinning.
|
|
346
|
+
if (statError && statError.code === "ENOENT") continue;
|
|
103
347
|
}
|
|
104
348
|
if (Date.now() >= deadline) throw new Error("history lock timeout");
|
|
105
349
|
sleepSync(10);
|
|
@@ -108,21 +352,33 @@ function withHistoryLock(callback) {
|
|
|
108
352
|
try {
|
|
109
353
|
return callback();
|
|
110
354
|
} finally {
|
|
111
|
-
|
|
355
|
+
releaseHistoryLock(lockState);
|
|
112
356
|
}
|
|
113
357
|
}
|
|
114
358
|
|
|
115
359
|
function atomicWriteHistoryFile(file, document) {
|
|
116
360
|
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
117
|
-
|
|
361
|
+
const directory = ensureHistoryDirectory();
|
|
362
|
+
const payload = Buffer.from(JSON.stringify(document), "utf8");
|
|
363
|
+
if (payload.length <= 0 || payload.length > HISTORY_MAX_FILE_BYTES) throw new Error("history document exceeds its safety limit");
|
|
364
|
+
let fd = null;
|
|
118
365
|
try {
|
|
119
|
-
fd = fs.openSync(tmp,
|
|
120
|
-
fs.
|
|
366
|
+
fd = fs.openSync(tmp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
367
|
+
const created = fs.fstatSync(fd);
|
|
368
|
+
if (!created.isFile() || created.nlink !== 1) throw new Error("history temp file is unsafe");
|
|
369
|
+
let offset = 0;
|
|
370
|
+
while (offset < payload.length) offset += fs.writeSync(fd, payload, offset, payload.length - offset);
|
|
121
371
|
fs.fsyncSync(fd);
|
|
372
|
+
fs.fchmodSync(fd, 0o600);
|
|
373
|
+
const written = fs.fstatSync(fd);
|
|
374
|
+
if (!sameFileIdentity(created, written) || written.size !== payload.length) throw new Error("history temp file changed while writing");
|
|
122
375
|
fs.closeSync(fd);
|
|
123
376
|
fd = null;
|
|
377
|
+
const currentDirectory = fs.lstatSync(directory.path);
|
|
378
|
+
if (!sameFileIdentity(directory.stat, currentDirectory) || !currentDirectory.isDirectory() || currentDirectory.isSymbolicLink()) {
|
|
379
|
+
throw new Error("history directory changed while writing");
|
|
380
|
+
}
|
|
124
381
|
fs.renameSync(tmp, file);
|
|
125
|
-
try { fs.chmodSync(file, 0o600); } catch { /* win32 */ }
|
|
126
382
|
} finally {
|
|
127
383
|
if (fd != null) {
|
|
128
384
|
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
@@ -151,8 +407,8 @@ function loadHistory(cwd = process.cwd()) {
|
|
|
151
407
|
|
|
152
408
|
function saveHistory(list, cwd = process.cwd()) {
|
|
153
409
|
try {
|
|
154
|
-
|
|
155
|
-
const clean = (list || []).filter(
|
|
410
|
+
ensureHistoryDirectory({ create: true });
|
|
411
|
+
const clean = (list || []).filter(validHistoryText).slice(0, HISTORY_MAX);
|
|
156
412
|
const scope = historyScope(cwd);
|
|
157
413
|
return withHistoryLock(() => {
|
|
158
414
|
const currentFileWasValid = Boolean(readHistoryFile(historyPath()));
|
|
@@ -263,7 +519,6 @@ const SLASH_COMMAND_META = [
|
|
|
263
519
|
{ command: "/help", description: "Show Agentlas terminal commands", category: "Help", usage: "/help", detail: "Open the command reference, shortcuts, and common flows." },
|
|
264
520
|
{ command: "/status", description: "Show model/runtime, agent, permission, and directory", category: "Session", usage: "/status", detail: "Print the current runtime, active agent or company, permission level, and cwd." },
|
|
265
521
|
{ command: "/skills", description: "List available Agentlas terminal skills", category: "Discovery", usage: "/skills", detail: "Show the slash-command skills Agentlas can run inside this terminal." },
|
|
266
|
-
{ command: "/career-graph", description: "Show or add Career Graph source refs", category: "Knowledge", usage: "/career-graph add ./docs", detail: "Career Graph routes agents to source Markdown, JSONL ledgers, sitemap, and code map before broad scans.", examples: ["/career-graph status", "/career-graph add ./docs", "/career-graph open"] },
|
|
267
522
|
{ command: "/ontology", description: "Turn on, list, or add project ontology sources", category: "Knowledge", usage: "/ontology add ./docs", detail: "Also understands natural text like /ontology use ./docs as company knowledge.", examples: ["/ontology list", "/ontology use ./docs as company knowledge", "/ontology open"] },
|
|
268
523
|
{ command: "/agents", description: "List installed agents", category: "Routing", usage: "/agents", detail: "Show local agents and their routed runtime." },
|
|
269
524
|
{ command: "/team", description: "View or pin each agent runtime", category: "Routing", usage: "/team <agent> <runtime|auto>", detail: "Pin one agent to claude-code, codex, gemini, or automatic routing." },
|
|
@@ -314,7 +569,6 @@ const HELP_KEY_BY_COMMAND = {
|
|
|
314
569
|
"/help": "help.help",
|
|
315
570
|
"/status": "help.status",
|
|
316
571
|
"/skills": "help.skills",
|
|
317
|
-
"/career-graph": "help.careerGraph",
|
|
318
572
|
"/ontology": "help.ontology",
|
|
319
573
|
"/agents": "help.agents",
|
|
320
574
|
"/team": "help.team",
|
|
@@ -860,7 +1114,6 @@ function makeCompleter(ctx) {
|
|
|
860
1114
|
return [uniqStartsWith(RUNTIME_SPECS.concat(["auto"]), last), last];
|
|
861
1115
|
case "/cwd":
|
|
862
1116
|
case "/import":
|
|
863
|
-
case "/career-graph":
|
|
864
1117
|
case "/ontology":
|
|
865
1118
|
return [completePath(last, getCwd(), ""), last];
|
|
866
1119
|
default:
|
|
Binary file
|
|
@@ -68,31 +68,85 @@ function findEnvValue(source, key) {
|
|
|
68
68
|
return match ? source[match] : undefined;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
function samePath(left, right) {
|
|
72
|
+
const a = path.normalize(String(left || ""));
|
|
73
|
+
const b = path.normalize(String(right || ""));
|
|
74
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sameDirectoryIdentity(left, right) {
|
|
78
|
+
return !!left && !!right
|
|
79
|
+
&& left.isDirectory() && right.isDirectory()
|
|
80
|
+
&& !left.isSymbolicLink() && !right.isSymbolicLink()
|
|
81
|
+
&& left.dev === right.dev && left.ino === right.ino;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function realPath(directory) {
|
|
85
|
+
// Keep the non-native resolver on Windows so HOMEDRIVE/HOMEPATH retain the
|
|
86
|
+
// normal Win32 spelling; POSIX still gets the platform's canonical path.
|
|
87
|
+
return fs.realpathSync(path.resolve(directory));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inspectPrivateDirectory(directory) {
|
|
91
|
+
const requestedPath = path.resolve(String(directory || ""));
|
|
92
|
+
const stat = fs.lstatSync(requestedPath);
|
|
93
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
94
|
+
throw new Error("MCP runtime home must be a real directory");
|
|
95
|
+
}
|
|
96
|
+
const realpath = realPath(requestedPath);
|
|
97
|
+
// A swap between the first lstat and realpath must not become the anchor
|
|
98
|
+
// returned to callers. Re-read the pathname and retain both identities.
|
|
99
|
+
const verified = fs.lstatSync(requestedPath);
|
|
100
|
+
if (!sameDirectoryIdentity(stat, verified)) {
|
|
101
|
+
throw new Error("MCP runtime directory changed during inspection");
|
|
102
|
+
}
|
|
103
|
+
return { requestedPath, stat: verified, realpath };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function assertPrivateDirectoryIdentity(expected, label = "MCP runtime directory") {
|
|
107
|
+
const actual = inspectPrivateDirectory(expected.requestedPath);
|
|
108
|
+
if (!sameDirectoryIdentity(expected.stat, actual.stat) || !samePath(expected.realpath, actual.realpath)) {
|
|
109
|
+
throw new Error(`${label} changed during setup`);
|
|
110
|
+
}
|
|
111
|
+
return actual;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensurePrivateDirectoryInfo(directory) {
|
|
115
|
+
const requestedPath = path.resolve(String(directory || ""));
|
|
116
|
+
fs.mkdirSync(requestedPath, { recursive: true, mode: 0o700 });
|
|
117
|
+
const before = inspectPrivateDirectory(requestedPath);
|
|
118
|
+
try { fs.chmodSync(requestedPath, 0o700); } catch { /* Windows/best-effort */ }
|
|
119
|
+
const after = inspectPrivateDirectory(requestedPath);
|
|
120
|
+
if (!sameDirectoryIdentity(before.stat, after.stat) || !samePath(before.realpath, after.realpath)) {
|
|
121
|
+
throw new Error("MCP runtime directory changed during setup");
|
|
122
|
+
}
|
|
123
|
+
return after;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isStrictChild(parent, child) {
|
|
127
|
+
const relative = path.relative(parent, child);
|
|
128
|
+
return !!relative && !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`);
|
|
77
129
|
}
|
|
78
130
|
|
|
79
131
|
function mcpRuntimeHome(dataDir, serverIdentity) {
|
|
80
|
-
const root = path.resolve(dataDir || path.join(os.tmpdir(), "agentlas-mcp-runtime"));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
const root = ensurePrivateDirectoryInfo(path.resolve(dataDir || path.join(os.tmpdir(), "agentlas-mcp-runtime")));
|
|
133
|
+
const parent = ensurePrivateDirectoryInfo(path.join(root.realpath, "mcp-runtime-homes"));
|
|
134
|
+
if (!isStrictChild(root.realpath, parent.realpath)) throw new Error("MCP runtime home escaped its private root");
|
|
135
|
+
assertPrivateDirectoryIdentity(root, "MCP runtime root");
|
|
136
|
+
assertPrivateDirectoryIdentity(parent, "MCP runtime parent");
|
|
137
|
+
|
|
84
138
|
const digest = crypto.createHash("sha256").update(String(serverIdentity || "mcp"), "utf8").digest("hex").slice(0, 32);
|
|
85
|
-
const target = path.join(parent, digest);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return target;
|
|
139
|
+
const target = ensurePrivateDirectoryInfo(path.join(parent.realpath, digest));
|
|
140
|
+
if (!isStrictChild(parent.realpath, target.realpath)) throw new Error("MCP runtime home escaped its private parent");
|
|
141
|
+
assertPrivateDirectoryIdentity(root, "MCP runtime root");
|
|
142
|
+
assertPrivateDirectoryIdentity(parent, "MCP runtime parent");
|
|
143
|
+
assertPrivateDirectoryIdentity(target, "MCP runtime target");
|
|
144
|
+
return target.realpath;
|
|
92
145
|
}
|
|
93
146
|
|
|
94
147
|
function safeBaseEnvironment(source, options = {}) {
|
|
95
|
-
const
|
|
148
|
+
const home = ensurePrivateDirectoryInfo(path.resolve(options.runtimeHome || mcpRuntimeHome(null, options.serverIdentity)));
|
|
149
|
+
const runtimeHome = home.realpath;
|
|
96
150
|
const result = {};
|
|
97
151
|
for (const key of SAFE_BASE_ENV_KEYS) {
|
|
98
152
|
const value = findEnvValue(source, key);
|
|
@@ -100,15 +154,22 @@ function safeBaseEnvironment(source, options = {}) {
|
|
|
100
154
|
}
|
|
101
155
|
// Never expose the user's real home through the environment. MCPs that need
|
|
102
156
|
// credentials must use their declared registry key, not ambient dotfiles.
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
// Anchor HOME before creating TMP so a target/parent swap cannot redirect
|
|
158
|
+
// the mkdir through a successor symlink.
|
|
159
|
+
assertPrivateDirectoryIdentity(home, "MCP runtime home");
|
|
160
|
+
const runtimeTmpInfo = ensurePrivateDirectoryInfo(path.join(runtimeHome, "tmp"));
|
|
161
|
+
assertPrivateDirectoryIdentity(home, "MCP runtime home");
|
|
162
|
+
const runtimeTmp = assertPrivateDirectoryIdentity(runtimeTmpInfo, "MCP runtime tmp").realpath;
|
|
163
|
+
if (!samePath(path.dirname(runtimeTmp), runtimeHome)) throw new Error("MCP runtime tmp escaped its private home");
|
|
164
|
+
const canonicalHome = assertPrivateDirectoryIdentity(home, "MCP runtime home").realpath;
|
|
165
|
+
result.HOME = canonicalHome;
|
|
105
166
|
result.TMPDIR = runtimeTmp;
|
|
106
167
|
result.TMP = runtimeTmp;
|
|
107
168
|
result.TEMP = runtimeTmp;
|
|
108
169
|
if (process.platform === "win32") {
|
|
109
|
-
result.USERPROFILE =
|
|
110
|
-
result.HOMEDRIVE = path.parse(
|
|
111
|
-
result.HOMEPATH =
|
|
170
|
+
result.USERPROFILE = canonicalHome;
|
|
171
|
+
result.HOMEDRIVE = path.parse(canonicalHome).root.replace(/[\\/]$/, "") || "C:";
|
|
172
|
+
result.HOMEPATH = canonicalHome.slice(result.HOMEDRIVE.length) || "\\";
|
|
112
173
|
}
|
|
113
174
|
return result;
|
|
114
175
|
}
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
const { spawn } = require("node:child_process");
|
|
9
9
|
const { buildMcpChildEnv, decodeLaunchDescriptor } = require("./agentlas-mcp-env.cjs");
|
|
10
10
|
|
|
11
|
+
const CHILD_SHUTDOWN_GRACE_MS = 5_000;
|
|
12
|
+
|
|
11
13
|
function fail(code) {
|
|
12
14
|
process.stderr.write(`agentlas MCP wrapper: ${code}\n`);
|
|
13
15
|
process.exitCode = 1;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
let descriptor;
|
|
17
|
-
try {
|
|
19
|
+
try {
|
|
20
|
+
if (process.argv.length !== 3) throw new Error("invalid wrapper arguments");
|
|
21
|
+
descriptor = decodeLaunchDescriptor(process.argv[2]);
|
|
22
|
+
}
|
|
18
23
|
catch { fail("invalid_launch_descriptor"); }
|
|
19
24
|
|
|
20
25
|
if (descriptor) {
|
|
@@ -25,6 +30,7 @@ if (descriptor) {
|
|
|
25
30
|
env: buildMcpChildEnv(process.env, descriptor.credentialKeyNames, {
|
|
26
31
|
runtimeHome: descriptor.runtimeHome,
|
|
27
32
|
}),
|
|
33
|
+
detached: process.platform !== "win32",
|
|
28
34
|
stdio: ["inherit", "inherit", "inherit"],
|
|
29
35
|
windowsHide: true,
|
|
30
36
|
});
|
|
@@ -33,15 +39,41 @@ if (descriptor) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
if (child) {
|
|
36
|
-
|
|
42
|
+
let terminationSignal = null;
|
|
43
|
+
let shutdownTimer = null;
|
|
44
|
+
const clearShutdownTimer = () => {
|
|
45
|
+
if (shutdownTimer) clearTimeout(shutdownTimer);
|
|
46
|
+
shutdownTimer = null;
|
|
47
|
+
};
|
|
48
|
+
const terminateChild = (signal) => {
|
|
49
|
+
const pid = Number(child.pid);
|
|
50
|
+
if (process.platform !== "win32" && Number.isInteger(pid) && pid > 1) {
|
|
51
|
+
try { process.kill(-pid, signal); return; } catch { /* fall through */ }
|
|
52
|
+
}
|
|
37
53
|
try { child.kill(signal); } catch { /* child already stopped */ }
|
|
38
54
|
};
|
|
55
|
+
const relay = (signal) => {
|
|
56
|
+
if (terminationSignal) {
|
|
57
|
+
terminateChild("SIGKILL");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
terminationSignal = signal;
|
|
61
|
+
terminateChild(signal);
|
|
62
|
+
shutdownTimer = setTimeout(() => {
|
|
63
|
+
terminateChild("SIGKILL");
|
|
64
|
+
}, CHILD_SHUTDOWN_GRACE_MS);
|
|
65
|
+
if (typeof shutdownTimer.unref === "function") shutdownTimer.unref();
|
|
66
|
+
};
|
|
39
67
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
40
68
|
try { process.on(signal, () => relay(signal)); } catch { /* unavailable on this platform */ }
|
|
41
69
|
}
|
|
42
|
-
child.once("error", () =>
|
|
70
|
+
child.once("error", () => {
|
|
71
|
+
clearShutdownTimer();
|
|
72
|
+
fail("child_spawn_failed");
|
|
73
|
+
});
|
|
43
74
|
child.once("close", (code, signal) => {
|
|
44
|
-
|
|
75
|
+
clearShutdownTimer();
|
|
76
|
+
if (terminationSignal || signal) {
|
|
45
77
|
process.exitCode = 1;
|
|
46
78
|
return;
|
|
47
79
|
}
|
|
@@ -32,6 +32,7 @@ const CONFIDENCE_LEVELS = new Set(["high", "medium", "low"]);
|
|
|
32
32
|
|
|
33
33
|
const { SECRET_PATTERNS } = require("./agentlas-secret-patterns.cjs");
|
|
34
34
|
const { runWriteTransaction } = require("./agentlas-sqlite-policy.cjs");
|
|
35
|
+
const permissions = require("./agentlas-permissions.cjs");
|
|
35
36
|
const ABSOLUTE_PATH_PATTERNS = [
|
|
36
37
|
/(?:^|[\s("'`])~\/[A-Za-z0-9._-]/,
|
|
37
38
|
/(?:^|[\s("'`])\/(?:Users|home|private|var|tmp|opt|etc|Volumes|Applications|System|Library)\//,
|
|
@@ -67,7 +68,7 @@ function stableJson(value) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function normalizePermission(value) {
|
|
70
|
-
return
|
|
71
|
+
return permissions.normalize(value, "read");
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function projectKey(projectPath) {
|