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
@@ -13,23 +13,25 @@
13
13
  * - 상태 파일은 <userData>/terminal/experience-intents-v1.json — v1과 동일
14
14
  * 포맷/경로. 쓰기는 0600 원자적 쓰기(임시파일+rename)만 사용한다.
15
15
  *
16
- * 검증 프리미티브(assert 계열, UNSAFE_TEXT_PATTERNS)는 engine/mcp/contract.cjs에도
17
- * 존재하지만, 의도 저장소는 mcp 서브시스템 변경과 독립적으로 보안 경계를
18
- * 유지해야 하므로 v1 원문을 여기 동결한다(우연한 규칙 완화 전파 방지).
16
+ * 검증 프리미티브(assert 계열, UNSAFE_TEXT_PATTERNS)는 MCP 계약과 분리해
17
+ * v1 원문을 여기 동결한다(우연한 규칙 완화 전파 방지). 파일 I/O와 lock은
18
+ * engine/mcp/contract.cjs의 공통 hardened primitive를 사용한다.
19
19
  */
20
20
 
21
21
  const crypto = require("node:crypto");
22
22
  const fs = require("node:fs");
23
23
  const path = require("node:path");
24
+ const {
25
+ readJsonFile,
26
+ writePrivateJsonAtomic,
27
+ withPrivateStateLock,
28
+ } = require("../mcp/contract.cjs");
24
29
 
25
30
  const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
26
31
  const HASH_RE = /^sha256:[0-9a-f]{64}$/;
27
32
  const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
28
33
  const EXPERIENCE_STATE_SCHEMA = "agentlas.terminal-experience-intents.v1";
29
34
  const EXPERIENCE_INTENT_SCHEMA = "agentlas.terminal-experience-intent.v1";
30
- const MAX_JSON_BYTES = 2 * 1024 * 1024;
31
- const EXPERIENCE_LOCK_STALE_MS = 30_000;
32
- const EXPERIENCE_LOCK_WAIT_MS = 2_000;
33
35
 
