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
package/engine/oberon/render.cjs
CHANGED
|
@@ -18,10 +18,343 @@
|
|
|
18
18
|
const fs = require("node:fs");
|
|
19
19
|
const os = require("node:os");
|
|
20
20
|
const path = require("node:path");
|
|
21
|
+
const crypto = require("node:crypto");
|
|
22
|
+
const { TextDecoder } = require("node:util");
|
|
21
23
|
const { spawn } = require("node:child_process");
|
|
22
24
|
const { packageRoot } = require("../core/paths.cjs");
|
|
23
25
|
const { fail, parseFlags, oberonBar, oberonBytes, slugifyOberon } = require("./common.cjs");
|
|
24
26
|
|
|
27
|
+
const MAX_OBERON_MANIFEST_BYTES = 2 * 1024 * 1024;
|
|
28
|
+
const MAX_OBERON_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
const MAX_OBERON_REQUEST_BYTES = 2 * 1024 * 1024;
|
|
30
|
+
const MAX_OBERON_STDOUT_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
const MAX_OBERON_STDERR_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
const MAX_OBERON_LINE_BYTES = 256 * 1024;
|
|
33
|
+
const MAX_OBERON_FILES = 512;
|
|
34
|
+
const MAX_OBERON_FIELD_BYTES = 4 * 1024;
|
|
35
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
|
|
36
|
+
const DEFAULT_TOTAL_TIMEOUT_MS = 15 * 60_000;
|
|
37
|
+
const DEFAULT_KILL_GRACE_MS = 5_000;
|
|
38
|
+
const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
39
|
+
const BOOTSTRAP_SOURCE = `
|
|
40
|
+
"use strict";
|
|
41
|
+
const fs = require("node:fs");
|
|
42
|
+
const path = require("node:path");
|
|
43
|
+
const Module = require("node:module");
|
|
44
|
+
|
|
45
|
+
function readFdAt(fd, size) {
|
|
46
|
+
const out = Buffer.alloc(size);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
while (offset < size) {
|
|
49
|
+
const count = fs.readSync(fd, out, offset, size - offset, offset);
|
|
50
|
+
if (!count) throw new Error("bound Oberon source ended before its verified length");
|
|
51
|
+
offset += count;
|
|
52
|
+
}
|
|
53
|
+
return out.toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createModule(filename) {
|
|
57
|
+
const mod = new Module(filename, null);
|
|
58
|
+
mod.filename = filename;
|
|
59
|
+
mod.path = path.dirname(filename);
|
|
60
|
+
mod.paths = Module._nodeModulePaths(mod.path);
|
|
61
|
+
return mod;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cacheModule(filename, mod) {
|
|
65
|
+
Module._cache[filename] = mod;
|
|
66
|
+
try { Module._cache[fs.realpathSync(filename)] = mod; } catch { /* bound source may have no live pathname */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compile(filename, source) {
|
|
70
|
+
const mod = createModule(filename);
|
|
71
|
+
cacheModule(filename, mod);
|
|
72
|
+
mod._compile(source, filename);
|
|
73
|
+
mod.loaded = true;
|
|
74
|
+
return mod;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scriptPath = process.env.AGENTLAS_OBERON_SCRIPT_PATH;
|
|
78
|
+
const builtPath = process.env.AGENTLAS_OBERON_BUILT_PATH;
|
|
79
|
+
const scriptFd = Number(process.env.AGENTLAS_OBERON_SCRIPT_FD);
|
|
80
|
+
const builtFd = Number(process.env.AGENTLAS_OBERON_BUILT_FD);
|
|
81
|
+
const scriptBytes = Number(process.env.AGENTLAS_OBERON_SCRIPT_BYTES);
|
|
82
|
+
const builtBytes = Number(process.env.AGENTLAS_OBERON_BUILT_BYTES);
|
|
83
|
+
if (!scriptPath || !builtPath || !Number.isSafeInteger(scriptFd) || !Number.isSafeInteger(builtFd) ||
|
|
84
|
+
!Number.isSafeInteger(scriptBytes) || !Number.isSafeInteger(builtBytes) || scriptBytes < 1 || builtBytes < 1) {
|
|
85
|
+
throw new Error("bound Oberon execution inputs are incomplete");
|
|
86
|
+
}
|
|
87
|
+
const originalResolveFilename = Module._resolveFilename;
|
|
88
|
+
Module._resolveFilename = function(request, parent, isMain, options) {
|
|
89
|
+
if (request === scriptPath || request === builtPath) return request;
|
|
90
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
91
|
+
};
|
|
92
|
+
const main = createModule(scriptPath);
|
|
93
|
+
process.mainModule = main;
|
|
94
|
+
require.main = main;
|
|
95
|
+
process.argv[1] = scriptPath;
|
|
96
|
+
cacheModule(scriptPath, main);
|
|
97
|
+
compile(builtPath, readFdAt(builtFd, builtBytes));
|
|
98
|
+
main._compile(readFdAt(scriptFd, scriptBytes), scriptPath);
|
|
99
|
+
main.loaded = true;
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
function regularSnapshot(stat) {
|
|
103
|
+
return stat && stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1
|
|
104
|
+
? stat
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sameFileSnapshot(left, right) {
|
|
109
|
+
return Boolean(
|
|
110
|
+
regularSnapshot(left) && regularSnapshot(right) &&
|
|
111
|
+
left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink &&
|
|
112
|
+
left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sameFileIdentity(left, right) {
|
|
117
|
+
return Boolean(
|
|
118
|
+
regularSnapshot(left) && regularSnapshot(right) &&
|
|
119
|
+
left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink &&
|
|
120
|
+
left.size === right.size && left.mtimeMs === right.mtimeMs,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readFdBounded(fd, maxBytes) {
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
while (total <= maxBytes) {
|
|
128
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
129
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
130
|
+
if (!count) break;
|
|
131
|
+
chunks.push(buffer.subarray(0, count));
|
|
132
|
+
total += count;
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks, total);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function decodeUtf8(buffer, label) {
|
|
138
|
+
try { return new TextDecoder("utf-8", { fatal: true }).decode(buffer); }
|
|
139
|
+
catch { fail(`${label} must contain valid UTF-8`); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readVerifiedFile(file, label, maxBytes) {
|
|
143
|
+
let before;
|
|
144
|
+
try { before = fs.lstatSync(file); }
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error && error.code === "ENOENT") throw error;
|
|
147
|
+
throw new Error(`${label} could not be inspected: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
if (!regularSnapshot(before) || before.size <= 0 || before.size > maxBytes) {
|
|
150
|
+
throw new Error(`${label} must be a non-empty regular non-symbolic-link single-link file no larger than ${maxBytes} bytes`);
|
|
151
|
+
}
|
|
152
|
+
let fd;
|
|
153
|
+
try {
|
|
154
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
155
|
+
const opened = fs.fstatSync(fd);
|
|
156
|
+
if (!sameFileSnapshot(opened, before)) throw new Error(`${label} changed while opening`);
|
|
157
|
+
const bytes = readFdBounded(fd, maxBytes);
|
|
158
|
+
const after = fs.fstatSync(fd);
|
|
159
|
+
if (!sameFileSnapshot(after, opened) || bytes.length !== after.size || bytes.length > maxBytes) {
|
|
160
|
+
throw new Error(`${label} changed while reading`);
|
|
161
|
+
}
|
|
162
|
+
return { path: file, fd, bytes, text: decodeUtf8(bytes, label), snapshot: after };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (fd !== undefined) {
|
|
165
|
+
try { fs.closeSync(fd); } catch { /* preserve the original failure */ }
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readOberonManifest(manifestPath) {
|
|
172
|
+
let source;
|
|
173
|
+
let fd;
|
|
174
|
+
try {
|
|
175
|
+
source = readVerifiedFile(manifestPath, "Manifest", MAX_OBERON_MANIFEST_BYTES);
|
|
176
|
+
fd = source.fd;
|
|
177
|
+
return JSON.parse(source.text);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error && error.code === "ENOENT") fail(`Manifest not found: ${manifestPath}`);
|
|
180
|
+
if (error && error.oberonFail) throw error;
|
|
181
|
+
fail(`Failed to parse manifest JSON: ${error.message}`);
|
|
182
|
+
} finally {
|
|
183
|
+
if (fd !== undefined) {
|
|
184
|
+
try { fs.closeSync(fd); } catch { /* preserve the parse result */ }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function positiveIntegerFlag(flags, key, { min = 1, max }) {
|
|
190
|
+
if (flags[key] === undefined) return null;
|
|
191
|
+
const value = Number(flags[key]);
|
|
192
|
+
if (!Number.isInteger(value) || value < min || value > max) fail(`Oberon --${key} must be an integer from ${min} to ${max}`);
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function safeManifestId(value, label) {
|
|
197
|
+
if (
|
|
198
|
+
typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ||
|
|
199
|
+
value === "." || value === ".." || value.includes("..")
|
|
200
|
+
) fail(`Manifest ${label} is unsafe`);
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writeAll(fd, bytes) {
|
|
205
|
+
let offset = 0;
|
|
206
|
+
while (offset < bytes.length) {
|
|
207
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
|
|
208
|
+
if (!written) throw new Error("Oberon temporary file write made no progress");
|
|
209
|
+
offset += written;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeOwnedFile(file, bytes, label) {
|
|
214
|
+
let fd;
|
|
215
|
+
try {
|
|
216
|
+
fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
217
|
+
writeAll(fd, bytes);
|
|
218
|
+
fs.fsyncSync(fd);
|
|
219
|
+
const snapshot = fs.fstatSync(fd);
|
|
220
|
+
if (!regularSnapshot(snapshot) || snapshot.size !== bytes.length) {
|
|
221
|
+
throw new Error(`${label} did not remain a regular single-link file`);
|
|
222
|
+
}
|
|
223
|
+
return { path: file, fd, snapshot };
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (fd !== undefined) {
|
|
226
|
+
try { fs.closeSync(fd); } catch { /* preserve the original failure */ }
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reopenOwnedReadable(file, expected, label) {
|
|
233
|
+
let fd;
|
|
234
|
+
try {
|
|
235
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
236
|
+
const current = fs.fstatSync(fd);
|
|
237
|
+
if (!sameFileSnapshot(current, expected)) throw new Error(`${label} changed while reopening`);
|
|
238
|
+
return fd;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (fd !== undefined) {
|
|
241
|
+
try { fs.closeSync(fd); } catch { /* preserve the original failure */ }
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function privateRunDirectory() {
|
|
248
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-oberon-run-"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cleanupOwnedFile(file, expected, label) {
|
|
252
|
+
let current;
|
|
253
|
+
try { current = fs.lstatSync(file); }
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error && error.code === "ENOENT") return null;
|
|
256
|
+
return new Error(`${label} could not be inspected during cleanup: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
if (!sameFileIdentity(current, expected)) {
|
|
259
|
+
return new Error(`${label} changed before cleanup; refusing to unlink an unknown file`);
|
|
260
|
+
}
|
|
261
|
+
// Rename first. If the original pathname is replaced in the final window,
|
|
262
|
+
// only that successor is moved to a recoverable quarantine name; it is never
|
|
263
|
+
// unlinked as if it were our temporary file.
|
|
264
|
+
const quarantine = `${file}.quarantine-${process.pid}-${crypto.randomUUID()}`;
|
|
265
|
+
try {
|
|
266
|
+
fs.renameSync(file, quarantine);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return new Error(`${label} could not be quarantined during cleanup: ${error.message}`);
|
|
269
|
+
}
|
|
270
|
+
let moved;
|
|
271
|
+
try { moved = fs.lstatSync(quarantine); }
|
|
272
|
+
catch (error) {
|
|
273
|
+
return new Error(`${label} quarantine could not be verified: ${error.message}`);
|
|
274
|
+
}
|
|
275
|
+
if (!sameFileIdentity(moved, expected)) {
|
|
276
|
+
return new Error(`${label} cleanup found an unknown successor; preserved at ${quarantine}`);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
fs.unlinkSync(quarantine);
|
|
280
|
+
return null;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return new Error(`${label} quarantine could not be removed: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function cleanupPrivateRun(runDir, ownedFiles) {
|
|
287
|
+
const errors = [];
|
|
288
|
+
for (const item of ownedFiles || []) {
|
|
289
|
+
if (item.fd !== undefined) {
|
|
290
|
+
try { fs.closeSync(item.fd); } catch { /* cleanup below still verifies the path */ }
|
|
291
|
+
item.fd = undefined;
|
|
292
|
+
}
|
|
293
|
+
const error = cleanupOwnedFile(item.path, item.snapshot, item.label);
|
|
294
|
+
if (error) errors.push(error.message);
|
|
295
|
+
}
|
|
296
|
+
if (runDir) {
|
|
297
|
+
try { fs.rmdirSync(runDir); }
|
|
298
|
+
catch (error) { if (!error || error.code !== "ENOENT") errors.push(`private render directory cleanup failed: ${error.message}`); }
|
|
299
|
+
}
|
|
300
|
+
return errors.length ? new Error(errors.join("; ")) : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function prepareBoundExecution(runDir, scriptSource, builtSource, ownedFiles) {
|
|
304
|
+
const scriptCopy = writeOwnedFile(path.join(runDir, "script.cjs"), scriptSource.bytes, "bound Oberon render script");
|
|
305
|
+
const scriptOwned = { path: scriptCopy.path, snapshot: scriptCopy.snapshot, label: "bound Oberon render script" };
|
|
306
|
+
ownedFiles.push(scriptOwned);
|
|
307
|
+
fs.closeSync(scriptCopy.fd);
|
|
308
|
+
scriptCopy.fd = undefined;
|
|
309
|
+
scriptOwned.fd = reopenOwnedReadable(scriptCopy.path, scriptCopy.snapshot, scriptOwned.label);
|
|
310
|
+
const builtCopy = writeOwnedFile(path.join(runDir, "built-render.js"), builtSource.bytes, "bound Electron render build");
|
|
311
|
+
const builtOwned = { path: builtCopy.path, snapshot: builtCopy.snapshot, label: "bound Electron render build" };
|
|
312
|
+
ownedFiles.push(builtOwned);
|
|
313
|
+
fs.closeSync(builtCopy.fd);
|
|
314
|
+
builtCopy.fd = undefined;
|
|
315
|
+
builtOwned.fd = reopenOwnedReadable(builtCopy.path, builtCopy.snapshot, builtOwned.label);
|
|
316
|
+
const bootstrap = writeOwnedFile(path.join(runDir, "bootstrap.cjs"), Buffer.from(BOOTSTRAP_SOURCE, "utf8"), "bound Oberon bootstrap");
|
|
317
|
+
ownedFiles.push({ path: bootstrap.path, snapshot: bootstrap.snapshot, label: "bound Oberon bootstrap" });
|
|
318
|
+
fs.closeSync(bootstrap.fd);
|
|
319
|
+
return {
|
|
320
|
+
bootstrapPath: bootstrap.path,
|
|
321
|
+
scriptFd: scriptOwned.fd,
|
|
322
|
+
builtFd: builtOwned.fd,
|
|
323
|
+
scriptPath: scriptSource.path,
|
|
324
|
+
builtPath: builtSource.path,
|
|
325
|
+
scriptBytes: scriptSource.bytes.length,
|
|
326
|
+
builtBytes: builtSource.bytes.length,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function boundedProtocolNumber(value, label) {
|
|
331
|
+
const parsed = Number(value);
|
|
332
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) fail(`Oberon ${label} value is invalid`);
|
|
333
|
+
return parsed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function boundedProtocolField(value, label, maxBytes = MAX_OBERON_FIELD_BYTES) {
|
|
337
|
+
if (Buffer.byteLength(value, "utf8") > maxBytes) fail(`Oberon ${label} exceeds ${maxBytes} bytes`);
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function timeoutSetting(config, keys, fallback, max = 24 * 60 * 60_000) {
|
|
342
|
+
const source = config || {};
|
|
343
|
+
for (const key of keys) {
|
|
344
|
+
if (source[key] === undefined) continue;
|
|
345
|
+
const value = Number(source[key]);
|
|
346
|
+
if (Number.isFinite(value) && value > 0) return Math.min(Math.trunc(value), max);
|
|
347
|
+
fail(`Oberon ${key} timeout must be a positive finite number`);
|
|
348
|
+
}
|
|
349
|
+
return fallback;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function closeVerifiedSource(source) {
|
|
353
|
+
if (!source || source.fd === undefined) return;
|
|
354
|
+
try { fs.closeSync(source.fd); } catch { /* source cleanup is best effort after validation */ }
|
|
355
|
+
source.fd = undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
25
358
|
// v1 oberonRepoRoot() = engine/의 부모 = 패키지 루트. v2에서는 core/paths가 정본.
|
|
26
359
|
function defaultRenderEntry() {
|
|
27
360
|
const root = packageRoot();
|
|
@@ -31,6 +364,19 @@ function defaultRenderEntry() {
|
|
|
31
364
|
};
|
|
32
365
|
}
|
|
33
366
|
|
|
367
|
+
function readRenderEntry(file, label, missingMessage) {
|
|
368
|
+
let target;
|
|
369
|
+
try { target = path.resolve(String(file)); }
|
|
370
|
+
catch (error) { fail(`${label} path is invalid: ${error.message}`); }
|
|
371
|
+
try {
|
|
372
|
+
return readVerifiedFile(target, label, MAX_OBERON_SOURCE_BYTES);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error && error.code === "ENOENT") fail(missingMessage(target));
|
|
375
|
+
if (error && error.oberonFail) throw error;
|
|
376
|
+
fail(`${label} is unsafe or changed: ${error.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
34
380
|
/*
|
|
35
381
|
* 렌더 자식의 stdout 프로토콜 한 줄 처리 (v1 oberonRenderLine).
|
|
36
382
|
* POLL status= phase= clips= percent= → \r 진행률 바 갱신
|
|
@@ -39,24 +385,39 @@ function defaultRenderEntry() {
|
|
|
39
385
|
* WARNINGS= → 경고 표시
|
|
40
386
|
* JOB= / OUT_DIR= / *=present|missing → 내부 추적/키 존재 점검 라인 — 숨김
|
|
41
387
|
*/
|
|
42
|
-
function oberonRenderLine(line, files, io) {
|
|
388
|
+
function oberonRenderLine(line, files, io, evidence = null) {
|
|
43
389
|
let m;
|
|
44
390
|
if ((m = line.match(/^POLL status=(\S+) phase=(\S+) clips=(\S+) percent=(\d+)/))) {
|
|
45
391
|
const [, status, phase, clips, pct] = m;
|
|
392
|
+
boundedProtocolField(status, "POLL status");
|
|
393
|
+
boundedProtocolField(phase, "POLL phase");
|
|
394
|
+
boundedProtocolField(clips, "POLL clips");
|
|
395
|
+
boundedProtocolNumber(pct, "POLL percent");
|
|
46
396
|
const bar = oberonBar(Number(pct));
|
|
47
397
|
io.write(`\r⏳ ${bar} ${String(pct).padStart(3)}% ${phase} clips ${clips} `);
|
|
48
398
|
if (status === "succeeded") io.write("\n");
|
|
49
399
|
return;
|
|
50
400
|
}
|
|
51
401
|
if ((m = line.match(/^FILE kind=(\S+) name=(\S+) bytes=(\d+)/))) {
|
|
52
|
-
files.
|
|
402
|
+
if (files.length >= MAX_OBERON_FILES) fail(`Oberon render emitted more than ${MAX_OBERON_FILES} FILE records`);
|
|
403
|
+
boundedProtocolField(m[1], "FILE kind");
|
|
404
|
+
boundedProtocolField(m[2], "FILE name");
|
|
405
|
+
const bytes = boundedProtocolNumber(m[3], "FILE bytes");
|
|
406
|
+
files.push({ kind: m[1], name: m[2], bytes });
|
|
407
|
+
if (evidence) evidence.files += 1;
|
|
53
408
|
return;
|
|
54
409
|
}
|
|
55
410
|
if ((m = line.match(/^DELIVERY kind=(\S+) name=(\S+) path=(\S+) bytes=(\d+)/))) {
|
|
56
|
-
|
|
411
|
+
boundedProtocolField(m[1], "DELIVERY kind");
|
|
412
|
+
boundedProtocolField(m[2], "DELIVERY name");
|
|
413
|
+
boundedProtocolField(m[3], "DELIVERY path");
|
|
414
|
+
const bytes = boundedProtocolNumber(m[4], "DELIVERY bytes");
|
|
415
|
+
if (evidence) evidence.deliveries += 1;
|
|
416
|
+
io.out(` 📦 ${m[1].padEnd(11)} ${m[2]} (${oberonBytes(bytes)})`);
|
|
57
417
|
return;
|
|
58
418
|
}
|
|
59
419
|
if (line.startsWith("WARNINGS=")) {
|
|
420
|
+
boundedProtocolField(line.slice("WARNINGS=".length), "WARNINGS");
|
|
60
421
|
io.out(` ⚠ ${line.slice("WARNINGS=".length)}`);
|
|
61
422
|
return;
|
|
62
423
|
}
|
|
@@ -68,38 +429,128 @@ function oberonRenderLine(line, files, io) {
|
|
|
68
429
|
// `oberon render <manifest.json> [--delivery DIR] [--max-shots N] [--takes N]
|
|
69
430
|
// [--resolution R] [--max-polls N] [--poll-ms N] [--open] [--dry-run]`
|
|
70
431
|
async function render(io, args, deps = {}) {
|
|
71
|
-
const { flags, rest } = parseFlags(args
|
|
72
|
-
|
|
432
|
+
const { flags, rest } = parseFlags(args, {
|
|
433
|
+
delivery: "value",
|
|
434
|
+
"max-shots": "value",
|
|
435
|
+
takes: "value",
|
|
436
|
+
resolution: "value",
|
|
437
|
+
"max-polls": "value",
|
|
438
|
+
"poll-ms": "value",
|
|
439
|
+
open: "boolean",
|
|
440
|
+
"dry-run": "boolean",
|
|
441
|
+
});
|
|
442
|
+
if (rest.length !== 1) fail("A single manifest path is required: agentlas oberon render <manifest.json>");
|
|
73
443
|
const manifestPath = path.resolve(rest[0]);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
79
|
-
} catch (e) {
|
|
80
|
-
fail(`Failed to parse manifest JSON: ${e.message}`);
|
|
444
|
+
const manifest = readOberonManifest(manifestPath);
|
|
445
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) fail("The manifest must be a JSON object.");
|
|
446
|
+
if (typeof manifest.title !== "string" || !manifest.title.trim() || manifest.title.length > 300 || /[\u0000\r\n]/.test(manifest.title)) {
|
|
447
|
+
fail("The manifest title is invalid.");
|
|
81
448
|
}
|
|
82
449
|
if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("The manifest has no shots[].");
|
|
450
|
+
safeManifestId(manifest.productionId, "productionId");
|
|
451
|
+
if (manifest.shots.length > 12) fail("The manifest exceeds the 12-shot safety limit.");
|
|
452
|
+
const shotIds = new Set();
|
|
453
|
+
for (const [index, shot] of manifest.shots.entries()) {
|
|
454
|
+
if (!shot || typeof shot !== "object" || Array.isArray(shot)) fail(`Manifest shots[${index}] is invalid`);
|
|
455
|
+
const shotId = safeManifestId(shot.shotId, `shots[${index}].shotId`);
|
|
456
|
+
if (shotIds.has(shotId)) fail(`Manifest shotId is duplicated: ${shotId}`);
|
|
457
|
+
shotIds.add(shotId);
|
|
458
|
+
}
|
|
459
|
+
if (manifest.maxShots != null && (!Number.isInteger(manifest.maxShots) || manifest.maxShots < 1 || manifest.maxShots > 12)) {
|
|
460
|
+
fail("Manifest maxShots must be an integer from 1 to 12");
|
|
461
|
+
}
|
|
462
|
+
if (manifest.takesPerShot != null && (!Number.isInteger(manifest.takesPerShot) || manifest.takesPerShot < 1 || manifest.takesPerShot > 8)) {
|
|
463
|
+
fail("Manifest takesPerShot must be an integer from 1 to 8");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const overrides = {};
|
|
467
|
+
const maxShots = positiveIntegerFlag(flags, "max-shots", { max: 12 });
|
|
468
|
+
const takes = positiveIntegerFlag(flags, "takes", { max: 8 });
|
|
469
|
+
positiveIntegerFlag(flags, "max-polls", { max: 10_000 });
|
|
470
|
+
positiveIntegerFlag(flags, "poll-ms", { min: 100, max: 600_000 });
|
|
471
|
+
if (maxShots != null) overrides.maxShots = maxShots;
|
|
472
|
+
if (takes != null) overrides.takesPerShot = takes;
|
|
473
|
+
if (flags.resolution) {
|
|
474
|
+
if (flags.resolution.length > 32 || /[\u0000\r\n]/.test(flags.resolution)) fail("Oberon --resolution is invalid");
|
|
475
|
+
overrides.resolution = flags.resolution;
|
|
476
|
+
}
|
|
477
|
+
const patchedManifest = Object.keys(overrides).length ? { ...manifest, ...overrides } : null;
|
|
478
|
+
if (flags.delivery && (flags.delivery.length > 4096 || /[\u0000\r\n]/.test(flags.delivery))) fail("Oberon --delivery path is invalid");
|
|
479
|
+
const deliveryDir = path.resolve(flags.delivery || path.join(path.dirname(manifestPath), `${slugifyOberon(manifest.title)}-delivery`));
|
|
480
|
+
const timeoutConfig = deps.timeoutConfig || deps.timeouts || deps.timeout || {};
|
|
481
|
+
const idleTimeoutMs = timeoutSetting(
|
|
482
|
+
{ ...timeoutConfig, idleMs: deps.idleTimeoutMs ?? timeoutConfig.idleMs },
|
|
483
|
+
["idleMs", "idleTimeoutMs", "idle"],
|
|
484
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
485
|
+
);
|
|
486
|
+
const totalTimeoutMs = timeoutSetting(
|
|
487
|
+
{ ...timeoutConfig, totalMs: deps.totalTimeoutMs ?? timeoutConfig.totalMs },
|
|
488
|
+
["totalMs", "totalTimeoutMs", "total"],
|
|
489
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
490
|
+
);
|
|
491
|
+
const killGraceMs = timeoutSetting(
|
|
492
|
+
{ ...timeoutConfig, killGraceMs: deps.killGraceMs ?? timeoutConfig.killGraceMs },
|
|
493
|
+
["killGraceMs", "killGraceTimeoutMs", "killGrace"],
|
|
494
|
+
DEFAULT_KILL_GRACE_MS,
|
|
495
|
+
);
|
|
83
496
|
|
|
84
497
|
// seam: 다른 산출물 배치(예: Desktop 동봉 빌드)가 생기면 여기만 주입 교체.
|
|
85
498
|
const { script, builtRender } = (deps.resolveRenderEntry || defaultRenderEntry)();
|
|
86
499
|
// 정직 정지 — v1과 동일한 문구. 렌더 진입점이 없으면 절대 가짜 렌더를 하지 않는다.
|
|
87
|
-
|
|
88
|
-
|
|
500
|
+
let scriptSource;
|
|
501
|
+
let builtSource;
|
|
502
|
+
try {
|
|
503
|
+
scriptSource = readRenderEntry(
|
|
504
|
+
script,
|
|
505
|
+
"Headless render script",
|
|
506
|
+
(target) => `Headless render script not found (not included in the packaged app): ${target}`,
|
|
507
|
+
);
|
|
508
|
+
builtSource = readRenderEntry(
|
|
509
|
+
builtRender,
|
|
510
|
+
"Electron render build",
|
|
511
|
+
(target) => `An Electron build is required. Run npm run build:electron first (missing: ${target})`,
|
|
512
|
+
);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
closeVerifiedSource(scriptSource);
|
|
515
|
+
closeVerifiedSource(builtSource);
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
89
518
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
519
|
+
// The child must not reopen the user path after validation: always hand it a
|
|
520
|
+
// private immutable request snapshot, with overrides applied only to that copy.
|
|
521
|
+
let reqPath;
|
|
522
|
+
let temporaryRequestPath = null;
|
|
523
|
+
let temporaryRequestSnapshot = null;
|
|
524
|
+
let renderTempDir = null;
|
|
525
|
+
const ownedTempFiles = [];
|
|
526
|
+
try {
|
|
527
|
+
renderTempDir = privateRunDirectory();
|
|
528
|
+
const requestBytes = Buffer.from(`${JSON.stringify(patchedManifest || manifest, null, 2)}\n`, "utf8");
|
|
529
|
+
if (requestBytes.length <= 0 || requestBytes.length > MAX_OBERON_REQUEST_BYTES) {
|
|
530
|
+
fail(`Oberon render request exceeds ${MAX_OBERON_REQUEST_BYTES} bytes`);
|
|
531
|
+
}
|
|
532
|
+
temporaryRequestPath = path.join(renderTempDir, "request.json");
|
|
533
|
+
const requestFile = writeOwnedFile(temporaryRequestPath, requestBytes, "temporary Oberon request");
|
|
534
|
+
temporaryRequestSnapshot = requestFile.snapshot;
|
|
535
|
+
ownedTempFiles.push({ path: requestFile.path, snapshot: requestFile.snapshot, label: "temporary Oberon request" });
|
|
536
|
+
fs.closeSync(requestFile.fd);
|
|
537
|
+
requestFile.fd = undefined;
|
|
538
|
+
reqPath = temporaryRequestPath;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
closeVerifiedSource(scriptSource);
|
|
541
|
+
closeVerifiedSource(builtSource);
|
|
542
|
+
const cleanupError = cleanupPrivateRun(renderTempDir, ownedTempFiles);
|
|
543
|
+
if (cleanupError) fail(`${error.message}; ${cleanupError.message}`);
|
|
544
|
+
if (error && error.oberonFail) throw error;
|
|
545
|
+
fail(`Failed to prepare temporary Oberon request: ${error.message}`);
|
|
100
546
|
}
|
|
101
547
|
|
|
102
|
-
const
|
|
548
|
+
const cleanupTemporaryRequest = () => {
|
|
549
|
+
if (!temporaryRequestPath) return null;
|
|
550
|
+
const error = cleanupOwnedFile(temporaryRequestPath, temporaryRequestSnapshot, "temporary Oberon request");
|
|
551
|
+
if (!error) temporaryRequestPath = null;
|
|
552
|
+
return error;
|
|
553
|
+
};
|
|
103
554
|
|
|
104
555
|
const childEnv = { ...process.env };
|
|
105
556
|
delete childEnv.ELECTRON_RUN_AS_NODE; // full Electron으로 부팅 (Desktop execPath일 때)
|
|
@@ -118,14 +569,41 @@ async function render(io, args, deps = {}) {
|
|
|
118
569
|
if (flags["dry-run"]) {
|
|
119
570
|
const execPath = deps.execPath || process.execPath;
|
|
120
571
|
io.out("\n[dry-run] Command to run:");
|
|
121
|
-
io.out(` ${execPath} ${
|
|
572
|
+
io.out(` ${execPath} ${scriptSource.path}`);
|
|
122
573
|
io.out(" env: OBERON_LIVE_VEO=1");
|
|
123
574
|
io.out(` OBERON_LIVE_REQUEST_FILE=${reqPath}`);
|
|
124
575
|
io.out(` OBERON_LIVE_DELIVERY_DIR=${deliveryDir}`);
|
|
125
576
|
io.out(" (full Electron · GEMINI_API_KEY/GOOGLE_CLOUD_PROJECT vault required)");
|
|
577
|
+
closeVerifiedSource(scriptSource);
|
|
578
|
+
closeVerifiedSource(builtSource);
|
|
579
|
+
const cleanupError = cleanupTemporaryRequest();
|
|
580
|
+
const privateCleanupError = cleanupPrivateRun(renderTempDir, ownedTempFiles);
|
|
581
|
+
if (cleanupError || privateCleanupError) {
|
|
582
|
+
fail(`Failed to clean up temporary Oberon files: ${(cleanupError || privateCleanupError).message}`);
|
|
583
|
+
}
|
|
126
584
|
return 0;
|
|
127
585
|
}
|
|
128
586
|
|
|
587
|
+
let boundExecution;
|
|
588
|
+
try {
|
|
589
|
+
boundExecution = prepareBoundExecution(renderTempDir, scriptSource, builtSource, ownedTempFiles);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
closeVerifiedSource(scriptSource);
|
|
592
|
+
closeVerifiedSource(builtSource);
|
|
593
|
+
const cleanupError = cleanupPrivateRun(renderTempDir, ownedTempFiles);
|
|
594
|
+
if (cleanupError) fail(`${error.message}; ${cleanupError.message}`);
|
|
595
|
+
if (error && error.oberonFail) throw error;
|
|
596
|
+
fail(`Failed to prepare bound Oberon render: ${error.message}`);
|
|
597
|
+
}
|
|
598
|
+
closeVerifiedSource(scriptSource);
|
|
599
|
+
closeVerifiedSource(builtSource);
|
|
600
|
+
childEnv.AGENTLAS_OBERON_SCRIPT_PATH = scriptSource.path;
|
|
601
|
+
childEnv.AGENTLAS_OBERON_BUILT_PATH = builtSource.path;
|
|
602
|
+
childEnv.AGENTLAS_OBERON_SCRIPT_FD = "3";
|
|
603
|
+
childEnv.AGENTLAS_OBERON_BUILT_FD = "4";
|
|
604
|
+
childEnv.AGENTLAS_OBERON_SCRIPT_BYTES = String(boundExecution.scriptBytes);
|
|
605
|
+
childEnv.AGENTLAS_OBERON_BUILT_BYTES = String(boundExecution.builtBytes);
|
|
606
|
+
|
|
129
607
|
const doSpawn = deps.spawn || spawn;
|
|
130
608
|
const execPath = deps.execPath || process.execPath;
|
|
131
609
|
const stdoutStream = deps.stdout || process.stdout;
|
|
@@ -133,21 +611,58 @@ async function render(io, args, deps = {}) {
|
|
|
133
611
|
const lineIo = { out: io.out, write: (s) => stdoutStream.write(s) };
|
|
134
612
|
|
|
135
613
|
return new Promise((resolve) => {
|
|
136
|
-
const child = doSpawn(execPath, [script], { env: childEnv, stdio: ["ignore", "pipe", "pipe"] });
|
|
137
614
|
const files = [];
|
|
615
|
+
const evidence = { files: 0, deliveries: 0 };
|
|
138
616
|
let buf = "";
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
617
|
+
let stdoutBytes = 0;
|
|
618
|
+
let stderrBytes = 0;
|
|
619
|
+
let settled = false;
|
|
620
|
+
let stopRequested = false;
|
|
621
|
+
let stopReason = null;
|
|
622
|
+
let child = null;
|
|
623
|
+
let idleTimer = null;
|
|
624
|
+
let totalTimer = null;
|
|
625
|
+
let killTimer = null;
|
|
626
|
+
let forceTimer = null;
|
|
627
|
+
|
|
628
|
+
const clearTimers = () => {
|
|
629
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
630
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
631
|
+
if (killTimer) clearTimeout(killTimer);
|
|
632
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
633
|
+
idleTimer = null;
|
|
634
|
+
totalTimer = null;
|
|
635
|
+
killTimer = null;
|
|
636
|
+
forceTimer = null;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const cleanup = () => {
|
|
640
|
+
const requestError = cleanupTemporaryRequest();
|
|
641
|
+
const privateError = cleanupPrivateRun(renderTempDir, ownedTempFiles);
|
|
642
|
+
if (requestError && privateError) return new Error(`${requestError.message}; ${privateError.message}`);
|
|
643
|
+
return requestError || privateError;
|
|
147
644
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
645
|
+
|
|
646
|
+
const finish = (code, error = null) => {
|
|
647
|
+
if (settled) return;
|
|
648
|
+
settled = true;
|
|
649
|
+
clearTimers();
|
|
650
|
+
let finalError = error || stopReason;
|
|
651
|
+
if (!finalError && buf) {
|
|
652
|
+
try { oberonRenderLine(buf.replace(/\r$/, ""), files, lineIo, evidence); }
|
|
653
|
+
catch (parseError) { finalError = parseError; }
|
|
654
|
+
}
|
|
655
|
+
buf = "";
|
|
656
|
+
const cleanupError = cleanup();
|
|
657
|
+
if (!finalError && cleanupError) finalError = cleanupError;
|
|
658
|
+
if (!finalError && code === 0 && evidence.files === 0 && evidence.deliveries === 0) {
|
|
659
|
+
finalError = new Error("Render exited successfully without FILE or DELIVERY evidence");
|
|
660
|
+
}
|
|
661
|
+
if (finalError) {
|
|
662
|
+
stderrStream.write(`\n✖ Render process failed: ${finalError.message || finalError}\n`);
|
|
663
|
+
resolve(1);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
151
666
|
if (code === 0) {
|
|
152
667
|
io.out(`\n✓ Render complete — delivery folder: ${deliveryDir}`);
|
|
153
668
|
const titled = files.filter((f) => f.kind && f.kind.startsWith("titled"));
|
|
@@ -157,8 +672,118 @@ async function render(io, args, deps = {}) {
|
|
|
157
672
|
stderrStream.write(`\n✖ Render failed (exit ${code})\n`);
|
|
158
673
|
resolve(code || 1); // v1은 process.exitCode 설정 — v2는 코드 반환
|
|
159
674
|
}
|
|
160
|
-
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const stop = (reason) => {
|
|
678
|
+
if (settled || stopRequested) return;
|
|
679
|
+
stopRequested = true;
|
|
680
|
+
stopReason = reason instanceof Error ? reason : new Error(String(reason));
|
|
681
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
682
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
683
|
+
idleTimer = null;
|
|
684
|
+
totalTimer = null;
|
|
685
|
+
if (!child || typeof child.kill !== "function") {
|
|
686
|
+
finish(1, stopReason);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
try { child.kill("SIGTERM"); }
|
|
690
|
+
catch (error) { stopReason = new Error(`${stopReason.message}; failed to stop render process: ${error.message}`); }
|
|
691
|
+
killTimer = setTimeout(() => {
|
|
692
|
+
if (settled) return;
|
|
693
|
+
try { child.kill("SIGKILL"); }
|
|
694
|
+
catch (error) { stopReason = new Error(`${stopReason.message}; failed to force-stop render process: ${error.message}`); }
|
|
695
|
+
forceTimer = setTimeout(() => finish(1, stopReason), killGraceMs);
|
|
696
|
+
}, killGraceMs);
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const markActivity = () => {
|
|
700
|
+
if (settled || stopRequested) return;
|
|
701
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
702
|
+
idleTimer = setTimeout(() => stop(new Error(`Render process produced no output for ${idleTimeoutMs} ms`)), idleTimeoutMs);
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const handle = (chunk) => {
|
|
706
|
+
try {
|
|
707
|
+
markActivity();
|
|
708
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
709
|
+
if (stdoutBytes + bytes.length > MAX_OBERON_STDOUT_BYTES) {
|
|
710
|
+
stop(new Error(`Render stdout exceeded ${MAX_OBERON_STDOUT_BYTES} bytes`));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
stdoutBytes += bytes.length;
|
|
714
|
+
buf += bytes.toString("utf8");
|
|
715
|
+
let nl;
|
|
716
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
717
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
718
|
+
buf = buf.slice(nl + 1);
|
|
719
|
+
if (Buffer.byteLength(line, "utf8") > MAX_OBERON_LINE_BYTES) {
|
|
720
|
+
stop(new Error(`Render stdout line exceeded ${MAX_OBERON_LINE_BYTES} bytes`));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
oberonRenderLine(line, files, lineIo, evidence);
|
|
724
|
+
if (stopRequested) return;
|
|
725
|
+
}
|
|
726
|
+
if (Buffer.byteLength(buf, "utf8") > MAX_OBERON_LINE_BYTES) {
|
|
727
|
+
stop(new Error(`Render stdout line exceeded ${MAX_OBERON_LINE_BYTES} bytes`));
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
stop(error);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const handleStderr = (chunk) => {
|
|
735
|
+
try {
|
|
736
|
+
markActivity();
|
|
737
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
738
|
+
const remaining = Math.max(0, MAX_OBERON_STDERR_BYTES - stderrBytes);
|
|
739
|
+
if (remaining > 0) stderrStream.write(bytes.subarray(0, remaining).toString("utf8"));
|
|
740
|
+
stderrBytes += bytes.length;
|
|
741
|
+
if (stderrBytes > MAX_OBERON_STDERR_BYTES) stop(new Error(`Render stderr exceeded ${MAX_OBERON_STDERR_BYTES} bytes`));
|
|
742
|
+
} catch (error) {
|
|
743
|
+
stop(error);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
child = doSpawn(execPath, [boundExecution.bootstrapPath], {
|
|
749
|
+
env: childEnv,
|
|
750
|
+
stdio: ["ignore", "pipe", "pipe", boundExecution.scriptFd, boundExecution.builtFd],
|
|
751
|
+
});
|
|
752
|
+
if (!child || !child.stdout || !child.stderr || typeof child.on !== "function") {
|
|
753
|
+
throw new Error("Render process did not provide stdout/stderr streams");
|
|
754
|
+
}
|
|
755
|
+
child.stdout.on("data", handle);
|
|
756
|
+
child.stdout.on("error", (error) => stop(error));
|
|
757
|
+
child.stderr.on("data", handleStderr);
|
|
758
|
+
child.stderr.on("error", (error) => stop(error));
|
|
759
|
+
child.on("error", (error) => stop(error));
|
|
760
|
+
child.on("close", (code) => finish(code));
|
|
761
|
+
markActivity();
|
|
762
|
+
totalTimer = setTimeout(() => stop(new Error(`Render process exceeded total timeout of ${totalTimeoutMs} ms`)), totalTimeoutMs);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (boundExecution.scriptFd !== undefined) {
|
|
765
|
+
try { fs.closeSync(boundExecution.scriptFd); } catch { /* cleanup below */ }
|
|
766
|
+
boundExecution.scriptFd = undefined;
|
|
767
|
+
}
|
|
768
|
+
if (boundExecution.builtFd !== undefined) {
|
|
769
|
+
try { fs.closeSync(boundExecution.builtFd); } catch { /* cleanup below */ }
|
|
770
|
+
boundExecution.builtFd = undefined;
|
|
771
|
+
}
|
|
772
|
+
finish(1, error);
|
|
773
|
+
}
|
|
161
774
|
});
|
|
162
775
|
}
|
|
163
776
|
|
|
164
|
-
module.exports = {
|
|
777
|
+
module.exports = {
|
|
778
|
+
render,
|
|
779
|
+
oberonRenderLine,
|
|
780
|
+
defaultRenderEntry,
|
|
781
|
+
MAX_OBERON_MANIFEST_BYTES,
|
|
782
|
+
MAX_OBERON_SOURCE_BYTES,
|
|
783
|
+
MAX_OBERON_STDOUT_BYTES,
|
|
784
|
+
MAX_OBERON_LINE_BYTES,
|
|
785
|
+
MAX_OBERON_FILES,
|
|
786
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
787
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
788
|
+
DEFAULT_KILL_GRACE_MS,
|
|
789
|
+
};
|