agentlas 0.7.0 → 0.9.2

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -1,218 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const assert = require("node:assert/strict");
5
- const { EventEmitter } = require("node:events");
6
- const { PassThrough } = require("node:stream");
7
- const { fetchHubCli, hubTimeoutConfig } = require("../engine/agentlas.cjs");
8
- const { runNativeTurn, nativeTimeoutConfig } = require("../engine/agentlas-native-host.cjs");
9
-
10
- function delayedResponse(parts, delayMs, options = {}) {
11
- let timer = null;
12
- let index = 0;
13
- return new Response(new ReadableStream({
14
- start(controller) {
15
- const push = () => {
16
- if (index >= parts.length) {
17
- if (!options.stall) controller.close();
18
- return;
19
- }
20
- controller.enqueue(Buffer.from(parts[index++]));
21
- timer = setTimeout(push, delayMs);
22
- };
23
- timer = setTimeout(push, options.immediate ? 0 : delayMs);
24
- },
25
- cancel() {
26
- if (timer) clearTimeout(timer);
27
- },
28
- }), { status: options.status || 200, headers: { "content-type": "application/json" } });
29
- }
30
-
31
- class FakeChild extends EventEmitter {
32
- constructor(options = {}) {
33
- super();
34
- this.stdout = new PassThrough();
35
- this.stderr = new PassThrough();
36
- this.signals = [];
37
- this.ignoreTerm = !!options.ignoreTerm;
38
- this.closed = false;
39
- }
40
-
41
- kill(signal) {
42
- this.signals.push(signal);
43
- if (signal === "SIGTERM" && this.ignoreTerm) return true;
44
- this.finish(null, signal);
45
- return true;
46
- }
47
-
48
- finish(code = 0, signal = null) {
49
- if (this.closed) return;
50
- this.closed = true;
51
- this.stdout.end();
52
- this.stderr.end();
53
- queueMicrotask(() => this.emit("close", code, signal));
54
- }
55
- }
56
-
57
- function fakeUi() {
58
- const errors = [];
59
- return {
60
- errors,
61
- c: { faint: (value) => value, italic: (value) => value },
62
- status() {},
63
- error(value) { errors.push(String(value)); },
64
- warn() {},
65
- info() {},
66
- line() {},
67
- tool() {},
68
- toolResult() {},
69
- streamStart() {},
70
- streamDelta() {},
71
- streamEnd() {},
72
- stopSpinner() {},
73
- cost() {},
74
- };
75
- }
76
-
77
- function nativeRequest(child, timeoutConfig, ui = fakeUi()) {
78
- return {
79
- kind: "gemini",
80
- bin: "fake-gemini",
81
- prompt: "test",
82
- systemPrompt: "system",
83
- cwd: process.cwd(),
84
- permission: "read",
85
- session: {},
86
- env: {},
87
- ui,
88
- timeoutConfig,
89
- spawn: () => child,
90
- };
91
- }
92
-
93
- async function testHubTimeouts() {
94
- assert.deepEqual(
95
- hubTimeoutConfig({
96
- AGENTLAS_HUB_CONNECT_TIMEOUT_MS: "NaN",
97
- AGENTLAS_HUB_IDLE_TIMEOUT_MS: "Infinity",
98
- AGENTLAS_HUB_TOTAL_TIMEOUT_MS: "not-a-number",
99
- }),
100
- { connectMs: 15_000, idleMs: 30_000, totalMs: 180_000 },
101
- );
102
- assert.deepEqual(
103
- hubTimeoutConfig({
104
- AGENTLAS_HUB_CONNECT_TIMEOUT_MS: "-1",
105
- AGENTLAS_HUB_IDLE_TIMEOUT_MS: "0",
106
- AGENTLAS_HUB_TOTAL_TIMEOUT_MS: "999999999999",
107
- }),
108
- { connectMs: 1_000, idleMs: 1_000, totalMs: 900_000 },
109
- );
110
-
111
- await assert.rejects(
112
- fetchHubCli("https://blackhole.invalid", {}, {
113
- fetch: () => new Promise(() => {}),
114
- timeoutConfig: { connectMs: 25, idleMs: 100, totalMs: 200 },
115
- }),
116
- (error) => error && error.code === "AGENTLAS_HUB_CONNECT_TIMEOUT",
117
- );
118
-
119
- await assert.rejects(
120
- fetchHubCli("https://idle.invalid", {}, {
121
- fetch: async () => delayedResponse(["{"], 1, { immediate: true, stall: true }),
122
- timeoutConfig: { connectMs: 50, idleMs: 30, totalMs: 200 },
123
- }),
124
- (error) => error && error.code === "AGENTLAS_HUB_IDLE_TIMEOUT",
125
- );
126
-
127
- await assert.rejects(
128
- fetchHubCli("https://total.invalid", {}, {
129
- fetch: async () => delayedResponse(Array(30).fill(" "), 10, { immediate: true }),
130
- timeoutConfig: { connectMs: 50, idleMs: 30, totalMs: 75 },
131
- }),
132
- (error) => error && error.code === "AGENTLAS_HUB_TOTAL_TIMEOUT",
133
- );
134
-
135
- const streamed = await fetchHubCli("https://stream.invalid", {}, {
136
- fetch: async () => delayedResponse(["{\"", "ok", "\":", "true", "}"], 15),
137
- timeoutConfig: { connectMs: 50, idleMs: 35, totalMs: 250 },
138
- });
139
- assert.equal(streamed.ok, true);
140
- assert.deepEqual(JSON.parse(streamed.text), { ok: true }, "regular chunks must keep the idle watchdog alive");
141
-
142
- const abort = new AbortController();
143
- const aborted = fetchHubCli("https://abort.invalid", { signal: abort.signal }, {
144
- fetch: () => new Promise(() => {}),
145
- timeoutConfig: { connectMs: 200, idleMs: 200, totalMs: 300 },
146
- });
147
- setTimeout(() => abort.abort(new Error("caller stop")), 15);
148
- await assert.rejects(aborted, /caller stop/);
149
- }
150
-
151
- async function testNativeTimeouts() {
152
- assert.deepEqual(
153
- nativeTimeoutConfig({
154
- AGENTLAS_NATIVE_IDLE_TIMEOUT_MS: "NaN",
155
- AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS: "Infinity",
156
- AGENTLAS_NATIVE_KILL_GRACE_MS: "bad",
157
- }),
158
- { idleMs: 600_000, totalMs: 14_400_000, killGraceMs: 3_000 },
159
- );
160
- assert.deepEqual(
161
- nativeTimeoutConfig({
162
- AGENTLAS_NATIVE_IDLE_TIMEOUT_MS: "-1",
163
- AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS: "999999999999",
164
- AGENTLAS_NATIVE_KILL_GRACE_MS: "999999999999",
165
- }),
166
- { idleMs: 5_000, totalMs: 43_200_000, killGraceMs: 15_000 },
167
- );
168
-
169
- const silent = new FakeChild({ ignoreTerm: true });
170
- const silentResult = await runNativeTurn(nativeRequest(silent, { idleMs: 30, totalMs: 250, killGraceMs: 15 }));
171
- assert.match(silentResult.error || "", /idle timeout/);
172
- assert.deepEqual(silent.signals, ["SIGTERM", "SIGKILL"], "silent child must escalate when SIGTERM is ignored");
173
-
174
- const streaming = new FakeChild();
175
- const streamingRun = runNativeTurn(nativeRequest(streaming, { idleMs: 35, totalMs: 250, killGraceMs: 15 }));
176
- const chunks = ["one", "two", "three", "four"];
177
- chunks.forEach((content, index) => {
178
- setTimeout(() => streaming.stdout.write(`${JSON.stringify({ type: "message", role: "assistant", content })}\n`), 15 + index * 20);
179
- });
180
- setTimeout(() => {
181
- streaming.stdout.write(`${JSON.stringify({ type: "result", status: "success", stats: {} })}\n`);
182
- streaming.finish(0);
183
- }, 105);
184
- const streamingResult = await streamingRun;
185
- assert.equal(streamingResult.error, null);
186
- assert.equal(streamingResult.text, "onetwothreefour");
187
- assert.deepEqual(streaming.signals, [], "active streaming must not be killed by the idle watchdog");
188
-
189
- const endless = new FakeChild({ ignoreTerm: true });
190
- const endlessRun = runNativeTurn(nativeRequest(endless, { idleMs: 35, totalMs: 85, killGraceMs: 15 }));
191
- const activity = setInterval(() => endless.stderr.write("progress\n"), 12);
192
- const endlessResult = await endlessRun;
193
- clearInterval(activity);
194
- assert.match(endlessResult.error || "", /total timeout/);
195
- assert.deepEqual(endless.signals, ["SIGTERM", "SIGKILL"], "total timeout must cap even an active stream");
196
-
197
- const cancellable = new FakeChild();
198
- const controller = new AbortController();
199
- const cancelRun = runNativeTurn({
200
- ...nativeRequest(cancellable, { idleMs: 200, totalMs: 300, killGraceMs: 15 }),
201
- signal: controller.signal,
202
- });
203
- setTimeout(() => controller.abort(), 15);
204
- const cancelResult = await cancelRun;
205
- assert.equal(cancelResult.error, "aborted");
206
- assert.deepEqual(cancellable.signals, ["SIGTERM"]);
207
- }
208
-
209
- async function main() {
210
- await testHubTimeouts();
211
- await testNativeTimeouts();
212
- console.log("timeout-regression: PASS");
213
- }
214
-
215
- main().catch((error) => {
216
- console.error(error);
217
- process.exitCode = 1;
218
- });
@@ -1,165 +0,0 @@
1
- "use strict";
2
-
3
- const assert = require("node:assert/strict");
4
- const fs = require("node:fs");
5
- const os = require("node:os");
6
- const path = require("node:path");
7
- const { runTool } = require("../engine/agentlas-tools.cjs");
8
-
9
- const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-tool-boundary-"));
10
- const workspace = path.join(fixture, "workspace");
11
- const outside = path.join(fixture, "outside");
12
- fs.mkdirSync(path.join(workspace, "docs"), { recursive: true });
13
- fs.mkdirSync(outside, { recursive: true });
14
- fs.writeFileSync(path.join(workspace, "docs", "guide.md"), "alpha\n", "utf8");
15
- fs.writeFileSync(path.join(workspace, "docs", "file..md"), "valid dots\n", "utf8");
16
- fs.writeFileSync(path.join(outside, "secret.txt"), "outside-secret\n", "utf8");
17
-
18
- const readCtx = { cwd: workspace, permission: "read" };
19
- const writeCtx = { cwd: workspace, permission: "write" };
20
-
21
- function expectAllowed(name, args, ctx = readCtx) {
22
- const result = runTool(name, args, ctx);
23
- assert.equal(result.ok, true, `${name} unexpectedly failed: ${result.content}`);
24
- return result.content;
25
- }
26
-
27
- function expectDenied(name, args, ctx = readCtx) {
28
- const result = runTool(name, args, ctx);
29
- assert.equal(result.ok, false, `${name} unexpectedly escaped the workspace`);
30
- assert.match(result.content, /workspace path denied:/, `${name} did not use the workspace boundary`);
31
- return result.content;
32
- }
33
-
34
- try {
35
- // Normal workspace-relative reads, creates, and edits must keep working.
36
- assert.equal(expectAllowed("read_file", { path: "docs/guide.md" }), "alpha\n");
37
- assert.equal(expectAllowed("read_file", { path: "docs/file..md" }), "valid dots\n");
38
- assert.match(expectAllowed("list_dir", { path: "docs" }), /guide\.md/);
39
- expectAllowed("write_file", { path: "notes/new.md", content: "draft\n" }, writeCtx);
40
- expectAllowed(
41
- "edit_file",
42
- { path: "notes/new.md", old_string: "draft", new_string: "ready" },
43
- writeCtx,
44
- );
45
- assert.equal(fs.readFileSync(path.join(workspace, "notes", "new.md"), "utf8"), "ready\n");
46
-
47
- const outsideSecret = path.join(outside, "secret.txt");
48
- const originalSecret = fs.readFileSync(outsideSecret, "utf8");
49
-
50
- // All absolute path dialects are denied, including an absolute path that
51
- // happens to point back into the workspace.
52
- for (const [name, args, ctx] of [
53
- ["list_dir", { path: outside }, readCtx],
54
- ["read_file", { path: outsideSecret }, readCtx],
55
- ["read_file", { path: path.join(workspace, "docs", "guide.md") }, readCtx],
56
- ["write_file", { path: outsideSecret, content: "changed\n" }, writeCtx],
57
- ["edit_file", { path: outsideSecret, old_string: "outside", new_string: "changed" }, writeCtx],
58
- ["read_file", { path: "C:\\Windows\\System32\\drivers\\etc\\hosts" }, readCtx],
59
- ["read_file", { path: "C:relative-drive-path.txt" }, readCtx],
60
- ["read_file", { path: "\\\\server\\share\\secret.txt" }, readCtx],
61
- ]) {
62
- expectDenied(name, args, ctx);
63
- }
64
- assert.equal(fs.readFileSync(outsideSecret, "utf8"), originalSecret, "absolute path changed outside data");
65
-
66
- // A parent segment is denied before normalization, even if it would land
67
- // back inside the workspace or uses the other platform's separator.
68
- for (const [name, args, ctx] of [
69
- ["list_dir", { path: "../outside" }, readCtx],
70
- ["read_file", { path: "../outside/secret.txt" }, readCtx],
71
- ["read_file", { path: "docs/../docs/guide.md" }, readCtx],
72
- ["read_file", { path: "..\\outside\\secret.txt" }, readCtx],
73
- ["write_file", { path: "../outside/created.txt", content: "escape\n" }, writeCtx],
74
- ["edit_file", { path: "../outside/secret.txt", old_string: "outside", new_string: "changed" }, writeCtx],
75
- ]) {
76
- expectDenied(name, args, ctx);
77
- }
78
- assert.equal(fs.existsSync(path.join(outside, "created.txt")), false, "traversal created an outside file");
79
- assert.equal(fs.readFileSync(outsideSecret, "utf8"), originalSecret, "traversal changed outside data");
80
-
81
- const outsideLink = path.join(workspace, "outside-link");
82
- const insideLink = path.join(workspace, "inside-link");
83
- fs.symlinkSync(outside, outsideLink, process.platform === "win32" ? "junction" : "dir");
84
- fs.symlinkSync(path.join(workspace, "docs"), insideLink, process.platform === "win32" ? "junction" : "dir");
85
-
86
- // Existing targets and not-yet-created descendants cannot escape through a
87
- // directory symlink. The denied create must have no mkdir side effect.
88
- expectDenied("list_dir", { path: "outside-link" });
89
- expectDenied("read_file", { path: "outside-link/secret.txt" });
90
- expectDenied("write_file", { path: "outside-link/new/deep.txt", content: "escape\n" }, writeCtx);
91
- expectDenied(
92
- "edit_file",
93
- { path: "outside-link/secret.txt", old_string: "outside", new_string: "changed" },
94
- writeCtx,
95
- );
96
- assert.equal(fs.existsSync(path.join(outside, "new")), false, "symlink escape created outside directories");
97
- assert.equal(fs.readFileSync(outsideSecret, "utf8"), originalSecret, "symlink escape changed outside data");
98
-
99
- // In-workspace symlinks remain valid and resolve to their canonical target.
100
- assert.equal(expectAllowed("read_file", { path: "inside-link/guide.md" }), "alpha\n");
101
- expectAllowed("write_file", { path: "inside-link/linked-write.md", content: "inside\n" }, writeCtx);
102
- expectAllowed(
103
- "edit_file",
104
- { path: "inside-link/linked-write.md", old_string: "inside", new_string: "safe" },
105
- writeCtx,
106
- );
107
- assert.equal(fs.readFileSync(path.join(workspace, "docs", "linked-write.md"), "utf8"), "safe\n");
108
-
109
- // A hard link shares an inode even though both paths are lexically valid.
110
- // Writes and edits must replace the workspace entry, not mutate the outside inode.
111
- if (process.platform !== "win32") {
112
- const hardWrite = path.join(workspace, "hard-write.txt");
113
- fs.linkSync(outsideSecret, hardWrite);
114
- expectAllowed("write_file", { path: "hard-write.txt", content: "workspace-only\n" }, writeCtx);
115
- assert.equal(fs.readFileSync(hardWrite, "utf8"), "workspace-only\n");
116
- assert.equal(fs.readFileSync(outsideSecret, "utf8"), originalSecret, "hard-link write changed outside data");
117
-
118
- const hardEdit = path.join(workspace, "hard-edit.txt");
119
- fs.linkSync(outsideSecret, hardEdit);
120
- expectAllowed(
121
- "edit_file",
122
- { path: "hard-edit.txt", old_string: "outside", new_string: "workspace" },
123
- writeCtx,
124
- );
125
- assert.match(fs.readFileSync(hardEdit, "utf8"), /workspace-secret/);
126
- assert.equal(fs.readFileSync(outsideSecret, "utf8"), originalSecret, "hard-link edit changed outside data");
127
- }
128
-
129
- const executable = path.join(workspace, "script.sh");
130
- fs.writeFileSync(executable, "#!/bin/sh\necho old\n", { encoding: "utf8", mode: 0o755 });
131
- fs.chmodSync(executable, 0o755);
132
- expectAllowed(
133
- "edit_file",
134
- { path: "script.sh", old_string: "old", new_string: "new" },
135
- writeCtx,
136
- );
137
- assert.equal(fs.statSync(executable).mode & 0o777, 0o755, "atomic edit stripped executable mode bits");
138
- expectAllowed("write_file", { path: "script.sh", content: "#!/bin/sh\necho overwritten\n" }, writeCtx);
139
- assert.equal(fs.statSync(executable).mode & 0o777, 0o755, "atomic overwrite stripped executable mode bits");
140
-
141
- if (process.platform !== "win32") {
142
- const outsideFileLink = path.join(workspace, "outside-file-link");
143
- fs.symlinkSync(outsideSecret, outsideFileLink, "file");
144
- expectDenied("read_file", { path: "outside-file-link" });
145
- expectDenied("write_file", { path: "outside-file-link", content: "changed\n" }, writeCtx);
146
- expectDenied(
147
- "edit_file",
148
- { path: "outside-file-link", old_string: "outside", new_string: "changed" },
149
- writeCtx,
150
- );
151
-
152
- const brokenLink = path.join(workspace, "broken-link");
153
- fs.symlinkSync(path.join(outside, "missing-target"), brokenLink, "dir");
154
- expectDenied("write_file", { path: "broken-link/file.txt", content: "no\n" }, writeCtx);
155
- assert.equal(fs.existsSync(path.join(outside, "missing-target")), false, "broken symlink created an outside target");
156
- }
157
-
158
- for (const invalidPath of ["", 42, "bad\0path"]) {
159
- expectDenied("read_file", { path: invalidPath });
160
- }
161
-
162
- console.log("tool workspace boundary: PASS");
163
- } finally {
164
- fs.rmSync(fixture, { recursive: true, force: true });
165
- }