agentlas 1.0.64 → 1.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +8 -6
  3. package/bin/agentlas.cjs +341 -16
  4. package/engine/acp/server.cjs +27 -1
  5. package/engine/agentlas-api-agent.cjs +151 -120
  6. package/engine/agentlas-banner.cjs +6 -2
  7. package/engine/agentlas-capabilities.cjs +17 -10
  8. package/engine/agentlas-cloud-runtime.cjs +4 -5
  9. package/engine/agentlas-config.cjs +19 -54
  10. package/engine/agentlas-core-harness.cjs +59 -5
  11. package/engine/agentlas-desktop-loadout.cjs +17 -3
  12. package/engine/agentlas-evolution.cjs +75 -18
  13. package/engine/agentlas-experience-exchange.cjs +145 -14
  14. package/engine/agentlas-experience-intake.cjs +2 -2
  15. package/engine/agentlas-i18n.cjs +0 -2
  16. package/engine/agentlas-input.cjs +273 -20
  17. package/engine/agentlas-judgment.cjs +0 -0
  18. package/engine/agentlas-mcp-env.cjs +84 -23
  19. package/engine/agentlas-mcp-wrapper.cjs +36 -4
  20. package/engine/agentlas-memory-governance.cjs +2 -1
  21. package/engine/agentlas-memory-import.cjs +49 -12
  22. package/engine/agentlas-native-host.cjs +279 -44
  23. package/engine/agentlas-onboard.cjs +23 -1
  24. package/engine/agentlas-permissions.cjs +5 -1
  25. package/engine/agentlas-sqlite-policy.cjs +40 -2
  26. package/engine/agentlas-tasks.cjs +7 -0
  27. package/engine/agentlas-tools.cjs +37 -10
  28. package/engine/agentlas-ui.cjs +26 -4
  29. package/engine/agentlas-workforce.cjs +29 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +10 -3
  32. package/engine/agents/builder.cjs +35 -10
  33. package/engine/agents/files.cjs +81 -10
  34. package/engine/agents/import-local.cjs +10 -2
  35. package/engine/agents/registry.cjs +10 -3
  36. package/engine/agents/router.cjs +7 -7
  37. package/engine/agents/routes.cjs +31 -9
  38. package/engine/automation/daemon.cjs +154 -40
  39. package/engine/automation/launchd.cjs +395 -16
  40. package/engine/automation/schedule.cjs +90 -3
  41. package/engine/automation/store.cjs +163 -59
  42. package/engine/bootstrap-schema.sql +4 -4
  43. package/engine/browser/cdp.cjs +315 -32
  44. package/engine/browser/vault.cjs +1 -0
  45. package/engine/cli-output.cjs +80 -28
  46. package/engine/cloud/auth.cjs +131 -13
  47. package/engine/cloud/hub-client.cjs +41 -16
  48. package/engine/cloud-assets/cas.cjs +18 -9
  49. package/engine/cloud-assets/commands.cjs +108 -30
  50. package/engine/cloud-assets/package.cjs +66 -3
  51. package/engine/cloud-assets/restore.cjs +12 -9
  52. package/engine/cloud-assets/state.cjs +308 -113
  53. package/engine/commands/acp.cjs +13 -2
  54. package/engine/commands/automation.cjs +75 -24
  55. package/engine/commands/billing.cjs +6 -1
  56. package/engine/commands/browser.cjs +67 -34
  57. package/engine/commands/build.cjs +42 -12
  58. package/engine/commands/call.cjs +1 -1
  59. package/engine/commands/cd.cjs +1 -1
  60. package/engine/commands/cloud.cjs +2 -1
  61. package/engine/commands/connect.cjs +38 -13
  62. package/engine/commands/creds.cjs +74 -29
  63. package/engine/commands/doctor.cjs +17 -1
  64. package/engine/commands/document.cjs +40 -12
  65. package/engine/commands/env.cjs +6 -1
  66. package/engine/commands/firm.cjs +42 -12
  67. package/engine/commands/graph.cjs +36 -32
  68. package/engine/commands/help.cjs +11 -1
  69. package/engine/commands/hep.cjs +1 -1
  70. package/engine/commands/import.cjs +5 -4
  71. package/engine/commands/index.cjs +1 -1
  72. package/engine/commands/install.cjs +6 -4
  73. package/engine/commands/list.cjs +56 -16
  74. package/engine/commands/login.cjs +5 -0
  75. package/engine/commands/logout.cjs +6 -1
  76. package/engine/commands/mcp.cjs +8 -2
  77. package/engine/commands/memory.cjs +3 -2
  78. package/engine/commands/multimodal.cjs +12 -0
  79. package/engine/commands/native.cjs +5 -1
  80. package/engine/commands/one.cjs +78 -33
  81. package/engine/commands/plugin.cjs +13 -2
  82. package/engine/commands/project.cjs +15 -12
  83. package/engine/commands/research.cjs +18 -7
  84. package/engine/commands/roles.cjs +104 -28
  85. package/engine/commands/route.cjs +2 -2
  86. package/engine/commands/run.cjs +42 -9
  87. package/engine/commands/search.cjs +23 -2
  88. package/engine/commands/setup.cjs +6 -1
  89. package/engine/commands/storm.cjs +44 -6
  90. package/engine/commands/swarm.cjs +47 -9
  91. package/engine/commands/telegram.cjs +6 -1
  92. package/engine/commands/uninstall.cjs +12 -6
  93. package/engine/commands/update.cjs +5 -0
  94. package/engine/commands/upload.cjs +2 -1
  95. package/engine/commands/usage.cjs +6 -1
  96. package/engine/commands/version.cjs +6 -1
  97. package/engine/commands/whoami.cjs +6 -1
  98. package/engine/commands/workforce.cjs +60 -16
  99. package/engine/core/capability-grants.cjs +7 -2
  100. package/engine/core/daemon-client.cjs +36 -7
  101. package/engine/core/db.cjs +10 -2
  102. package/engine/core/desktop-core-fetch.cjs +477 -77
  103. package/engine/core/schema-ensure.cjs +23 -11
  104. package/engine/experience/build.cjs +5 -1
  105. package/engine/experience/intents.cjs +29 -82
  106. package/engine/experience/runtime.cjs +7 -1
  107. package/engine/experience/variant.cjs +58 -4
  108. package/engine/firms/orchestrate.cjs +9 -6
  109. package/engine/graph/ask-model.cjs +22 -11
  110. package/engine/graph/interview.cjs +74 -23
  111. package/engine/graph/package.cjs +63 -8
  112. package/engine/hephaestus/local-core.cjs +18 -5
  113. package/engine/hephaestus/runtime.cjs +48 -43
  114. package/engine/hub/install.cjs +230 -61
  115. package/engine/hub/plugins.cjs +391 -29
  116. package/engine/mcp/consent.cjs +25 -17
  117. package/engine/mcp/contract.cjs +229 -24
  118. package/engine/mcp/inventory.cjs +15 -5
  119. package/engine/mcp/plan.cjs +5 -1
  120. package/engine/mcp/probe.cjs +24 -9
  121. package/engine/memory-cli/curate.cjs +74 -16
  122. package/engine/oberon/common.cjs +37 -12
  123. package/engine/oberon/manifest.cjs +25 -2
  124. package/engine/oberon/outputs.cjs +49 -19
  125. package/engine/oberon/render.cjs +665 -40
  126. package/engine/project/career-graph.cjs +15 -2
  127. package/engine/project/controller.cjs +133 -15
  128. package/engine/project/credentials.cjs +212 -34
  129. package/engine/project/env-file.cjs +93 -8
  130. package/engine/project/memory-context.cjs +11 -57
  131. package/engine/project/ontology.cjs +66 -25
  132. package/engine/project/paths.cjs +42 -8
  133. package/engine/project/seed.cjs +88 -51
  134. package/engine/project/state.cjs +41 -16
  135. package/engine/project/team.cjs +26 -1
  136. package/engine/runtime-refusal.cjs +4 -1
  137. package/engine/runtimes/acp-driver.cjs +56 -9
  138. package/engine/runtimes/auth-evidence.cjs +21 -2
  139. package/engine/runtimes/detect.cjs +28 -8
  140. package/engine/runtimes/kinds.cjs +12 -4
  141. package/engine/runtimes/overrides.cjs +4 -3
  142. package/engine/runtimes/resolve.cjs +8 -7
  143. package/engine/runtimes/roles.cjs +7 -5
  144. package/engine/sessions/apply-fences.cjs +396 -56
  145. package/engine/sessions/fences.cjs +111 -27
  146. package/engine/sessions/memory-turn.cjs +62 -3
  147. package/engine/sessions/orchestrator.cjs +11 -1
  148. package/engine/sessions/prompt.cjs +66 -9
  149. package/engine/sessions/session.cjs +70 -10
  150. package/engine/sessions/sink.cjs +78 -12
  151. package/engine/sessions/store.cjs +29 -7
  152. package/engine/storm/storm.cjs +26 -3
  153. package/engine/storm/swarm.cjs +31 -3
  154. package/engine/telegram/connect.cjs +187 -24
  155. package/engine/ui/commands-catalog.cjs +1 -1
  156. package/engine/ui/palette.cjs +12 -2
  157. package/engine/ui/repl.cjs +57 -9
  158. package/engine/ui/screens.cjs +2 -4
  159. package/engine/vendor/desktop-core.manifest.json +5 -5
  160. package/engine/workforce/capture.cjs +9 -27
  161. package/engine/workforce/deps.cjs +18 -6
  162. package/engine/workforce/local-core-transport.cjs +68 -5
  163. package/package.json +1 -1
  164. package/engine/commands/career-graph.cjs +0 -51
