agentlas 1.0.64 → 1.0.66

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 (164) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +8 -6
  3. package/bin/agentlas.cjs +341 -16
  4. package/engine/acp/server.cjs +27 -1
  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 +29 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +10 -3
  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/bootstrap-schema.sql +4 -4
  43. package/engine/browser/cdp.cjs +315 -32
  44. package/engine/browser/vault.cjs +1 -0
  45. package/engine/cli-output.cjs +80 -28
  46. package/engine/cloud/auth.cjs +131 -13
  47. package/engine/cloud/hub-client.cjs +41 -16
  48. package/engine/cloud-assets/cas.cjs +18 -9
  49. package/engine/cloud-assets/commands.cjs +108 -30
  50. package/engine/cloud-assets/package.cjs +66 -3
  51. package/engine/cloud-assets/restore.cjs +12 -9
  52. package/engine/cloud-assets/state.cjs +308 -113
  53. package/engine/commands/acp.cjs +13 -2
  54. package/engine/commands/automation.cjs +75 -24
  55. package/engine/commands/billing.cjs +6 -1
  56. package/engine/commands/browser.cjs +67 -34
  57. package/engine/commands/build.cjs +42 -12
  58. package/engine/commands/call.cjs +1 -1
  59. package/engine/commands/cd.cjs +1 -1
  60. package/engine/commands/cloud.cjs +2 -1
  61. package/engine/commands/connect.cjs +38 -13
  62. package/engine/commands/creds.cjs +74 -29
  63. package/engine/commands/doctor.cjs +17 -1
  64. package/engine/commands/document.cjs +40 -12
  65. package/engine/commands/env.cjs +6 -1
  66. package/engine/commands/firm.cjs +42 -12
  67. package/engine/commands/graph.cjs +36 -32
  68. package/engine/commands/help.cjs +11 -1
  69. package/engine/commands/hep.cjs +1 -1
  70. package/engine/commands/import.cjs +5 -4
  71. package/engine/commands/index.cjs +1 -1
  72. package/engine/commands/install.cjs +6 -4
  73. package/engine/commands/list.cjs +56 -16
  74. package/engine/commands/login.cjs +5 -0
  75. package/engine/commands/logout.cjs +6 -1
  76. package/engine/commands/mcp.cjs +8 -2
  77. package/engine/commands/memory.cjs +3 -2
  78. package/engine/commands/multimodal.cjs +12 -0
  79. package/engine/commands/native.cjs +5 -1
  80. package/engine/commands/one.cjs +78 -33
  81. package/engine/commands/plugin.cjs +13 -2
  82. package/engine/commands/project.cjs +15 -12
  83. package/engine/commands/research.cjs +18 -7
  84. package/engine/commands/roles.cjs +104 -28
  85. package/engine/commands/route.cjs +2 -2
  86. package/engine/commands/run.cjs +42 -9
  87. package/engine/commands/search.cjs +23 -2
  88. package/engine/commands/setup.cjs +6 -1
  89. package/engine/commands/storm.cjs +44 -6
  90. package/engine/commands/swarm.cjs +47 -9
  91. package/engine/commands/telegram.cjs +6 -1
  92. package/engine/commands/uninstall.cjs +12 -6
  93. package/engine/commands/update.cjs +5 -0
  94. package/engine/commands/upload.cjs +2 -1
  95. package/engine/commands/usage.cjs +6 -1
  96. package/engine/commands/version.cjs +6 -1
  97. package/engine/commands/whoami.cjs +6 -1
  98. package/engine/commands/workforce.cjs +60 -16
  99. package/engine/core/capability-grants.cjs +7 -2
  100. package/engine/core/daemon-client.cjs +36 -7
  101. package/engine/core/db.cjs +10 -2
  102. package/engine/core/desktop-core-fetch.cjs +477 -77
  103. package/engine/core/schema-ensure.cjs +23 -11
  104. package/engine/experience/build.cjs +5 -1
  105. package/engine/experience/intents.cjs +29 -82
  106. package/engine/experience/runtime.cjs +7 -1
  107. package/engine/experience/variant.cjs +58 -4
  108. package/engine/firms/orchestrate.cjs +9 -6
  109. package/engine/graph/ask-model.cjs +22 -11
  110. package/engine/graph/interview.cjs +74 -23
  111. package/engine/graph/package.cjs +63 -8
  112. package/engine/hephaestus/local-core.cjs +18 -5
  113. package/engine/hephaestus/runtime.cjs +48 -43
  114. package/engine/hub/install.cjs +230 -61
  115. package/engine/hub/plugins.cjs +391 -29
  116. package/engine/mcp/consent.cjs +25 -17
  117. package/engine/mcp/contract.cjs +229 -24
  118. package/engine/mcp/inventory.cjs +15 -5
  119. package/engine/mcp/plan.cjs +5 -1
  120. package/engine/mcp/probe.cjs +24 -9
  121. package/engine/memory-cli/curate.cjs +74 -16
  122. package/engine/oberon/common.cjs +37 -12
  123. package/engine/oberon/manifest.cjs +25 -2
  124. package/engine/oberon/outputs.cjs +49 -19
  125. package/engine/oberon/render.cjs +665 -40
  126. package/engine/project/career-graph.cjs +15 -2
  127. package/engine/project/controller.cjs +133 -15
  128. package/engine/project/credentials.cjs +212 -34
  129. package/engine/project/env-file.cjs +93 -8
  130. package/engine/project/memory-context.cjs +11 -57
  131. package/engine/project/ontology.cjs +66 -25
  132. package/engine/project/paths.cjs +42 -8
  133. package/engine/project/seed.cjs +88 -51
  134. package/engine/project/state.cjs +41 -16
  135. package/engine/project/team.cjs +26 -1
  136. package/engine/runtime-refusal.cjs +4 -1
  137. package/engine/runtimes/acp-driver.cjs +56 -9
  138. package/engine/runtimes/auth-evidence.cjs +21 -2
  139. package/engine/runtimes/detect.cjs +28 -8
  140. package/engine/runtimes/kinds.cjs +12 -4
  141. package/engine/runtimes/overrides.cjs +4 -3
  142. package/engine/runtimes/resolve.cjs +8 -7
  143. package/engine/runtimes/roles.cjs +7 -5
  144. package/engine/sessions/apply-fences.cjs +396 -56
  145. package/engine/sessions/fences.cjs +111 -27
  146. package/engine/sessions/memory-turn.cjs +62 -3
  147. package/engine/sessions/orchestrator.cjs +11 -1
  148. package/engine/sessions/prompt.cjs +66 -9
  149. package/engine/sessions/session.cjs +70 -10
  150. package/engine/sessions/sink.cjs +78 -12
  151. package/engine/sessions/store.cjs +29 -7
  152. package/engine/storm/storm.cjs +26 -3
  153. package/engine/storm/swarm.cjs +31 -3
  154. package/engine/telegram/connect.cjs +187 -24
  155. package/engine/ui/commands-catalog.cjs +1 -1
  156. package/engine/ui/palette.cjs +12 -2
  157. package/engine/ui/repl.cjs +57 -9
  158. package/engine/ui/screens.cjs +2 -4
  159. package/engine/vendor/desktop-core.manifest.json +5 -5
  160. package/engine/workforce/capture.cjs +9 -27
  161. package/engine/workforce/deps.cjs +18 -6
  162. package/engine/workforce/local-core-transport.cjs +68 -5
  163. package/package.json +1 -1
  164. package/engine/commands/career-graph.cjs +0 -51
