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.
- package/CHANGELOG.md +78 -0
- package/README.md +6 -4
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +308 -113
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +42 -12
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +8 -2
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +29 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +230 -61
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +15 -5
- package/engine/mcp/plan.cjs +5 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +212 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- 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
|
|
140
|
-
if (
|
|
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:
|
|
149
|
+
updatedAt: direct?.updatedAt || orchestrator.updatedAt,
|
|
148
150
|
sourceLayer:
|
|
149
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
576
|
+
permission,
|
|
237
577
|
cwd: session.cwd,
|
|
238
578
|
parentKey: session.key,
|
|
239
579
|
activate: false,
|