34
36
  const EXPERIENCE_PACK_REQUIRED = [
35
37
  "schemaVersion", "kind", "experiencePackId", "releaseId", "ownerRef", "version",
@@ -190,6 +192,12 @@ function validateExperiencePack(value) {
190
192
  assertUniqueIds(value.evidenceReceiptIds, "experience pack.evidenceReceiptIds");
191
193
  if (!Array.isArray(value.mcpRequirements) || value.mcpRequirements.length > 64) throw new Error("experience pack.mcpRequirements is invalid");
192
194
  value.mcpRequirements.forEach((requirement, index) => validateMcpRequirement(requirement, `experience pack.mcpRequirements[${index}]`));
195
+ const requirementIds = value.mcpRequirements.map((requirement) => requirement.requirementId);
196
+ if (new Set(requirementIds).size !== requirementIds.length) {
197
+ const error = new Error("experience pack.mcpRequirements requirementId values must be unique");
198
+ error.code = "duplicate_mcp_requirement_id";
199
+ throw error;
200
+ }
193
201
  // base 패키지 복사 반입 금지 — 참조만 허용(오너 결정, 완화 불가).
194
202
  if (value.containsBasePackageMaterial !== false) throw new Error("experience pack must reference the base release; copied base material is forbidden");
195
203
  if (!HASH_RE.test(String(value.contentHash || ""))) throw new Error("experience pack.contentHash is invalid");
@@ -199,75 +207,15 @@ function validateExperiencePack(value) {
199
207
  return value;
200
208
  }
201
209
 
202
- function readJsonFile(filePath, label) {
203
- const absolute = path.resolve(filePath);
204
- const stat = fs.lstatSync(absolute);
205
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} must be a regular file (symlinks are not accepted)`);
206
- if (stat.size <= 0 || stat.size > MAX_JSON_BYTES) throw new Error(`${label} has an invalid size`);
207
- let value;
208
- try { value = JSON.parse(fs.readFileSync(absolute, "utf8")); } catch (error) { throw new Error(`${label} is not valid JSON: ${error.message}`); }
209
- return { absolute, value };
210
- }
211
-
212
- function writePrivateJsonAtomic(filePath, value) {
213
- const dir = path.dirname(filePath);
214
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
215
- try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
216
- const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
217
- try {
218
- fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
219
- fs.renameSync(temp, filePath);
220
- try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
221
- } finally {
222
- try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
223
- }
224
- }
225
-
226
210
  function experienceStatePath(userDataDir) {
227
211
  return path.join(userDataDir, "terminal", "experience-intents-v1.json");
228
212
  }
229
213
 
230
- function waitSync(milliseconds) {
231
- // Atomics.wait은 지원 Node 20+에서 스핀 없이 대기하는 유일한 동기 sleep이다.
232
- const signal = new Int32Array(new SharedArrayBuffer(4));
233
- Atomics.wait(signal, 0, 0, milliseconds);
234
- }
235
-
236
214
  function withExperienceStateLock(userDataDir, action) {
237
- const stateFile = experienceStatePath(userDataDir);
238
- const dir = path.dirname(stateFile);
239
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
240
- try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
241
- const lockFile = `${stateFile}.lock`;
242
- const deadline = Date.now() + EXPERIENCE_LOCK_WAIT_MS;
243
- let descriptor = null;
244
- while (descriptor == null) {
245
- try {
246
- descriptor = fs.openSync(lockFile, "wx", 0o600);
247
- fs.writeFileSync(descriptor, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
248
- } catch (error) {
249
- if (!error || error.code !== "EEXIST") throw error;
250
- try {
251
- const stat = fs.lstatSync(lockFile);
252
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Terminal experience lock is unsafe");
253
- if (Date.now() - stat.mtimeMs > EXPERIENCE_LOCK_STALE_MS) {
254
- fs.unlinkSync(lockFile);
255
- continue;
256
- }
257
- } catch (statError) {
258
- if (statError && statError.code === "ENOENT") continue;
259
- throw statError;
260
- }
261
- if (Date.now() >= deadline) throw new Error("Terminal experience state is busy; retry the command");
262
- waitSync(25);
263
- }
264
- }
265
- try {
266
- return action();
267
- } finally {
268
- try { fs.closeSync(descriptor); } catch { /* noop */ }
269
- try { fs.unlinkSync(lockFile); } catch { /* crash recovery handles leftovers */ }
270
- }
215
+ return withPrivateStateLock(experienceStatePath(userDataDir), {
216
+ unsafe: "Terminal experience lock is unsafe",
217
+ busy: "Terminal experience state is busy; retry the command",
218
+ }, action);
271
219
  }
272
220
 
273
221
  function emptyExperienceState() {
@@ -276,10 +224,12 @@ function emptyExperienceState() {
276
224
 
277
225
  function loadExperienceState(userDataDir) {
278
226
  const file = experienceStatePath(userDataDir);
279
- if (!fs.existsSync(file)) return emptyExperienceState();
280
- const stat = fs.lstatSync(file);
281
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_JSON_BYTES) throw new Error("Terminal experience state is unsafe or too large");
282
- const state = JSON.parse(fs.readFileSync(file, "utf8"));
227
+ let state;
228
+ try { ({ value: state } = readJsonFile(file, "Terminal experience state")); }
229
+ catch (error) {
230
+ if (error && error.code === "ENOENT") return emptyExperienceState();
231
+ throw error;
232
+ }
283
233
  assertExactKeys(state, new Set(["schemaVersion", "updatedAt", "intents"]), ["schemaVersion", "updatedAt", "intents"], "Terminal experience state");
284
234
  if (state.schemaVersion !== EXPERIENCE_STATE_SCHEMA || !Array.isArray(state.intents)) throw new Error("Terminal experience state schema is invalid");
285
235
  assertIsoDateOrNull(state.updatedAt, "Terminal experience state.updatedAt");
@@ -336,16 +286,16 @@ function publicExperienceIntent(intent) {
336
286
 
337
287
  function parseSimpleFlags(args) {
338
288
  const flags = { _: [] };
289
+ let passthrough = false;
339
290
  for (let index = 0; index < args.length; index += 1) {
340
291
  const token = String(args[index]);
341
- if (token === "--json") flags.json = true;
342
- else if (token.startsWith("--") && token.includes("=")) {
343
- const at = token.indexOf("=");
344
- flags[token.slice(2, at)] = token.slice(at + 1);
292
+ if (passthrough) { flags._.push(token); continue; }
293
+ if (token === "--") { passthrough = true; continue; }
294
+ if (token === "--json") {
295
+ if (flags.json === true) throw new Error("duplicate option: --json");
296
+ flags.json = true;
345
297
  } else if (token.startsWith("--")) {
346
- const key = token.slice(2);
347
- if (index + 1 < args.length && !String(args[index + 1]).startsWith("--")) flags[key] = String(args[++index]);
348
- else flags[key] = true;
298
+ throw new Error(`unknown option: ${token}`);
349
299
  } else flags._.push(token);
350
300
  }
351
301
  return flags;
@@ -428,13 +378,14 @@ function cmdExperience(options) {
428
378
  const userData = options.userDataDir;
429
379
  if (!userData) throw new Error("Terminal userData path is required");
430
380
  if (sub === "list" || sub === "ls") {
381
+ if (flags._.length) throw new Error("usage: agentlas experience legacy-list [--json]");
431
382
  const list = loadExperienceState(userData).intents.map(publicExperienceIntent);
432
383
  emit(flags.json ? JSON.stringify({ localOnly: true, hubPublicationAttempted: false, intents: list }, null, 2) : renderExperienceList(list));
433
384
  return list;
434
385
  }
435
386
  if (sub === "inspect" || sub === "show") {
436
387
  const ref = flags._[0];
437
- if (!ref) throw new Error("usage: agentlas experience inspect <pack-id|release-id|intent-id>");
388
+ if (!ref || flags._.length !== 1) throw new Error("usage: agentlas experience legacy-inspect <pack-id|release-id|intent-id> [--json]");
438
389
  const intent = findIntent(loadExperienceState(userData), ref);
439
390
  if (!intent) throw new Error(`local Experience Pack intent not found: ${ref}`);
440
391
  const projected = publicExperienceIntent(intent);
@@ -448,6 +399,7 @@ function cmdExperience(options) {
448
399
  return projected;
449
400
  }
450
401
  if (sub === "publish") {
402
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience legacy-publish <experience-pack.json> [--json]");
451
403
  const intent = publishExperienceIntent(userData, flags._[0], options.cwd);
452
404
  emit(flags.json ? JSON.stringify(intent, null, 2) : [
453
405
  `Local publish intent saved: ${intent.experiencePackId}@${intent.version}`,
@@ -457,6 +409,7 @@ function cmdExperience(options) {
457
409
  return intent;
458
410
  }
459
411
  if (sub === "unpublish") {
412
+ if (flags._.length !== 1) throw new Error("usage: agentlas experience legacy-unpublish <pack-id|release-id|intent-id> [--json]");
460
413
  const intent = unpublishExperienceIntent(userData, flags._[0]);
461
414
  emit(flags.json ? JSON.stringify(intent, null, 2) : [
462
415
  `Local unpublish intent saved: ${intent.experiencePackId}@${intent.version}`,
@@ -23,6 +23,7 @@ const { tableExists } = require("../core/db.cjs");
23
23
  const { agentFolder } = require("../agents/files.cjs");
24
24
  const { routesMap } = require("../agents/routes.cjs");
25
25
  const terminalExperienceExchange = require("../agentlas-experience-exchange.cjs");
26
+ const permissions = require("../agentlas-permissions.cjs");
26
27
  const terminalExperienceIntake = require("../agentlas-experience-intake.cjs");
27
28
  const desktopOntologyLoadout = require("../agentlas-desktop-loadout.cjs");
28
29
 
@@ -103,9 +104,14 @@ function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
103
104
  const packageHash = /^[a-f0-9]{64}$/.test(rawHash) ? `sha256:${rawHash}` : null;
104
105
  const explicitDefinition = String(runtimeExperience?.agentDefinitionId || "");
105
106
  const explicitRelease = String(runtimeExperience?.baseAgentReleaseId || "");
107
+ const hasExplicitBinding = !!runtimeExperience && (
108
+ Object.prototype.hasOwnProperty.call(runtimeExperience, "agentDefinitionId") ||
109
+ Object.prototype.hasOwnProperty.call(runtimeExperience, "baseAgentReleaseId")
110
+ );
106
111
  if (portableId.test(explicitDefinition) && portableId.test(explicitRelease)) {
107
112
  return { agentDefinitionId: explicitDefinition, agentReleaseId: explicitRelease, packageHash, authority: "explicit-runtime-binding" };
108
113
  }
114
+ if (hasExplicitBinding) return null;
109
115
  if (binding && portableId.test(String(binding.agent_definition_id)) && portableId.test(String(binding.agent_release_id))) {
110
116
  return { agentDefinitionId: binding.agent_definition_id, agentReleaseId: binding.agent_release_id, packageHash, authority: "installed-hub-binding" };
111
117
  }
@@ -185,7 +191,7 @@ function resolveRuntimeExperienceCli(agent, prompt, requested, cwd, overrides =
185
191
  * null — 적립이 실행 성공/실패 판정을 바꾸지 않는다.
186
192
  */
187
193
  function finalizeExperienceExecutionCli(db, input) {
188
- if (input.permission === "read") return null;
194
+ if (permissions.normalize(input.permission) === "read") return null;
189
195
  if (!input.agentId) return null;
190
196
  let agent;
191
197
  try { agent = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(input.agentId); }
@@ -21,11 +21,52 @@ const {
21
21
  assertObject,
22
22
  assertExactKeys,
23
23
  assertId,
24
+ assertSafeText,
24
25
  validateMcpRequirement,
25
26
  readJsonFile,
26
- parseSimpleFlags,
27
27
  } = require("./intents.cjs");
28
28
 
29
+ const MAX_VARIANT_CANDIDATES = 512;
30
+
31
+ function parseVariantFlags(args) {
32
+ const flags = { _: [] };
33
+ const seen = new Set();
34
+ const valueFlags = new Set(["base-release", "prefer", "candidates"]);
35
+ const booleanFlags = new Set(["json", "no-base-only"]);
36
+ let positionalOnly = false;
37
+ for (let index = 0; index < args.length; index += 1) {
38
+ const token = String(args[index]);
39
+ if (positionalOnly) {
40
+ flags._.push(token);
41
+ continue;
42
+ }
43
+ if (token === "--") {
44
+ positionalOnly = true;
45
+ continue;
46
+ }
47
+ if (!token.startsWith("--")) {
48
+ flags._.push(token);
49
+ continue;
50
+ }
51
+ const at = token.indexOf("=");
52
+ const key = token.slice(2, at < 0 ? undefined : at);
53
+ if (!valueFlags.has(key) && !booleanFlags.has(key)) throw new Error(`unknown variant option: --${key}`);
54
+ if (seen.has(key)) throw new Error(`duplicate variant option: --${key}`);
55
+ seen.add(key);
56
+ if (booleanFlags.has(key)) {
57
+ if (at >= 0) throw new Error(`--${key} does not take a value`);
58
+ flags[key] = true;
59
+ continue;
60
+ }
61
+ const value = at >= 0 ? token.slice(at + 1) : String(args[++index] ?? "");
62
+ if (!value || (at < 0 && value.startsWith("--"))) throw new Error(`--${key} requires a value`);
63
+ flags[key] = value;
64
+ }
65
+ if (flags._.length > 1) throw new Error("variant resolve accepts at most one candidates.json path");
66
+ if (flags.candidates && flags._.length) throw new Error("choose either --candidates or a positional candidates.json path");
67
+ return flags;
68
+ }
69
+
29
70
  function validateVariantCandidate(candidate, index) {
30
71
  const allowed = new Set(["variantId", "baseAgentReleaseId", "experiencePackReleaseId", "status", "compatibilityStatus", "score", "mcpRequirements"]);
31
72
  assertExactKeys(candidate, allowed, ["variantId", "baseAgentReleaseId", "experiencePackReleaseId", "status", "compatibilityStatus", "score", "mcpRequirements"], `candidate[${index}]`);
@@ -39,15 +80,19 @@ function validateVariantCandidate(candidate, index) {
39
80
  variantId: candidate.variantId,
40
81
  baseAgentReleaseId: candidate.baseAgentReleaseId,
41
82
  experiencePackReleaseId: candidate.experiencePackReleaseId,
42
- status: String(candidate.status || "draft"),
43
- compatibilityStatus: String(candidate.compatibilityStatus || "unverified"),
83
+ status: assertSafeText(candidate.status || "draft", `candidate[${index}].status`, 64),
84
+ compatibilityStatus: assertSafeText(candidate.compatibilityStatus || "unverified", `candidate[${index}].compatibilityStatus`, 64),
44
85
  score,
45
86
  mcpRequirements: requirements,
46
87
  };
47
88
  }
48
89
 
49
90
  function resolveVariantCandidates(options) {
91
+ if (!options || !Array.isArray(options.candidates || [])) throw new Error("variant candidates must be an array");
92
+ if ((options.candidates || []).length > MAX_VARIANT_CANDIDATES) throw new Error("too many variant candidates");
50
93
  const candidates = (options.candidates || []).map(validateVariantCandidate);
94
+ const candidateIds = candidates.map((candidate) => candidate.variantId);
95
+ if (new Set(candidateIds).size !== candidateIds.length) throw new Error("variantId values must be unique");
51
96
  const inventoryById = indexInventory(options.inventory || []);
52
97
  if (!options.baseAgentReleaseId) {
53
98
  return {
@@ -67,6 +112,8 @@ function resolveVariantCandidates(options) {
67
112
  requiredMcpFailureScope: "variant-only",
68
113
  };
69
114
  }
115
+ assertId(options.baseAgentReleaseId, "baseAgentReleaseId");
116
+ if (options.preferredVariantId) assertId(options.preferredVariantId, "preferredVariantId");
70
117
  const excluded = [];
71
118
  const eligible = [];
72
119
  for (const candidate of candidates) {
@@ -202,7 +249,7 @@ function cmdVariant(options) {
202
249
  return null;
203
250
  }
204
251
  if (sub !== "resolve") throw new Error(`unknown variant subcommand: ${sub} (resolve|help)`);
205
- const flags = parseSimpleFlags(args.slice(1));
252
+ const flags = parseVariantFlags(args.slice(1));
206
253
  let candidates = [];
207
254
  let baseAgentReleaseId = flags["base-release"] || null;
208
255
  const candidateFile = flags.candidates || flags._[0];
@@ -211,6 +258,12 @@ function cmdVariant(options) {
211
258
  if (Array.isArray(value)) candidates = value;
212
259
  else {
213
260
  assertObject(value, "variant candidate document");
261
+ assertExactKeys(
262
+ value,
263
+ new Set(["baseAgentReleaseId", "candidates"]),
264
+ ["candidates"],
265
+ "variant candidate document",
266
+ );
214
267
  if (!Array.isArray(value.candidates)) throw new Error("variant candidate document must contain candidates[]");
215
268
  candidates = value.candidates;
216
269
  if (!baseAgentReleaseId && value.baseAgentReleaseId) baseAgentReleaseId = value.baseAgentReleaseId;
@@ -236,5 +289,6 @@ module.exports = {
236
289
  resolveVariantCandidates,
237
290
  renderVariantResolution,
238
291
  variantUsage,
292
+ parseVariantFlags,
239
293
  cmdVariant,
240
294
  };
@@ -238,7 +238,9 @@ async function parallelCap(items, cap, fn) {
238
238
  }
239
239
 
240
240
  function isVerificationDivision(node) {
241
- const label = `${node && node.role || ""} ${node && node.name || ""} ${node && node.key || ""}`
241
+ // key에는 `<firm-slug>:` 접두사가 들어간다. 회사 이름이 `firm-test`/`qa-studio`인
242
+ // 것만으로 모든 본부를 검증 본부로 오분류하면 구현 슬롯이 하나도 실행되지 않는다.
243
+ const label = `${node && node.role || ""} ${node && node.name || ""}`
242
244
  .toLowerCase()
243
245
  .replace(/[_-]+/g, " ");
244
246
  return /\b(?:eval|qa|quality|test|verification|verifier)\b|policy\s+gate/.test(label);
@@ -278,9 +280,9 @@ function verificationResultOk(text, sessionOk) {
278
280
  if (!sessionOk) return false;
279
281
  const source = String(text || "").trim();
280
282
  const explicit = source.match(/<verification_verdict>\s*(PASS|FAIL)\s*<\/verification_verdict>/i);
281
- if (explicit) return explicit[1].toUpperCase() === "PASS";
282
- const opening = source.slice(0, 900);
283
- return !/(?:\bverdict\s*:\s*fail\b|\brelease[- ]blocking\b|\bnot complete\b|\bcannot truthfully\b|\bno[- ]go\b|\bblocking defect\b)/i.test(opening);
283
+ // 검증 프롬프트가 요구한 machine-readable verdict가 유일한 성공 근거다.
284
+ // 본문 어조로 성공을 추측하면 태그를 빠뜨린 응답이나 명시적 FAIL까지 통과할 수 있다.
285
+ return Boolean(explicit && explicit[1].toUpperCase() === "PASS");
284
286
  }
285
287
 
286
288
  function latestResultsAllOk(results) {
@@ -400,9 +402,10 @@ async function runFirmTurn(p) {
400
402
  ? `${m.brief || task}\n\n[Release-blocking repair stage]\nIndependent verification found the following failures in the current integrated product. Inspect the evidence and current files, repair the actual shipped experience, and rerun the relevant checks before returning. Do not merely describe the fix.\n${stageContext}`
401
403
  : (m.brief || task);
402
404
  const res = await session.send(prompt);
403
- text = cleanFenceText(turnText(res));
405
+ const rawText = turnText(res);
406
+ text = cleanFenceText(rawText);
404
407
  ok = stageKind === "verification"
405
- ? verificationResultOk(text, session.status === "done")
408
+ ? verificationResultOk(rawText, session.status === "done")
406
409
  : session.status === "done";
407
410
  if (!ok && !text) text = session.lastError || "no response";
408
411
  } catch (e) {
@@ -61,7 +61,7 @@ function consentedMcpServersFor(ctx) {
61
61
  try {
62
62
  const mcp = require("../mcp/index.cjs");
63
63
  if (typeof mcp.readConsentedSystemMcpServers !== "function") return [];
64
- return mcp.readConsentedSystemMcpServers(ctx.db(), { env: process.env }) || [];
64
+ return mcp.readConsentedSystemMcpServers(ctx.db(), { env: process.env, createRuntimeHome: false }) || [];
65
65
  } catch {
66
66
  return [];
67
67
  }
@@ -85,7 +85,7 @@ async function askModel(ctx, prompt, opts = {}) {
85
85
 
86
86
  // 시도 순서: 사용자가 정한 것 먼저, 그다음 이 컴퓨터에 있는 나머지.
87
87
  const candidates = [];
88
- if (primary && primary.kind && primary.bin) candidates.push(primary);
88
+ if (primary && primary.kind && (primary.bin || primary.kind === "ollama")) candidates.push(primary);
89
89
  if (!opts.runtime) {
90
90
  // listAvailableCliRuntimes()는 문자열이 아니라 {kind, bin, path} 객체를 돌려준다.
91
91
  // 문자열로 읽으면 후보가 하나도 안 쌓여 폴백이 통째로 죽는다(실측).
@@ -114,12 +114,23 @@ async function askModel(ctx, prompt, opts = {}) {
114
114
  for (const runtime of candidates) {
115
115
  let res;
116
116
  try {
117
- res = await nativeHost.runNativeTurn({
118
- kind: runtime.kind,
119
- bin: runtime.bin,
120
- ui: quietSink(),
121
- cwd: opts.cwd || process.cwd(),
122
- prompt,
117
+ const cwd = opts.cwd || process.cwd();
118
+ const ui = quietSink();
119
+ res = runtime.kind === "ollama"
120
+ ? await require("../agentlas-api-agent.cjs").runApiTurn({
121
+ backend: "ollama",
122
+ model: runtime.model || "llama3.1",
123
+ system: "",
124
+ messages: [{ role: "user", content: prompt }],
125
+ ctx: { cwd, permission: "read", env: process.env, db },
126
+ ui,
127
+ })
128
+ : await nativeHost.runNativeTurn({
129
+ kind: runtime.kind,
130
+ bin: runtime.bin,
131
+ ui,
132
+ cwd,
133
+ prompt,
123
134
  /*
124
135
  * 읽기 권한 — 만드는 동안 바깥을 바꾸지 않는다. 메일이 나가거나 글이 올라가면 안 된다.
125
136
  * ★런타임의 "read"는 **쓰기 금지가 아니라 도구 금지에 가깝다**(이 저장소 실측:
@@ -140,9 +151,9 @@ async function askModel(ctx, prompt, opts = {}) {
140
151
  * 새로 동의를 받지 않는다 — 이미 받아 둔 것만 그대로 쓴다(consent 영수증 기준).
141
152
  * 못 읽으면 빈 배열로 간다: 예전과 같아질 뿐 나빠지지 않는다.
142
153
  */
143
- mcpServers: consentedMcpServersFor(ctx),
144
- mcpAllowlistMode: "exact",
145
- });
154
+ mcpServers: consentedMcpServersFor(ctx),
155
+ mcpAllowlistMode: "exact",
156
+ });
146
157
  } catch (err) {
147
158
  failures.push(`${runtime.kind}: ${(err && err.message) || err}`);
148
159
  continue;