@securecoms/agent-runtime 0.2.0 → 0.4.0

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/PROVENANCE.md CHANGED
@@ -35,3 +35,12 @@ recognisable here. **No code was cloned from any of them.** The frontmatter
35
35
  parser in `src/skill-md.ts` is original and deliberately accepts only the
36
36
  documented subset; the built-ins are written against this package's own
37
37
  `BuiltinContext`.
38
+
39
+ ## Managed configuration directory (2026-09-09)
40
+
41
+ `src/config-dirs.ts` and the search-path form of the persona, memory and
42
+ skills.json readers are original to this package. The convention
43
+ `~/.securecoms/runtime-config/<identity-id>/` is SecureComs' own: the runtime
44
+ computes the path from the home directory and its own identity rather than
45
+ being told it, so no path crosses a process boundary and nothing can redirect
46
+ a configuration read. No code was cloned for this round.
package/README.md CHANGED
@@ -233,13 +233,20 @@ fresh on every `http_request` call — never cached, never logged. A Postgres
233
233
  database needs the optional `pg` package installed separately
234
234
  (`npm i -g pg`); SQLite needs nothing extra (`node:sqlite`).
235
235
 
236
+ `skills.json` may also carry a `tools` block — `{"tools": {"allow": [...],
237
+ "deny": [...]}}` — the owner's on/off choices for the curated skills. A skill
238
+ that is switched off is not offered to the model at all. The environment wins:
239
+ see "Managed configuration" below.
240
+
236
241
  By default `skills.json` lives at `<dataDir>/skills.json`; point elsewhere
237
- with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A missing file
238
- means no endpoints, no databases and no flags — not an error.
242
+ with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A managed copy
243
+ (see below) wins over both. A missing file means no endpoints, no databases
244
+ and no flags — not an error.
239
245
 
240
246
  ## Persona and memory
241
247
 
242
- Three optional files in the data dir make up an agent's persona, read fresh
248
+ Three optional files make up an agent's persona, looked up along the config
249
+ search path (see "Managed configuration" below) and read fresh
243
250
  on **every turn** — no restart needed after an edit:
244
251
 
245
252
  | File | Heading |
@@ -267,6 +274,62 @@ channel. It is capped at 8 kB; once it overflows, the **oldest** lines are
267
274
  dropped first, on the theory that the newest remembered facts are the ones
268
275
  still likely to be true.
269
276
 
277
+ ## Managed configuration (from the web app)
278
+
279
+ When an agent is managed from the SecureComs web app, the owner's saved
280
+ configuration is written to disk by the customer's daemon at:
281
+
282
+ ```
283
+ ~/.securecoms/runtime-config/<identity-id>/
284
+ ```
285
+
286
+ The runtime reads its configuration along a **search path**: that managed
287
+ directory first, then the data dir. The lookup is per FILE, so an owner who
288
+ saved only `IDENTITY.md` in the app keeps the machine's own `SOUL.md`; a file
289
+ with no managed copy still comes from the data dir, and an agent with no
290
+ managed directory at all behaves exactly as it did before. `skills.json` is
291
+ looked up the same way, and re-read on every turn — a new endpoint or a skill
292
+ switched off in the app takes effect on the next message, with no restart.
293
+
294
+ Two deliberate exceptions:
295
+
296
+ - **The environment still wins for the tool policy.** `SECURECOMS_TOOL_ALLOW`
297
+ overrides the `allow` list in the `tools` block of `skills.json`, so an
298
+ install that pins its allow list in its service unit is unaffected by a save
299
+ in the app. `SECURECOMS_TOOL_DENY` is UNIONED with the file's `deny` list
300
+ rather than replacing it: deny is a safety mechanism, so one entry in the
301
+ service unit must not silently un-deny every tool the owner switched off in
302
+ the app. Everything the environment denies stays denied either way.
303
+ - **Writes go to the data dir**, not the managed directory — except `MEMORY.md`,
304
+ which is written wherever the agent already keeps it. An existing
305
+ `<dataDir>/MEMORY.md` is copied into the managed directory once, at startup,
306
+ so the owner can read and prune it in the app.
307
+
308
+ The runtime also writes `local-config.json` into the managed directory when one
309
+ exists: what this agent's OWN persona files say, and a single boolean for
310
+ whether it has a readable search key. It is written at startup and rewritten
311
+ whenever the search-key answer changes, so a key file dropped in (or taken
312
+ away) mid-run is reported without a restart. It is a report the daemon passes
313
+ upward — nothing in the runtime reads it back, and it holds no key value.
314
+
315
+ ### A known limitation: the generated tool paragraph
316
+
317
+ The `AGENTS.md` the app delivers ends in a generated paragraph naming the tools
318
+ this agent has. The backend derives that list from the saved configuration plus
319
+ the search-key boolean above; it does not see the running process. Three things
320
+ can therefore make it diverge from what the runtime actually offers:
321
+
322
+ - a `SECURECOMS_TOOL_ALLOW` or `SECURECOMS_TOOL_DENY` set in the service unit,
323
+ which the backend cannot see;
324
+ - a customer skill folder whose `requires:` is narrower than the shipped one, so
325
+ the runtime withholds a tool the backend counted;
326
+ - a custom skill the backend knows nothing about, so it is offered but unnamed.
327
+
328
+ In any of those cases the paragraph is advisory rather than exact. The gate is
329
+ still authoritative — a tool named in the paragraph but denied by policy is
330
+ still refused at call time — so the risk is a wrong claim in conversation, not
331
+ a wrong permission.
332
+
270
333
  ## Provenance
