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
@@ -21,9 +21,9 @@ const { captureCoreJsonSync, resolveContextMapCoreRoot } = require("../agentlas-
21
21
  const terminalMemoryGovernance = require("../agentlas-memory-governance.cjs");
22
22
  const terminalExperienceIntake = require("../agentlas-experience-intake.cjs");
23
23
  const terminalExperienceExchange = require("../agentlas-experience-exchange.cjs");
24
+ const permissions = require("../agentlas-permissions.cjs");
24
25
  const { routesMap } = require("../agents/routes.cjs");
25
26
  const { agentFolder } = require("../agents/files.cjs");
26
- const { ensureProjectMemoryCli } = require("./seed.cjs");
27
27
  const { projectCwd } = require("./paths.cjs");
28
28
 
29
29
  const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[A-Za-z0-9]{20,}/, /xox[baprs]-[A-Za-z0-9-]{10,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i];
@@ -50,16 +50,7 @@ function langDirective(lang) {
50
50
  }
51
51
 
52
52
  function logCli(projectPath, rec) {
53
- if (!projectPath) return;
54
- try {
55
- // 0.9.10 경계: 초기화되지 않은 프로젝트에 시드를 만들지 않는다. 로그는
56
- // 이미 존재하는 .agentlas/ 에만 덧붙인다 (seed는 project init 전용).
57
- const { initializedAgentlasProjectPathCli } = require("./state.cjs");
58
- if (!initializedAgentlasProjectPathCli(projectPath)) return;
59
- const dir = ensureProjectMemoryCli(projectPath);
60
- if (!dir) return;
61
- fs.appendFileSync(path.join(dir, loadArch().logFile || "memory-log.jsonl"), JSON.stringify(rec) + "\n", "utf8");
62
- } catch { /* ignore */ }
53
+ return require("../memory-cli/curate.cjs").logCli(projectPath, rec);
63
54
  }
64
55
 
65
56
  function coerceText(v, max) {
@@ -242,51 +233,7 @@ function parseMemoryEventsCli(text) {
242
233
  }
243
234
 
244
235
  function curateCliReply(db, text, ctx) {
245
- const { events, cleaned } = parseMemoryEventsCli(text);
246
- const style = require("../agentlas-style.cjs");
247
- // read 권한: 어떤 durable write도 없이 숨김 메타데이터만 제거해 돌려준다.
248
- if (ctx && ctx.permission === "read") return style.sanitizeAssistantText(cleaned);
249
- if (!events.length || !tableExists(db, "memory_entries")) return style.sanitizeAssistantText(cleaned);
250
- ensureMemoryContextColumn(db);
251
- const arch = loadArch();
252
- const { randomUUID } = require("node:crypto");
253
- const now = new Date().toISOString();
254
- const rememberCurated = (memory) => {
255
- if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
256
- if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
257
- };
258
- const kinds = Array.isArray(arch.kinds) ? arch.kinds : [];
259
- const scopes = Array.isArray(arch.scopes) ? arch.scopes : [];
260
- for (const ev of events) {
261
- const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
262
- if (!content) continue;
263
- if (ev.sensitivity === "secret" || SECRET_RE.some((re) => re.test(content))) continue;
264
- const kind = kinds.includes(ev.memory_kind) ? ev.memory_kind : "fact";
265
- let scope = ev.suggested_scope === "agent_team"
266
- ? "team_memory"
267
- : scopes.includes(ev.suggested_scope) ? ev.suggested_scope : "session";
268
- const kindAllowsUserIdentity = ["fact", "decision", "preference", "procedure"].includes(kind);
269
- if (scope === "user_identity" && (ev.confidence !== "high" || !kindAllowsUserIdentity)) scope = "session";
270
- if (scope === "discard" || scope === "session") { logCli(ctx.projectPath, { action: scope, kind, content, at: now }); continue; }
271
- if (scope === "project" && !ctx.projectPath) scope = "team_memory";
272
- const ppath = scope === "project" ? ctx.projectPath : null;
273
- const scopedAgentId = scope === "agent_repo" ? (ctx.agentId || null) : null;
274
- const requestContext = normalizeRequestContext(ev, ctx, ppath);
275
- try {
276
- const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) AND (agent_id IS ? OR agent_id=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath, scopedAgentId, scopedAgentId);
277
- if (dup) {
278
- rememberCurated({ ...dup, requestContext });
279
- continue;
280
- }
281
- const memoryId = randomUUID();
282
- const confidence = ev.confidence || "medium";
283
- const sensitivity = ev.sensitivity || "internal";
284
- db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, scopedAgentId, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
285
- rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
286
- logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
287
- } catch { /* ignore */ }
288
- }
289
- return style.sanitizeAssistantText(cleaned);
236
+ return require("../memory-cli/curate.cjs").curateCliReply(db, text, ctx);
290
237
  }
291
238
 
292
239
  const TERMINAL_MEMORY_CORE_MAX_TOKENS = 150;
@@ -382,9 +329,16 @@ function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
382
329
  const packageHash = /^[a-f0-9]{64}$/.test(rawHash) ? `sha256:${rawHash}` : null;
383
330
  const explicitDefinition = String(runtimeExperience?.agentDefinitionId || "");
384
331
  const explicitRelease = String(runtimeExperience?.baseAgentReleaseId || "");
332
+ const hasExplicitBinding = !!runtimeExperience && (
333
+ Object.prototype.hasOwnProperty.call(runtimeExperience, "agentDefinitionId") ||
334
+ Object.prototype.hasOwnProperty.call(runtimeExperience, "baseAgentReleaseId")
335
+ );
385
336
  if (portableId.test(explicitDefinition) && portableId.test(explicitRelease)) {
386
337
  return { agentDefinitionId: explicitDefinition, agentReleaseId: explicitRelease, packageHash, authority: "explicit-runtime-binding" };
387
338
  }
339
+ // A partially supplied runtime binding is an attempted exact authority, not
340
+ // permission to fall through to an unrelated installed/local identity.
341
+ if (hasExplicitBinding) return null;
388
342
  if (binding && portableId.test(String(binding.agent_definition_id)) && portableId.test(String(binding.agent_release_id))) {
389
343
  return { agentDefinitionId: binding.agent_definition_id, agentReleaseId: binding.agent_release_id, packageHash, authority: "installed-hub-binding" };
390
344
  }
@@ -400,7 +354,7 @@ function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
400
354
  }
