agentlas 0.7.0 → 0.9.1
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 +190 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +205 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const assert = require("node:assert/strict");
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const os = require("node:os");
|
|
7
|
-
const path = require("node:path");
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
runApi,
|
|
11
|
-
DEFAULT_API_MODEL,
|
|
12
|
-
ANTHROPIC_COMPAT_API,
|
|
13
|
-
} = require("../engine/agentlas.cjs");
|
|
14
|
-
const { create: createParity } = require("../engine/agentlas-parity.cjs");
|
|
15
|
-
|
|
16
|
-
function response(status, payload, errorText) {
|
|
17
|
-
return {
|
|
18
|
-
ok: status >= 200 && status < 300,
|
|
19
|
-
status,
|
|
20
|
-
json: async () => payload,
|
|
21
|
-
text: async () => errorText || "",
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function openFixtureDb(file) {
|
|
26
|
-
try {
|
|
27
|
-
const Database = require("better-sqlite3");
|
|
28
|
-
return new Database(file);
|
|
29
|
-
} catch {
|
|
30
|
-
const { DatabaseSync } = require("node:sqlite");
|
|
31
|
-
return new DatabaseSync(file);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function quietUi() {
|
|
36
|
-
const same = (value) => String(value ?? "");
|
|
37
|
-
return {
|
|
38
|
-
lang: "en",
|
|
39
|
-
c: { paw: same, bold: same, text: same, dim: same },
|
|
40
|
-
line() {},
|
|
41
|
-
info() {},
|
|
42
|
-
warn() {},
|
|
43
|
-
error() {},
|
|
44
|
-
tool() {},
|
|
45
|
-
toolResult() {},
|
|
46
|
-
startSpinner() {},
|
|
47
|
-
stopSpinner() {},
|
|
48
|
-
markdown() {},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function testAnthropicCompatibleProviders() {
|
|
53
|
-
for (const backend of ["glm", "kimi", "deepseek"]) {
|
|
54
|
-
const calls = [];
|
|
55
|
-
const text = await runApi(backend, null, "system", "prompt", {
|
|
56
|
-
apiKey: `${backend}-secret`,
|
|
57
|
-
fetch: async (url, init) => {
|
|
58
|
-
calls.push({ url, init });
|
|
59
|
-
return response(200, { content: [{ type: "text", text: `${backend}-ok` }] });
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
assert.equal(text, `${backend}-ok`);
|
|
64
|
-
assert.equal(calls.length, 1);
|
|
65
|
-
assert.equal(calls[0].url, `${ANTHROPIC_COMPAT_API[backend].baseUrl}/v1/messages`);
|
|
66
|
-
assert.equal(calls[0].init.headers["x-api-key"], `${backend}-secret`);
|
|
67
|
-
assert.equal(calls[0].init.headers.authorization, `Bearer ${backend}-secret`);
|
|
68
|
-
assert.equal(calls[0].init.headers["anthropic-version"], "2023-06-01");
|
|
69
|
-
assert.equal(JSON.parse(calls[0].init.body).model, DEFAULT_API_MODEL[backend]);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function testCustomBaseUrlComesFromSharedDb() {
|
|
74
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-run-api-"));
|
|
75
|
-
const previous = process.env.AGENTLAS_USER_DATA_DIR;
|
|
76
|
-
process.env.AGENTLAS_USER_DATA_DIR = dir;
|
|
77
|
-
const db = openFixtureDb(path.join(dir, "agentlas.sqlite"));
|
|
78
|
-
try {
|
|
79
|
-
db.exec("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
80
|
-
db.prepare("INSERT INTO meta(key, value) VALUES (?, ?)").run(
|
|
81
|
-
"custom_base_url",
|
|
82
|
-
"https://gateway.example.test/openai/v1/",
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const calls = [];
|
|
86
|
-
const text = await runApi("custom", "company-model", "system", "prompt", {
|
|
87
|
-
apiKey: "custom-secret",
|
|
88
|
-
fetch: async (url, init) => {
|
|
89
|
-
calls.push({ url, init });
|
|
90
|
-
return response(200, { choices: [{ message: { content: "custom-ok" } }] });
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
assert.equal(text, "custom-ok");
|
|
95
|
-
assert.equal(calls.length, 1);
|
|
96
|
-
assert.equal(calls[0].url, "https://gateway.example.test/openai/v1/chat/completions");
|
|
97
|
-
assert.equal(calls[0].init.headers.authorization, "Bearer custom-secret");
|
|
98
|
-
assert.equal(JSON.parse(calls[0].init.body).model, "company-model");
|
|
99
|
-
|
|
100
|
-
db.prepare("UPDATE meta SET value = ? WHERE key = ?").run(
|
|
101
|
-
"http://public.example.test/v1",
|
|
102
|
-
"custom_base_url",
|
|
103
|
-
);
|
|
104
|
-
let unsafeFetchCalled = false;
|
|
105
|
-
await assert.rejects(
|
|
106
|
-
runApi("custom", "company-model", "system", "prompt", {
|
|
107
|
-
apiKey: "custom-secret",
|
|
108
|
-
fetch: async () => {
|
|
109
|
-
unsafeFetchCalled = true;
|
|
110
|
-
return response(200, {});
|
|
111
|
-
},
|
|
112
|
-
}),
|
|
113
|
-
/HTTPS.*localhost\/LAN/,
|
|
114
|
-
);
|
|
115
|
-
assert.equal(unsafeFetchCalled, false, "invalid public HTTP base URL must not receive the API key");
|
|
116
|
-
} finally {
|
|
117
|
-
try { db.close(); } catch { /* ignore */ }
|
|
118
|
-
if (previous === undefined) delete process.env.AGENTLAS_USER_DATA_DIR;
|
|
119
|
-
else process.env.AGENTLAS_USER_DATA_DIR = previous;
|
|
120
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function testProviderErrorsNeverExitTheHostProcess() {
|
|
125
|
-
const originalExit = process.exit;
|
|
126
|
-
let exitCalls = 0;
|
|
127
|
-
process.exit = (code) => {
|
|
128
|
-
exitCalls += 1;
|
|
129
|
-
throw new Error(`unexpected process.exit(${code})`);
|
|
130
|
-
};
|
|
131
|
-
try {
|
|
132
|
-
await assert.rejects(
|
|
133
|
-
runApi("openai", "gpt-test", "system", "prompt", {
|
|
134
|
-
apiKey: "openai-secret",
|
|
135
|
-
fetch: async () => response(429, null, "rate limited"),
|
|
136
|
-
}),
|
|
137
|
-
/OpenAI 429: rate limited/,
|
|
138
|
-
);
|
|
139
|
-
await assert.rejects(
|
|
140
|
-
runApi("deepseek", "deepseek-chat", "system", "prompt", {
|
|
141
|
-
apiKey: "",
|
|
142
|
-
fetch: async () => response(200, {}),
|
|
143
|
-
}),
|
|
144
|
-
/deepseek API \ud0a4/,
|
|
145
|
-
);
|
|
146
|
-
await assert.rejects(
|
|
147
|
-
runApi("unknown", null, "system", "prompt", {
|
|
148
|
-
apiKey: "secret",
|
|
149
|
-
fetch: async () => response(200, {}),
|
|
150
|
-
}),
|
|
151
|
-
/\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 backend/,
|
|
152
|
-
);
|
|
153
|
-
assert.equal(exitCalls, 0, "runApi must throw to swarm/automation instead of exiting");
|
|
154
|
-
} finally {
|
|
155
|
-
process.exit = originalExit;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function testSwarmAndAutomationContainProviderFailure() {
|
|
160
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-run-api-host-"));
|
|
161
|
-
const db = openFixtureDb(path.join(dir, "agentlas.sqlite"));
|
|
162
|
-
const originalExit = process.exit;
|
|
163
|
-
const originalExitCode = process.exitCode;
|
|
164
|
-
let exitCalls = 0;
|
|
165
|
-
process.exit = (code) => {
|
|
166
|
-
exitCalls += 1;
|
|
167
|
-
throw new Error(`unexpected process.exit(${code})`);
|
|
168
|
-
};
|
|
169
|
-
try {
|
|
170
|
-
db.exec(`
|
|
171
|
-
CREATE TABLE installed_agents (
|
|
172
|
-
id TEXT PRIMARY KEY,
|
|
173
|
-
name TEXT NOT NULL,
|
|
174
|
-
system_prompt TEXT
|
|
175
|
-
);
|
|
176
|
-
CREATE TABLE automations (
|
|
177
|
-
id TEXT PRIMARY KEY,
|
|
178
|
-
name TEXT NOT NULL,
|
|
179
|
-
schedule TEXT,
|
|
180
|
-
target_type TEXT NOT NULL,
|
|
181
|
-
target_id TEXT NOT NULL,
|
|
182
|
-
prompt_template TEXT NOT NULL,
|
|
183
|
-
enabled INTEGER NOT NULL,
|
|
184
|
-
claimed_at TEXT,
|
|
185
|
-
lease_owner TEXT,
|
|
186
|
-
last_run_at TEXT,
|
|
187
|
-
next_run_at TEXT,
|
|
188
|
-
schedule_json TEXT,
|
|
189
|
-
timezone TEXT,
|
|
190
|
-
trigger_type TEXT,
|
|
191
|
-
run_count INTEGER NOT NULL DEFAULT 0,
|
|
192
|
-
max_runs INTEGER
|
|
193
|
-
);
|
|
194
|
-
CREATE TABLE run_history (
|
|
195
|
-
id TEXT PRIMARY KEY,
|
|
196
|
-
automation_id TEXT,
|
|
197
|
-
scheduled_for TEXT,
|
|
198
|
-
ran_at TEXT,
|
|
199
|
-
status TEXT,
|
|
200
|
-
skipped_count INTEGER,
|
|
201
|
-
error TEXT
|
|
202
|
-
);
|
|
203
|
-
INSERT INTO installed_agents(id, name, system_prompt)
|
|
204
|
-
VALUES ('agent-1', 'Regression Agent', 'System');
|
|
205
|
-
INSERT INTO automations(
|
|
206
|
-
id, name, schedule, target_type, target_id, prompt_template, enabled,
|
|
207
|
-
next_run_at, timezone, trigger_type, run_count
|
|
208
|
-
) VALUES (
|
|
209
|
-
'automation-1', 'Regression Automation', 'daily-09:00', 'agent', 'agent-1', 'Run', 1,
|
|
210
|
-
'2026-01-01T00:00:00.000Z', 'Asia/Seoul', 'schedule', 0
|
|
211
|
-
);
|
|
212
|
-
`);
|
|
213
|
-
|
|
214
|
-
const failingRunApi = (backend, model, system, prompt) => runApi(backend, model, system, prompt, {
|
|
215
|
-
apiKey: "openai-secret",
|
|
216
|
-
fetch: async () => response(429, null, "rate limited"),
|
|
217
|
-
});
|
|
218
|
-
const parity = createParity({
|
|
219
|
-
prefsLang: () => "en",
|
|
220
|
-
resolveRuntime: () => ({ mode: "api", backend: "openai", model: "gpt-test" }),
|
|
221
|
-
buildChildEnvCli: async () => ({}),
|
|
222
|
-
runApi: failingRunApi,
|
|
223
|
-
captureRuntime: async () => "",
|
|
224
|
-
runCwd: () => dir,
|
|
225
|
-
out() {},
|
|
226
|
-
fail(message) { throw new Error(message); },
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const swarm = await parity.swarmRun(db, "provider failure containment", { ui: quietUi(), concurrency: 2 });
|
|
230
|
-
assert.equal(swarm.ok, false, "all-failed swarm should finish normally with ok=false");
|
|
231
|
-
|
|
232
|
-
await parity.cmdAutomation(db, ["run", "automation-1"]);
|
|
233
|
-
const automation = db.prepare(
|
|
234
|
-
"SELECT claimed_at, lease_owner, last_run_at, run_count FROM automations WHERE id = ?",
|
|
235
|
-
).get("automation-1");
|
|
236
|
-
assert.equal(automation.claimed_at, null, "automation lease must be released after provider error");
|
|
237
|
-
assert.equal(automation.lease_owner, null, "automation lease owner must be cleared after provider error");
|
|
238
|
-
assert.ok(automation.last_run_at, "automation failure must still be recorded");
|
|
239
|
-
assert.equal(automation.run_count, 0, "failed automation must not count as success");
|
|
240
|
-
const history = db.prepare(
|
|
241
|
-
"SELECT status, error FROM run_history WHERE automation_id = ? ORDER BY ran_at DESC LIMIT 1",
|
|
242
|
-
).get("automation-1");
|
|
243
|
-
assert.equal(history.status, "error");
|
|
244
|
-
assert.match(history.error, /OpenAI 429: rate limited/);
|
|
245
|
-
assert.equal(exitCalls, 0, "swarm/automation must contain provider errors without exiting the host");
|
|
246
|
-
|
|
247
|
-
const fixedFrom = new Date("2026-07-10T00:30:00.000Z"); // 09:30 Asia/Seoul
|
|
248
|
-
assert.equal(
|
|
249
|
-
parity.nextAutomationRun(
|
|
250
|
-
{ schedule: "daily-09:00", schedule_json: null, timezone: "Asia/Seoul" },
|
|
251
|
-
fixedFrom,
|
|
252
|
-
).toISOString(),
|
|
253
|
-
"2026-07-11T00:00:00.000Z",
|
|
254
|
-
"legacy desktop token must advance in its stored timezone",
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const scheduledFailureRow = db.prepare("SELECT * FROM automations WHERE id = ?").get("automation-1");
|
|
258
|
-
const failedScheduled = await parity.runAutomationOnce(db, scheduledFailureRow, {
|
|
259
|
-
ui: quietUi(),
|
|
260
|
-
advanceSchedule: true,
|
|
261
|
-
scheduledFor: scheduledFailureRow.next_run_at,
|
|
262
|
-
});
|
|
263
|
-
assert.equal(failedScheduled.ok, false);
|
|
264
|
-
const afterScheduledFailure = db.prepare(
|
|
265
|
-
"SELECT next_run_at, run_count, enabled FROM automations WHERE id = ?",
|
|
266
|
-
).get("automation-1");
|
|
267
|
-
assert.ok(
|
|
268
|
-
Date.parse(afterScheduledFailure.next_run_at) > Date.now(),
|
|
269
|
-
"failed scheduled automation must advance beyond now instead of retrying every poll",
|
|
270
|
-
);
|
|
271
|
-
assert.equal(afterScheduledFailure.run_count, 0, "failed run remains excluded from success count");
|
|
272
|
-
|
|
273
|
-
db.prepare(
|
|
274
|
-
`INSERT INTO automations(
|
|
275
|
-
id, name, schedule, target_type, target_id, prompt_template, enabled,
|
|
276
|
-
next_run_at, timezone, trigger_type, run_count
|
|
277
|
-
) VALUES ('automation-2', 'Success Automation', 'cron:*/5 * * * *', 'agent', 'agent-1', 'Run', 1,
|
|
278
|
-
'2026-01-01T00:00:00.000Z', 'UTC', 'schedule', 0)`,
|
|
279
|
-
).run();
|
|
280
|
-
const successParity = createParity({
|
|
281
|
-
prefsLang: () => "en",
|
|
282
|
-
resolveRuntime: () => ({ mode: "api", backend: "openai", model: "gpt-test" }),
|
|
283
|
-
buildChildEnvCli: async () => ({}),
|
|
284
|
-
runApi: async () => "ok",
|
|
285
|
-
captureRuntime: async () => "",
|
|
286
|
-
runCwd: () => dir,
|
|
287
|
-
out() {},
|
|
288
|
-
fail(message) { throw new Error(message); },
|
|
289
|
-
});
|
|
290
|
-
const successRow = db.prepare("SELECT * FROM automations WHERE id = ?").get("automation-2");
|
|
291
|
-
const scheduledSuccess = await successParity.runAutomationOnce(db, successRow, {
|
|
292
|
-
ui: quietUi(),
|
|
293
|
-
advanceSchedule: true,
|
|
294
|
-
scheduledFor: successRow.next_run_at,
|
|
295
|
-
});
|
|
296
|
-
assert.equal(scheduledSuccess.ok, true);
|
|
297
|
-
const afterScheduledSuccess = db.prepare(
|
|
298
|
-
"SELECT next_run_at, run_count, enabled FROM automations WHERE id = ?",
|
|
299
|
-
).get("automation-2");
|
|
300
|
-
assert.ok(Date.parse(afterScheduledSuccess.next_run_at) > Date.now());
|
|
301
|
-
assert.equal(afterScheduledSuccess.run_count, 1);
|
|
302
|
-
assert.equal(afterScheduledSuccess.enabled, 1);
|
|
303
|
-
} finally {
|
|
304
|
-
process.exit = originalExit;
|
|
305
|
-
process.exitCode = originalExitCode;
|
|
306
|
-
try { db.close(); } catch { /* ignore */ }
|
|
307
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async function main() {
|
|
312
|
-
await testAnthropicCompatibleProviders();
|
|
313
|
-
await testCustomBaseUrlComesFromSharedDb();
|
|
314
|
-
await testProviderErrorsNeverExitTheHostProcess();
|
|
315
|
-
await testSwarmAndAutomationContainProviderFailure();
|
|
316
|
-
process.stdout.write("run-api regression: PASS (BYOK parity + swarm/automation failure containment)\n");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
main().catch((error) => {
|
|
320
|
-
process.stderr.write(`${error && error.stack ? error.stack : error}\n`);
|
|
321
|
-
process.exitCode = 1;
|
|
322
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const runtime = require("../engine/agentlas.cjs");
|
|
4
|
-
|
|
5
|
-
const base = {
|
|
6
|
-
HOME: "/trusted/home",
|
|
7
|
-
PATH: "/trusted/bin",
|
|
8
|
-
CODEX_HOME: "/trusted/codex",
|
|
9
|
-
AGENTLAS_CODEX_HOME: "/trusted/agentlas-codex",
|
|
10
|
-
AGENTLAS_USER_DATA_DIR: "/trusted/agentlas-data",
|
|
11
|
-
CLAUDE_CONFIG_DIR: "/trusted/claude",
|
|
12
|
-
GEMINI_CLI_HOME: "/trusted/gemini",
|
|
13
|
-
API_TOKEN: "old",
|
|
14
|
-
};
|
|
15
|
-
const maliciousDotenv = runtime.parseDotEnvCli([
|
|
16
|
-
"HOME=/tmp/attacker",
|
|
17
|
-
"PATH=/tmp/attacker/bin",
|
|
18
|
-
"CODEX_HOME=/tmp/attacker/codex",
|
|
19
|
-
"AGENTLAS_CODEX_HOME=/tmp/attacker/victim",
|
|
20
|
-
"AGENTLAS_USER_DATA_DIR=/tmp/attacker/data",
|
|
21
|
-
"CLAUDE_CONFIG_DIR=/tmp/attacker/claude",
|
|
22
|
-
"GEMINI_CLI_HOME=/tmp/attacker/gemini",
|
|
23
|
-
"GEMINI_CLI_EXTENSION_REGISTRY_URI=https://attacker.invalid/extensions",
|
|
24
|
-
"CLAUDE_CODE_SAFE_MODE=1",
|
|
25
|
-
"NODE_OPTIONS=--require=/tmp/attacker.js",
|
|
26
|
-
"API_TOKEN=new",
|
|
27
|
-
].join("\n"));
|
|
28
|
-
maliciousDotenv.Path = "/tmp/attacker/windows-bin";
|
|
29
|
-
|
|
30
|
-
runtime.mergeChildEnvValuesCli(base, maliciousDotenv, true);
|
|
31
|
-
|
|
32
|
-
assert.equal(base.HOME, "/trusted/home");
|
|
33
|
-
assert.equal(base.PATH, "/trusted/bin");
|
|
34
|
-
assert.equal(base.Path, undefined);
|
|
35
|
-
assert.equal(base.CODEX_HOME, "/trusted/codex");
|
|
36
|
-
assert.equal(base.AGENTLAS_CODEX_HOME, "/trusted/agentlas-codex");
|
|
37
|
-
assert.equal(base.AGENTLAS_USER_DATA_DIR, "/trusted/agentlas-data");
|
|
38
|
-
assert.equal(base.CLAUDE_CONFIG_DIR, "/trusted/claude");
|
|
39
|
-
assert.equal(base.GEMINI_CLI_HOME, "/trusted/gemini");
|
|
40
|
-
assert.equal(base.GEMINI_CLI_EXTENSION_REGISTRY_URI, undefined);
|
|
41
|
-
assert.equal(base.CLAUDE_CODE_SAFE_MODE, undefined);
|
|
42
|
-
assert.equal(base.NODE_OPTIONS, undefined);
|
|
43
|
-
assert.equal(base.API_TOKEN, "new");
|
|
44
|
-
|
|
45
|
-
// ── 네트워크 무결성 키: 비신뢰(프로젝트/에이전트) dotenv는 TLS/프록시/엔드포인트/세션을 못 바꾼다 ──
|
|
46
|
-
// (bug-hunter 2026-07-12: 원샷 API 경로가 프로젝트 .env를 process.env에 병합해 MITM/SSRF/세션 하이재킹 가능했음)
|
|
47
|
-
{
|
|
48
|
-
const untrustedBase = { API_TOKEN: "keep" };
|
|
49
|
-
const untrustedDotenv = runtime.parseDotEnvCli([
|
|
50
|
-
"NODE_TLS_REJECT_UNAUTHORIZED=0",
|
|
51
|
-
"HTTPS_PROXY=http://attacker.invalid:8080",
|
|
52
|
-
"HTTP_PROXY=http://attacker.invalid:8080",
|
|
53
|
-
"NODE_EXTRA_CA_CERTS=/tmp/attacker-ca.pem",
|
|
54
|
-
"AGENTLAS_SESSION=forged-session",
|
|
55
|
-
"AGENTLAS_MCP_BASE_URL=https://evil.example/mcp",
|
|
56
|
-
"AGENTLAS_WEB_BASE_URL=https://evil.example",
|
|
57
|
-
"OLLAMA_HOST=http://169.254.169.254",
|
|
58
|
-
"OPENAI_API_KEY=sk-project-supplied", // 일반 API 키는 프로젝트가 넣을 수 있어야 함
|
|
59
|
-
].join("\n"));
|
|
60
|
-
runtime.mergeChildEnvValuesCli(untrustedBase, untrustedDotenv, true /* overwrite */, false /* untrusted */);
|
|
61
|
-
for (const k of ["NODE_TLS_REJECT_UNAUTHORIZED", "HTTPS_PROXY", "HTTP_PROXY", "NODE_EXTRA_CA_CERTS",
|
|
62
|
-
"AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "OLLAMA_HOST"]) {
|
|
63
|
-
assert.equal(untrustedBase[k], undefined, `untrusted dotenv must not set ${k}`);
|
|
64
|
-
}
|
|
65
|
-
assert.equal(untrustedBase.OPENAI_API_KEY, "sk-project-supplied", "일반 API 키는 프로젝트 dotenv로 허용");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 신뢰 출처(사용자 전역 credentials.env/볼트)는 같은 키를 정당하게 설정할 수 있다 —
|
|
69
|
-
// 로컬 Ollama·자체호스팅 엔드포인트를 쓰는 정상 사용자를 깨지 않는다.
|
|
70
|
-
{
|
|
71
|
-
const trustedBase = {};
|
|
72
|
-
const trustedDotenv = runtime.parseDotEnvCli([
|
|
73
|
-
"OLLAMA_HOST=http://127.0.0.1:11434",
|
|
74
|
-
"AGENTLAS_MCP_BASE_URL=https://self-hosted.internal/mcp",
|
|
75
|
-
].join("\n"));
|
|
76
|
-
runtime.mergeChildEnvValuesCli(trustedBase, trustedDotenv, false /* overwrite */, true /* trusted */);
|
|
77
|
-
assert.equal(trustedBase.OLLAMA_HOST, "http://127.0.0.1:11434", "신뢰 출처는 OLLAMA_HOST 허용");
|
|
78
|
-
assert.equal(trustedBase.AGENTLAS_MCP_BASE_URL, "https://self-hosted.internal/mcp", "신뢰 출처는 엔드포인트 허용");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 호스트 신원 키(HOME/PATH/NODE_OPTIONS 등)는 신뢰 출처라도 절대 불가
|
|
82
|
-
{
|
|
83
|
-
const b = {};
|
|
84
|
-
runtime.mergeChildEnvValuesCli(b, runtime.parseDotEnvCli("NODE_OPTIONS=--require=/x.js\nPATH=/evil"), true, true);
|
|
85
|
-
assert.equal(b.NODE_OPTIONS, undefined, "NODE_OPTIONS는 신뢰 출처라도 불가");
|
|
86
|
-
assert.equal(b.PATH, undefined, "PATH는 신뢰 출처라도 불가");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log(JSON.stringify({ ok: true, checks: 25 }, null, 2));
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const assert = require("node:assert/strict");
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const { compareSemVer, normalizeSemVer, parseSemVer } = require("../engine/semver.cjs");
|
|
8
|
-
|
|
9
|
-
const precedence = [
|
|
10
|
-
"1.0.0-alpha",
|
|
11
|
-
"1.0.0-alpha.1",
|
|
12
|
-
"1.0.0-alpha.beta",
|
|
13
|
-
"1.0.0-beta",
|
|
14
|
-
"1.0.0-beta.2",
|
|
15
|
-
"1.0.0-beta.11",
|
|
16
|
-
"1.0.0-rc.1",
|
|
17
|
-
"1.0.0",
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
for (let index = 0; index < precedence.length - 1; index += 1) {
|
|
21
|
-
assert.equal(compareSemVer(precedence[index], precedence[index + 1]), -1);
|
|
22
|
-
assert.equal(compareSemVer(precedence[index + 1], precedence[index]), 1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
assert.equal(compareSemVer("v2.3.4", "2.3.4"), 0);
|
|
26
|
-
assert.equal(normalizeSemVer("v2.3.4-rc.1+build.7"), "2.3.4-rc.1+build.7");
|
|
27
|
-
assert.equal(compareSemVer("1.0.0+build.1", "1.0.0+build.99"), 0);
|
|
28
|
-
assert.equal(compareSemVer("1.0.0-1", "1.0.0-alpha"), -1);
|
|
29
|
-
assert.equal(compareSemVer("999999999999999999999.0.0", "2.0.0"), 1);
|
|
30
|
-
assert.equal(compareSemVer("1.0.0", "1.0.0-rc.99"), 1);
|
|
31
|
-
assert.equal(parseSemVer("1.0.0-01"), null);
|
|
32
|
-
assert.equal(parseSemVer("01.0.0"), null);
|
|
33
|
-
assert.equal(compareSemVer("not-a-version", "1.0.0"), null);
|
|
34
|
-
|
|
35
|
-
const updater = fs.readFileSync(path.join(__dirname, "../engine/agentlas.cjs"), "utf8");
|
|
36
|
-
assert.match(updater, /compareSemVer\(currentVersion, latestVersion\)/);
|
|
37
|
-
assert.doesNotMatch(updater, /function versionParts/);
|
|
38
|
-
|
|
39
|
-
console.log("semver-precedence: PASS");
|
package/test/smoke.sh
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# agentlas 터미널 CLI 스모크 테스트.
|
|
3
|
-
# 1) 기본(auto=bundled) where/version/list/doctor
|
|
4
|
-
# 2) 신선 환경(빈 userData) 첫 실행: DB 부트스트랩 + 빌트인 시드
|
|
5
|
-
set -eu
|
|
6
|
-
|
|
7
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
-
BIN="$SCRIPT_DIR/../bin/agentlas.cjs"
|
|
9
|
-
|
|
10
|
-
pass=0
|
|
11
|
-
fail=0
|
|
12
|
-
check() {
|
|
13
|
-
name="$1"; shift
|
|
14
|
-
if out="$("$@" 2>&1)"; then
|
|
15
|
-
echo "PASS $name"
|
|
16
|
-
pass=$((pass + 1))
|
|
17
|
-
else
|
|
18
|
-
echo "FAIL $name"
|
|
19
|
-
echo "$out" | sed 's/^/ /'
|
|
20
|
-
fail=$((fail + 1))
|
|
21
|
-
fi
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
check "where" node "$BIN" --where
|
|
25
|
-
check "version" node "$BIN" version
|
|
26
|
-
check "list" node "$BIN" list
|
|
27
|
-
check "doctor" node "$BIN" doctor
|
|
28
|
-
check "help" node "$BIN" help
|
|
29
|
-
check "usage" node "$BIN" usage
|
|
30
|
-
check "mcp" node "$BIN" mcp
|
|
31
|
-
check "chats" node "$BIN" chats
|
|
32
|
-
check "run-api-regression" node "$SCRIPT_DIR/run-api-regression.cjs"
|
|
33
|
-
check "cloud-runtime-paths" node "$SCRIPT_DIR/cloud-runtime-paths.cjs"
|
|
34
|
-
check "cloud-save-publish" node "$SCRIPT_DIR/cloud-save-publish.cjs"
|
|
35
|
-
check "cloud-asset-restore" node "$SCRIPT_DIR/cloud-asset-restore.cjs"
|
|
36
|
-
check "cloud-owner-restore" node "$SCRIPT_DIR/cloud-owner-restore.cjs"
|
|
37
|
-
check "cloud-cas-client" node "$SCRIPT_DIR/cloud-cas-client.cjs"
|
|
38
|
-
check "runtime-env-protection" node "$SCRIPT_DIR/runtime-env-protection.cjs"
|
|
39
|
-
check "credential-env-regression" node "$SCRIPT_DIR/credential-env-regression.cjs"
|
|
40
|
-
check "tool-workspace-boundary" node "$SCRIPT_DIR/tool-workspace-boundary.cjs"
|
|
41
|
-
check "mcp-config-isolation" node "$SCRIPT_DIR/mcp-config-isolation.cjs"
|
|
42
|
-
check "experience-mcp-contract" node "$SCRIPT_DIR/experience-mcp-contract.cjs"
|
|
43
|
-
check "experience-exchange-contract" node "$SCRIPT_DIR/experience-exchange-contract.cjs"
|
|
44
|
-
check "bootstrap-race" node "$SCRIPT_DIR/bootstrap-race.cjs"
|
|
45
|
-
check "login-loopback-security" node "$SCRIPT_DIR/login-loopback-security.cjs"
|
|
46
|
-
check "timeout-regression" node "$SCRIPT_DIR/timeout-regression.cjs"
|
|
47
|
-
check "terminal-ui-regression" node "$SCRIPT_DIR/terminal-ui-regression.cjs"
|
|
48
|
-
check "route-regression" node "$SCRIPT_DIR/route-regression.cjs"
|
|
49
|
-
check "engine-hardening-regression" node "$SCRIPT_DIR/engine-hardening-regression.cjs"
|
|
50
|
-
check "permission-mapping" node "$SCRIPT_DIR/permission-mapping.cjs"
|
|
51
|
-
check "sqlite-driver-probe" node "$SCRIPT_DIR/sqlite-driver-probe.cjs"
|
|
52
|
-
check "capture-runtime-guard" node "$SCRIPT_DIR/capture-runtime-guard.cjs"
|
|
53
|
-
check "update-safety" node "$SCRIPT_DIR/update-safety.cjs"
|
|
54
|
-
check "semver-precedence" node "$SCRIPT_DIR/semver-precedence.cjs"
|
|
55
|
-
|
|
56
|
-
# Agentlas OS 표면: 무인자 호출은 usage를 내고 exit 1 (프롬프트 오라우팅 방지 확인)
|
|
57
|
-
guard() {
|
|
58
|
-
name="$1"; shift
|
|
59
|
-
if out="$("$@" 2>&1)"; then echo "FAIL $name (should exit non-zero)"; fail=$((fail + 1));
|
|
60
|
-
else echo "PASS $name"; pass=$((pass + 1)); fi
|
|
61
|
-
}
|
|
62
|
-
guard "guard-search" node "$BIN" search
|
|
63
|
-
guard "guard-install" node "$BIN" install
|
|
64
|
-
guard "guard-upload" node "$BIN" upload
|
|
65
|
-
|
|
66
|
-
# 신선 환경 첫 실행 (표준: mktemp 사용, 검증 후 Trash로 이동)
|
|
67
|
-
FRESH="$(mktemp -d "${TMPDIR:-/tmp}/agentlas-smoke-XXXXXX")"
|
|
68
|
-
check "fresh-first-run" env AGENTLAS_USER_DATA_DIR="$FRESH" node "$BIN" list
|
|
69
|
-
if [ -f "$FRESH/agentlas.sqlite" ]; then
|
|
70
|
-
echo "PASS fresh-db-created"
|
|
71
|
-
pass=$((pass + 1))
|
|
72
|
-
else
|
|
73
|
-
echo "FAIL fresh-db-created"
|
|
74
|
-
fail=$((fail + 1))
|
|
75
|
-
fi
|
|
76
|
-
mv "$FRESH" "$HOME/.Trash/agentlas-smoke-$(date +%s)" 2>/dev/null || true
|
|
77
|
-
|
|
78
|
-
# Runtime Doctor 3제품 패리티 게이트 — 데스크탑 TS ↔ 이 repo CJS ↔ system-optimizer
|
|
79
|
-
# 플레이북이 어긋나면 여기서 FAIL. (형제 repo가 없는 CI/신선 클론에선 자동 스킵)
|
|
80
|
-
SYNC="$(cd "$(dirname "$0")/.." && pwd)/../scripts/sync-runtime-doctor.sh"
|
|
81
|
-
if [ -f "$SYNC" ]; then
|
|
82
|
-
if bash "$SYNC"; then
|
|
83
|
-
echo "PASS doctor-parity"
|
|
84
|
-
pass=$((pass + 1))
|
|
85
|
-
else
|
|
86
|
-
echo "FAIL doctor-parity"
|
|
87
|
-
fail=$((fail + 1))
|
|
88
|
-
fi
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
echo ""
|
|
92
|
-
echo "smoke: $pass passed, $fail failed"
|
|
93
|
-
[ "$fail" -eq 0 ]
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const assert = require("node:assert/strict");
|
|
5
|
-
const { spawnSync } = require("node:child_process");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const { probeSqliteDriver } = require("../bin/agentlas.cjs");
|
|
8
|
-
|
|
9
|
-
const driver = probeSqliteDriver();
|
|
10
|
-
assert.ok(driver === "better-sqlite3" || driver === "node:sqlite", `unexpected SQLite driver: ${driver}`);
|
|
11
|
-
|
|
12
|
-
const launcher = path.join(__dirname, "..", "bin", "agentlas.cjs");
|
|
13
|
-
const result = spawnSync(process.execPath, [launcher, "--where"], {
|
|
14
|
-
encoding: "utf8",
|
|
15
|
-
env: { ...process.env, NODE_NO_WARNINGS: "" },
|
|
16
|
-
});
|
|
17
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
18
|
-
assert.doesNotMatch(result.stderr, /ExperimentalWarning|SQLite is an experimental feature/i);
|
|
19
|
-
const where = JSON.parse(result.stdout);
|
|
20
|
-
assert.equal(where.sqliteDriver, driver, "--where must report the driver that can actually open a database");
|
|
21
|
-
|
|
22
|
-
console.log(`sqlite-driver-probe: PASS (${driver})`);
|