@@ -23,6 +23,95 @@ const { userDataDir } = require("../core/paths.cjs");
23
23
  const LOGIN_CALLBACK_PATH = "/callback";
24
24
  const LOGIN_TIMEOUT_MS = 180_000;
25
25
  const MAX_LOGIN_SESSION_BYTES = 16 * 1024;
26
+ const MAX_LOGIN_SESSION_FILE_BYTES = MAX_LOGIN_SESSION_BYTES * 2;
27
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
28
+
29
+ function sameFileIdentity(left, right) {
30
+ return !!left && !!right
31
+ && left.dev === right.dev
32
+ && left.ino === right.ino
33
+ && left.nlink === right.nlink;
34
+ }
35
+
36
+ function sameDirectoryIdentity(info) {
37
+ try {
38
+ const current = fs.lstatSync(info.path);
39
+ return current.isDirectory()
40
+ && !current.isSymbolicLink()
41
+ && current.dev === info.stat.dev
42
+ && current.ino === info.stat.ino;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function ensureRealDirectory(directory, { create = false } = {}) {
49
+ const resolved = path.resolve(String(directory || ""));
50
+ if (create) fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
51
+ const stat = fs.lstatSync(resolved);
52
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
53
+ throw new Error("Refusing to use a symbolic-link Agentlas session directory.");
54
+ }
55
+ return { path: resolved, stat, realpath: fs.realpathSync.native(resolved) };
56
+ }
57
+
58
+ function ensureAuthDirectory({ create = false } = {}) {
59
+ const root = ensureRealDirectory(userDataDir(), { create });
60
+ const info = ensureRealDirectory(path.join(root.path, "auth"), { create });
61
+ // The auth directory must be a child of the configured user-data directory,
62
+ // not a mounted/symlinked redirect that merely passes the final lstat.
63
+ if (path.dirname(info.realpath) !== root.realpath) {
64
+ throw new Error("Refusing to use a redirected Agentlas session directory.");
65
+ }
66
+ return info;
67
+ }
68
+
69
+ function readBoundedFd(fd, maxBytes) {
70
+ const chunks = [];
71
+ let total = 0;
72
+ while (total <= maxBytes) {
73
+ const want = Math.min(8192, maxBytes - total + 1);
74
+ const buffer = Buffer.allocUnsafe(want);
75
+ const count = fs.readSync(fd, buffer, 0, want, null);
76
+ if (!count) break;
77
+ total += count;
78
+ if (total > maxBytes) return null;
79
+ chunks.push(buffer.subarray(0, count));
80
+ }
81
+ return Buffer.concat(chunks, total);
82
+ }
83
+
84
+ function readPrivateBoundedFile(filePath, maxBytes) {
85
+ let before;
86
+ let fd = null;
87
+ try {
88
+ before = fs.lstatSync(filePath);
89
+ if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size > maxBytes) return null;
90
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);
91
+ const opened = fs.fstatSync(fd);
92
+ if (!opened.isFile() || opened.nlink !== 1
93
+ || opened.size > maxBytes || !sameFileIdentity(before, opened)) return null;
94
+ const contents = readBoundedFd(fd, maxBytes);
95
+ const after = fs.fstatSync(fd);
96
+ if (
97
+ contents === null || !after.isFile() || after.nlink !== 1 || !sameFileIdentity(opened, after) ||
98
+ after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
99
+ after.size !== contents.length
100
+ ) return null;
101
+ return contents.toString("utf8");
102
+ } catch {
103
+ return null;
104
+ } finally {
105
+ if (fd !== null) {
106
+ try { fs.closeSync(fd); } catch { /* ignore */ }
107
+ }
108
+ }
109
+ }
110
+
111
+ function writeAll(fd, buffer) {
112
+ let offset = 0;
113
+ while (offset < buffer.length) offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
114
+ }
26
115
 
