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/cli-output.cjs
CHANGED
|
@@ -67,12 +67,23 @@ function textOf(value) {
|
|
|
67
67
|
return JSON.stringify(value);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function terminalTextOf(value, maxLength = Number.POSITIVE_INFINITY) {
|
|
71
|
+
const text = textOf(value);
|
|
72
|
+
if (typeof text !== "string") return "";
|
|
73
|
+
return text
|
|
74
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "")
|
|
75
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
76
|
+
.replace(/\u001b[@-_]/g, "")
|
|
77
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ")
|
|
78
|
+
.slice(0, maxLength);
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* 표시 폭 — ANSI 를 벗기고, CJK 전각을 2칸으로 센다.
|
|
72
83
|
* 한글 표가 어긋나던 이유가 이것이다(폭을 문자 수로 세면 열이 밀린다).
|
|
73
84
|
*/
|
|
74
85
|
function displayWidth(value) {
|
|
75
|
-
const plain =
|
|
86
|
+
const plain = terminalTextOf(value);
|
|
76
87
|
let width = 0;
|
|
77
88
|
for (const char of plain) {
|
|
78
89
|
const code = char.codePointAt(0) ?? 0;
|
|
@@ -102,7 +113,7 @@ function padTo(value, width, align) {
|
|
|
102
113
|
function renderQuiet(result) {
|
|
103
114
|
const idField = result.schema.idField;
|
|
104
115
|
return rowsOf(result)
|
|
105
|
-
.map((item) =>
|
|
116
|
+
.map((item) => terminalTextOf(readField(item, idField)))
|
|
106
117
|
.filter((line) => line.length > 0)
|
|
107
118
|
.join("\n");
|
|
108
119
|
}
|
|
@@ -122,31 +133,65 @@ function renderYaml(result) {
|
|
|
122
133
|
if (value === null || value === undefined) return "null";
|
|
123
134
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
124
135
|
const text = textOf(value);
|
|
125
|
-
|
|
136
|
+
// YAML implicitly retypes unquoted strings such as yes/null/001/dates.
|
|
137
|
+
// Machine output must round-trip the command result without changing a
|
|
138
|
+
// string into a boolean, null, number, or timestamp. Conservatively leave
|
|
139
|
+
// only unambiguous word/path-like strings plain; JSON quoting is valid YAML.
|
|
140
|
+
const ambiguousWord = /^(?:~|null|true|false|yes|no|on|off|[-+]?\.?(?:inf|nan))$/i.test(text);
|
|
141
|
+
return /^[A-Za-z_./][\w./:@-]*$/.test(text) && !ambiguousWord ? text : JSON.stringify(text);
|
|
142
|
+
};
|
|
143
|
+
const isObject = (value) => value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
144
|
+
const assertBoundedTree = (root) => {
|
|
145
|
+
let nodes = 0;
|
|
146
|
+
const visit = (value, ancestors, depth) => {
|
|
147
|
+
if ((!isObject(value) && !Array.isArray(value)) || value instanceof Date) return;
|
|
148
|
+
if (depth > 64) throw new TypeError("YAML output exceeds the maximum nesting depth");
|
|
149
|
+
if (ancestors.has(value)) throw new TypeError("YAML output contains a circular reference");
|
|
150
|
+
nodes += 1;
|
|
151
|
+
if (nodes > 100_000) throw new TypeError("YAML output contains too many nodes");
|
|
152
|
+
const next = new Set(ancestors);
|
|
153
|
+
next.add(value);
|
|
154
|
+
for (const child of Array.isArray(value) ? value : Object.values(value)) visit(child, next, depth + 1);
|
|
155
|
+
};
|
|
156
|
+
visit(root, new Set(), 0);
|
|
126
157
|
};
|
|
127
158
|
const objectLines = (object, indent) => {
|
|
128
159
|
const pad = " ".repeat(indent);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
const entries = Object.entries(object);
|
|
161
|
+
if (!entries.length) return [`${pad}{}`];
|
|
162
|
+
return entries.flatMap(([key, value]) => {
|
|
163
|
+
const renderedKey = scalar(key);
|
|
164
|
+
if (isObject(value) || Array.isArray(value)) {
|
|
165
|
+
const nested = value.length === 0 ? [`${pad} ${Array.isArray(value) ? "[]" : "{}"}`] : valueLines(value, indent + 2);
|
|
166
|
+
if (nested.length === 1 && /^(?:\{\}|\[\])$/.test(nested[0].trim())) return [`${pad}${renderedKey}: ${nested[0].trim()}`];
|
|
167
|
+
return [`${pad}${renderedKey}:`, ...nested];
|
|
132
168
|
}
|
|
133
|
-
|
|
134
|
-
if (value.length === 0) return `${pad}${key}: []`;
|
|
135
|
-
return `${pad}${key}:\n${value.map((entry) => `${pad} - ${scalar(entry)}`).join("\n")}`;
|
|
136
|
-
}
|
|
137
|
-
return `${pad}${key}: ${scalar(value)}`;
|
|
169
|
+
return [`${pad}${renderedKey}: ${scalar(value)}`];
|
|
138
170
|
});
|
|
139
171
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
const valueLines = (value, indent) => {
|
|
173
|
+
const pad = " ".repeat(indent);
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
if (!value.length) return [`${pad}[]`];
|
|
176
|
+
return value.flatMap((entry) => {
|
|
177
|
+
if (isObject(entry)) {
|
|
178
|
+
const nested = objectLines(entry, indent + 2);
|
|
179
|
+
return [`${pad}- ${nested[0].trimStart()}`, ...nested.slice(1)];
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(entry)) {
|
|
182
|
+
const nested = valueLines(entry, indent + 2);
|
|
183
|
+
return [`${pad}-`, ...nested];
|
|
184
|
+
}
|
|
185
|
+
return [`${pad}- ${scalar(entry)}`];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (isObject(value)) return objectLines(value, indent);
|
|
189
|
+
return [`${pad}${scalar(value)}`];
|
|
190
|
+
};
|
|
191
|
+
const payload = result.type === "list" ? result.data.map(project) : project(result.data);
|
|
192
|
+
assertBoundedTree(payload);
|
|
193
|
+
if (result.type === "list" && result.data.length === 0) return "[]";
|
|
194
|
+
return valueLines(payload, 0).join("\n");
|
|
150
195
|
}
|
|
151
196
|
|
|
152
197
|
function renderTable(result, options) {
|
|
@@ -154,11 +199,11 @@ function renderTable(result, options) {
|
|
|
154
199
|
const rows = rowsOf(result);
|
|
155
200
|
if (rows.length === 0) return "";
|
|
156
201
|
const cells = rows.map((item) =>
|
|
157
|
-
columns.map((column) =>
|
|
202
|
+
columns.map((column) => terminalTextOf(readField(item, column.field))),
|
|
158
203
|
);
|
|
159
204
|
const widths = columns.map((column, index) =>
|
|
160
205
|
Math.max(
|
|
161
|
-
options.noHeaders ? 0 : displayWidth(column.header),
|
|
206
|
+
options.noHeaders ? 0 : displayWidth(terminalTextOf(column.header)),
|
|
162
207
|
...cells.map((row) => displayWidth(row[index] ?? "")),
|
|
163
208
|
),
|
|
164
209
|
);
|
|
@@ -166,7 +211,7 @@ function renderTable(result, options) {
|
|
|
166
211
|
if (!options.noHeaders) {
|
|
167
212
|
lines.push(
|
|
168
213
|
columns
|
|
169
|
-
.map((column, index) => padTo(column.header.toUpperCase(), widths[index], column.align))
|
|
214
|
+
.map((column, index) => padTo(terminalTextOf(column.header).toUpperCase(), widths[index], column.align))
|
|
170
215
|
.join(" ")
|
|
171
216
|
.trimEnd(),
|
|
172
217
|
);
|
|
@@ -212,12 +257,13 @@ function renderError(error, options = {}) {
|
|
|
212
257
|
const commandError = toCommandError(error);
|
|
213
258
|
if (opts.format === "json") return JSON.stringify({ error: commandError }, null, 2);
|
|
214
259
|
if (opts.format === "yaml") {
|
|
215
|
-
return ["error:", ` code: ${commandError.code}`, ` message: ${JSON.stringify(commandError.message)}`].join("\n");
|
|
260
|
+
return ["error:", ` code: ${JSON.stringify(commandError.code)}`, ` message: ${JSON.stringify(commandError.message)}`].join("\n");
|
|
216
261
|
}
|
|
217
262
|
const prefix = opts.noColor ? "Error: " : "[31mError: [0m";
|
|
263
|
+
const safeMessage = terminalTextOf(commandError.message, 8192);
|
|
218
264
|
return commandError.details && typeof commandError.details === "string" && opts.format === "table"
|
|
219
|
-
? `${prefix}${
|
|
220
|
-
: `${prefix}${
|
|
265
|
+
? `${prefix}${safeMessage}`
|
|
266
|
+
: `${prefix}${safeMessage}`;
|
|
221
267
|
}
|
|
222
268
|
|
|
223
269
|
/**
|
|
@@ -230,14 +276,33 @@ function renderError(error, options = {}) {
|
|
|
230
276
|
function parseOutputFlags(argv, env = process.env, isTty = Boolean(process.stdout.isTTY)) {
|
|
231
277
|
const rest = [];
|
|
232
278
|
const options = { ...DEFAULT_OPTIONS };
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
279
|
+
let sawJson = false;
|
|
280
|
+
let sawYaml = false;
|
|
281
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
282
|
+
const token = argv[index];
|
|
283
|
+
if (token === "--") {
|
|
284
|
+
rest.push(...argv.slice(index + 1));
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (token === "--json") {
|
|
288
|
+
sawJson = true;
|
|
289
|
+
options.format = "json";
|
|
290
|
+
}
|
|
291
|
+
else if (token === "--yaml") {
|
|
292
|
+
sawYaml = true;
|
|
293
|
+
options.format = "yaml";
|
|
294
|
+
}
|
|
236
295
|
else if (token === "--quiet" || token === "-q") options.quiet = true;
|
|
237
296
|
else if (token === "--no-headers") options.noHeaders = true;
|
|
238
297
|
else if (token === "--no-color") options.noColor = true;
|
|
239
298
|
else rest.push(token);
|
|
240
299
|
}
|
|
300
|
+
if (sawJson && sawYaml) {
|
|
301
|
+
const error = new Error("--json and --yaml cannot be used together");
|
|
302
|
+
error.code = "INVALID_ARGUMENT";
|
|
303
|
+
error.details = { flags: ["--json", "--yaml"] };
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
241
306
|
if (env && (env.NO_COLOR || env.AGENTLAS_NO_COLOR)) options.noColor = true;
|
|
242
307
|
if (!isTty) options.noColor = true;
|
|
243
308
|
return { options, rest };
|
|
@@ -259,4 +324,5 @@ module.exports = {
|
|
|
259
324
|
parseOutputFlags,
|
|
260
325
|
isRichUi,
|
|
261
326
|
displayWidth,
|
|
327
|
+
terminalTextOf,
|
|
262
328
|
};
|
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. ` +
|