271
334
 
272
335
  See [PROVENANCE.md](./PROVENANCE.md) for which files were ported from
package/dist/bin.js CHANGED
@@ -3,10 +3,10 @@
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync9 } from "node:fs";
5
5
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6
- import { dirname as dirname4, join as join7 } from "node:path";
6
+ import { dirname as dirname4, join as join8 } from "node:path";
7
7
  import { spawnSync } from "node:child_process";
8
- import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
9
- import { homedir as homedir3, userInfo } from "node:os";
8
+ import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
9
+ import { homedir as homedir4, userInfo } from "node:os";
10
10
 
11
11
  // src/config.ts
12
12
  import { homedir } from "node:os";
@@ -95,7 +95,11 @@ function loadConfig(argv, env) {
95
95
  dataDir,
96
96
  skillsConfigPath: parseStringFlag(argv, "--skills-config", env, "SECURECOMS_SKILLS_CONFIG") ?? join(dataDir, "skills.json"),
97
97
  logLevel,
98
- toolGate
98
+ toolGate,
99
+ toolGateEnv: {
100
+ ...parsedAllow !== void 0 ? { allow: parsedAllow } : {},
101
+ ...deny !== void 0 ? { deny } : {}
102
+ }
99
103
  };
100
104
  }
101
105
 
