agentlas 1.0.65 → 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 (163) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +6 -4
  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/browser/cdp.cjs +190 -17
  43. package/engine/browser/vault.cjs +1 -0
  44. package/engine/cli-output.cjs +80 -28
  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 +308 -113
  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 +42 -12
  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 +8 -2
  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 +29 -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 +230 -61
  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 +15 -5
  118. package/engine/mcp/plan.cjs +5 -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 +212 -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
@@ -5,6 +5,7 @@
5
5
  * Persistence is owned by Desktop migration v79. Terminal reads defensively:
6
6
  * orchestrator row -> legacy active_runtime -> null
7
7
  * worker direct row -> orchestrator row/legacy active_runtime
8
+ * multimodal direct row -> null (never inherits a conversational runtime)
8
9
  *
9
10
  * The worker may inherit upward for quality; the orchestrator never falls
10
11
  * downward to the worker row.
@@ -13,7 +14,7 @@ const { canonicalRuntimeKind } = require("./kinds.cjs");
13
14
  const { tableExists, columnExists } = require("../core/db.cjs");
14
15
 
15
16
  const MODEL_ROLE_TABLE = "model_roles";
16
- const VALID_ROLES = new Set(["orchestrator", "worker"]);
17
+ const VALID_ROLES = new Set(["orchestrator", "worker", "multimodal"]);
17
18
 
