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.
Files changed (163) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +12 -4
  3. package/bin/agentlas.cjs +354 -16
  4. package/engine/acp/server.cjs +34 -3
  5. package/engine/agentlas-api-agent.cjs +151 -120
  6. package/engine/agentlas-banner.cjs +6 -2
  7. package/engine/agentlas-capabilities.cjs +17 -10
  8. package/engine/agentlas-cloud-runtime.cjs +4 -5
  9. package/engine/agentlas-config.cjs +19 -54
  10. package/engine/agentlas-core-harness.cjs +59 -5
  11. package/engine/agentlas-desktop-loadout.cjs +17 -3
  12. package/engine/agentlas-evolution.cjs +75 -18
  13. package/engine/agentlas-experience-exchange.cjs +145 -14
  14. package/engine/agentlas-experience-intake.cjs +2 -2
  15. package/engine/agentlas-i18n.cjs +0 -2
  16. package/engine/agentlas-input.cjs +273 -20
  17. package/engine/agentlas-judgment.cjs +0 -0
  18. package/engine/agentlas-mcp-env.cjs +84 -23
  19. package/engine/agentlas-mcp-wrapper.cjs +36 -4
  20. package/engine/agentlas-memory-governance.cjs +2 -1
  21. package/engine/agentlas-memory-import.cjs +49 -12
  22. package/engine/agentlas-native-host.cjs +279 -44
  23. package/engine/agentlas-onboard.cjs +23 -1
  24. package/engine/agentlas-permissions.cjs +5 -1
  25. package/engine/agentlas-sqlite-policy.cjs +40 -2
  26. package/engine/agentlas-tasks.cjs +7 -0
  27. package/engine/agentlas-tools.cjs +37 -10
  28. package/engine/agentlas-ui.cjs +26 -4
  29. package/engine/agentlas-workforce.cjs +30 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +21 -4
  32. package/engine/agents/builder.cjs +35 -10
  33. package/engine/agents/files.cjs +81 -10
  34. package/engine/agents/import-local.cjs +10 -2
  35. package/engine/agents/registry.cjs +10 -3
  36. package/engine/agents/router.cjs +7 -7
  37. package/engine/agents/routes.cjs +31 -9
  38. package/engine/automation/daemon.cjs +154 -40
  39. package/engine/automation/launchd.cjs +395 -16
  40. package/engine/automation/schedule.cjs +90 -3
  41. package/engine/automation/store.cjs +163 -59
  42. package/engine/browser/cdp.cjs +190 -17
  43. package/engine/browser/vault.cjs +1 -0
  44. package/engine/cli-output.cjs +96 -30
  45. package/engine/cloud/auth.cjs +131 -13
  46. package/engine/cloud/hub-client.cjs +41 -16
  47. package/engine/cloud-assets/cas.cjs +18 -9
  48. package/engine/cloud-assets/commands.cjs +108 -30
  49. package/engine/cloud-assets/package.cjs +66 -3
  50. package/engine/cloud-assets/restore.cjs +12 -9
  51. package/engine/cloud-assets/state.cjs +609 -116
  52. package/engine/commands/acp.cjs +13 -2
  53. package/engine/commands/automation.cjs +75 -24
  54. package/engine/commands/billing.cjs +6 -1
  55. package/engine/commands/browser.cjs +61 -29
  56. package/engine/commands/build.cjs +72 -14
  57. package/engine/commands/call.cjs +1 -1
  58. package/engine/commands/cd.cjs +1 -1
  59. package/engine/commands/cloud.cjs +2 -1
  60. package/engine/commands/connect.cjs +38 -13
  61. package/engine/commands/creds.cjs +74 -29
  62. package/engine/commands/doctor.cjs +17 -1
  63. package/engine/commands/document.cjs +38 -10
  64. package/engine/commands/env.cjs +6 -1
  65. package/engine/commands/firm.cjs +42 -12
  66. package/engine/commands/graph.cjs +36 -32
  67. package/engine/commands/help.cjs +11 -1
  68. package/engine/commands/hep.cjs +1 -1
  69. package/engine/commands/import.cjs +5 -4
  70. package/engine/commands/index.cjs +1 -1
  71. package/engine/commands/install.cjs +6 -4
  72. package/engine/commands/list.cjs +56 -16
  73. package/engine/commands/login.cjs +5 -0
  74. package/engine/commands/logout.cjs +6 -1
  75. package/engine/commands/mcp.cjs +150 -37
  76. package/engine/commands/memory.cjs +3 -2
  77. package/engine/commands/multimodal.cjs +12 -0
  78. package/engine/commands/native.cjs +5 -1
  79. package/engine/commands/one.cjs +78 -33
  80. package/engine/commands/plugin.cjs +13 -2
  81. package/engine/commands/project.cjs +15 -12
  82. package/engine/commands/research.cjs +18 -7
  83. package/engine/commands/roles.cjs +104 -28
  84. package/engine/commands/route.cjs +2 -2
  85. package/engine/commands/run.cjs +42 -9
  86. package/engine/commands/search.cjs +23 -2
  87. package/engine/commands/setup.cjs +6 -1
  88. package/engine/commands/storm.cjs +44 -6
  89. package/engine/commands/swarm.cjs +47 -9
  90. package/engine/commands/telegram.cjs +6 -1
  91. package/engine/commands/uninstall.cjs +12 -6
  92. package/engine/commands/update.cjs +5 -0
  93. package/engine/commands/upload.cjs +2 -1
  94. package/engine/commands/usage.cjs +6 -1
  95. package/engine/commands/version.cjs +6 -1
  96. package/engine/commands/whoami.cjs +6 -1
  97. package/engine/commands/workforce.cjs +60 -16
  98. package/engine/core/capability-grants.cjs +7 -2
  99. package/engine/core/daemon-client.cjs +36 -7
  100. package/engine/core/db.cjs +10 -2
  101. package/engine/core/desktop-core-fetch.cjs +477 -77
  102. package/engine/core/schema-ensure.cjs +23 -11
  103. package/engine/experience/build.cjs +5 -1
  104. package/engine/experience/intents.cjs +35 -82
  105. package/engine/experience/runtime.cjs +7 -1
  106. package/engine/experience/variant.cjs +58 -4
  107. package/engine/firms/orchestrate.cjs +9 -6
  108. package/engine/graph/ask-model.cjs +22 -11
  109. package/engine/graph/interview.cjs +74 -23
  110. package/engine/graph/package.cjs +63 -8
  111. package/engine/hephaestus/local-core.cjs +18 -5
  112. package/engine/hephaestus/runtime.cjs +48 -43
  113. package/engine/hub/install.cjs +484 -63
  114. package/engine/hub/plugins.cjs +391 -29
  115. package/engine/mcp/consent.cjs +25 -17
  116. package/engine/mcp/contract.cjs +229 -24
  117. package/engine/mcp/inventory.cjs +46 -10
  118. package/engine/mcp/plan.cjs +10 -1
  119. package/engine/mcp/probe.cjs +24 -9
  120. package/engine/memory-cli/curate.cjs +74 -16
  121. package/engine/oberon/common.cjs +37 -12
  122. package/engine/oberon/manifest.cjs +25 -2
  123. package/engine/oberon/outputs.cjs +49 -19
  124. package/engine/oberon/render.cjs +665 -40
  125. package/engine/project/career-graph.cjs +15 -2
  126. package/engine/project/controller.cjs +133 -15
  127. package/engine/project/credentials.cjs +382 -34
  128. package/engine/project/env-file.cjs +93 -8
  129. package/engine/project/memory-context.cjs +11 -57
  130. package/engine/project/ontology.cjs +66 -25
  131. package/engine/project/paths.cjs +42 -8
  132. package/engine/project/seed.cjs +88 -51
  133. package/engine/project/state.cjs +41 -16
  134. package/engine/project/team.cjs +26 -1
  135. package/engine/runtime-refusal.cjs +4 -1
  136. package/engine/runtimes/acp-driver.cjs +56 -9
  137. package/engine/runtimes/auth-evidence.cjs +21 -2
  138. package/engine/runtimes/detect.cjs +28 -8
  139. package/engine/runtimes/kinds.cjs +12 -4
  140. package/engine/runtimes/overrides.cjs +4 -3
  141. package/engine/runtimes/resolve.cjs +8 -7
  142. package/engine/runtimes/roles.cjs +7 -5
  143. package/engine/sessions/apply-fences.cjs +396 -56
  144. package/engine/sessions/fences.cjs +111 -27
  145. package/engine/sessions/memory-turn.cjs +62 -3
  146. package/engine/sessions/orchestrator.cjs +11 -1
  147. package/engine/sessions/prompt.cjs +66 -9
  148. package/engine/sessions/session.cjs +70 -10
  149. package/engine/sessions/sink.cjs +78 -12
  150. package/engine/sessions/store.cjs +29 -7
  151. package/engine/storm/storm.cjs +26 -3
  152. package/engine/storm/swarm.cjs +31 -3
  153. package/engine/telegram/connect.cjs +185 -22
  154. package/engine/ui/commands-catalog.cjs +1 -1
  155. package/engine/ui/palette.cjs +12 -2
  156. package/engine/ui/repl.cjs +57 -9
  157. package/engine/ui/screens.cjs +2 -4
  158. package/engine/vendor/desktop-core.manifest.json +5 -5
  159. package/engine/workforce/capture.cjs +9 -27
  160. package/engine/workforce/deps.cjs +18 -6
  161. package/engine/workforce/local-core-transport.cjs +68 -5
  162. package/package.json +1 -1
  163. package/engine/commands/career-graph.cjs +0 -51
