agentlas 1.0.65 → 1.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +6 -4
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +308 -113
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +42 -12
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +8 -2
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +29 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +230 -61
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +15 -5
- package/engine/mcp/plan.cjs +5 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +212 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const path = require("node:path");
|
|
11
11
|
const fs = require("node:fs");
|
|
12
|
+
const crypto = require("node:crypto");
|
|
12
13
|
const { spawnSync } = require("node:child_process");
|
|
13
14
|
|
|
14
15
|
const PERM_RANK = { read: 0, write: 1, full: 2 };
|
|
16
|
+
const MAX_TEXT_FILE_BYTES = 8 * 1024 * 1024;
|
|
15
17
|
|
|
16
18
|
function pathDenied(reason) {
|
|
17
19
|
throw new Error(`workspace path denied: ${reason}`);
|
|
@@ -126,9 +128,12 @@ function openRegularFile(file, flags, mode) {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
function readUtf8File(file) {
|
|
129
|
-
if (!fs.statSync(file).isFile()) pathDenied("only regular files may be read");
|
|
130
131
|
const fd = openRegularFile(file, fs.constants.O_RDONLY);
|
|
131
132
|
try {
|
|
133
|
+
const stat = fs.fstatSync(fd);
|
|
134
|
+
if (stat.size > MAX_TEXT_FILE_BYTES) {
|
|
135
|
+
throw new Error(`text file exceeds ${MAX_TEXT_FILE_BYTES} byte limit`);
|
|
136
|
+
}
|
|
132
137
|
return fs.readFileSync(fd, "utf8");
|
|
133
138
|
} finally {
|
|
134
139
|
fs.closeSync(fd);
|
|
@@ -136,6 +141,11 @@ function readUtf8File(file) {
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
function writeUtf8File(file, content) {
|
|
144
|
+
if (typeof content !== "string") throw new Error("content must be a string");
|
|
145
|
+
const contentBytes = Buffer.byteLength(content, "utf8");
|
|
146
|
+
if (contentBytes > MAX_TEXT_FILE_BYTES) {
|
|
147
|
+
throw new Error(`text content exceeds ${MAX_TEXT_FILE_BYTES} byte limit`);
|
|
148
|
+
}
|
|
139
149
|
// Replace through a fresh inode instead of truncating an existing one. If the
|
|
140
150
|
// workspace entry is a hard link to a file elsewhere, this updates only the
|
|
141
151
|
// workspace path and cannot mutate the other link's inode.
|
|
@@ -218,6 +228,12 @@ const TOOLS = [
|
|
|
218
228
|
},
|
|
219
229
|
run(args, ctx) {
|
|
220
230
|
const file = resolveExistingIn(ctx.cwd, args.path);
|
|
231
|
+
if (args.offset != null && (!Number.isInteger(args.offset) || args.offset < 1)) {
|
|
232
|
+
throw new Error("offset must be a positive integer");
|
|
233
|
+
}
|
|
234
|
+
if (args.limit != null && (!Number.isInteger(args.limit) || args.limit < 1)) {
|
|
235
|
+
throw new Error("limit must be a positive integer");
|
|
236
|
+
}
|
|
221
237
|
let content = readUtf8File(file);
|
|
222
238
|
if (args.offset || args.limit) {
|
|
223
239
|
const lines = content.split("\n");
|
|
@@ -344,16 +360,17 @@ function toolAskFor(tool, args) {
|
|
|
344
360
|
function runTool(name, args, ctx) {
|
|
345
361
|
const tool = BY_NAME[name];
|
|
346
362
|
if (!tool) return { ok: false, content: `unknown tool: ${name}` };
|
|
363
|
+
const context = ctx && typeof ctx === "object" ? ctx : {};
|
|
347
364
|
const ask = toolAskFor(tool, args);
|
|
348
365
|
let ruled = null;
|
|
349
|
-
if (
|
|
366
|
+
if (context.db) {
|
|
350
367
|
try {
|
|
351
368
|
const permissions = require("./agentlas-permissions.cjs");
|
|
352
|
-
const verdict = permissions.decideCapability(
|
|
369
|
+
const verdict = permissions.decideCapability(context.db, {
|
|
353
370
|
...ask,
|
|
354
|
-
permission:
|
|
355
|
-
agentId:
|
|
356
|
-
chatId:
|
|
371
|
+
permission: context.permission,
|
|
372
|
+
agentId: context.agentId,
|
|
373
|
+
chatId: context.chatId,
|
|
357
374
|
});
|
|
358
375
|
ruled = verdict.ruled;
|
|
359
376
|
if (ruled === "deny") {
|
|
@@ -369,15 +386,15 @@ function runTool(name, args, ctx) {
|
|
|
369
386
|
ruled = null;
|
|
370
387
|
}
|
|
371
388
|
}
|
|
372
|
-
const rank = PERM_RANK[
|
|
389
|
+
const rank = PERM_RANK[context.permission] ?? 0;
|
|
373
390
|
if (ruled !== "allow" && (PERM_RANK[tool.minPerm] ?? 0) > rank) {
|
|
374
391
|
return {
|
|
375
392
|
ok: false,
|
|
376
|
-
content: `permission denied: '${name}' requires '${tool.minPerm}' but current is '${
|
|
393
|
+
content: `permission denied: '${name}' requires '${tool.minPerm}' but current is '${context.permission || "read"}'. Ask the user to run /permission ${tool.minPerm}.`,
|
|
377
394
|
};
|
|
378
395
|
}
|
|
379
396
|
try {
|
|
380
|
-
return { ok: true, content: String(tool.run(args || {},
|
|
397
|
+
return { ok: true, content: String(tool.run(args || {}, context)) };
|
|
381
398
|
} catch (e) {
|
|
382
399
|
return { ok: false, content: `${name} error: ${e && e.message ? e.message : String(e)}` };
|
|
383
400
|
}
|
|
@@ -398,4 +415,14 @@ function openaiTools(permission) {
|
|
|
398
415
|
}));
|
|
399
416
|
}
|
|
400
417
|
|
|
401
|
-
module.exports = {
|
|
418
|
+
module.exports = {
|
|
419
|
+
TOOLS,
|
|
420
|
+
BY_NAME,
|
|
421
|
+
allowedTools,
|
|
422
|
+
runTool,
|
|
423
|
+
anthropicTools,
|
|
424
|
+
openaiTools,
|
|
425
|
+
PERM_RANK,
|
|
426
|
+
MAX_TEXT_FILE_BYTES,
|
|
427
|
+
toolAskFor,
|
|
428
|
+
};
|
package/engine/agentlas-ui.cjs
CHANGED
|
@@ -116,10 +116,32 @@ function compactHomePath(value) {
|
|
|
116
116
|
return home && value.startsWith(home + "/") ? "~/" + value.slice(home.length + 1) : value;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function maskSecretValue(value) {
|
|
120
|
+
const text = String(value || "");
|
|
121
|
+
const quote = text[0];
|
|
122
|
+
return (quote === "\"" || quote === "'") && text[text.length - 1] === quote
|
|
123
|
+
? quote + "••••" + quote
|
|
124
|
+
: "••••";
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function redactCommandSecrets(value) {
|
|
120
128
|
return value
|
|
121
|
-
.
|
|
122
|
-
.replace(/\b(
|
|
129
|
+
// URL userinfo is a credential even when it is not an assignment.
|
|
130
|
+
.replace(/\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1[redacted]@")
|
|
131
|
+
// Authorization schemes and common credential-bearing headers. Keep the
|
|
132
|
+
// header name/scheme visible so the action remains understandable.
|
|
133
|
+
.replace(/\b((?:proxy-)?authorization\s*[:=]\s*)((?:basic|bearer)\s+)?("[^"]*"|'[^']*'|[^\s"']+)/gi, (_m, prefix, scheme, secret) => prefix + (scheme || "") + maskSecretValue(secret))
|
|
134
|
+
.replace(/\b((?:x[-_]?api[-_]?key|api[-_]?key|x[-_]?(?:auth(?:entication)?|access|refresh)[-_]?token|x[-_]?(?:client[-_]?secret)|cookie|set[-_]?cookie)\s*:\s*)("[^"]*"|'[^']*'|[^\s"']+)/gi, (_m, prefix, secret) => prefix + maskSecretValue(secret))
|
|
135
|
+
// Environment assignments are case-insensitive in shells. Keep the
|
|
136
|
+
// suffix allowlist narrow so ordinary arguments remain readable, while
|
|
137
|
+
// also covering bare names such as token= and password=.
|
|
138
|
+
.replace(/(^|[^A-Za-z0-9_])((?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|[A-Za-z0-9]+(?:[_-][A-Za-z0-9]+)*[_-](?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)))(\s*=\s*)("[^"]*"|'[^']*'|[^\s"']+)/gi, (_m, lead, name, equals, secret) => lead + name + equals + maskSecretValue(secret))
|
|
139
|
+
// Header and CLI bearer forms are common in curl/npm-style commands.
|
|
140
|
+
.replace(/(\b((?:proxy-)?authorization\s*(?::|=|\s)\s*bearer)\s+)("[^"]*"|'[^']*'|[^\s"']+)/gi, (_m, prefix, _header, secret) => prefix + maskSecretValue(secret))
|
|
141
|
+
.replace(/(--(?:api[-_]?key|auth(?:entication)?[-_]?token|access[-_]?token|refresh[-_]?token|client[-_]?secret|token|secret|password|passwd)(?:\s*=\s*|\s+))("[^"]*"|'[^']*'|[^\s"']+)/gi, (_m, prefix, secret) => prefix + maskSecretValue(secret))
|
|
142
|
+
// A standalone bearer value is only masked when it has token-like length;
|
|
143
|
+
// this avoids turning ordinary prose such as "bearer token" into noise.
|
|
144
|
+
.replace(/(\bbearer)\s+([A-Za-z0-9._~+/=-]{16,})\b/gi, "$1 ••••")
|
|
123
145
|
// Some remote MCPs put a bearer-like credential in the URL path instead of a header.
|
|
124
146
|
// Keep the provider/path useful while ensuring activity summaries never print the secret.
|
|
125
147
|
.replace(/(https?:\/\/[^\s]+\/)(ocm_[A-Za-z0-9_-]{12,})\b/gi, "$1[redacted]")
|
|
@@ -145,7 +167,7 @@ function compactToolArg(name, value, max = 120) {
|
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
function compactResult(text, ok, toolName, maxCells = 120) {
|
|
148
|
-
const clean = stripAnsi(String(text || "")).replace(/\r/g, "").trim();
|
|
170
|
+
const clean = redactCommandSecrets(stripAnsi(String(text || ""))).replace(/\r/g, "").trim();
|
|
149
171
|
const lines = clean.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
150
172
|
const count = lines.length;
|
|
151
173
|
if (!ok) {
|
|
@@ -546,7 +568,7 @@ class Ui {
|
|
|
546
568
|
toolResult(text, ok = true, options = {}) {
|
|
547
569
|
if (!ok) this._turnFailures += 1;
|
|
548
570
|
if (options.verbose) {
|
|
549
|
-
const body = truncate(stripAnsi(String(text || "")).trim(), options.maxChars || 4_000);
|
|
571
|
+
const body = truncate(redactCommandSecrets(stripAnsi(String(text || "")).trim()), options.maxChars || 4_000);
|
|
550
572
|
const lines = body ? body.split("\n") : [ok ? "done" : "error"];
|
|
551
573
|
const marker = ok ? this.c.green(" └ ") : this.c.paw(" └ ");
|
|
552
574
|
for (let index = 0; index < lines.length; index++) {
|
|
@@ -4649,18 +4649,42 @@ const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reaso
|
|
|
4649
4649
|
function parseArgs(args) {
|
|
4650
4650
|
const task = [];
|
|
4651
4651
|
const options = {};
|
|
4652
|
+
const seen = new Set();
|
|
4653
|
+
let passthrough = false;
|
|
4652
4654
|
for (let index = 0; index < args.length; index += 1) {
|
|
4653
4655
|
const token = String(args[index]);
|
|
4654
|
-
if (
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4656
|
+
if (passthrough) { task.push(token); continue; }
|
|
4657
|
+
if (token === "--") { passthrough = true; continue; }
|
|
4658
|
+
if (token === "--benchmark" || token === "--json") {
|
|
4659
|
+
const key = token.slice(2);
|
|
4660
|
+
if (seen.has(key)) throw new Error(`duplicate option: ${token}`);
|
|
4661
|
+
seen.add(key);
|
|
4662
|
+
options[key] = true;
|
|
4663
|
+
} else if (token === "--parallel" || token === "-n" || token.startsWith("--parallel=")) {
|
|
4664
|
+
if (seen.has("parallel")) throw new Error("duplicate option: --parallel");
|
|
4665
|
+
seen.add("parallel");
|
|
4666
|
+
const inline = token.startsWith("--parallel=");
|
|
4667
|
+
const raw = inline ? token.slice(11) : args[++index];
|
|
4668
|
+
const value = Number(raw);
|
|
4669
|
+
if (raw === undefined || raw === "" || (!inline && String(raw).startsWith("--")) || !Number.isInteger(value) || value < 1 || value > 8) {
|
|
4670
|
+
throw new Error("--parallel requires a positive integer (maximum 8)");
|
|
4671
|
+
}
|
|
4672
|
+
options.concurrency = value;
|
|
4673
|
+
} else if (token.startsWith("-")) {
|
|
4674
|
+
throw new Error(`unknown option: ${token} (use -- before a task token that starts with '-')`);
|
|
4675
|
+
} else task.push(token);
|
|
4658
4676
|
}
|
|
4659
4677
|
return { task: task.join(" ").trim(), options };
|
|
4660
4678
|
}
|
|
4661
4679
|
|
|
4662
4680
|
async function cmdWorkforce(db, args, runtimeOverride, executionContext = {}) {
|
|
4663
|
-
|
|
4681
|
+
let parsed;
|
|
4682
|
+
try { parsed = parseArgs(args); }
|
|
4683
|
+
catch (error) {
|
|
4684
|
+
const ui = executionContext.ui || newUi();
|
|
4685
|
+
ui.error(String((error && error.message) || error), { reveal: true });
|
|
4686
|
+
return { ok: false, error: "invalid-arguments" };
|
|
4687
|
+
}
|
|
4664
4688
|
if (!parsed.task) {
|
|
4665
4689
|
const ui = executionContext.ui || newUi();
|
|
4666
4690
|
ui.warn("usage: agentlas workforce <task> [--parallel N] [--benchmark] [--json]");
|
|
@@ -28,6 +28,52 @@ const EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhig
|
|
|
28
28
|
const EFFORT_TOKEN_RE = /^[a-z][a-z0-9-]{0,23}$/;
|
|
29
29
|
const CAPABILITIES = new Set(["code", "image", "tools", "long-context"]);
|
|
30
30
|
const PHASES = new Set(["plan", "delegate", "synthesize"]);
|
|
31
|
+
const MAX_CODEX_MODEL_CACHE_BYTES = 2 * 1024 * 1024;
|
|
32
|
+
const MAX_DECISION_RECEIPT_BYTES = 16 * 1024 * 1024;
|
|
33
|
+
const MAX_DECISION_RECEIPT_RECORD_BYTES = 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
function sameFileIdentity(left, right) {
|
|
36
|
+
return Boolean(left && right && left.dev === right.dev && left.ino === right.ino);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeRegularFile(stat, maxBytes, { nonEmpty = false } = {}) {
|
|
40
|
+
return Boolean(
|
|
41
|
+
stat && stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 &&
|
|
42
|
+
Number.isSafeInteger(stat.size) && stat.size >= (nonEmpty ? 1 : 0) && stat.size <= maxBytes,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBoundedRegularFile(file, maxBytes) {
|
|
47
|
+
const before = fs.lstatSync(file);
|
|
48
|
+
if (!safeRegularFile(before, maxBytes, { nonEmpty: true })) throw new Error("model inventory must be a bounded single-link regular file");
|
|
49
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
50
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
51
|
+
try {
|
|
52
|
+
const opened = fs.fstatSync(fd);
|
|
53
|
+
if (!safeRegularFile(opened, maxBytes, { nonEmpty: true }) || !sameFileIdentity(before, opened)) {
|
|
54
|
+
throw new Error("model inventory changed while opening");
|
|
55
|
+
}
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let total = 0;
|
|
58
|
+
while (total <= maxBytes) {
|
|
59
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
60
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
61
|
+
if (!count) break;
|
|
62
|
+
chunks.push(buffer.subarray(0, count));
|
|
63
|
+
total += count;
|
|
64
|
+
}
|
|
65
|
+
if (total > maxBytes) throw new Error("model inventory exceeds its safety limit");
|
|
66
|
+
const after = fs.fstatSync(fd);
|
|
67
|
+
if (
|
|
68
|
+
!safeRegularFile(after, maxBytes, { nonEmpty: true }) || !sameFileIdentity(opened, after) ||
|
|
69
|
+
after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
|
|
70
|
+
total !== after.size
|
|
71
|
+
) throw new Error("model inventory changed while reading");
|
|
72
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
|
|
73
|
+
} finally {
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
31
77
|
|
|
32
78
|
function cleanText(value, max = 240) {
|
|
33
79
|
return String(value || "").replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
|
|
@@ -161,11 +207,12 @@ function defaultAvailableModels(runtime) {
|
|
|
161
207
|
}
|
|
162
208
|
|
|
163
209
|
function readCodexModelInventory(codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex")) {
|
|
164
|
-
const file = path.join(codexHome, "models_cache.json");
|
|
165
210
|
try {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
211
|
+
const realHome = fs.realpathSync(codexHome);
|
|
212
|
+
const homeStat = fs.lstatSync(realHome);
|
|
213
|
+
if (!homeStat.isDirectory() || homeStat.isSymbolicLink()) return [];
|
|
214
|
+
const file = path.join(realHome, "models_cache.json");
|
|
215
|
+
const parsed = JSON.parse(readBoundedRegularFile(file, MAX_CODEX_MODEL_CACHE_BYTES));
|
|
169
216
|
if (!Array.isArray(parsed.models)) return [];
|
|
170
217
|
const out = [];
|
|
171
218
|
const seen = new Set();
|
|
@@ -517,22 +564,48 @@ function appendDecisionReceipt(receipt, file = defaultReceiptPath()) {
|
|
|
517
564
|
const directory = path.dirname(file);
|
|
518
565
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
519
566
|
try { fs.chmodSync(directory, 0o700); } catch { /* Windows/best effort */ }
|
|
567
|
+
const realDirectory = fs.realpathSync(directory);
|
|
568
|
+
const directoryStat = fs.lstatSync(realDirectory);
|
|
569
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
570
|
+
throw new Error("model routing receipt directory must be a real directory");
|
|
571
|
+
}
|
|
572
|
+
const target = path.join(realDirectory, path.basename(file));
|
|
573
|
+
const line = Buffer.from(JSON.stringify(receipt) + "\n", "utf8");
|
|
574
|
+
if (line.length <= 1 || line.length > MAX_DECISION_RECEIPT_RECORD_BYTES) {
|
|
575
|
+
throw new Error("model routing receipt record exceeds its safety limit");
|
|
576
|
+
}
|
|
577
|
+
let before = null;
|
|
520
578
|
try {
|
|
521
|
-
|
|
522
|
-
if (
|
|
579
|
+
before = fs.lstatSync(target);
|
|
580
|
+
if (before.isSymbolicLink()) {
|
|
581
|
+
throw new Error("model routing receipt path must not be a symlink");
|
|
582
|
+
}
|
|
583
|
+
if (!safeRegularFile(before, MAX_DECISION_RECEIPT_BYTES - line.length)) {
|
|
584
|
+
throw new Error("model routing receipt path must be a bounded single-link regular file");
|
|
585
|
+
}
|
|
523
586
|
} catch (error) {
|
|
524
587
|
if (error && error.code !== "ENOENT") throw error;
|
|
525
588
|
}
|
|
526
589
|
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW || 0);
|
|
527
|
-
const fd = fs.openSync(
|
|
590
|
+
const fd = fs.openSync(target, flags, 0o600);
|
|
528
591
|
try {
|
|
529
|
-
fs.
|
|
592
|
+
const opened = fs.fstatSync(fd);
|
|
593
|
+
if (
|
|
594
|
+
!safeRegularFile(opened, MAX_DECISION_RECEIPT_BYTES - line.length) ||
|
|
595
|
+
(before && !sameFileIdentity(before, opened))
|
|
596
|
+
) throw new Error("model routing receipt changed while opening");
|
|
597
|
+
fs.fchmodSync(fd, 0o600);
|
|
598
|
+
const written = fs.writeSync(fd, line, 0, line.length, null);
|
|
599
|
+
if (written !== line.length) throw new Error("model routing receipt write was incomplete");
|
|
530
600
|
fs.fsyncSync(fd);
|
|
601
|
+
const after = fs.fstatSync(fd);
|
|
602
|
+
if (!safeRegularFile(after, MAX_DECISION_RECEIPT_BYTES) || !sameFileIdentity(opened, after)) {
|
|
603
|
+
throw new Error("model routing receipt changed while writing");
|
|
604
|
+
}
|
|
531
605
|
} finally {
|
|
532
606
|
fs.closeSync(fd);
|
|
533
607
|
}
|
|
534
|
-
|
|
535
|
-
return file;
|
|
608
|
+
return target;
|
|
536
609
|
}
|
|
537
610
|
|
|
538
611
|
function plannerSystemPrompt({ language = "English", maxTasks = 12, mode = "swarm", liveRuntimeInventory = [] } = {}) {
|
package/engine/agentlas.cjs
CHANGED
|
@@ -171,7 +171,14 @@ function main() {
|
|
|
171
171
|
const code = commands.SELF_HELP_COMMANDS.has(command)
|
|
172
172
|
? commands.dispatch(ctx, [command, "help"])
|
|
173
173
|
: require("./commands/help.cjs").runForCommand(ctx, command);
|
|
174
|
-
|
|
174
|
+
// Self-owned help may be an async native passthrough (Research is the first
|
|
175
|
+
// such command). Exiting on the unresolved Promise kills its child before
|
|
176
|
+
// the help text reaches stdout.
|
|
177
|
+
Promise.resolve(code).then(
|
|
178
|
+
(n) => process.exit(typeof n === "number" ? n : 0),
|
|
179
|
+
(error) => { ctx.fail(error); process.exit(1); },
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
175
182
|
}
|
|
176
183
|
// 옵션 정규화: -h/--help/-V/--version 은 하위 명령으로 변환
|
|
177
184
|
const normalized = argv.map((a) => {
|
|
@@ -199,7 +206,7 @@ function main() {
|
|
|
199
206
|
const { startRepl } = require("./ui/repl.cjs");
|
|
200
207
|
return startRepl(ctx).then(
|
|
201
208
|
(replCode) => process.exit(replCode || 0),
|
|
202
|
-
(e) => { ctx.
|
|
209
|
+
(e) => { ctx.fail(e); process.exit(1); },
|
|
203
210
|
);
|
|
204
211
|
}
|
|
205
212
|
|
|
@@ -236,7 +243,7 @@ function main() {
|
|
|
236
243
|
|
|
237
244
|
Promise.resolve(code).then(
|
|
238
245
|
(n) => process.exit(typeof n === "number" ? n : 0),
|
|
239
|
-
(e) => { ctx.
|
|
246
|
+
(e) => { ctx.fail(e); process.exit(1); },
|
|
240
247
|
);
|
|
241
248
|
}
|
|
242
249
|
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
* `agentlas import <folder>`로 언제든 설치할 수 있다.
|
|
16
16
|
*/
|
|
17
17
|
const crypto = require("node:crypto");
|
|
18
|
+
const path = require("node:path");
|
|
18
19
|
const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
19
20
|
const { columnExists } = require("../core/db.cjs");
|
|
20
21
|
|
|
21
22
|
const BUILDER_SLUG = "agentlas-builder";
|
|
22
23
|
const BUILDER_ID = "builtin-agentlas-builder";
|
|
24
|
+
const MAX_BUILDER_OUTPUT_BYTES = 256 * 1024;
|
|
25
|
+
const MAX_BUILT_FOLDER_BYTES = 4 * 1024;
|
|
23
26
|
|
|
24
27
|
const BUILDER_SYSTEM_PROMPT = [
|
|
25
28
|
"You are the Agentlas local agent builder, running inside the Agentlas terminal.",
|
|
@@ -45,12 +48,18 @@ const BUILDER_SYSTEM_PROMPT = [
|
|
|
45
48
|
function ensureBuilderAgent(db) {
|
|
46
49
|
const now = new Date().toISOString();
|
|
47
50
|
const hasVisibility = columnExists(db, "installed_agents", "visibility");
|
|
48
|
-
runWriteTransaction(db, () => {
|
|
49
|
-
const existing = db.prepare("SELECT id FROM installed_agents WHERE id=? OR slug=?").
|
|
50
|
-
if (existing) {
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
51
|
+
const identity = runWriteTransaction(db, () => {
|
|
52
|
+
const existing = db.prepare("SELECT id, slug FROM installed_agents WHERE id=? OR slug=?").all(BUILDER_ID, BUILDER_SLUG);
|
|
53
|
+
if (existing.length) {
|
|
54
|
+
if (existing.length !== 1 || existing[0].id !== BUILDER_ID || existing[0].slug !== BUILDER_SLUG) {
|
|
55
|
+
throw Object.assign(new Error("The reserved Agentlas builder identity is already used by another installed agent."), { code: "builder_identity_conflict", honestStop: true });
|
|
56
|
+
}
|
|
57
|
+
const visibilitySet = hasVisibility ? ", visibility='visible'" : "";
|
|
58
|
+
db.prepare(
|
|
59
|
+
`UPDATE installed_agents SET system_prompt=?, name=?, name_en=?, tagline=?, tagline_en=?, ` +
|
|
60
|
+
`builtin=1, role='orchestrator', tone='blue', trust_grade='A'${visibilitySet} WHERE id=?`,
|
|
61
|
+
).run(BUILDER_SYSTEM_PROMPT, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_ID);
|
|
62
|
+
return { id: BUILDER_ID, slug: BUILDER_SLUG };
|
|
54
63
|
}
|
|
55
64
|
if (hasVisibility) {
|
|
56
65
|
db.prepare(
|
|
@@ -63,10 +72,18 @@ function ensureBuilderAgent(db) {
|
|
|
63
72
|
"VALUES (?,?,?,?,?,?,?,'[]','[]',NULL,'A',?,?,1,?)",
|
|
64
73
|
).run(BUILDER_ID, BUILDER_SLUG, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_SYSTEM_PROMPT, now, "blue", "orchestrator");
|
|
65
74
|
}
|
|
75
|
+
return { id: BUILDER_ID, slug: BUILDER_SLUG };
|
|
66
76
|
});
|
|
77
|
+
const stored = db.prepare("SELECT id, slug, builtin, role FROM installed_agents WHERE id=?").get(BUILDER_ID);
|
|
78
|
+
if (
|
|
79
|
+
!stored || stored.id !== identity.id || stored.slug !== identity.slug ||
|
|
80
|
+
Number(stored.builtin) !== 1 || stored.role !== "orchestrator"
|
|
81
|
+
) {
|
|
82
|
+
throw Object.assign(new Error("The reserved Agentlas builder identity could not be verified after seeding."), { code: "builder_identity_conflict", honestStop: true });
|
|
83
|
+
}
|
|
67
84
|
return {
|
|
68
|
-
id:
|
|
69
|
-
slug:
|
|
85
|
+
id: identity.id,
|
|
86
|
+
slug: identity.slug,
|
|
70
87
|
name: "Agent Builder",
|
|
71
88
|
nameEn: "Agent Builder",
|
|
72
89
|
systemPrompt: BUILDER_SYSTEM_PROMPT,
|
|
@@ -77,10 +94,18 @@ function ensureBuilderAgent(db) {
|
|
|
77
94
|
|
|
78
95
|
/** 빌더 산출물 마지막 줄에서 `BUILT: <folder>`를 뽑는다. */
|
|
79
96
|
function parseBuiltFolder(finalText) {
|
|
80
|
-
const
|
|
97
|
+
const text = String(finalText || "");
|
|
98
|
+
if (Buffer.byteLength(text, "utf8") > MAX_BUILDER_OUTPUT_BYTES) return null;
|
|
99
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
81
100
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
82
101
|
const m = lines[i].match(/^BUILT:\s*(.+)$/);
|
|
83
|
-
if (m)
|
|
102
|
+
if (!m) continue;
|
|
103
|
+
const folder = m[1].trim();
|
|
104
|
+
if (!folder || Buffer.byteLength(folder, "utf8") > MAX_BUILT_FOLDER_BYTES || /[\u0000-\u001f\u007f]/.test(folder)) return null;
|
|
105
|
+
if (path.isAbsolute(folder) || /^[A-Za-z]:[\\/]/.test(folder) || folder.includes("\\")) return null;
|
|
106
|
+
const segments = folder.split("/");
|
|
107
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) return null;
|
|
108
|
+
return folder;
|
|
84
109
|
}
|
|
85
110
|
return null;
|
|
86
111
|
}
|
package/engine/agents/files.cjs
CHANGED
|
@@ -6,33 +6,68 @@
|
|
|
6
6
|
* 관찰상 읽기 전용으로 유지돼야 소스 패키지를 재분류하지 않는다.
|
|
7
7
|
*/
|
|
8
8
|
const fs = require("node:fs");
|
|
9
|
+
const os = require("node:os");
|
|
9
10
|
const path = require("node:path");
|
|
10
11
|
const { userDataDir } = require("../core/paths.cjs");
|
|
11
12
|
const { routesMap } = require("./routes.cjs");
|
|
12
13
|
|
|
13
14
|
const CLOUD_RESTORE_MARKER_PATH = ".agentlas-cloud-package.json";
|
|
15
|
+
const NATIVE_CONTEXT_MAX_CHARS = 256 * 1024;
|
|
14
16
|
|
|
15
17
|
function cloudSlug(value) {
|
|
16
18
|
return (String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "agentlas-cloud-agent");
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
|
|
20
|
-
|
|
21
21
|
function agentSystemPrompt(agent) {
|
|
22
|
-
|
|
22
|
+
const value = agent && agent.systemPrompt ? agent.systemPrompt : `You are ${(agent && agent.name) || "an Agentlas agent"}.`;
|
|
23
|
+
const text = String(value);
|
|
24
|
+
if (text.length > NATIVE_CONTEXT_MAX_CHARS) throw new Error("agent system prompt exceeds the native context file limit");
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeExternalAgentPath(value) {
|
|
29
|
+
if (typeof value !== "string" || !path.isAbsolute(value) || /[\u0000\r\n]/.test(value)) return null;
|
|
30
|
+
const resolved = path.resolve(value);
|
|
31
|
+
const unsafe = new Set([path.parse(resolved).root, path.resolve(os.homedir()), path.resolve(userDataDir())]);
|
|
32
|
+
if (unsafe.has(resolved)) return null;
|
|
33
|
+
try {
|
|
34
|
+
const stat = fs.lstatSync(resolved);
|
|
35
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return null;
|
|
36
|
+
const canonical = fs.realpathSync(resolved);
|
|
37
|
+
if (unsafe.has(canonical)) return null;
|
|
38
|
+
return canonical;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Preserve a safe missing local route for read-only `agentlas cd` output.
|
|
41
|
+
// The write boundary below refuses to recreate it.
|
|
42
|
+
return error && error.code === "ENOENT" ? resolved : null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRegularNoFollow(file) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs.lstatSync(file);
|
|
49
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
function agentFolder(agent) {
|
|
26
56
|
const routes = routesMap();
|
|
27
57
|
const r = routes[agent.id];
|
|
28
|
-
|
|
58
|
+
const localPath = safeExternalAgentPath(r && r.path);
|
|
59
|
+
if (localPath) return localPath; // 로컬 임포트는 원본 폴더
|
|
29
60
|
const cloudRoot = path.join(userDataDir(), "cloud-agent-installs", cloudSlug(agent.slug));
|
|
30
|
-
if (
|
|
31
|
-
return path.join(userDataDir(), "agents", agent.slug);
|
|
61
|
+
if (safeExternalAgentPath(cloudRoot) && isRegularNoFollow(path.join(cloudRoot, CLOUD_RESTORE_MARKER_PATH))) return cloudRoot;
|
|
62
|
+
return path.join(userDataDir(), "agents", cloudSlug(agent.slug));
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
function writeIfMissing(file, content) {
|
|
35
|
-
try {
|
|
66
|
+
try {
|
|
67
|
+
const stat = fs.lstatSync(file);
|
|
68
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`native context target must be a regular non-symbolic-link file: ${file}`);
|
|
69
|
+
return false;
|
|
70
|
+
} catch (error) {
|
|
36
71
|
if (!error || error.code !== "ENOENT") throw error;
|
|
37
72
|
}
|
|
38
73
|
const fd = fs.openSync(file, "wx", 0o600);
|
|
@@ -45,12 +80,40 @@ function writeIfMissing(file, content) {
|
|
|
45
80
|
return true;
|
|
46
81
|
}
|
|
47
82
|
|
|
83
|
+
function assertNativeFolder(folder) {
|
|
84
|
+
if (typeof folder !== "string" || !path.isAbsolute(folder) || /[\u0000\r\n]/.test(folder)) {
|
|
85
|
+
throw new Error("native context folder must be an absolute safe path");
|
|
86
|
+
}
|
|
87
|
+
const resolved = path.resolve(folder);
|
|
88
|
+
const data = path.resolve(userDataDir());
|
|
89
|
+
const internalRoots = [path.join(data, "agents"), path.join(data, "cloud-agent-installs")];
|
|
90
|
+
const internal = internalRoots.some((root) => resolved.startsWith(root + path.sep));
|
|
91
|
+
const unsafe = new Set([path.parse(resolved).root, path.resolve(os.homedir()), data]);
|
|
92
|
+
if (unsafe.has(resolved)) throw new Error("refusing to write native context files to a broad user/system directory");
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.lstatSync(resolved);
|
|
95
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("native context folder must be a real directory");
|
|
96
|
+
const canonical = fs.realpathSync(resolved);
|
|
97
|
+
if (unsafe.has(canonical)) throw new Error("refusing to write native context files to a broad user/system directory");
|
|
98
|
+
return canonical;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
101
|
+
if (!internal) throw new Error("refusing to recreate a missing imported agent source folder");
|
|
102
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
103
|
+
const created = fs.lstatSync(resolved);
|
|
104
|
+
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("native context folder changed type during creation");
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
48
109
|
function ensureNativeFiles(agent, folder) {
|
|
49
|
-
|
|
110
|
+
folder = assertNativeFolder(folder);
|
|
50
111
|
const sys = agentSystemPrompt(agent);
|
|
51
112
|
const created = [];
|
|
52
113
|
if (writeIfMissing(path.join(folder, "system-prompt.md"), sys)) created.push("system-prompt.md");
|
|
53
|
-
const
|
|
114
|
+
const name = String(agent.name || agent.slug || "Agentlas agent").replace(/[\u0000\r\n]/g, " ").slice(0, 500);
|
|
115
|
+
const tagline = String(agent.tagline || "").replace(/[\u0000\r]/g, " ").slice(0, 2_000);
|
|
116
|
+
const header = `# ${name}\n\n${tagline}\n\n${sys}\n`;
|
|
54
117
|
// 네이티브 CLI가 프로젝트 지시로 자동 인식하는 파일들
|
|
55
118
|
if (writeIfMissing(path.join(folder, "CLAUDE.md"), header)) created.push("CLAUDE.md");
|
|
56
119
|
if (writeIfMissing(path.join(folder, "AGENTS.md"), header)) created.push("AGENTS.md");
|
|
@@ -58,4 +121,12 @@ function ensureNativeFiles(agent, folder) {
|
|
|
58
121
|
return created;
|
|
59
122
|
}
|
|
60
123
|
|
|
61
|
-
module.exports = {
|
|
124
|
+
module.exports = {
|
|
125
|
+
agentFolder,
|
|
126
|
+
ensureNativeFiles,
|
|
127
|
+
agentSystemPrompt,
|
|
128
|
+
cloudSlug,
|
|
129
|
+
safeExternalAgentPath,
|
|
130
|
+
assertNativeFolder,
|
|
131
|
+
CLOUD_RESTORE_MARKER_PATH,
|
|
132
|
+
};
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* v1 모놀리스 890–1112 구간의 충실 이식 — 규칙 변경 금지(데스크탑과 판정 동형이어야 함).
|
|
6
6
|
*/
|
|
7
7
|
const fs = require("node:fs");
|
|
8
|
+
const os = require("node:os");
|
|
8
9
|
const path = require("node:path");
|
|
9
10
|
const crypto = require("node:crypto");
|
|
10
11
|
const { tableExists, columnExists } = require("../core/db.cjs");
|
|
11
12
|
const { routesMap, saveRoutes } = require("./routes.cjs");
|
|
13
|
+
const { userDataDir } = require("../core/paths.cjs");
|
|
12
14
|
|
|
13
15
|
function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
|
|
14
16
|
function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
|
@@ -159,8 +161,14 @@ function upsertLocalTeamFirm(db, dir, ceoAgentId, agentSlug, name, tagline) {
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
function importLocalFolder(db, absPath) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
+
const requested = path.resolve(absPath);
|
|
165
|
+
const unsafe = new Set([path.parse(requested).root, path.resolve(os.homedir()), path.resolve(userDataDir())]);
|
|
166
|
+
if (unsafe.has(requested)) throw new Error(`Refusing to import a broad user/system directory: ${absPath}`);
|
|
167
|
+
let stat;
|
|
168
|
+
try { stat = fs.lstatSync(requested); } catch { throw new Error(`Not a directory: ${absPath}`); }
|
|
169
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Imported agent path must be a real non-symbolic-link directory: ${absPath}`);
|
|
170
|
+
const dir = fs.realpathSync(requested);
|
|
171
|
+
if (unsafe.has(dir)) throw new Error(`Refusing to import a broad user/system directory: ${absPath}`);
|
|
164
172
|
const labels = detectRuntimeLabels(dir);
|
|
165
173
|
const runtime = labels[0];
|
|
166
174
|
const kind = detectKind(dir);
|
|
@@ -122,9 +122,16 @@ function findAgent(db, token) {
|
|
|
122
122
|
const q = String(token || "").trim().toLowerCase();
|
|
123
123
|
if (!q) return null;
|
|
124
124
|
const agents = listRoutableAgents(db);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const exactId = agents.find((agent) => agent.id === token);
|
|
126
|
+
if (exactId) return exactId; // primary key is authoritative
|
|
127
|
+
const exactSlug = agents.filter((agent) => agent.slug.toLowerCase() === q);
|
|
128
|
+
if (exactSlug.length === 1) return exactSlug[0];
|
|
129
|
+
if (exactSlug.length > 1) return null;
|
|
130
|
+
const exactName = agents.filter((agent) =>
|
|
131
|
+
(agent.name || "").toLowerCase() === q || (agent.nameEn || "").toLowerCase() === q,
|
|
132
|
+
);
|
|
133
|
+
if (exactName.length === 1) return exactName[0];
|
|
134
|
+
if (exactName.length > 1) return null;
|
|
128
135
|
const partial = agents.filter((a) =>
|
|
129
136
|
a.slug.toLowerCase().includes(q) || (a.name || "").toLowerCase().includes(q) || (a.nameEn || "").toLowerCase().includes(q));
|
|
130
137
|
if (partial.length === 1) return partial[0];
|