@securecoms/agent-runtime 0.9.7 → 0.9.9
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 +291 -118
- 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) {
|
|
@@ -3564,6 +3699,10 @@ var NO_TURN_AUTHORIZATION = Object.freeze({
|
|
|
3564
3699
|
sender_membership: "unknown",
|
|
3565
3700
|
tool_grants: Object.freeze([])
|
|
3566
3701
|
});
|
|
3702
|
+
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
3703
|
+
function runtimeHelloFrame() {
|
|
3704
|
+
return { type: "hello", features: ["nack", PER_TOOL_GRANTS_FEATURE] };
|
|
3705
|
+
}
|
|
3567
3706
|
function parseTurnAuthorization(value) {
|
|
3568
3707
|
if (!value || typeof value !== "object" || Array.isArray(value)) return NO_TURN_AUTHORIZATION;
|
|
3569
3708
|
const raw = value;
|
|
@@ -3591,7 +3730,8 @@ function parseTurnAuthorization(value) {
|
|
|
3591
3730
|
version: 1,
|
|
3592
3731
|
audience: raw.audience,
|
|
3593
3732
|
sender_membership: raw.sender_membership,
|
|
3594
|
-
tool_grants: Object.freeze(grants)
|
|
3733
|
+
tool_grants: Object.freeze(grants),
|
|
3734
|
+
...raw.relay_speaker === "owner" ? { relay_speaker: "owner" } : {}
|
|
3595
3735
|
});
|
|
3596
3736
|
}
|
|
3597
3737
|
var VOICE_GUIDANCE_MAX_CHARS = 1e3;
|
|
@@ -3669,16 +3809,16 @@ var SecureComsDataPlane = class {
|
|
|
3669
3809
|
status;
|
|
3670
3810
|
constructor(cfg, onMessage) {
|
|
3671
3811
|
this.identityId = cfg.identityId;
|
|
3672
|
-
const dir = cfg.connectionsDir ??
|
|
3673
|
-
this.connectionPath =
|
|
3812
|
+
const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
|
|
3813
|
+
this.connectionPath = join6(dir, `${cfg.identityId}.json`);
|
|
3674
3814
|
this.establishedMs = cfg.establishedMs ?? 5e3;
|
|
3675
3815
|
this.initialDelayMs = cfg.initialDelayMs ?? 250;
|
|
3676
3816
|
this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
|
|
3677
3817
|
this.log = cfg.log ?? writeServiceLog;
|
|
3678
|
-
this.sleep = cfg.sleep ?? ((ms2) => new Promise((
|
|
3818
|
+
this.sleep = cfg.sleep ?? ((ms2) => new Promise((resolve4) => {
|
|
3679
3819
|
this.sleepTimer = setTimeout(() => {
|
|
3680
3820
|
this.sleepTimer = null;
|
|
3681
|
-
|
|
3821
|
+
resolve4();
|
|
3682
3822
|
}, ms2);
|
|
3683
3823
|
}));
|
|
3684
3824
|
this.random = cfg.random ?? Math.random;
|
|
@@ -3748,13 +3888,13 @@ var SecureComsDataPlane = class {
|
|
|
3748
3888
|
/** The backoff sleep, made interruptible by stop(). */
|
|
3749
3889
|
wait(ms2) {
|
|
3750
3890
|
if (this.stopped) return Promise.resolve();
|
|
3751
|
-
return new Promise((
|
|
3891
|
+
return new Promise((resolve4) => {
|
|
3752
3892
|
let settled = false;
|
|
3753
3893
|
const finish = () => {
|
|
3754
3894
|
if (settled) return;
|
|
3755
3895
|
settled = true;
|
|
3756
3896
|
this.sleepWaiters = this.sleepWaiters.filter((w) => w !== finish);
|
|
3757
|
-
|
|
3897
|
+
resolve4();
|
|
3758
3898
|
};
|
|
3759
3899
|
this.sleepWaiters.push(finish);
|
|
3760
3900
|
void this.sleep(ms2).then(finish, finish);
|
|
@@ -3790,9 +3930,9 @@ var SecureComsDataPlane = class {
|
|
|
3790
3930
|
if (this.generation === generation) this.running = false;
|
|
3791
3931
|
}
|
|
3792
3932
|
async attemptOnce() {
|
|
3793
|
-
const conn = JSON.parse(
|
|
3933
|
+
const conn = JSON.parse(readFileSync8(this.connectionPath, "utf8"));
|
|
3794
3934
|
const httpBase = conn.bridge_url.replace(/^ws/, "http").replace(/\/v1\/ws$/, "");
|
|
3795
|
-
const token =
|
|
3935
|
+
const token = readFileSync8(conn.token_file, "utf8").trim();
|
|
3796
3936
|
const healthController = new AbortController();
|
|
3797
3937
|
this.healthController = healthController;
|
|
3798
3938
|
const healthTimer = setTimeout(
|
|
@@ -3817,12 +3957,12 @@ var SecureComsDataPlane = class {
|
|
|
3817
3957
|
let established = false;
|
|
3818
3958
|
let settle = null;
|
|
3819
3959
|
try {
|
|
3820
|
-
await new Promise((
|
|
3960
|
+
await new Promise((resolve4, reject) => {
|
|
3821
3961
|
ws.addEventListener("error", (e) => {
|
|
3822
3962
|
reject(Object.assign(new Error("socket_error"), { cause: e }));
|
|
3823
3963
|
});
|
|
3824
3964
|
ws.addEventListener("close", (e) => {
|
|
3825
|
-
if (established)
|
|
3965
|
+
if (established) resolve4();
|
|
3826
3966
|
else {
|
|
3827
3967
|
reject(
|
|
3828
3968
|
new Error(
|
|
@@ -3834,7 +3974,7 @@ var SecureComsDataPlane = class {
|
|
|
3834
3974
|
ws.addEventListener("open", () => {
|
|
3835
3975
|
this.helloPending.add(ws);
|
|
3836
3976
|
try {
|
|
3837
|
-
ws.send(JSON.stringify(
|
|
3977
|
+
ws.send(JSON.stringify(runtimeHelloFrame()));
|
|
3838
3978
|
} catch {
|
|
3839
3979
|
}
|
|
3840
3980
|
settle = setTimeout(() => {
|
|
@@ -4144,7 +4284,9 @@ function hasReadGrant(auth, toolName) {
|
|
|
4144
4284
|
function hasSideEffectGrant(auth, toolName) {
|
|
4145
4285
|
return auth.sender_membership === "active" && auth.tool_grants.some((grant) => grant.tool === toolName && grant.mode === "side_effect");
|
|
4146
4286
|
}
|
|
4287
|
+
var ROOM_SAFE_READ_TOOLS = /* @__PURE__ */ new Set(["current_time"]);
|
|
4147
4288
|
function toolIsOffered(auth, toolName) {
|
|
4289
|
+
if (auth.audience !== "owner_dm" && !(auth.audience === "team_room" && ROOM_SAFE_READ_TOOLS.has(toolName)) && auth.relay_speaker !== "owner") return false;
|
|
4148
4290
|
if (READ_ONLY_TOOLS.has(toolName)) return hasReadGrant(auth, toolName);
|
|
4149
4291
|
if (toolName === "http_request") {
|
|
4150
4292
|
return hasReadGrant(auth, toolName) || hasSideEffectGrant(auth, toolName);
|
|
@@ -4236,20 +4378,20 @@ function buildHandler(deps) {
|
|
|
4236
4378
|
function waitUnlessAborted(gate2, signal) {
|
|
4237
4379
|
if (!signal) return gate2;
|
|
4238
4380
|
if (signal.aborted) return Promise.reject(cancelledError());
|
|
4239
|
-
return new Promise((
|
|
4381
|
+
return new Promise((resolve4, reject) => {
|
|
4240
4382
|
const onAbort = () => reject(cancelledError());
|
|
4241
4383
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
4242
4384
|
void gate2.then(() => {
|
|
4243
4385
|
signal.removeEventListener("abort", onAbort);
|
|
4244
|
-
|
|
4386
|
+
resolve4();
|
|
4245
4387
|
});
|
|
4246
4388
|
});
|
|
4247
4389
|
}
|
|
4248
4390
|
async function lockChannel(channelId, signal) {
|
|
4249
4391
|
const previous = channelTails.get(channelId) ?? Promise.resolve();
|
|
4250
4392
|
let release;
|
|
4251
|
-
const mine = new Promise((
|
|
4252
|
-
release =
|
|
4393
|
+
const mine = new Promise((resolve4) => {
|
|
4394
|
+
release = resolve4;
|
|
4253
4395
|
});
|
|
4254
4396
|
const tail = previous.then(() => mine);
|
|
4255
4397
|
channelTails.set(channelId, tail);
|
|
@@ -4282,11 +4424,11 @@ function buildHandler(deps) {
|
|
|
4282
4424
|
return Promise.resolve(release);
|
|
4283
4425
|
}
|
|
4284
4426
|
if (signal?.aborted) return Promise.reject(cancelledError());
|
|
4285
|
-
return new Promise((
|
|
4427
|
+
return new Promise((resolve4, reject) => {
|
|
4286
4428
|
const grant = () => {
|
|
4287
4429
|
signal?.removeEventListener("abort", onAbort);
|
|
4288
4430
|
activeTurns++;
|
|
4289
|
-
|
|
4431
|
+
resolve4(release);
|
|
4290
4432
|
};
|
|
4291
4433
|
const onAbort = () => {
|
|
4292
4434
|
const i = slotWaiters.indexOf(grant);
|
|
@@ -4428,8 +4570,8 @@ function buildHandler(deps) {
|
|
|
4428
4570
|
wedged = true;
|
|
4429
4571
|
log("error", "turn.runtime_wedged", { abandoned_unsettled: abandonedUnsettled });
|
|
4430
4572
|
if (deps.onWedged && !retiring) {
|
|
4431
|
-
retiring = new Promise((
|
|
4432
|
-
onIdle =
|
|
4573
|
+
retiring = new Promise((resolve4) => {
|
|
4574
|
+
onIdle = resolve4;
|
|
4433
4575
|
});
|
|
4434
4576
|
deps.onWedged(retiring);
|
|
4435
4577
|
}
|
|
@@ -4859,14 +5001,14 @@ ${recalledText}` } : null;
|
|
|
4859
5001
|
}
|
|
4860
5002
|
|
|
4861
5003
|
// src/persona.ts
|
|
4862
|
-
import { existsSync as existsSync5, readFileSync as
|
|
4863
|
-
import { dirname as dirname6, join as
|
|
5004
|
+
import { existsSync as existsSync5, readFileSync as readFileSync10, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
5005
|
+
import { dirname as dirname6, join as join8 } from "node:path";
|
|
4864
5006
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4865
5007
|
|
|
4866
5008
|
// src/long-term-memory.ts
|
|
4867
5009
|
import { createHash as createHash2 } from "node:crypto";
|
|
4868
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, lstatSync, readFileSync as
|
|
4869
|
-
import { join as
|
|
5010
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, lstatSync, readFileSync as readFileSync9, readdirSync as readdirSync2, renameSync as renameSync2 } from "node:fs";
|
|
5011
|
+
import { join as join7 } from "node:path";
|
|
4870
5012
|
var MEMORY_FILE = "MEMORY.md";
|
|
4871
5013
|
var QUARANTINE_MEMORY_FILE = "MEMORY.quarantined.md";
|
|
4872
5014
|
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
@@ -4903,7 +5045,7 @@ function readLegacySource(path, kind, contentBudget, channelId) {
|
|
|
4903
5045
|
};
|
|
4904
5046
|
}
|
|
4905
5047
|
if (st.size > contentBudget) return null;
|
|
4906
|
-
const bytes =
|
|
5048
|
+
const bytes = readFileSync9(path);
|
|
4907
5049
|
if (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget) return null;
|
|
4908
5050
|
return {
|
|
4909
5051
|
id: sourceId(kind, channelId, path, bytes),
|
|
@@ -4938,13 +5080,13 @@ function collectLegacyMemorySources(managedDir, dataDir) {
|
|
|
4938
5080
|
}
|
|
4939
5081
|
for (const name of names.sort(quarantineOrder)) {
|
|
4940
5082
|
if (!/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) continue;
|
|
4941
|
-
const path =
|
|
5083
|
+
const path = join7(dir, name);
|
|
4942
5084
|
if (seen.has(path)) continue;
|
|
4943
5085
|
seen.add(path);
|
|
4944
5086
|
add(path, "global-memory");
|
|
4945
5087
|
}
|
|
4946
5088
|
}
|
|
4947
|
-
const channelsDir =
|
|
5089
|
+
const channelsDir = join7(dataDir, "channels");
|
|
4948
5090
|
let channelNames;
|
|
4949
5091
|
try {
|
|
4950
5092
|
channelNames = readdirSync2(channelsDir);
|
|
@@ -4952,12 +5094,12 @@ function collectLegacyMemorySources(managedDir, dataDir) {
|
|
|
4952
5094
|
channelNames = [];
|
|
4953
5095
|
}
|
|
4954
5096
|
for (const channelId of channelNames.sort()) {
|
|
4955
|
-
const channelDir =
|
|
5097
|
+
const channelDir = join7(channelsDir, channelId);
|
|
4956
5098
|
for (const [name, kind] of [
|
|
4957
5099
|
["conversation-history.json", "conversation-history"],
|
|
4958
5100
|
["memories.json", "captured-memory"]
|
|
4959
5101
|
]) {
|
|
4960
|
-
add(
|
|
5102
|
+
add(join7(channelDir, name), kind, channelId);
|
|
4961
5103
|
}
|
|
4962
5104
|
}
|
|
4963
5105
|
return sources;
|
|
@@ -4975,33 +5117,33 @@ function legacyMemoryFingerprint(managedDir, dataDir) {
|
|
|
4975
5117
|
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
4976
5118
|
try {
|
|
4977
5119
|
for (const name of readdirSync2(dir).sort(quarantineOrder)) {
|
|
4978
|
-
if (/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) add(
|
|
5120
|
+
if (/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) add(join7(dir, name));
|
|
4979
5121
|
}
|
|
4980
5122
|
} catch {
|
|
4981
5123
|
}
|
|
4982
5124
|
}
|
|
4983
5125
|
try {
|
|
4984
|
-
const channelsDir =
|
|
5126
|
+
const channelsDir = join7(dataDir, "channels");
|
|
4985
5127
|
for (const channelId of readdirSync2(channelsDir).sort()) {
|
|
4986
|
-
add(
|
|
4987
|
-
add(
|
|
5128
|
+
add(join7(channelsDir, channelId, "conversation-history.json"));
|
|
5129
|
+
add(join7(channelsDir, channelId, "memories.json"));
|
|
4988
5130
|
}
|
|
4989
5131
|
} catch {
|
|
4990
5132
|
}
|
|
4991
5133
|
return createHash2("sha256").update(metadata.join("\n")).digest("hex");
|
|
4992
5134
|
}
|
|
4993
5135
|
function nextQuarantinePath(dir) {
|
|
4994
|
-
const first =
|
|
5136
|
+
const first = join7(dir, QUARANTINE_MEMORY_FILE);
|
|
4995
5137
|
if (!existsSync4(first)) return first;
|
|
4996
5138
|
for (let n = 1; ; n++) {
|
|
4997
|
-
const candidate =
|
|
5139
|
+
const candidate = join7(dir, `MEMORY.quarantined.${n}.md`);
|
|
4998
5140
|
if (!existsSync4(candidate)) return candidate;
|
|
4999
5141
|
}
|
|
5000
5142
|
}
|
|
5001
5143
|
function quarantineLegacyMemory(managedDir, dataDir) {
|
|
5002
5144
|
const moved = [];
|
|
5003
5145
|
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
5004
|
-
const source =
|
|
5146
|
+
const source = join7(dir, MEMORY_FILE);
|
|
5005
5147
|
if (!existsSync4(source)) continue;
|
|
5006
5148
|
if (lstatSync(source).isSymbolicLink()) {
|
|
5007
5149
|
throw new Error(`refusing to quarantine symlinked legacy memory: ${source}`);
|
|
@@ -5029,7 +5171,7 @@ var SAFETY_PREAMBLE = [
|
|
|
5029
5171
|
"- Only use the tools you were given; do not invent capabilities."
|
|
5030
5172
|
].join("\n");
|
|
5031
5173
|
function defaultPersonaDir() {
|
|
5032
|
-
return
|
|
5174
|
+
return join8(dirname6(fileURLToPath2(import.meta.url)), "..", "persona");
|
|
5033
5175
|
}
|
|
5034
5176
|
function assemblePrompt(parts) {
|
|
5035
5177
|
return parts.map((p) => `${p.heading}
|
|
@@ -5042,7 +5184,7 @@ function loadPersona(dirs) {
|
|
|
5042
5184
|
if (!path) continue;
|
|
5043
5185
|
let body;
|
|
5044
5186
|
try {
|
|
5045
|
-
body =
|
|
5187
|
+
body = readFileSync10(path, "utf8");
|
|
5046
5188
|
} catch (error) {
|
|
5047
5189
|
writeServiceLog("warn", "persona.read_failed", {}, error);
|
|
5048
5190
|
continue;
|
|
@@ -5061,17 +5203,42 @@ function systemPromptFor(dirs, fallback) {
|
|
|
5061
5203
|
|
|
5062
5204
|
${SAFETY_PREAMBLE}`;
|
|
5063
5205
|
}
|
|
5206
|
+
function secretPathsReport(s) {
|
|
5207
|
+
return {
|
|
5208
|
+
mode: s.mode,
|
|
5209
|
+
inside: s.inside,
|
|
5210
|
+
allowlisted: s.allowlisted,
|
|
5211
|
+
outside: s.outside,
|
|
5212
|
+
outside_by_field: {
|
|
5213
|
+
headers_file: s.outsideByField.headersFile,
|
|
5214
|
+
postgres_url_file: s.outsideByField.postgresUrlFile,
|
|
5215
|
+
search_key_file: s.outsideByField.searchKeyFile,
|
|
5216
|
+
sqlite: s.outsideByField.sqlite
|
|
5217
|
+
}
|
|
5218
|
+
};
|
|
5219
|
+
}
|
|
5220
|
+
function existingAllowlist(path) {
|
|
5221
|
+
try {
|
|
5222
|
+
const doc = JSON.parse(readFileSync10(path, "utf8"));
|
|
5223
|
+
const list = doc?.[ALLOWLIST_KEY];
|
|
5224
|
+
return Array.isArray(list) ? list : void 0;
|
|
5225
|
+
} catch {
|
|
5226
|
+
return void 0;
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5064
5229
|
function writeLocalConfigReport(managedDir, dataDir, now, flags) {
|
|
5065
5230
|
if (!existsSync5(managedDir)) return false;
|
|
5066
5231
|
const persona = {};
|
|
5067
5232
|
for (const { file } of PERSONA_SOURCES) {
|
|
5068
|
-
const path2 =
|
|
5233
|
+
const path2 = join8(dataDir, file);
|
|
5069
5234
|
if (!existsSync5(path2)) continue;
|
|
5070
5235
|
try {
|
|
5071
|
-
persona[file] =
|
|
5236
|
+
persona[file] = readFileSync10(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
|
|
5072
5237
|
} catch {
|
|
5073
5238
|
}
|
|
5074
5239
|
}
|
|
5240
|
+
const path = join8(managedDir, "local-config.json");
|
|
5241
|
+
const allowlist = existingAllowlist(path);
|
|
5075
5242
|
const doc = {
|
|
5076
5243
|
schema: "securecoms.local-config/2",
|
|
5077
5244
|
written_at: now.toISOString(),
|
|
@@ -5080,9 +5247,10 @@ function writeLocalConfigReport(managedDir, dataDir, now, flags) {
|
|
|
5080
5247
|
persona,
|
|
5081
5248
|
// Paths stay in this owner-only local handoff file. The daemon validates
|
|
5082
5249
|
// each source again and strips `path` before reporting anything upstream.
|
|
5083
|
-
legacy_memory: collectLegacyMemorySources(managedDir, dataDir)
|
|
5250
|
+
legacy_memory: collectLegacyMemorySources(managedDir, dataDir),
|
|
5251
|
+
...flags.secretPaths ? { secret_paths: secretPathsReport(flags.secretPaths) } : {},
|
|
5252
|
+
...allowlist ? { [ALLOWLIST_KEY]: allowlist } : {}
|
|
5084
5253
|
};
|
|
5085
|
-
const path = join7(managedDir, "local-config.json");
|
|
5086
5254
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
5087
5255
|
try {
|
|
5088
5256
|
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
@@ -5101,12 +5269,12 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
|
5101
5269
|
const written = [];
|
|
5102
5270
|
const skipped = [];
|
|
5103
5271
|
for (const { file } of PERSONA_SOURCES) {
|
|
5104
|
-
const target =
|
|
5272
|
+
const target = join8(dataDir, file);
|
|
5105
5273
|
if (exists(target)) {
|
|
5106
5274
|
skipped.push(target);
|
|
5107
5275
|
continue;
|
|
5108
5276
|
}
|
|
5109
|
-
writeFile(target,
|
|
5277
|
+
writeFile(target, readFileSync10(join8(defaultsDir, file), "utf8"));
|
|
5110
5278
|
written.push(target);
|
|
5111
5279
|
}
|
|
5112
5280
|
return { written, skipped };
|
|
@@ -5115,13 +5283,15 @@ function createLocalConfigReporter(managedDir, dataDir, now) {
|
|
|
5115
5283
|
let lastWritten = null;
|
|
5116
5284
|
return (flags) => {
|
|
5117
5285
|
const legacyFingerprint = legacyMemoryFingerprint(managedDir, dataDir);
|
|
5118
|
-
|
|
5286
|
+
const secretPathsKey = JSON.stringify(flags.secretPaths ?? null);
|
|
5287
|
+
if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey && lastWritten.secretPathsKey === secretPathsKey && lastWritten.legacyFingerprint === legacyFingerprint) {
|
|
5119
5288
|
return false;
|
|
5120
5289
|
}
|
|
5121
5290
|
const wrote = writeLocalConfigReport(managedDir, dataDir, now(), flags);
|
|
5122
5291
|
if (wrote) {
|
|
5123
5292
|
lastWritten = {
|
|
5124
5293
|
...flags,
|
|
5294
|
+
secretPathsKey,
|
|
5125
5295
|
legacyFingerprint: legacyMemoryFingerprint(managedDir, dataDir)
|
|
5126
5296
|
};
|
|
5127
5297
|
}
|
|
@@ -5148,7 +5318,7 @@ function installWarningFilter(proc = process) {
|
|
|
5148
5318
|
|
|
5149
5319
|
// src/index.ts
|
|
5150
5320
|
if (process.argv.includes("--version")) {
|
|
5151
|
-
const pkg = JSON.parse(
|
|
5321
|
+
const pkg = JSON.parse(readFileSync11(join9(dirname7(dirname7(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
|
|
5152
5322
|
console.log(pkg.version);
|
|
5153
5323
|
process.exit(0);
|
|
5154
5324
|
}
|
|
@@ -5163,7 +5333,7 @@ function makeLogger(cfg) {
|
|
|
5163
5333
|
function realLlmSourceDeps() {
|
|
5164
5334
|
return {
|
|
5165
5335
|
home: homedir4(),
|
|
5166
|
-
readFile: (p) =>
|
|
5336
|
+
readFile: (p) => readFileSync11(p, "utf8"),
|
|
5167
5337
|
statFile: (p) => ({ mode: statSync3(p).mode }),
|
|
5168
5338
|
exists: (p) => existsSync6(p)
|
|
5169
5339
|
};
|
|
@@ -5192,7 +5362,7 @@ function realServiceDeps() {
|
|
|
5192
5362
|
}
|
|
5193
5363
|
function realResolveNodeDeps() {
|
|
5194
5364
|
return {
|
|
5195
|
-
realpath: (p) =>
|
|
5365
|
+
realpath: (p) => realpathSync2(p),
|
|
5196
5366
|
fileExists: (p) => existsSync6(p),
|
|
5197
5367
|
nodeMajorOf: (p) => {
|
|
5198
5368
|
const r = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
@@ -5288,7 +5458,7 @@ async function main() {
|
|
|
5288
5458
|
process.exit(2);
|
|
5289
5459
|
return;
|
|
5290
5460
|
}
|
|
5291
|
-
const ledger = new SideEffectLedger(
|
|
5461
|
+
const ledger = new SideEffectLedger(join9(cfgForLedger.dataDir, "side-effect-ledger.sqlite"));
|
|
5292
5462
|
try {
|
|
5293
5463
|
if (sideEffectsVerb === "list") {
|
|
5294
5464
|
console.log(JSON.stringify({ ambiguous: ledger.listAmbiguous() }, null, 2));
|
|
@@ -5362,7 +5532,7 @@ async function main() {
|
|
|
5362
5532
|
return;
|
|
5363
5533
|
}
|
|
5364
5534
|
const log = makeLogger(cfg);
|
|
5365
|
-
const budgetBase =
|
|
5535
|
+
const budgetBase = join9(cfg.dataDir, `llm-budget-${cfg.identityId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
5366
5536
|
const budgetOpts = {
|
|
5367
5537
|
callLimit: cfg.llmDailyCallLimit ?? DEFAULT_DAILY_CALL_LIMIT,
|
|
5368
5538
|
tokenLimit: cfg.llmDailyTokenLimit ?? DEFAULT_DAILY_TOKEN_LIMIT,
|
|
@@ -5388,20 +5558,19 @@ async function main() {
|
|
|
5388
5558
|
return mem;
|
|
5389
5559
|
};
|
|
5390
5560
|
const managedDir = managedConfigDir(cfg.identityId);
|
|
5391
|
-
const
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
};
|
|
5561
|
+
const secretPathMode = cfg.secretPathMode ?? DEFAULT_SECRET_PATH_MODE;
|
|
5562
|
+
const currentSkillsConfig = () => loadEffectiveSkillsConfig({
|
|
5563
|
+
managedDir,
|
|
5564
|
+
localPath: cfg.skillsConfigPath,
|
|
5565
|
+
dataDir: cfg.dataDir,
|
|
5566
|
+
mode: secretPathMode
|
|
5567
|
+
});
|
|
5568
|
+
const keyFilePath = (which) => searchKeyFilePath(currentSkillsConfig().search, which, which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile);
|
|
5400
5569
|
const readKeyFile = (which) => {
|
|
5401
5570
|
const path = keyFilePath(which);
|
|
5402
5571
|
if (!path) return null;
|
|
5403
5572
|
try {
|
|
5404
|
-
return
|
|
5573
|
+
return readFileSync11(path, "utf8").trim() || null;
|
|
5405
5574
|
} catch (e) {
|
|
5406
5575
|
log("warn", "runtime.search_key_unreadable", { provider_brave: which === "brave" }, e);
|
|
5407
5576
|
return null;
|
|
@@ -5411,7 +5580,7 @@ async function main() {
|
|
|
5411
5580
|
const reportSearchKeyGate = searchKeyGateReporter(log, (p) => p === keyFilePath("brave"));
|
|
5412
5581
|
const hasSearchKey = () => hasReadableKeyFile(
|
|
5413
5582
|
[keyFilePath("brave"), keyFilePath("tavily")],
|
|
5414
|
-
(p) =>
|
|
5583
|
+
(p) => readFileSync11(p, "utf8"),
|
|
5415
5584
|
reportSearchKeyGate
|
|
5416
5585
|
);
|
|
5417
5586
|
const reportLlmSourceUnresolved = failureEpisodeReporter(
|
|
@@ -5429,7 +5598,11 @@ async function main() {
|
|
|
5429
5598
|
};
|
|
5430
5599
|
quarantineLegacyMemory(managedDir, cfg.dataDir);
|
|
5431
5600
|
const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
|
|
5432
|
-
reportLocalConfig({
|
|
5601
|
+
reportLocalConfig({
|
|
5602
|
+
hasSearchKey: hasSearchKey(),
|
|
5603
|
+
hasLlmKey: hasLlmKey(),
|
|
5604
|
+
secretPaths: currentSkillsConfig().secretPaths
|
|
5605
|
+
});
|
|
5433
5606
|
const builtinCtx = {
|
|
5434
5607
|
dataDir: cfg.dataDir,
|
|
5435
5608
|
configDirs,
|
|
@@ -5442,7 +5615,7 @@ async function main() {
|
|
|
5442
5615
|
now: () => /* @__PURE__ */ new Date(),
|
|
5443
5616
|
log
|
|
5444
5617
|
};
|
|
5445
|
-
const sideEffectLedger = new SideEffectLedger(
|
|
5618
|
+
const sideEffectLedger = new SideEffectLedger(join9(cfg.dataDir, "side-effect-ledger.sqlite"));
|
|
5446
5619
|
const skills = new SkillInvoker(cfg.skillsDir, builtinCtx, sideEffectLedger);
|
|
5447
5620
|
skills.loadSkills();
|
|
5448
5621
|
const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
|
|
@@ -5454,7 +5627,7 @@ async function main() {
|
|
|
5454
5627
|
const sc = currentSkillsConfig();
|
|
5455
5628
|
const g = createLocalToolGate(policyFor());
|
|
5456
5629
|
const searchKey = hasSearchKey();
|
|
5457
|
-
reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey() });
|
|
5630
|
+
reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey(), secretPaths: sc.secretPaths });
|
|
5458
5631
|
return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
|
|
5459
5632
|
};
|
|
5460
5633
|
const unavailableCount = skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey())).length;
|