@@ -604,8 +608,8 @@ ${memories}`;
604
608
  };
605
609
 
606
610
  // src/skill-invoker.ts
607
- import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync4, statSync } from "node:fs";
608
- import { join as join4 } from "node:path";
611
+ import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync5, statSync } from "node:fs";
612
+ import { join as join5 } from "node:path";
609
613
 
610
614
  // src/skill-md.ts
611
615
  var SkillMdError = class extends Error {
@@ -760,13 +764,33 @@ function resolveRequires(req, env) {
760
764
  }
761
765
 
762
766
  // src/skills-config.ts
763
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
767
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
764
768
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
769
+
770
+ // src/config-dirs.ts
771
+ import { existsSync as existsSync2 } from "node:fs";
772
+ import { homedir as homedir2 } from "node:os";
773
+ import { join as join3 } from "node:path";
774
+ function managedConfigDir(identityId, home = homedir2()) {
775
+ return join3(home, ".securecoms", "runtime-config", identityId);
776
+ }
777
+ function configSearchPath(identityId, dataDir, home = homedir2()) {
778
+ return [managedConfigDir(identityId, home), dataDir];
779
+ }
780
+ function findConfigFile(name, dirs) {
781
+ for (const dir of dirs) {
782
+ const path = join3(dir, name);
783
+ if (existsSync2(path)) return path;
784
+ }
785
+ return null;
786
+ }
787
+
788
+ // src/skills-config.ts
765
789
  var EMPTY_SKILLS_CONFIG = { endpoints: {}, databases: {}, flags: [] };
766
790
  var empty = () => ({ endpoints: {}, databases: {}, flags: [] });
767
791
  var abs2 = (p, dataDir) => isAbsolute2(p) ? p : resolve2(dataDir, p);
768
- function loadSkillsConfig(path, dataDir) {
769
- if (!existsSync2(path)) return empty();
792
+ function loadSkillsConfigFile(path, dataDir) {
793
+ if (!existsSync3(path)) return empty();
770
794
  let raw;
771
795
  try {
772
796
  raw = JSON.parse(readFileSync2(path, "utf8"));
@@ -800,7 +824,24 @@ function loadSkillsConfig(path, dataDir) {
800
824
  } else console.warn(`[skills-config] database "${name}" in ${path} needs sqlite or postgresUrlFile; skipped`);
801
825
  }
802
826
  const flags = Array.isArray(src.flags) ? src.flags.filter((f) => typeof f === "string") : [];
803
- return { endpoints, databases, flags };
827
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
828
+ const rawTools = src.tools ?? void 0;
829
+ const tools = rawTools ? {
830
+ ...strings(rawTools.allow) ? { allow: strings(rawTools.allow) } : {},
831
+ ...strings(rawTools.deny) ? { deny: strings(rawTools.deny) } : {}
832
+ } : void 0;
833
+ let search;
834
+ const s = src.search;
835
+ if (s && typeof s === "object") {
836
+ if (s.provider !== "brave" && s.provider !== "tavily") {
837
+ console.warn(`[skills-config] search in ${path} has an unknown provider; skipped`);
838
+ } else if (typeof s.keyFile !== "string" || s.keyFile === "") {
839
+ console.warn(`[skills-config] search in ${path} has no keyFile; skipped`);
840
+ } else {
841
+ search = { provider: s.provider, keyFile: abs2(s.keyFile, dataDir) };
842
+ }
843
+ }
844
+ return { endpoints, databases, flags, ...tools ? { tools } : {}, ...search ? { search } : {} };
804
845
  }
805
846
 
806
847
  // src/builtins/current-time.ts
@@ -1076,33 +1117,43 @@ var httpRequest = async (args, ctx) => {
1076
1117
  };
1077
1118
 
1078
1119
  // src/long-term-memory.ts
1079
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1080
- import { join as join3 } from "node:path";
1120
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1121
+ import { join as join4 } from "node:path";
1081
1122
  var MEMORY_FILE = "MEMORY.md";
1082
1123
  var MEMORY_MAX_BYTES = 8192;
1083
1124
  var FACT_MAX_CHARS = 300;
1084
- function memoryPath(dataDir) {
1085
- return join3(dataDir, MEMORY_FILE);
1086
- }
1087
- function readLongTermMemory(dataDir) {
1088
- const p = memoryPath(dataDir);
1089
- if (!existsSync3(p)) return null;
1125
+ function readLongTermMemory(dirs) {
1126
+ const p = findConfigFile(MEMORY_FILE, dirs);
1127
+ if (!p) return null;
1090
1128
  const body = readFileSync4(p, "utf8").trim();
1091
1129
  return body || null;
1092
1130
  }
1093
- function rememberFact(dataDir, fact, now) {
1131
+ function memoryTarget(dirs) {
1132
+ for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
1133
+ for (const d of dirs) if (existsSync4(d)) return d;
1134
+ return dirs[dirs.length - 1];
1135
+ }
1136
+ function migrateMemoryIntoManaged(managedDir, dataDir) {
1137
+ const managed = join4(managedDir, MEMORY_FILE);
1138
+ const local = join4(dataDir, MEMORY_FILE);
1139
+ if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
1140
+ copyFileSync(local, managed);
1141
+ return true;
1142
+ }
1143
+ function rememberFact(dirs, fact, now) {
1094
1144
  const clean = fact.replace(/\s+/g, " ").trim().slice(0, FACT_MAX_CHARS);
1095
1145
  if (!clean) throw new Error("remember needs a non-empty fact");
1096
1146
  const line = `- ${now.toISOString().slice(0, 10)} ${clean}`;
1097
- mkdirSync2(dataDir, { recursive: true });
1098
- const existing = readLongTermMemory(dataDir);
1147
+ const dir = memoryTarget(dirs);
1148
+ mkdirSync2(dir, { recursive: true });
1149
+ const existing = readLongTermMemory(dirs);
1099
1150
  let lines = existing ? existing.split("\n") : [];
1100
1151
  lines.push(line);
1101
1152
  while (Buffer.byteLength(`${lines.join("\n")}
