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
@@ -182,6 +182,7 @@ async function runCareerGraphCli(args, opts) {
182
182
  const sub = normalizedArgs[0] || "status";
183
183
  const passivePaths = careerGraphPathsForCli(projectPath);
184
184
  if (sub === "status" || sub === "list") {
185
+ if (normalizedArgs.length > 1) throw new Error(`usage: career-graph ${sub}`);
185
186
  if (!initializedAgentlasProjectPathCli(projectPath)) {
186
187
  return [
187
188
  ko ? "커리어 그래프: 초기화되지 않음" : "Career Graph: not initialized",
@@ -192,7 +193,10 @@ async function runCareerGraphCli(args, opts) {
192
193
  }
193
194
  return formatCareerGraphStatusCli(passivePaths, opts.lang);
194
195
  }
195
- if (sub === "help" || sub === "--help" || sub === "-h") return careerGraphUsageLinesCli(opts.lang);
196
+ if (sub === "help" || sub === "--help" || sub === "-h") {
197
+ if (normalizedArgs.length > 1) throw new Error("usage: career-graph help");
198
+ return careerGraphUsageLinesCli(opts.lang);
199
+ }
196
200
  if (["ingest", "query", "verify", "trace"].includes(String(sub))) {
197
201
  return [
198
202
  ko
@@ -207,6 +211,15 @@ async function runCareerGraphCli(args, opts) {
207
211
  const parsed = await parseOntologyNaturalArgsCli(normalizedArgs.join(" "), cwd);
208
212
  return runCareerGraphCli(parsed, opts);
209
213
  }
214
+ let directFlags = null;
215
+ if (sub === "open") {
216
+ if (normalizedArgs.length !== 1) throw new Error("usage: career-graph open");
217
+ } else if (sub === "add") {
218
+ directFlags = parseFlagsCli(normalizedArgs.slice(1));
219
+ if (directFlags._.length < 1 || directFlags._.length > 3) throw new Error("usage: career-graph add <path> [kind] [scope] [--kind <kind>] [--scope <scope>]");
220
+ if (directFlags.kind !== undefined && directFlags._[1] !== undefined) throw new Error("kind was provided twice; use either the positional value or --kind");
221
+ if (directFlags.scope !== undefined && directFlags._[2] !== undefined) throw new Error("scope was provided twice; use either the positional value or --scope");
222
+ }
210
223
  const paths = ensureCareerGraphCli(projectPath, opts.lang);
211
224
  if (sub === "open") {
212
225
  const opened = opts.noOpen ? true : openLocalPathCli(paths.inboxPath, opts.notify);
@@ -215,7 +228,7 @@ async function runCareerGraphCli(args, opts) {
215
228
  : (ko ? "커리어 그래프 수신함을 자동으로 열지 못했습니다. 직접 여세요" : "Could not open Career Graph inbox automatically; open it manually")}: ${paths.inboxPath}`];
216
229
  }
217
230
  if (sub === "add") {
218
- const flags = parseFlagsCli(normalizedArgs.slice(1));
231
+ const flags = directFlags;
219
232
  const source = flags._[0];
220
233
  const kind = inferOntologyKindCli(flags.kind || flags._[1], normalizedArgs.join(" "));
221
234
  const scope = inferOntologyScopeCli(flags.scope || flags._[2], normalizedArgs.join(" "), kind);
@@ -4,6 +4,30 @@ const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
  const { listRoutableAgents } = require("../agents/registry.cjs");
6
6
 
7
+ const MAX_AGENT_POOL_MEMBERS = 64;
8
+ const MAX_AGENT_POOL_BYTES = 256 * 1024;
9
+ const MAX_AGENT_POOL_MEMBER_BYTES = 8 * 1024;
10
+ const MAX_AGENT_ID_BYTES = 512;
11
+ const MAX_RELEASE_ID_BYTES = 512;
12
+ const MAX_MEMBER_NAME_BYTES = 4 * 1024;
13
+ const MAX_PROJECT_NAME_BYTES = 4 * 1024;
14
+ const MAX_SYSTEM_PROMPT_BYTES = 256 * 1024;
15
+ const MAX_COMBINED_SYSTEM_PROMPT_BYTES = 512 * 1024;
16
+
17
+ function projectError(code, message) {
18
+ return Object.assign(new Error(message), { code, honestStop: true });
19
+ }
20
+
21
+ function boundedText(value, maxBytes, label, { allowEmpty = false } = {}) {
22
+ if (typeof value !== "string") throw projectError("project_team_unreadable", `This project's ${label} is invalid.`);
23
+ const text = value.trim();
24
+ if (!allowEmpty && !text) throw projectError("project_team_unreadable", `This project's ${label} is empty.`);
25
+ if (Buffer.byteLength(text, "utf8") > maxBytes || /[\u0000\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) {
26
+ throw projectError("project_team_unreadable", `This project's ${label} is invalid or too large.`);
27
+ }
28
+ return text;
29
+ }
30
+
7
31
  function canonicalPath(value) {
8
32
  const resolved = path.resolve(String(value || ""));
9
33
  try {
@@ -27,19 +51,80 @@ function projectColumns(db) {
27
51
  }
28
52
 
29
53
  function parseAgentPool(raw) {
54
+ if (raw !== undefined && raw !== null && typeof raw !== "string") {
55
+ throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
56
+ }
57
+ const encoded = raw === undefined || raw === null ? "[]" : raw;
58
+ if (Buffer.byteLength(encoded, "utf8") > MAX_AGENT_POOL_BYTES) {
59
+ throw projectError("project_team_unreadable", "This project's agent team is too large. Open the project in Agentlas Desktop and save its team again.");
60
+ }
30
61
  let parsed;
31
62
  try {
32
- parsed = JSON.parse(raw || "[]");
63
+ parsed = JSON.parse(encoded || "[]");
33
64
  } catch {
34
- throw Object.assign(new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again."), { code: "project_team_unreadable", honestStop: true });
65
+ throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
35
66
  }
36
67
  if (!Array.isArray(parsed)) {
37
- throw Object.assign(new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again."), { code: "project_team_unreadable", honestStop: true });
68
+ throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
38
69
  }
39
- return parsed.filter((member) => member && typeof member === "object"
40
- && typeof member.agentId === "string" && member.agentId.trim()
41
- && ["local", "cloud", "hub"].includes(member.source)
42
- && typeof member.nameSnapshot === "string");
70
+ if (parsed.length > MAX_AGENT_POOL_MEMBERS) {
71
+ throw projectError("project_team_too_large", "This project's agent team has too many members. Open the project in Agentlas Desktop and save its team again.");
72
+ }
73
+ const seen = new Set();
74
+ return parsed.map((member, index) => {
75
+ if (!member || typeof member !== "object" || Array.isArray(member)) {
76
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is invalid.`);
77
+ }
78
+ if (Buffer.byteLength(JSON.stringify(member), "utf8") > MAX_AGENT_POOL_MEMBER_BYTES) {
79
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is too large.`);
80
+ }
81
+ const keys = Object.keys(member).sort();
82
+ const legacyKeys = ["agentId", "nameSnapshot", "releaseId", "source"];
83
+ const canonicalKeys = ["agentId", "controllerAgentId", "entityKind", "firmId", "nameSnapshot", "releaseId", "source", "targetId"];
84
+ const isLegacy = keys.length === legacyKeys.length && keys.every((key, keyIndex) => key === legacyKeys[keyIndex]);
85
+ const isCanonical = keys.length === canonicalKeys.length && keys.every((key, keyIndex) => key === canonicalKeys[keyIndex]);
86
+ if (!isLegacy && !isCanonical) {
87
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is incomplete.`);
88
+ }
89
+ const nameSnapshot = boundedText(member.nameSnapshot, MAX_MEMBER_NAME_BYTES, `agent team member ${index + 1} name`);
90
+ if (!["local", "cloud", "hub"].includes(member.source)) {
91
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has an invalid source.`);
92
+ }
93
+ const releaseId = member.releaseId === null
94
+ ? null
95
+ : boundedText(member.releaseId, MAX_RELEASE_ID_BYTES, `agent team member ${index + 1} release`);
96
+ if (isLegacy) {
97
+ const agentId = boundedText(member.agentId, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} agentId`);
98
+ const key = `${member.source}:agent:${agentId}:${releaseId || ""}`;
99
+ if (seen.has(key)) throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is duplicated.`);
100
+ seen.add(key);
101
+ return {
102
+ entityKind: "agent", targetId: agentId, agentId, firmId: null, controllerAgentId: null,
103
+ source: member.source, releaseId, nameSnapshot,
104
+ };
105
+ }
106
+
107
+ if (member.entityKind !== "agent" && member.entityKind !== "team") {
108
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has an invalid entity kind.`);
109
+ }
110
+ const optionalId = (value, label) => value === null
111
+ ? null
112
+ : boundedText(value, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} ${label}`);
113
+ const targetId = boundedText(member.targetId, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} targetId`);
114
+ const agentId = optionalId(member.agentId, "agentId");
115
+ const firmId = optionalId(member.firmId, "firmId");
116
+ const controllerAgentId = optionalId(member.controllerAgentId, "controllerAgentId");
117
+ if (member.entityKind === "agent" && (firmId !== null || controllerAgentId !== null || (member.source === "local" && agentId === null))) {
118
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has inconsistent agent identity fields.`);
119
+ }
120
+ if (member.entityKind === "team" && (agentId !== null || (member.source === "local" && firmId === null))) {
121
+ throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has inconsistent team identity fields.`);
122
+ }
123
+ const key = `${member.source}:${member.entityKind}:${targetId}:${releaseId || ""}`;
124
+ if (seen.has(key)) throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is duplicated.`);
125
+ seen.add(key);
126
+ return { entityKind: member.entityKind, targetId, agentId, firmId, controllerAgentId, source: member.source, releaseId, nameSnapshot };
127
+ });
43
128
  }
44
129
 
45
130
  function resolveProjectForCwd(db, cwd) {
@@ -64,7 +149,23 @@ function resolveProjectForCwd(db, cwd) {
64
149
  if (best.length !== 1) {
65
150
  throw Object.assign(new Error("More than one Agentlas project is connected to this folder. Keep one source connection, then retry."), { code: "project_ambiguous", honestStop: true });
66
151
  }
67
- return { ...best[0].row, rootPath: best[0].root, agentPool: parseAgentPool(best[0].row.agent_pool_json) };
152
+ const row = best[0].row;
153
+ boundedText(row.name, MAX_PROJECT_NAME_BYTES, "name");
154
+ if (row.system_prompt !== null && row.system_prompt !== undefined) {
155
+ boundedText(row.system_prompt, MAX_SYSTEM_PROMPT_BYTES, "system prompt", { allowEmpty: true });
156
+ }
157
+ return { ...row, rootPath: best[0].root, agentPool: parseAgentPool(row.agent_pool_json) };
158
+ }
159
+
160
+ function exactInstalledRelease(db, agentId) {
161
+ try {
162
+ const row = db.prepare(
163
+ "SELECT agent_release_id FROM installed_agent_hub_bindings WHERE installed_agent_id=?",
164
+ ).get(agentId);
165
+ return row && typeof row.agent_release_id === "string" ? row.agent_release_id : null;
166
+ } catch {
167
+ return null;
168
+ }
68
169
  }
69
170
 
70
171
  function resolveProjectController(db, cwd) {
@@ -73,10 +174,19 @@ function resolveProjectController(db, cwd) {
73
174
  throw Object.assign(new Error("This project has no agent team. Drag agents into the project in Desktop Work, then retry."), { code: "project_team_empty", honestStop: true });
74
175
  }
75
176
  const controllerRef = project.agentPool[0];
76
- if (controllerRef.source !== "local") {
177
+ const controllerAgentId = controllerRef.entityKind === "team"
178
+ ? controllerRef.controllerAgentId
179
+ : controllerRef.agentId;
180
+ if (!controllerAgentId) {
77
181
  throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is not installed locally for Terminal execution. Install that exact release locally or reorder the project team.`), { code: "controller_not_installed", honestStop: true });
78
182
  }
79
- const controller = listRoutableAgents(db).find((agent) => agent.id === controllerRef.agentId) || null;
183
+ if (controllerRef.entityKind === "team" && controllerRef.source !== "local" && controllerRef.releaseId !== null) {
184
+ throw projectError("controller_release_unavailable", `The project controller team “${controllerRef.nameSnapshot}” needs its exact team release restored before Terminal can execute it.`);
185
+ }
186
+ if (controllerRef.entityKind === "agent" && controllerRef.releaseId !== null && exactInstalledRelease(db, controllerAgentId) !== controllerRef.releaseId) {
187
+ throw projectError("controller_release_unavailable", `The project controller “${controllerRef.nameSnapshot}” is pinned to an unavailable exact release. Restore that release or explicitly choose a new first agent in Desktop Work.`);
188
+ }
189
+ const controller = listRoutableAgents(db).find((agent) => agent.id === controllerAgentId) || null;
80
190
  if (!controller) {
81
191
  throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is unavailable. Restore that agent or explicitly choose a new first agent in Desktop Work.`), { code: "controller_unavailable", honestStop: true });
82
192
  }
@@ -84,22 +194,30 @@ function resolveProjectController(db, cwd) {
84
194
  }
85
195
 
86
196
  function withProjectControllerContext(controller, project) {
197
+ const projectName = boundedText(project.name, MAX_PROJECT_NAME_BYTES, "name");
87
198
  const team = project.agentPool.map((member, index) =>
88
199
  `${index + 1}. ${member.nameSnapshot} (${member.source}${member.releaseId ? `, exact release ${member.releaseId}` : ""})`,
89
200
  ).join("\n");
90
- const projectInstruction = String(project.system_prompt || "").trim();
201
+ const projectInstruction = project.system_prompt === null || project.system_prompt === undefined
202
+ ? ""
203
+ : boundedText(project.system_prompt, MAX_SYSTEM_PROMPT_BYTES, "system prompt", { allowEmpty: true });
204
+ const controllerPrompt = boundedText(controller.systemPrompt || "", MAX_SYSTEM_PROMPT_BYTES, "controller system prompt", { allowEmpty: true });
91
205
  const ownership = [
92
- `You are the controller for the Agentlas Work project “${project.name}”.`,
206
+ `You are the controller for the Agentlas Work project “${projectName}”.`,
93
207
  "You own this task. The remaining ordered project members are eligible sub-agents for task-scoped WorkOrders only.",
94
208
  "Do not transfer task ownership, silently substitute an unavailable member, or claim a sub-agent ran without an assignment and execution receipt.",
95
209
  "Project team:",
96
210
  team,
97
211
  ].join("\n");
212
+ const systemPrompt = [controllerPrompt, projectInstruction && `Project system prompt:\n${projectInstruction}`, ownership]
213
+ .filter(Boolean)
214
+ .join("\n\n");
215
+ if (Buffer.byteLength(systemPrompt, "utf8") > MAX_COMBINED_SYSTEM_PROMPT_BYTES) {
216
+ throw projectError("project_context_too_large", "This project's controller context is too large to execute safely.");
217
+ }
98
218
  return {
99
219
  ...controller,
100
- systemPrompt: [controller.systemPrompt, projectInstruction && `Project system prompt:\n${projectInstruction}`, ownership]
101
- .filter(Boolean)
102
- .join("\n\n"),
220
+ systemPrompt,
103
221
  };
104
222
  }
105
223
 
@@ -10,9 +10,119 @@
10
10
  */
11
11
  const fs = require("node:fs");
12
12
  const path = require("node:path");
13
+ const crypto = require("node:crypto");
13
14
  const { loadArch } = require("../core/db.cjs");
14
15
  const { runCwd } = require("./paths.cjs");
15
16
 
17
+ const MAX_MANAGED_CREDENTIAL_METADATA_BYTES = 1024 * 1024;
18
+ const MAX_CREDENTIAL_FILE_BYTES = 16 * 1024 * 1024;
19
+
20
+ function credentialProjectRootCli(projectPath) {
21
+ const requested = path.resolve(projectPath || "");
22
+ const root = fs.realpathSync.native(requested);
23
+ const stat = fs.statSync(root);
24
+ if (!stat.isDirectory()) throw new Error("credential project must be a real directory");
25
+ return root;
26
+ }
27
+
28
+ function ensureManagedDirectoryCli(root, relativePath, mode = 0o700) {
29
+ let current = root;
30
+ for (const segment of String(relativePath || "").split(/[\\/]+/).filter(Boolean)) {
31
+ current = path.join(current, segment);
32
+ try {
33
+ const stat = fs.lstatSync(current);
34
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("credential store directories must not be symbolic links");
35
+ } catch (error) {
36
+ if (!error || error.code !== "ENOENT") throw error;
37
+ fs.mkdirSync(current, { recursive: false, mode });
38
+ }
39
+ }
40
+ return current;
41
+ }
42
+
43
+ function readManagedTextSnapshotCli(filePath, maxBytes = MAX_MANAGED_CREDENTIAL_METADATA_BYTES) {
44
+ let before;
45
+ try { before = fs.lstatSync(filePath); }
46
+ catch (error) {
47
+ if (error && error.code === "ENOENT") return { exists: false, text: "", mode: 0o600 };
48
+ throw error;
49
+ }
50
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) {
51
+ throw new Error("credential store file must be a bounded regular non-symbolic-link file");
52
+ }
53
+ const fd = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
54
+ try {
55
+ const opened = fs.fstatSync(fd);
56
+ if (!opened.isFile() || opened.size > maxBytes || opened.dev !== before.dev || opened.ino !== before.ino) {
57
+ throw new Error("credential store file changed while opening");
58
+ }
59
+ return {
60
+ exists: true,
61
+ text: fs.readFileSync(fd, "utf8"),
62
+ mode: before.mode & 0o777,
63
+ stat: before,
64
+ };
65
+ } finally {
66
+ fs.closeSync(fd);
67
+ }
68
+ }
69
+
70
+ function assertManagedSnapshotUnchangedCli(filePath, snapshot) {
71
+ let current;
72
+ try { current = fs.lstatSync(filePath); }
73
+ catch (error) {
74
+ if (error && error.code === "ENOENT" && !snapshot.exists) return;
75
+ throw new Error("credential store file changed before replacement");
76
+ }
77
+ if (
78
+ !snapshot.exists || !current.isFile() || current.isSymbolicLink() || !snapshot.stat ||
79
+ current.dev !== snapshot.stat.dev || current.ino !== snapshot.stat.ino ||
80
+ current.size !== snapshot.stat.size || current.mtimeMs !== snapshot.stat.mtimeMs
81
+ ) throw new Error("credential store file changed before replacement");
82
+ }
83
+
84
+ function replaceManagedFileCli(tempPath, filePath, snapshot) {
85
+ assertManagedSnapshotUnchangedCli(filePath, snapshot);
86
+ try {
87
+ fs.renameSync(tempPath, filePath);
88
+ return;
89
+ } catch (error) {
90
+ if (
91
+ process.platform !== "win32" || !snapshot.exists ||
92
+ !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)
93
+ ) throw error;
94
+ }
95
+ assertManagedSnapshotUnchangedCli(filePath, snapshot);
96
+ const backup = `${filePath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
97
+ fs.renameSync(filePath, backup);
98
+ try {
99
+ fs.renameSync(tempPath, filePath);
100
+ } catch (error) {
101
+ try { if (!fs.existsSync(filePath)) fs.renameSync(backup, filePath); } catch { /* leave recoverable backup */ }
102
+ throw error;
103
+ }
104
+ try { fs.rmSync(backup, { force: true }); } catch { /* committed target is authoritative */ }
105
+ }
106
+
107
+ function writeManagedTextAtomicCli(filePath, text, options = {}) {
108
+ const snapshot = options.snapshot || readManagedTextSnapshotCli(filePath, options.maxBytes);
109
+ const bytes = Buffer.byteLength(text, "utf8");
110
+ if (bytes > (options.maxBytes || MAX_MANAGED_CREDENTIAL_METADATA_BYTES)) throw new Error("credential store file exceeds its safety limit");
111
+ const temp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
112
+ try {
113
+ fs.writeFileSync(temp, text, { encoding: "utf8", mode: options.mode || snapshot.mode || 0o600, flag: "wx" });
114
+ replaceManagedFileCli(temp, filePath, snapshot);
115
+ } finally {
116
+ try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
117
+ }
118
+ }
119
+
120
+ function ensureManagedTextFileCli(filePath, content, mode = 0o600) {
121
+ const snapshot = readManagedTextSnapshotCli(filePath);
122
+ if (!snapshot.exists) writeManagedTextAtomicCli(filePath, content, { snapshot, mode });
123
+ return snapshot.exists ? snapshot.text : content;
124
+ }
125
+
16
126
  function localCredentialConfigCli(arch) {
17
127
  return {
18
128
  mapFile: arch.localCredentialsMapFile || "local-credentials.map.json",
@@ -94,24 +204,24 @@ ${credentialIndexSectionContentCli(arch)}
94
204
  `;
95
205
  }
96
206
  function ensureSoulCredentialIndexCli(projectPath, projectName, arch) {
207
+ const root = credentialProjectRootCli(projectPath);
97
208
  const memoryDir = (arch && arch.memoryDir) || ".agentlas";
98
209
  const soulFile = (arch && arch.soulFile) || "project-soul-memory.md";
99
- const dir = path.join(projectPath, memoryDir);
210
+ const dir = ensureManagedDirectoryCli(root, memoryDir);
100
211
  const soul = path.join(dir, soulFile);
101
- fs.mkdirSync(dir, { recursive: true });
102
- if (!fs.existsSync(soul)) {
103
- fs.writeFileSync(soul, projectSoulTemplateCli(projectName, arch), "utf8");
212
+ const snapshot = readManagedTextSnapshotCli(soul);
213
+ if (!snapshot.exists) {
214
+ writeManagedTextAtomicCli(soul, projectSoulTemplateCli(projectName, arch), { snapshot, mode: 0o600 });
104
215
  return soul;
105
216
  }
106
- let content = "";
107
- try { content = fs.readFileSync(soul, "utf8"); } catch { content = ""; }
217
+ const content = snapshot.text;
108
218
  if (!content.includes(CREDENTIAL_INDEX_SECTION_CLI)) {
109
219
  const section = credentialIndexSectionContentCli(arch);
110
220
  const marker = "\n## Project Purpose";
111
221
  const next = content.includes(marker)
112
222
  ? content.replace(marker, `\n${section}\n## Project Purpose`)
113
223
  : `${content.trimEnd()}\n\n${section}\n`;
114
- fs.writeFileSync(soul, next.endsWith("\n") ? next : next + "\n", "utf8");
224
+ writeManagedTextAtomicCli(soul, next.endsWith("\n") ? next : next + "\n", { snapshot, mode: 0o600 });
115
225
  }
116
226
  return soul;
117
227
  }
@@ -157,7 +267,8 @@ Do not commit files from this folder.
157
267
  `;
158
268
  }
159
269
  function ensureAgentlasCredentialIgnoreCli(projectPath, cfg) {
160
- const gitignorePath = path.join(projectPath, ".gitignore");
270
+ const root = credentialProjectRootCli(projectPath);
271
+ const gitignorePath = path.join(root, ".gitignore");
161
272
  const marker = "# Agentlas local credentials";
162
273
  const block = `${marker}
163
274
  .env
@@ -171,45 +282,62 @@ ${cfg.credentialsDir}/*
171
282
  !${cfg.credentialsDir}/
172
283
  !${cfg.credentialsDir}/${cfg.readmeFile}
173
284
  `;
174
- let existing = "";
175
- try { existing = fs.readFileSync(gitignorePath, "utf8"); } catch { existing = ""; }
285
+ const snapshot = readManagedTextSnapshotCli(gitignorePath);
286
+ const existing = snapshot.text;
176
287
  if (existing.includes(marker)) {
177
- if (!/^\._\*$/m.test(existing)) fs.writeFileSync(gitignorePath, `${existing.trimEnd()}\n._*\n`, "utf8");
288
+ if (!/^\._\*$/m.test(existing)) {
289
+ writeManagedTextAtomicCli(gitignorePath, `${existing.trimEnd()}\n._*\n`, { snapshot, mode: snapshot.mode || 0o644 });
290
+ }
178
291
  return;
179
292
  }
180
293
  const next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
181
- fs.writeFileSync(gitignorePath, next.endsWith("\n") ? next : next + "\n", "utf8");
294
+ writeManagedTextAtomicCli(gitignorePath, next.endsWith("\n") ? next : next + "\n", {
295
+ snapshot,
296
+ mode: snapshot.exists ? snapshot.mode : 0o644,
297
+ });
182
298
  }
183
299
  function ensureLocalCredentialStoreCli(projectPath, projectName, arch) {
300
+ const root = credentialProjectRootCli(projectPath);
184
301
  const cfg = localCredentialConfigCli(arch || loadArch());
185
- const dir = path.join(projectPath, (arch && arch.memoryDir) || ".agentlas");
186
- fs.mkdirSync(dir, { recursive: true });
187
- fs.mkdirSync(path.join(projectPath, cfg.signingDir), { recursive: true });
188
- fs.mkdirSync(path.join(projectPath, cfg.credentialsDir), { recursive: true });
189
- const envExample = path.join(projectPath, cfg.envExampleFile);
190
- if (!fs.existsSync(envExample)) fs.writeFileSync(envExample, envExampleContentCli(cfg), "utf8");
191
- const signingReadme = path.join(projectPath, cfg.signingDir, cfg.readmeFile);
192
- if (!fs.existsSync(signingReadme)) fs.writeFileSync(signingReadme, signingReadmeContentCli(cfg), "utf8");
193
- const credentialsReadme = path.join(projectPath, cfg.credentialsDir, cfg.readmeFile);
194
- if (!fs.existsSync(credentialsReadme)) fs.writeFileSync(credentialsReadme, credentialsReadmeContentCli(cfg), "utf8");
302
+ const dir = ensureManagedDirectoryCli(root, (arch && arch.memoryDir) || ".agentlas");
303
+ const signingDir = ensureManagedDirectoryCli(root, cfg.signingDir);
304
+ const credentialsDir = ensureManagedDirectoryCli(root, cfg.credentialsDir);
305
+ const envExample = path.join(root, cfg.envExampleFile);
306
+ ensureManagedTextFileCli(envExample, envExampleContentCli(cfg), 0o600);
307
+ const signingReadme = path.join(signingDir, cfg.readmeFile);
308
+ ensureManagedTextFileCli(signingReadme, signingReadmeContentCli(cfg), 0o600);
309
+ const credentialsReadme = path.join(credentialsDir, cfg.readmeFile);
310
+ ensureManagedTextFileCli(credentialsReadme, credentialsReadmeContentCli(cfg), 0o600);
195
311
  const mapPath = path.join(dir, cfg.mapFile);
196
- if (!fs.existsSync(mapPath)) {
197
- fs.writeFileSync(mapPath, JSON.stringify(localCredentialsMapSkeletonCli(projectPath, projectName, cfg), null, 2) + "\n", "utf8");
312
+ const mapText = ensureManagedTextFileCli(
313
+ mapPath,
314
+ JSON.stringify(localCredentialsMapSkeletonCli(root, projectName, cfg), null, 2) + "\n",
315
+ 0o600,
316
+ );
317
+ let mapValue;
318
+ try { mapValue = JSON.parse(mapText); }
319
+ catch (error) { throw new Error(`credential map is invalid JSON: ${String((error && error.message) || error)}`); }
320
+ if (!mapValue || typeof mapValue !== "object" || Array.isArray(mapValue)) {
321
+ throw new Error("credential map must contain a JSON object");
198
322
  }
199
- ensureAgentlasCredentialIgnoreCli(projectPath, cfg);
200
- return { cfg, mapPath };
323
+ ensureAgentlasCredentialIgnoreCli(root, cfg);
324
+ return { cfg, mapPath, projectRoot: root };
201
325
  }
202
- function readJsonObjectCli(file, fallback) {
326
+ function readJsonObjectCli(file, fallback, options = {}) {
203
327
  try {
204
- const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
205
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
206
- } catch {
328
+ const parsed = JSON.parse(readManagedTextSnapshotCli(file).text);
329
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
330
+ if (options.strict) throw new Error("credential map must contain a JSON object");
331
+ return fallback;
332
+ } catch (error) {
333
+ if (options.strict) throw new Error(`credential map is unreadable or invalid: ${String((error && error.message) || error)}`);
207
334
  return fallback;
208
335
  }
209
336
  }
210
337
  function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
211
- const { cfg, mapPath } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
212
- const data = readJsonObjectCli(mapPath, localCredentialsMapSkeletonCli(projectPath, projectName, cfg));
338
+ const { cfg, mapPath, projectRoot } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
339
+ const snapshot = readManagedTextSnapshotCli(mapPath);
340
+ const data = readJsonObjectCli(mapPath, localCredentialsMapSkeletonCli(projectPath, projectName, cfg), { strict: true });
213
341
  const now = new Date().toISOString();
214
342
  data.updatedAt = now;
215
343
  if (!Array.isArray(data.entries)) data.entries = [];
@@ -230,17 +358,64 @@ function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
230
358
  const idx = data.entries.findIndex((row) => row && row.id === id);
231
359
  if (idx >= 0) data.entries[idx] = { ...data.entries[idx], ...clean };
232
360
  else data.entries.push(clean);
233
- fs.writeFileSync(mapPath, JSON.stringify(data, null, 2) + "\n", "utf8");
361
+ data.projectRoot = projectRoot;
362
+ writeManagedTextAtomicCli(mapPath, JSON.stringify(data, null, 2) + "\n", { snapshot, mode: 0o600 });
234
363
  }
235
364
  // v1은 fail()로 즉시 종료했다 — v2 규칙은 throw → 명령이 ctx.err + return 1 로 변환.
236
365
  function safeCredentialDestRelCli(destRel) {
237
366
  const rel = path.normalize(String(destRel || "")).replace(/\\/g, "/");
238
- if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || rel.includes("\0")) {
367
+ if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || /[\u0000-\u001f\u007f]/.test(rel)) {
239
368
  throw new Error("credential destination must be a relative path inside the project");
240
369
  }
241
370
  return rel;
242
371
  }
243
372
 
373
+ function resolveCredentialDestinationCli(projectPath, destRel) {
374
+ const root = credentialProjectRootCli(projectPath);
375
+ const rel = safeCredentialDestRelCli(destRel);
376
+ const parentRel = path.dirname(rel);
377
+ const parent = parentRel === "." ? root : ensureManagedDirectoryCli(root, parentRel);
378
+ const realParent = fs.realpathSync.native(parent);
379
+ const relativeParent = path.relative(root, realParent);
380
+ if (path.isAbsolute(relativeParent) || relativeParent === ".." || relativeParent.startsWith(`..${path.sep}`)) {
381
+ throw new Error("credential destination escaped the project");
382
+ }
383
+ return path.join(realParent, path.basename(rel));
384
+ }
385
+
386
+ function copyCredentialFileAtomicCli(sourcePath, destinationPath, options = {}) {
387
+ const source = fs.realpathSync.native(path.resolve(sourcePath));
388
+ const sourceStat = fs.statSync(source);
389
+ if (!sourceStat.isFile() || sourceStat.size <= 0 || sourceStat.size > MAX_CREDENTIAL_FILE_BYTES) {
390
+ throw new Error("credential source must be a non-empty regular file no larger than 16 MiB");
391
+ }
392
+ const snapshot = (() => {
393
+ try {
394
+ const stat = fs.lstatSync(destinationPath);
395
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("credential destination must be a regular non-symbolic-link file");
396
+ if (!options.force) throw new Error("credential destination already exists (use --force to replace)");
397
+ return { exists: true, stat };
398
+ } catch (error) {
399
+ if (error && error.code === "ENOENT") return { exists: false };
400
+ throw error;
401
+ }
402
+ })();
403
+ const temp = path.join(path.dirname(destinationPath), `.${path.basename(destinationPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
404
+ try {
405
+ fs.copyFileSync(source, temp, fs.constants.COPYFILE_EXCL);
406
+ const copied = fs.lstatSync(temp);
407
+ if (!copied.isFile() || copied.isSymbolicLink() || copied.size !== sourceStat.size) {
408
+ throw new Error("credential copy did not produce the exact regular file");
409
+ }
410
+ try { fs.chmodSync(temp, 0o600); } catch { /* Windows/best-effort */ }
411
+ replaceManagedFileCli(temp, destinationPath, snapshot);
412
+ try { fs.chmodSync(destinationPath, 0o600); } catch { /* Windows/best-effort */ }
413
+ } finally {
414
+ try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
415
+ }
416
+ return destinationPath;
417
+ }
418
+
244
419
  module.exports = {
245
420
  CREDENTIAL_INDEX_SECTION_CLI,
246
421
  localCredentialConfigCli,
@@ -259,4 +434,7 @@ module.exports = {
259
434
  readJsonObjectCli,
260
435
  upsertLocalCredentialMapCli,
261
436
  safeCredentialDestRelCli,
437
+ credentialProjectRootCli,
438
+ resolveCredentialDestinationCli,
439
+ copyCredentialFileAtomicCli,
262
440
  };