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,487 +0,0 @@
1
- #!/usr/bin/env node
2
- const assert = require("node:assert/strict");
3
- const crypto = require("node:crypto");
4
- const { execFileSync } = require("node:child_process");
5
- const fs = require("node:fs");
6
- const http = require("node:http");
7
- const os = require("node:os");
8
- const path = require("node:path");
9
-
10
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-cloud-save-publish-"));
11
- process.env.AGENTLAS_USER_DATA_DIR = path.join(tempDir, "user-data");
12
- process.env.AGENTLAS_SESSION = "test-owner-session";
13
-
14
- const {
15
- cloudActionForTopLevelUpload,
16
- cloudHashPackage,
17
- cloudPortableExecutableForFile,
18
- cloudPortablePathConflict,
19
- cloudVisibilityForAction,
20
- packageCloudAgentCli,
21
- } = require("../engine/agentlas.cjs");
22
-
23
- function writePrivateNotes(root) {
24
- fs.mkdirSync(root, { recursive: true });
25
- fs.writeFileSync(path.join(root, "notes.md"), "Owner-private agent working notes.\n", "utf8");
26
- fs.writeFileSync(path.join(root, "asset.bin"), Buffer.from([0x00, 0xff, 0x81, 0x41, 0x00]));
27
- fs.writeFileSync(path.join(root, "run.sh"), "#!/bin/sh\nexit 0\n", { mode: 0o700 });
28
- fs.chmodSync(path.join(root, "run.sh"), 0o700);
29
- fs.writeFileSync(path.join(root, ".agentlas-cloud-package.json"), JSON.stringify({
30
- packageHash: "local-only-marker",
31
- packageHashVersion: "path-sha256-executable-v2",
32
- executablePaths: ["run.sh"],
33
- }));
34
- }
35
-
36
- function writePublicAgent(root) {
37
- writePrivateNotes(root);
38
- fs.mkdirSync(path.join(root, ".agentlas"), { recursive: true });
39
- fs.writeFileSync(path.join(root, "AGENTS.md"), "# Public Test Agent\n\nRun the public test task.\n", "utf8");
40
- fs.writeFileSync(
41
- path.join(root, ".agentlas", "routing-card.json"),
42
- JSON.stringify({
43
- schemaVersion: "routing-card/2.0",
44
- id: "public-test-agent",
45
- type: "agent",
46
- name: "Public Test Agent",
47
- summary: "Routes public test requests.",
48
- capabilities: ["public_test"],
49
- routing_status: "routing_ready",
50
- }, null, 2) + "\n",
51
- "utf8",
52
- );
53
- }
54
-
55
- function listen(server) {
56
- return new Promise((resolve, reject) => {
57
- server.once("error", reject);
58
- server.listen(0, "127.0.0.1", () => resolve(server.address()));
59
- });
60
- }
61
-
62
- function close(server) {
63
- return new Promise((resolve) => server.close(resolve));
64
- }
65
-
66
- (async () => {
67
- const requests = [];
68
- const requestHeaders = [];
69
- const server = http.createServer((req, res) => {
70
- const chunks = [];
71
- req.on("data", (chunk) => chunks.push(chunk));
72
- req.on("end", () => {
73
- requests.push(JSON.parse(Buffer.concat(chunks).toString("utf8")));
74
- requestHeaders.push(req.headers);
75
- if (requests.at(-1).manifest.slug === "invalid-receipt-agent") {
76
- res.writeHead(200, { "content-type": "application/json" });
77
- res.end(JSON.stringify({ cloudId: "synthetic-success-must-not-be-accepted" }));
78
- return;
79
- }
80
- const revision = `rev-${requests.length}-${requests.at(-1).manifest.packageHash.slice(0, 16)}`;
81
- res.writeHead(200, {
82
- "content-type": "application/json",
83
- "cache-control": "no-store",
84
- etag: `"${revision}"`,
85
- });
86
- res.end(JSON.stringify({
87
- schema: "agentlas.agent_cloud.registration.v1",
88
- operation: "created",
89
- source: requests.at(-1).visibility === "marketplace" ? "hub" : "agent-cloud",
90
- visibility: requests.at(-1).visibility === "marketplace" ? "marketplace" : "owner-private",
91
- scope: requests.at(-1).visibility === "marketplace" ? "hub-public" : "owner-private",
92
- owner: true,
93
- publicHubPublished: requests.at(-1).visibility === "marketplace",
94
- cloudId: `cloud-test-${requests.length}`,
95
- slug: requests.at(-1).manifest.slug,
96
- packageHash: requests.at(-1).manifest.packageHash,
97
- packageHashVersion: requests.at(-1).manifest.packageHashVersion,
98
- revision,
99
- url: `http://agent-cloud.test/owned/${requests.at(-1).manifest.slug}`,
100
- marketplaceUrl: requests.at(-1).visibility === "marketplace"
101
- ? `http://agent-cloud.test/hub/${requests.at(-1).manifest.slug}`
102
- : undefined,
103
- registeredAt: new Date().toISOString(),
104
- dryRun: false,
105
- }));
106
- });
107
- });
108
-
109
- try {
110
- const address = await listen(server);
111
- process.env.AGENTLAS_WEB_BASE_URL = `http://127.0.0.1:${address.port}`;
112
-
113
- assert.equal(cloudVisibilityForAction("package", { _: [] }), "private-link");
114
- assert.equal(cloudVisibilityForAction("save", { _: [] }), "private-link");
115
- assert.equal(cloudVisibilityForAction("save", { _: [], visibility: "private-link" }), "private-link");
116
- assert.equal(cloudVisibilityForAction("publish", { _: [] }), "marketplace");
117
- assert.equal(cloudVisibilityForAction("publish", { _: [], visibility: "marketplace" }), "marketplace");
118
- assert.throws(
119
- () => cloudVisibilityForAction("save", { _: [], visibility: "marketplace" }),
120
- /owner-private/,
121
- );
122
- assert.throws(
123
- () => cloudVisibilityForAction("publish", { _: [], visibility: "private-link" }),
124
- /public Hub publication/,
125
- );
126
- assert.equal(cloudActionForTopLevelUpload(["/tmp/agent"]), "save");
127
- assert.equal(cloudActionForTopLevelUpload(["/tmp/agent", "--visibility", "private-link"]), "save");
128
- assert.equal(cloudActionForTopLevelUpload(["/tmp/agent", "--visibility", "marketplace"]), "publish");
129
-
130
- const privateRoot = path.join(tempDir, "private-notes-only");
131
- writePrivateNotes(privateRoot);
132
- const privateDryRun = await packageCloudAgentCli(null, privateRoot, {
133
- dryRun: true,
134
- llmReview: true,
135
- });
136
- assert.equal(privateDryRun.status, "dry-run");
137
- assert.equal(privateDryRun.manifest.visibility, "private-link");
138
- assert.equal(privateDryRun.review.mode, "static-only");
139
- assert.equal(privateDryRun.review.costOwner, "none");
140
- assert.equal(privateDryRun.review.verdict, "pass");
141
- assert.equal(privateDryRun.review.findings.some((finding) => finding.id === "missing-agent-definition"), false);
142
- assert.equal(privateDryRun.review.findings.some((finding) => finding.id.startsWith("routing-card")), false);
143
- const privateBundle = JSON.parse(fs.readFileSync(privateDryRun.bundlePath, "utf8"));
144
- assert.equal(privateBundle.manifest.packageHashVersion, "path-sha256-executable-v2");
145
- assert.equal(
146
- privateBundle.manifest.packageHash,
147
- cloudHashPackage(privateBundle.files, privateBundle.manifest.packageHashVersion),
148
- );
149
- assert.equal(
150
- privateBundle.manifest.rootFingerprint,
151
- crypto.createHash("sha256").update(`agentlas-package-root:${privateBundle.manifest.packageHash}`).digest("hex"),
152
- "root fingerprint must be content-derived and match Desktop",
153
- );
154
- const binary = privateBundle.files.find((file) => file.path === "asset.bin");
155
- assert.deepEqual(Buffer.from(binary.contentBase64, "base64"), Buffer.from([0x00, 0xff, 0x81, 0x41, 0x00]));
156
- assert.equal(binary.executable, false);
157
- assert.equal(privateBundle.files.find((file) => file.path === "run.sh")?.executable, true);
158
- assert.equal(privateBundle.files.some((file) => file.path === ".agentlas-cloud-package.json"), false);
159
-
160
- // Experience lineage is local, rebuildable, and owned separately from the
161
- // immutable base Agent package. Canonical, backup, and crash-safe hidden
162
- // temp siblings must neither ship nor perturb the base package hash.
163
- const lineageDir = path.join(privateRoot, ".agentlas");
164
- fs.mkdirSync(lineageDir, { recursive: true });
165
- const lineagePaths = [
166
- "experience-relations.jsonl",
167
- "experience-relations.jsonl.previous",
168
- ".experience-relations.jsonl.1234.tmp",
169
- ".experience-relations.jsonl.tmp-recovery",
170
- ];
171
- for (const [index, name] of lineagePaths.entries()) {
172
- fs.writeFileSync(path.join(lineageDir, name), `private-lineage-${index}\n`, "utf8");
173
- }
174
- const lineageExcluded = await packageCloudAgentCli(null, privateRoot, { dryRun: true, llmReview: false });
175
- const lineageBundle = JSON.parse(fs.readFileSync(lineageExcluded.bundlePath, "utf8"));
176
- assert.equal(lineageBundle.manifest.packageHash, privateBundle.manifest.packageHash, "local lineage must not change the base hash");
177
- for (const name of lineagePaths) {
178
- assert.equal(lineageBundle.files.some((file) => file.path === `.agentlas/${name}`), false, `${name} must not ship`);
179
- assert.equal(lineageExcluded.files.find((file) => file.path === `.agentlas/${name}`)?.reason, "experience-lineage-separate-asset");
180
- fs.appendFileSync(path.join(lineageDir, name), "changed-without-base-release\n", "utf8");
181
- }
182
- const lineageChanged = await packageCloudAgentCli(null, privateRoot, { dryRun: true, llmReview: false });
183
- assert.equal(lineageChanged.manifest.packageHash, privateBundle.manifest.packageHash, "lineage mutation must not create a new base release identity");
184
- assert.equal(
185
- cloudHashPackage([...privateBundle.files, {
186
- path: ".agentlas/experience-relations.jsonl.previous",
187
- sha256: "f".repeat(64),
188
- executable: false,
189
- }], privateBundle.manifest.packageHashVersion),
190
- privateBundle.manifest.packageHash,
191
- "hash helper must defensively omit local Experience lineage siblings",
192
- );
193
- assert.equal(
194
- cloudPortablePathConflict(["Skills/writer/SKILL.md", "skills/reviewer/SKILL.md"])?.code,
195
- "path-alias-collision",
196
- );
197
- assert.equal(
198
- cloudPortablePathConflict(["Caf\u00e9/a.md", "Cafe\u0301/b.md"])?.code,
199
- "path-alias-collision",
200
- );
201
- assert.equal(
202
- cloudPortableExecutableForFile("run.sh", 0, new Set(["run.sh"]), "win32"),
203
- true,
204
- "Windows re-save must recover the portable bit from restore metadata",
205
- );
206
-
207
- const symlinkRoot = path.join(tempDir, "private-symlink-agent");
208
- writePrivateNotes(symlinkRoot);
209
- const outsideFile = path.join(tempDir, "outside-secret.txt");
210
- fs.writeFileSync(outsideFile, "must not follow this link\n", "utf8");
211
- fs.symlinkSync(outsideFile, path.join(symlinkRoot, "outside-link.txt"));
212
- const symlinkBlocked = await packageCloudAgentCli(null, symlinkRoot, {
213
- dryRun: true,
214
- llmReview: false,
215
- });
216
- assert.equal(symlinkBlocked.status, "blocked");
217
- assert.ok(symlinkBlocked.review.findings.some((finding) => finding.id.startsWith("symlink-")));
218
-
219
- const rootSymlink = path.join(tempDir, "private-root-link");
220
- try {
221
- fs.symlinkSync(privateRoot, rootSymlink, "dir");
222
- await assert.rejects(
223
- packageCloudAgentCli(null, rootSymlink, { dryRun: true, llmReview: false }),
224
- /실제 폴더가 아닙니다/,
225
- );
226
- } catch (error) {
227
- if (!error || !["EPERM", "EACCES"].includes(error.code)) throw error;
228
- }
229
-
230
- const outsideRaceSecret = path.join(tempDir, "outside-race-secret.txt");
231
- fs.writeFileSync(outsideRaceSecret, "glpat-abcdefghijklmnopqrstuvwxyz123456\n", "utf8");
232
- const swapRoot = path.join(tempDir, "file-swap-agent");
233
- writePrivateNotes(swapRoot);
234
- const swapTarget = path.join(swapRoot, "zz-race.txt");
235
- fs.writeFileSync(swapTarget, "safe captured bytes\n", "utf8");
236
- const originalOpenSync = fs.openSync;
237
- let fileSwapped = false;
238
- fs.openSync = function patchedOpenSync(file, flags, ...rest) {
239
- if (!fileSwapped && String(file).endsWith(`${path.sep}zz-race.txt`)) {
240
- fileSwapped = true;
241
- fs.renameSync(swapTarget, `${swapTarget}.original`);
242
- fs.symlinkSync(outsideRaceSecret, swapTarget);
243
- }
244
- return originalOpenSync.call(fs, file, flags, ...rest);
245
- };
246
- let swapBlocked;
247
- try {
248
- swapBlocked = await packageCloudAgentCli(null, swapRoot, { dryRun: true, llmReview: false });
249
- } finally {
250
- fs.openSync = originalOpenSync;
251
- }
252
- assert.equal(swapBlocked.status, "blocked", JSON.stringify(swapBlocked.review.findings));
253
- assert.ok(swapBlocked.review.findings.some((finding) => finding.id.startsWith("unstable-file")));
254
- const swapBundle = JSON.parse(fs.readFileSync(swapBlocked.bundlePath, "utf8"));
255
- assert.equal(JSON.stringify(swapBundle).includes(Buffer.from("glpat-abcdefghijklmnopqrstuvwxyz123456\n").toString("base64")), false);
256
-
257
- const growthRoot = path.join(tempDir, "file-growth-agent");
258
- writePrivateNotes(growthRoot);
259
- const growthTarget = path.join(growthRoot, "zz-growth.txt");
260
- fs.writeFileSync(growthTarget, "stable start\n", "utf8");
261
- const originalReadSync = fs.readSync;
262
- let growthFd = null;
263
- let grew = false;
264
- fs.openSync = function captureGrowthFd(file, flags, ...rest) {
265
- const fd = originalOpenSync.call(fs, file, flags, ...rest);
266
- if (String(file).endsWith(`${path.sep}zz-growth.txt`) && (Number(flags) & 3) === fs.constants.O_RDONLY) growthFd = fd;
267
- return fd;
268
- };
269
- fs.readSync = function growAfterFirstRead(fd, ...args) {
270
- const read = originalReadSync.call(fs, fd, ...args);
271
- if (!grew && fd === growthFd && read > 0) {
272
- grew = true;
273
- const appendFd = originalOpenSync(growthTarget, fs.constants.O_WRONLY | fs.constants.O_APPEND);
274
- try { fs.writeSync(appendFd, Buffer.from("changed during scan\n")); } finally { fs.closeSync(appendFd); }
275
- }
276
- return read;
277
- };
278
- let growthBlocked;
279
- try {
280
- growthBlocked = await packageCloudAgentCli(null, growthRoot, { dryRun: true, llmReview: false });
281
- } finally {
282
- fs.openSync = originalOpenSync;
283
- fs.readSync = originalReadSync;
284
- }
285
- assert.equal(growthBlocked.status, "blocked");
286
- assert.ok(growthBlocked.review.findings.some((finding) => finding.id.startsWith("unstable-file")));
287
-
288
- const directorySwapRoot = path.join(tempDir, "directory-swap-agent");
289
- writePrivateNotes(directorySwapRoot);
290
- const nested = path.join(directorySwapRoot, "nested");
291
- fs.mkdirSync(nested);
292
- fs.writeFileSync(path.join(nested, "safe.txt"), "safe\n", "utf8");
293
- const outsideDirectory = path.join(tempDir, "outside-directory");
294
- fs.mkdirSync(outsideDirectory);
295
- fs.writeFileSync(path.join(outsideDirectory, "safe.txt"), "outside must not enter\n", "utf8");
296
- const originalReadDirSync = fs.readdirSync;
297
- let directorySwapped = false;
298
- fs.readdirSync = function swapDirectoryAfterListing(dir, options) {
299
- const entries = originalReadDirSync.call(fs, dir, options);
300
- if (!directorySwapped && String(dir).endsWith(`${path.sep}directory-swap-agent`)) {
301
- directorySwapped = true;
302
- fs.renameSync(nested, `${nested}.original`);
303
- fs.symlinkSync(outsideDirectory, nested, "dir");
304
- }
305
- return entries;
306
- };
307
- let directoryBlocked;
308
- try {
309
- directoryBlocked = await packageCloudAgentCli(null, directorySwapRoot, { dryRun: true, llmReview: false });
310
- } finally {
311
- fs.readdirSync = originalReadDirSync;
312
- }
313
- assert.equal(directoryBlocked.status, "blocked");
314
- assert.ok(directoryBlocked.review.findings.some((finding) => /unsafe-directory|unstable-directory/.test(finding.id)));
315
-
316
- if (process.platform !== "win32") {
317
- const fifoRoot = path.join(tempDir, "fifo-agent");
318
- writePrivateNotes(fifoRoot);
319
- execFileSync("mkfifo", [path.join(fifoRoot, "blocked.pipe")]);
320
- const fifoBlocked = await packageCloudAgentCli(null, fifoRoot, { dryRun: true, llmReview: false });
321
- assert.equal(fifoBlocked.status, "blocked");
322
- assert.ok(fifoBlocked.review.findings.some((finding) => finding.id.startsWith("unsupported-entry")));
323
- }
324
-
325
- const requestsBeforeSecretGates = requests.length;
326
- const unquotedSecretRoot = path.join(tempDir, "unquoted-secret-agent");
327
- writePrivateNotes(unquotedSecretRoot);
328
- fs.writeFileSync(path.join(unquotedSecretRoot, "config.yaml"), "password: hunter2secret\n", "utf8");
329
- const unquotedSecret = await packageCloudAgentCli(null, unquotedSecretRoot, { dryRun: false, llmReview: false });
330
- assert.equal(unquotedSecret.status, "blocked");
331
- assert.ok(unquotedSecret.review.findings.some((finding) => finding.id.startsWith("generic-unquoted-secret")));
332
-
333
- const utf16SecretRoot = path.join(tempDir, "utf16-secret-agent");
334
- writePrivateNotes(utf16SecretRoot);
335
- fs.writeFileSync(
336
- path.join(utf16SecretRoot, "settings.ps1"),
337
- Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from("api_key=unquoted-secret-value-123456\r\n", "utf16le")]),
338
- );
339
- const utf16Secret = await packageCloudAgentCli(null, utf16SecretRoot, { dryRun: false, llmReview: false });
340
- assert.equal(utf16Secret.status, "blocked");
341
-
342
- const bomlessUtf16SecretRoot = path.join(tempDir, "bomless-utf16-secret-agent");
343
- writePrivateNotes(bomlessUtf16SecretRoot);
344
- fs.writeFileSync(
345
- path.join(bomlessUtf16SecretRoot, "opaque.payload"),
346
- Buffer.from(`${"A".repeat(5000)}\napi_key=unquoted-secret-value-123456\n`, "utf16le"),
347
- );
348
- const bomlessUtf16Secret = await packageCloudAgentCli(null, bomlessUtf16SecretRoot, { dryRun: false, llmReview: false });
349
- assert.equal(bomlessUtf16Secret.status, "blocked");
350
-
351
- const binarySecretRoot = path.join(tempDir, "binary-secret-agent");
352
- writePrivateNotes(binarySecretRoot);
353
- fs.writeFileSync(path.join(binarySecretRoot, "opaque.payload"), Buffer.from([0x00, ...Buffer.from("glpat-abcdefghijklmnopqrstuvwxyz123456"), 0xff]));
354
- const binarySecret = await packageCloudAgentCli(null, binarySecretRoot, { dryRun: false, llmReview: false });
355
- assert.equal(binarySecret.status, "blocked");
356
- assert.ok(binarySecret.review.findings.some((finding) => finding.id.startsWith("gitlab-token")));
357
- assert.equal(requests.length, requestsBeforeSecretGates, "blocked secret packages must perform zero registration fetches");
358
-
359
- const placeholderRoot = path.join(tempDir, "placeholder-agent");
360
- writePrivateNotes(placeholderRoot);
361
- fs.writeFileSync(path.join(placeholderRoot, "config.yaml"), "password: configure_on_this_machine\napi_key: ${API_KEY}\n", "utf8");
362
- const placeholderPackage = await packageCloudAgentCli(null, placeholderRoot, { dryRun: true, llmReview: false });
363
- assert.equal(placeholderPackage.status, "dry-run");
364
-
365
- const privateSaved = await packageCloudAgentCli(null, privateRoot, {
366
- dryRun: false,
367
- llmReview: false,
368
- });
369
- assert.equal(privateSaved.status, "registered");
370
- assert.match(privateSaved.summary, /Saved .* privately in Agent Cloud/);
371
-
372
- const invalidReceiptRoot = path.join(tempDir, "invalid-receipt-agent");
373
- writePrivateNotes(invalidReceiptRoot);
374
- await assert.rejects(
375
- packageCloudAgentCli(null, invalidReceiptRoot, {
376
- slug: "invalid-receipt-agent",
377
- dryRun: false,
378
- llmReview: false,
379
- }),
380
- /invalid or mismatched registration receipt/,
381
- "malformed HTTP 2xx must never become synthetic registration success",
382
- );
383
-
384
- const publicWithoutRoutingRoot = path.join(tempDir, "public-without-routing");
385
- fs.mkdirSync(publicWithoutRoutingRoot, { recursive: true });
386
- fs.writeFileSync(path.join(publicWithoutRoutingRoot, "AGENTS.md"), "# Missing Routing\n", "utf8");
387
- const publicBlocked = await packageCloudAgentCli(null, publicWithoutRoutingRoot, {
388
- visibility: "marketplace",
389
- dryRun: true,
390
- llmReview: false,
391
- });
392
- assert.equal(publicBlocked.status, "blocked");
393
- assert.ok(publicBlocked.review.findings.some((finding) => finding.id === "routing-card-required"));
394
-
395
- const publicRoot = path.join(tempDir, "public-agent");
396
- writePublicAgent(publicRoot);
397
- const publicPublished = await packageCloudAgentCli(null, publicRoot, {
398
- visibility: "marketplace",
399
- dryRun: false,
400
- llmReview: false,
401
- });
402
- assert.equal(publicPublished.status, "registered");
403
- assert.match(publicPublished.summary, /Published .* publicly to Agentlas Hub/);
404
-
405
- const publicCareerRoot = path.join(tempDir, "public-career-agent");
406
- writePublicAgent(publicCareerRoot);
407
- const rawCareerCard = {
408
- kind: "agentlas-public-career-card",
409
- schemaVersion: "1",
410
- projectName: "Career fixture",
411
- privacy: {
412
- rawLocalPathsIncluded: false,
413
- rawPromptsIncluded: false,
414
- rawTranscriptsIncluded: false,
415
- sourceTextIncluded: false,
416
- },
417
- counts: { evidence: 3 },
418
- generatorInternal: { rawSourceId: "must-not-leave-host" },
419
- };
420
- fs.writeFileSync(
421
- path.join(publicCareerRoot, ".agentlas", "public-career-card.json"),
422
- JSON.stringify(rawCareerCard, null, 2) + "\n",
423
- "utf8",
424
- );
425
- const publicCareer = await packageCloudAgentCli(null, publicCareerRoot, {
426
- visibility: "marketplace",
427
- dryRun: true,
428
- llmReview: false,
429
- });
430
- assert.equal(publicCareer.status, "dry-run");
431
- const publicCareerBundle = JSON.parse(fs.readFileSync(publicCareer.bundlePath, "utf8"));
432
- const sanitizedCareerFile = publicCareerBundle.files.find((file) => file.path === ".agentlas/public-career-card.json");
433
- assert.ok(sanitizedCareerFile);
434
- const sanitizedCareer = JSON.parse(Buffer.from(sanitizedCareerFile.contentBase64, "base64").toString("utf8"));
435
- assert.equal("generatorInternal" in sanitizedCareer, false);
436
- assert.deepEqual(sanitizedCareer, publicCareerBundle.manifest.careerGraph);
437
- assert.deepEqual(sanitizedCareer, publicCareerBundle.careerGraph);
438
-
439
- const requestsBeforeLeakyCareer = requests.length;
440
- const leakyCareerRoot = path.join(tempDir, "leaky-career-agent");
441
- writePublicAgent(leakyCareerRoot);
442
- fs.writeFileSync(
443
- path.join(leakyCareerRoot, ".agentlas", "public-career-card.json"),
444
- JSON.stringify({ ...rawCareerCard, generatorInternal: { sourcePath: "/Users/private/career.sqlite" } }, null, 2) + "\n",
445
- "utf8",
446
- );
447
- const leakyCareer = await packageCloudAgentCli(null, leakyCareerRoot, {
448
- visibility: "marketplace",
449
- dryRun: false,
450
- llmReview: false,
451
- });
452
- assert.equal(leakyCareer.status, "blocked");
453
- assert.ok(leakyCareer.review.findings.some((finding) => finding.id === "career-card-local-path"));
454
- const leakyCareerBundle = JSON.parse(fs.readFileSync(leakyCareer.bundlePath, "utf8"));
455
- assert.equal(leakyCareerBundle.files.some((file) => file.path === ".agentlas/public-career-card.json"), false);
456
- assert.equal(requests.length, requestsBeforeLeakyCareer, "blocked Career Graph packages must perform zero registration fetches");
457
-
458
- const secretRoot = path.join(tempDir, "secret-agent");
459
- writePrivateNotes(secretRoot);
460
- fs.writeFileSync(path.join(secretRoot, ".env"), "TOKEN=not-a-real-secret-for-tests\n", "utf8");
461
- const secretBlocked = await packageCloudAgentCli(null, secretRoot, {
462
- dryRun: true,
463
- llmReview: false,
464
- });
465
- assert.equal(secretBlocked.status, "blocked");
466
- assert.ok(secretBlocked.review.findings.some((finding) => finding.category === "secret"));
467
-
468
- assert.equal(requests.length, 3);
469
- assert.equal(requests[0].visibility, "private-link");
470
- assert.equal(requests[0].manifest.visibility, "private-link");
471
- assert.equal(requests[0].manifest.packageHashVersion, "path-sha256-executable-v2");
472
- assert.equal(requests[0].manifest.routingCard, undefined);
473
- assert.equal(requestHeaders[0]["if-none-match"], "*");
474
- assert.equal(requestHeaders[0]["if-match"], undefined);
475
- assert.equal(requests[2].visibility, "marketplace");
476
- assert.equal(requests[2].manifest.visibility, "marketplace");
477
- assert.equal(requests[2].manifest.routingCard.schemaVersion, "routing-card/2.0");
478
-
479
- console.log("cloud private-save/public-publish: PASS");
480
- } finally {
481
- await close(server).catch(() => {});
482
- fs.rmSync(tempDir, { recursive: true, force: true });
483
- }
484
- })().catch((error) => {
485
- console.error(error);
486
- process.exitCode = 1;
487
- });
@@ -1,52 +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 root = path.resolve(__dirname, "..");
10
- const terminal = require(path.join(root, "engine", "agentlas.cjs"));
11
- const tools = require(path.join(root, "engine", "agentlas-tools.cjs"));
12
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-credential-env-"));
13
-
14
- try {
15
- const shellCwd = path.join(temp, "project");
16
- fs.mkdirSync(shellCwd, { recursive: true });
17
- assert.equal(
18
- terminal.resolveCredentialSourcePath("keys/service.json", shellCwd),
19
- path.join(shellCwd, "keys", "service.json"),
20
- "relative credential source must resolve from the caller cwd",
21
- );
22
-
23
- const credentials = path.join(temp, "credentials.env");
24
- terminal.upsertEnvLine(credentials, "SERVICE_TOKEN", "first");
25
- terminal.upsertEnvLine(credentials, "SERVICE_TOKEN", "second");
26
- assert.equal(fs.readFileSync(credentials, "utf8"), "SERVICE_TOKEN=second\n");
27
- if (process.platform !== "win32") {
28
- assert.equal(fs.statSync(credentials).mode & 0o777, 0o600, "credential env must be owner-only");
29
- }
30
-
31
- const before = process.env.AGENTLAS_SCOPE_TEST;
32
- const result = tools.runTool(
33
- "bash",
34
- { command: 'printf %s "$AGENTLAS_SCOPE_TEST"' },
35
- {
36
- cwd: shellCwd,
37
- permission: "full",
38
- env: { ...process.env, AGENTLAS_SCOPE_TEST: "turn-only" },
39
- },
40
- );
41
- assert.equal(result.ok, true);
42
- assert.match(result.content, /turn-only/);
43
- assert.equal(process.env.AGENTLAS_SCOPE_TEST, before, "turn env must not leak into the host process");
44
-
45
- const replSource = fs.readFileSync(path.join(root, "engine", "agentlas-repl.cjs"), "utf8");
46
- assert.equal(replSource.includes("Object.assign(process.env"), false);
47
- assert.match(replSource, /ctx:\s*\{ \.\.\.ctx, env: runEnv \}/);
48
-
49
- console.log(JSON.stringify({ ok: true, checks: 8 }, null, 2));
50
- } finally {
51
- fs.rmSync(temp, { recursive: true, force: true });
52
- }
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /*
4
- * bug-hunter(2026-07-12) 확정 결함 회귀 고정:
5
- * 1) node:sqlite 폴백 래퍼 API 패리티 — exec/pragma 누락으로 ensureMemoryContextColumn이
6
- * 조용히 죽어 context_json 마이그레이션이 안 되던 버그.
7
- * 2) 민감 상태 파일 원자적·0600 쓰기 — 크래시 중간쓰기 JSON 손상 + world-readable 노출.
8
- * 3) /install 중복 case — i18n 핸들러가 죽은 코드가 되던 버그(정적 검증).
9
- * 4) gemini 2턴+ 시스템 프롬프트 소실(정적 검증: resume 게이트).
10
- */
11
- const assert = require("node:assert/strict");
12
- const fs = require("node:fs");
13
- const os = require("node:os");
14
- const path = require("node:path");
15
- const engine = require("../engine/agentlas.cjs");
16
-
17
- // ── 1) node:sqlite 폴백 래퍼: exec/pragma 존재 + ensureMemoryContextColumn 실동작 ──
18
- {
19
- const tmpDb = path.join(os.tmpdir(), `agentlas-wrap-${process.pid}.sqlite`);
20
- try { fs.unlinkSync(tmpDb); } catch { /* fresh */ }
21
- const db = engine.openNodeSqliteDb(tmpDb);
22
- assert.equal(typeof db.exec, "function", "폴백 래퍼에 exec가 있어야 함");
23
- assert.equal(typeof db.pragma, "function", "폴백 래퍼에 pragma가 있어야 함");
24
-
25
- // 앱 마이그레이션 전(context_json 없는) memory_entries를 만든 뒤 ensureMemoryContextColumn 실행
26
- db.exec("CREATE TABLE memory_entries (id INTEGER PRIMARY KEY, kind TEXT, content TEXT)");
27
- engine.ensureMemoryContextColumn(db); // 예전 버그: db.exec undefined → TypeError를 try/catch가 삼킴
28
- const cols = db.prepare("PRAGMA table_info(memory_entries)").all().map((c) => c.name);
29
- assert.ok(cols.includes("context_json"), `context_json 컬럼이 추가돼야 함 — 실제: ${cols.join(",")}`);
30
-
31
- // pragma() 스칼라 근사 관례
32
- const uv = db.pragma("user_version");
33
- assert.ok(typeof uv === "number" || typeof uv === "bigint", `pragma 스칼라 반환 — 실제: ${typeof uv}`);
34
- db.close();
35
- try { fs.unlinkSync(tmpDb); } catch { /* ignore */ }
36
- }
37
-
38
- // ── 2) writeJsonPrivateAtomicCli: 유효 JSON + 0600 + 원자성(temp 잔여물 없음) ──
39
- {
40
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-priv-"));
41
- const target = path.join(dir, "cli-sessions.json");
42
- const value = [{ kind: "claude-code", sessionId: "s-123", cwd: "/x" }];
43
- engine.writeJsonPrivateAtomicCli(target, value);
44
-
45
- assert.deepEqual(JSON.parse(fs.readFileSync(target, "utf8")), value, "쓴 JSON을 그대로 읽어야 함");
46
- if (process.platform !== "win32") {
47
- const mode = fs.statSync(target).mode & 0o777;
48
- assert.equal(mode, 0o600, `민감 파일은 0600 — 실제: 0o${mode.toString(8)}`);
49
- }
50
- // temp 잔여물이 남지 않아야 함
51
- const leftovers = fs.readdirSync(dir).filter((n) => n.includes(".tmp"));
52
- assert.equal(leftovers.length, 0, `temp 잔여물 없어야 함 — 실제: ${leftovers.join(",")}`);
53
- fs.rmSync(dir, { recursive: true, force: true });
54
- }
55
-
56
- // ── 3) /install 중복 case 제거(정적) — handleSlash에 case "install"은 정확히 하나 ──
57
- {
58
- const repl = fs.readFileSync(path.join(__dirname, "..", "engine", "agentlas-repl.cjs"), "utf8");
59
- const count = (repl.match(/case "install":/g) || []).length;
60
- assert.equal(count, 1, `case "install"은 하나여야 함(중복 시 i18n 핸들러가 죽은 코드) — 실제: ${count}`);
61
- // 살아남은 핸들러는 i18n 키를 쓴다(하드코딩 한국어 아님)
62
- assert.ok(/ui\.t\("installUsage"\)/.test(repl), "살아있는 install 핸들러는 installUsage i18n 키 사용");
63
- }
64
-
65
- // ── 4) gemini 시스템 프롬프트 유지(정적) — resume 게이트가 claude/codex로 한정 ──
66
- {
67
- const repl = fs.readFileSync(path.join(__dirname, "..", "engine", "agentlas-repl.cjs"), "utf8");
68
- assert.ok(/resumesServerSide\s*=\s*rt\.kind\s*===\s*"claude-code"\s*\|\|\s*rt\.kind\s*===\s*"codex"/.test(repl),
69
- "resume 게이트는 claude-code/codex로 한정돼야 함(gemini는 매 턴 시스템 프롬프트 재전송)");
70
- assert.ok(/systemPrompt:\s*session\.id\s*&&\s*resumesServerSide\s*\?\s*""\s*:\s*sys/.test(repl),
71
- "gemini는 session.id가 있어도 시스템 프롬프트를 비우지 않아야 함");
72
- }
73
-
74
- console.log("engine-hardening-regression: PASS");