1102
1153
  `, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
1103
1154
  lines = lines.slice(1);
1104
1155
  }
1105
- writeFileSync2(memoryPath(dataDir), `${lines.join("\n")}
1156
+ writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
1106
1157
  `, { mode: 384 });
1107
1158
  return line;
1108
1159
  }
@@ -1110,7 +1161,7 @@ function rememberFact(dataDir, fact, now) {
1110
1161
  // src/builtins/remember.ts
1111
1162
  var remember = async (args, ctx) => {
1112
1163
  try {
1113
- return JSON.stringify({ remembered: rememberFact(ctx.dataDir, String(args.fact ?? ""), ctx.now()) });
1164
+ return JSON.stringify({ remembered: rememberFact(ctx.configDirs, String(args.fact ?? ""), ctx.now()) });
1114
1165
  } catch (e) {
1115
1166
  return toolError(e instanceof Error ? e.message : String(e));
1116
1167
  }
@@ -1314,6 +1365,7 @@ function offlineContext() {
1314
1365
  };
1315
1366
  return {
1316
1367
  dataDir: "",
1368
+ configDirs: [""],
1317
1369
  readKeyFile: () => null,
1318
1370
  fetch: refuse,
1319
1371
  resolve: async () => refuse(),
@@ -1356,19 +1408,19 @@ var SkillInvoker = class {
1356
1408
  * `<skillsDir>/<subdir>/manifest.json`.
1357
1409
  */
1358
1410
  loadSkills() {
1359
- if (!existsSync4(this.skillsDir)) {
1411
+ if (!existsSync5(this.skillsDir)) {
1360
1412
  console.warn(`[SkillInvoker] Skills directory not found: ${this.skillsDir}`);
1361
1413
  return;
1362
1414
  }
1363
1415
  const flat = [], manifests = [], skillMds = [];
1364
1416
  for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
1365
1417
  if (entry.isFile() && entry.name.endsWith(".json")) {
1366
- flat.push(join4(this.skillsDir, entry.name));
1418
+ flat.push(join5(this.skillsDir, entry.name));
1367
1419
  } else if (entry.isDirectory()) {
1368
- const m = join4(this.skillsDir, entry.name, "manifest.json");
1369
- if (existsSync4(m) && statSync(m).isFile()) manifests.push(m);
1370
- const s = join4(this.skillsDir, entry.name, "SKILL.md");
1371
- if (existsSync4(s) && statSync(s).isFile()) skillMds.push(s);
1420
+ const m = join5(this.skillsDir, entry.name, "manifest.json");
1421
+ if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
1422
+ const s = join5(this.skillsDir, entry.name, "SKILL.md");
1423
+ if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
1372
1424
  }
1373
1425
  }
1374
1426
  for (const p of [...flat.sort(), ...manifests.sort()]) this.loadManifestFile(p);
@@ -1537,12 +1589,27 @@ function createLocalToolGate(policy) {
1537
1589
  }
1538
1590
  };
1539
1591
  }
1592
+ function resolveToolPolicy(env, file, defaults) {
1593
+ const deny = env.deny || file?.deny ? [.../* @__PURE__ */ new Set([...env.deny ?? [], ...file?.deny ?? []])] : void 0;
1594
+ return {
1595
+ default: defaults.default,
1596
+ allow: env.allow ?? file?.allow ?? defaults.allow,
1597
+ ...deny ? { deny } : {}
1598
+ };
1599
+ }
1600
+ function createDynamicToolGate(policyFor) {
1601
+ return {
1602
+ gate(toolName) {
1603
+ return createLocalToolGate(policyFor()).gate(toolName);
1604
+ }
1605
+ };
1606
+ }
1540
1607
 
1541
1608
  // src/data-plane.ts
1542
1609
  import { readFileSync as readFileSync7 } from "node:fs";
1543
1610
  import { WebSocket } from "node:http";
1544
- import { homedir as homedir2 } from "node:os";
1545
- import { join as join5 } from "node:path";
1611
+ import { homedir as homedir3 } from "node:os";
1612
+ import { join as join6 } from "node:path";
1546
1613
  var SEEN_MAX = 5e3;
1547
1614
  function errText(e) {
1548
1615
  return e instanceof Error ? e.message : String(e);
@@ -1585,8 +1652,8 @@ var SecureComsDataPlane = class {
1585
1652
  generation = 0;
1586
1653
  constructor(cfg, onMessage) {
1587
1654
  this.identityId = cfg.identityId;
1588
- const dir = cfg.connectionsDir ?? join5(homedir2(), ".securecoms", "connections");
1589
- this.connectionPath = join5(dir, `${cfg.identityId}.json`);
1655
+ const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
1656
+ this.connectionPath = join6(dir, `${cfg.identityId}.json`);
1590
1657
  this.establishedMs = cfg.establishedMs ?? 5e3;
1591
1658
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
1592
1659
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
@@ -1901,8 +1968,8 @@ ${longTerm}` });
1901
1968
  }
