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.
- package/CHANGELOG.md +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- 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 +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- 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 +96 -30
- 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 +609 -116
- 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 +72 -14
- 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 +150 -37
- 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 +35 -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 +484 -63
- 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 +46 -10
- package/engine/mcp/plan.cjs +10 -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 +382 -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
|
@@ -20,11 +20,98 @@ const path = require("node:path");
|
|
|
20
20
|
const { spawnSync } = require("node:child_process");
|
|
21
21
|
const { userDataDir } = require("./paths.cjs");
|
|
22
22
|
|
|
23
|
+
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
24
|
+
const MAX_MARKER_BYTES = 64 * 1024;
|
|
25
|
+
const MAX_RUN_GRAPH_BYTES = 16 * 1024 * 1024;
|
|
26
|
+
const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024;
|
|
27
|
+
const MAX_ARCHIVE_LIST_BYTES = 8 * 1024 * 1024;
|
|
28
|
+
const MAX_ARCHIVE_ENTRIES = 100_000;
|
|
29
|
+
const MAX_ARCHIVE_ENTRY_BYTES = 4 * 1024;
|
|
30
|
+
const MAX_LOCK_RECORD_BYTES = 16 * 1024;
|
|
31
|
+
const LOCK_WAIT_MS = 120_000;
|
|
32
|
+
const LOCK_RETRY_MS = 50;
|
|
33
|
+
const UNKNOWN_LOCK_STALE_MS = 5 * 60_000;
|
|
34
|
+
const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
35
|
+
const versionQueues = new Map();
|
|
36
|
+
|
|
37
|
+
function sameFileIdentity(left, right) {
|
|
38
|
+
return !!left && !!right
|
|
39
|
+
&& left.dev === right.dev
|
|
40
|
+
&& left.ino === right.ino
|
|
41
|
+
&& left.nlink === right.nlink;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readFdBounded(fd, maxBytes) {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let total = 0;
|
|
47
|
+
while (total <= maxBytes) {
|
|
48
|
+
const want = Math.min(8192, maxBytes - total + 1);
|
|
49
|
+
const buffer = Buffer.allocUnsafe(want);
|
|
50
|
+
const count = fs.readSync(fd, buffer, 0, want, null);
|
|
51
|
+
if (!count) break;
|
|
52
|
+
total += count;
|
|
53
|
+
if (total > maxBytes) return null;
|
|
54
|
+
chunks.push(buffer.subarray(0, count));
|
|
55
|
+
}
|
|
56
|
+
return Buffer.concat(chunks, total);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readBoundedRegularFile(filePath, maxBytes) {
|
|
60
|
+
let fd = null;
|
|
61
|
+
try {
|
|
62
|
+
const before = fs.lstatSync(filePath);
|
|
63
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size > maxBytes) return null;
|
|
64
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
65
|
+
const opened = fs.fstatSync(fd);
|
|
66
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.size > maxBytes || !sameFileIdentity(before, opened)) return null;
|
|
67
|
+
const contents = readFdBounded(fd, maxBytes);
|
|
68
|
+
const after = fs.fstatSync(fd);
|
|
69
|
+
if (
|
|
70
|
+
contents === null || !after.isFile() || after.nlink !== 1 || !sameFileIdentity(opened, after) ||
|
|
71
|
+
after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
|
|
72
|
+
after.size !== contents.length
|
|
73
|
+
) return null;
|
|
74
|
+
return contents;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
} finally {
|
|
78
|
+
if (fd !== null) {
|
|
79
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ensureRealDirectory(directory, { create = false } = {}) {
|
|
85
|
+
const resolved = path.resolve(String(directory || ""));
|
|
86
|
+
if (create) fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
87
|
+
const stat = fs.lstatSync(resolved);
|
|
88
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe desktop-core directory: ${resolved}`);
|
|
89
|
+
return { path: resolved, stat, realpath: fs.realpathSync.native(resolved) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function safeRemoveVersionDir(directory) {
|
|
93
|
+
let stat;
|
|
94
|
+
try { stat = fs.lstatSync(directory); } catch (error) {
|
|
95
|
+
if (error?.code === "ENOENT") return;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) fs.unlinkSync(directory);
|
|
99
|
+
else fs.rmSync(directory, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeAll(fd, buffer) {
|
|
103
|
+
let offset = 0;
|
|
104
|
+
while (offset < buffer.length) offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
|
|
105
|
+
}
|
|
106
|
+
|
|
23
107
|
// env 오버라이드는 테스트 전용(게이트가 로컬 서버를 가리키는 가짜 매니페스트로 전 경로를 잠근다).
|
|
24
108
|
function manifestPath() { return process.env.AGENTLAS_DESKTOP_CORE_MANIFEST || path.join(__dirname, "..", "vendor", "desktop-core.manifest.json"); }
|
|
25
109
|
|
|
26
110
|
function readManifest() {
|
|
27
|
-
try {
|
|
111
|
+
try {
|
|
112
|
+
const raw = readBoundedRegularFile(manifestPath(), MAX_MANIFEST_BYTES);
|
|
113
|
+
return raw === null ? null : JSON.parse(raw.toString("utf8"));
|
|
114
|
+
} catch { return null; }
|
|
28
115
|
}
|
|
29
116
|
|
|
30
117
|
function cacheRoot() { return path.join(userDataDir(), "desktop-core-cache"); }
|
|
@@ -40,25 +127,189 @@ function cacheDir(version) {
|
|
|
40
127
|
}
|
|
41
128
|
function cacheDistDir(version) { return path.join(cacheDir(version), "dist"); }
|
|
42
129
|
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
130
|
+
function normalizedCacheManifest(manifest) {
|
|
131
|
+
const version = normalizedCacheVersion(manifest?.version);
|
|
132
|
+
const sha256 = typeof manifest?.sha256 === "string" ? manifest.sha256.toLowerCase() : "";
|
|
133
|
+
if (!version || !/^[0-9a-f]{64}$/.test(sha256)) return null;
|
|
134
|
+
return { version, sha256 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizedManifest(manifest) {
|
|
138
|
+
const cacheManifest = normalizedCacheManifest(manifest);
|
|
139
|
+
if (!cacheManifest) return null;
|
|
140
|
+
if (typeof manifest.url !== "string" || Buffer.byteLength(manifest.url, "utf8") > 2048) return null;
|
|
141
|
+
let url;
|
|
142
|
+
try { url = new URL(manifest.url); } catch { return null; }
|
|
143
|
+
if (!/^https?:$/.test(url.protocol)) return null;
|
|
144
|
+
let sizeBytes = null;
|
|
145
|
+
if (manifest.sizeBytes !== undefined && manifest.sizeBytes !== null) {
|
|
146
|
+
const candidate = Number(manifest.sizeBytes);
|
|
147
|
+
if (!Number.isSafeInteger(candidate) || candidate <= 0 || candidate > MAX_ARCHIVE_BYTES) return null;
|
|
148
|
+
sizeBytes = candidate;
|
|
149
|
+
}
|
|
150
|
+
return { ...cacheManifest, url: url.toString(), sizeBytes };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cacheRootInfo({ create = false } = {}) {
|
|
154
|
+
const dataRoot = ensureRealDirectory(userDataDir(), { create });
|
|
155
|
+
const root = ensureRealDirectory(cacheRoot(), { create });
|
|
156
|
+
if (path.dirname(root.realpath) !== dataRoot.realpath) throw new Error("Refusing to use a redirected desktop-core cache directory.");
|
|
157
|
+
return root;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLivePid(pid) {
|
|
161
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 0);
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return error?.code === "EPERM";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readLockRecord(lockPath) {
|
|
171
|
+
const raw = readBoundedRegularFile(lockPath, MAX_LOCK_RECORD_BYTES);
|
|
172
|
+
if (raw === null) return null;
|
|
173
|
+
try {
|
|
174
|
+
const record = JSON.parse(raw.toString("utf8"));
|
|
175
|
+
if (!record || typeof record !== "object" || typeof record.token !== "string" || !record.token
|
|
176
|
+
|| !Number.isSafeInteger(record.pid) || record.pid <= 0) return null;
|
|
177
|
+
return record;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function removeLockIfOwned(lockPath, token) {
|
|
184
|
+
try {
|
|
185
|
+
const before = fs.lstatSync(lockPath);
|
|
186
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1) return false;
|
|
187
|
+
const record = readLockRecord(lockPath);
|
|
188
|
+
if (!record || record.token !== token) return false;
|
|
189
|
+
const current = fs.lstatSync(lockPath);
|
|
190
|
+
if (!sameFileIdentity(before, current)) return false;
|
|
191
|
+
fs.unlinkSync(lockPath);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function removeUnknownLockIfUnchanged(lockPath, expected) {
|
|
199
|
+
try {
|
|
200
|
+
const current = fs.lstatSync(lockPath);
|
|
201
|
+
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1 || !sameFileIdentity(expected, current)) return false;
|
|
202
|
+
fs.unlinkSync(lockPath);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function waitForLock() {
|
|
210
|
+
return new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function acquireFileVersionLock(version) {
|
|
214
|
+
const rootInfo = cacheRootInfo({ create: true });
|
|
215
|
+
const lockPath = path.join(rootInfo.path, `${version}.lock`);
|
|
216
|
+
const token = crypto.randomUUID();
|
|
217
|
+
const payload = Buffer.from(JSON.stringify({ token, pid: process.pid, claimedAt: new Date().toISOString() }) + "\n", "utf8");
|
|
218
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
219
|
+
while (true) {
|
|
220
|
+
let fd = null;
|
|
221
|
+
let created = false;
|
|
222
|
+
try {
|
|
223
|
+
const currentRoot = fs.lstatSync(rootInfo.path);
|
|
224
|
+
if (!currentRoot.isDirectory() || currentRoot.isSymbolicLink() || currentRoot.dev !== rootInfo.stat.dev || currentRoot.ino !== rootInfo.stat.ino) {
|
|
225
|
+
throw new Error("Desktop-core cache directory changed while locking.");
|
|
226
|
+
}
|
|
227
|
+
fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
228
|
+
created = true;
|
|
229
|
+
const createdStat = fs.fstatSync(fd);
|
|
230
|
+
if (!createdStat.isFile() || createdStat.nlink !== 1) throw new Error("Unsafe desktop-core cache lock.");
|
|
231
|
+
writeAll(fd, payload);
|
|
232
|
+
fs.fsyncSync(fd);
|
|
233
|
+
const writtenStat = fs.fstatSync(fd);
|
|
234
|
+
if (!sameFileIdentity(createdStat, writtenStat) || writtenStat.size !== payload.length) throw new Error("Desktop-core cache lock changed while claiming.");
|
|
235
|
+
fs.closeSync(fd);
|
|
236
|
+
fd = null;
|
|
237
|
+
return { path: lockPath, token };
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (fd !== null) {
|
|
240
|
+
try { fs.closeSync(fd); } catch { /* preserve original */ }
|
|
241
|
+
}
|
|
242
|
+
if (created) removeLockIfOwned(lockPath, token);
|
|
243
|
+
if (error?.code !== "EEXIST") throw error;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let stat;
|
|
247
|
+
try { stat = fs.lstatSync(lockPath); } catch (error) {
|
|
248
|
+
if (error?.code === "ENOENT") continue;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) throw new Error("Unsafe desktop-core cache lock.");
|
|
252
|
+
const record = readLockRecord(lockPath);
|
|
253
|
+
if (record && isLivePid(record.pid)) {
|
|
254
|
+
if (Date.now() >= deadline) throw Object.assign(new Error("Another process is downloading this desktop core."), { code: "desktop_core_lock_busy" });
|
|
255
|
+
await waitForLock();
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (!record && Date.now() - stat.mtimeMs < UNKNOWN_LOCK_STALE_MS) {
|
|
259
|
+
if (Date.now() >= deadline) throw Object.assign(new Error("The desktop-core cache lock is unreadable."), { code: "desktop_core_lock_busy" });
|
|
260
|
+
await waitForLock();
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (record) removeLockIfOwned(lockPath, record.token);
|
|
264
|
+
else removeUnknownLockIfUnchanged(lockPath, stat);
|
|
265
|
+
if (Date.now() >= deadline) throw Object.assign(new Error("Could not claim the desktop-core cache lock."), { code: "desktop_core_lock_busy" });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function releaseFileVersionLock(lock) {
|
|
270
|
+
if (lock) removeLockIfOwned(lock.path, lock.token);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function withVersionLock(version, work) {
|
|
274
|
+
const previous = versionQueues.get(version) || Promise.resolve();
|
|
275
|
+
let finishTurn;
|
|
276
|
+
const turn = new Promise((resolve) => { finishTurn = resolve; });
|
|
277
|
+
versionQueues.set(version, turn);
|
|
278
|
+
let lock = null;
|
|
279
|
+
try {
|
|
280
|
+
await previous;
|
|
281
|
+
lock = await acquireFileVersionLock(version);
|
|
282
|
+
return await work();
|
|
283
|
+
} finally {
|
|
284
|
+
releaseFileVersionLock(lock);
|
|
285
|
+
finishTurn();
|
|
286
|
+
if (versionQueues.get(version) === turn) versionQueues.delete(version);
|
|
287
|
+
}
|
|
47
288
|
}
|
|
48
289
|
|
|
49
290
|
/** 이미 캐시에 온전히 풀려 있으면 그 dist 경로, 아니면 null. */
|
|
50
291
|
function cachedCoreRoot(manifest = readManifest()) {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
292
|
+
const normalized = normalizedCacheManifest(manifest);
|
|
293
|
+
if (!normalized) return null;
|
|
294
|
+
const { version, sha256 } = normalized;
|
|
53
295
|
const dist = cacheDistDir(version);
|
|
54
296
|
const marker = path.join(cacheDir(version), ".complete");
|
|
55
|
-
if (!fs.existsSync(path.join(dist, "electron", "workflow", "run-graph.js"))) return null;
|
|
56
297
|
try {
|
|
57
|
-
|
|
298
|
+
cacheRootInfo();
|
|
299
|
+
ensureRealDirectory(cacheDir(version));
|
|
300
|
+
ensureRealDirectory(dist);
|
|
301
|
+
ensureRealDirectory(path.join(dist, "electron"));
|
|
302
|
+
ensureRealDirectory(path.join(dist, "electron", "workflow"));
|
|
303
|
+
const runGraph = path.join(dist, "electron", "workflow", "run-graph.js");
|
|
304
|
+
if (readBoundedRegularFile(runGraph, MAX_RUN_GRAPH_BYTES) === null) return null;
|
|
305
|
+
const markerRaw = readBoundedRegularFile(marker, MAX_MARKER_BYTES);
|
|
306
|
+
if (markerRaw === null) return null;
|
|
307
|
+
const completed = JSON.parse(markerRaw.toString("utf8"));
|
|
58
308
|
if (
|
|
59
309
|
completed?.schemaVersion === 2
|
|
60
310
|
&& String(completed.version) === version
|
|
61
|
-
&& completed.sha256 ===
|
|
311
|
+
&& typeof completed.sha256 === "string"
|
|
312
|
+
&& completed.sha256.toLowerCase() === sha256
|
|
62
313
|
) return dist;
|
|
63
314
|
} catch {
|
|
64
315
|
// Timestamp-only and malformed markers predate the content-bound cache
|
|
@@ -73,23 +324,150 @@ function cachedCoreRoot(manifest = readManifest()) {
|
|
|
73
324
|
* widen from a versioned engine cache into arbitrary user data.
|
|
74
325
|
*/
|
|
75
326
|
function pruneStaleCaches(keepVersion) {
|
|
76
|
-
|
|
327
|
+
let root;
|
|
328
|
+
try { root = cacheRootInfo().path; } catch { return 0; }
|
|
77
329
|
let removed = 0;
|
|
78
330
|
let entries = [];
|
|
79
331
|
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return removed; }
|
|
80
332
|
for (const entry of entries) {
|
|
81
333
|
if (!entry.isDirectory() || entry.name === String(keepVersion) || entry.name.includes(".partial-")) continue;
|
|
82
334
|
const dir = path.join(root, entry.name);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
335
|
+
const activeLock = readLockRecord(path.join(root, `${entry.name}.lock`));
|
|
336
|
+
if (activeLock && isLivePid(activeLock.pid)) continue;
|
|
337
|
+
const recognizable = readBoundedRegularFile(path.join(dir, ".complete"), MAX_MARKER_BYTES) !== null
|
|
338
|
+
|| readBoundedRegularFile(path.join(dir, "desktop-core.tar.gz"), MAX_ARCHIVE_BYTES) !== null
|
|
339
|
+
|| readBoundedRegularFile(path.join(dir, "dist", "electron", "workflow", "run-graph.js"), MAX_RUN_GRAPH_BYTES) !== null;
|
|
86
340
|
if (!recognizable) continue;
|
|
87
|
-
|
|
341
|
+
safeRemoveVersionDir(dir);
|
|
88
342
|
removed += 1;
|
|
89
343
|
}
|
|
90
344
|
return removed;
|
|
91
345
|
}
|
|
92
346
|
|
|
347
|
+
async function downloadArchive(response, tarPath, expectedSize) {
|
|
348
|
+
const maxBytes = MAX_ARCHIVE_BYTES;
|
|
349
|
+
const maxStoredBytes = expectedSize === null ? maxBytes : Math.min(maxBytes, expectedSize + 1);
|
|
350
|
+
const hash = crypto.createHash("sha256");
|
|
351
|
+
let fd = null;
|
|
352
|
+
let readBytes = 0;
|
|
353
|
+
let storedBytes = 0;
|
|
354
|
+
let hardOverflow = false;
|
|
355
|
+
let sizeOverflow = false;
|
|
356
|
+
const writeChunk = (chunk) => {
|
|
357
|
+
if (!chunk || !chunk.length) return true;
|
|
358
|
+
if (readBytes + chunk.length > maxBytes) {
|
|
359
|
+
const keep = Math.max(0, maxBytes - readBytes);
|
|
360
|
+
if (keep) hash.update(chunk.subarray(0, keep));
|
|
361
|
+
readBytes = maxBytes;
|
|
362
|
+
hardOverflow = true;
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
hash.update(chunk);
|
|
366
|
+
readBytes += chunk.length;
|
|
367
|
+
if (expectedSize !== null && readBytes > expectedSize) sizeOverflow = true;
|
|
368
|
+
if (storedBytes < maxStoredBytes) {
|
|
369
|
+
const keep = chunk.subarray(0, Math.min(chunk.length, maxStoredBytes - storedBytes));
|
|
370
|
+
if (keep.length) {
|
|
371
|
+
writeAll(fd, keep);
|
|
372
|
+
storedBytes += keep.length;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const contentLength = Number(response.headers?.get?.("content-length"));
|
|
380
|
+
if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) {
|
|
381
|
+
hardOverflow = true;
|
|
382
|
+
try { await response.body?.cancel?.(); } catch { /* best effort */ }
|
|
383
|
+
} else {
|
|
384
|
+
fd = fs.openSync(tarPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
385
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
386
|
+
throw new Error("Download response is not streamable.");
|
|
387
|
+
}
|
|
388
|
+
const reader = response.body.getReader();
|
|
389
|
+
while (true) {
|
|
390
|
+
const next = await reader.read();
|
|
391
|
+
if (next.done) break;
|
|
392
|
+
if (!writeChunk(Buffer.from(next.value))) {
|
|
393
|
+
try { await reader.cancel(); } catch { /* best effort */ }
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (fd === null) {
|
|
399
|
+
fd = fs.openSync(tarPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
400
|
+
}
|
|
401
|
+
fs.fsyncSync(fd);
|
|
402
|
+
fs.closeSync(fd);
|
|
403
|
+
fd = null;
|
|
404
|
+
if (expectedSize !== null && readBytes !== expectedSize) sizeOverflow = true;
|
|
405
|
+
return { size: readBytes, storedSize: storedBytes, digest: hash.digest("hex"), hardOverflow, sizeOverflow };
|
|
406
|
+
} finally {
|
|
407
|
+
if (fd !== null) {
|
|
408
|
+
try { fs.closeSync(fd); } catch { /* preserve original */ }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function writeCompleteMarker(directory, marker) {
|
|
414
|
+
const markerPath = path.join(directory, ".complete");
|
|
415
|
+
try {
|
|
416
|
+
fs.lstatSync(markerPath);
|
|
417
|
+
throw new Error("Unsafe desktop-core completion marker already exists.");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error?.code !== "ENOENT") throw error;
|
|
420
|
+
}
|
|
421
|
+
const payload = Buffer.from(JSON.stringify(marker) + "\n", "utf8");
|
|
422
|
+
if (payload.length > MAX_MARKER_BYTES) throw new Error("Desktop-core completion marker is too large.");
|
|
423
|
+
let fd = null;
|
|
424
|
+
try {
|
|
425
|
+
fd = fs.openSync(markerPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
426
|
+
const created = fs.fstatSync(fd);
|
|
427
|
+
if (!created.isFile() || created.nlink !== 1) throw new Error("Unsafe desktop-core completion marker.");
|
|
428
|
+
writeAll(fd, payload);
|
|
429
|
+
fs.fsyncSync(fd);
|
|
430
|
+
const written = fs.fstatSync(fd);
|
|
431
|
+
if (!sameFileIdentity(created, written) || written.size !== payload.length) throw new Error("Desktop-core completion marker changed while writing.");
|
|
432
|
+
} finally {
|
|
433
|
+
if (fd !== null) {
|
|
434
|
+
try { fs.closeSync(fd); } catch { /* preserve original */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function archiveList(tarPath, verbose = false) {
|
|
440
|
+
const listed = spawnSync("tar", [verbose ? "-tvzf" : "-tzf", tarPath], {
|
|
441
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
442
|
+
timeout: 30_000,
|
|
443
|
+
});
|
|
444
|
+
if (listed.status !== 0 || listed.error || !Buffer.isBuffer(listed.stdout)) return null;
|
|
445
|
+
let text;
|
|
446
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(listed.stdout); } catch { return null; }
|
|
447
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
448
|
+
return lines.length > 0 && lines.length <= MAX_ARCHIVE_ENTRIES ? lines : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validateArchiveEntries(tarPath) {
|
|
452
|
+
const names = archiveList(tarPath, false);
|
|
453
|
+
const verbose = archiveList(tarPath, true);
|
|
454
|
+
if (!names || !verbose || names.length !== verbose.length) return false;
|
|
455
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
456
|
+
const raw = names[index];
|
|
457
|
+
if (
|
|
458
|
+
Buffer.byteLength(raw, "utf8") > MAX_ARCHIVE_ENTRY_BYTES ||
|
|
459
|
+
/[\\\u0000-\u001f\u007f]/.test(raw) || path.isAbsolute(raw) ||
|
|
460
|
+
!["-", "d"].includes(verbose[index][0])
|
|
461
|
+
) return false;
|
|
462
|
+
const name = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
463
|
+
const segments = name.split("/");
|
|
464
|
+
if (!name || segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
|
|
465
|
+
if (!["dist", "node_modules", "VENDORED.json"].includes(segments[0])) return false;
|
|
466
|
+
if (segments[0] === "VENDORED.json" && segments.length !== 1) return false;
|
|
467
|
+
}
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
93
471
|
/**
|
|
94
472
|
* 매니페스트가 가리키는 데스크탑 코어를 내려받아 캐시에 푼다.
|
|
95
473
|
* onNotice(text): 사용자에게 보여줄 안내(무엇을·왜 받는지) — 조용히 받지 않는다.
|
|
@@ -97,75 +475,97 @@ function pruneStaleCaches(keepVersion) {
|
|
|
97
475
|
*/
|
|
98
476
|
async function fetchDesktopCore({ onNotice } = {}) {
|
|
99
477
|
const manifest = readManifest();
|
|
100
|
-
const version = normalizedCacheVersion(manifest?.version);
|
|
101
|
-
if (!manifest || !version || !manifest.url || !manifest.sha256) return null;
|
|
102
|
-
|
|
103
|
-
const existing = cachedCoreRoot({ ...manifest, version });
|
|
104
|
-
if (existing) return existing;
|
|
105
|
-
|
|
106
478
|
const say = (t) => { if (typeof onNotice === "function") onNotice(t); };
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const partialDir = path.join(
|
|
111
|
-
cacheRoot(),
|
|
112
|
-
`${version}.partial-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
|
|
113
|
-
);
|
|
114
|
-
fs.mkdirSync(partialDir, { recursive: true });
|
|
115
|
-
const tarPath = path.join(partialDir, "desktop-core.tar.gz");
|
|
116
|
-
|
|
117
|
-
let res;
|
|
479
|
+
const normalized = normalizedManifest(manifest);
|
|
480
|
+
if (!normalized) return null;
|
|
481
|
+
const { version, sha256, url, sizeBytes } = normalized;
|
|
118
482
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if (!res.ok) {
|
|
126
|
-
say(`Download failed: HTTP ${res.status}`);
|
|
127
|
-
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
131
|
-
fs.writeFileSync(tarPath, buf);
|
|
483
|
+
return await withVersionLock(version, async () => {
|
|
484
|
+
const existing = cachedCoreRoot({ ...manifest, ...normalized });
|
|
485
|
+
if (existing) return existing;
|
|
132
486
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
487
|
+
say(`Downloading the graph-execution engine (${sizeBytes ? Math.round(sizeBytes / 1024 / 1024) + " MB" : "one-time"}) from ${url} …`);
|
|
488
|
+
const rootInfo = cacheRootInfo({ create: true });
|
|
489
|
+
const dir = cacheDir(version);
|
|
490
|
+
const partialDir = path.join(
|
|
491
|
+
rootInfo.path,
|
|
492
|
+
`${version}.partial-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
|
|
493
|
+
);
|
|
494
|
+
fs.mkdirSync(partialDir, { recursive: false, mode: 0o700 });
|
|
495
|
+
let keepPartial = true;
|
|
496
|
+
const tarPath = path.join(partialDir, "desktop-core.tar.gz");
|
|
497
|
+
try {
|
|
498
|
+
let res;
|
|
499
|
+
try {
|
|
500
|
+
res = await fetch(url);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
say(`Download failed: ${error?.message || error}`);
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
if (!res.ok) {
|
|
506
|
+
say(`Download failed: HTTP ${res.status}`);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const downloaded = await downloadArchive(res, tarPath, sizeBytes);
|
|
510
|
+
if (downloaded.digest !== sha256) {
|
|
511
|
+
say(`Checksum mismatch (expected ${sha256.slice(0, 12)}…, got ${downloaded.digest.slice(0, 12)}…) — refusing to use it.`);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
if (downloaded.hardOverflow || downloaded.sizeOverflow || (sizeBytes !== null && downloaded.size !== sizeBytes)) {
|
|
515
|
+
say(`Size mismatch (expected ${sizeBytes ?? MAX_ARCHIVE_BYTES} bytes, got ${downloaded.size}${downloaded.hardOverflow ? "+" : ""}) — refusing to use it.`);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
if (!validateArchiveEntries(tarPath)) {
|
|
519
|
+
say("Downloaded archive contains unsafe or unexpected files.");
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
144
522
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fs.rmSync(tarPath);
|
|
523
|
+
const extract = spawnSync("tar", ["-xzf", tarPath, "-C", partialDir, "--no-same-owner", "--no-same-permissions"], { encoding: "utf8" });
|
|
524
|
+
if (extract.status !== 0) {
|
|
525
|
+
say(`Extraction failed: ${extract.stderr || extract.error || "unknown error"}`);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
fs.unlinkSync(tarPath);
|
|
152
529
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
530
|
+
const extractedDist = path.join(partialDir, "dist");
|
|
531
|
+
const workflowDir = path.join(extractedDist, "electron", "workflow");
|
|
532
|
+
ensureRealDirectory(partialDir);
|
|
533
|
+
ensureRealDirectory(extractedDist);
|
|
534
|
+
ensureRealDirectory(path.join(extractedDist, "electron"));
|
|
535
|
+
ensureRealDirectory(workflowDir);
|
|
536
|
+
if (readBoundedRegularFile(path.join(workflowDir, "run-graph.js"), MAX_RUN_GRAPH_BYTES) === null) {
|
|
537
|
+
say("Downloaded archive did not contain safe expected engine files.");
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
writeCompleteMarker(partialDir, {
|
|
541
|
+
schemaVersion: 2,
|
|
542
|
+
version,
|
|
543
|
+
sha256,
|
|
544
|
+
completedAt: new Date().toISOString(),
|
|
545
|
+
});
|
|
546
|
+
cacheRootInfo();
|
|
547
|
+
safeRemoveVersionDir(dir);
|
|
548
|
+
fs.renameSync(partialDir, dir);
|
|
549
|
+
keepPartial = false;
|
|
550
|
+
const ready = cachedCoreRoot({ ...manifest, ...normalized });
|
|
551
|
+
if (!ready) {
|
|
552
|
+
safeRemoveVersionDir(dir);
|
|
553
|
+
say("Downloaded archive failed cache safety validation.");
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
pruneStaleCaches(version);
|
|
557
|
+
say("Engine ready.");
|
|
558
|
+
return ready;
|
|
559
|
+
} finally {
|
|
560
|
+
if (keepPartial) {
|
|
561
|
+
try { safeRemoveVersionDir(partialDir); } catch { /* preserve the original result */ }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
} catch (error) {
|
|
566
|
+
say(`Desktop-core cache unavailable: ${error?.message || error}`);
|
|
156
567
|
return null;
|
|
157
568
|
}
|
|
158
|
-
fs.writeFileSync(path.join(partialDir, ".complete"), JSON.stringify({
|
|
159
|
-
schemaVersion: 2,
|
|
160
|
-
version,
|
|
161
|
-
sha256: manifest.sha256,
|
|
162
|
-
completedAt: new Date().toISOString(),
|
|
163
|
-
}) + "\n");
|
|
164
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
165
|
-
fs.renameSync(partialDir, dir);
|
|
166
|
-
pruneStaleCaches(version);
|
|
167
|
-
say("Engine ready.");
|
|
168
|
-
return cacheDistDir(version);
|
|
169
569
|
}
|
|
170
570
|
|
|
171
571
|
module.exports = { readManifest, cachedCoreRoot, fetchDesktopCore, cacheDistDir, pruneStaleCaches };
|
|
@@ -23,23 +23,33 @@ const applied = new WeakMap();
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* `key` 작업을 이 커넥션에서 아직 안 했으면 실행한다.
|
|
26
|
-
*
|
|
27
|
-
* (일시적 락
|
|
26
|
+
* 실패하거나 콜백이 `false`를 반환하면 **기억하지 않는다** — 다음 호출이 다시 시도할
|
|
27
|
+
* 수 있어야 한다. (일시적 락 경합이나 아직 없는 테이블을 "완료"로 기억하면 스키마가
|
|
28
|
+
* 영구히 안 맞는다.)
|
|
29
|
+
*
|
|
30
|
+
* `inFlight`는 동기 콜백이 같은 커넥션/키를 재진입 호출하는 경우를 막는다. 재진입
|
|
31
|
+
* 호출은 아직 완료를 증명할 수 없으므로 `false`를 돌려주고, 바깥 호출만 완료를
|
|
32
|
+
* 기록한다. 기존 호출부는 예외가 없으면 `undefined`를 반환하므로, 호환성을 위해
|
|
33
|
+
* `false`만 명시적인 미완료 신호로 취급한다.
|
|
28
34
|
*/
|
|
29
35
|
function ensureOnce(db, key, fn) {
|
|
30
|
-
if (!db) return false;
|
|
31
|
-
let
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
applied.set(db,
|
|
36
|
+
if (!db || (typeof db !== "object" && typeof db !== "function") || typeof fn !== "function") return false;
|
|
37
|
+
let state = applied.get(db);
|
|
38
|
+
if (!state) {
|
|
39
|
+
state = { done: new Set(), inFlight: new Set() };
|
|
40
|
+
applied.set(db, state);
|
|
35
41
|
}
|
|
36
|
-
if (done.has(key)) return true;
|
|
42
|
+
if (state.done.has(key)) return true;
|
|
43
|
+
if (state.inFlight.has(key)) return false;
|
|
44
|
+
state.inFlight.add(key);
|
|
37
45
|
try {
|
|
38
|
-
fn(db);
|
|
39
|
-
done.add(key);
|
|
46
|
+
if (fn(db) === false) return false;
|
|
47
|
+
state.done.add(key);
|
|
40
48
|
return true;
|
|
41
49
|
} catch {
|
|
42
50
|
return false;
|
|
51
|
+
} finally {
|
|
52
|
+
state.inFlight.delete(key);
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -66,9 +76,11 @@ function columnExists(db, table, column) {
|
|
|
66
76
|
*/
|
|
67
77
|
function ensureMemoryContextColumn(db) {
|
|
68
78
|
return ensureOnce(db, "memory_entries.context_json", (conn) => {
|
|
69
|
-
if (tableExists(conn, "memory_entries")
|
|
79
|
+
if (!tableExists(conn, "memory_entries")) return false;
|
|
80
|
+
if (!columnExists(conn, "memory_entries", "context_json")) {
|
|
70
81
|
conn.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
71
82
|
}
|
|
83
|
+
return columnExists(conn, "memory_entries", "context_json");
|
|
72
84
|
});
|
|
73
85
|
}
|
|
74
86
|
|
|
@@ -133,7 +133,11 @@ async function cmdBuild(options) {
|
|
|
133
133
|
if (!parsed.noMcp) {
|
|
134
134
|
if (parsed.approveAll) approvedIds = [...plan.availableCatalogIds];
|
|
135
135
|
else if (parsed.approvedIds.length) approvedIds = mcp.normalizeConsentAnswer(parsed.approvedIds.join(","), plan.availableCatalogIds);
|
|
136
|
-
else approvedIds = await mcp.askMcpConsentOnce(plan, {
|
|
136
|
+
else approvedIds = await mcp.askMcpConsentOnce(plan, {
|
|
137
|
+
db: options.db,
|
|
138
|
+
input: options.input,
|
|
139
|
+
output: options.promptOutput,
|
|
140
|
+
});
|
|
137
141
|
}
|
|
138
142
|
approvedIds = mcp.fitApprovedMcpIds(plan, approvedIds);
|
|
139
143
|
const runtimeAllowlist = await mcp.resolveApprovedMcpRuntimeAllowlist({
|