27
116
  function validLoginSessionValue(value) {
28
117
  return typeof value === "string" && value.length > 0 &&
@@ -230,9 +319,11 @@ function cliSessionPath() {
230
319
  function readCliSessionValue() {
231
320
  try {
232
321
  const p = cliSessionPath();
233
- const stat = fs.lstatSync(p);
234
- if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_LOGIN_SESSION_BYTES * 2) return null;
235
- const j = JSON.parse(fs.readFileSync(p, "utf8"));
322
+ const authDir = ensureAuthDirectory();
323
+ const raw = readPrivateBoundedFile(p, MAX_LOGIN_SESSION_FILE_BYTES);
324
+ if (raw === null) return null;
325
+ if (!sameDirectoryIdentity(authDir)) return null;
326
+ const j = JSON.parse(raw);
236
327
  return j && validLoginSessionValue(j.value) ? j.value : null;
237
328
  } catch {
238
329
  return null;
@@ -243,29 +334,56 @@ function saveCliSession(value) {
243
334
  if (!validLoginSessionValue(value)) throw new Error("Refusing to save an invalid Agentlas login session value.");
244
335
  const p = cliSessionPath();
245
336
  // 디렉터리 0700 + 파일 0600 — 기본 umask(0644)로 세션이 world-readable 이 되는 것을 막는다.
246
- const dir = path.dirname(p);
247
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
337
+ const authDir = ensureAuthDirectory({ create: true });
338
+ const dir = authDir.path;
248
339
  try { fs.chmodSync(dir, 0o700); } catch { /* win32 */ }
249
340
  const temp = path.join(dir, `.cli-session.${process.pid}.${crypto.randomUUID()}.tmp`);
341
+ const payload = Buffer.from(JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", "utf8");
342
+ if (payload.length > MAX_LOGIN_SESSION_FILE_BYTES) throw new Error("Refusing to save an oversized Agentlas login session.");
343
+ let fd = null;
250
344
  try {
251
- fs.writeFileSync(temp, JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", {
252
- encoding: "utf8",
253
- mode: 0o600,
254
- flag: "wx",
255
- });
345
+ fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
346
+ const created = fs.fstatSync(fd);
347
+ if (!created.isFile() || created.nlink !== 1) throw new Error("Refusing to create an unsafe Agentlas login session file.");
348
+ writeAll(fd, payload);
349
+ fs.fsyncSync(fd);
350
+ const written = fs.fstatSync(fd);
351
+ if (!sameFileIdentity(created, written) || written.size !== payload.length) throw new Error("Agentlas login session changed while saving.");
352
+ fs.closeSync(fd);
353
+ fd = null;
354
+ if (!sameDirectoryIdentity(authDir)) throw new Error("Agentlas login session directory changed while saving.");
256
355
  fs.renameSync(temp, p);
356
+ const installed = fs.lstatSync(p);
357
+ if (installed.isSymbolicLink() || !installed.isFile() || installed.nlink !== 1) throw new Error("Refusing to install an unsafe Agentlas login session file.");
257
358
  } catch (error) {
359
+ if (fd !== null) {
360
+ try { fs.closeSync(fd); } catch { /* preserve original */ }
361
+ }
258
362
  try { fs.rmSync(temp, { force: true }); } catch { /* preserve original */ }
259
363
  throw error;
260
364
  }
261
- try { fs.chmodSync(p, 0o600); } catch { /* win32 */ }
262
365
  return p;
263
366
  }
264
367
 
265
368
  function deleteCliSession() {
266
369
  const p = cliSessionPath();
267
- if (!fs.existsSync(p)) return { existed: false, path: p };
268
- fs.rmSync(p);
370
+ let authDir;
371
+ try { authDir = ensureAuthDirectory(); } catch (error) {
372
+ if (error?.code === "ENOENT") return { existed: false, path: p };
373
+ throw error;
374
+ }
375
+ let stat;
376
+ try { stat = fs.lstatSync(p); } catch (error) {
377
+ if (error?.code === "ENOENT") return { existed: false, path: p };
378
+ throw error;
379
+ }
380
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
381
+ throw new Error("Refusing to delete an unsafe Agentlas login session file.");
382
+ }
383
+ if (!sameDirectoryIdentity(authDir)) throw new Error("Agentlas login session directory changed while deleting.");
384
+ const current = fs.lstatSync(p);
385
+ if (!sameFileIdentity(stat, current)) throw new Error("Agentlas login session changed while deleting.");
386
+ fs.unlinkSync(p);
269
387
  return { existed: true, path: p };
270
388
  }
271
389
 
@@ -8,9 +8,9 @@
8
8
  * - 업스트림 AbortSignal 전파
9
9
  * 세션 쿠키 해석 순서: AGENTLAS_SESSION env → auth/cli-session.v1.json → (레거시) keytar.
10
10
  */
11
- const fs = require("node:fs");
12
11
  const path = require("node:path");
13
12
  const { userDataDir } = require("../core/paths.cjs");
13
+ const { readJsonFile } = require("../mcp/contract.cjs");
14
14
 
15
15
  const HUB_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
16
16
  const HUB_TIMEOUT_DEFAULTS = Object.freeze({ connectMs: 15_000, idleMs: 30_000, totalMs: 180_000 });
@@ -69,6 +69,7 @@ async function fetchHub(url, init = {}, options = {}) {
69
69
  let idleTimer = null;
70
70
  let totalTimer = null;
71
71
  let reader = null;
72
+ let iterator = null;
72
73
  let terminalError = null;
73
74
  let rejectTerminal;
74
75
  const terminal = new Promise((_, reject) => { rejectTerminal = reject; });
@@ -106,27 +107,46 @@ async function fetchHub(url, init = {}, options = {}) {
106
107
  const chunks = [];
107
108
  let bytes = 0;
108
109
  armIdle();
110
+ const acceptChunk = (value) => {
111
+ armIdle();
112
+ const chunk = Buffer.from(value || []);
113
+ bytes += chunk.length;
114
+ if (bytes > HUB_RESPONSE_MAX_BYTES) {
115
+ const error = new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
116
+ error.code = "AGENTLAS_HUB_RESPONSE_TOO_LARGE";
117
+ stop(error);
118
+ throw error;
119
+ }
120
+ chunks.push(chunk);
121
+ };
109
122
  if (response.body && typeof response.body.getReader === "function") {
110
123
  reader = response.body.getReader();
111
124
  while (true) {
112
125
  const part = await Promise.race([reader.read(), terminal]);
113
126
  if (part.done) break;
114
- armIdle();
115
- const chunk = Buffer.from(part.value || []);
116
- bytes += chunk.length;
117
- if (bytes > HUB_RESPONSE_MAX_BYTES) {
118
- const error = new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
119
- error.code = "AGENTLAS_HUB_RESPONSE_TOO_LARGE";
120
- stop(error);
121
- throw error;
122
- }
123
- chunks.push(chunk);
127
+ acceptChunk(part.value);
124
128
  }
129
+ } else if (response.body && typeof response.body[Symbol.asyncIterator] === "function") {
130
+ iterator = response.body[Symbol.asyncIterator]();
131
+ while (true) {
132
+ // Node streams and test adapters can expose only an async iterator. A
133
+ // plain `for await` leaves us stuck inside next() after idle/total expiry,
134
+ // even though the request's AbortController has already fired.
135
+ const part = await Promise.race([
136
+ Promise.resolve().then(() => iterator.next()),
137
+ terminal,
138
+ ]);
139
+ if (part.done) break;
140
+ acceptChunk(part.value);
141
+ }
142
+ } else if (response.body == null) {
143
+ // Standards-compliant empty/HEAD/204 response. There is no body to read.
125
144
  } else {
126
- const raw = Buffer.from(await Promise.race([response.arrayBuffer(), terminal]));
127
- bytes = raw.length;
128
- if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub response exceeds the allowed size (${HUB_RESPONSE_MAX_BYTES} bytes).`);
129
- chunks.push(raw);
145
+ // arrayBuffer()/text() allocates the complete attacker-controlled body
146
+ // before a size check can run, defeating the 16 MiB contract.
147
+ const error = new Error("Hub response body is not stream-readable; refusing an unbounded buffered read.");
148
+ error.code = "AGENTLAS_HUB_RESPONSE_NOT_STREAMABLE";
149
+ throw error;
130
150
  }
131
151
  if (idleTimer) clearTimeout(idleTimer);
132
152
  idleTimer = null;
@@ -143,6 +163,11 @@ async function fetchHub(url, init = {}, options = {}) {
143
163
  if (reader && terminalError) {
144
164
  try { await reader.cancel(terminalError); } catch { /* ignore */ }
145
165
  }
166
+ if (iterator && terminalError && typeof iterator.return === "function") {
167
+ // Do not await an untrusted iterator's cleanup hook: the timeout must
168
+ // remain a hard upper bound even when return() itself never settles.
169
+ Promise.resolve().then(() => iterator.return()).catch(() => {});
170
+ }
146
171
  }
147
172
  }
148
173
 
@@ -161,7 +186,7 @@ function cliSessionPath() {
161
186
 
162
187
  function readCliSessionValue() {
163
188
  try {
164
- const j = JSON.parse(fs.readFileSync(cliSessionPath(), "utf8"));
189
+ const { value: j } = readJsonFile(cliSessionPath(), "Agentlas CLI session");
165
190
  return (j && typeof j.value === "string" && j.value) || null;
166
191
  } catch {
167
192
  return null;
@@ -224,18 +224,27 @@ async function deleteCloudAgent(slug, options = {}) {
224
224
  ) {
225
225
  throw new Error("Agentlas Cloud delete returned an invalid or mismatched deletion receipt.");
226
226
  }
227
- const stateValue = state.readCloudAssetState();
228
227
  const key = state.cloudDescriptorKey(descriptor);
229
- const roots = stateValue.assets[key]?.sourceRoots || [];
228
+ // The server delete used this exact observed descriptor. Keep its roots even
229
+ // if another local process advances the same key before the local journal
230
+ // update; tombstones authenticate the deleted revision, not a later one.
231
+ const roots = [...(localEntry.sourceRoots || [])];
230
232
  const warnings = [];
231
- for (const rootPath of roots) {
232
- // 톰스톤: 삭제된 베이스는 재저장 시 절대 If-Match 베이스로 재사용되지 않는다.
233
- stateValue.deletedBases.push({ rootPath, slug: descriptor.slug, scope: descriptor.scope, cloudId: descriptor.cloudId, revision: descriptor.revision });
234
- }
235
- delete stateValue.assets[key];
236
- stateValue.deletedBases = stateValue.deletedBases.slice(-256);
237
233
  try {
238
- state.writeCloudAssetState(stateValue);
234
+ state.updateCloudAssetState((stateValue) => {
235
+ for (const rootPath of roots) {
236
+ // 톰스톤: 삭제된 베이스는 재저장 시 절대 If-Match 베이스로 재사용되지 않는다.
237
+ stateValue.deletedBases.push({ rootPath, slug: descriptor.slug, scope: descriptor.scope, cloudId: descriptor.cloudId, revision: descriptor.revision });
238
+ }
239
+ const current = stateValue.assets[key];
240
+ if (
241
+ current && current.descriptor.cloudId === descriptor.cloudId &&
242
+ current.descriptor.revision === descriptor.revision
243
+ ) {
244
+ delete stateValue.assets[key];
245
+ }
246
+ stateValue.deletedBases = stateValue.deletedBases.slice(-256);
247
+ });
239
248
  } catch (error) {
240
249
  const stateError = new Error(
241
250
  `Cloud delete committed on the server, but this machine could not persist the deletion tombstone. ` +
@@ -166,30 +166,80 @@ function writePurposeRepairCopy(root, projection) {
166
166
  return tempRoot;
167
167
  }
168
168
 
169
- function parseCloudFlags(args) {
169
+ const CLOUD_VALUE_FLAGS = ["limit", "name", "purpose", "scope", "slug", "visibility"];
170
+ const CLOUD_BOOLEAN_FLAGS = ["dry-run", "json", "llm-review", "overwrite", "strict"];
171
+
172
+ function cloudArgumentError(message) {
173
+ const error = new Error(message);
174
+ error.code = "INVALID_ARGUMENT";
175
+ return error;
176
+ }
177
+
178
+ function parseCloudFlags(args, spec = {}) {
179
+ const values = new Set(spec.values || CLOUD_VALUE_FLAGS);
180
+ const booleans = new Set(spec.booleans || CLOUD_BOOLEAN_FLAGS);
170
181
  const flags = { _: [] };
182
+ const seen = new Set();
183
+ let positionalOnly = false;
171
184
  for (let i = 0; i < args.length; i++) {
172
185
  const a = args[i];
173
- if (a && a.startsWith("--")) {
174
- const key = a.slice(2);
175
- const next = args[i + 1];
176
- if (next !== undefined && !String(next).startsWith("--")) {
177
- flags[key] = next;
178
- i++;
179
- } else {
180
- flags[key] = true;
181
- }
182
- } else {
186
+ if (positionalOnly) {
183
187
  flags._.push(a);
188
+ continue;
184
189
  }
190
+ if (a === "--") {
191
+ positionalOnly = true;
192
+ continue;
193
+ }
194
+ if (!String(a).startsWith("-")) {
195
+ flags._.push(a);
196
+ continue;
197
+ }
198
+ if (!String(a).startsWith("--")) throw cloudArgumentError(`unknown option: ${a}`);
199
+ const equal = String(a).indexOf("=");
200
+ const key = String(a).slice(2, equal === -1 ? undefined : equal);
201
+ if (!key || (!values.has(key) && !booleans.has(key))) throw cloudArgumentError(`unknown option: --${key || ""}`);
202
+ if (seen.has(key)) throw cloudArgumentError(`duplicate option: --${key}`);
203
+ seen.add(key);
204
+ if (booleans.has(key)) {
205
+ if (equal !== -1) throw cloudArgumentError(`--${key} does not take a value`);
206
+ flags[key] = true;
207
+ continue;
208
+ }
209
+ let value;
210
+ if (equal !== -1) {
211
+ value = String(a).slice(equal + 1);
212
+ } else {
213
+ const next = args[i + 1];
214
+ if (next === undefined || String(next).startsWith("--")) throw cloudArgumentError(`--${key} requires a value`);
215
+ value = String(next);
216
+ i++;
217
+ }
218
+ if (!value) throw cloudArgumentError(`--${key} requires a non-empty value`);
219
+ flags[key] = value;
185
220
  }
186
221
  return flags;
187
222
  }
188
223
 
224
+ function requireCloudPositionals(flags, min, max, usage) {
225
+ if (flags._.length < min || flags._.length > max) throw cloudArgumentError(usage);
226
+ return flags;
227
+ }
228
+
229
+ function positiveIntegerFlag(value, fallback, name, max) {
230
+ if (value == null) return fallback;
231
+ if (!/^\d+$/.test(String(value))) throw cloudArgumentError(`--${name} must be a positive integer`);
232
+ const parsed = Number(value);
233
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
234
+ throw cloudArgumentError(`--${name} must be between 1 and ${max}`);
235
+ }
236
+ return parsed;
237
+ }
238
+
189
239
  function cloudVisibilityFlag(value) {
190
240
  if (value == null) return null;
191
241
  if (value === "private-link" || value === "marketplace") return value;
192
- throw new Error("--visibility must be private-link or marketplace");
242
+ throw cloudArgumentError("--visibility must be private-link or marketplace");
193
243
  }
194
244
 
195
245
  function cloudVisibilityForAction(sub, flags) {
@@ -213,7 +263,10 @@ function cloudVisibilityForAction(sub, flags) {
213
263
 
214
264
  /** 최상위 `upload`의 실제 동작 결정: 기본 save, 명시적 marketplace만 publish. */
215
265
  function cloudActionForTopLevelUpload(args) {
216
- const flags = parseCloudFlags(args);
266
+ const flags = requireCloudPositionals(parseCloudFlags(args, {
267
+ values: ["purpose", "slug", "visibility"],
268
+ booleans: ["dry-run", "json", "llm-review", "overwrite"],
269
+ }), 1, 1, "usage: agentlas upload <path> [--visibility marketplace]");
217
270
  return cloudVisibilityFlag(flags.visibility) === "marketplace" ? "publish" : "save";
218
271
  }
219
272
 
@@ -269,17 +322,20 @@ const CLOUD_HELP = [
269
322
  async function runCloud(ctx, args) {
270
323
  const sub = args[0] || "help";
271
324
  if (sub === "help" || sub === "--help" || sub === "-h") {
325
+ requireCloudPositionals(parseCloudFlags(args.slice(1), { values: [], booleans: ["json"] }), 0, 0, "usage: agentlas cloud help");
272
326
  ctx.out(CLOUD_HELP);
273
327
  return 0;
274
328
  }
275
329
  if (sub === "search") {
276
- const flags = parseCloudFlags(args.slice(1));
330
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
331
+ values: ["limit"], booleans: ["json"],
332
+ }), 1, Number.MAX_SAFE_INTEGER, 'usage: agentlas cloud search "<what you need>" [--limit 10]');
277
333
  const query = flags._.join(" ").trim();
278
334
  if (!query) {
279
335
  ctx.err('usage: agentlas cloud search "<what you need>" [--limit 10]');
280
336
  return 1;
281
337
  }
282
- const limit = Math.max(1, Math.min(30, Number(flags.limit) || 10));
338
+ const limit = positiveIntegerFlag(flags.limit, 10, "limit", 30);
283
339
  let result;
284
340
  try {
285
341
  result = await callHubTool("marketplace.search_agents", { q: query, limit });
@@ -287,6 +343,7 @@ async function runCloud(ctx, args) {
287
343
  ctx.err(e instanceof HubError ? e.message : `Marketplace connection failed: ${(e && e.message) || e}`);
288
344
  return 1;
289
345
  }
346
+ if (flags.json) { ctx.out(JSON.stringify(result, null, 2)); return 0; }
290
347
  const items = (result && (result.results || result.agents || result.items)) || [];
291
348
  if (!Array.isArray(items) || !items.length) {
292
349
  ctx.out(`No results for "${query}"`);
@@ -298,8 +355,10 @@ async function runCloud(ctx, args) {
298
355
  return 0;
299
356
  }
300
357
  if (sub === "list") {
301
- const flags = parseCloudFlags(args.slice(1));
302
- const result = await listOwnedCloudAgents(Number(flags.limit || 100));
358
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
359
+ values: ["limit"], booleans: ["json"],
360
+ }), 0, 0, "usage: agentlas cloud list [--limit 100] [--json]");
361
+ const result = await listOwnedCloudAgents(positiveIntegerFlag(flags.limit, 100, "limit", 1000));
303
362
  if (flags.json) { ctx.out(JSON.stringify(result, null, 2)); return 0; }
304
363
  const agents = Array.isArray(result.results) ? result.results : [];
305
364
  if (!agents.length) { ctx.out("No agents are stored in Private Agent Cloud."); return 0; }
@@ -332,7 +391,9 @@ async function runCloud(ctx, args) {
332
391
  return 0;
333
392
  }
334
393
  if (sub === "restore") {
335
- const flags = parseCloudFlags(args.slice(1));
394
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
395
+ values: [], booleans: ["json"],
396
+ }), 1, 1, "usage: agentlas cloud restore <slug> [--json]");
336
397
  const slug = flags._[0];
337
398
  if (!slug) { ctx.err("usage: agentlas cloud restore <slug> [--json]"); return 1; }
338
399
  const result = await restoreOwnedCloudAgent(ctx.db(), slug);
@@ -344,7 +405,9 @@ async function runCloud(ctx, args) {
344
405
  return 0;
345
406
  }
346
407
  if (sub === "delete" || sub === "unpublish") {
347
- const flags = parseCloudFlags(args.slice(1));
408
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
409
+ values: ["scope"], booleans: ["json"],
410
+ }), 1, 1, `usage: agentlas cloud ${sub} <slug> [--scope owner-private|hub-public] [--json]`);
348
411
  const slug = flags._[0];
349
412
  if (!slug) { ctx.err(`usage: agentlas cloud ${sub} <slug> [--json]`); return 1; }
350
413
  const result = await deleteCloudAgent(slug, { scope: flags.scope });
@@ -356,7 +419,9 @@ async function runCloud(ctx, args) {
356
419
  }
357
420
  // ── agentlas-cloud-runtime 소비 지점 (스캔/매니페스트/lazy-read/필드테스트) ──
358
421
  if (sub === "wizard") {
359
- const flags = parseCloudFlags(args.slice(1));
422
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
423
+ values: ["name"], booleans: ["json"],
424
+ }), 1, 1, "usage: agentlas cloud wizard <path> [--name name] [--json]");
360
425
  const root = flags._[0];
361
426
  if (!root) { ctx.err("usage: agentlas cloud wizard <path> [--name name]"); return 1; }
362
427
  const result = cloudRuntime.runWizard(root, { name: typeof flags.name === "string" ? flags.name : undefined });
@@ -365,7 +430,9 @@ async function runCloud(ctx, args) {
365
430
  }
366
431
  if (sub === "security") {
367
432
  if (args[1] !== "scan") { ctx.err("usage: agentlas cloud security scan <path> [--strict]"); return 1; }
368
- const flags = parseCloudFlags(args.slice(2));
433
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
434
+ values: [], booleans: ["json", "strict"],
435
+ }), 1, 1, "usage: agentlas cloud security scan <path> [--strict]");
369
436
  const root = flags._[0];
370
437
  if (!root) { ctx.err("usage: agentlas cloud security scan <path> [--strict]"); return 1; }
371
438
  const report = cloudRuntime.scanFolder(root);
@@ -375,15 +442,18 @@ async function runCloud(ctx, args) {
375
442
  if (sub === "runtime") {
376
443
  const action = args[1];
377
444
  if (action === "bundle") {
378
- const root = args[2];
379
- if (!root) { ctx.err("usage: agentlas cloud runtime bundle <path>"); return 1; }
445
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
446
+ values: [], booleans: ["json"],
447
+ }), 1, 1, "usage: agentlas cloud runtime bundle <path> [--json]");
448
+ const root = flags._[0];
380
449
  ctx.out(JSON.stringify(cloudRuntime.compileBundle(root), null, 2));
381
450
  return 0;
382
451
  }
383
452
  if (action === "read-agent-file") {
384
- const root = args[2];
385
- const targetPath = args[3];
386
- if (!root || !targetPath) { ctx.err("usage: agentlas cloud runtime read-agent-file <path> <file>"); return 1; }
453
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
454
+ values: [], booleans: ["json"],
455
+ }), 2, 2, "usage: agentlas cloud runtime read-agent-file <path> <file> [--json]");
456
+ const [root, targetPath] = flags._;
387
457
  ctx.out(JSON.stringify(cloudRuntime.readAgentFile(root, targetPath), null, 2));
388
458
  return 0;
389
459
  }
@@ -391,15 +461,20 @@ async function runCloud(ctx, args) {
391
461
  return 1;
392
462
  }
393
463
  if (sub === "field-test") {
394
- const flags = parseCloudFlags(args.slice(1));
464
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
465
+ values: [], booleans: ["json"],
466
+ }), 0, 0, "usage: agentlas cloud field-test [--json]");
395
467
  const result = cloudRuntime.runFieldTest();
396
468
  ctx.out(flags.json ? JSON.stringify(result, null, 2) : `${result.suite}: ${result.status}`);
397
469
  return result.status === "PASS" ? 0 : 1;
398
470
  }
399
471
  if (sub === "install") {
400
- const slug = args[1];
401
- if (!slug) { ctx.err("usage: agentlas cloud install <slug>"); return 1; }
472
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
473
+ values: [], booleans: ["json"],
474
+ }), 1, 1, "usage: agentlas cloud install <slug> [--json]");
475
+ const slug = flags._[0];
402
476
  const agent = await installHubAgent(ctx.db(), slug);
477
+ if (flags.json) { ctx.out(JSON.stringify(agent, null, 2)); return 0; }
403
478
  ctx.out(`${ctx.ui.green("✓")} Hub installed ${agent.slug} — ${agent.name}`);
404
479
  if (agent.localPath) ctx.out(` files: ${agent.localPath}`);
405
480
  return 0;
@@ -408,7 +483,10 @@ async function runCloud(ctx, args) {
408
483
  ctx.err("usage: agentlas cloud <save|publish|package|list|restore|install|delete|field-test> ...");
409
484
  return 1;
410
485
  }
411
- const flags = parseCloudFlags(args.slice(1));
486
+ const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
487
+ values: ["purpose", "slug", "visibility"],
488
+ booleans: ["dry-run", "json", "llm-review", "overwrite"],
489
+ }), 1, 1, `usage: agentlas cloud ${sub} <path>`);
412
490
  const root = flags._[0];
413
491
  if (!root) { ctx.err(`usage: agentlas cloud ${sub} <path>`); return 1; }
414
492
  const visibility = cloudVisibilityForAction(sub, flags);