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
@@ -280,8 +280,14 @@ function triggerQuestion() {
280
280
  */
281
281
  function autofillOutputChecks(bp) {
282
282
  if (!bp || !Array.isArray(bp.steps)) return bp;
283
+ // 잘못된 모델 출력을 빈 목록으로 고쳐 쓰지 않는다. validateBlueprint가 retry 사유로
284
+ // 돌려줘야 모델이 자기 형식을 바로잡을 수 있다.
285
+ if (bp.checks != null && !Array.isArray(bp.checks)) return bp;
283
286
  const checks = Array.isArray(bp.checks) ? [...bp.checks] : [];
284
- const checked = new Set(checks.map((c) => (c.subject || "").trim()).filter(Boolean));
287
+ const checked = new Set(checks
288
+ .filter((check) => check && typeof check === "object")
289
+ .map((check) => String(check.subject || "").trim())
290
+ .filter(Boolean));
285
291
  bp.steps.forEach((step, index) => {
286
292
  if (step.effect !== "mutation") return;
287
293
  for (const value of Array.isArray(step.consumes) ? step.consumes : []) {
@@ -310,10 +316,14 @@ function validateBlueprint(bp, ctx = {}) {
310
316
  const problems = [];
311
317
  const push = (reason, ask = null) => problems.push({ reason, ask });
312
318
  if (!bp || typeof bp !== "object") { push("만들 내용을 읽지 못했습니다."); return problems; }
313
- if (!String(bp.name || "").trim()) {
319
+ if (typeof bp.name !== "string") {
320
+ push("이름(name)의 형식이 올바르지 않습니다.");
321
+ } else if (!bp.name.trim()) {
314
322
  push("이름이 없습니다.", { id: "name", question: "이 자동화를 뭐라고 부를까요?", why: "목록에서 이 이름으로 찾게 됩니다." });
315
323
  }
316
- if (!String(bp.goal || "").trim()) {
324
+ if (typeof bp.goal !== "string") {
325
+ push("목표(goal)의 형식이 올바르지 않습니다.");
326
+ } else if (!bp.goal.trim()) {
317
327
  push("무엇을 위한 자동화인지가 없습니다.", {
318
328
  id: "goal", question: "이 자동화로 무엇을 얻고 싶으신가요? 한 문장이면 됩니다.",
319
329
  why: "나중에 목록에서 보고 무엇이었는지 알아보려면 필요합니다.",
@@ -323,7 +333,9 @@ function validateBlueprint(bp, ctx = {}) {
323
333
  if (!trigger || typeof trigger !== "object") {
324
334
  push("언제 시작하는지가 없습니다.", triggerQuestion());
325
335
  } else if (trigger.kind === "cron") {
326
- if (!String(trigger.schedule || "").trim()) {
336
+ if (typeof trigger.schedule !== "string") {
337
+ push("실행 시각(schedule)의 형식이 올바르지 않습니다.");
338
+ } else if (!trigger.schedule.trim()) {
327
339
  push("실행 시각이 없습니다.", {
328
340
  id: "schedule", question: "몇 시에 돌릴까요?",
329
341
  why: "시각을 대신 정하면, 보지 않는 시간에 조용히 돌게 됩니다.",
@@ -331,18 +343,22 @@ function validateBlueprint(bp, ctx = {}) {
331
343
  });
332
344
  }
333
345
  } else if (trigger.kind === "input") {
334
- if (!String(trigger.label || "").trim()) {
346
+ if (typeof trigger.label !== "string") {
347
+ push("입력 안내(label)의 형식이 올바르지 않습니다.");
348
+ } else if (!trigger.label.trim()) {
335
349
  push("무엇을 입력받는지가 없습니다.", {
336
350
  id: "input-label", question: "시작할 때 무엇을 입력받을까요? (예: 만들 프로젝트의 주제)",
337
351
  why: "입력창에 이 문구가 그대로 보입니다.",
338
352
  });
339
353
  }
340
- if (!VAR_RE.test(String(trigger.varName || ""))) push("입력값의 이름이 올바르지 않습니다.");
354
+ if (typeof trigger.varName !== "string" || !VAR_RE.test(trigger.varName)) push("입력값의 이름이 올바르지 않습니다.");
341
355
  } else {
342
356
  push("언제 시작하는지를 알 수 없습니다.", triggerQuestion());
343
357
  }
344
358
 
345
359
  const steps = Array.isArray(bp.steps) ? bp.steps : [];
360
+ const checks = Array.isArray(bp.checks) ? bp.checks : [];
361
+ const branches = Array.isArray(bp.branches) ? bp.branches : [];
346
362
  if (steps.length === 0) {
347
363
  push("할 일이 하나도 없습니다.", {
348
364
  id: "steps", question: "무슨 일을 해야 하나요? 순서대로 적어 주세요.",
@@ -350,14 +366,19 @@ function validateBlueprint(bp, ctx = {}) {
350
366
  });
351
367
  }
352
368
  if (steps.length > MAX_STEPS) push(`단계가 ${steps.length}개입니다. 한 번에 만들 수 있는 것은 ${MAX_STEPS}개까지입니다.`);
369
+ if (bp.checks != null && !Array.isArray(bp.checks)) push("검증 목록(checks)의 형식이 올바르지 않습니다.");
370
+ if (bp.branches != null && !Array.isArray(bp.branches)) push("갈림길 목록(branches)의 형식이 올바르지 않습니다.");
353
371
 
354
372
  const produced = new Set();
355
373
  if (trigger && trigger.kind === "input" && trigger.varName) produced.add(trigger.varName);
356
374
  steps.forEach((step, index) => {
357
375
  const at = `${index + 1}번째 단계`;
358
376
  if (!step || typeof step !== "object") { push(`${at}를 읽지 못했습니다.`); return; }
359
- if (!String(step.title || "").trim()) push(`${at} 이름이 없습니다.`);
360
- if (!String(step.instruction || "").trim()) {
377
+ if (typeof step.title !== "string") push(`${at} 이름(title) 형식이 올바르지 않습니다.`);
378
+ else if (!step.title.trim()) push(`${at}에 이름이 없습니다.`);
379
+ if (typeof step.instruction !== "string") {
380
+ push(`${at}의 지시(instruction) 형식이 올바르지 않습니다.`);
381
+ } else if (!step.instruction.trim()) {
361
382
  push(`${at}가 무엇을 할지 적혀 있지 않습니다.`, {
362
383
  id: `step-${index}-instruction`,
363
384
  question: `"${step.title || at}" 단계에서 정확히 무엇을 해야 하나요?`,
@@ -372,11 +393,14 @@ function validateBlueprint(bp, ctx = {}) {
372
393
  choices: ["아니요, 만들기만 합니다", "네, 바깥으로 나갑니다"],
373
394
  });
374
395
  }
396
+ if (step.kind != null && !["agent", "code", "runGraph"].includes(step.kind)) {
397
+ push(`${at}의 실행 종류(kind) 형식이 올바르지 않습니다.`);
398
+ }
375
399
  // ★부를 자동화는 실재해야 한다(데스크탑 shared/graph-blueprint.ts와 같은 규칙). 이름이
376
400
  // 아니라 id로 가리키는 이유도 같다 — 이름은 바뀌고 지어낸 id는 실행에서 죽는다.
377
401
  // 목록을 모르는 호출부에서는 형식만 본다(ctx.knownGraphs 없으면 건너뜀).
378
402
  if (step.kind === "runGraph") {
379
- const ref = String(step.graphRef || "").trim();
403
+ const ref = typeof step.graphRef === "string" ? step.graphRef.trim() : "";
380
404
  if (!ref) {
381
405
  push(`${at}가 어느 자동화를 부를지 정하지 않았습니다.`);
382
406
  } else if (ctx.selfId && ref === ctx.selfId) {
@@ -388,7 +412,9 @@ function validateBlueprint(bp, ctx = {}) {
388
412
  // ★코드 스텝은 스크립트가 있어야 한다. 없으면 "코드로 하겠다"고 해 놓고 빈 채로 저장돼
389
413
  // 실행에서 CODE_NODE_EMPTY로 죽는다(데스크탑과 같은 규칙, 저작 시점에 막는다).
390
414
  if (step.kind === "code") {
391
- if (!String(step.code || "").trim()) {
415
+ if (typeof step.code !== "string") {
416
+ push(`${at}의 코드(code) 형식이 올바르지 않습니다.`);
417
+ } else if (!step.code.trim()) {
392
418
  push(`${at}는 코드로 실행한다고 했는데 스크립트가 비어 있습니다.`, {
393
419
  id: `step-${index}-code`,
394
420
  question: `"${step.title || at}" 단계에서 무엇을 계산·가공하나요? (AI가 스크립트를 채웁니다)`,
@@ -398,8 +424,19 @@ function validateBlueprint(bp, ctx = {}) {
398
424
  if (step.codeLang && step.codeLang !== "python" && step.codeLang !== "js") {
399
425
  push(`${at}의 코드 언어 "${step.codeLang}"을(를) 이 제품이 모릅니다(python 또는 js).`);
400
426
  }
427
+ if (step.packages != null && !Array.isArray(step.packages)) {
428
+ push(`${at}의 코드 패키지 목록(packages) 형식이 올바르지 않습니다.`);
429
+ } else if (Array.isArray(step.packages) && step.packages.some((value) => typeof value !== "string" || !value.trim())) {
430
+ push(`${at}의 코드 패키지 이름 하나가 올바르지 않습니다.`);
431
+ }
401
432
  }
402
- for (const name of step.consumes || []) {
433
+ const consumes = Array.isArray(step.consumes) ? step.consumes : [];
434
+ if (step.consumes != null && !Array.isArray(step.consumes)) push(`${at}의 입력값 목록(consumes) 형식이 올바르지 않습니다.`);
435
+ for (const name of consumes) {
436
+ if (typeof name !== "string" || !VAR_RE.test(name)) {
437
+ push(`${at}의 입력값 이름 하나가 올바르지 않습니다.`);
438
+ continue;
439
+ }
403
440
  if (!produced.has(name)) {
404
441
  push(`${at}가 쓰는 "${name}" 값을 아무도 만들지 않습니다.`, {
405
442
  id: `step-${index}-consumes-${name}`,
@@ -409,7 +446,9 @@ function validateBlueprint(bp, ctx = {}) {
409
446
  }
410
447
  }
411
448
 
412
- for (const use of step.uses || []) {
449
+ const uses = Array.isArray(step.uses) ? step.uses : [];
450
+ if (step.uses != null && !Array.isArray(step.uses)) push(`${at}의 도구 목록(uses) 형식이 올바르지 않습니다.`);
451
+ for (const use of uses) {
413
452
  if (!use || typeof use !== "object") { push(`${at}의 도구 선언을 읽지 못했습니다.`); continue; }
414
453
  if (!CAPABILITIES.includes(use.capability)) {
415
454
  push(`${at}가 이 제품이 모르는 도구("${use.capability}")를 쓰려고 합니다.`, {
@@ -430,15 +469,20 @@ function validateBlueprint(bp, ctx = {}) {
430
469
  }
431
470
  }
432
471
  if (step.produces) {
433
- if (!VAR_RE.test(step.produces)) push(`${at}의 결과 이름 "${step.produces}"은(는) 쓸 수 없습니다.`);
472
+ if (typeof step.produces !== "string" || !VAR_RE.test(step.produces)) push(`${at}의 결과 이름 "${step.produces}"은(는) 쓸 수 없습니다.`);
434
473
  else produced.add(step.produces);
474
+ } else if (step.produces != null && typeof step.produces !== "string") {
475
+ push(`${at}의 결과 이름은 문자열이어야 합니다.`);
435
476
  }
477
+ if (step.role != null && typeof step.role !== "string") push(`${at}의 역할(role) 형식이 올바르지 않습니다.`);
478
+ if (step.roleEn != null && typeof step.roleEn !== "string") push(`${at}의 영어 역할(roleEn) 형식이 올바르지 않습니다.`);
436
479
  });
437
480
 
438
481
 
439
482
  // 검증 단계
440
483
  const checkVerdicts = new Set();
441
- for (const check of bp.checks || []) {
484
+ for (const check of checks) {
485
+ if (!check || typeof check !== "object") { push("검증 항목 하나의 형식이 올바르지 않습니다."); continue; }
442
486
  const at = `${(check.afterStep ?? 0) + 1}번째 단계 뒤의 검증`;
443
487
  if (!steps[check.afterStep]) { push(`${at}가 없는 단계를 가리킵니다.`); continue; }
444
488
  if (!check.subject || !produced.has(check.subject)) {
@@ -478,7 +522,10 @@ function validateBlueprint(bp, ctx = {}) {
478
522
  */
479
523
  {
480
524
  const checkedSubjects = new Set(
481
- (bp.checks || []).map((check) => (check.subject || "").trim()).filter(Boolean),
525
+ checks
526
+ .filter((check) => check && typeof check === "object")
527
+ .map((check) => String(check.subject || "").trim())
528
+ .filter(Boolean),
482
529
  );
483
530
  steps.forEach((step, index) => {
484
531
  if (step.effect !== "mutation") return;
@@ -514,8 +561,9 @@ function validateBlueprint(bp, ctx = {}) {
514
561
  */
515
562
  {
516
563
  const resultChecks = new Map();
517
- for (const check of bp.checks || []) {
518
- const subj = (check.subject || "").trim();
564
+ for (const check of checks) {
565
+ if (!check || typeof check !== "object") continue;
566
+ const subj = String(check.subject || "").trim();
519
567
  if (subj) resultChecks.set(subj, check);
520
568
  }
521
569
  steps.forEach((step, index) => {
@@ -552,7 +600,8 @@ function validateBlueprint(bp, ctx = {}) {
552
600
  }
553
601
 
554
602
  // 반복이 있는데 검증이 없으면 "마음에 들 때까지"를 글자 찾기로 흉내 내게 된다(실측).
555
- for (const branch of bp.branches || []) {
603
+ for (const branch of branches) {
604
+ if (!branch || typeof branch !== "object") { push("갈림길 항목 하나의 형식이 올바르지 않습니다."); continue; }
556
605
  if (branch.repeatStep === undefined) continue;
557
606
  if (!checkVerdicts.has(branch.var)) {
558
607
  push(`${(branch.afterStep ?? 0) + 1}번째 단계 뒤의 반복이 검증 결과가 아니라 "${branch.var}"의 내용을 보고 돌지 말지 정합니다.`, {
@@ -569,15 +618,16 @@ function validateBlueprint(bp, ctx = {}) {
569
618
  * 정본은 데스크탑 shared/graph-blueprint.ts — 이 파일은 손복사본이고 패리티 게이트가 지킨다.
570
619
  */
571
620
  const emptinessTestedVars = new Set(
572
- (bp.branches || [])
573
- .filter((branch) => branch.op === "truthy" || branch.op === "falsy")
621
+ branches
622
+ .filter((branch) => branch && typeof branch === "object" && (branch.op === "truthy" || branch.op === "falsy"))
574
623
  .map((branch) => String(branch.var || "").trim())
575
624
  .filter(Boolean),
576
625
  );
577
- for (const check of bp.checks || []) {
626
+ for (const check of checks) {
627
+ if (!check || typeof check !== "object") continue;
578
628
  const subject = String(check.subject || "").trim();
579
629
  if (!subject || !emptinessTestedVars.has(subject)) continue;
580
- const branch = (bp.branches || []).find((b) => String(b.var || "").trim() === subject);
630
+ const branch = branches.find((b) => b && typeof b === "object" && String(b.var || "").trim() === subject);
581
631
  if (!branch) continue;
582
632
  // 위치를 본다 — 갈림길 뒤 값-있는 쪽의 검증은 비는 날 아예 안 돌므로 문제가 아니다.
583
633
  const at = typeof check.afterStep === "number" ? check.afterStep : -1;
@@ -599,7 +649,8 @@ function validateBlueprint(bp, ctx = {}) {
599
649
  );
600
650
  }
601
651
 
602
- for (const branch of bp.branches || []) {
652
+ for (const branch of branches) {
653
+ if (!branch || typeof branch !== "object") continue;
603
654
  const at = `${(branch.afterStep ?? 0) + 1}번째 단계 뒤의 갈림길`;
604
655
  if (!steps[branch.afterStep]) { push(`${at}가 없는 단계를 가리킵니다.`); continue; }
605
656
  if (!OPS.has(branch.op)) { push(`${at}의 판단 방법을 알 수 없습니다.`); continue; }
@@ -35,14 +35,55 @@ const PERSONAL_PATH_RE = /(\/Users\/[^/\s"']+|\/home\/[^/\s"']+|C:\\Users\\[^\\\
35
35
 
36
36
 
37
37
 
38
- function vaultKeyFor(nodeId, key) {
39
- return `${String(key).replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
38
+ function vaultKeyFor(fieldPath) {
39
+ return String(fieldPath).replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
40
40
  }
41
41
 
42
42
  function looksSecretValue(value) {
43
43
  return typeof value === "string" && SECRET_VALUE_PATTERNS.some((re) => re.test(value));
44
44
  }
45
45
 
46
+ function scrubNestedConfigValue(nodeId, fieldPath, value, state) {
47
+ if (Array.isArray(value)) {
48
+ return value.map((item, index) => scrubNestedConfigValue(nodeId, `${fieldPath}[${index}]`, item, state));
49
+ }
50
+ if (!value || typeof value !== "object") return value;
51
+
52
+ const out = {};
53
+ for (const [key, child] of Object.entries(value)) {
54
+ const childPath = fieldPath ? `${fieldPath}.${key}` : key;
55
+ if (SECRET_KEY_RE.test(key)) {
56
+ const vaultKey = vaultKeyFor(childPath);
57
+ out[key] = `\${vault.${vaultKey}}`;
58
+ state.vaultTemplate.push({ key: vaultKey, kind: "secret", requiredBy: [nodeId], sourceField: childPath });
59
+ state.findings.push({ rule: "secret-field", nodeId, field: childPath, action: `templated:${vaultKey}` });
60
+ continue;
61
+ }
62
+ if (looksSecretValue(child)) {
63
+ state.blockers.push({
64
+ nodeId,
65
+ field: childPath,
66
+ reason: `"${childPath}" 값이 자격증명처럼 보입니다. 어떤 키인지 알 수 없어 자동으로 빈칸 처리할 수 없습니다.`,
67
+ nextAction: `이 값을 금고 변수로 바꾼 뒤(예: \${vault.MY_TOKEN}) 다시 내보내세요.`,
68
+ });
69
+ out[key] = child;
70
+ continue;
71
+ }
72
+ if (typeof child === "string") {
73
+ PERSONAL_PATH_RE.lastIndex = 0;
74
+ if (PERSONAL_PATH_RE.test(child)) {
75
+ PERSONAL_PATH_RE.lastIndex = 0;
76
+ out[key] = child.replace(PERSONAL_PATH_RE, "<사용자 폴더>");
77
+ PERSONAL_PATH_RE.lastIndex = 0;
78
+ state.findings.push({ rule: "personal-path", nodeId, field: childPath, action: "removed" });
79
+ continue;
80
+ }
81
+ }
82
+ out[key] = scrubNestedConfigValue(nodeId, childPath, child, state);
83
+ }
84
+ return out;
85
+ }
86
+
46
87
  /**
47
88
  * 노드 설정을 훑어 비밀·모델핀·개인 경로를 처리한다.
48
89
  * 반환: { config, findings, vaultTemplate, blockers }
@@ -55,7 +96,7 @@ function scrubNodeConfig(nodeId, config) {
55
96
  for (const [key, value] of Object.entries(config || {})) {
56
97
  // 1) 비밀로 선언된 칸 — 값 유무와 무관하게 금고 변수로 바꾼다.
57
98
  if (SECRET_KEY_RE.test(key)) {
58
- const vaultKey = vaultKeyFor(nodeId, key);
99
+ const vaultKey = vaultKeyFor(key);
59
100
  out[key] = `$\{vault.${vaultKey}}`;
60
101
  vaultTemplate.push({ key: vaultKey, kind: "secret", requiredBy: [nodeId], sourceField: key });
61
102
  findings.push({ rule: "secret-field", nodeId, field: key, action: `templated:${vaultKey}` });
@@ -86,7 +127,7 @@ function scrubNodeConfig(nodeId, config) {
86
127
  PERSONAL_PATH_RE.lastIndex = 0;
87
128
  continue;
88
129
  }
89
- out[key] = value;
130
+ out[key] = scrubNestedConfigValue(nodeId, key, value, { findings, vaultTemplate, blockers });
90
131
  }
91
132
  return { config: out, findings, vaultTemplate, blockers };
92
133
  }
@@ -134,8 +175,13 @@ function buildPackage(input) {
134
175
  }
135
176
  }
136
177
  const server = node.config?.mcpServer;
137
- if (typeof server === "string" && server && !dependencies.mcp.some((m) => m.serverSlug === server)) {
138
- dependencies.mcp.push({ serverSlug: server, requiredBy: [node.id] });
178
+ if (typeof server === "string" && server) {
179
+ const existingMcp = dependencies.mcp.find((item) => item.serverSlug === server);
180
+ if (existingMcp) {
181
+ if (!existingMcp.requiredBy.includes(node.id)) existingMcp.requiredBy.push(node.id);
182
+ } else {
183
+ dependencies.mcp.push({ serverSlug: server, requiredBy: [node.id] });
184
+ }
139
185
  }
140
186
  }
141
187
 
@@ -210,11 +256,20 @@ function verifyPackage(pkg) {
210
256
  if (!graph || !Array.isArray(graph.nodes) || graph.nodes.length === 0) {
211
257
  problems.push("그래프에 단계가 없습니다.");
212
258
  }
213
- if (manifest?.integrity?.graphDigest && graph) {
214
- if (digestOf(graph) !== manifest.integrity.graphDigest) {
259
+ const integrity = manifest?.integrity;
260
+ if (integrity && typeof integrity.graphDigest === "string" && graph) {
261
+ if (digestOf(graph) !== integrity.graphDigest) {
215
262
  problems.push("그래프 내용이 매니페스트 지문과 다릅니다(전송 중 변형되었을 수 있습니다).");
216
263
  }
217
264
  }
265
+ if (!integrity || typeof integrity.graphDigest !== "string" || typeof integrity.manifestDigest !== "string") {
266
+ problems.push("매니페스트 무결성 지문이 없습니다.");
267
+ } else if (digestOf({
268
+ ...manifest,
269
+ integrity: { graphDigest: integrity.graphDigest, manifestDigest: null },
270
+ }) !== integrity.manifestDigest) {
271
+ problems.push("매니페스트 내용이 자체 지문과 다릅니다(전송 중 변형되었을 수 있습니다).");
272
+ }
218
273
  return problems;
219
274
  }
220
275
 
@@ -22,6 +22,8 @@ const os = require("node:os");
22
22
  const path = require("node:path");
23
23
  const { spawn } = require("node:child_process");
24
24
 
25
+ const MAX_CORE_STDOUT_LINE_BYTES = 16 * 1024 * 1024;
26
+
25
27
  function localCoreBin() {
26
28
  const candidates = [
27
29
  process.env.HEPHAESTUS_BIN,
@@ -66,7 +68,7 @@ function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
66
68
  // 세션에서 `hephaestus mcp serve` 17개 누적. exit 훅으로 반드시 걷는다.
67
69
  const reap = () => { try { child.kill(); } catch { /* already gone */ } };
68
70
  process.once("exit", reap);
69
- let buffer = "";
71
+ let buffer = Buffer.alloc(0);
70
72
  let nextId = 0;
71
73
  let initialized = null;
72
74
  const pending = new Map();
@@ -93,10 +95,21 @@ function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
93
95
  failPending("local_core_transport_error", `the local core input stream failed: ${(error && error.message) || error}`);
94
96
  });
95
97
  child.stdout.on("data", (chunk) => {
96
- buffer += chunk;
98
+ if (exited) return;
99
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
100
+ if (buffer.length + bytes.length > MAX_CORE_STDOUT_LINE_BYTES) {
101
+ failPending(
102
+ "local_core_response_too_large",
103
+ `the local core emitted a response line larger than ${MAX_CORE_STDOUT_LINE_BYTES} bytes`,
104
+ );
105
+ buffer = Buffer.alloc(0);
106
+ try { child.kill(); } catch { /* already gone */ }
107
+ return;
108
+ }
109
+ buffer = buffer.length ? Buffer.concat([buffer, bytes]) : bytes;
97
110
  let index;
98
- while ((index = buffer.indexOf("\n")) >= 0) {
99
- const line = buffer.slice(0, index);
111
+ while ((index = buffer.indexOf(0x0a)) >= 0) {
112
+ const line = buffer.subarray(0, index).toString("utf8");
100
113
  buffer = buffer.slice(index + 1);
101
114
  if (!line.trim()) continue;
102
115
  let message;
@@ -191,4 +204,4 @@ function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
191
204
  return { call, close };
192
205
  }
193
206
 
194
- module.exports = { createLocalCoreClient, localCoreBin };
207
+ module.exports = { createLocalCoreClient, localCoreBin, MAX_CORE_STDOUT_LINE_BYTES };
@@ -10,10 +10,8 @@
10
10
  *
11
11
  * v2 이식 규칙:
12
12
  * - D.out → ctx.out, D.prefsLang() → ctx.lang.
13
- * - D.runCwd — v1 검증 결과: AGENTLAS_RUN_CWD 같은 env는 v1에 존재하지 않았다.
14
- * v1 runCwd()의 실제 동작 = userDataDir()/agent-cwd 샌드박스(mkdir, 실패 시
15
- * homedir 폴백). 그 동작을 그대로 보존해 여기 로컬로 이식했다. (프로젝트
16
- * 실행 위치가 필요한 곳은 v1과 동일하게 projectCwd()를 쓴다.)
13
+ * - D.runCwd — Terminal 정본(project/paths.cjs)의 private agent-cwd 경계를 쓴다.
14
+ * 생성 실패 폴더로 권한을 넓히지 않는다.
17
15
  * - 런타임 발견은 engine/agentlas-core-harness.cjs(복원본)를 소비만 한다.
18
16
  * - 런타임 부재 = 정직 정지(exit 1) + v1 설치 안내 문구 그대로. 폴백 금지.
19
17
  *
@@ -30,8 +28,10 @@ const { Ui } = require("../agentlas-ui.cjs");
30
28
  const { truncateWidth, visWidth, wrapWidth } = require("../ui/width.cjs");
31
29
  const coreHarness = require("../agentlas-core-harness.cjs");
32
30
  const { userDataDir } = require("../core/paths.cjs");
31
+ const { runCwd, projectCwd } = require("../project/paths.cjs");
33
32
 
34
33
  const { CONTEXT_MAP_MIN_CORE_VERSION, resolveContextMapCoreRoot } = coreHarness;
34
+ const CAPTURE_KILL_GRACE_MS = 250;
35
35
 
36
36
  // ── 명령 usage 문자열 (v1 TOP_LEVEL_COMMAND_USAGE에서 hephaestus 클러스터만 발췌) ──
37
37
  const USAGE = Object.freeze({
@@ -47,7 +47,7 @@ const USAGE = Object.freeze({
47
47
  hephaestus: "usage: agentlas hephaestus <subcommand> [args]",
48
48
  journal: "usage: agentlas journal <status|verify|repair|gate> --run-id <id> | --journal <path>",
49
49
  netadmin: "usage: agentlas netadmin <init|status|reindex|bench|add-source> [args]",
50
- research: "usage: agentlas research <status|gather|search|read|plan> [args]",
50
+ research: "usage: agentlas research <subcommand> [args] (full list: agentlas research --help)",
51
51
  route: 'usage: agentlas route "<request>" [--json]',
52
52
  });
53
53
 
@@ -66,6 +66,13 @@ function isHelpToken(value) {
66
66
  return value === "help" || value === "--help" || value === "-h";
67
67
  }
68
68
 
69
+ function isHelpRequest(args) {
70
+ if (!Array.isArray(args) || !args.length) return false;
71
+ if (args.length === 1) return isHelpToken(args[0]);
72
+ if (args.length === 2 && args[1] === "help") return true;
73
+ return args.some((value) => value === "--help" || value === "-h");
74
+ }
75
+
69
76
  // ── 라우팅/리서치 미리보기 모델 (v1 module-level 순수 함수, 그대로) ──
70
77
  function routePreviewModel(value) {
71
78
  const json = value && typeof value === "object" ? value : {};
@@ -204,31 +211,6 @@ function create(ctx, deps = {}) {
204
211
  return new Ui({ lang: uiLang || lang() });
205
212
  }
206
213
 
207
- // v1 agentlas.cjs runCwd() 그대로: 에이전트 전용 안전 샌드박스 폴더.
208
- // (검증: v1에 AGENTLAS_RUN_CWD env는 없었다 — process.cwd() 매핑이 아니라
209
- // 이 샌드박스가 원래 동작이므로 그대로 보존한다.)
210
- function runCwd() {
211
- const dir = path.join(userDataDir(), "agent-cwd");
212
- try {
213
- fs.mkdirSync(dir, { recursive: true });
214
- return dir;
215
- } catch {
216
- return os.homedir();
217
- }
218
- }
219
-
220
- // v1: 에이전트가 실제로 실행될 작업 폴더 = 사용자가 명령을 친 현재 디렉터리.
221
- // 단, home/userData/agent-cwd 같은 "프로젝트 아님" 위치면 샌드박스로 폴백.
222
- function projectCwd() {
223
- try {
224
- const cwd = process.cwd();
225
- if (!cwd || cwd === os.homedir() || cwd === userDataDir() || cwd === runCwd()) return runCwd();
226
- return cwd;
227
- } catch {
228
- return runCwd();
229
- }
230
- }
231
-
232
214
  // ── Hephaestus 런타임 해석 (설치 런처 우선, 앱 번들 폴백) ──
233
215
  // v1 후보 순서 그대로: HEPHAESTUS_BIN → ~/.agentlas/runtime/current/bin/hephaestus
234
216
  // → Core python 모듈(resolveCoreRuntimeRoot). win32는 bin 후보를 건너뛴다(v1 동일).
@@ -300,29 +282,44 @@ function create(ctx, deps = {}) {
300
282
  let overflow = false;
301
283
  let timedOut = false;
302
284
  let settled = false;
285
+ let forceKillTimer = null;
286
+ const terminate = () => {
287
+ if (settled) return;
288
+ try { child.kill("SIGTERM"); } catch { /* already gone */ }
289
+ if (forceKillTimer) return;
290
+ forceKillTimer = setTimeout(() => {
291
+ forceKillTimer = null;
292
+ if (!settled) {
293
+ try { child.kill("SIGKILL"); } catch { /* already gone */ }
294
+ }
295
+ }, CAPTURE_KILL_GRACE_MS);
296
+ if (forceKillTimer.unref) forceKillTimer.unref();
297
+ };
303
298
  const append = (target, chunk) => {
304
299
  if (overflow) return;
305
300
  bytes += chunk.length;
306
301
  if (bytes > maxBytes) {
307
302
  overflow = true;
308
- child.kill("SIGTERM");
303
+ terminate();
309
304
  return;
310
305
  }
311
306
  target.push(chunk);
312
307
  };
313
308
  child.stdout.on("data", (chunk) => append(stdout, Buffer.from(chunk)));
314
309
  child.stderr.on("data", (chunk) => append(stderr, Buffer.from(chunk)));
315
- const onInterrupt = () => child.kill("SIGINT");
310
+ const onInterrupt = () => terminate();
316
311
  process.once("SIGINT", onInterrupt);
317
312
  const timer = setTimeout(() => {
318
313
  timedOut = true;
319
- child.kill("SIGTERM");
314
+ terminate();
320
315
  }, timeoutMs);
321
316
  if (timer.unref) timer.unref();
322
317
  const done = (code, error) => {
323
318
  if (settled) return;
324
319
  settled = true;
325
320
  clearTimeout(timer);
321
+ if (forceKillTimer) clearTimeout(forceKillTimer);
322
+ forceKillTimer = null;
326
323
  process.removeListener("SIGINT", onInterrupt);
327
324
  resolve({
328
325
  code: overflow || timedOut || error ? 1 : (code ?? 0),
@@ -426,14 +423,15 @@ function create(ctx, deps = {}) {
426
423
  ui.line(" " + (index === 0 ? ui.c.paw("✗ ") : " ") + ui.c.text(line));
427
424
  });
428
425
  };
429
- if (json) renderRoutePreview(json, ui);
426
+ if (result.overflow || result.timedOut) {
427
+ if (result.overflow) renderError(ui.lang === "ko" ? "라우팅 출력이 2MB 제한을 넘었습니다." : "Route output exceeded the 2 MB limit.");
428
+ if (result.timedOut) renderError(ui.lang === "ko" ? "라우팅이 30초 제한을 넘었습니다." : "Route timed out after 30 seconds.");
429
+ } else if (json) renderRoutePreview(json, ui);
430
430
  else {
431
431
  const detail = (result.stderr || result.stdout || "").trim();
432
432
  const message = detail || (ui.lang === "ko" ? "라우팅 결과를 읽지 못했습니다." : "Could not read the route result.");
433
433
  renderError(message);
434
434
  }
435
- if (result.overflow) renderError(ui.lang === "ko" ? "라우팅 출력이 2MB 제한을 넘었습니다." : "Route output exceeded the 2 MB limit.");
436
- if (result.timedOut) renderError(ui.lang === "ko" ? "라우팅이 30초 제한을 넘었습니다." : "Route timed out after 30 seconds.");
437
435
  return result.code;
438
436
  }
439
437
 
@@ -551,7 +549,7 @@ function create(ctx, deps = {}) {
551
549
  }
552
550
 
553
551
  async function runHephaestusResearch(args, opts = {}) {
554
- if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "help")) {
552
+ if (isHelpRequest(args)) {
555
553
  return runHephaestusInteractive(args, { ...opts, human: false });
556
554
  }
557
555
  const raw = args.includes("--json");
@@ -573,8 +571,13 @@ function create(ctx, deps = {}) {
573
571
  const preview = json ? researchPreviewModel(json) : null;
574
572
  const code = result.code !== 0 || preview?.totalEvidenceFailure ? 1 : 0;
575
573
  if (raw) {
576
- if (result.stdout) process.stdout.write(result.stdout);
577
- if (result.stderr) process.stderr.write(result.stderr);
574
+ if (!result.overflow && !result.timedOut) {
575
+ if (result.stdout) process.stdout.write(result.stdout);
576
+ if (result.stderr) process.stderr.write(result.stderr);
577
+ } else {
578
+ if (result.overflow) process.stderr.write("Research output exceeded the 2 MB limit.\n");
579
+ if (result.timedOut) process.stderr.write("Research timed out after 60 seconds.\n");
580
+ }
578
581
  return code;
579
582
  }
580
583
  const renderError = (message) => {
@@ -583,15 +586,16 @@ function create(ctx, deps = {}) {
583
586
  ui.line(" " + (index === 0 ? ui.c.paw("✗ ") : " ") + ui.c.text(line));
584
587
  });
585
588
  };
586
- if (json) {
589
+ if (result.overflow || result.timedOut) {
590
+ if (result.overflow) renderError(ui.lang === "ko" ? "리서치 출력이 2MB 제한을 넘었습니다." : "Research output exceeded the 2 MB limit.");
591
+ if (result.timedOut) renderError(ui.lang === "ko" ? "리서치가 60초 제한을 넘었습니다." : "Research timed out after 60 seconds.");
592
+ } else if (json) {
587
593
  renderResearchPreview(json, ui);
588
594
  } else {
589
595
  const message = (result.stderr || result.stdout || "").trim()
590
596
  || (ui.lang === "ko" ? "리서치 결과를 읽지 못했습니다." : "Could not read the research result.");
591
597
  renderError(message);
592
598
  }
593
- if (result.overflow) renderError(ui.lang === "ko" ? "리서치 출력이 2MB 제한을 넘었습니다." : "Research output exceeded the 2 MB limit.");
594
- if (result.timedOut) renderError(ui.lang === "ko" ? "리서치가 60초 제한을 넘었습니다." : "Research timed out after 60 seconds.");
595
599
  return code;
596
600
  }
597
601
 
@@ -638,7 +642,7 @@ function create(ctx, deps = {}) {
638
642
  }
639
643
  return Promise.resolve(1);
640
644
  }
641
- const helpOnly = args.some((arg) => arg === "--help" || arg === "-h" || arg === "help");
645
+ const helpOnly = isHelpRequest(args);
642
646
  const cwd = opts.cwd || (helpOnly ? process.cwd() : runCwd());
643
647
  const moduleName = found.kind === "python" && isCareerGraph ? "career_graph" : "agentlas_cloud";
644
648
  const moduleArgs = moduleName === "career_graph" ? args.slice(1) : args;
@@ -744,5 +748,6 @@ module.exports = {
744
748
  USAGE,
745
749
  usageFor,
746
750
  isHelpToken,
751
+ isHelpRequest,
747
752
  CONTEXT_MAP_MIN_CORE_VERSION,
748
753
  };