agentlas 1.0.64 → 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 +87 -0
- package/README.md +8 -6
- 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/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- 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 +67 -34
- 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 +40 -12
- 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 +187 -24
- 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/cloud/auth.cjs
CHANGED
|
@@ -23,6 +23,95 @@ const { userDataDir } = require("../core/paths.cjs");
|
|
|
23
23
|
const LOGIN_CALLBACK_PATH = "/callback";
|
|
24
24
|
const LOGIN_TIMEOUT_MS = 180_000;
|
|
25
25
|
const MAX_LOGIN_SESSION_BYTES = 16 * 1024;
|
|
26
|
+
const MAX_LOGIN_SESSION_FILE_BYTES = MAX_LOGIN_SESSION_BYTES * 2;
|
|
27
|
+
const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
28
|
+
|
|
29
|
+
function sameFileIdentity(left, right) {
|
|
30
|
+
return !!left && !!right
|
|
31
|
+
&& left.dev === right.dev
|
|
32
|
+
&& left.ino === right.ino
|
|
33
|
+
&& left.nlink === right.nlink;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sameDirectoryIdentity(info) {
|
|
37
|
+
try {
|
|
38
|
+
const current = fs.lstatSync(info.path);
|
|
39
|
+
return current.isDirectory()
|
|
40
|
+
&& !current.isSymbolicLink()
|
|
41
|
+
&& current.dev === info.stat.dev
|
|
42
|
+
&& current.ino === info.stat.ino;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureRealDirectory(directory, { create = false } = {}) {
|
|
49
|
+
const resolved = path.resolve(String(directory || ""));
|
|
50
|
+
if (create) fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
51
|
+
const stat = fs.lstatSync(resolved);
|
|
52
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
53
|
+
throw new Error("Refusing to use a symbolic-link Agentlas session directory.");
|
|
54
|
+
}
|
|
55
|
+
return { path: resolved, stat, realpath: fs.realpathSync.native(resolved) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureAuthDirectory({ create = false } = {}) {
|
|
59
|
+
const root = ensureRealDirectory(userDataDir(), { create });
|
|
60
|
+
const info = ensureRealDirectory(path.join(root.path, "auth"), { create });
|
|
61
|
+
// The auth directory must be a child of the configured user-data directory,
|
|
62
|
+
// not a mounted/symlinked redirect that merely passes the final lstat.
|
|
63
|
+
if (path.dirname(info.realpath) !== root.realpath) {
|
|
64
|
+
throw new Error("Refusing to use a redirected Agentlas session directory.");
|
|
65
|
+
}
|
|
66
|
+
return info;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readBoundedFd(fd, maxBytes) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let total = 0;
|
|
72
|
+
while (total <= maxBytes) {
|
|
73
|
+
const want = Math.min(8192, maxBytes - total + 1);
|
|
74
|
+
const buffer = Buffer.allocUnsafe(want);
|
|
75
|
+
const count = fs.readSync(fd, buffer, 0, want, null);
|
|
76
|
+
if (!count) break;
|
|
77
|
+
total += count;
|
|
78
|
+
if (total > maxBytes) return null;
|
|
79
|
+
chunks.push(buffer.subarray(0, count));
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks, total);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readPrivateBoundedFile(filePath, maxBytes) {
|
|
85
|
+
let before;
|
|
86
|
+
let fd = null;
|
|
87
|
+
try {
|
|
88
|
+
before = fs.lstatSync(filePath);
|
|
89
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size > maxBytes) return null;
|
|
90
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
91
|
+
const opened = fs.fstatSync(fd);
|
|
92
|
+
if (!opened.isFile() || opened.nlink !== 1
|
|
93
|
+
|| opened.size > maxBytes || !sameFileIdentity(before, opened)) return null;
|
|
94
|
+
const contents = readBoundedFd(fd, maxBytes);
|
|
95
|
+
const after = fs.fstatSync(fd);
|
|
96
|
+
if (
|
|
97
|
+
contents === null || !after.isFile() || after.nlink !== 1 || !sameFileIdentity(opened, after) ||
|
|
98
|
+
after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
|
|
99
|
+
after.size !== contents.length
|
|
100
|
+
) return null;
|
|
101
|
+
return contents.toString("utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
} finally {
|
|
105
|
+
if (fd !== null) {
|
|
106
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeAll(fd, buffer) {
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (offset < buffer.length) offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
|
|
114
|
+
}
|
|
26
115
|
|
|
27
116
|
function validLoginSessionValue(value) {
|
|
28
117
|
return typeof value === "string" && value.length > 0 &&
|
|
@@ -230,9 +319,11 @@ function cliSessionPath() {
|
|
|
230
319
|
function readCliSessionValue() {
|
|
231
320
|
try {
|
|
232
321
|
const p = cliSessionPath();
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
322
|
+
const authDir = ensureAuthDirectory();
|
|
323
|
+
const raw = readPrivateBoundedFile(p, MAX_LOGIN_SESSION_FILE_BYTES);
|
|
324
|
+
if (raw === null) return null;
|
|
325
|
+
if (!sameDirectoryIdentity(authDir)) return null;
|
|
326
|
+
const j = JSON.parse(raw);
|
|
236
327
|
return j && validLoginSessionValue(j.value) ? j.value : null;
|
|
237
328
|
} catch {
|
|
238
329
|
return null;
|
|
@@ -243,29 +334,56 @@ function saveCliSession(value) {
|
|
|
243
334
|
if (!validLoginSessionValue(value)) throw new Error("Refusing to save an invalid Agentlas login session value.");
|
|
244
335
|
const p = cliSessionPath();
|
|
245
336
|
// 디렉터리 0700 + 파일 0600 — 기본 umask(0644)로 세션이 world-readable 이 되는 것을 막는다.
|
|
246
|
-
const
|
|
247
|
-
|
|
337
|
+
const authDir = ensureAuthDirectory({ create: true });
|
|
338
|
+
const dir = authDir.path;
|
|
248
339
|
try { fs.chmodSync(dir, 0o700); } catch { /* win32 */ }
|
|
249
340
|
const temp = path.join(dir, `.cli-session.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
341
|
+
const payload = Buffer.from(JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", "utf8");
|
|
342
|
+
if (payload.length > MAX_LOGIN_SESSION_FILE_BYTES) throw new Error("Refusing to save an oversized Agentlas login session.");
|
|
343
|
+
let fd = null;
|
|
250
344
|
try {
|
|
251
|
-
fs.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
345
|
+
fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
346
|
+
const created = fs.fstatSync(fd);
|
|
347
|
+
if (!created.isFile() || created.nlink !== 1) throw new Error("Refusing to create an unsafe Agentlas login session file.");
|
|
348
|
+
writeAll(fd, payload);
|
|
349
|
+
fs.fsyncSync(fd);
|
|
350
|
+
const written = fs.fstatSync(fd);
|
|
351
|
+
if (!sameFileIdentity(created, written) || written.size !== payload.length) throw new Error("Agentlas login session changed while saving.");
|
|
352
|
+
fs.closeSync(fd);
|
|
353
|
+
fd = null;
|
|
354
|
+
if (!sameDirectoryIdentity(authDir)) throw new Error("Agentlas login session directory changed while saving.");
|
|
256
355
|
fs.renameSync(temp, p);
|
|
356
|
+
const installed = fs.lstatSync(p);
|
|
357
|
+
if (installed.isSymbolicLink() || !installed.isFile() || installed.nlink !== 1) throw new Error("Refusing to install an unsafe Agentlas login session file.");
|
|
257
358
|
} catch (error) {
|
|
359
|
+
if (fd !== null) {
|
|
360
|
+
try { fs.closeSync(fd); } catch { /* preserve original */ }
|
|
361
|
+
}
|
|
258
362
|
try { fs.rmSync(temp, { force: true }); } catch { /* preserve original */ }
|
|
259
363
|
throw error;
|
|
260
364
|
}
|
|
261
|
-
try { fs.chmodSync(p, 0o600); } catch { /* win32 */ }
|
|
262
365
|
return p;
|
|
263
366
|
}
|
|
264
367
|
|
|
265
368
|
function deleteCliSession() {
|
|
266
369
|
const p = cliSessionPath();
|
|
267
|
-
|
|
268
|
-
|
|
370
|
+
let authDir;
|
|
371
|
+
try { authDir = ensureAuthDirectory(); } catch (error) {
|
|
372
|
+
if (error?.code === "ENOENT") return { existed: false, path: p };
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
let stat;
|
|
376
|
+
try { stat = fs.lstatSync(p); } catch (error) {
|
|
377
|
+
if (error?.code === "ENOENT") return { existed: false, path: p };
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
|
|
381
|
+
throw new Error("Refusing to delete an unsafe Agentlas login session file.");
|
|
382
|
+
}
|
|
383
|
+
if (!sameDirectoryIdentity(authDir)) throw new Error("Agentlas login session directory changed while deleting.");
|
|
384
|
+
const current = fs.lstatSync(p);
|
|
385
|
+
if (!sameFileIdentity(stat, current)) throw new Error("Agentlas login session changed while deleting.");
|
|
386
|
+
fs.unlinkSync(p);
|
|
269
387
|
return { existed: true, path: p };
|
|
270
388
|
}
|
|
271
389
|
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* - 업스트림 AbortSignal 전파
|
|
9
9
|
* 세션 쿠키 해석 순서: AGENTLAS_SESSION env → auth/cli-session.v1.json → (레거시) keytar.
|
|
10
10
|
*/
|
|
11
|
-
const fs = require("node:fs");
|
|
12
11
|
const path = require("node:path");
|
|
13
12
|
const { userDataDir } = require("../core/paths.cjs");
|
|
13
|
+
const { readJsonFile } = require("../mcp/contract.cjs");
|
|
14
14
|
|
|
15
15
|
const HUB_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
|
|
16
16
|
const HUB_TIMEOUT_DEFAULTS = Object.freeze({ connectMs: 15_000, idleMs: 30_000, totalMs: 180_000 });
|
|
@@ -69,6 +69,7 @@ async function fetchHub(url, init = {}, options = {}) {
|
|
|
69
69
|
let idleTimer = null;
|
|
70
70
|
let totalTimer = null;
|
|
71
71
|
let reader = null;
|
|
72
|
+
let iterator = null;
|
|
72
73
|
let terminalError = null;
|
|
73
74
|
let rejectTerminal;
|
|
74
75
|
const terminal = new Promise((_, reject) => { rejectTerminal = reject; });
|
|
@@ -106,27 +107,46 @@ async function fetchHub(url, init = {}, options = {}) {
|
|
|
106
107
|
const chunks = [];
|
|
107
108
|
let bytes = 0;
|
|
108
109
|
armIdle();
|
|
110
|
+
const acceptChunk = (value) => {
|
|
111
|
+
armIdle();
|
|
112
|
+
const chunk = Buffer.from(value || []);
|
|
113
|
+
bytes += chunk.length;
|
|
114
|
+
if (bytes > HUB_RESPONSE_MAX_BYTES) {
|
|
115
|
+
const error = new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
|
|
116
|
+
error.code = "AGENTLAS_HUB_RESPONSE_TOO_LARGE";
|
|
117
|
+
stop(error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
chunks.push(chunk);
|
|
121
|
+
};
|
|
109
122
|
if (response.body && typeof response.body.getReader === "function") {
|
|
110
123
|
reader = response.body.getReader();
|
|
111
124
|
while (true) {
|
|
112
125
|
const part = await Promise.race([reader.read(), terminal]);
|
|
113
126
|
if (part.done) break;
|
|
114
|
-
|
|
115
|
-
const chunk = Buffer.from(part.value || []);
|
|
116
|
-
bytes += chunk.length;
|
|
117
|
-
if (bytes > HUB_RESPONSE_MAX_BYTES) {
|
|
118
|
-
const error = new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
|
|
119
|
-
error.code = "AGENTLAS_HUB_RESPONSE_TOO_LARGE";
|
|
120
|
-
stop(error);
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
chunks.push(chunk);
|
|
127
|
+
acceptChunk(part.value);
|
|
124
128
|
}
|
|
129
|
+
} else if (response.body && typeof response.body[Symbol.asyncIterator] === "function") {
|
|
130
|
+
iterator = response.body[Symbol.asyncIterator]();
|
|
131
|
+
while (true) {
|
|
132
|
+
// Node streams and test adapters can expose only an async iterator. A
|
|
133
|
+
// plain `for await` leaves us stuck inside next() after idle/total expiry,
|
|
134
|
+
// even though the request's AbortController has already fired.
|
|
135
|
+
const part = await Promise.race([
|
|
136
|
+
Promise.resolve().then(() => iterator.next()),
|
|
137
|
+
terminal,
|
|
138
|
+
]);
|
|
139
|
+
if (part.done) break;
|
|
140
|
+
acceptChunk(part.value);
|
|
141
|
+
}
|
|
142
|
+
} else if (response.body == null) {
|
|
143
|
+
// Standards-compliant empty/HEAD/204 response. There is no body to read.
|
|
125
144
|
} else {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
// arrayBuffer()/text() allocates the complete attacker-controlled body
|
|
146
|
+
// before a size check can run, defeating the 16 MiB contract.
|
|
147
|
+
const error = new Error("Hub response body is not stream-readable; refusing an unbounded buffered read.");
|
|
148
|
+
error.code = "AGENTLAS_HUB_RESPONSE_NOT_STREAMABLE";
|
|
149
|
+
throw error;
|
|
130
150
|
}
|
|
131
151
|
if (idleTimer) clearTimeout(idleTimer);
|
|
132
152
|
idleTimer = null;
|
|
@@ -143,6 +163,11 @@ async function fetchHub(url, init = {}, options = {}) {
|
|
|
143
163
|
if (reader && terminalError) {
|
|
144
164
|
try { await reader.cancel(terminalError); } catch { /* ignore */ }
|
|
145
165
|
}
|
|
166
|
+
if (iterator && terminalError && typeof iterator.return === "function") {
|
|
167
|
+
// Do not await an untrusted iterator's cleanup hook: the timeout must
|
|
168
|
+
// remain a hard upper bound even when return() itself never settles.
|
|
169
|
+
Promise.resolve().then(() => iterator.return()).catch(() => {});
|
|
170
|
+
}
|
|
146
171
|
}
|
|
147
172
|
}
|
|
148
173
|
|
|
@@ -161,7 +186,7 @@ function cliSessionPath() {
|
|
|
161
186
|
|
|
162
187
|
function readCliSessionValue() {
|
|
163
188
|
try {
|
|
164
|
-
const j =
|
|
189
|
+
const { value: j } = readJsonFile(cliSessionPath(), "Agentlas CLI session");
|
|
165
190
|
return (j && typeof j.value === "string" && j.value) || null;
|
|
166
191
|
} catch {
|
|
167
192
|
return null;
|
|
@@ -224,18 +224,27 @@ async function deleteCloudAgent(slug, options = {}) {
|
|
|
224
224
|
) {
|
|
225
225
|
throw new Error("Agentlas Cloud delete returned an invalid or mismatched deletion receipt.");
|
|
226
226
|
}
|
|
227
|
-
const stateValue = state.readCloudAssetState();
|
|
228
227
|
const key = state.cloudDescriptorKey(descriptor);
|
|
229
|
-
|
|
228
|
+
// The server delete used this exact observed descriptor. Keep its roots even
|
|
229
|
+
// if another local process advances the same key before the local journal
|
|
230
|
+
// update; tombstones authenticate the deleted revision, not a later one.
|
|
231
|
+
const roots = [...(localEntry.sourceRoots || [])];
|
|
230
232
|
const warnings = [];
|
|
231
|
-
for (const rootPath of roots) {
|
|
232
|
-
// 톰스톤: 삭제된 베이스는 재저장 시 절대 If-Match 베이스로 재사용되지 않는다.
|
|
233
|
-
stateValue.deletedBases.push({ rootPath, slug: descriptor.slug, scope: descriptor.scope, cloudId: descriptor.cloudId, revision: descriptor.revision });
|
|
234
|
-
}
|
|
235
|
-
delete stateValue.assets[key];
|
|
236
|
-
stateValue.deletedBases = stateValue.deletedBases.slice(-256);
|
|
237
233
|
try {
|
|
238
|
-
state.
|
|
234
|
+
state.updateCloudAssetState((stateValue) => {
|
|
235
|
+
for (const rootPath of roots) {
|
|
236
|
+
// 톰스톤: 삭제된 베이스는 재저장 시 절대 If-Match 베이스로 재사용되지 않는다.
|
|
237
|
+
stateValue.deletedBases.push({ rootPath, slug: descriptor.slug, scope: descriptor.scope, cloudId: descriptor.cloudId, revision: descriptor.revision });
|
|
238
|
+
}
|
|
239
|
+
const current = stateValue.assets[key];
|
|
240
|
+
if (
|
|
241
|
+
current && current.descriptor.cloudId === descriptor.cloudId &&
|
|
242
|
+
current.descriptor.revision === descriptor.revision
|
|
243
|
+
) {
|
|
244
|
+
delete stateValue.assets[key];
|
|
245
|
+
}
|
|
246
|
+
stateValue.deletedBases = stateValue.deletedBases.slice(-256);
|
|
247
|
+
});
|
|
239
248
|
} catch (error) {
|
|
240
249
|
const stateError = new Error(
|
|
241
250
|
`Cloud delete committed on the server, but this machine could not persist the deletion tombstone. ` +
|
|
@@ -166,30 +166,80 @@ function writePurposeRepairCopy(root, projection) {
|
|
|
166
166
|
return tempRoot;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
const CLOUD_VALUE_FLAGS = ["limit", "name", "purpose", "scope", "slug", "visibility"];
|
|
170
|
+
const CLOUD_BOOLEAN_FLAGS = ["dry-run", "json", "llm-review", "overwrite", "strict"];
|
|
171
|
+
|
|
172
|
+
function cloudArgumentError(message) {
|
|
173
|
+
const error = new Error(message);
|
|
174
|
+
error.code = "INVALID_ARGUMENT";
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseCloudFlags(args, spec = {}) {
|
|
179
|
+
const values = new Set(spec.values || CLOUD_VALUE_FLAGS);
|
|
180
|
+
const booleans = new Set(spec.booleans || CLOUD_BOOLEAN_FLAGS);
|
|
170
181
|
const flags = { _: [] };
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
let positionalOnly = false;
|
|
171
184
|
for (let i = 0; i < args.length; i++) {
|
|
172
185
|
const a = args[i];
|
|
173
|
-
if (
|
|
174
|
-
const key = a.slice(2);
|
|
175
|
-
const next = args[i + 1];
|
|
176
|
-
if (next !== undefined && !String(next).startsWith("--")) {
|
|
177
|
-
flags[key] = next;
|
|
178
|
-
i++;
|
|
179
|
-
} else {
|
|
180
|
-
flags[key] = true;
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
186
|
+
if (positionalOnly) {
|
|
183
187
|
flags._.push(a);
|
|
188
|
+
continue;
|
|
184
189
|
}
|
|
190
|
+
if (a === "--") {
|
|
191
|
+
positionalOnly = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!String(a).startsWith("-")) {
|
|
195
|
+
flags._.push(a);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!String(a).startsWith("--")) throw cloudArgumentError(`unknown option: ${a}`);
|
|
199
|
+
const equal = String(a).indexOf("=");
|
|
200
|
+
const key = String(a).slice(2, equal === -1 ? undefined : equal);
|
|
201
|
+
if (!key || (!values.has(key) && !booleans.has(key))) throw cloudArgumentError(`unknown option: --${key || ""}`);
|
|
202
|
+
if (seen.has(key)) throw cloudArgumentError(`duplicate option: --${key}`);
|
|
203
|
+
seen.add(key);
|
|
204
|
+
if (booleans.has(key)) {
|
|
205
|
+
if (equal !== -1) throw cloudArgumentError(`--${key} does not take a value`);
|
|
206
|
+
flags[key] = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
let value;
|
|
210
|
+
if (equal !== -1) {
|
|
211
|
+
value = String(a).slice(equal + 1);
|
|
212
|
+
} else {
|
|
213
|
+
const next = args[i + 1];
|
|
214
|
+
if (next === undefined || String(next).startsWith("--")) throw cloudArgumentError(`--${key} requires a value`);
|
|
215
|
+
value = String(next);
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
if (!value) throw cloudArgumentError(`--${key} requires a non-empty value`);
|
|
219
|
+
flags[key] = value;
|
|
185
220
|
}
|
|
186
221
|
return flags;
|
|
187
222
|
}
|
|
188
223
|
|
|
224
|
+
function requireCloudPositionals(flags, min, max, usage) {
|
|
225
|
+
if (flags._.length < min || flags._.length > max) throw cloudArgumentError(usage);
|
|
226
|
+
return flags;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function positiveIntegerFlag(value, fallback, name, max) {
|
|
230
|
+
if (value == null) return fallback;
|
|
231
|
+
if (!/^\d+$/.test(String(value))) throw cloudArgumentError(`--${name} must be a positive integer`);
|
|
232
|
+
const parsed = Number(value);
|
|
233
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
|
|
234
|
+
throw cloudArgumentError(`--${name} must be between 1 and ${max}`);
|
|
235
|
+
}
|
|
236
|
+
return parsed;
|
|
237
|
+
}
|
|
238
|
+
|
|
189
239
|
function cloudVisibilityFlag(value) {
|
|
190
240
|
if (value == null) return null;
|
|
191
241
|
if (value === "private-link" || value === "marketplace") return value;
|
|
192
|
-
throw
|
|
242
|
+
throw cloudArgumentError("--visibility must be private-link or marketplace");
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
function cloudVisibilityForAction(sub, flags) {
|
|
@@ -213,7 +263,10 @@ function cloudVisibilityForAction(sub, flags) {
|
|
|
213
263
|
|
|
214
264
|
/** 최상위 `upload`의 실제 동작 결정: 기본 save, 명시적 marketplace만 publish. */
|
|
215
265
|
function cloudActionForTopLevelUpload(args) {
|
|
216
|
-
const flags = parseCloudFlags(args
|
|
266
|
+
const flags = requireCloudPositionals(parseCloudFlags(args, {
|
|
267
|
+
values: ["purpose", "slug", "visibility"],
|
|
268
|
+
booleans: ["dry-run", "json", "llm-review", "overwrite"],
|
|
269
|
+
}), 1, 1, "usage: agentlas upload <path> [--visibility marketplace]");
|
|
217
270
|
return cloudVisibilityFlag(flags.visibility) === "marketplace" ? "publish" : "save";
|
|
218
271
|
}
|
|
219
272
|
|
|
@@ -269,17 +322,20 @@ const CLOUD_HELP = [
|
|
|
269
322
|
async function runCloud(ctx, args) {
|
|
270
323
|
const sub = args[0] || "help";
|
|
271
324
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
325
|
+
requireCloudPositionals(parseCloudFlags(args.slice(1), { values: [], booleans: ["json"] }), 0, 0, "usage: agentlas cloud help");
|
|
272
326
|
ctx.out(CLOUD_HELP);
|
|
273
327
|
return 0;
|
|
274
328
|
}
|
|
275
329
|
if (sub === "search") {
|
|
276
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
330
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
331
|
+
values: ["limit"], booleans: ["json"],
|
|
332
|
+
}), 1, Number.MAX_SAFE_INTEGER, 'usage: agentlas cloud search "<what you need>" [--limit 10]');
|
|
277
333
|
const query = flags._.join(" ").trim();
|
|
278
334
|
if (!query) {
|
|
279
335
|
ctx.err('usage: agentlas cloud search "<what you need>" [--limit 10]');
|
|
280
336
|
return 1;
|
|
281
337
|
}
|
|
282
|
-
const limit =
|
|
338
|
+
const limit = positiveIntegerFlag(flags.limit, 10, "limit", 30);
|
|
283
339
|
let result;
|
|
284
340
|
try {
|
|
285
341
|
result = await callHubTool("marketplace.search_agents", { q: query, limit });
|
|
@@ -287,6 +343,7 @@ async function runCloud(ctx, args) {
|
|
|
287
343
|
ctx.err(e instanceof HubError ? e.message : `Marketplace connection failed: ${(e && e.message) || e}`);
|
|
288
344
|
return 1;
|
|
289
345
|
}
|
|
346
|
+
if (flags.json) { ctx.out(JSON.stringify(result, null, 2)); return 0; }
|
|
290
347
|
const items = (result && (result.results || result.agents || result.items)) || [];
|
|
291
348
|
if (!Array.isArray(items) || !items.length) {
|
|
292
349
|
ctx.out(`No results for "${query}"`);
|
|
@@ -298,8 +355,10 @@ async function runCloud(ctx, args) {
|
|
|
298
355
|
return 0;
|
|
299
356
|
}
|
|
300
357
|
if (sub === "list") {
|
|
301
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
302
|
-
|
|
358
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
359
|
+
values: ["limit"], booleans: ["json"],
|
|
360
|
+
}), 0, 0, "usage: agentlas cloud list [--limit 100] [--json]");
|
|
361
|
+
const result = await listOwnedCloudAgents(positiveIntegerFlag(flags.limit, 100, "limit", 1000));
|
|
303
362
|
if (flags.json) { ctx.out(JSON.stringify(result, null, 2)); return 0; }
|
|
304
363
|
const agents = Array.isArray(result.results) ? result.results : [];
|
|
305
364
|
if (!agents.length) { ctx.out("No agents are stored in Private Agent Cloud."); return 0; }
|
|
@@ -332,7 +391,9 @@ async function runCloud(ctx, args) {
|
|
|
332
391
|
return 0;
|
|
333
392
|
}
|
|
334
393
|
if (sub === "restore") {
|
|
335
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
394
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
395
|
+
values: [], booleans: ["json"],
|
|
396
|
+
}), 1, 1, "usage: agentlas cloud restore <slug> [--json]");
|
|
336
397
|
const slug = flags._[0];
|
|
337
398
|
if (!slug) { ctx.err("usage: agentlas cloud restore <slug> [--json]"); return 1; }
|
|
338
399
|
const result = await restoreOwnedCloudAgent(ctx.db(), slug);
|
|
@@ -344,7 +405,9 @@ async function runCloud(ctx, args) {
|
|
|
344
405
|
return 0;
|
|
345
406
|
}
|
|
346
407
|
if (sub === "delete" || sub === "unpublish") {
|
|
347
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
408
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
409
|
+
values: ["scope"], booleans: ["json"],
|
|
410
|
+
}), 1, 1, `usage: agentlas cloud ${sub} <slug> [--scope owner-private|hub-public] [--json]`);
|
|
348
411
|
const slug = flags._[0];
|
|
349
412
|
if (!slug) { ctx.err(`usage: agentlas cloud ${sub} <slug> [--json]`); return 1; }
|
|
350
413
|
const result = await deleteCloudAgent(slug, { scope: flags.scope });
|
|
@@ -356,7 +419,9 @@ async function runCloud(ctx, args) {
|
|
|
356
419
|
}
|
|
357
420
|
// ── agentlas-cloud-runtime 소비 지점 (스캔/매니페스트/lazy-read/필드테스트) ──
|
|
358
421
|
if (sub === "wizard") {
|
|
359
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
422
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
423
|
+
values: ["name"], booleans: ["json"],
|
|
424
|
+
}), 1, 1, "usage: agentlas cloud wizard <path> [--name name] [--json]");
|
|
360
425
|
const root = flags._[0];
|
|
361
426
|
if (!root) { ctx.err("usage: agentlas cloud wizard <path> [--name name]"); return 1; }
|
|
362
427
|
const result = cloudRuntime.runWizard(root, { name: typeof flags.name === "string" ? flags.name : undefined });
|
|
@@ -365,7 +430,9 @@ async function runCloud(ctx, args) {
|
|
|
365
430
|
}
|
|
366
431
|
if (sub === "security") {
|
|
367
432
|
if (args[1] !== "scan") { ctx.err("usage: agentlas cloud security scan <path> [--strict]"); return 1; }
|
|
368
|
-
const flags = parseCloudFlags(args.slice(2)
|
|
433
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
|
|
434
|
+
values: [], booleans: ["json", "strict"],
|
|
435
|
+
}), 1, 1, "usage: agentlas cloud security scan <path> [--strict]");
|
|
369
436
|
const root = flags._[0];
|
|
370
437
|
if (!root) { ctx.err("usage: agentlas cloud security scan <path> [--strict]"); return 1; }
|
|
371
438
|
const report = cloudRuntime.scanFolder(root);
|
|
@@ -375,15 +442,18 @@ async function runCloud(ctx, args) {
|
|
|
375
442
|
if (sub === "runtime") {
|
|
376
443
|
const action = args[1];
|
|
377
444
|
if (action === "bundle") {
|
|
378
|
-
const
|
|
379
|
-
|
|
445
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
|
|
446
|
+
values: [], booleans: ["json"],
|
|
447
|
+
}), 1, 1, "usage: agentlas cloud runtime bundle <path> [--json]");
|
|
448
|
+
const root = flags._[0];
|
|
380
449
|
ctx.out(JSON.stringify(cloudRuntime.compileBundle(root), null, 2));
|
|
381
450
|
return 0;
|
|
382
451
|
}
|
|
383
452
|
if (action === "read-agent-file") {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
453
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(2), {
|
|
454
|
+
values: [], booleans: ["json"],
|
|
455
|
+
}), 2, 2, "usage: agentlas cloud runtime read-agent-file <path> <file> [--json]");
|
|
456
|
+
const [root, targetPath] = flags._;
|
|
387
457
|
ctx.out(JSON.stringify(cloudRuntime.readAgentFile(root, targetPath), null, 2));
|
|
388
458
|
return 0;
|
|
389
459
|
}
|
|
@@ -391,15 +461,20 @@ async function runCloud(ctx, args) {
|
|
|
391
461
|
return 1;
|
|
392
462
|
}
|
|
393
463
|
if (sub === "field-test") {
|
|
394
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
464
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
465
|
+
values: [], booleans: ["json"],
|
|
466
|
+
}), 0, 0, "usage: agentlas cloud field-test [--json]");
|
|
395
467
|
const result = cloudRuntime.runFieldTest();
|
|
396
468
|
ctx.out(flags.json ? JSON.stringify(result, null, 2) : `${result.suite}: ${result.status}`);
|
|
397
469
|
return result.status === "PASS" ? 0 : 1;
|
|
398
470
|
}
|
|
399
471
|
if (sub === "install") {
|
|
400
|
-
const
|
|
401
|
-
|
|
472
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
473
|
+
values: [], booleans: ["json"],
|
|
474
|
+
}), 1, 1, "usage: agentlas cloud install <slug> [--json]");
|
|
475
|
+
const slug = flags._[0];
|
|
402
476
|
const agent = await installHubAgent(ctx.db(), slug);
|
|
477
|
+
if (flags.json) { ctx.out(JSON.stringify(agent, null, 2)); return 0; }
|
|
403
478
|
ctx.out(`${ctx.ui.green("✓")} Hub installed ${agent.slug} — ${agent.name}`);
|
|
404
479
|
if (agent.localPath) ctx.out(` files: ${agent.localPath}`);
|
|
405
480
|
return 0;
|
|
@@ -408,7 +483,10 @@ async function runCloud(ctx, args) {
|
|
|
408
483
|
ctx.err("usage: agentlas cloud <save|publish|package|list|restore|install|delete|field-test> ...");
|
|
409
484
|
return 1;
|
|
410
485
|
}
|
|
411
|
-
const flags = parseCloudFlags(args.slice(1)
|
|
486
|
+
const flags = requireCloudPositionals(parseCloudFlags(args.slice(1), {
|
|
487
|
+
values: ["purpose", "slug", "visibility"],
|
|
488
|
+
booleans: ["dry-run", "json", "llm-review", "overwrite"],
|
|
489
|
+
}), 1, 1, `usage: agentlas cloud ${sub} <path>`);
|
|
412
490
|
const root = flags._[0];
|
|
413
491
|
if (!root) { ctx.err(`usage: agentlas cloud ${sub} <path>`); return 1; }
|
|
414
492
|
const visibility = cloudVisibilityForAction(sub, flags);
|