@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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/bin.js +291 -118
  3. 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 readFileSync10 } from "node:fs";
4
+ import { readFileSync as readFileSync11 } from "node:fs";
5
5
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6
- import { dirname as dirname7, join as join8 } from "node:path";
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 join(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
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") || join(homedir(), ".securecoms", "agent-runtime");
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") ?? join(dataDir, "skills.json"),
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 isAbsolute(p) ? p : resolve(p);
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 join2 } from "node:path";
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 = join2(deps.home, ".securecoms", "runtime-config", cfg.identityId);
1569
- const sourcePath = join2(managedDir, "llm-source.json");
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 = join2(managedDir, `llm.key.${slug}`);
1609
- const hostPath = join2(managedDir, `llm.key.${slug}.host`);
1610
- const legacyKey = join2(managedDir, "llm.key");
1611
- const legacyHost = join2(managedDir, "llm.key.host");
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 readFileSync6, readdirSync, existsSync as existsSync3, statSync as statSync2 } from "node:fs";
1736
- import { join as join4 } from "node:path";
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 readFileSync3 } from "node:fs";
1911
- import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
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 join3 } from "node:path";
2015
+ import { join as join4 } from "node:path";
1917
2016
  function managedConfigDir(identityId, home = homedir2()) {
1918
- return join3(home, ".securecoms", "runtime-config", identityId);
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 = join3(dir, name);
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) => isAbsolute2(p) ? p : resolve2(dataDir, p);
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(readFileSync3(path, "utf8"));
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 e.headersFile === "string" ? { headersFile: abs2(e.headersFile, dataDir) } : {}
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 !== "") databases[name] = { sqlite: abs2(d.sqlite, dataDir) };
1965
- else if (typeof d?.postgresUrlFile === "string" && d.postgresUrlFile !== "") {
1966
- databases[name] = { postgresUrlFile: abs2(d.postgresUrlFile, dataDir) };
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
- search = { provider: s.provider, keyFile: abs2(s.keyFile, dataDir) };
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 { endpoints, databases, flags, ...tools ? { tools } : {}, ...search ? { search } : {} };
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, resolve3, signal) {
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 resolve3(host, signal);
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 readFileSync4 } from "node:fs";
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(readFileSync4(endpoint.headersFile, "utf8")))) {
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 readFileSync5 } from "node:fs";
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((resolve3, reject) => {
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) resolve3(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
- readFileSync5(db.postgresUrlFile, "utf8").trim(),
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(join4(this.skillsDir, entry.name));
3323
+ flat.push(join5(this.skillsDir, entry.name));
3189
3324
  } else if (entry.isDirectory()) {
3190
- const m = join4(this.skillsDir, entry.name, "manifest.json");
3325
+ const m = join5(this.skillsDir, entry.name, "manifest.json");
3191
3326
  if (existsSync3(m) && statSync2(m).isFile()) manifests.push(m);
3192
- const s = join4(this.skillsDir, entry.name, "SKILL.md");
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(readFileSync6(path, "utf-8"), path), path);
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 = readFileSync6(path, "utf-8");
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: resolve3, relative } = await import("node:path");
3340
- const resolved = resolve3(this.skillsDir, scriptPath);
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(resolve3(this.skillsDir))) {
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: realpathSync2, existsSync: fsExistsSync } = await import("node:fs");
3480
+ const { realpathSync: realpathSync3, existsSync: fsExistsSync } = await import("node:fs");
3346
3481
  if (fsExistsSync(resolved)) {
3347
- const realResolved = realpathSync2(resolved);
3348
- const realSkillsDir = realpathSync2(this.skillsDir);
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 readFileSync7 } from "node:fs";
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 join5 } from "node:path";
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 ?? join5(homedir3(), ".securecoms", "connections");
3673
- this.connectionPath = join5(dir, `${cfg.identityId}.json`);
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((resolve3) => {
3818
+ this.sleep = cfg.sleep ?? ((ms2) => new Promise((resolve4) => {
3679
3819
  this.sleepTimer = setTimeout(() => {
3680
3820
  this.sleepTimer = null;
3681
- resolve3();
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((resolve3) => {
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
- resolve3();
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(readFileSync7(this.connectionPath, "utf8"));
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 = readFileSync7(conn.token_file, "utf8").trim();
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((resolve3, reject) => {
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) resolve3();
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({ type: "hello", features: ["nack"] }));
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((resolve3, reject) => {
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
- resolve3();
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((resolve3) => {
4252
- release = resolve3;
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((resolve3, reject) => {
4427
+ return new Promise((resolve4, reject) => {
4286
4428
  const grant = () => {
4287
4429
  signal?.removeEventListener("abort", onAbort);
4288
4430
  activeTurns++;
4289
- resolve3(release);
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((resolve3) => {
4432
- onIdle = resolve3;
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 readFileSync9, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
4863
- import { dirname as dirname6, join as join7 } from "node:path";
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 readFileSync8, readdirSync as readdirSync2, renameSync as renameSync2 } from "node:fs";
4869
- import { join as join6 } from "node:path";
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 = readFileSync8(path);
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 = join6(dir, name);
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 = join6(dataDir, "channels");
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 = join6(channelsDir, channelId);
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(join6(channelDir, name), kind, channelId);
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(join6(dir, name));
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 = join6(dataDir, "channels");
5126
+ const channelsDir = join7(dataDir, "channels");
4985
5127
  for (const channelId of readdirSync2(channelsDir).sort()) {
4986
- add(join6(channelsDir, channelId, "conversation-history.json"));
4987
- add(join6(channelsDir, channelId, "memories.json"));
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 = join6(dir, QUARANTINE_MEMORY_FILE);
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 = join6(dir, `MEMORY.quarantined.${n}.md`);
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 = join6(dir, MEMORY_FILE);
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 join7(dirname6(fileURLToPath2(import.meta.url)), "..", "persona");
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 = readFileSync9(path, "utf8");
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 = join7(dataDir, file);
5233
+ const path2 = join8(dataDir, file);
5069
5234
  if (!existsSync5(path2)) continue;
5070
5235
  try {
5071
- persona[file] = readFileSync9(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
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 = join7(dataDir, file);
5272
+ const target = join8(dataDir, file);
5105
5273
  if (exists(target)) {
5106
5274
  skipped.push(target);
5107
5275
  continue;
5108
5276
  }
5109
- writeFile(target, readFileSync9(join7(defaultsDir, file), "utf8"));
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
- if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey && lastWritten.legacyFingerprint === legacyFingerprint) {
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(readFileSync10(join8(dirname7(dirname7(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
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) => readFileSync10(p, "utf8"),
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) => realpathSync(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(join8(cfgForLedger.dataDir, "side-effect-ledger.sqlite"));
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 = join8(cfg.dataDir, `llm-budget-${cfg.identityId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
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 currentSkillsConfig = () => {
5392
- const managed = findConfigFile("skills.json", [managedDir]);
5393
- return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
5394
- };
5395
- const keyFilePath = (which) => {
5396
- const owner = currentSkillsConfig().search;
5397
- if (owner && owner.provider === which) return owner.keyFile;
5398
- return which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
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 readFileSync10(path, "utf8").trim() || null;
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) => readFileSync10(p, "utf8"),
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({ hasSearchKey: hasSearchKey(), hasLlmKey: hasLlmKey() });
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(join8(cfg.dataDir, "side-effect-ledger.sqlite"));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-runtime",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"