401
355
 
402
356
  function finalizeExperienceExecutionCli(db, input) {
403
- if (input.permission === "read") return null;
357
+ if (permissions.normalize(input.permission) === "read") return null;
404
358
  if (!input.agentId) return null;
405
359
  let agent;
406
360
  try { agent = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(input.agentId); }
@@ -20,23 +20,31 @@ const { initializedAgentlasProjectPathCli } = require("./state.cjs");
20
20
 
21
21
  const ONTOLOGY_SUPPORTED_EXTS = new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv"]);
22
22
 
23
- /** `--key value` / `--flag` 파서 v1 parseCloudFlags와 동일 규칙의 로컬 복사본. */
24
- function parseFlagsCli(args) {
23
+ /** Ontology source registration options. Unknown/duplicate flags fail closed. */
24
+ function parseFlagsCli(args, schema = {}) {
25
25
  const flags = { _: [] };
26
+ const values = new Set(schema.values || ["kind", "scope"]);
27
+ const booleans = new Set(schema.booleans || []);
28
+ let passthrough = false;
26
29
  for (let i = 0; i < args.length; i++) {
27
- const a = args[i];
28
- if (a && a.startsWith("--")) {
29
- const key = a.slice(2);
30
- const next = args[i + 1];
31
- if (next !== undefined && !String(next).startsWith("--")) {
32
- flags[key] = next;
33
- i++;
34
- } else {
35
- flags[key] = true;
36
- }
37
- } else {
38
- flags._.push(a);
30
+ const token = String(args[i]);
31
+ if (passthrough) { flags._.push(token); continue; }
32
+ if (token === "--") { passthrough = true; continue; }
33
+ if (!token.startsWith("--")) { flags._.push(token); continue; }
34
+ const at = token.indexOf("=");
35
+ const key = token.slice(2, at > 2 ? at : undefined);
36
+ if (!values.has(key) && !booleans.has(key)) throw new Error(`unknown option: --${key}`);
37
+ if (Object.prototype.hasOwnProperty.call(flags, key)) throw new Error(`duplicate option: --${key}`);
38
+ if (booleans.has(key)) {
39
+ if (at > 2) throw new Error(`--${key} does not take a value`);
40
+ flags[key] = true;
41
+ continue;
42
+ }
43
+ const value = at > 2 ? token.slice(at + 1) : args[++i];
44
+ if (value === undefined || value === "" || (at <= 2 && String(value).startsWith("--"))) {
45
+ throw new Error(`--${key} requires a value`);
39
46
  }
47
+ flags[key] = String(value);
40
48
  }
41
49
  return flags;
42
50
  }
@@ -233,18 +241,24 @@ function resolveOntologyPathCli(value, cwd) {
233
241
  function inferOntologyKindCli(value, text) {
234
242
  const v = String(value || "").toLowerCase();
235
243
  const hay = String(text || "").toLowerCase();
236
- if (["company", "work", "business", "corp", "team", "회사", "업무", "팀", "조직"].includes(v) || /(company|work|business|corp|team|회사|업무|조직)/i.test(hay)) return "company";
237
- if (["personal", "private-life", "life", "me", "개인", "내자료", "일상"].includes(v) || /(personal|private-life|\bme\b|개인|내\s*자료|일상)/i.test(hay)) return "personal";
238
- if (["project", "repo", "프로젝트", "레포"].includes(v) || /(project|repo|프로젝트|레포)/i.test(hay)) return "project";
244
+ if (["company", "work", "business", "corp", "team", "회사", "업무", "팀", "조직"].includes(v)) return "company";
245
+ if (["personal", "private-life", "life", "me", "개인", "내자료", "일상"].includes(v)) return "personal";
246
+ if (["project", "repo", "프로젝트", "레포"].includes(v)) return "project";
247
+ if (/(company|work|business|corp|team|회사|업무|조직)/i.test(hay)) return "company";
248
+ if (/(personal|private-life|\bme\b|개인|내\s*자료|일상)/i.test(hay)) return "personal";
249
+ if (/(project|repo|프로젝트|레포)/i.test(hay)) return "project";
239
250
  return "project";
240
251
  }
241
252
 
242
253
  function inferOntologyScopeCli(value, text, kind) {
243
254
  const v = String(value || "").toLowerCase();
244
255
  const hay = String(text || "").toLowerCase();
245
- if (["public", "open", "공개"].includes(v) || /(public|open|공개)/i.test(hay)) return "public";
246
- if (["internal", "team", "내부", "팀"].includes(v) || /(internal|team-only|company-wide|내부|팀\s*공유|회사\s*공유)/i.test(hay)) return "internal";
247
- if (["private", "secret", "local", "비공개", "개인"].includes(v) || /(private|secret|local-only|비공개|개인만|나만)/i.test(hay)) return "private";
256
+ if (["public", "open", "공개"].includes(v)) return "public";
257
+ if (["internal", "team", "내부", "팀"].includes(v)) return "internal";
258
+ if (["private", "secret", "local", "비공개", "개인"].includes(v)) return "private";
259
+ if (/(public|open|공개)/i.test(hay)) return "public";
260
+ if (/(internal|team-only|company-wide|내부|팀\s*공유|회사\s*공유)/i.test(hay)) return "internal";
261
+ if (/(private|secret|local-only|비공개|개인만|나만)/i.test(hay)) return "private";
248
262
  return kind === "company" || kind === "personal" ? "private" : "private";
249
263
  }
250
264
 
@@ -366,7 +380,7 @@ function formatOntologyStatusCli(paths, lang) {
366
380
  lines.push("", `${ko ? "소스" : "Sources"} (${sources.length}):`);
367
381
  for (const source of sources) {
368
382
  const sourcePath = path.resolve(String(source.path || ""));
369
- lines.push(` ${fs.existsSync(sourcePath) ? "✓" : "!"} ${sourcePath} ${source.kind || "project"} / ${source.scope || "internal"}`);
383
+ lines.push(` ${fs.existsSync(sourcePath) ? "✓" : "!"} ${sourcePath} ${source.kind || "project"} / ${source.scope || "private"}`);
370
384
  }
371
385
  if (!sources.length) lines.push(ko ? " (없음)" : " (none)");
372
386
  lines.push(
@@ -438,6 +452,7 @@ async function runOntologyCli(args, opts) {
438
452
  const sub = normalizedArgs[0] || "status";
439
453
  const passivePaths = ontologyPathsForCli(projectPath);
440
454
  if (sub === "status" || sub === "list") {
455
+ if (normalizedArgs.length > 1) throw new Error(`usage: agentlas ontology ${sub}`);
441
456
  if (!initializedAgentlasProjectPathCli(projectPath)) {
442
457
  return [
443
458
  ko ? "온톨로지: 초기화되지 않음" : "Ontology: not initialized",
@@ -448,7 +463,10 @@ async function runOntologyCli(args, opts) {
448
463
  }
449
464
  return formatOntologyStatusCli(passivePaths, opts.lang);
450
465
  }
451
- if (sub === "help" || sub === "--help" || sub === "-h") return ontologyUsageLinesCli(opts.lang);
466
+ if (sub === "help" || sub === "--help" || sub === "-h") {
467
+ if (normalizedArgs.length > 1) throw new Error("usage: agentlas ontology help");
468
+ return ontologyUsageLinesCli(opts.lang);
469
+ }
452
470
  const directOntologyCommand = ["open", "add", "company", "personal", "project"].includes(String(sub).toLowerCase())
453
471
  || isOntologyPathishCli(sub, cwd, true);
454
472
  if (!directOntologyCommand) {
@@ -456,6 +474,22 @@ async function runOntologyCli(args, opts) {
456
474
  const parsed = await parseOntologyNaturalArgsCli(normalizedArgs.join(" "), cwd);
457
475
  return runOntologyCli(parsed, opts);
458
476
  }
477
+ let directFlags = null;
478
+ if (sub === "open") {
479
+ if (normalizedArgs.length !== 1) throw new Error("usage: agentlas ontology open");
480
+ } else if (sub === "add") {
481
+ directFlags = parseFlagsCli(normalizedArgs.slice(1));
482
+ if (directFlags._.length < 1 || directFlags._.length > 3) throw new Error("usage: agentlas ontology add <path> [kind] [scope] [--kind <kind>] [--scope <scope>]");
483
+ if (directFlags.kind !== undefined && directFlags._[1] !== undefined) throw new Error("kind was provided twice; use either the positional value or --kind");
484
+ if (directFlags.scope !== undefined && directFlags._[2] !== undefined) throw new Error("scope was provided twice; use either the positional value or --scope");
485
+ } else if (["company", "personal", "project"].includes(String(sub).toLowerCase())) {
486
+ directFlags = parseFlagsCli(normalizedArgs.slice(1), { values: ["scope"] });
487
+ if (directFlags._.length < 1 || directFlags._.length > 2) throw new Error(`usage: agentlas ontology ${sub} <path> [scope] [--scope <scope>]`);
488
+ if (directFlags.scope !== undefined && directFlags._[1] !== undefined) throw new Error("scope was provided twice; use either the positional value or --scope");
489
+ } else if (isOntologyPathishCli(sub, cwd, true)) {
490
+ directFlags = parseFlagsCli(normalizedArgs.slice(1));
491
+ if (directFlags._.length) throw new Error("unexpected positional argument after ontology source path");
492
+ }
459
493
  const paths = ensureOntologyCli(projectPath, opts.lang);
460
494
  if (sub === "open") {
461
495
  const opened = opts.noOpen ? true : openLocalPathCli(paths.inboxPath, opts.notify);
@@ -464,20 +498,27 @@ async function runOntologyCli(args, opts) {
464
498
  : (ko ? "온톨로지 수신함을 자동으로 열지 못했습니다. 직접 여세요" : "Could not open ontology inbox automatically; open it manually")}: ${paths.inboxPath}`];
465
499
  }
466
500
  if (sub === "add") {
467
- const flags = parseFlagsCli(normalizedArgs.slice(1));
501
+ const flags = directFlags;
468
502
  const source = flags._[0];
469
503
  const kind = inferOntologyKindCli(flags.kind || flags._[1], normalizedArgs.join(" "));
470
504
  const scope = inferOntologyScopeCli(flags.scope || flags._[2], normalizedArgs.join(" "), kind);
471
505
  return registerOntologySourceCli(paths, source, kind, scope, cwd, opts.lang);
472
506
  }
473
507
  if (["company", "personal", "project"].includes(String(sub).toLowerCase())) {
474
- const flags = parseFlagsCli(normalizedArgs.slice(1));
508
+ const flags = directFlags;
475
509
  const kind = inferOntologyKindCli(sub, normalizedArgs.join(" "));
476
510
  const scope = inferOntologyScopeCli(flags.scope || flags._[1], normalizedArgs.join(" "), kind);
477
511
  return registerOntologySourceCli(paths, flags._[0], kind, scope, cwd, opts.lang);
478
512
  }
479
513
  if (isOntologyPathishCli(sub, cwd, true)) {
480
- return registerOntologySourceCli(paths, sub, inferOntologyKindCli(null, normalizedArgs.join(" ")), inferOntologyScopeCli(null, normalizedArgs.join(" "), "project"), cwd, opts.lang);
514
+ return registerOntologySourceCli(
515
+ paths,
516
+ sub,
517
+ inferOntologyKindCli(directFlags.kind, normalizedArgs.join(" ")),
518
+ inferOntologyScopeCli(directFlags.scope, normalizedArgs.join(" "), directFlags.kind || "project"),
519
+ cwd,
520
+ opts.lang,
521
+ );
481
522
  }
482
523
  throw new Error(ko
483
524
  ? "사용법: /ontology status|list|open|add <경로>"
@@ -2,23 +2,50 @@
2
2
  /*
3
3
  * project/paths — 프로젝트 경계 판정용 경로 헬퍼 (v1 runCwd/projectCwd 포팅).
4
4
  *
5
- * workforce/capture.cjs에도 같은 규칙의 projectCwd가 있지만 기능 디렉터리
6
- * 수평 의존을 만들지 않기 위해 여기 별도 구현을 둔다. 규칙이 바뀌면 둘 다 함께
7
- * 바꿔야 한다 (v1 monolith 9961–9981 라인이 원본 계약).
5
+ * 파일이 Terminal 전체의 단일 정본이다. Workforce와 Hephaestus가 각자 복제하면
6
+ * 폴백 보안 경계가 갈리므로 여기의 함수를 그대로 export한다.
8
7
  */
9
8
  const fs = require("node:fs");
10
9
  const os = require("node:os");
11
10
  const path = require("node:path");
12
11
  const { userDataDir } = require("../core/paths.cjs");
13
12
 
13
+ let temporaryRunCwd = null;
14
+
15
+ function ensurePrivateDirectory(directory) {
16
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
17
+ const stat = fs.lstatSync(directory);
18
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
19
+ throw new Error("agent run folder is not a private directory");
20
+ }
21
+ if (process.platform !== "win32") {
22
+ fs.chmodSync(directory, 0o700);
23
+ if ((fs.statSync(directory).mode & 0o777) !== 0o700) {
24
+ throw new Error("agent run folder permissions could not be restricted");
25
+ }
26
+ }
27
+ return fs.realpathSync.native(directory);
28
+ }
29
+
14
30
  /** 에이전트 격리 실행용 전용 폴더 — "프로젝트 아님" 위치의 안전한 폴백. */
15
31
  function runCwd() {
16
32
  const dir = path.join(userDataDir(), "agent-cwd");
17
33
  try {
18
- fs.mkdirSync(dir, { recursive: true });
19
- return dir;
20
- } catch {
21
- return os.homedir();
34
+ return ensurePrivateDirectory(dir);
35
+ } catch (primaryError) {
36
+ try {
37
+ if (!temporaryRunCwd) {
38
+ temporaryRunCwd = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-agent-cwd-"));
39
+ }
40
+ return ensurePrivateDirectory(temporaryRunCwd);
41
+ } catch (fallbackError) {
42
+ const error = new Error(
43
+ "Agentlas could not create a private agent working directory; refusing to use the home folder.",
44
+ );
45
+ error.code = "AGENTLAS_RUN_CWD_UNAVAILABLE";
46
+ error.cause = fallbackError || primaryError;
47
+ throw error;
48
+ }
22
49
  }
23
50
  }
24
51
 
@@ -29,7 +56,14 @@ function runCwd() {
29
56
  function projectCwd() {
30
57
  try {
31
58
  const cwd = process.cwd();
32
- if (!cwd || cwd === os.homedir() || cwd === userDataDir() || cwd === runCwd()) return runCwd();
59
+ const neutral = runCwd();
60
+ const canonical = cwd ? fs.realpathSync.native(cwd) : "";
61
+ const home = fs.realpathSync.native(os.homedir());
62
+ let data = path.resolve(userDataDir());
63
+ try { data = fs.realpathSync.native(userDataDir()); } catch { /* compare the unresolved configured path */ }
64
+ if (!canonical || canonical === home || canonical === data || canonical === neutral || path.parse(canonical).root === canonical) {
65
+ return neutral;
66
+ }
33
67
  return cwd;
34
68
  } catch {
35
69
  return runCwd();
@@ -60,34 +60,83 @@ function removeLegacySuperOntologyFiles(dir) {
60
60
  }
61
61
  }
62
62
 
63
+ function privateSeedName(value, fallback) {
64
+ const name = String(value || fallback || "").trim();
65
+ if (!name || name !== path.basename(name) || name === "." || name === ".." || name.length > 180) {
66
+ throw new Error("Agentlas project seed contains an unsafe file name");
67
+ }
68
+ return name;
69
+ }
70
+
71
+ function ensurePrivateDirectory(dir) {
72
+ try { fs.mkdirSync(dir, { recursive: false, mode: 0o700 }); }
73
+ catch (error) { if (!error || error.code !== "EEXIST") throw error; }
74
+ const listed = fs.lstatSync(dir);
75
+ if (!listed.isDirectory() || listed.isSymbolicLink()) {
76
+ throw new Error("Agentlas project state path must be a real directory");
77
+ }
78
+ if (process.platform !== "win32") fs.chmodSync(dir, 0o700);
79
+ return dir;
80
+ }
81
+
82
+ function ensurePrivateSeedFile(file, content) {
83
+ let fd = null;
84
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
85
+ try {
86
+ try {
87
+ fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
88
+ const bytes = Buffer.from(String(content), "utf8");
89
+ const written = fs.writeSync(fd, bytes, 0, bytes.length, null);
90
+ if (written !== bytes.length) throw new Error("Agentlas project seed write was incomplete");
91
+ fs.fsyncSync(fd);
92
+ } catch (error) {
93
+ if (!error || error.code !== "EEXIST") throw error;
94
+ if (fd != null) { try { fs.closeSync(fd); } catch { /* ignore */ } fd = null; }
95
+ fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
96
+ }
97
+ const opened = fs.fstatSync(fd);
98
+ const listed = fs.lstatSync(file);
99
+ if (
100
+ !opened.isFile() || opened.nlink !== 1 ||
101
+ !listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1 ||
102
+ opened.dev !== listed.dev || opened.ino !== listed.ino
103
+ ) throw new Error("Agentlas project seed target must be a single-link regular file");
104
+ try { fs.fchmodSync(fd, 0o600); } catch { /* Windows/ACL-only host */ }
105
+ return file;
106
+ } finally {
107
+ if (fd != null) try { fs.closeSync(fd); } catch { /* ignore */ }
108
+ }
109
+ }
110
+
63
111
  function ensureProjectMemoryCli(projectPath, projectName) {
64
112
  const arch = loadArch();
65
113
  try {
66
- const dir = path.join(projectPath, arch.memoryDir || ".agentlas");
67
- fs.mkdirSync(dir, { recursive: true });
114
+ const root = path.resolve(projectPath);
115
+ const canonicalRoot = fs.realpathSync.native(root);
116
+ const rootStat = fs.lstatSync(root);
117
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || path.parse(canonicalRoot).root === canonicalRoot) return null;
118
+ const memoryDirName = privateSeedName(arch.memoryDir, ".agentlas");
119
+ const dir = ensurePrivateDirectory(path.join(root, memoryDirName));
68
120
  removeLegacySuperOntologyFiles(dir);
69
- const name = projectName || path.basename(projectPath) || "Project";
70
- ensureLocalCredentialStoreCli(projectPath, name, arch);
71
- ensureSoulCredentialIndexCli(projectPath, name, arch);
72
- const sitemap = path.join(dir, arch.sitemapFile || "sitemap.json");
73
- if (!fs.existsSync(sitemap)) {
74
- const now = new Date().toISOString();
75
- fs.writeFileSync(sitemap, JSON.stringify({ project: name, created_at: now, updated_at: now, nodes: [] }, null, 2), "utf8");
76
- }
77
- const skillRegistryFile = arch.skillRegistryFile || "skill-registry.json";
78
- const skillTrialsFile = arch.skillTrialsFile || "skill-trials.jsonl";
79
- const curatorDecisionsFile = arch.curatorDecisionsFile || "curator-decisions.jsonl";
80
- const ontologyRuntimeFile = arch.ontologyRuntimeFile || "ontology-runtime.json";
81
- const ontologySourceManifestFile = arch.ontologySourceManifestFile || "ontology-sources.json";
82
- const ontologyInboxDir = arch.ontologyInboxDir || "ontology-inbox";
83
- const ontologyDbFile = arch.ontologyDbFile || "ontology-runtime.sqlite";
84
- const careerGraphConfigFile = arch.careerGraphConfigFile || "career-graph.json";
85
- const careerGraphSourceManifestFile = arch.careerGraphSourceManifestFile || "career-graph-sources.json";
86
- const careerGraphInboxDir = arch.careerGraphInboxDir || "career-graph-inbox";
87
- const careerGraphDbFile = arch.careerGraphDbFile || "career-graph.sqlite";
121
+ const name = String(projectName || path.basename(root) || "Project").slice(0, 200);
122
+ ensureLocalCredentialStoreCli(root, name, arch);
123
+ ensureSoulCredentialIndexCli(root, name, arch);
124
+ const sitemap = path.join(dir, privateSeedName(arch.sitemapFile, "sitemap.json"));
125
+ const now = new Date().toISOString();
126
+ ensurePrivateSeedFile(sitemap, JSON.stringify({ project: name, created_at: now, updated_at: now, nodes: [] }, null, 2));
127
+ const skillRegistryFile = privateSeedName(arch.skillRegistryFile, "skill-registry.json");
128
+ const skillTrialsFile = privateSeedName(arch.skillTrialsFile, "skill-trials.jsonl");
129
+ const curatorDecisionsFile = privateSeedName(arch.curatorDecisionsFile, "curator-decisions.jsonl");
130
+ const ontologyRuntimeFile = privateSeedName(arch.ontologyRuntimeFile, "ontology-runtime.json");
131
+ const ontologySourceManifestFile = privateSeedName(arch.ontologySourceManifestFile, "ontology-sources.json");
132
+ const ontologyInboxDir = privateSeedName(arch.ontologyInboxDir, "ontology-inbox");
133
+ const ontologyDbFile = privateSeedName(arch.ontologyDbFile, "ontology-runtime.sqlite");
134
+ const careerGraphConfigFile = privateSeedName(arch.careerGraphConfigFile, "career-graph.json");
135
+ const careerGraphSourceManifestFile = privateSeedName(arch.careerGraphSourceManifestFile, "career-graph-sources.json");
136
+ const careerGraphInboxDir = privateSeedName(arch.careerGraphInboxDir, "career-graph-inbox");
137
+ const careerGraphDbFile = privateSeedName(arch.careerGraphDbFile, "career-graph.sqlite");
88
138
  const skillRegistry = path.join(dir, skillRegistryFile);
89
- if (!fs.existsSync(skillRegistry)) {
90
- fs.writeFileSync(skillRegistry, JSON.stringify({
139
+ ensurePrivateSeedFile(skillRegistry, JSON.stringify({
91
140
  schemaVersion: "1.0",
92
141
  kind: "agentlas-skill-lifecycle-registry",
93
142
  state: "local_candidate",
@@ -127,18 +176,15 @@ function ensureProjectMemoryCli(projectPath, projectName) {
127
176
  lowRiskCanaryOnly: true,
128
177
  severeFailureTolerance: 0,
129
178
  },
130
- }, null, 2), "utf8");
131
- }
132
- const ontologyInbox = path.join(dir, ontologyInboxDir);
133
- if (!fs.existsSync(ontologyInbox)) fs.mkdirSync(ontologyInbox, { recursive: true });
179
+ }, null, 2));
180
+ const ontologyInbox = ensurePrivateDirectory(path.join(dir, ontologyInboxDir));
134
181
  const ontologyRuntime = path.join(dir, ontologyRuntimeFile);
135
- if (!fs.existsSync(ontologyRuntime)) {
136
- fs.writeFileSync(ontologyRuntime, JSON.stringify({
182
+ ensurePrivateSeedFile(ontologyRuntime, JSON.stringify({
137
183
  schemaVersion: "1.0",
138
184
  kind: "agentlas-ontology-runtime",
139
185
  state: "active",
140
186
  activation: "automatic",
141
- projectRoot: projectPath,
187
+ projectRoot: root,
142
188
  projectName: name,
143
189
  dbPath: path.join(dir, ontologyDbFile),
144
190
  inboxPath: ontologyInbox,
@@ -161,27 +207,22 @@ function ensureProjectMemoryCli(projectPath, projectName) {
161
207
  durableWrites: "candidate-ticket-only",
162
208
  workingMemory: "runtime-cache-only",
163
209
  },
164
- }, null, 2), "utf8");
165
- }
210
+ }, null, 2));
166
211
  const ontologySources = path.join(dir, ontologySourceManifestFile);
167
- if (!fs.existsSync(ontologySources)) {
168
- fs.writeFileSync(ontologySources, JSON.stringify({
212
+ ensurePrivateSeedFile(ontologySources, JSON.stringify({
169
213
  schemaVersion: "1.0",
170
214
  kind: "agentlas-ontology-source-manifest",
171
- projectRoot: projectPath,
215
+ projectRoot: root,
172
216
  sources: [],
173
- }, null, 2), "utf8");
174
- }
175
- const careerGraphInbox = path.join(dir, careerGraphInboxDir);
176
- if (!fs.existsSync(careerGraphInbox)) fs.mkdirSync(careerGraphInbox, { recursive: true });
217
+ }, null, 2));
218
+ const careerGraphInbox = ensurePrivateDirectory(path.join(dir, careerGraphInboxDir));
177
219
  const careerGraphConfig = path.join(dir, careerGraphConfigFile);
178
- if (!fs.existsSync(careerGraphConfig)) {
179
- fs.writeFileSync(careerGraphConfig, JSON.stringify({
220
+ ensurePrivateSeedFile(careerGraphConfig, JSON.stringify({
180
221
  schemaVersion: "1.0",
181
222
  kind: "agentlas-career-graph",
182
223
  state: "active",
183
224
  model: "ledger_first_derived_index",
184
- projectRoot: projectPath,
225
+ projectRoot: root,
185
226
  projectName: name,
186
227
  dbPath: path.join(dir, careerGraphDbFile),
187
228
  inboxPath: careerGraphInbox,
@@ -193,20 +234,16 @@ function ensureProjectMemoryCli(projectPath, projectName) {
193
234
  neverScanHomeDirectory: true,
194
235
  neverScanSiblingProjects: true,
195
236
  },
196
- }, null, 2), "utf8");
197
- }
237
+ }, null, 2));
198
238
  const careerGraphSources = path.join(dir, careerGraphSourceManifestFile);
199
- if (!fs.existsSync(careerGraphSources)) {
200
- fs.writeFileSync(careerGraphSources, JSON.stringify({
239
+ ensurePrivateSeedFile(careerGraphSources, JSON.stringify({
201
240
  schemaVersion: "1.0",
202
241
  kind: "agentlas-career-graph-source-manifest",
203
- projectRoot: projectPath,
242
+ projectRoot: root,
204
243
  sources: [],
205
- }, null, 2), "utf8");
206
- }
244
+ }, null, 2));
207
245
  for (const fileName of [skillTrialsFile, curatorDecisionsFile]) {
208
- const filePath = path.join(dir, fileName);
209
- if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf8");
246
+ ensurePrivateSeedFile(path.join(dir, fileName), "");
210
247
  }
211
248
  return dir;
212
249
  } catch { return null; }