@@ -305,7 +305,7 @@ function readPrivateFeed(file, now) {
305
305
  if (error && error.code === "ENOENT") return { mode: "skip", reason: "desktop-loadout-feed-absent" };
306
306
  return { mode: "skip", reason: "desktop-loadout-feed-unreadable" };
307
307
  }
308
- if (!stat.isFile() || stat.isSymbolicLink()) {
308
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
309
309
  return { mode: "skip", reason: "desktop-loadout-feed-not-private-regular-file" };
310
310
  }
311
311
  if (process.platform !== "win32") {
@@ -340,6 +340,7 @@ function readPrivateFeed(file, now) {
340
340
  const opened = fs.fstatSync(descriptor);
341
341
  if (
342
342
  !opened.isFile() ||
343
+ opened.nlink !== 1 ||
343
344
  opened.dev !== stat.dev ||
344
345
  opened.ino !== stat.ino ||
345
346
  opened.size !== stat.size ||
@@ -348,7 +349,19 @@ function readPrivateFeed(file, now) {
348
349
  return { mode: "skip", reason: "desktop-loadout-feed-changed-during-read" };
349
350
  }
350
351
  raw = fs.readFileSync(descriptor, "utf8");
351
- if (Buffer.byteLength(raw, "utf8") > MAX_FILE_BYTES) {
352
+ const after = fs.fstatSync(descriptor);
353
+ if (
354
+ after.nlink !== 1 ||
355
+ after.dev !== opened.dev ||
356
+ after.ino !== opened.ino ||
357
+ after.size !== opened.size ||
358
+ after.mtimeMs !== opened.mtimeMs ||
359
+ after.ctimeMs !== opened.ctimeMs
360
+ ) {
361
+ return { mode: "skip", reason: "desktop-loadout-feed-changed-during-read" };
362
+ }
363
+ const rawBytes = Buffer.byteLength(raw, "utf8");
364
+ if (rawBytes !== after.size || rawBytes > MAX_FILE_BYTES) {
352
365
  return { mode: "skip", reason: "desktop-loadout-feed-size-invalid" };
353
366
  }
354
367
  } finally { fs.closeSync(descriptor); }
@@ -407,7 +420,8 @@ function exactLocalAuthority(db) {
407
420
  function sameExactList(value, expected) {
408
421
  if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) return true;
409
422
  if (!Array.isArray(value)) return false;
410
- const normalized = [...new Set(value.map(String).filter(Boolean))];
423
+ const normalized = value.map(String);
424
+ if (normalized.some((item) => !item) || new Set(normalized).size !== normalized.length) return false;
411
425
  return normalized.length === expected.length && normalized.every((item, index) => item === expected[index]);
412
426
  }
413
427
 
@@ -22,6 +22,8 @@ function sha256(content) {
22
22
  }
23
23
 
24
24
  const ABSENT_TARGET_HASH = sha256("agentlas:absent-agent-asset:v1");
25
+ const MAX_EVOLUTION_CONTENT_BYTES = 512 * 1024;
26
+ const SHA256_RE = /^[a-f0-9]{64}$/;
25
27
  const RULE_TARGETS = new Set([
26
28
  "system-prompt.md",
27
29
  "soul.md",
@@ -42,6 +44,15 @@ function parseSource(json) {
42
44
  }
43
45
  }
44
46
 
47
+ function terminalSafe(value, maxLength = 300) {
48
+ return String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
49
+ }
50
+
51
+ function normalizedListLimit(value) {
52
+ const parsed = Number(value);
53
+ return Number.isInteger(parsed) ? Math.max(1, Math.min(parsed, 200)) : 50;
54
+ }
55
+
45
56
  /** 대기 중(사람 결정 필요) 고위험 성장 제안 개수 — 터미널 홈 배너용. */
46
57
  function countPendingGrowthProposals(db) {
47
58
  try {
@@ -68,19 +79,23 @@ function listGrowthProposals(db, limit = 50) {
68
79
  ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
69
80
  LIMIT ?`,
70
81
  )
71
- .all(limit);
82
+ .all(normalizedListLimit(limit));
72
83
  } catch {
73
84
  rows = [];
74
85
  }
75
86
  const pending = [];
87
+ const applied = [];
76
88
  const autoApplied = [];
77
89
  for (const row of rows) {
78
90
  const source = parseSource(row.source_json);
79
91
  const entry = { row, source };
80
92
  if (row.status === "candidate") pending.push(entry);
81
- else if (source._autoApplied === true) autoApplied.push(entry);
93
+ else {
94
+ applied.push(entry);
95
+ if (source._autoApplied === true) autoApplied.push(entry);
96
+ }
82
97
  }
83
- return { pending, autoApplied };
98
+ return { pending, applied, autoApplied };
84
99
  }
85
100
 
86
101
  function agentById(db, id) {
@@ -145,27 +160,28 @@ function printCard(out, entry, index) {
145
160
  const { row, source } = entry;
146
161
  const card = source.humanCard && typeof source.humanCard === "object" ? source.humanCard : null;
147
162
  const tier = source.riskTier === "high" ? "high" : "low";
148
- out(` [${index}] ${row.id} (${tier} · ${row.status} · ${row.agent_id})`);
163
+ const application = row.status === "candidate" ? "승인 전" : source._autoApplied === true ? "자동 반영" : "승인 후 반영";
164
+ out(` [${index}] ${terminalSafe(row.id)} (${tier} · ${terminalSafe(row.status)} · ${application} · ${terminalSafe(row.agent_id)})`);
149
165
  if (card) {
150
- out(` 배운 것 : ${card.learned}`);
151
- out(` 바뀌는 것: ${card.change}`);
152
- out(` 되돌리기 : ${card.reversible}`);
166
+ out(` 배운 것 : ${terminalSafe(card.learned)}`);
167
+ out(` 바뀐 : ${terminalSafe(card.change)}`);
168
+ out(` 안전장치: ${terminalSafe(card.reversible)}`);
153
169
  } else {
154
- out(` ${row.summary}`);
170
+ out(` ${terminalSafe(row.summary)}`);
155
171
  }
156
172
  }
157
173
 
158
174
  function cmdList(db, out) {
159
- const { pending, autoApplied } = listGrowthProposals(db);
175
+ const { pending, applied } = listGrowthProposals(db);
160
176
  out("== agent growth proposals ==");
161
177
  out("");
162
178
  out(`대기(승인 필요) ${pending.length}건:`);
163
179
  if (!pending.length) out(" (없음)");
164
180
  pending.forEach((entry, i) => printCard(out, entry, i + 1));
165
181
  out("");
166
- out(`자동 적용됨(저위험) ${autoApplied.length}건:`);
167
- if (!autoApplied.length) out(" (없음)");
168
- autoApplied.forEach((entry, i) => printCard(out, entry, i + 1));
182
+ out(`성장 반영 완료 ${applied.length}건 (필요할 때 되돌릴 수 있는 안전장치):`);
183
+ if (!applied.length) out(" (없음)");
184
+ applied.forEach((entry, i) => printCard(out, entry, i + 1));
169
185
  out("");
170
186
  out("적용: agentlas evolve apply <id> · 되돌리기: agentlas evolve revert <id>");
171
187
  }
@@ -182,6 +198,36 @@ function loadProposal(db, id, fail) {
182
198
  return row;
183
199
  }
184
200
 
201
+ function validateProposalPayload(row, fail) {
202
+ if (!SHA256_RE.test(String(row.before_hash)) || !SHA256_RE.test(String(row.after_hash))) {
203
+ fail("Evolution proposal contains an invalid content hash");
204
+ return false;
205
+ }
206
+ if (typeof row.before_content !== "string" || typeof row.after_content !== "string") {
207
+ fail("Evolution proposal content is invalid");
208
+ return false;
209
+ }
210
+ if (
211
+ Buffer.byteLength(row.before_content, "utf8") > MAX_EVOLUTION_CONTENT_BYTES ||
212
+ Buffer.byteLength(row.after_content, "utf8") > MAX_EVOLUTION_CONTENT_BYTES
213
+ ) {
214
+ fail("Evolution proposal exceeds the portable 512 KiB content limit");
215
+ return false;
216
+ }
217
+ if (sha256(row.after_content) !== row.after_hash) {
218
+ fail("Evolution proposal after-content does not match its approved hash");
219
+ return false;
220
+ }
221
+ if (
222
+ (row.before_hash === ABSENT_TARGET_HASH && row.before_content !== "") ||
223
+ (row.before_hash !== ABSENT_TARGET_HASH && sha256(row.before_content) !== row.before_hash)
224
+ ) {
225
+ fail("Evolution proposal before-content does not match its approved baseline hash");
226
+ return false;
227
+ }
228
+ return true;
229
+ }
230
+
185
231
  // 터미널이 부트스트랩한 DB는 v45라 진화 영수증 테이블이 없을 수 있다(데스크탑 v51+ 마이그레이션이
186
232
  // 추가). 앱과 동일한 스키마로 idempotent 생성 — 앱이 이미 만들었으면 no-op.
187
233
  function ensureReceiptTable(db) {
@@ -206,7 +252,7 @@ function ensureReceiptTable(db) {
206
252
 
207
253
  function insertReceipt(db, row, action, hashBefore, hashAfter, now) {
208
254
  ensureReceiptTable(db);
209
- db.prepare(
255
+ const inserted = db.prepare(
210
256
  `INSERT INTO agent_evolution_receipts (
211
257
  id, proposal_id, agent_id, action, target_path,
212
258
  version_before, version_after, target_hash_before, target_hash_after,
@@ -225,11 +271,13 @@ function insertReceipt(db, row, action, hashBefore, hashAfter, now) {
225
271
  hashAfter,
226
272
  now,
227
273
  );
274
+ if (inserted.changes !== 1) throw new Error(`Evolution ${action} receipt already exists; proposal state is inconsistent`);
228
275
  }
229
276
 
230
277
  function cmdApply(db, id, out, fail, agentFolder) {
231
278
  const row = loadProposal(db, id, fail);
232
279
  if (!row) return;
280
+ if (!validateProposalPayload(row, fail)) return;
233
281
  if (row.status !== "candidate") {
234
282
  return fail(`Only a pending candidate can be applied; ${id} is '${row.status}'.`);
235
283
  }
@@ -273,6 +321,7 @@ function cmdApply(db, id, out, fail, agentFolder) {
273
321
  function cmdRevert(db, id, out, fail, agentFolder) {
274
322
  const row = loadProposal(db, id, fail);
275
323
  if (!row) return;
324
+ if (!validateProposalPayload(row, fail)) return;
276
325
  if (row.status !== "applied" && row.status !== "measured") {
277
326
  return fail(`Only an applied proposal can be reverted; ${id} is '${row.status}'.`);
278
327
  }
@@ -294,7 +343,11 @@ function cmdRevert(db, id, out, fail, agentFolder) {
294
343
  const locked = db.prepare("SELECT status FROM agent_evolution_proposals WHERE id=?").get(row.id);
295
344
  if (!locked || !["applied", "measured"].includes(locked.status)) throw new Error("Evolution proposal changed before revert; reload it first");
296
345
  if (currentTargetHash(file).hash !== row.after_hash) throw new Error("Agent prompt changed before revert; reload it first");
297
- writeTargetAtomic(file, row.before_content);
346
+ // A proposal may have created the prompt asset from an absent baseline.
347
+ // Restoring that baseline means removing the created file, not writing an
348
+ // empty file (whose sha256 can never equal ABSENT_TARGET_HASH).
349
+ if (row.before_hash === ABSENT_TARGET_HASH) fs.rmSync(file, { force: true });
350
+ else writeTargetAtomic(file, row.before_content);
298
351
  wroteTarget = true;
299
352
  if (currentTargetHash(file).hash !== row.before_hash) throw new Error("Reverted content did not match the original hash");
300
353
  db.prepare("UPDATE installed_agents SET system_prompt = ? WHERE id = ?").run(row.before_content, row.agent_id);
@@ -309,7 +362,7 @@ function cmdRevert(db, id, out, fail, agentFolder) {
309
362
  });
310
363
  } catch (error) {
311
364
  if (wroteTarget) {
312
- try { restoreTargetAfterFailure(file, before, sha256(row.before_content)); }
365
+ try { restoreTargetAfterFailure(file, before, row.before_hash); }
313
366
  catch (rollbackError) { return fail(`${error.message}. ${rollbackError.message}`); }
314
367
  }
315
368
  return fail(`${error.message}; the applied target was restored.`);
@@ -326,19 +379,23 @@ function cmdEvolve(ctx) {
326
379
  const args = Array.isArray(ctx.args) ? ctx.args : [];
327
380
  const sub = args[0] || "list";
328
381
  if (sub === "help" || sub === "--help" || sub === "-h") {
382
+ if (args.length !== 1) return fail("usage: agentlas evolve [list | apply <id> | revert <id>]");
329
383
  out("usage: agentlas evolve [list | apply <id> | revert <id>]");
330
384
  out(" list — review pending (approval-needed) and auto-applied agent growth proposals");
331
385
  out(" apply <id> — approve and apply a pending proposal to the agent prompt");
332
386
  out(" revert <id> — roll back an applied proposal");
333
387
  return;
334
388
  }
335
- if (sub === "list" || sub === "ls") return cmdList(db, out);
389
+ if (sub === "list" || sub === "ls") {
390
+ if (args.length > 1) return fail("usage: agentlas evolve list");
391
+ return cmdList(db, out);
392
+ }
336
393
  if (sub === "apply") {
337
- if (!args[1]) return fail("usage: agentlas evolve apply <id>");
394
+ if (args.length !== 2 || !args[1]) return fail("usage: agentlas evolve apply <id>");
338
395
  return cmdApply(db, args[1], out, fail, agentFolder);
339
396
  }
340
397
  if (sub === "revert" || sub === "rollback") {
341
- if (!args[1]) return fail("usage: agentlas evolve revert <id>");
398
+ if (args.length !== 2 || !args[1]) return fail("usage: agentlas evolve revert <id>");
342
399
  return cmdRevert(db, args[1], out, fail, agentFolder);
343
400
  }
344
401
  return fail(`Unknown evolve subcommand: ${sub} (list|apply|revert)`);
@@ -12,6 +12,7 @@
12
12
  const crypto = require("node:crypto");
13
13
  const fs = require("node:fs");
14
14
  const path = require("node:path");
15
+ const { TextDecoder } = require("node:util");
15
16
 
16
17
  const BUNDLE_SCHEMA = "agentlas.experience-bundle.v1";
17
18
  const RECEIPT_SCHEMA = "agentlas.experience-upload-receipt.v1";
@@ -604,13 +605,94 @@ function validateExperienceBundle(payload) {
604
605
  return { bundle: value, canonicalJson: canonicalJson(value), canonicalBytes, expectedPackHash, expectedBundleHash, expectedBundleId: experienceBundleId(value) };
605
606
  }
606
607
 
608
+ function isBoundedSingleLinkRegular(stat, maxBytes, minBytes = 0) {
609
+ return Boolean(
610
+ stat && stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 &&
611
+ Number.isSafeInteger(stat.size) && stat.size >= minBytes && stat.size <= maxBytes,
612
+ );
613
+ }
614
+
615
+ function sameBoundedFileSnapshot(left, right) {
616
+ return Boolean(
617
+ left && right && left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink &&
618
+ left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs,
619
+ );
620
+ }
621
+
622
+ function readBoundedTextFile(filePath, maxBytes, options = {}) {
623
+ const {
624
+ label = "private file",
625
+ minBytes = 0,
626
+ allowMissing = false,
627
+ unsafeMessage = `${label} is unsafe`,
628
+ sizeMessage = `${label} exceeds ${maxBytes} bytes`,
629
+ symlinkMessage = unsafeMessage,
630
+ } = options;
631
+ let before;
632
+ try {
633
+ before = fs.lstatSync(filePath);
634
+ } catch (error) {
635
+ if (allowMissing && error?.code === "ENOENT") return null;
636
+ throw error;
637
+ }
638
+ if (before.isSymbolicLink()) throw new Error(symlinkMessage);
639
+ if (!before.isFile() || before.nlink !== 1) throw new Error(unsafeMessage);
640
+ if (!Number.isSafeInteger(before.size) || before.size < minBytes || before.size > maxBytes) throw new Error(sizeMessage);
641
+
642
+ const noFollow = fs.constants.O_NOFOLLOW;
643
+ if (!Number.isInteger(noFollow)) throw new Error(`${label} cannot enforce symlink-safe open`);
644
+ const nonBlock = Number.isInteger(fs.constants.O_NONBLOCK) ? fs.constants.O_NONBLOCK : 0;
645
+ const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow | nonBlock);
646
+ try {
647
+ const opened = fs.fstatSync(descriptor);
648
+ if (!isBoundedSingleLinkRegular(opened, maxBytes, minBytes) || !sameBoundedFileSnapshot(before, opened)) {
649
+ throw new Error(`${label} changed while opening`);
650
+ }
651
+
652
+ const chunks = [];
653
+ let total = 0;
654
+ while (total <= maxBytes) {
655
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
656
+ const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
657
+ if (count === 0) break;
658
+ chunks.push(buffer.subarray(0, count));
659
+ total += count;
660
+ }
661
+ if (total > maxBytes) throw new Error(sizeMessage);
662
+
663
+ const after = fs.fstatSync(descriptor);
664
+ const pathAfter = fs.lstatSync(filePath);
665
+ if (
666
+ !isBoundedSingleLinkRegular(after, maxBytes, minBytes) ||
667
+ !isBoundedSingleLinkRegular(pathAfter, maxBytes, minBytes) ||
668
+ !sameBoundedFileSnapshot(opened, after) ||
669
+ !sameBoundedFileSnapshot(before, pathAfter) ||
670
+ !sameBoundedFileSnapshot(opened, pathAfter) ||
671
+ total !== after.size
672
+ ) {
673
+ throw new Error(`${label} changed while reading`);
674
+ }
675
+
676
+ try {
677
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
678
+ } catch (error) {
679
+ throw new Error(`${label} is invalid UTF-8: ${error.message}`);
680
+ }
681
+ } finally {
682
+ fs.closeSync(descriptor);
683
+ }
684
+ }
685
+
607
686
  function readBundleFile(filePath, cwd = process.cwd()) {
608
687
  const absolute = path.resolve(cwd, filePath);
609
- const stat = fs.lstatSync(absolute);
610
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Experience bundle must be a regular file; symlinks are forbidden");
611
- if (stat.size < 1 || stat.size > MAX_BUNDLE_FILE_BYTES) throw new Error(`Experience bundle file must be 1..${MAX_BUNDLE_FILE_BYTES} bytes`);
688
+ const text = readBoundedTextFile(absolute, MAX_BUNDLE_FILE_BYTES, {
689
+ label: "Experience bundle",
690
+ minBytes: 1,
691
+ unsafeMessage: "Experience bundle must be a regular file; symlinks are forbidden",
692
+ sizeMessage: `Experience bundle file must be 1..${MAX_BUNDLE_FILE_BYTES} bytes`,
693
+ });
612
694
  let payload;
613
- try { payload = JSON.parse(fs.readFileSync(absolute, "utf8")); } catch (error) { throw new Error(`Experience bundle is invalid JSON: ${error.message}`); }
695
+ try { payload = JSON.parse(text); } catch (error) { throw new Error(`Experience bundle is invalid JSON: ${error.message}`); }
614
696
  return { absolute, ...validateExperienceBundle(payload) };
615
697
  }
616
698
 
@@ -776,10 +858,14 @@ function withExchangeStateLock(userDataDir, action) {
776
858
  function loadExchangeState(userDataDir) {
777
859
  const file = exchangeStatePath(userDataDir);
778
860
  recoverPrivateAtomicTarget(file);
779
- if (!fs.existsSync(file)) return emptyState();
780
- const stat = fs.lstatSync(file);
781
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_STATE_BYTES) throw new Error("Terminal Experience exchange state is unsafe or too large");
782
- const state = JSON.parse(fs.readFileSync(file, "utf8"));
861
+ const text = readBoundedTextFile(file, MAX_STATE_BYTES, {
862
+ label: "Terminal Experience exchange state",
863
+ allowMissing: true,
864
+ unsafeMessage: "Terminal Experience exchange state is unsafe or too large",
865
+ sizeMessage: "Terminal Experience exchange state is unsafe or too large",
866
+ });
867
+ if (text === null) return emptyState();
868
+ const state = JSON.parse(text);
783
869
  const allowed = new Set(["schemaVersion", "updatedAt", "bundles"]);
784
870
  const issues = [];
785
871
  strictObject(state, allowed, allowed, "exchange state", issues);
@@ -896,20 +982,51 @@ function resolveBundleInput(userDataDir, sourceOrRef, cwd) {
896
982
  return readBundleFile(sourceOrRef, cwd);
897
983
  }
898
984
 
899
- function parseFlags(args) {
985
+ function parseFlags(args, schema = {}) {
900
986
  const flags = { _: [] };
987
+ const values = new Set(schema.values || []);
988
+ const booleans = new Set(schema.booleans || []);
989
+ let passthrough = false;
901
990
  for (let index = 0; index < args.length; index += 1) {
902
991
  const token = String(args[index]);
992
+ if (passthrough) { flags._.push(token); continue; }
993
+ if (token === "--") { passthrough = true; continue; }
903
994
  if (!token.startsWith("--")) { flags._.push(token); continue; }
904
995
  const equal = token.indexOf("=");
905
- if (equal > 2) { flags[token.slice(2, equal)] = token.slice(equal + 1); continue; }
906
- const key = token.slice(2);
907
- if (index + 1 < args.length && !String(args[index + 1]).startsWith("--")) flags[key] = String(args[++index]);
908
- else flags[key] = true;
996
+ const key = token.slice(2, equal > 2 ? equal : undefined);
997
+ if (!values.has(key) && !booleans.has(key)) throw new Error(`unknown option: --${key}`);
998
+ if (Object.prototype.hasOwnProperty.call(flags, key)) throw new Error(`duplicate option: --${key}`);
999
+ if (booleans.has(key)) {
1000
+ if (equal > 2) throw new Error(`--${key} does not take a value`);
1001
+ flags[key] = true;
1002
+ continue;
1003
+ }
1004
+ const value = equal > 2 ? token.slice(equal + 1) : args[++index];
1005
+ if (value === undefined || value === "" || (equal <= 2 && String(value).startsWith("--"))) {
1006
+ throw new Error(`--${key} requires a value`);
1007
+ }
1008
+ flags[key] = String(value);
909
1009
  }
910
1010
  return flags;
911
1011
  }
912
1012
 
1013
+ const EXPERIENCE_COMMAND_FLAG_SCHEMA = Object.freeze({
1014
+ list: { booleans: ["json"] },
1015
+ inspect: { booleans: ["json"] },
1016
+ validate: { booleans: ["json"] },
1017
+ save: {
1018
+ booleans: ["json", "dry-run", "local-only"],
1019
+ values: ["idempotency-key", "base-slug", "base-cloud-id", "base-package-hash", "base-package-hash-version"],
1020
+ },
1021
+ publish: {
1022
+ booleans: ["json", "dry-run"],
1023
+ values: ["visibility", "idempotency-key", "base-slug", "base-cloud-id", "base-package-hash", "base-package-hash-version"],
1024
+ },
1025
+ status: { booleans: ["json"] },
1026
+ export: { booleans: ["json", "overwrite"], values: ["out"] },
1027
+ unpublish: { booleans: ["json", "dry-run"] },
1028
+ });
1029
+
913
1030
  function idempotencyKeyForBundle(bundle, explicit, operation = "save") {
914
1031
  const defaultDigest = crypto.createHash("sha256")
915
1032
  .update(canonicalJson({ bundleHash: bundle.bundleHash, operation, requestedVisibility: bundle.requestedVisibility }), "utf8")
@@ -1935,7 +2052,6 @@ function baseDescriptorFromFlags(flags) {
1935
2052
  async function cmdExperienceExchange(options = {}) {
1936
2053
  const args = options.args || [];
1937
2054
  const sub = args[0] || "list";
1938
- const flags = parseFlags(args.slice(1));
1939
2055
  const emit = options.out || console.log;
1940
2056
  if (!options.userDataDir) throw new Error("Terminal userData path is required");
1941
2057
 
@@ -1963,6 +2079,15 @@ async function cmdExperienceExchange(options = {}) {
1963
2079
  const mapped = sub.slice("legacy-".length);
1964
2080
  return options.legacyCommand({ ...options, args: [mapped, ...args.slice(1)] });
1965
2081
  }
2082
+ const canonicalSub = sub === "ls" ? "list"
2083
+ : sub === "show" ? "inspect"
2084
+ : sub === "withdraw" ? "unpublish"
2085
+ : sub;
2086
+ const schema = EXPERIENCE_COMMAND_FLAG_SCHEMA[canonicalSub];
2087
+ if (!schema) {
2088
+ throw new Error("unknown experience subcommand (list|inspect|validate|save|publish|status|export|unpublish|withdraw; legacy: legacy-list|legacy-inspect|legacy-publish|legacy-unpublish)");
2089
+ }
2090
+ const flags = parseFlags(args.slice(1), schema);
1966
2091
  if (sub === "list" || sub === "ls") {
1967
2092
  if (flags._.length) throw new Error("usage: agentlas experience list [--json]");
1968
2093
  const bundles = listStoredExperienceBundles(options.userDataDir, options.cwd);
@@ -1998,12 +2123,14 @@ async function cmdExperienceExchange(options = {}) {
1998
2123
  return result;
1999
2124
  }
2000
2125
  if (sub === "validate") {
2126
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience validate <bundle.agentlas-experience.json> [--json]");
2001
2127
  const validation = readBundleFile(flags._[0], options.cwd);
2002
2128
  const result = { valid: true, bundleId: validation.bundle.bundleId, bundleHash: validation.bundle.bundleHash, packContentHash: validation.bundle.pack.contentHash, items: validation.bundle.items.length, canonicalBytes: validation.canonicalBytes, networkUsed: false, authority: "local-validation" };
2003
2129
  emit(flags.json ? JSON.stringify(result, null, 2) : renderValidation(validation));
2004
2130
  return result;
2005
2131
  }
2006
2132
  if (sub === "save") {
2133
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience save <bundle> [options]");
2007
2134
  const validation = readBundleFile(flags._[0], options.cwd);
2008
2135
  if (flags["local-only"] === true) {
2009
2136
  if (flags["dry-run"] === true) {
@@ -2027,6 +2154,7 @@ async function cmdExperienceExchange(options = {}) {
2027
2154
  return result;
2028
2155
  }
2029
2156
  if (sub === "publish") {
2157
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience publish <bundle> [options]");
2030
2158
  const source = flags._[0];
2031
2159
  const validation = resolveBundleInput(options.userDataDir, source, options.cwd);
2032
2160
  const result = await publishBundle(validation, {
@@ -2041,11 +2169,13 @@ async function cmdExperienceExchange(options = {}) {
2041
2169
  return result;
2042
2170
  }
2043
2171
  if (sub === "status") {
2172
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience status <bundle-id|upload-id> [--json]");
2044
2173
  const result = await fetchUploadStatus(flags._[0], options);
2045
2174
  emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : `Server-authoritative status: ${result.receipt.status} · ${result.receipt.uploadId}\nrequested visibility: ${result.receipt.requestedVisibility} · Terminal did not assert public activation/evaluator reputation`);
2046
2175
  return result;
2047
2176
  }
2048
2177
  if (sub === "export") {
2178
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience export <bundle-id|upload-id> [--out file] [--overwrite] [--json]");
2049
2179
  const result = await fetchUploadExport(flags._[0], {
2050
2180
  ...options,
2051
2181
  outputPath: typeof flags.out === "string" ? flags.out : null,
@@ -2111,6 +2241,7 @@ module.exports = {
2111
2241
  previewWithdrawUpload,
2112
2242
  publicUploadReceipt,
2113
2243
  publicCommandExchangeResult,
2244
+ parseFlags,
2114
2245
  projectScopeHash,
2115
2246
  idempotencyKeyForBundle,
2116
2247
  idempotencyKeyHash,
@@ -75,9 +75,9 @@ function createRunReceipt(input) {
75
75
  if (!input.exactBase || !ID_RE.test(String(input.exactBase.agentReleaseId || ""))) {
76
76
  throw new Error("RunReceipt requires an exact agent definition release.");
77
77
  }
78
- const runId = cleanId(input.runId, opaqueId("run", input.exactBase.agentReleaseId, taskSignature, input.createdAt));
79
- const idempotencyKey = cleanId(input.idempotencyKey, opaqueId("run-key", runId));
80
78
  const createdAt = input.createdAt || new Date().toISOString();
79
+ const runId = cleanId(input.runId, opaqueId("run", input.exactBase.agentReleaseId, taskSignature, createdAt));
80
+ const idempotencyKey = cleanId(input.idempotencyKey, opaqueId("run-key", runId));
81
81
  const provider = cleanId(input.model?.provider, "terminal-runtime");
82
82
  const modelId = cleanId(input.model?.modelId, provider);
83
83
  const outcome = ["succeeded", "partial", "failed", "cancelled"].includes(input.outcome?.status)
@@ -174,7 +174,6 @@ const STRINGS = {
174
174
  "help.setup": "run language, runtime, and permission setup again",
175
175
  "help.cwd": "show or change the working folder",
176
176
  "help.memory": "show the memory being injected",
177
- "help.careerGraph": "show/add source refs for the Career Graph routing index",
178
177
  "help.ontology": "turn on/list/add project ontology sources with short natural commands",
179
178
  "help.side": "ask a side question without saving it to project task context",
180
179
  "help.status": "show model/runtime, agent, permission, and directory",
@@ -422,7 +421,6 @@ const STRINGS = {
422
421
  "help.setup": "언어·런타임·권한 설정 다시 실행",
423
422
  "help.cwd": "작업 폴더 보기/변경",
424
423
  "help.memory": "주입되는 메모리 보기",
425
- "help.careerGraph": "경력 그래프 라우팅 색인의 원본 경로 보기/등록",
426
424
  "help.ontology": "짧은 자연어로 프로젝트 온톨로지 켜기/목록/자료 등록",
427
425
  "help.side": "프로젝트 작업 맥락에 저장하지 않는 사이드 질문",
428
426
  "help.status": "모델/런타임, 에이전트, 권한, 작업 폴더 보기",