1902
1969
 
1903
1970
  // src/persona.ts
1904
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
1905
- import { dirname as dirname3, join as join6 } from "node:path";
1971
+ import { existsSync as existsSync6, readFileSync as readFileSync8, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
1972
+ import { dirname as dirname3, join as join7 } from "node:path";
1906
1973
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1907
1974
  var PERSONA_MAX_BYTES = 8192;
1908
1975
  var PERSONA_SOURCES = [
@@ -1918,17 +1985,17 @@ var SAFETY_PREAMBLE = [
1918
1985
  "- Only use the tools you were given; do not invent capabilities."
1919
1986
  ].join("\n");
1920
1987
  function defaultPersonaDir() {
1921
- return join6(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1988
+ return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1922
1989
  }
1923
1990
  function assemblePrompt(parts) {
1924
1991
  return parts.map((p) => `${p.heading}
1925
1992
  ${p.body}`).join("\n\n");
1926
1993
  }
1927
- function loadPersona(dataDir) {
1994
+ function loadPersona(dirs) {
1928
1995
  const parts = [];
1929
1996
  for (const { file, heading } of PERSONA_SOURCES) {
1930
- const path = join6(dataDir, file);
1931
- if (!existsSync5(path)) continue;
1997
+ const path = findConfigFile(file, dirs);
1998
+ if (!path) continue;
1932
1999
  let body;
1933
2000
  try {
1934
2001
  body = readFileSync8(path, "utf8");
@@ -1945,25 +2012,66 @@ function loadPersona(dataDir) {
1945
2012
  }
1946
2013
  return parts.length ? assemblePrompt(parts) : null;
1947
2014
  }
1948
- function systemPromptFor(dataDir, fallback) {
1949
- return `${loadPersona(dataDir) ?? fallback}
2015
+ function systemPromptFor(dirs, fallback) {
2016
+ return `${loadPersona(dirs) ?? fallback}
1950
2017
 
1951
2018
  ${SAFETY_PREAMBLE}`;
1952
2019
  }
2020
+ function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2021
+ if (!existsSync6(managedDir)) return false;
2022
+ const persona = {};
2023
+ for (const { file } of PERSONA_SOURCES) {
2024
+ const path2 = join7(dataDir, file);
2025
+ if (!existsSync6(path2)) continue;
2026
+ try {
2027
+ persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
2028
+ } catch {
2029
+ }
2030
+ }
2031
+ const doc = {
2032
+ schema: "securecoms.local-config/1",
2033
+ written_at: now.toISOString(),
2034
+ has_search_key: hasSearchKey,
2035
+ persona
2036
+ };
2037
+ const path = join7(managedDir, "local-config.json");
2038
+ const tmp = `${path}.${process.pid}.tmp`;
2039
+ try {
2040
+ writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
2041
+ `, { mode: 384 });
2042
+ renameSync(tmp, path);
2043
+ } catch (e) {
2044
+ try {
2045
+ unlinkSync(tmp);
2046
+ } catch {
2047
+ }
2048
+ throw e;
2049
+ }
2050
+ return true;
2051
+ }
1953
2052
  function personaInit(dataDir, defaultsDir, writeFile, exists) {
1954
2053
  const written = [];
1955
2054
  const skipped = [];
1956
2055
  for (const { file } of PERSONA_SOURCES) {
1957
- const target = join6(dataDir, file);
2056
+ const target = join7(dataDir, file);
1958
2057
  if (exists(target)) {
1959
2058
  skipped.push(target);
1960
2059
  continue;
1961
2060
  }
1962
- writeFile(target, readFileSync8(join6(defaultsDir, file), "utf8"));
2061
+ writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
1963
2062
  written.push(target);
1964
2063
  }
1965
2064
  return { written, skipped };
1966
2065
  }
2066
+ function createLocalConfigReporter(managedDir, dataDir, now) {
2067
+ let lastWritten = null;
2068
+ return (hasSearchKey) => {
2069
+ if (lastWritten === hasSearchKey) return false;
2070
+ const wrote = writeLocalConfigReport(managedDir, dataDir, now(), hasSearchKey);
2071
+ if (wrote) lastWritten = hasSearchKey;
2072
+ return wrote;
2073
+ };
2074
+ }
1967
2075
 
1968
2076
  // src/warning-filter.ts
1969
2077
  function installWarningFilter(proc = process) {
@@ -1978,7 +2086,7 @@ function installWarningFilter(proc = process) {
1978
2086
 
1979
2087
  // src/index.ts
1980
2088
  if (process.argv.includes("--version")) {
1981
- const pkg = JSON.parse(readFileSync9(join7(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2089
+ const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
1982
2090
  console.log(pkg.version);
1983
2091
  process.exit(0);
1984
2092
  }
@@ -2009,10 +2117,10 @@ function realServiceDeps() {
2009
2117
  const r = spawnSync(cmd, args, { encoding: "utf8" });
2010
2118
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
2011
2119
  },
2012
- writeFile: (p, data) => writeFileSync3(p, data, { mode: 420 }),
2120
+ writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
2013
2121
  removeFile: (p) => rmSync(p, { force: true }),
2014
2122
  mkdirp: (p) => mkdirSync3(p, { recursive: true }),
2015
- fileExists: (p) => existsSync6(p),
2123
+ fileExists: (p) => existsSync7(p),
2016
2124
  canRead: (p) => {
2017
2125
  try {
2018
2126
  accessSync(p, constants.R_OK);
@@ -2027,7 +2135,7 @@ function realServiceDeps() {
2027
2135
  function realResolveNodeDeps() {
2028
2136
  return {
2029
2137
  realpath: (p) => realpathSync(p),
2030
- fileExists: (p) => existsSync6(p),
2138
+ fileExists: (p) => existsSync7(p),
2031
2139
  nodeMajorOf: (p) => {
2032
2140
  const r = spawnSync(p, ["--version"], { encoding: "utf8" });
2033
2141
  if (r.status !== 0) return null;
@@ -2047,7 +2155,7 @@ function runServiceVerb(verb) {
2047
2155
  console.error(`securecoms-agent: ${MISSING_IDENTITY_MESSAGE}`);
2048
2156
  process.exit(2);
2049
2157
  }
2050
- const home = homedir3();
2158
+ const home = homedir4();
2051
2159
  try {
2052
2160
  const execPath = chooseVerbExecPath(
2053
2161
  verb,
@@ -2123,13 +2231,18 @@ async function main() {
2123
2231
  const r = personaInit(
2124
2232
  cfgForPersona.dataDir,
2125
2233
  defaultPersonaDir(),
2126
- (p, d) => writeFileSync3(p, d, { mode: 384 }),
2127
- (p) => existsSync6(p)
2234
+ (p, d) => writeFileSync4(p, d, { mode: 384 }),
2235
+ (p) => existsSync7(p)
2128
2236
  );
2129
2237
  for (const p of r.written) console.log(`wrote ${p}`);
2130
2238
  for (const p of r.skipped) console.log(`kept ${p} (already exists)`);
2131
2239
  } else {
2132
- console.log(systemPromptFor(cfgForPersona.dataDir, DEFAULT_SYSTEM_PROMPT));
2240
+ console.log(
2241
+ systemPromptFor(
2242
+ configSearchPath(cfgForPersona.identityId, cfgForPersona.dataDir),
2243
+ DEFAULT_SYSTEM_PROMPT
2244
+ )
2245
+ );
2133
2246
  }
2134
2247
  process.exit(0);
2135
2248
  }
@@ -2155,13 +2268,23 @@ async function main() {
2155
2268
  const memoryFor = (channelId) => {
2156
2269
  let mem = memories.get(channelId);
2157
2270
  if (!mem) {
2158
- mem = new AgentMemory(join7(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2271
+ mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2159
2272
  memories.set(channelId, mem);
2160
2273
  }
2161
2274
  return mem;
2162
2275
  };
2276
+ const managedDir = managedConfigDir(cfg.identityId);
2277
+ const currentSkillsConfig = () => {
2278
+ const managed = findConfigFile("skills.json", [managedDir]);
2279
+ return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
2280
+ };
2281
+ const keyFilePath = (which) => {
2282
+ const owner = currentSkillsConfig().search;
2283
+ if (owner && owner.provider === which) return owner.keyFile;
2284
+ return which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
2285
+ };
2163
2286
  const readKeyFile = (which) => {
2164
- const path = which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
2287
+ const path = keyFilePath(which);
2165
2288
  if (!path) return null;
2166
2289
  try {
2167
2290
  return readFileSync9(path, "utf8").trim() || null;
@@ -2170,24 +2293,40 @@ async function main() {
2170
2293
  return null;
2171
2294
  }
2172
2295
  };
2296
+ const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
2297
+ const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) => readFileSync9(p, "utf8"));
2298
+ migrateMemoryIntoManaged(managedDir, cfg.dataDir);
2299
+ const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
2300
+ reportLocalConfig(hasSearchKey());
2173
2301
  const builtinCtx = {
2174
2302
  dataDir: cfg.dataDir,
2303
+ configDirs,
2175
2304
  readKeyFile,
2176
2305
  fetch,
2177
2306
  resolve: systemResolver(),
2178
- skillsConfig: loadSkillsConfig(cfg.skillsConfigPath, cfg.dataDir),
2307
+ get skillsConfig() {
2308
+ return currentSkillsConfig();
2309
+ },
2179
2310
  now: () => /* @__PURE__ */ new Date(),
2180
2311
  log
2181
2312
  };
2182
2313
  const skills = new SkillInvoker(cfg.skillsDir, builtinCtx);
2183
2314
  skills.loadSkills();
2184
- const skillsConfig = builtinCtx.skillsConfig;
2185
- const hasSearchKey = () => hasReadableKeyFile([cfg.braveKeyFile, cfg.tavilyKeyFile], (p) => readFileSync9(p, "utf8"));
2186
- const requiresEnv = requiresEnvFrom(skillsConfig, hasSearchKey());
2187
- for (const held of skills.unavailable(requiresEnv)) {
2315
+ const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
2316
+ default: cfg.toolGate.default,
2317
+ allow: DEFAULT_TOOL_ALLOW
2318
+ });
2319
+ const gate = createDynamicToolGate(policyFor);
2320
+ const offeredTools = () => {
2321
+ const sc = currentSkillsConfig();
2322
+ const g = createLocalToolGate(policyFor());
2323
+ const searchKey = hasSearchKey();
2324
+ reportLocalConfig(searchKey);
2325
+ return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
2326
+ };
2327
+ for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
2188
2328
  console.log(`unavailable: ${held.name} (missing ${held.missing.join(", ")})`);
2189
2329
  }
2190
- const gate = createLocalToolGate(cfg.toolGate);
2191
2330
  const plane = createSecureComsDataPlane(
2192
2331
  { identityId: cfg.identityId, log: (level, msg) => log(level, msg) },
2193
2332
  buildHandler({
@@ -2196,16 +2335,16 @@ async function main() {
2196
2335
  skills,
2197
2336
  gate,
2198
2337
  log,
2199
- // All three are read PER TURN: a persona edit, a new remembered fact, or
2200
- // a key file dropped in while the agent runs takes effect on the next
2201
- // message, with no restart.
2202
- systemPromptFor: () => systemPromptFor(cfg.dataDir, DEFAULT_SYSTEM_PROMPT),
2203
- longTermMemory: () => readLongTermMemory(cfg.dataDir),
2204
- tools: () => skills.available(requiresEnvFrom(skillsConfig, hasSearchKey()))
2338
+ // All of these are read PER TURN: a persona edit, a new remembered fact, a
2339
+ // key file dropped in, or configuration saved in the web app takes effect
2340
+ // on the next message, with no restart.
2341
+ systemPromptFor: () => systemPromptFor(configDirs, DEFAULT_SYSTEM_PROMPT),
2342
+ longTermMemory: () => readLongTermMemory(configDirs),
2343
+ tools: offeredTools
2205
2344
  })
2206
2345
  );
2207
2346
  console.log(
2208
- `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${skills.available(requiresEnv).length} skills, tool gate default=${cfg.toolGate.default}`
2347
+ `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
2209
2348
  );
2210
2349
  await plane.start();
2211
2350
  const shutdown = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"