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
@@ -19,6 +19,49 @@ const SOURCE_TOKEN_PREFIX = "mem-import:v1";
19
19
  const MAX_FILES = 400;
20
20
  const MAX_CONTENT = 4000;
21
21
 
22
+ function memoryArgumentError(message) {
23
+ const error = new Error(message);
24
+ error.code = "INVALID_ARGUMENT";
25
+ return error;
26
+ }
27
+
28
+ function parseMemoryArgs(args) {
29
+ const input = Array.isArray(args) ? args : [];
30
+ const sub = input[0] || "help";
31
+ if (["help", "--help", "-h"].includes(sub)) {
32
+ if (input.length > 1) throw memoryArgumentError("usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]");
33
+ return { help: true };
34
+ }
35
+ if (sub !== "import") throw memoryArgumentError(`Unknown memory subcommand: ${sub} (import)`);
36
+ let source = null;
37
+ let agentId = null;
38
+ let apply = false;
39
+ const seen = new Set();
40
+ for (let index = 1; index < input.length; index += 1) {
41
+ const token = input[index];
42
+ if (token === "--apply") {
43
+ if (seen.has("apply")) throw memoryArgumentError("duplicate option: --apply");
44
+ seen.add("apply");
45
+ apply = true;
46
+ continue;
47
+ }
48
+ if (token === "--agent") {
49
+ if (seen.has("agent")) throw memoryArgumentError("duplicate option: --agent");
50
+ const value = input[index + 1];
51
+ if (!value || String(value).startsWith("--")) throw memoryArgumentError("--agent requires a value");
52
+ seen.add("agent");
53
+ agentId = String(value).trim();
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (String(token).startsWith("-")) throw memoryArgumentError(`unknown option: ${token}`);
58
+ if (source !== null) throw memoryArgumentError("memory import accepts exactly one source path");
59
+ source = String(token);
60
+ }
61
+ if (!source || !agentId) throw memoryArgumentError("usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]");
62
+ return { help: false, source, agentId, apply };
63
+ }
64
+
22
65
  // ── Section extraction (parity with electron/memory/import.ts) ───────────────
23
66
  const TEMPLATE_LINE =
24
67
  /^\s*[-*]?\s*(Add\b|Fill in\b|Example:|Record\b|Link\b|Prefer\b|Note\b|Which\b|Date:\s*$|Topic:\s*$|Decision:\s*$|Why:\s*$|Risk accepted:\s*$)/i;
@@ -234,21 +277,15 @@ function existsByToken(db, token) {
234
277
  function cmdMemory(ctx) {
235
278
  const { db, out, fail } = ctx;
236
279
  const args = Array.isArray(ctx.args) ? ctx.args : [];
237
- const sub = args[0] || "help";
238
- if (sub === "help" || sub === "--help" || sub === "-h") {
280
+ let parsed;
281
+ try { parsed = parseMemoryArgs(args); }
282
+ catch (error) { return fail(error.message); }
283
+ if (parsed.help) {
239
284
  out("usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]");
240
285
  out(" dry-run by default (prints the preview table); --apply writes to the shared DB.");
241
286
  return;
242
287
  }
243
- if (sub !== "import") return fail(`Unknown memory subcommand: ${sub} (import)`);
244
-
245
- const apply = args.includes("--apply");
246
- const agentIdx = args.indexOf("--agent");
247
- const agentId = agentIdx >= 0 ? String(args[agentIdx + 1] || "").trim() : "";
248
- const positional = args.slice(1).filter((a, i, arr) => a !== "--apply" && a !== "--agent" && arr[i - 1] !== "--agent");
249
- const rawPath = positional[0];
250
- if (!rawPath) return fail('usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]');
251
- if (!agentId) return fail("memory import requires --agent <agentId> (the single agent or team to import into).");
288
+ const { apply, agentId, source: rawPath } = parsed;
252
289
  const sourcePath = path.resolve(rawPath);
253
290
  if (!fs.existsSync(sourcePath)) return fail(`Import source not found: ${sourcePath}`);
254
291
 
@@ -307,4 +344,4 @@ function pad(value, width) {
307
344
  return s.length >= width ? s.slice(0, width - 1) + " " : s + " ".repeat(width - s.length);
308
345
  }
309
346
 
310
- module.exports = { cmdMemory, buildEntries, resolveTarget, decideOwner };
347
+ module.exports = { cmdMemory, parseMemoryArgs, buildEntries, resolveTarget, decideOwner };
@@ -32,6 +32,8 @@ const NATIVE_TIMEOUT_DEFAULTS = Object.freeze({
32
32
  totalMs: 4 * 60 * 60_000,
33
33
  killGraceMs: 3_000,
34
34
  });
35
+ const NATIVE_STDOUT_MAX_BYTES = 16 * 1024 * 1024;
36
+ const MANAGED_MCP_CONFIG_MAX_BYTES = 2 * 1024 * 1024;
35
37
 
36
38
  function finiteTimeoutMs(value, fallback, min, max) {
37
39
  const parsed = Number(value);
@@ -85,22 +87,244 @@ function geminiMcpIsolationArgs() {
85
87
  return ["--allowed-mcp-server-names", `__agentlas_no_mcp_${crypto.randomUUID()}__`];
86
88
  }
87
89
 
88
- function writeManagedFile(file, content) {
89
- const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
90
+ function assertManagedDirectory(dir) {
91
+ const requestedPath = path.resolve(dir);
92
+ fs.mkdirSync(requestedPath, { recursive: true, mode: 0o700 });
93
+ const stat = fs.lstatSync(requestedPath);
94
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
95
+ throw new Error(`managed runtime directory must be a real directory: ${requestedPath}`);
96
+ }
97
+ const realPath = fs.realpathSync.native(requestedPath);
98
+ try { fs.chmodSync(requestedPath, 0o700); } catch { /* Windows/best-effort */ }
99
+ return { requestedPath, realPath, dev: stat.dev, ino: stat.ino };
100
+ }
101
+
102
+ function recheckManagedDirectory(identity) {
103
+ let stat;
104
+ try {
105
+ stat = fs.lstatSync(identity.requestedPath);
106
+ } catch {
107
+ throw new Error(`managed runtime directory changed while publishing: ${identity.requestedPath}`);
108
+ }
109
+ let realPath;
110
+ try {
111
+ realPath = fs.realpathSync.native(identity.requestedPath);
112
+ } catch {
113
+ throw new Error(`managed runtime directory changed while publishing: ${identity.requestedPath}`);
114
+ }
115
+ if (
116
+ !stat.isDirectory() || stat.isSymbolicLink() ||
117
+ stat.dev !== identity.dev || stat.ino !== identity.ino || realPath !== identity.realPath
118
+ ) {
119
+ throw new Error(`managed runtime directory changed while publishing: ${identity.requestedPath}`);
120
+ }
121
+ return stat;
122
+ }
123
+
124
+ function managedFileLocation(file, directory = null) {
125
+ const requestedFile = path.resolve(file);
126
+ const requestedDirectory = path.dirname(requestedFile);
127
+ const identity = directory || assertManagedDirectory(requestedDirectory);
128
+ if (identity.requestedPath !== requestedDirectory) {
129
+ throw new Error(`managed runtime file directory mismatch: ${requestedFile}`);
130
+ }
131
+ recheckManagedDirectory(identity);
132
+ return {
133
+ requestedFile,
134
+ file: path.join(identity.realPath, path.basename(requestedFile)),
135
+ directory: identity,
136
+ };
137
+ }
138
+
139
+ function existingManagedFileStat(file) {
90
140
  try {
91
- fs.writeFileSync(temp, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
141
+ const stat = fs.lstatSync(file);
142
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
143
+ throw new Error(`managed runtime file must be a single-link regular file: ${file}`);
144
+ }
145
+ return stat;
146
+ } catch (error) {
147
+ if (error && error.code === "ENOENT") return null;
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ function readManagedFileDetails(file, maxBytes = MANAGED_MCP_CONFIG_MAX_BYTES) {
153
+ const listed = existingManagedFileStat(file);
154
+ if (!listed) return null;
155
+ if (listed.size > maxBytes) throw new Error(`managed runtime file exceeds ${maxBytes} bytes: ${file}`);
156
+ let fd = null;
157
+ try {
158
+ fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
159
+ const opened = fs.fstatSync(fd);
160
+ if (
161
+ !opened.isFile() || opened.nlink !== 1 ||
162
+ opened.dev !== listed.dev || opened.ino !== listed.ino || opened.size !== listed.size
163
+ ) throw new Error(`managed runtime file changed while opening: ${file}`);
164
+ const chunks = [];
165
+ let total = 0;
166
+ while (total <= maxBytes) {
167
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
168
+ const count = fs.readSync(fd, buffer, 0, buffer.length, null);
169
+ if (!count) break;
170
+ chunks.push(buffer.subarray(0, count));
171
+ total += count;
172
+ }
173
+ const after = fs.fstatSync(fd);
174
+ if (
175
+ total > maxBytes || !after.isFile() || after.nlink !== 1 ||
176
+ after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size ||
177
+ after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || total !== after.size
178
+ ) throw new Error(`managed runtime file changed while reading: ${file}`);
179
+ let content;
180
+ try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); }
181
+ catch { throw new Error(`managed runtime file must contain valid UTF-8: ${file}`); }
182
+ return {
183
+ content,
184
+ stat: after,
185
+ };
186
+ } finally {
187
+ if (fd != null) fs.closeSync(fd);
188
+ }
189
+ }
190
+
191
+ function readManagedFile(file, maxBytes = MANAGED_MCP_CONFIG_MAX_BYTES) {
192
+ const result = readManagedFileDetails(file, maxBytes);
193
+ return result && result.content;
194
+ }
195
+
196
+ function sameManagedFileIdentity(stat, expected) {
197
+ return stat && stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino;
198
+ }
199
+
200
+ function sameManagedFileSnapshot(stat, expected) {
201
+ return sameManagedFileIdentity(stat, expected) && stat.nlink === expected.nlink &&
202
+ stat.size === expected.size && stat.mtimeMs === expected.mtimeMs && stat.ctimeMs === expected.ctimeMs;
203
+ }
204
+
205
+ function assertManagedFileStable(file, expected, expectedContent, directory) {
206
+ recheckManagedDirectory(directory);
207
+ const listed = existingManagedFileStat(file);
208
+ if (!sameManagedFileSnapshot(listed, expected)) {
209
+ throw new Error(`managed runtime file changed after reading: ${file}`);
210
+ }
211
+ const reread = readManagedFileDetails(file);
212
+ if (!reread || reread.content !== expectedContent || !sameManagedFileSnapshot(reread.stat, expected)) {
213
+ throw new Error(`managed runtime file content changed after reading: ${file}`);
214
+ }
215
+ recheckManagedDirectory(directory);
216
+ const final = existingManagedFileStat(file);
217
+ if (!sameManagedFileSnapshot(final, expected)) {
218
+ throw new Error(`managed runtime file changed before returning: ${file}`);
219
+ }
220
+ }
221
+
222
+ function removeOwnedManagedTemp(temp, expected, directory = null) {
223
+ const candidates = [temp];
224
+ if (directory) {
225
+ try {
226
+ const parent = path.dirname(directory.requestedPath);
227
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
228
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
229
+ const candidateDirectory = path.join(parent, entry.name);
230
+ const candidateStat = fs.lstatSync(candidateDirectory);
231
+ if (candidateStat.dev === directory.dev && candidateStat.ino === directory.ino) {
232
+ candidates.push(path.join(candidateDirectory, path.basename(temp)));
233
+ }
234
+ }
235
+ } catch { /* leave an unknown successor untouched */ }
236
+ }
237
+ for (const candidate of candidates) {
92
238
  try {
93
- fs.renameSync(temp, file);
94
- } catch (error) {
95
- // Windows cannot always replace an existing destination atomically.
96
- if (!error || !["EEXIST", "EPERM"].includes(error.code)) throw error;
97
- fs.rmSync(file, { force: true });
98
- fs.renameSync(temp, file);
239
+ const stat = fs.lstatSync(candidate);
240
+ if (sameManagedFileIdentity(stat, expected)) fs.unlinkSync(candidate);
241
+ } catch { /* leave an unknown successor untouched */ }
242
+ }
243
+ }
244
+
245
+ function writeManagedFile(file, content, location = null) {
246
+ const managed = location || managedFileLocation(file);
247
+ const value = String(content);
248
+ const bytes = Buffer.from(value, "utf8");
249
+ if (bytes.length > MANAGED_MCP_CONFIG_MAX_BYTES) {
250
+ throw new Error(`managed runtime file exceeds ${MANAGED_MCP_CONFIG_MAX_BYTES} bytes: ${managed.file}`);
251
+ }
252
+ recheckManagedDirectory(managed.directory);
253
+ const currentRead = readManagedFileDetails(managed.file);
254
+ const current = currentRead && currentRead.content;
255
+ if (current !== null) {
256
+ if (current !== value) {
257
+ assertManagedFileStable(managed.file, currentRead.stat, current, managed.directory);
258
+ throw new Error(`managed runtime file already exists with different content: ${managed.file}`);
99
259
  }
260
+ // The first check binds the pathname to the inode that was read. Repeat
261
+ // it immediately before returning so an intervening successor cannot be
262
+ // mistaken for the idempotent managed file.
263
+ assertManagedFileStable(managed.file, currentRead.stat, current, managed.directory);
264
+ assertManagedFileStable(managed.file, currentRead.stat, current, managed.directory);
265
+ return managed.file;
266
+ }
267
+ recheckManagedDirectory(managed.directory);
268
+
269
+ const temp = `${managed.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
270
+ let tempIdentity = null;
271
+ let fd = null;
272
+ try {
273
+ fd = fs.openSync(
274
+ temp,
275
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0),
276
+ 0o600,
277
+ );
278
+ const opened = fs.fstatSync(fd);
279
+ if (!opened.isFile() || opened.nlink !== 1) throw new Error(`managed runtime temp file is unsafe: ${managed.file}`);
280
+ tempIdentity = { dev: opened.dev, ino: opened.ino };
281
+ let offset = 0;
282
+ while (offset < bytes.length) {
283
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
284
+ if (!Number.isInteger(written) || written <= 0) throw new Error(`managed runtime file write stalled: ${managed.file}`);
285
+ offset += written;
286
+ }
287
+ try { fs.fchmodSync(fd, 0o600); } catch { /* Windows/ACL-only host */ }
288
+ fs.fsyncSync(fd);
289
+ const finished = fs.fstatSync(fd);
290
+ if (
291
+ !sameManagedFileIdentity(finished, tempIdentity) || finished.nlink !== 1 ||
292
+ finished.size !== bytes.length
293
+ ) throw new Error(`managed runtime temp file changed while writing: ${managed.file}`);
100
294
  } finally {
101
- try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
295
+ if (fd !== null) fs.closeSync(fd);
296
+ }
297
+
298
+ try {
299
+ const listedTemp = fs.lstatSync(temp);
300
+ if (!sameManagedFileIdentity(listedTemp, tempIdentity) || listedTemp.nlink !== 1 || listedTemp.size !== bytes.length) {
301
+ throw new Error(`managed runtime temp file changed before publication: ${managed.file}`);
302
+ }
303
+ recheckManagedDirectory(managed.directory);
304
+ // link(2) is an atomic create-if-absent publication on POSIX and never
305
+ // replaces a destination. Windows has no safe overwrite fallback here:
306
+ // linkSync failure is deliberately surfaced and the successor is kept.
307
+ fs.linkSync(temp, managed.file);
308
+ const linked = fs.lstatSync(managed.file);
309
+ if (!sameManagedFileIdentity(linked, tempIdentity) || linked.nlink < 2 || linked.size !== bytes.length) {
310
+ throw new Error(`managed runtime file publication produced an unsafe target: ${managed.file}`);
311
+ }
312
+ removeOwnedManagedTemp(temp, tempIdentity, managed.directory);
313
+ const published = existingManagedFileStat(managed.file);
314
+ if (!published || !sameManagedFileIdentity(published, tempIdentity) || published.size !== bytes.length) {
315
+ throw new Error(`managed runtime file publication changed while finishing: ${managed.file}`);
316
+ }
317
+ recheckManagedDirectory(managed.directory);
318
+ return managed.file;
319
+ } catch (error) {
320
+ // If the directory moved to a symlink during publication, the canonical
321
+ // path can temporarily resolve outside the managed tree. Remove only our
322
+ // inode; a successor at the same path remains untouched.
323
+ removeOwnedManagedTemp(managed.file, tempIdentity, managed.directory);
324
+ throw error;
325
+ } finally {
326
+ removeOwnedManagedTemp(temp, tempIdentity, managed.directory);
102
327
  }
103
- try { fs.chmodSync(file, 0o600); } catch { /* Windows/best-effort */ }
104
328
  }
105
329
 
106
330
  function prepareCodexRuntimeEnv(env = process.env) {
@@ -110,10 +334,9 @@ function prepareCodexRuntimeEnv(env = process.env) {
110
334
  const explicitTarget = Boolean(base.AGENTLAS_CODEX_HOME);
111
335
  const targetHome = path.resolve(base.AGENTLAS_CODEX_HOME || path.join(dataHome, "runtime-homes", "codex"));
112
336
  fs.mkdirSync(dataHome, { recursive: true, mode: 0o700 });
113
- fs.mkdirSync(targetHome, { recursive: true, mode: 0o700 });
114
- try { fs.chmodSync(targetHome, 0o700); } catch { /* Windows/best-effort */ }
337
+ const targetDirectory = assertManagedDirectory(targetHome);
115
338
  const realDataHome = fs.realpathSync(dataHome);
116
- const realTargetHome = fs.realpathSync(targetHome);
339
+ const realTargetHome = targetDirectory.realPath;
117
340
  let realSourceHome = sourceHome;
118
341
  try { realSourceHome = fs.realpathSync(sourceHome); } catch (error) { if (!error || error.code !== "ENOENT") throw error; }
119
342
  const targetRelative = path.relative(realDataHome, realTargetHome);
@@ -127,9 +350,11 @@ function prepareCodexRuntimeEnv(env = process.env) {
127
350
  // CODEX_HOME has no replace-config CLI flag: profiles and `mcp_servers={}`
128
351
  // merge with the user's global config. A dedicated home is the only reliable
129
352
  // way to exclude global/project/plugin MCP while keeping Agentlas sessions.
353
+ const configLocation = managedFileLocation(path.join(targetHome, "config.toml"), targetDirectory);
130
354
  writeManagedFile(
131
- path.join(targetHome, "config.toml"),
355
+ configLocation.file,
132
356
  "# Managed by Agentlas Terminal. MCP is supplied only for explicit full-access turns.\n",
357
+ configLocation,
133
358
  );
134
359
 
135
360
  if (sourceHome !== targetHome) {
@@ -191,26 +416,16 @@ function wrappedMcpServerMap(servers, options = {}) {
191
416
  // stdio servers. Empty means empty; there is no legacy or provider seed.
192
417
  function cliMcpConfigPath(servers, options = {}) {
193
418
  const dir = path.join(userDataDir(options.env || process.env), "mcp");
194
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
195
- try { fs.chmodSync(dir, 0o700); } catch { /* Windows/best-effort */ }
419
+ const directory = assertManagedDirectory(dir);
196
420
  const mcpServers = wrappedMcpServerMap(servers, options);
197
421
  const body = JSON.stringify({ mcpServers }, null, 2);
198
422
  // 서로 다른 동시 실행이 하나의 agentlas-cli-mcp.json을 덮어쓰지 않도록 내용 주소 파일을 쓴다.
199
423
  const digest = crypto.createHash("sha256").update(body).digest("hex").slice(0, 20);
200
- const file = path.join(dir, `agentlas-cli-mcp-${digest}.json`);
201
- let current = null;
202
- try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
203
- if (current !== body) {
204
- const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
205
- try {
206
- fs.writeFileSync(temp, body, { encoding: "utf8", mode: 0o600, flag: "wx" });
207
- fs.renameSync(temp, file);
208
- } finally {
209
- try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
210
- }
211
- }
212
- try { fs.chmodSync(file, 0o600); } catch { /* Windows/best-effort */ }
213
- return { file, names: Object.keys(mcpServers) };
424
+ const location = managedFileLocation(path.join(dir, `agentlas-cli-mcp-${digest}.json`), directory);
425
+ // Always delegate the idempotent case to the identity-checked writer; a
426
+ // caller-side read followed by an early return would reopen a TOCTOU gap.
427
+ writeManagedFile(location.file, body, location);
428
+ return { file: location.file, names: Object.keys(mcpServers) };
214
429
  }
215
430
  // Full-access turns only: codex -c mcp_servers.* with the same exact Build
216
431
  // allowlist semantics as Claude.
@@ -246,11 +461,24 @@ function summarizeToolInput(name, input) {
246
461
  }
247
462
 
248
463
  // child.stdout → 줄 단위 콜백. 종료 시 잔여 버퍼 flush. cleanup 반환.
249
- function lineReader(stream, onLine, onActivity) {
464
+ function lineReader(stream, onLine, onActivity, options = {}) {
250
465
  let buf = "";
466
+ let totalBytes = 0;
467
+ let limited = false;
468
+ const maxBytes = Number.isFinite(Number(options.maxBytes))
469
+ ? Math.max(1, Math.trunc(Number(options.maxBytes)))
470
+ : NATIVE_STDOUT_MAX_BYTES;
251
471
  stream.setEncoding("utf8");
252
472
  const onData = (chunk) => {
473
+ if (limited) return;
253
474
  if (onActivity) onActivity();
475
+ totalBytes += Buffer.byteLength(chunk, "utf8");
476
+ if (totalBytes > maxBytes) {
477
+ limited = true;
478
+ buf = "";
479
+ if (typeof options.onLimit === "function") options.onLimit(maxBytes);
480
+ return;
481
+ }
254
482
  buf += chunk;
255
483
  let nl;
256
484
  while ((nl = buf.indexOf("\n")) >= 0) {
@@ -260,7 +488,7 @@ function lineReader(stream, onLine, onActivity) {
260
488
  }
261
489
  };
262
490
  const onEnd = () => {
263
- if (buf.trim()) onLine(buf);
491
+ if (!limited && buf.trim()) onLine(buf);
264
492
  };
265
493
  stream.on("data", onData);
266
494
  stream.on("end", onEnd);
@@ -661,16 +889,15 @@ function prepareGeminiRuntimeEnv(env = process.env, options = {}) {
661
889
  throw error;
662
890
  }
663
891
  const dir = path.join(userDataDir(base), "mcp");
664
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
665
- try { fs.chmodSync(dir, 0o700); } catch { /* Windows/best-effort */ }
892
+ const directory = assertManagedDirectory(dir);
666
893
  const names = Object.keys(mcpServers);
667
894
  const body = JSON.stringify({ mcpServers, mcp: { allowed: names } }, null, 2);
668
895
  const digest = crypto.createHash("sha256").update(body).digest("hex").slice(0, 20);
669
- const file = path.join(dir, `agentlas-gemini-mcp-${digest}.json`);
670
- let current = null;
671
- try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
672
- if (current !== body) writeManagedFile(file, body);
673
- base.GEMINI_CLI_SYSTEM_SETTINGS_PATH = file;
896
+ const location = managedFileLocation(path.join(dir, `agentlas-gemini-mcp-${digest}.json`), directory);
897
+ // Keep the idempotent read and return inside writeManagedFile's identity
898
+ // checks; do not leave a caller-side read/early-return race here.
899
+ writeManagedFile(location.file, body, location);
900
+ base.GEMINI_CLI_SYSTEM_SETTINGS_PATH = location.file;
674
901
  return base;
675
902
  }
676
903
 
@@ -1042,7 +1269,7 @@ function runNativeTurn(req) {
1042
1269
  session: st.session,
1043
1270
  usage: st.usage,
1044
1271
  error: termination ? termination.message : "native runtime stopped",
1045
- errorKind: "timeout",
1272
+ errorKind: termination?.reason === "output" ? "output_limit" : "timeout",
1046
1273
  errorSource: "marker",
1047
1274
  terminationVerified: Boolean(termination?.verified),
1048
1275
  });
@@ -1065,7 +1292,11 @@ function runNativeTurn(req) {
1065
1292
  };
1066
1293
  const requestStop = (reason) => {
1067
1294
  if (termination || settled) return;
1068
- const message = reason === "abort" ? "aborted" : nativeTimeoutMessage(reason, reason === "idle" ? timeout.idleMs : timeout.totalMs);
1295
+ const message = reason === "abort"
1296
+ ? "aborted"
1297
+ : reason === "output"
1298
+ ? `native runtime stdout exceeded ${NATIVE_STDOUT_MAX_BYTES} bytes`
1299
+ : nativeTimeoutMessage(reason, reason === "idle" ? timeout.idleMs : timeout.totalMs);
1069
1300
  termination = { reason, message };
1070
1301
  st.error = message;
1071
1302
  st.errorShown = true;
@@ -1105,9 +1336,12 @@ function runNativeTurn(req) {
1105
1336
  }
1106
1337
  ui.streamDelta(l + "\n");
1107
1338
  st.text += l + "\n";
1108
- }, markActivity);
1339
+ }, markActivity, { maxBytes: NATIVE_STDOUT_MAX_BYTES, onLimit: () => requestStop("output") });
1109
1340
  } else {
1110
- removeLineReader = lineReader(child.stdout, lineHandler, markActivity);
1341
+ removeLineReader = lineReader(child.stdout, lineHandler, markActivity, {
1342
+ maxBytes: NATIVE_STDOUT_MAX_BYTES,
1343
+ onLimit: () => requestStop("output"),
1344
+ });
1111
1345
  }
1112
1346
 
1113
1347
  child.stderr.setEncoding("utf8");
@@ -1345,4 +1579,5 @@ module.exports = {
1345
1579
  activeNativeProcessIds,
1346
1580
  forceStopNativeProcessTree,
1347
1581
  terminateNativeChild,
1582
+ NATIVE_STDOUT_MAX_BYTES,
1348
1583
  };
@@ -37,10 +37,19 @@ async function runOnboard({ ui, rl, helpers, persist }) {
37
37
  };
38
38
  const pendingLines = [];
39
39
  let lineWaiter = null;
40
+ let lineRejecter = null;
41
+ let inputClosed = Boolean(rl.closed);
42
+ const inputClosedError = () => Object.assign(new Error(
43
+ ui.lang === "ko"
44
+ ? "입력이 닫혀 설정을 완료하지 못했습니다. setup을 다시 실행하세요."
45
+ : "Setup input closed before all choices were entered. Run setup again.",
46
+ ), { code: "SETUP_INPUT_CLOSED", reason: "eof", honestStop: true });
40
47
  const onLine = (line) => {
48
+ if (inputClosed) return;
41
49
  if (lineWaiter) {
42
50
  const resolve = lineWaiter;
43
51
  lineWaiter = null;
52
+ lineRejecter = null;
44
53
  resolve((line || "").trim());
45
54
  } else {
46
55
  // A pasted multi-line setup response can arrive in one terminal data
@@ -48,11 +57,22 @@ async function runOnboard({ ui, rl, helpers, persist }) {
48
57
  pendingLines.push((line || "").trim());
49
58
  }
50
59
  };
60
+ const onClose = () => {
61
+ inputClosed = true;
62
+ if (!lineRejecter) return;
63
+ const reject = lineRejecter;
64
+ lineWaiter = null;
65
+ lineRejecter = null;
66
+ reject(inputClosedError());
67
+ };
51
68
  rl.on("line", onLine);
69
+ rl.on("close", onClose);
52
70
  const ask = (q) => {
53
71
  if (pendingLines.length) return Promise.resolve(pendingLines.shift());
54
- return new Promise((resolve) => {
72
+ if (inputClosed) return Promise.reject(inputClosedError());
73
+ return new Promise((resolve, reject) => {
55
74
  lineWaiter = resolve;
75
+ lineRejecter = reject;
56
76
  rl.setPrompt(q);
57
77
  rl.prompt();
58
78
  });
@@ -198,8 +218,10 @@ async function runOnboard({ ui, rl, helpers, persist }) {
198
218
  return { onboarded: true, saved: !saveError, saveError, lang, runtime, permission };
199
219
  } finally {
200
220
  rl.removeListener("line", onLine);
221
+ rl.removeListener("close", onClose);
201
222
  if (ownsSigint) rl.removeListener("SIGINT", onSigint);
202
223
  lineWaiter = null;
224
+ lineRejecter = null;
203
225
  }
204
226
  }
205
227
 
@@ -57,7 +57,11 @@ function normalize(value, fallback = "read") {
57
57
  }
58
58
 
59
59
  function persistent(value, fallback = "write") {
60
- const level = normalize(value, fallback);
60
+ const raw = String(value ?? "").trim();
61
+ // Missing is a product default; malformed is damaged state. Treating both as
62
+ // `fallback` let a corrupt persisted value become write-capable whenever the
63
+ // caller's normal first-run default was write.
64
+ const level = raw ? normalize(raw, "read") : normalize(fallback, "read");
61
65
  if (level !== "full") return level;
62
66
  const safeFallback = normalize(fallback, "write");
63
67
  return safeFallback === "full" ? "write" : safeFallback;
@@ -38,9 +38,47 @@ function configureSqliteConnection(db) {
38
38
  return db;
39
39
  }
40
40
 
41
+ let fallbackSavepointCounter = 0;
42
+
41
43
  function runWriteTransaction(db, fn, ...args) {
42
- if (!db || typeof db.transaction !== "function") return fn(...args);
43
- const transaction = db.transaction(fn);
44
+ if (!db || typeof fn !== "function") throw new TypeError("SQLite connection and transaction callback are required");
45
+ const invoke = (...invokeArgs) => {
46
+ const result = fn(...invokeArgs);
47
+ if (result && typeof result.then === "function") {
48
+ throw new TypeError("SQLite write transactions must be synchronous");
49
+ }
50
+ return result;
51
+ };
52
+ if (typeof db.transaction !== "function") {
53
+ if (typeof db.exec !== "function") throw new TypeError("SQLite connection must expose transaction() or exec()");
54
+ // node:sqlite DatabaseSync has no transaction() helper. Execute the same
55
+ // atomic contract explicitly; nested callers use a SAVEPOINT so an outer
56
+ // transaction keeps ownership, while top-level writers acquire authority
57
+ // up front with BEGIN IMMEDIATE.
58
+ if (db.isTransaction === true) {
59
+ const savepoint = `agentlas_write_${process.pid}_${++fallbackSavepointCounter}`;
60
+ db.exec(`SAVEPOINT ${savepoint}`);
61
+ try {
62
+ const result = invoke(...args);
63
+ db.exec(`RELEASE SAVEPOINT ${savepoint}`);
64
+ return result;
65
+ } catch (error) {
66
+ try { db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`); } catch { /* preserve original error */ }
67
+ try { db.exec(`RELEASE SAVEPOINT ${savepoint}`); } catch { /* outer transaction may already be gone */ }
68
+ throw error;
69
+ }
70
+ }
71
+ db.exec("BEGIN IMMEDIATE");
72
+ try {
73
+ const result = invoke(...args);
74
+ db.exec("COMMIT");
75
+ return result;
76
+ } catch (error) {
77
+ try { db.exec("ROLLBACK"); } catch { /* preserve original error */ }
78
+ throw error;
79
+ }
80
+ }
81
+ const transaction = db.transaction(invoke);
44
82
  if (typeof transaction.immediate === "function") {
45
83
  return transaction.immediate(...args);
46
84
  }
@@ -96,6 +96,13 @@ function applyTaskResult(current, name, payload, toolId) {
96
96
  const id = String(record.id || record.taskId || payload.taskId || "");
97
97
  if (!id) return current;
98
98
  const next = Array.isArray(current) ? current.map((task) => ({ ...task })) : [];
99
+ // Claude emits the successful tool result after the TaskUpdate input. A
100
+ // delete result must remain a delete: otherwise the input removes the row,
101
+ // then this result-only path recreates it as a pending placeholder.
102
+ const resultStatus = record.status ?? payload.status;
103
+ if (tool === "taskupdate" && String(resultStatus || "").trim().toLowerCase() === "deleted") {
104
+ return next.filter((task) => task.id !== id);
105
+ }
99
106
  const provisional = next.findIndex((task) => task.id === String(toolId || ""));
100
107
  const existing = next.findIndex((task) => task.id === id);
101
108
  const index = existing >= 0 ? existing : provisional;