18
19
  function cleanText(value) {
19
20
  const trimmed = typeof value === "string" ? value.trim() : "";
@@ -136,17 +137,18 @@ function resolvedModelRole(db, role = "orchestrator") {
136
137
  if (role === "orchestrator") {
137
138
  return normalizedRow(roleRow(db, "orchestrator"), role) || legacyOrchestrator(db);
138
139
  }
139
- const worker = normalizedRow(roleRow(db, "worker"), "worker");
140
- if (worker && !worker.inherit) return worker;
140
+ const direct = normalizedRow(roleRow(db, role), role);
141
+ if (role === "multimodal") return direct;
142
+ if (direct && !direct.inherit) return direct;
141
143
  const orchestrator = resolvedModelRole(db, "orchestrator");
142
144
  if (!orchestrator) return null;
143
145
  return {
144
146
  ...orchestrator,
145
147
  role: "worker",
146
148
  inherit: true,
147
- updatedAt: worker?.updatedAt || orchestrator.updatedAt,
149
+ updatedAt: direct?.updatedAt || orchestrator.updatedAt,
148
150
  sourceLayer:
149
- worker?.sourceLayer === "model-role"
151
+ direct?.sourceLayer === "model-role"
150
152
  ? "model-role-inherit"
151
153
  : "active-runtime-inherit",
152
154
  };
@@ -14,74 +14,412 @@
14
14
  * - asks: 세션 이벤트로만 표면화한다. UI 는 REPL 렌더러 소관.
15
15
  */
16
16
 
17
+ const crypto = require("node:crypto");
18
+ const fs = require("node:fs");
19
+ const os = require("node:os");
20
+ const path = require("node:path");
17
21
  const memoryCurate = require("../memory-cli/curate.cjs");
18
22
  const automationStore = require("../automation/store.cjs");
23
+ const permissions = require("../agentlas-permissions.cjs");
19
24
  const schedule = require("../automation/schedule.cjs");
20
25
 
21
26
  /** One 이 가져가는 스코프. 프로젝트 스코프는 프로젝트에 남는다. */
22
27
  const ONE_SCOPES = new Set(["agent_repo", "user_identity"]);
28
+ const ONE_STATE_MAX_BYTES = 64 * 1024;
29
+ const ONE_LEDGER_MAX_BYTES = 16 * 1024 * 1024;
30
+ const ONE_MAX_CANDIDATES = 64;
31
+ const ONE_MAX_CONTENT_CHARS = 4_000;
32
+ const ONE_TICKET_CONTENT_CHARS = 600;
33
+ const ONE_MAX_EVIDENCE_ITEMS = 8;
34
+ const ONE_MAX_EVIDENCE_ITEM_CHARS = 500;
35
+ const ONE_MAX_TICKET_BYTES = 16 * 1024;
36
+ const ONE_LOCK_STALE_MS = 30_000;
37
+ const ONE_LOCK_ATTEMPTS = 50;
38
+ const ONE_LOCK_RETRY_MS = 20;
39
+ const ONE_LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
40
+
41
+ function oneWaitSync(milliseconds) {
42
+ Atomics.wait(ONE_LOCK_WAIT, 0, 0, milliseconds);
43
+ }
44
+
45
+ function oneHash(value) {
46
+ return crypto.createHash("sha256").update(String(value || ""), "utf8").digest("hex");
47
+ }
48
+
49
+ function oneContent(value) {
50
+ return typeof value === "string" ? value.trim().slice(0, ONE_MAX_CONTENT_CHARS) : "";
51
+ }
52
+
53
+ function oneContentKey(value) {
54
+ return oneContent(value).toLowerCase().replace(/\s+/g, " ");
55
+ }
56
+
57
+ function oneBoundedText(value, maxChars) {
58
+ return typeof value === "string" && value.trim() ? value.trim().slice(0, maxChars) : undefined;
59
+ }
60
+
61
+ function oneBoundedRequestContext(value) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
63
+ const out = {};
64
+ const userIntent = oneBoundedText(value.user_intent, 240);
65
+ const outcome = value.outcome === null ? null : oneBoundedText(value.outcome, 240);
66
+ const cwd = value.cwd_at_request === null ? null : oneBoundedText(value.cwd_at_request, 500);
67
+ const targetProject = value.target_project === null ? null : oneBoundedText(value.target_project, 120);
68
+ const targetPath = value.target_path === null ? null : oneBoundedText(value.target_path, 500);
69
+ const triggerTerms = Array.isArray(value.trigger_terms)
70
+ ? value.trigger_terms.filter((item) => typeof item === "string")
71
+ .map((item) => item.trim().slice(0, 40)).filter(Boolean).slice(0, 12)
72
+ : [];
73
+ if (userIntent) out.user_intent = userIntent;
74
+ if (outcome !== undefined) out.outcome = outcome;
75
+ if (cwd !== undefined) out.cwd_at_request = cwd;
76
+ if (targetProject !== undefined) out.target_project = targetProject;
77
+ if (targetPath !== undefined) out.target_path = targetPath;
78
+ if (triggerTerms.length) out.trigger_terms = [...new Set(triggerTerms)];
79
+ if (typeof value.cross_context === "boolean") out.cross_context = value.cross_context;
80
+ return Object.keys(out).length ? out : undefined;
81
+ }
82
+
83
+ /** Bound untrusted memory-event data before curation's wire JSON is serialized. */
84
+ function boundMemoryEvents(events) {
85
+ const bounded = [];
86
+ let count = 0;
87
+ outer:
88
+ for (const raw of (Array.isArray(events) ? events : [])) {
89
+ const candidates = raw && Array.isArray(raw.candidates) ? raw.candidates : [raw];
90
+ for (const candidate of candidates) {
91
+ if (count >= ONE_MAX_CANDIDATES) break outer;
92
+ count += 1;
93
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
94
+ const item = {};
95
+ const content = oneContent(candidate.content);
96
+ const memoryKind = oneBoundedText(candidate.memory_kind, 64);
97
+ const suggestedScope = oneBoundedText(candidate.suggested_scope, 64);
98
+ const confidence = oneBoundedText(candidate.confidence, 32);
99
+ const sensitivity = oneBoundedText(candidate.sensitivity, 32);
100
+ const evidenceRefs = Array.isArray(candidate.evidence_refs)
101
+ ? candidate.evidence_refs.filter((value) => typeof value === "string")
102
+ .map((value) => value.trim().slice(0, ONE_MAX_EVIDENCE_ITEM_CHARS))
103
+ .filter(Boolean).slice(0, ONE_MAX_EVIDENCE_ITEMS)
104
+ : [];
105
+ if (content) item.content = content;
106
+ if (memoryKind) item.memory_kind = memoryKind;
107
+ if (suggestedScope) item.suggested_scope = suggestedScope;
108
+ if (confidence) item.confidence = confidence;
109
+ if (sensitivity) item.sensitivity = sensitivity;
110
+ if (evidenceRefs.length) item.evidence_refs = evidenceRefs;
111
+ const requestContext = oneBoundedRequestContext(candidate.request_context);
112
+ if (requestContext) item.request_context = requestContext;
113
+ bounded.push(item);
114
+ }
115
+ }
116
+ return bounded;
117
+ }
118
+
119
+ function oneSafeRegularStat(stat, maxBytes) {
120
+ return Boolean(
121
+ stat && stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 &&
122
+ Number.isSafeInteger(stat.size) && stat.size >= 0 && stat.size <= maxBytes,
123
+ );
124
+ }
125
+
126
+ function oneSameFile(before, after) {
127
+ return before.dev === after.dev && before.ino === after.ino;
128
+ }
129
+
130
+ /** Read a bounded regular file through an fd, refusing link and replacement races. */
131
+ function readOneFile(filePath, maxBytes, { allowMissing = false } = {}) {
132
+ let before;
133
+ try {
134
+ before = fs.lstatSync(filePath);
135
+ } catch (error) {
136
+ if (allowMissing && error && error.code === "ENOENT") return null;
137
+ throw error;
138
+ }
139
+ if (!oneSafeRegularStat(before, maxBytes)) throw new Error("One state file is not a bounded regular file");
140
+
141
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
142
+ let fd;
143
+ try {
144
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
145
+ } catch (error) {
146
+ if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) throw error;
147
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY);
148
+ }
149
+ try {
150
+ const opened = fs.fstatSync(fd);
151
+ if (!oneSafeRegularStat(opened, maxBytes) || !oneSameFile(before, opened)) {
152
+ throw new Error("One file changed while opening");
153
+ }
154
+ const chunks = [];
155
+ let total = 0;
156
+ while (total <= maxBytes) {
157
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
158
+ const count = fs.readSync(fd, buffer, 0, buffer.length, null);
159
+ if (!count) break;
160
+ chunks.push(buffer.subarray(0, count));
161
+ total += count;
162
+ }
163
+ if (total > maxBytes) throw new Error("One file exceeds its safety limit");
164
+ const after = fs.fstatSync(fd);
165
+ if (
166
+ !oneSafeRegularStat(after, maxBytes) || !oneSameFile(opened, after) ||
167
+ after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
168
+ total !== after.size
169
+ ) {
170
+ throw new Error("One file changed while reading");
171
+ }
172
+ try {
173
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
174
+ } catch {
175
+ throw new Error("One file must contain valid UTF-8");
176
+ }
177
+ } finally {
178
+ fs.closeSync(fd);
179
+ }
180
+ }
181
+
182
+ function oneSafeDirectory(directory) {
183
+ let stat;
184
+ try { stat = fs.lstatSync(directory); } catch { return false; }
185
+ return stat.isDirectory() && !stat.isSymbolicLink();
186
+ }
187
+
188
+ function oneProcessIsAlive(pid) {
189
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
190
+ try {
191
+ process.kill(pid, 0);
192
+ return true;
193
+ } catch (error) {
194
+ return !error || error.code !== "ESRCH";
195
+ }
196
+ }
197
+
198
+ function oneLockOwner(lockPath) {
199
+ let raw;
200
+ try { raw = readOneFile(path.join(lockPath, "owner.json"), 512, { allowMissing: true }); } catch { return null; }
201
+ if (raw == null) return null;
202
+ try {
203
+ const owner = JSON.parse(raw);
204
+ if (
205
+ !owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 ||
206
+ typeof owner.nonce !== "string" || !/^[a-f0-9]{32}$/.test(owner.nonce)
207
+ ) return null;
208
+ return owner;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function oneQuarantineLock(lockPath, label) {
215
+ const quarantine = `${lockPath}.${label}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
216
+ try {
217
+ fs.renameSync(lockPath, quarantine);
218
+ } catch (error) {
219
+ if (error && error.code === "ENOENT") return false;
220
+ throw error;
221
+ }
222
+ try { fs.rmSync(quarantine, { recursive: true, force: true }); } catch { /* best effort */ }
223
+ return true;
224
+ }
225
+
226
+ /** Acquire a directory lock; stale takeover proves the recorded owner is gone. */
227
+ function acquireOneLedgerLock(lockPath) {
228
+ for (let attempt = 0; attempt < ONE_LOCK_ATTEMPTS; attempt += 1) {
229
+ let created = false;
230
+ try {
231
+ fs.mkdirSync(lockPath, { mode: 0o700 });
232
+ created = true;
233
+ const nonce = crypto.randomBytes(16).toString("hex");
234
+ fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify({
235
+ pid: process.pid,
236
+ nonce,
237
+ createdAt: new Date().toISOString(),
238
+ }) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
239
+ return { lockPath, pid: process.pid, nonce };
240
+ } catch (error) {
241
+ if (created) {
242
+ try { oneQuarantineLock(lockPath, "abandoned"); } catch { /* best effort */ }
243
+ return null;
244
+ }
245
+ if (!error || (error.code !== "EEXIST" && error.code !== "ENOENT")) return null;
246
+ let stat;
247
+ try { stat = fs.lstatSync(lockPath); } catch (statError) {
248
+ if (statError && statError.code === "ENOENT") continue;
249
+ return null;
250
+ }
251
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
252
+ const owner = oneLockOwner(lockPath);
253
+ if (
254
+ Number.isFinite(stat.mtimeMs) && Date.now() - stat.mtimeMs > ONE_LOCK_STALE_MS &&
255
+ owner && !oneProcessIsAlive(owner.pid)
256
+ ) {
257
+ try {
258
+ oneQuarantineLock(lockPath, "stale");
259
+ continue;
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+ oneWaitSync(ONE_LOCK_RETRY_MS);
265
+ }
266
+ }
267
+ return null;
268
+ }
269
+
270
+ /** Release only this holder's lock; a successor's path is never recursively removed. */
271
+ function releaseOneLedgerLock(lock) {
272
+ let stat;
273
+ try { stat = fs.lstatSync(lock.lockPath); } catch { return; }
274
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
275
+ const owner = oneLockOwner(lock.lockPath);
276
+ if (!owner || owner.pid !== lock.pid || owner.nonce !== lock.nonce) return;
277
+ try {
278
+ if (oneQuarantineLock(lock.lockPath, "done")) return;
279
+ } catch { /* retain the lock for stale recovery; never delete an unknown owner */ }
280
+ }
281
+
282
+ function withOneLedgerLock(lockPath, action) {
283
+ const lock = acquireOneLedgerLock(lockPath);
284
+ if (!lock) return null;
285
+ try {
286
+ return action();
287
+ } finally {
288
+ releaseOneLedgerLock(lock);
289
+ }
290
+ }
291
+
292
+ function readExistingOneKeys(raw) {
293
+ const seen = new Set();
294
+ for (const line of String(raw || "").split(/\r?\n/)) {
295
+ if (!line.trim() || Buffer.byteLength(line, "utf8") > ONE_MAX_TICKET_BYTES) continue;
296
+ try {
297
+ const row = JSON.parse(line);
298
+ const content = row && row.candidate && row.candidate.content;
299
+ const key = oneContentKey(content);
300
+ if (key) seen.add(key);
301
+ } catch { /* preserve unrelated malformed local lines */ }
302
+ }
303
+ return seen;
304
+ }
305
+
306
+ function appendOneLedgerRecord(ledger, record) {
307
+ const encoded = JSON.stringify(record);
308
+ const payload = Buffer.from(encoded + "\n", "utf8");
309
+ const payloadBytes = payload.byteLength;
310
+ if (payloadBytes > ONE_MAX_TICKET_BYTES) return false;
311
+ let before;
312
+ try { before = fs.lstatSync(ledger); } catch { return false; }
313
+ if (!oneSafeRegularStat(before, ONE_LEDGER_MAX_BYTES) || before.size + payloadBytes > ONE_LEDGER_MAX_BYTES) return false;
314
+
315
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
316
+ let fd;
317
+ try {
318
+ fd = fs.openSync(ledger, fs.constants.O_WRONLY | fs.constants.O_APPEND | noFollow, 0o600);
319
+ } catch (error) {
320
+ if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) return false;
321
+ try { fd = fs.openSync(ledger, fs.constants.O_WRONLY | fs.constants.O_APPEND, 0o600); } catch { return false; }
322
+ }
323
+ try {
324
+ const opened = fs.fstatSync(fd);
325
+ if (
326
+ !oneSafeRegularStat(opened, ONE_LEDGER_MAX_BYTES) || !oneSameFile(before, opened) ||
327
+ opened.size + payloadBytes > ONE_LEDGER_MAX_BYTES
328
+ ) return false;
329
+ let offset = 0;
330
+ while (offset < payloadBytes) {
331
+ const written = fs.writeSync(fd, payload, offset, payloadBytes - offset, null);
332
+ if (!written) return false;
333
+ offset += written;
334
+ }
335
+ fs.fsyncSync(fd);
336
+ const after = fs.fstatSync(fd);
337
+ return (
338
+ oneSafeRegularStat(after, ONE_LEDGER_MAX_BYTES) && oneSameFile(opened, after) &&
339
+ after.size === opened.size + payloadBytes
340
+ );
341
+ } finally {
342
+ fs.closeSync(fd);
343
+ }
344
+ }
23
345
 
24
346
  /**
25
347
  * Agentlas One 서랍(`~/.agentlas/one/.agentlas/memory-tickets.jsonl`)으로 후보를 넘긴다.
26
348
  *
27
349
  * One 은 프로젝트를 넘나드는 정체성이라 agent_repo/user_identity 만 가져간다.
28
350
  * One 이 꺼져 있거나 서랍이 없으면 아무것도 하지 않는다 — 없는 폴더를 만들지 않는다.
351
+ * permission 이 read 이거나 입력/서랍 경계가 안전하지 않으면 아무것도 쓰지 않는다.
29
352
  * 실패해도 턴을 죽이지 않는다(펜스 적용 계약과 동일).
30
353
  */
31
- function forwardToOne(events) {
354
+ function forwardToOne(events, permission = "read") {
32
355
  try {
33
- const fs = require("node:fs");
34
- const os = require("node:os");
35
- const path = require("node:path");
36
- const root = process.env.AGENTLAS_ONE_DIR || path.join(os.homedir(), ".agentlas", "one");
37
- const state = JSON.parse(fs.readFileSync(path.join(root, "state.json"), "utf8"));
356
+ const normalizedPermission = permissions.normalize(
357
+ permission && typeof permission === "object" ? permission.permission : permission,
358
+ );
359
+ if (normalizedPermission === "read") return 0;
360
+ const root = path.resolve(process.env.AGENTLAS_ONE_DIR || path.join(os.homedir(), ".agentlas", "one"));
361
+ if (!oneSafeDirectory(root)) return 0;
362
+ const stateRaw = readOneFile(path.join(root, "state.json"), ONE_STATE_MAX_BYTES, { allowMissing: true });
363
+ if (stateRaw == null) return 0;
364
+ const state = JSON.parse(stateRaw);
38
365
  if (!state || state.on !== true) return 0;
366
+ const stateDir = path.join(root, ".agentlas");
367
+ if (!oneSafeDirectory(stateDir)) return 0;
39
368
  const ledger = path.join(root, ".agentlas", "memory-tickets.jsonl");
40
- if (!fs.existsSync(ledger)) return 0;
41
-
42
- // 이미 올라온 내용은 다시 넣지 않는다(같은 계약을 One 쪽 emit_ticket 도 쓴다).
43
- const seen = new Set();
44
- for (const line of fs.readFileSync(ledger, "utf8").split("\n")) {
45
- if (!line.trim()) continue;
46
- try {
47
- const row = JSON.parse(line);
48
- const text = String((row.candidate || {}).content || "");
49
- if (text) seen.add(text.trim().toLowerCase().replace(/\s+/g, " "));
50
- } catch { /* 깨진 줄은 건너뛴다 */ }
51
- }
52
-
53
- let written = 0;
54
- for (const raw of events) {
55
- for (const candidate of (raw && Array.isArray(raw.candidates) ? raw.candidates : [raw])) {
56
- if (!candidate || typeof candidate !== "object") continue;
57
- const scope = String(candidate.suggested_scope || candidate.scope || "");
58
- if (!ONE_SCOPES.has(scope)) continue;
59
- const content = String(candidate.content || "").trim();
60
- if (!content) continue;
61
- const key = content.toLowerCase().replace(/\s+/g, " ");
62
- if (seen.has(key)) continue;
63
- seen.add(key);
64
- const evidence = Array.isArray(candidate.evidence) ? candidate.evidence.slice(0, 8) : [];
65
- fs.appendFileSync(ledger, JSON.stringify({
66
- schemaVersion: "agentlas.one-workspace.v1",
67
- ticketId: `one-tkt-${Date.now()}-${written}`,
68
- agentId: "builtin-agentlas-one",
69
- turnKey: "",
70
- source: "terminal-memory-events",
71
- state: "queued",
72
- candidate: {
73
- type: String(candidate.memory_kind || candidate.type || "hypothesis"),
74
- scope,
75
- content: content.slice(0, 600),
76
- evidence,
77
- },
78
- downgraded: false,
79
- createdAt: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
80
- }) + "\n", "utf8");
81
- written += 1;
369
+ let stat;
370
+ try { stat = fs.lstatSync(ledger); } catch { return 0; }
371
+ if (!oneSafeRegularStat(stat, ONE_LEDGER_MAX_BYTES)) return 0;
372
+ const result = withOneLedgerLock(`${ledger}.lock`, () => {
373
+ const ledgerRaw = readOneFile(ledger, ONE_LEDGER_MAX_BYTES);
374
+ if (ledgerRaw == null) return 0;
375
+ const seen = readExistingOneKeys(ledgerRaw);
376
+ let written = 0;
377
+ let candidatesSeen = 0;
378
+ outer:
379
+ for (const raw of (Array.isArray(events) ? events : [])) {
380
+ const candidates = raw && Array.isArray(raw.candidates) ? raw.candidates : [raw];
381
+ for (const candidate of candidates) {
382
+ if (candidatesSeen >= ONE_MAX_CANDIDATES) break outer;
383
+ candidatesSeen += 1;
384
+ if (!candidate || typeof candidate !== "object" || candidate.sensitivity === "secret") continue;
385
+ const scope = String(candidate.suggested_scope || candidate.scope || "");
386
+ if (!ONE_SCOPES.has(scope)) continue;
387
+ const content = oneContent(candidate.content);
388
+ if (!content) continue;
389
+ const ticketContent = content.slice(0, ONE_TICKET_CONTENT_CHARS);
390
+ const key = oneContentKey(ticketContent);
391
+ if (!key || seen.has(key)) continue;
392
+ const evidenceSource = Array.isArray(candidate.evidence)
393
+ ? candidate.evidence
394
+ : Array.isArray(candidate.evidence_refs) ? candidate.evidence_refs : [];
395
+ const evidence = evidenceSource
396
+ .filter((item) => typeof item === "string")
397
+ .map((item) => item.trim().slice(0, ONE_MAX_EVIDENCE_ITEM_CHARS))
398
+ .filter(Boolean)
399
+ .slice(0, ONE_MAX_EVIDENCE_ITEMS);
400
+ const kindValue = candidate.memory_kind || candidate.kind || candidate.type;
401
+ const kind = typeof kindValue === "string" && kindValue.trim()
402
+ ? kindValue.trim().slice(0, 64)
403
+ : "hypothesis";
404
+ const ticket = {
405
+ schemaVersion: "agentlas.one-workspace.v1",
406
+ ticketId: `one-tkt-${oneHash(key)}`,
407
+ agentId: "builtin-agentlas-one",
408
+ turnKey: "",
409
+ source: "terminal-memory-events",
410
+ state: "queued",
411
+ candidate: { type: kind, scope, content: ticketContent, evidence },
412
+ downgraded: false,
413
+ createdAt: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
414
+ };
415
+ if (!appendOneLedgerRecord(ledger, ticket)) return written;
416
+ seen.add(key);
417
+ written += 1;
418
+ }
82
419
  }
83
- }
84
- return written;
420
+ return written;
421
+ });
422
+ return Number.isInteger(result) ? result : 0;
85
423
  } catch {
86
424
  return 0;
87
425
  }
@@ -97,7 +435,8 @@ function applyReplyFences(session, parsed, opts = {}) {
97
435
  const orch = opts.orch || session.orchestrator || null;
98
436
  const record = (ev) => session._record({ at: Date.now(), ...ev });
99
437
  const receipts = { asks: 0, memory: null, automations: [], delegates: [], refused: [] };
100
- const canWrite = session.permission !== "read";
438
+ const permission = permissions.normalize(session.permission);
439
+ const canWrite = permission !== "read";
101
440
 
102
441
  // 파서가 표면화한 오류(잘못된 스케줄 등) — 조용히 삼키지 않는다.
103
442
  for (const err of parsed.errors || []) {
@@ -112,6 +451,7 @@ function applyReplyFences(session, parsed, opts = {}) {
112
451
 
113
452
  /* ── memoryEvents → curate 게이트 (제안 ≠ 승인) ─────────────────────── */
114
453
  if (Array.isArray(parsed.memoryEvents) && parsed.memoryEvents.length) {
454
+ const memoryEvents = boundMemoryEvents(parsed.memoryEvents);
115
455
  // 프로젝트 경계: 명시 초기화된(.agentlas 존재) 폴더만 project 스코프 대상 —
116
456
  // 임의 cwd 에 .agentlas 스캐폴딩을 만들지 않는다(session.cjs 프롬프트 증강과 동일 기준).
117
457
  let projectPath = null;
@@ -121,7 +461,7 @@ function applyReplyFences(session, parsed, opts = {}) {
121
461
  projectPath = fs.existsSync(path.join(session.cwd, ".agentlas")) ? session.cwd : null;
122
462
  } catch { projectPath = null; }
123
463
  const ctx = {
124
- permission: session.permission,
464
+ permission,
125
465
  projectPath,
126
466
  agentId: session.agent.id,
127
467
  curatedMemories: [],
@@ -129,19 +469,19 @@ function applyReplyFences(session, parsed, opts = {}) {
129
469
  // 게이트 로직을 복제하지 않기 위해 이벤트를 정확한 wire 블록으로 재구성해
130
470
  // curateCliReply 에 그대로 통과시킨다(시크릿/스코프/중복/권한 게이트 전부 재사용).
131
471
  const heading = memoryCurate.loadArch().eventsHeading;
132
- const block = `${heading}\n\`\`\`json\n${JSON.stringify(parsed.memoryEvents)}\n\`\`\``;
472
+ const block = `${heading}\n\`\`\`json\n${JSON.stringify(memoryEvents)}\n\`\`\``;
133
473
  try {
134
474
  memoryCurate.curateCliReply(session.db, block, ctx);
135
475
  } catch { /* 게이트 실패가 턴 자체를 죽이면 안 된다 */ }
136
476
  receipts.memory = {
137
477
  candidates: parsed.memoryEvents.length,
138
478
  written: ctx.curatedMemories.length,
139
- permission: session.permission,
479
+ permission,
140
480
  };
141
481
  // Agentlas One 이 켜져 있으면 에이전트 스코프 후보를 One 서랍에도 티켓으로 넘긴다.
142
482
  // 프로젝트 스코프는 여기 남기고 옮기지 않는다 — One 은 프로젝트를 넘나드는 정체성이라
143
483
  // agent_repo/user_identity 만 One 의 것이다(기획 2.2 스코프 경계).
144
- receipts.memory.one = forwardToOne(parsed.memoryEvents);
484
+ receipts.memory.one = forwardToOne(ctx.curatedMemories, permission);
145
485
  // read 권한 턴 = durable 쓰기 0 — 영수증 이벤트만 남는다.
146
486
  record({ type: "memory-curated", ...receipts.memory });
147
487
  }
@@ -233,7 +573,7 @@ function applyReplyFences(session, parsed, opts = {}) {
233
573
  const child = orch.spawn({
234
574
  agent: session.agent,
235
575
  runtime: session.runtime,
236
- permission: session.permission,
576
+ permission,
237
577
  cwd: session.cwd,
238
578
  parentKey: session.key,
239
579
  activate: false,