@securecoms/agent-runtime 0.9.7 → 0.9.8
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/README.md +33 -0
- package/dist/bin.js +282 -116
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,7 @@ All configuration is via `SECURECOMS_*` environment variables (see
|
|
|
163
163
|
| `SECURECOMS_TOOL_DEFAULT` | `deny` | Fallback policy for a tool matched by neither list |
|
|
164
164
|
| `SECURECOMS_LLM_MAX_OUTPUT_TOKENS` | `8192` | Output-token cap sent on every model request (`max_tokens`; `max_completion_tokens` for `api.openai.com`). Whole number ≥ 1 |
|
|
165
165
|
| `SECURECOMS_LLM_DAILY_CALL_LIMIT` | `2000` | Model requests per UTC day. When spent, each channel gets one "daily usage limit" reply and the provider is not called again until 00:00 UTC. `0` = unlimited |
|
|
166
|
+
| `SECURECOMS_SECRET_PATH_MODE` | `report` (or `--secret-path-mode <mode>`) | What happens to a secret path in the MANAGED `skills.json` that is outside this agent's secrets directory: `report` reads it as before and reports a count; `enforce` refuses it. Local only: nothing the server sends can set it. See *Secret paths in managed config* below |
|
|
166
167
|
| `SECURECOMS_LLM_DAILY_TOKEN_LIMIT` | `5000000` | Tokens per UTC day, same behaviour as the call limit. `0` = unlimited. Each request first reserves its worst case (its size in bytes plus the output-token cap), is refused if that does not fit, and the reserve is saved to disk before the request is sent (if it cannot be saved, the request is refused). The reserve is refunded down to the provider-reported usage, kept whole when the provider may have billed without reporting (timeout, oversize or unreadable answer, no usage in the answer), and refunded whole only when the request never left (DNS failure, connection refused). A non-zero limit smaller than the largest possible request (128 KiB plus the output cap) is refused at startup. Counters persist in `<dataDir>/llm-budget-<identity>.json` (counts only, no content). See *Daily budget: limits of the guarantee* below |
|
|
167
168
|
|
|
168
169
|
### The LLM source: explicit vs managed
|
|
@@ -325,6 +326,38 @@ with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A managed copy
|
|
|
325
326
|
(see below) wins over both. A missing file means no endpoints, no databases
|
|
326
327
|
and no flags — not an error.
|
|
327
328
|
|
|
329
|
+
### Secret paths in managed config
|
|
330
|
+
|
|
331
|
+
The managed `skills.json` is written from what the SecureComs server sends, so
|
|
332
|
+
the server chooses which file each secret path names. Three fields are secret
|
|
333
|
+
paths: an endpoint's `headersFile`, a database's `postgresUrlFile`, and the
|
|
334
|
+
search `keyFile`. Each must resolve, after following symlinks, strictly inside
|
|
335
|
+
|
|
336
|
+
~/.securecoms/runtime-config/<identity>/secrets/
|
|
337
|
+
|
|
338
|
+
or be listed exactly in `allowed_secret_paths` in
|
|
339
|
+
`~/.securecoms/runtime-config/<identity>/local-config.json`, a file only this
|
|
340
|
+
machine writes:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{ "allowed_secret_paths": ["/home/me/.config/brave.key"] }
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A relative path, a path with a `..` segment, and a symlink that leaves the
|
|
347
|
+
directory are all outside. In the default `report` mode an outside path is
|
|
348
|
+
still read as before, and the agent reports only how many there are (never the
|
|
349
|
+
path or the content). In `enforce` mode it is refused: `http_request` and
|
|
350
|
+
`sql_query` return an error that says so, and `web_search` is not offered
|
|
351
|
+
unless an install-time key file (`--brave-key-file` / `--tavily-key-file`)
|
|
352
|
+
applies. The agent keeps running either way. A hand-written local
|
|
353
|
+
`skills.json` is yours and is never confined.
|
|
354
|
+
|
|
355
|
+
A database's `sqlite` path in the managed `skills.json` is a data file rather
|
|
356
|
+
than a secret, but the server chooses it too. In this release it is counted in
|
|
357
|
+
report mode (inside means under `~/.securecoms/runtime-config/<identity>/data/`
|
|
358
|
+
or `.../secrets/`, or allowlisted) and is still read in either mode; it will be
|
|
359
|
+
confined in enforce mode in a later release.
|
|
360
|
+
|
|
328
361
|
## Persona and memory
|
|
329
362
|
|
|
330
363
|
Three optional files make up an agent's persona, looked up along the config
|
package/dist/bin.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
5
5
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6
|
-
import { dirname as dirname7, join as
|
|
6
|
+
import { dirname as dirname7, join as join9 } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, realpathSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8
|
+
import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, realpathSync as realpathSync2, rmSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9
9
|
import { homedir as homedir4, userInfo } from "node:os";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { dirname as dirname3, join } from "node:path";
|
|
13
|
+
import { dirname as dirname3, join as join2 } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
// src/service-log.ts
|
|
@@ -1046,6 +1046,104 @@ function withDailyBudget(llm, budget) {
|
|
|
1046
1046
|
};
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
+
// src/secret-path.ts
|
|
1050
|
+
import { readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
1051
|
+
import { isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
1052
|
+
var DEFAULT_SECRET_PATH_MODE = "report";
|
|
1053
|
+
var SECRET_PATH_MODE_ENV = "SECURECOMS_SECRET_PATH_MODE";
|
|
1054
|
+
var SECRETS_SUBDIR = "secrets";
|
|
1055
|
+
var DATA_SUBDIR = "data";
|
|
1056
|
+
var LOCAL_CONFIG_FILE = "local-config.json";
|
|
1057
|
+
var ALLOWLIST_KEY = "allowed_secret_paths";
|
|
1058
|
+
function emptySecretPathSummary(mode) {
|
|
1059
|
+
return {
|
|
1060
|
+
mode,
|
|
1061
|
+
inside: 0,
|
|
1062
|
+
allowlisted: 0,
|
|
1063
|
+
outside: 0,
|
|
1064
|
+
outsideByField: { headersFile: 0, postgresUrlFile: 0, searchKeyFile: 0, sqlite: 0 }
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function secretsDirFor(managedDir) {
|
|
1068
|
+
return join(managedDir, SECRETS_SUBDIR);
|
|
1069
|
+
}
|
|
1070
|
+
function sqliteDirsFor(managedDir) {
|
|
1071
|
+
return [join(managedDir, DATA_SUBDIR), secretsDirFor(managedDir)];
|
|
1072
|
+
}
|
|
1073
|
+
function parseSecretPathMode(value) {
|
|
1074
|
+
const v = (value ?? "").trim();
|
|
1075
|
+
if (v === "") return DEFAULT_SECRET_PATH_MODE;
|
|
1076
|
+
if (v === "report" || v === "enforce") return v;
|
|
1077
|
+
throw new Error(`${SECRET_PATH_MODE_ENV} must be "report" or "enforce", got ${JSON.stringify(value)}`);
|
|
1078
|
+
}
|
|
1079
|
+
function readSecretPathAllowlist(managedDir, readFile = (p) => readFileSync3(p, "utf8")) {
|
|
1080
|
+
let doc;
|
|
1081
|
+
try {
|
|
1082
|
+
doc = JSON.parse(readFile(join(managedDir, LOCAL_CONFIG_FILE)));
|
|
1083
|
+
} catch {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return [];
|
|
1087
|
+
const list = doc[ALLOWLIST_KEY];
|
|
1088
|
+
if (!Array.isArray(list)) return [];
|
|
1089
|
+
return list.filter((p) => typeof p === "string" && p !== "" && isAbsolute(p));
|
|
1090
|
+
}
|
|
1091
|
+
var isStrictlyUnder = (p, dir) => p.startsWith(dir.endsWith(sep) ? dir : dir + sep);
|
|
1092
|
+
var realpathOrNull = (p) => {
|
|
1093
|
+
try {
|
|
1094
|
+
return realpathSync(p);
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
function classifySecretPath(raw, opts) {
|
|
1100
|
+
if (!isAbsolute(raw)) return { kind: "outside", reason: "relative" };
|
|
1101
|
+
if (opts.allowlist.includes(raw)) return { kind: "allowlisted", readPath: raw };
|
|
1102
|
+
if (raw.split(/[\\/]+/).includes("..")) return { kind: "outside", reason: "parent-segment" };
|
|
1103
|
+
const lexicalDirs = (opts.dirs ?? [opts.secretsDir]).map((d) => normalize(d));
|
|
1104
|
+
const realDirs = lexicalDirs.map((d) => realpathOrNull(d) ?? d);
|
|
1105
|
+
const lexical = normalize(raw);
|
|
1106
|
+
const lexicallyInside = [...lexicalDirs, ...realDirs].some((d) => isStrictlyUnder(lexical, d));
|
|
1107
|
+
const real = realpathOrNull(raw);
|
|
1108
|
+
if (real === null) {
|
|
1109
|
+
return lexicallyInside ? { kind: "inside", readPath: lexical } : { kind: "outside", reason: "outside-dir" };
|
|
1110
|
+
}
|
|
1111
|
+
if (realDirs.some((d) => isStrictlyUnder(real, d))) return { kind: "inside", readPath: real };
|
|
1112
|
+
return { kind: "outside", reason: lexicallyInside ? "symlink-escape" : "outside-dir" };
|
|
1113
|
+
}
|
|
1114
|
+
function confineSecretPath(raw, field, policy, dataDir, summary) {
|
|
1115
|
+
const verdict = classifySecretPath(raw, policy);
|
|
1116
|
+
if (verdict.kind === "inside") {
|
|
1117
|
+
summary.inside += 1;
|
|
1118
|
+
return verdict.readPath;
|
|
1119
|
+
}
|
|
1120
|
+
if (verdict.kind === "allowlisted") {
|
|
1121
|
+
summary.allowlisted += 1;
|
|
1122
|
+
return verdict.readPath;
|
|
1123
|
+
}
|
|
1124
|
+
summary.outside += 1;
|
|
1125
|
+
summary.outsideByField[field] += 1;
|
|
1126
|
+
if (policy.mode === "enforce") return null;
|
|
1127
|
+
return isAbsolute(raw) ? raw : resolve(dataDir, raw);
|
|
1128
|
+
}
|
|
1129
|
+
function countSqlitePath(raw, policy, dataDir, summary) {
|
|
1130
|
+
const verdict = classifySecretPath(raw, {
|
|
1131
|
+
secretsDir: policy.secretsDir,
|
|
1132
|
+
allowlist: policy.allowlist,
|
|
1133
|
+
dirs: policy.sqliteDirs ?? [policy.secretsDir]
|
|
1134
|
+
});
|
|
1135
|
+
if (verdict.kind === "inside") summary.inside += 1;
|
|
1136
|
+
else if (verdict.kind === "allowlisted") summary.allowlisted += 1;
|
|
1137
|
+
else {
|
|
1138
|
+
summary.outside += 1;
|
|
1139
|
+
summary.outsideByField.sqlite += 1;
|
|
1140
|
+
}
|
|
1141
|
+
return isAbsolute(raw) ? raw : resolve(dataDir, raw);
|
|
1142
|
+
}
|
|
1143
|
+
function secretPathRefusedMessage(what) {
|
|
1144
|
+
return `${what} is outside this agent's secrets directory (~/.securecoms/runtime-config/<agent>/${SECRETS_SUBDIR}/) and is not listed in ${ALLOWLIST_KEY} in ${LOCAL_CONFIG_FILE} on the agent's machine, so it was not read. Move the file into the secrets directory, or list its exact path there.`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1049
1147
|
// src/config.ts
|
|
1050
1148
|
var MISSING_IDENTITY_MESSAGE = "Missing agent identity: pass --identity <id> or set SECURECOMS_IDENTITY";
|
|
1051
1149
|
var LOG_LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
@@ -1092,7 +1190,7 @@ function parseIntSetting(env, name, fallback, min) {
|
|
|
1092
1190
|
return Number(raw);
|
|
1093
1191
|
}
|
|
1094
1192
|
function defaultSkillsDir() {
|
|
1095
|
-
return
|
|
1193
|
+
return join2(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1096
1194
|
}
|
|
1097
1195
|
var DEFAULT_TOOL_ALLOW = [
|
|
1098
1196
|
"echo",
|
|
@@ -1124,7 +1222,7 @@ function loadConfig(argv, env) {
|
|
|
1124
1222
|
...allow !== void 0 ? { allow } : {},
|
|
1125
1223
|
...deny !== void 0 ? { deny } : {}
|
|
1126
1224
|
};
|
|
1127
|
-
const dataDir = parseStringFlag(argv, "--data-dir", env, "SECURECOMS_DATA_DIR") ||
|
|
1225
|
+
const dataDir = parseStringFlag(argv, "--data-dir", env, "SECURECOMS_DATA_DIR") || join2(homedir(), ".securecoms", "agent-runtime");
|
|
1128
1226
|
return {
|
|
1129
1227
|
identityId,
|
|
1130
1228
|
llmEndpoint: env.SECURECOMS_LLM_ENDPOINT || "http://127.0.0.1:11434",
|
|
@@ -1136,7 +1234,7 @@ function loadConfig(argv, env) {
|
|
|
1136
1234
|
tavilyKeyFile: parseStringFlag(argv, "--tavily-key-file", env, "SECURECOMS_TAVILY_KEY_FILE"),
|
|
1137
1235
|
skillsDir: env.SECURECOMS_SKILLS_DIR || defaultSkillsDir(),
|
|
1138
1236
|
dataDir,
|
|
1139
|
-
skillsConfigPath: parseStringFlag(argv, "--skills-config", env, "SECURECOMS_SKILLS_CONFIG") ??
|
|
1237
|
+
skillsConfigPath: parseStringFlag(argv, "--skills-config", env, "SECURECOMS_SKILLS_CONFIG") ?? join2(dataDir, "skills.json"),
|
|
1140
1238
|
logLevel,
|
|
1141
1239
|
toolGate,
|
|
1142
1240
|
toolGateEnv: {
|
|
@@ -1145,7 +1243,8 @@ function loadConfig(argv, env) {
|
|
|
1145
1243
|
},
|
|
1146
1244
|
llmMaxOutputTokens: parseIntSetting(env, "SECURECOMS_LLM_MAX_OUTPUT_TOKENS", DEFAULT_MAX_OUTPUT_TOKENS, 1),
|
|
1147
1245
|
llmDailyCallLimit: parseIntSetting(env, "SECURECOMS_LLM_DAILY_CALL_LIMIT", DEFAULT_DAILY_CALL_LIMIT, 0),
|
|
1148
|
-
llmDailyTokenLimit: parseIntSetting(env, "SECURECOMS_LLM_DAILY_TOKEN_LIMIT", DEFAULT_DAILY_TOKEN_LIMIT, 0)
|
|
1246
|
+
llmDailyTokenLimit: parseIntSetting(env, "SECURECOMS_LLM_DAILY_TOKEN_LIMIT", DEFAULT_DAILY_TOKEN_LIMIT, 0),
|
|
1247
|
+
secretPathMode: parseSecretPathMode(parseStringFlag(argv, "--secret-path-mode", env, SECRET_PATH_MODE_ENV))
|
|
1149
1248
|
};
|
|
1150
1249
|
}
|
|
1151
1250
|
|
|
@@ -1219,7 +1318,7 @@ function parseSideEffectResolveFlags(argv) {
|
|
|
1219
1318
|
import { dirname as dirname4 } from "node:path";
|
|
1220
1319
|
|
|
1221
1320
|
// src/service/config.ts
|
|
1222
|
-
import { isAbsolute, resolve } from "node:path";
|
|
1321
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
1223
1322
|
|
|
1224
1323
|
// src/service/resolve-node.ts
|
|
1225
1324
|
var REQUIRED_NODE_MAJOR = 22;
|
|
@@ -1306,7 +1405,7 @@ function slugify(s) {
|
|
|
1306
1405
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
1307
1406
|
}
|
|
1308
1407
|
function abs(p) {
|
|
1309
|
-
return
|
|
1408
|
+
return isAbsolute2(p) ? p : resolve2(p);
|
|
1310
1409
|
}
|
|
1311
1410
|
function deriveServiceConfig(input) {
|
|
1312
1411
|
if (isVersionEmbeddedNodePath(input.execPath)) {
|
|
@@ -1492,7 +1591,7 @@ function uninstallService(input, deps) {
|
|
|
1492
1591
|
}
|
|
1493
1592
|
|
|
1494
1593
|
// src/llm-source.ts
|
|
1495
|
-
import { join as
|
|
1594
|
+
import { join as join3 } from "node:path";
|
|
1496
1595
|
|
|
1497
1596
|
// src/llm-providers.json
|
|
1498
1597
|
var llm_providers_default = {
|
|
@@ -1565,8 +1664,8 @@ function resolveLlmSource(cfg, explicit, deps) {
|
|
|
1565
1664
|
chatAuth: "bearer"
|
|
1566
1665
|
};
|
|
1567
1666
|
}
|
|
1568
|
-
const managedDir =
|
|
1569
|
-
const sourcePath =
|
|
1667
|
+
const managedDir = join3(deps.home, ".securecoms", "runtime-config", cfg.identityId);
|
|
1668
|
+
const sourcePath = join3(managedDir, "llm-source.json");
|
|
1570
1669
|
let raw;
|
|
1571
1670
|
try {
|
|
1572
1671
|
raw = deps.readFile(sourcePath);
|
|
@@ -1605,10 +1704,10 @@ function resolveLlmSource(cfg, explicit, deps) {
|
|
|
1605
1704
|
if (entry.needsKey) {
|
|
1606
1705
|
const expectedOrigin = providerOrigin(provider);
|
|
1607
1706
|
const slug = providerSlug(provider);
|
|
1608
|
-
const keyPath =
|
|
1609
|
-
const hostPath =
|
|
1610
|
-
const legacyKey =
|
|
1611
|
-
const legacyHost =
|
|
1707
|
+
const keyPath = join3(managedDir, `llm.key.${slug}`);
|
|
1708
|
+
const hostPath = join3(managedDir, `llm.key.${slug}.host`);
|
|
1709
|
+
const legacyKey = join3(managedDir, "llm.key");
|
|
1710
|
+
const legacyHost = join3(managedDir, "llm.key.host");
|
|
1612
1711
|
const useLegacy = !deps.exists(keyPath) && deps.exists(legacyKey) && deps.exists(legacyHost);
|
|
1613
1712
|
const boundKey = useLegacy ? legacyKey : keyPath;
|
|
1614
1713
|
const boundHost = useLegacy ? legacyHost : hostPath;
|
|
@@ -1732,8 +1831,8 @@ ${scored.map((entry) => `- ${entry.content}`).join("\n")}`;
|
|
|
1732
1831
|
};
|
|
1733
1832
|
|
|
1734
1833
|
// src/skill-invoker.ts
|
|
1735
|
-
import { readFileSync as
|
|
1736
|
-
import { join as
|
|
1834
|
+
import { readFileSync as readFileSync7, readdirSync, existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
1835
|
+
import { join as join5 } from "node:path";
|
|
1737
1836
|
|
|
1738
1837
|
// src/skill-md.ts
|
|
1739
1838
|
var SkillMdError = class extends Error {
|
|
@@ -1907,22 +2006,22 @@ function resolveRequires(req, env) {
|
|
|
1907
2006
|
}
|
|
1908
2007
|
|
|
1909
2008
|
// src/skills-config.ts
|
|
1910
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1911
|
-
import { isAbsolute as
|
|
2009
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
2010
|
+
import { isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
|
|
1912
2011
|
|
|
1913
2012
|
// src/config-dirs.ts
|
|
1914
2013
|
import { existsSync } from "node:fs";
|
|
1915
2014
|
import { homedir as homedir2 } from "node:os";
|
|
1916
|
-
import { join as
|
|
2015
|
+
import { join as join4 } from "node:path";
|
|
1917
2016
|
function managedConfigDir(identityId, home = homedir2()) {
|
|
1918
|
-
return
|
|
2017
|
+
return join4(home, ".securecoms", "runtime-config", identityId);
|
|
1919
2018
|
}
|
|
1920
2019
|
function configSearchPath(identityId, dataDir, home = homedir2()) {
|
|
1921
2020
|
return [managedConfigDir(identityId, home), dataDir];
|
|
1922
2021
|
}
|
|
1923
2022
|
function findConfigFile(name, dirs) {
|
|
1924
2023
|
for (const dir of dirs) {
|
|
1925
|
-
const path =
|
|
2024
|
+
const path = join4(dir, name);
|
|
1926
2025
|
if (existsSync(path)) return path;
|
|
1927
2026
|
}
|
|
1928
2027
|
return null;
|
|
@@ -1931,12 +2030,12 @@ function findConfigFile(name, dirs) {
|
|
|
1931
2030
|
// src/skills-config.ts
|
|
1932
2031
|
var EMPTY_SKILLS_CONFIG = { endpoints: {}, databases: {}, flags: [] };
|
|
1933
2032
|
var empty = () => ({ endpoints: {}, databases: {}, flags: [] });
|
|
1934
|
-
var abs2 = (p, dataDir) =>
|
|
1935
|
-
function loadSkillsConfigFile(path, dataDir) {
|
|
2033
|
+
var abs2 = (p, dataDir) => isAbsolute3(p) ? p : resolve3(dataDir, p);
|
|
2034
|
+
function loadSkillsConfigFile(path, dataDir, policy) {
|
|
1936
2035
|
if (!existsSync2(path)) return empty();
|
|
1937
2036
|
let raw;
|
|
1938
2037
|
try {
|
|
1939
|
-
raw = JSON.parse(
|
|
2038
|
+
raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
1940
2039
|
} catch (e) {
|
|
1941
2040
|
writeServiceLog("warn", "skills_config.parse_failed", {}, e);
|
|
1942
2041
|
return empty();
|
|
@@ -1946,6 +2045,8 @@ function loadSkillsConfigFile(path, dataDir) {
|
|
|
1946
2045
|
return empty();
|
|
1947
2046
|
}
|
|
1948
2047
|
const src = raw;
|
|
2048
|
+
const summary = policy ? emptySecretPathSummary(policy.mode) : void 0;
|
|
2049
|
+
const secretPath = (rawPath, field) => policy && summary ? confineSecretPath(rawPath, field, policy, dataDir, summary) : abs2(rawPath, dataDir);
|
|
1949
2050
|
const endpoints = {};
|
|
1950
2051
|
for (const [name, v] of Object.entries(src.endpoints ?? {})) {
|
|
1951
2052
|
const e = v;
|
|
@@ -1953,17 +2054,23 @@ function loadSkillsConfigFile(path, dataDir) {
|
|
|
1953
2054
|
writeServiceLog("warn", "skills_config.endpoint_invalid");
|
|
1954
2055
|
continue;
|
|
1955
2056
|
}
|
|
2057
|
+
const headersFile = typeof e.headersFile === "string" ? secretPath(e.headersFile, "headersFile") : void 0;
|
|
1956
2058
|
endpoints[name] = {
|
|
1957
2059
|
baseUrl: e.baseUrl,
|
|
1958
|
-
...typeof
|
|
2060
|
+
...typeof headersFile === "string" ? { headersFile } : {},
|
|
2061
|
+
...headersFile === null ? { headersFileRefused: true } : {}
|
|
1959
2062
|
};
|
|
1960
2063
|
}
|
|
1961
2064
|
const databases = {};
|
|
1962
2065
|
for (const [name, v] of Object.entries(src.databases ?? {})) {
|
|
1963
2066
|
const d = v;
|
|
1964
|
-
if (typeof d?.sqlite === "string" && d.sqlite !== "")
|
|
1965
|
-
|
|
1966
|
-
|
|
2067
|
+
if (typeof d?.sqlite === "string" && d.sqlite !== "") {
|
|
2068
|
+
databases[name] = {
|
|
2069
|
+
sqlite: policy && summary ? countSqlitePath(d.sqlite, policy, dataDir, summary) : abs2(d.sqlite, dataDir)
|
|
2070
|
+
};
|
|
2071
|
+
} else if (typeof d?.postgresUrlFile === "string" && d.postgresUrlFile !== "") {
|
|
2072
|
+
const postgresUrlFile = secretPath(d.postgresUrlFile, "postgresUrlFile");
|
|
2073
|
+
databases[name] = postgresUrlFile === null ? { postgresUrlFileRefused: true } : { postgresUrlFile };
|
|
1967
2074
|
} else writeServiceLog("warn", "skills_config.database_invalid");
|
|
1968
2075
|
}
|
|
1969
2076
|
const flags = Array.isArray(src.flags) ? src.flags.filter((f) => typeof f === "string") : [];
|
|
@@ -1981,10 +2088,32 @@ function loadSkillsConfigFile(path, dataDir) {
|
|
|
1981
2088
|
} else if (typeof s.keyFile !== "string" || s.keyFile === "") {
|
|
1982
2089
|
writeServiceLog("warn", "skills_config.search_key_file_missing");
|
|
1983
2090
|
} else {
|
|
1984
|
-
|
|
2091
|
+
const keyFile = secretPath(s.keyFile, "searchKeyFile");
|
|
2092
|
+
search = keyFile === null ? { provider: s.provider, keyFileRefused: true } : { provider: s.provider, keyFile };
|
|
1985
2093
|
}
|
|
1986
2094
|
}
|
|
1987
|
-
return {
|
|
2095
|
+
return {
|
|
2096
|
+
endpoints,
|
|
2097
|
+
databases,
|
|
2098
|
+
flags,
|
|
2099
|
+
...tools ? { tools } : {},
|
|
2100
|
+
...search ? { search } : {},
|
|
2101
|
+
...summary ? { secretPaths: summary } : {}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function loadEffectiveSkillsConfig(args) {
|
|
2105
|
+
const managed = findConfigFile("skills.json", [args.managedDir]);
|
|
2106
|
+
if (!managed) return loadSkillsConfigFile(args.localPath, args.dataDir);
|
|
2107
|
+
return loadSkillsConfigFile(managed, args.dataDir, {
|
|
2108
|
+
mode: args.mode,
|
|
2109
|
+
secretsDir: secretsDirFor(args.managedDir),
|
|
2110
|
+
allowlist: readSecretPathAllowlist(args.managedDir),
|
|
2111
|
+
sqliteDirs: sqliteDirsFor(args.managedDir)
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function searchKeyFilePath(search, which, installPath) {
|
|
2115
|
+
if (search && search.provider === which && search.keyFile !== void 0) return search.keyFile;
|
|
2116
|
+
return installPath;
|
|
1988
2117
|
}
|
|
1989
2118
|
|
|
1990
2119
|
// src/builtins/current-time.ts
|
|
@@ -2239,7 +2368,7 @@ function isPrivateAddress(ip) {
|
|
|
2239
2368
|
}
|
|
2240
2369
|
return (v6[0] & 65024) === 64512 || (v6[0] & 65472) === 65152;
|
|
2241
2370
|
}
|
|
2242
|
-
async function assertPublicHost(url,
|
|
2371
|
+
async function assertPublicHost(url, resolve4, signal) {
|
|
2243
2372
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2244
2373
|
throw new BlockedAddressError(`refused ${url.protocol}//: only http and https are allowed`);
|
|
2245
2374
|
}
|
|
@@ -2250,7 +2379,7 @@ async function assertPublicHost(url, resolve3, signal) {
|
|
|
2250
2379
|
let addresses;
|
|
2251
2380
|
try {
|
|
2252
2381
|
if (signal?.aborted) throw signal.reason ?? new Error("network request cancelled");
|
|
2253
|
-
addresses = await
|
|
2382
|
+
addresses = await resolve4(host, signal);
|
|
2254
2383
|
if (signal?.aborted) throw signal.reason ?? new Error("network request cancelled");
|
|
2255
2384
|
} catch (e) {
|
|
2256
2385
|
throw new BlockedAddressError(`refused ${url.hostname}: cannot resolve (${e instanceof Error ? e.message : String(e)})`);
|
|
@@ -2377,7 +2506,7 @@ var fetchUrl = async (args, ctx) => {
|
|
|
2377
2506
|
};
|
|
2378
2507
|
|
|
2379
2508
|
// src/builtins/http-request.ts
|
|
2380
|
-
import { readFileSync as
|
|
2509
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2381
2510
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE"]);
|
|
2382
2511
|
var httpRequest = async (args, ctx) => {
|
|
2383
2512
|
const name = typeof args.name === "string" ? args.name : "";
|
|
@@ -2387,6 +2516,9 @@ var httpRequest = async (args, ctx) => {
|
|
|
2387
2516
|
`unknown endpoint "${name}": configured endpoints are ${Object.keys(ctx.skillsConfig.endpoints).join(", ") || "(none)"}`
|
|
2388
2517
|
);
|
|
2389
2518
|
}
|
|
2519
|
+
if (endpoint.headersFileRefused) {
|
|
2520
|
+
return toolError(secretPathRefusedMessage(`the headers file for endpoint "${name}"`));
|
|
2521
|
+
}
|
|
2390
2522
|
const method = String(args.method ?? "GET").toUpperCase();
|
|
2391
2523
|
if (!METHODS.has(method)) return toolError(`unsupported method "${method}": use GET, POST, PUT or DELETE`);
|
|
2392
2524
|
let base;
|
|
@@ -2420,7 +2552,7 @@ var httpRequest = async (args, ctx) => {
|
|
|
2420
2552
|
const headers = { accept: "application/json" };
|
|
2421
2553
|
if (endpoint.headersFile) {
|
|
2422
2554
|
try {
|
|
2423
|
-
for (const [k, v] of Object.entries(JSON.parse(
|
|
2555
|
+
for (const [k, v] of Object.entries(JSON.parse(readFileSync5(endpoint.headersFile, "utf8")))) {
|
|
2424
2556
|
if (typeof v === "string") headers[k] = v;
|
|
2425
2557
|
}
|
|
2426
2558
|
} catch {
|
|
@@ -2447,7 +2579,7 @@ var httpRequest = async (args, ctx) => {
|
|
|
2447
2579
|
};
|
|
2448
2580
|
|
|
2449
2581
|
// src/builtins/sql-query.ts
|
|
2450
|
-
import { readFileSync as
|
|
2582
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2451
2583
|
import { spawn } from "node:child_process";
|
|
2452
2584
|
import { createConnection } from "node:net";
|
|
2453
2585
|
|
|
@@ -2561,7 +2693,7 @@ var SQLITE_HEAP_CAP_BYTES = 32 * 1024 * 1024;
|
|
|
2561
2693
|
function executeSqliteQuery(path, sql, options = {}) {
|
|
2562
2694
|
const timeoutMs = options.timeoutMs ?? SQL_TIMEOUT_MS;
|
|
2563
2695
|
if (options.signal?.aborted) return Promise.reject(new Error("SQLite query cancelled"));
|
|
2564
|
-
return new Promise((
|
|
2696
|
+
return new Promise((resolve4, reject) => {
|
|
2565
2697
|
const child = spawn(process.execPath, [
|
|
2566
2698
|
"--no-warnings",
|
|
2567
2699
|
"--max-old-space-size=32",
|
|
@@ -2625,7 +2757,7 @@ function executeSqliteQuery(path, sql, options = {}) {
|
|
|
2625
2757
|
}
|
|
2626
2758
|
try {
|
|
2627
2759
|
const message = JSON.parse(stdout);
|
|
2628
|
-
if (message.ok && message.result)
|
|
2760
|
+
if (message.ok && message.result) resolve4(message.result);
|
|
2629
2761
|
else reject(new Error(message.error ?? "SQLite query process failed"));
|
|
2630
2762
|
} catch {
|
|
2631
2763
|
reject(new Error("SQLite query process returned an invalid result"));
|
|
@@ -2832,6 +2964,9 @@ var sqlQuery = async (args, ctx) => {
|
|
|
2832
2964
|
`unknown database "${name}": configured databases are ${Object.keys(ctx.skillsConfig.databases).join(", ") || "(none)"}`
|
|
2833
2965
|
);
|
|
2834
2966
|
}
|
|
2967
|
+
if ("postgresUrlFileRefused" in db) {
|
|
2968
|
+
return toolError(secretPathRefusedMessage(`the connection-string file for database "${name}"`));
|
|
2969
|
+
}
|
|
2835
2970
|
let sql;
|
|
2836
2971
|
try {
|
|
2837
2972
|
sql = assertReadOnlySelect(String(args.sql ?? ""));
|
|
@@ -2845,7 +2980,7 @@ var sqlQuery = async (args, ctx) => {
|
|
|
2845
2980
|
}
|
|
2846
2981
|
try {
|
|
2847
2982
|
const result = await executePostgresQuery(
|
|
2848
|
-
|
|
2983
|
+
readFileSync6(db.postgresUrlFile, "utf8").trim(),
|
|
2849
2984
|
// never logged (D4)
|
|
2850
2985
|
sql,
|
|
2851
2986
|
{ signal: ctx.signal }
|
|
@@ -3185,11 +3320,11 @@ var SkillInvoker = class {
|
|
|
3185
3320
|
const flat = [], manifests = [], skillMds = [];
|
|
3186
3321
|
for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
|
|
3187
3322
|
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
3188
|
-
flat.push(
|
|
3323
|
+
flat.push(join5(this.skillsDir, entry.name));
|
|
3189
3324
|
} else if (entry.isDirectory()) {
|
|
3190
|
-
const m =
|
|
3325
|
+
const m = join5(this.skillsDir, entry.name, "manifest.json");
|
|
3191
3326
|
if (existsSync3(m) && statSync2(m).isFile()) manifests.push(m);
|
|
3192
|
-
const s =
|
|
3327
|
+
const s = join5(this.skillsDir, entry.name, "SKILL.md");
|
|
3193
3328
|
if (existsSync3(s) && statSync2(s).isFile()) skillMds.push(s);
|
|
3194
3329
|
}
|
|
3195
3330
|
}
|
|
@@ -3206,14 +3341,14 @@ var SkillInvoker = class {
|
|
|
3206
3341
|
}
|
|
3207
3342
|
loadSkillMdFile(path) {
|
|
3208
3343
|
try {
|
|
3209
|
-
this.register(parseSkillMd(
|
|
3344
|
+
this.register(parseSkillMd(readFileSync7(path, "utf-8"), path), path);
|
|
3210
3345
|
} catch (err) {
|
|
3211
3346
|
writeServiceLog("warn", "skills.skill_md_invalid", {}, err);
|
|
3212
3347
|
}
|
|
3213
3348
|
}
|
|
3214
3349
|
loadManifestFile(path) {
|
|
3215
3350
|
try {
|
|
3216
|
-
const raw =
|
|
3351
|
+
const raw = readFileSync7(path, "utf-8");
|
|
3217
3352
|
const manifest = JSON.parse(raw);
|
|
3218
3353
|
if (typeof manifest.handler !== "string" || !(manifest.handler.startsWith("builtin:") || manifest.handler.startsWith("script:"))) {
|
|
3219
3354
|
writeServiceLog("warn", "skills.manifest_handler_invalid");
|
|
@@ -3336,16 +3471,16 @@ var SkillInvoker = class {
|
|
|
3336
3471
|
}
|
|
3337
3472
|
}
|
|
3338
3473
|
async invokeScript(scriptPath, args, signal) {
|
|
3339
|
-
const { resolve:
|
|
3340
|
-
const resolved =
|
|
3474
|
+
const { resolve: resolve4, relative } = await import("node:path");
|
|
3475
|
+
const resolved = resolve4(this.skillsDir, scriptPath);
|
|
3341
3476
|
const rel = relative(this.skillsDir, resolved);
|
|
3342
|
-
if (rel.startsWith("..") || !resolved.startsWith(
|
|
3477
|
+
if (rel.startsWith("..") || !resolved.startsWith(resolve4(this.skillsDir))) {
|
|
3343
3478
|
return JSON.stringify({ error: "Script path must be within skills directory" });
|
|
3344
3479
|
}
|
|
3345
|
-
const { realpathSync:
|
|
3480
|
+
const { realpathSync: realpathSync3, existsSync: fsExistsSync } = await import("node:fs");
|
|
3346
3481
|
if (fsExistsSync(resolved)) {
|
|
3347
|
-
const realResolved =
|
|
3348
|
-
const realSkillsDir =
|
|
3482
|
+
const realResolved = realpathSync3(resolved);
|
|
3483
|
+
const realSkillsDir = realpathSync3(this.skillsDir);
|
|
3349
3484
|
const realRel = relative(realSkillsDir, realResolved);
|
|
3350
3485
|
if (realRel.startsWith("..") || !realResolved.startsWith(realSkillsDir)) {
|
|
3351
3486
|
return JSON.stringify({ error: "Script path must be within skills directory" });
|
|
@@ -3479,10 +3614,10 @@ function createDynamicToolGate(policyFor) {
|
|
|
3479
3614
|
}
|
|
3480
3615
|
|
|
3481
3616
|
// src/data-plane.ts
|
|
3482
|
-
import { readFileSync as
|
|
3617
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3483
3618
|
import { WebSocket } from "node:http";
|
|
3484
3619
|
import { homedir as homedir3 } from "node:os";
|
|
3485
|
-
import { join as
|
|
3620
|
+
import { join as join6 } from "node:path";
|
|
3486
3621
|
|
|
3487
3622
|
// src/status-reporter.ts
|
|
3488
3623
|
function createStatusReporter(log, fetchImpl = fetch) {
|
|
@@ -3669,16 +3804,16 @@ var SecureComsDataPlane = class {
|
|
|
3669
3804
|
status;
|
|
3670
3805
|
constructor(cfg, onMessage) {
|
|
3671
3806
|
this.identityId = cfg.identityId;
|
|
3672
|
-
const dir = cfg.connectionsDir ??
|
|
3673
|
-
this.connectionPath =
|
|
3807
|
+
const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
|
|
3808
|
+
this.connectionPath = join6(dir, `${cfg.identityId}.json`);
|
|
3674
3809
|
this.establishedMs = cfg.establishedMs ?? 5e3;
|
|
3675
3810
|
this.initialDelayMs = cfg.initialDelayMs ?? 250;
|
|
3676
3811
|
this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
|
|
3677
3812
|
this.log = cfg.log ?? writeServiceLog;
|
|
3678
|
-
this.sleep = cfg.sleep ?? ((ms2) => new Promise((
|
|
3813
|
+
this.sleep = cfg.sleep ?? ((ms2) => new Promise((resolve4) => {
|
|
3679
3814
|
this.sleepTimer = setTimeout(() => {
|
|
3680
3815
|
this.sleepTimer = null;
|
|
3681
|
-
|
|
3816
|
+
resolve4();
|
|
3682
3817
|
}, ms2);
|
|
3683
3818
|
}));
|
|
3684
3819
|
this.random = cfg.random ?? Math.random;
|
|
@@ -3748,13 +3883,13 @@ var SecureComsDataPlane = class {
|
|
|
3748
3883
|
/** The backoff sleep, made interruptible by stop(). */
|
|
3749
3884
|
wait(ms2) {
|
|
3750
3885
|
if (this.stopped) return Promise.resolve();
|
|
3751
|
-
return new Promise((
|
|
3886
|
+
return new Promise((resolve4) => {
|
|
3752
3887
|
let settled = false;
|
|
3753
3888
|
const finish = () => {
|
|
3754
3889
|
if (settled) return;
|
|
3755
3890
|
settled = true;
|
|
3756
3891
|
this.sleepWaiters = this.sleepWaiters.filter((w) => w !== finish);
|
|
3757
|
-
|
|
3892
|
+
resolve4();
|
|
3758
3893
|
};
|
|
3759
3894
|
this.sleepWaiters.push(finish);
|
|
3760
3895
|
void this.sleep(ms2).then(finish, finish);
|
|
@@ -3790,9 +3925,9 @@ var SecureComsDataPlane = class {
|
|
|
3790
3925
|
if (this.generation === generation) this.running = false;
|
|
3791
3926
|
}
|
|
3792
3927
|
async attemptOnce() {
|
|
3793
|
-
const conn = JSON.parse(
|
|
3928
|
+
const conn = JSON.parse(readFileSync8(this.connectionPath, "utf8"));
|
|
3794
3929
|
const httpBase = conn.bridge_url.replace(/^ws/, "http").replace(/\/v1\/ws$/, "");
|
|
3795
|
-
const token =
|
|
3930
|
+
const token = readFileSync8(conn.token_file, "utf8").trim();
|
|
3796
3931
|
const healthController = new AbortController();
|
|
3797
3932
|
this.healthController = healthController;
|
|
3798
3933
|
const healthTimer = setTimeout(
|
|
@@ -3817,12 +3952,12 @@ var SecureComsDataPlane = class {
|
|
|
3817
3952
|
let established = false;
|
|
3818
3953
|
let settle = null;
|
|
3819
3954
|
try {
|
|
3820
|
-
await new Promise((
|
|
3955
|
+
await new Promise((resolve4, reject) => {
|
|
3821
3956
|
ws.addEventListener("error", (e) => {
|
|
3822
3957
|
reject(Object.assign(new Error("socket_error"), { cause: e }));
|
|
3823
3958
|
});
|
|
3824
3959
|
ws.addEventListener("close", (e) => {
|
|
3825
|
-
if (established)
|
|
3960
|
+
if (established) resolve4();
|
|
3826
3961
|
else {
|
|
3827
3962
|
reject(
|
|
3828
3963
|
new Error(
|
|
@@ -4236,20 +4371,20 @@ function buildHandler(deps) {
|
|
|
4236
4371
|
function waitUnlessAborted(gate2, signal) {
|
|
4237
4372
|
if (!signal) return gate2;
|
|
4238
4373
|
if (signal.aborted) return Promise.reject(cancelledError());
|
|
4239
|
-
return new Promise((
|
|
4374
|
+
return new Promise((resolve4, reject) => {
|
|
4240
4375
|
const onAbort = () => reject(cancelledError());
|
|
4241
4376
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
4242
4377
|
void gate2.then(() => {
|
|
4243
4378
|
signal.removeEventListener("abort", onAbort);
|
|
4244
|
-
|
|
4379
|
+
resolve4();
|
|
4245
4380
|
});
|
|
4246
4381
|
});
|
|
4247
4382
|
}
|
|
4248
4383
|
async function lockChannel(channelId, signal) {
|
|
4249
4384
|
const previous = channelTails.get(channelId) ?? Promise.resolve();
|
|
4250
4385
|
let release;
|
|
4251
|
-
const mine = new Promise((
|
|
4252
|
-
release =
|
|
4386
|
+
const mine = new Promise((resolve4) => {
|
|
4387
|
+
release = resolve4;
|
|
4253
4388
|
});
|
|
4254
4389
|
const tail = previous.then(() => mine);
|
|
4255
4390
|
channelTails.set(channelId, tail);
|
|
@@ -4282,11 +4417,11 @@ function buildHandler(deps) {
|
|
|
4282
4417
|
return Promise.resolve(release);
|
|
4283
4418
|
}
|
|
4284
4419
|
if (signal?.aborted) return Promise.reject(cancelledError());
|
|
4285
|
-
return new Promise((
|
|
4420
|
+
return new Promise((resolve4, reject) => {
|
|
4286
4421
|
const grant = () => {
|
|
4287
4422
|
signal?.removeEventListener("abort", onAbort);
|
|
4288
4423
|
activeTurns++;
|
|
4289
|
-
|
|
4424
|
+
resolve4(release);
|
|
4290
4425
|
};
|
|
4291
4426
|
const onAbort = () => {
|
|
4292
4427
|
const i = slotWaiters.indexOf(grant);
|
|
@@ -4428,8 +4563,8 @@ function buildHandler(deps) {
|
|
|
4428
4563
|
wedged = true;
|
|
4429
4564
|
log("error", "turn.runtime_wedged", { abandoned_unsettled: abandonedUnsettled });
|
|
4430
4565
|
if (deps.onWedged && !retiring) {
|
|
4431
|
-
retiring = new Promise((
|
|
4432
|
-
onIdle =
|
|
4566
|
+
retiring = new Promise((resolve4) => {
|
|
4567
|
+
onIdle = resolve4;
|
|
4433
4568
|
});
|
|
4434
4569
|
deps.onWedged(retiring);
|
|
4435
4570
|
}
|
|
@@ -4859,14 +4994,14 @@ ${recalledText}` } : null;
|
|
|
4859
4994
|
}
|
|
4860
4995
|
|
|
4861
4996
|
// src/persona.ts
|
|
4862
|
-
import { existsSync as existsSync5, readFileSync as
|
|
4863
|
-
import { dirname as dirname6, join as
|
|
4997
|
+
import { existsSync as existsSync5, readFileSync as readFileSync10, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4998
|
+
import { dirname as dirname6, join as join8 } from "node:path";
|
|
4864
4999
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4865
5000
|
|
|
4866
5001
|
// src/long-term-memory.ts
|
|
4867
5002
|
import { createHash as createHash2 } from "node:crypto";
|
|
4868
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, lstatSync, readFileSync as
|
|
4869
|
-
import { join as
|
|
5003
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, lstatSync, readFileSync as readFileSync9, readdirSync as readdirSync2, renameSync as renameSync2 } from "node:fs";
|
|
5004
|
+
import { join as join7 } from "node:path";
|
|
4870
5005
|
var MEMORY_FILE = "MEMORY.md";
|
|
4871
5006
|
var QUARANTINE_MEMORY_FILE = "MEMORY.quarantined.md";
|
|
4872
5007
|
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
@@ -4903,7 +5038,7 @@ function readLegacySource(path, kind, contentBudget, channelId) {
|
|
|
4903
5038
|
};
|
|
4904
5039
|
}
|
|
4905
5040
|
if (st.size > contentBudget) return null;
|
|
4906
|
-
const bytes =
|
|
5041
|
+
const bytes = readFileSync9(path);
|
|
4907
5042
|
if (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget) return null;
|
|
4908
5043
|
return {
|
|
4909
5044
|
id: sourceId(kind, channelId, path, bytes),
|
|
@@ -4938,13 +5073,13 @@ function collectLegacyMemorySources(managedDir, dataDir) {
|
|
|
4938
5073
|
}
|
|
4939
5074
|
for (const name of names.sort(quarantineOrder)) {
|
|
4940
5075
|
if (!/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) continue;
|
|
4941
|
-
const path =
|
|
5076
|
+
const path = join7(dir, name);
|
|
4942
5077
|
if (seen.has(path)) continue;
|
|
4943
5078
|
seen.add(path);
|
|
4944
5079
|
add(path, "global-memory");
|
|
4945
5080
|
}
|
|
4946
5081
|
}
|
|
4947
|
-
const channelsDir =
|
|
5082
|
+
const channelsDir = join7(dataDir, "channels");
|
|
4948
5083
|
let channelNames;
|
|
4949
5084
|
try {
|
|
4950
5085
|
channelNames = readdirSync2(channelsDir);
|
|
@@ -4952,12 +5087,12 @@ function collectLegacyMemorySources(managedDir, dataDir) {
|
|
|
4952
5087
|
channelNames = [];
|
|
4953
5088
|
}
|
|
4954
5089
|
for (const channelId of channelNames.sort()) {
|
|
4955
|
-
const channelDir =
|
|
5090
|
+
const channelDir = join7(channelsDir, channelId);
|
|
4956
5091
|
for (const [name, kind] of [
|
|
4957
5092
|
["conversation-history.json", "conversation-history"],
|
|
4958
5093
|
["memories.json", "captured-memory"]
|
|
4959
5094
|
]) {
|
|
4960
|
-
add(
|
|
5095
|
+
add(join7(channelDir, name), kind, channelId);
|
|
4961
5096
|
}
|
|
4962
5097
|
}
|
|
4963
5098
|
return sources;
|
|
@@ -4975,33 +5110,33 @@ function legacyMemoryFingerprint(managedDir, dataDir) {
|
|
|
4975
5110
|
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
4976
5111
|
try {
|
|
4977
5112
|
for (const name of readdirSync2(dir).sort(quarantineOrder)) {
|
|
4978
|
-
if (/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) add(
|
|
5113
|
+
if (/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) add(join7(dir, name));
|
|
4979
5114
|
}
|
|
4980
5115
|
} catch {
|
|
4981
5116
|
}
|
|
4982
5117
|
}
|
|
4983
5118
|
try {
|
|
4984
|
-
const channelsDir =
|
|
5119
|
+
const channelsDir = join7(dataDir, "channels");
|
|
4985
5120
|
for (const channelId of readdirSync2(channelsDir).sort()) {
|
|
4986
|
-
add(
|
|
4987
|
-
add(
|
|
5121
|
+
add(join7(channelsDir, channelId, "conversation-history.json"));
|
|
5122
|
+
add(join7(channelsDir, channelId, "memories.json"));
|
|
4988
5123
|
}
|
|
4989
5124
|
} catch {
|
|
4990
5125
|
}
|
|
4991
5126
|
return createHash2("sha256").update(metadata.join("\n")).digest("hex");
|
|
4992
5127
|
}
|
|
4993
5128
|
function nextQuarantinePath(dir) {
|
|
4994
|
-
const first =
|
|
5129
|
+
const first = join7(dir, QUARANTINE_MEMORY_FILE);
|
|
4995
5130
|
if (!existsSync4(first)) return first;
|
|
4996
5131
|
for (let n = 1; ; n++) {
|
|
4997
|
-
const candidate =
|
|
5132
|
+
const candidate = join7(dir, `MEMORY.quarantined.${n}.md`);
|
|
4998
5133
|
if (!existsSync4(candidate)) return candidate;
|
|
4999
5134
|
}
|
|
5000
5135
|
}
|
|
5001
5136
|
function quarantineLegacyMemory(managedDir, dataDir) {
|
|
5002
5137
|
const moved = [];
|
|
5003
5138
|
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
5004
|
-
const source =
|
|
5139
|
+
const source = join7(dir, MEMORY_FILE);
|
|
5005
5140
|
if (!existsSync4(source)) continue;
|
|
5006
5141
|
if (lstatSync(source).isSymbolicLink()) {
|
|
5007
5142
|
throw new Error(`refusing to quarantine symlinked legacy memory: ${source}`);
|
|
@@ -5029,7 +5164,7 @@ var SAFETY_PREAMBLE = [
|
|
|
5029
5164
|
"- Only use the tools you were given; do not invent capabilities."
|
|
5030
5165
|
].join("\n");
|
|
5031
5166
|
function defaultPersonaDir() {
|
|
5032
|
-
return
|
|
5167
|
+
return join8(dirname6(fileURLToPath2(import.meta.url)), "..", "persona");
|
|
5033
5168
|
}
|
|
5034
5169
|
function assemblePrompt(parts) {
|
|
5035
5170
|
return parts.map((p) => `${p.heading}
|
|
@@ -5042,7 +5177,7 @@ function loadPersona(dirs) {
|
|
|
5042
5177
|
if (!path) continue;
|
|
5043
5178
|
let body;
|
|
5044
5179
|
try {
|
|
5045
|
-
body =
|
|
5180
|
+
body = readFileSync10(path, "utf8");
|
|
5046
5181
|
} catch (error) {
|
|
5047
5182
|
writeServiceLog("warn", "persona.read_failed", {}, error);
|
|
5048
5183
|
continue;
|
|
@@ -5061,17 +5196,42 @@ function systemPromptFor(dirs, fallback) {
|
|
|
5061
5196
|
|
|
5062
5197
|
${SAFETY_PREAMBLE}`;
|
|
5063
5198
|
}
|
|
5199
|
+
function secretPathsReport(s) {
|
|
5200
|
+
return {
|
|
5201
|
+
mode: s.mode,
|
|
5202
|
+
inside: s.inside,
|
|
5203
|
+
allowlisted: s.allowlisted,
|
|
5204
|
+
outside: s.outside,
|
|
5205
|
+
outside_by_field: {
|
|
5206
|
+
headers_file: s.outsideByField.headersFile,
|
|
5207
|
+
postgres_url_file: s.outsideByField.postgresUrlFile,
|
|
5208
|
+
search_key_file: s.outsideByField.searchKeyFile,
|
|
5209
|
+
sqlite: s.outsideByField.sqlite
|
|
5210
|
+
}
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
function existingAllowlist(path) {
|
|
5214
|
+
try {
|
|
5215
|
+
const doc = JSON.parse(readFileSync10(path, "utf8"));
|
|
5216
|
+
const list = doc?.[ALLOWLIST_KEY];
|
|
5217
|
+
return Array.isArray(list) ? list : void 0;
|
|
5218
|
+
} catch {
|
|
5219
|
+
return void 0;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5064
5222
|
function writeLocalConfigReport(managedDir, dataDir, now, flags) {
|
|
5065
5223
|
if (!existsSync5(managedDir)) return false;
|
|
5066
5224
|
const persona = {};
|
|
5067
5225
|
for (const { file } of PERSONA_SOURCES) {
|
|
5068
|
-
const path2 =
|
|
5226
|
+
const path2 = join8(dataDir, file);
|
|
5069
5227
|
if (!existsSync5(path2)) continue;
|
|
5070
5228
|
try {
|
|
5071
|
-
persona[file] =
|
|
5229
|
+
persona[file] = readFileSync10(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
|
|
5072
5230
|
} catch {
|
|
5073
5231
|
}
|
|
5074
5232
|
}
|
|
5233
|
+
const path = join8(managedDir, "local-config.json");
|
|
5234
|
+
const allowlist = existingAllowlist(path);
|
|
5075
5235
|
const doc = {
|
|
5076
5236
|
schema: "securecoms.local-config/2",
|
|
5077
5237
|
written_at: now.toISOString(),
|
|
@@ -5080,9 +5240,10 @@ function writeLocalConfigReport(managedDir, dataDir, now, flags) {
|
|
|
5080
5240
|
persona,
|
|
5081
5241
|
// Paths stay in this owner-only local handoff file. The daemon validates
|
|
5082
5242
|
// each source again and strips `path` before reporting anything upstream.
|
|
5083
|
-
legacy_memory: collectLegacyMemorySources(managedDir, dataDir)
|
|
5243
|
+
legacy_memory: collectLegacyMemorySources(managedDir, dataDir),
|
|
5244
|
+
...flags.secretPaths ? { secret_paths: secretPathsReport(flags.secretPaths) } : {},
|
|
5245
|
+
...allowlist ? { [ALLOWLIST_KEY]: allowlist } : {}
|
|
5084
5246
|
};
|
|
5085
|
-
const path = join7(managedDir, "local-config.json");
|
|
5086
5247
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
5087
5248
|
try {
|
|
5088
5249
|
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
@@ -5101,12 +5262,12 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
|
5101
5262
|
const written = [];
|
|
5102
5263
|
const skipped = [];
|
|
5103
5264
|
for (const { file } of PERSONA_SOURCES) {
|
|
5104
|
-
const target =
|
|
5265
|
+
const target = join8(dataDir, file);
|
|
5105
5266
|
if (exists(target)) {
|
|
5106
5267
|
skipped.push(target);
|
|
5107
5268
|
continue;
|
|
5108
5269
|
}
|
|
5109
|
-
writeFile(target,
|
|
5270
|
+
writeFile(target, readFileSync10(join8(defaultsDir, file), "utf8"));
|
|
5110
5271
|
written.push(target);
|
|
5111
5272
|
}
|
|
5112
5273
|
return { written, skipped };
|
|
@@ -5115,13 +5276,15 @@ function createLocalConfigReporter(managedDir, dataDir, now) {
|
|
|
5115
5276
|
let lastWritten = null;
|
|
5116
5277
|
return (flags) => {
|
|
5117
5278
|
const legacyFingerprint = legacyMemoryFingerprint(managedDir, dataDir);
|
|
5118
|
-
|
|
5279
|
+
const secretPathsKey = JSON.stringify(flags.secretPaths ?? null);
|
|
5280
|
+
if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey && lastWritten.secretPathsKey === secretPathsKey && lastWritten.legacyFingerprint === legacyFingerprint) {
|
|
5119
5281
|
return false;
|
|
5120
5282
|
}
|
|
5121
5283
|
const wrote = writeLocalConfigReport(managedDir, dataDir, now(), flags);
|
|
5122
5284
|
if (wrote) {
|
|
5123
5285
|
lastWritten = {
|
|
5124
5286
|
...flags,
|
|
5287
|
+
secretPathsKey,
|
|
5125
5288
|
legacyFingerprint: legacyMemoryFingerprint(managedDir, dataDir)
|
|
5126
5289
|
};
|
|
5127
5290
|
}
|
|
@@ -5148,7 +5311,7 @@ function installWarningFilter(proc = process) {
|
|
|
5148
5311
|
|
|
5149
5312
|
// src/index.ts
|
|
5150
5313
|
if (process.argv.includes("--version")) {
|
|
5151
|
-
const pkg = JSON.parse(
|
|
5314
|
+
const pkg = JSON.parse(readFileSync11(join9(dirname7(dirname7(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
|
|
5152
5315
|
console.log(pkg.version);
|
|
5153
5316
|
process.exit(0);
|
|
5154
5317
|
}
|
|
@@ -5163,7 +5326,7 @@ function makeLogger(cfg) {
|
|
|
5163
5326
|
function realLlmSourceDeps() {
|
|
5164
5327
|
return {
|
|
5165
5328
|
home: homedir4(),
|
|
5166
|
-
readFile: (p) =>
|
|
5329
|
+
readFile: (p) => readFileSync11(p, "utf8"),
|
|
5167
5330
|
statFile: (p) => ({ mode: statSync3(p).mode }),
|
|
5168
5331
|
exists: (p) => existsSync6(p)
|
|
5169
5332
|
};
|
|
@@ -5192,7 +5355,7 @@ function realServiceDeps() {
|
|
|
5192
5355
|
}
|
|
5193
5356
|
function realResolveNodeDeps() {
|
|
5194
5357
|
return {
|
|
5195
|
-
realpath: (p) =>
|
|
5358
|
+
realpath: (p) => realpathSync2(p),
|
|
5196
5359
|
fileExists: (p) => existsSync6(p),
|
|
5197
5360
|
nodeMajorOf: (p) => {
|
|
5198
5361
|
const r = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
@@ -5288,7 +5451,7 @@ async function main() {
|
|
|
5288
5451
|
process.exit(2);
|
|
5289
5452
|
return;
|
|
5290
5453
|
}
|
|
5291
|
-
const ledger = new SideEffectLedger(
|
|
5454
|
+
const ledger = new SideEffectLedger(join9(cfgForLedger.dataDir, "side-effect-ledger.sqlite"));
|
|
5292
5455
|
try {
|
|
5293
5456
|
if (sideEffectsVerb === "list") {
|
|
5294
5457
|
console.log(JSON.stringify({ ambiguous: ledger.listAmbiguous() }, null, 2));
|
|
@@ -5362,7 +5525,7 @@ async function main() {
|
|
|
5362
5525
|
return;
|
|
5363
5526
|
}
|
|
5364
5527
|
const log = makeLogger(cfg);
|
|
5365
|
-
const budgetBase =
|
|
5528
|
+
const budgetBase = join9(cfg.dataDir, `llm-budget-${cfg.identityId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
5366
5529
|
const budgetOpts = {
|
|
5367
5530
|
callLimit: cfg.llmDailyCallLimit ?? DEFAULT_DAILY_CALL_LIMIT,
|
|
5368
5531
|
tokenLimit: cfg.llmDailyTokenLimit ?? DEFAULT_DAILY_TOKEN_LIMIT,
|
|
@@ -5388,20 +5551,19 @@ async function main() {
|
|
|
5388
5551
|
return mem;
|
|
5389
5552
|
};
|
|
5390
5553
|
const managedDir = managedConfigDir(cfg.identityId);
|
|
5391
|
-
const
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
};
|
|
5554
|
+
const secretPathMode = cfg.secretPathMode ?? DEFAULT_SECRET_PATH_MODE;
|
|
5555
|
+
const currentSkillsConfig = () => loadEffectiveSkillsConfig({
|
|
5556
|
+
managedDir,
|
|
5557
|
+
localPath: cfg.skillsConfigPath,
|
|
5558
|
+
dataDir: cfg.dataDir,
|
|
5559
|
+
mode: secretPathMode
|
|
5560
|
+
});
|
|
5561
|
+
const keyFilePath = (which) => searchKeyFilePath(currentSkillsConfig().search, which, which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile);
|
|
5400
5562
|
const readKeyFile = (which) => {
|
|
5401
5563
|
const path = keyFilePath(which);
|
|
5402
5564
|
if (!path) return null;
|
|
5403
5565
|
try {
|
|
5404
|
-
return
|
|
5566
|
+
return readFileSync11(path, "utf8").trim() || null;
|
|
5405
5567
|
} catch (e) {
|
|
5406
5568
|
log("warn", "runtime.search_key_unreadable", { provider_brave: which === "brave" }, e);
|
|
5407
5569
|
return null;
|
|
@@ -5411,7 +5573,7 @@ async function main() {
|
|
|
5411
5573
|
const reportSearchKeyGate = searchKeyGateReporter(log, (p) => p === keyFilePath("brave"));
|
|
5412
5574
|
const hasSearchKey = () => hasReadableKeyFile(
|
|
5413
5575
|
[keyFilePath("brave"), keyFilePath("tavily")],
|
|
5414
|
-
(p) =>
|
|
5576
|
+
(p) => readFileSync11(p, "utf8"),
|
|
5415
5577
|
reportSearchKeyGate
|
|
5416
5578
|
);
|
|
5417
5579
|
const reportLlmSourceUnresolved = failureEpisodeReporter(
|
|
@@ -5429,7 +5591,11 @@ async function main() {
|
|
|
5429
5591
|
};
|
|
5430
5592
|
quarantineLegacyMemory(managedDir, cfg.dataDir);
|
|
5431
5593
|
const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
|
|
5432
|
-
reportLocalConfig({
|
|
5594
|
+
reportLocalConfig({
|
|
5595
|
+
hasSearchKey: hasSearchKey(),
|
|
5596
|
+
hasLlmKey: hasLlmKey(),
|
|
5597
|
+
secretPaths: currentSkillsConfig().secretPaths
|
|
5598
|
+
});
|
|
5433
5599
|
const builtinCtx = {
|
|
5434
5600
|
dataDir: cfg.dataDir,
|
|
5435
5601
|
configDirs,
|
|
@@ -5442,7 +5608,7 @@ async function main() {
|
|
|
5442
5608
|
now: () => /* @__PURE__ */ new Date(),
|
|
5443
5609
|
log
|
|
5444
5610
|
};
|
|
5445
|
-
const sideEffectLedger = new SideEffectLedger(
|
|
5611
|
+
const sideEffectLedger = new SideEffectLedger(join9(cfg.dataDir, "side-effect-ledger.sqlite"));
|
|
5446
5612
|
const skills = new SkillInvoker(cfg.skillsDir, builtinCtx, sideEffectLedger);
|
|
5447
5613
|
skills.loadSkills();
|
|
5448
5614
|
const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
|
|
@@ -5454,7 +5620,7 @@ async function main() {
|
|
|
5454
5620
|
const sc = currentSkillsConfig();
|
|
5455
5621
|
const g = createLocalToolGate(policyFor());
|
|
5456
5622
|
const searchKey = hasSearchKey();
|
|
5457
|
-
reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey() });
|
|
5623
|
+
reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey(), secretPaths: sc.secretPaths });
|
|
5458
5624
|
return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
|
|
5459
5625
|
};
|
|
5460
5626
|
const unavailableCount = skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey())).length;
|