@@ -8,13 +8,85 @@
8
8
  */
9
9
  const fs = require("node:fs");
10
10
  const path = require("node:path");
11
+ const crypto = require("node:crypto");
12
+
13
+ const MAX_DOT_ENV_BYTES = 512 * 1024;
14
+
15
+ function readEnvSnapshot(file) {
16
+ let before;
17
+ try {
18
+ before = fs.lstatSync(file);
19
+ } catch (error) {
20
+ if (error && error.code === "ENOENT") return { exists: false, body: "" };
21
+ throw error;
22
+ }
23
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_DOT_ENV_BYTES) {
24
+ throw new Error("credential env target must be a bounded regular non-symbolic-link file");
25
+ }
26
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
27
+ try {
28
+ const opened = fs.fstatSync(fd);
29
+ if (
30
+ !opened.isFile() || opened.size > MAX_DOT_ENV_BYTES ||
31
+ opened.dev !== before.dev || opened.ino !== before.ino
32
+ ) throw new Error("credential env target changed while opening");
33
+ return {
34
+ exists: true,
35
+ body: fs.readFileSync(fd, "utf8"),
36
+ dev: opened.dev,
37
+ ino: opened.ino,
38
+ size: opened.size,
39
+ mtimeMs: opened.mtimeMs,
40
+ ctimeMs: opened.ctimeMs,
41
+ };
42
+ } finally {
43
+ fs.closeSync(fd);
44
+ }
45
+ }
46
+
47
+ function assertEnvSnapshotUnchanged(file, snapshot) {
48
+ let current;
49
+ try { current = fs.lstatSync(file); }
50
+ catch (error) {
51
+ if (error && error.code === "ENOENT" && !snapshot.exists) return;
52
+ throw new Error("credential env target changed before replacement");
53
+ }
54
+ if (
55
+ !snapshot.exists || !current.isFile() || current.isSymbolicLink() ||
56
+ current.dev !== snapshot.dev || current.ino !== snapshot.ino ||
57
+ current.size !== snapshot.size || current.mtimeMs !== snapshot.mtimeMs || current.ctimeMs !== snapshot.ctimeMs
58
+ ) throw new Error("credential env target changed before replacement");
59
+ }
60
+
61
+ function replaceEnvFileAtomic(temp, file, snapshot) {
62
+ assertEnvSnapshotUnchanged(file, snapshot);
63
+ try {
64
+ fs.renameSync(temp, file);
65
+ return;
66
+ } catch (error) {
67
+ if (
68
+ process.platform !== "win32" || !snapshot.exists ||
69
+ !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)
70
+ ) throw error;
71
+ }
72
+ assertEnvSnapshotUnchanged(file, snapshot);
73
+ const backup = `${file}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
74
+ fs.renameSync(file, backup);
75
+ try {
76
+ fs.renameSync(temp, file);
77
+ } catch (error) {
78
+ try { if (!fs.existsSync(file)) fs.renameSync(backup, file); } catch { /* leave the backup recoverable */ }
79
+ throw error;
80
+ }
81
+ try { fs.rmSync(backup, { force: true }); } catch { /* committed target is authoritative */ }
82
+ }
11
83
 
12
84
  /** .env 파싱 — 값 보존 없이 키만 필요할 때도 같은 파서를 쓴다 (KEY=VALUE, # 주석). */
13
85
  function readDotEnvFile(file) {
14
86
  const result = {};
15
87
  let raw;
16
88
  try {
17
- raw = fs.readFileSync(file, "utf8");
89
+ raw = readEnvSnapshot(file).body;
18
90
  } catch {
19
91
  return result;
20
92
  }
@@ -30,17 +102,30 @@ function readDotEnvFile(file) {
30
102
  }
31
103
 
32
104
  function upsertEnvLine(file, key, value) {
33
- let body = "";
34
- try { body = fs.readFileSync(file, "utf8"); } catch { /* new file */ }
35
- const line = `${key}=${value}`;
105
+ if (!/^[A-Z][A-Z0-9_]*$/.test(String(key || ""))) throw new Error("credential env key must look like ENV_NAME");
106
+ const text = String(value == null ? "" : value);
107
+ if (/[\u0000\r\n]/.test(text)) throw new Error("credential env value must be a single line; use `agentlas creds file` for multiline credentials");
108
+ const snapshot = readEnvSnapshot(file);
109
+ let body = snapshot.body;
110
+ const line = `${key}=${text}`;
36
111
  const re = new RegExp("^" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=.*$", "m");
37
- if (re.test(body)) body = body.replace(re, line);
112
+ // Replacement strings interpret `$&`, `$1`, and similar tokens. Credentials
113
+ // are opaque bytes, so use a function replacement and preserve them verbatim.
114
+ if (re.test(body)) body = body.replace(re, () => line);
38
115
  else body = body ? body.replace(/\n?$/, "\n") + line + "\n" : line + "\n";
39
- fs.mkdirSync(path.dirname(file), { recursive: true });
116
+ if (Buffer.byteLength(body, "utf8") > MAX_DOT_ENV_BYTES) throw new Error("credential env file exceeds the 512 KiB safety limit");
117
+ const directory = path.dirname(file);
118
+ fs.mkdirSync(directory, { recursive: true });
119
+ const temp = path.join(directory, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
120
+ try {
121
+ fs.writeFileSync(temp, body, { encoding: "utf8", mode: 0o600, flag: "wx" });
122
+ replaceEnvFileAtomic(temp, file, snapshot);
123
+ } finally {
124
+ try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
125
+ }
40
126
  // 이 헬퍼의 모든 호출자는 credential 값/경로를 기록한다. 새 파일뿐 아니라 기존 0644
41
127
  // 파일도 매번 0600으로 수렴시켜 같은 머신의 다른 계정이 읽지 못하게 한다.
42
- fs.writeFileSync(file, body, { encoding: "utf8", mode: 0o600 });
43
128
  try { fs.chmodSync(file, 0o600); } catch { /* Windows/읽기전용 FS best-effort */ }
44
129
  }
45
130
 
46
- module.exports = { readDotEnvFile, upsertEnvLine };
131
+ module.exports = { MAX_DOT_ENV_BYTES, readDotEnvFile, upsertEnvLine };
@@ -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();