@securecoms/agent-runtime 0.2.0 → 0.3.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,13 @@ 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
+ return { endpoints, databases, flags, ...tools ? { tools } : {} };
804
834
  }
805
835
 
806
836
  // src/builtins/current-time.ts
@@ -1076,33 +1106,43 @@ var httpRequest = async (args, ctx) => {
1076
1106
  };
1077
1107
 
1078
1108
  // 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";
1109
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1110
+ import { join as join4 } from "node:path";
1081
1111
  var MEMORY_FILE = "MEMORY.md";
1082
1112
  var MEMORY_MAX_BYTES = 8192;
1083
1113
  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;
1114
+ function readLongTermMemory(dirs) {
1115
+ const p = findConfigFile(MEMORY_FILE, dirs);
1116
+ if (!p) return null;
1090
1117
  const body = readFileSync4(p, "utf8").trim();
1091
1118
  return body || null;
1092
1119
  }
1093
- function rememberFact(dataDir, fact, now) {
1120
+ function memoryTarget(dirs) {
1121
+ for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
1122
+ for (const d of dirs) if (existsSync4(d)) return d;
1123
+ return dirs[dirs.length - 1];
1124
+ }
1125
+ function migrateMemoryIntoManaged(managedDir, dataDir) {
1126
+ const managed = join4(managedDir, MEMORY_FILE);
1127
+ const local = join4(dataDir, MEMORY_FILE);
1128
+ if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
1129
+ copyFileSync(local, managed);
1130
+ return true;
1131
+ }
1132
+ function rememberFact(dirs, fact, now) {
1094
1133
  const clean = fact.replace(/\s+/g, " ").trim().slice(0, FACT_MAX_CHARS);
1095
1134
  if (!clean) throw new Error("remember needs a non-empty fact");
1096
1135
  const line = `- ${now.toISOString().slice(0, 10)} ${clean}`;
1097
- mkdirSync2(dataDir, { recursive: true });
1098
- const existing = readLongTermMemory(dataDir);
1136
+ const dir = memoryTarget(dirs);
1137
+ mkdirSync2(dir, { recursive: true });
1138
+ const existing = readLongTermMemory(dirs);
1099
1139
  let lines = existing ? existing.split("\n") : [];
1100
1140
  lines.push(line);
1101
1141
  while (Buffer.byteLength(`${lines.join("\n")}
1102
1142
  `, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
1103
1143
  lines = lines.slice(1);
1104
1144
  }
1105
- writeFileSync2(memoryPath(dataDir), `${lines.join("\n")}
1145
+ writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
1106
1146
  `, { mode: 384 });
1107
1147
  return line;
1108
1148
  }
@@ -1110,7 +1150,7 @@ function rememberFact(dataDir, fact, now) {
1110
1150
  // src/builtins/remember.ts
1111
1151
  var remember = async (args, ctx) => {
1112
1152
  try {
1113
- return JSON.stringify({ remembered: rememberFact(ctx.dataDir, String(args.fact ?? ""), ctx.now()) });
1153
+ return JSON.stringify({ remembered: rememberFact(ctx.configDirs, String(args.fact ?? ""), ctx.now()) });
1114
1154
  } catch (e) {
1115
1155
  return toolError(e instanceof Error ? e.message : String(e));
1116
1156
  }
@@ -1314,6 +1354,7 @@ function offlineContext() {
1314
1354
  };
1315
1355
  return {
1316
1356
  dataDir: "",
1357
+ configDirs: [""],
1317
1358
  readKeyFile: () => null,
1318
1359
  fetch: refuse,
1319
1360
  resolve: async () => refuse(),
@@ -1356,19 +1397,19 @@ var SkillInvoker = class {
1356
1397
  * `<skillsDir>/<subdir>/manifest.json`.
1357
1398
  */
1358
1399
  loadSkills() {
1359
- if (!existsSync4(this.skillsDir)) {
1400
+ if (!existsSync5(this.skillsDir)) {
1360
1401
  console.warn(`[SkillInvoker] Skills directory not found: ${this.skillsDir}`);
1361
1402
  return;
1362
1403
  }
1363
1404
  const flat = [], manifests = [], skillMds = [];
1364
1405
  for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
1365
1406
  if (entry.isFile() && entry.name.endsWith(".json")) {
1366
- flat.push(join4(this.skillsDir, entry.name));
1407
+ flat.push(join5(this.skillsDir, entry.name));
1367
1408
  } 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);
1409
+ const m = join5(this.skillsDir, entry.name, "manifest.json");
1410
+ if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
1411
+ const s = join5(this.skillsDir, entry.name, "SKILL.md");
1412
+ if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
1372
1413
  }
1373
1414
  }
1374
1415
  for (const p of [...flat.sort(), ...manifests.sort()]) this.loadManifestFile(p);
@@ -1537,12 +1578,27 @@ function createLocalToolGate(policy) {
1537
1578
  }
1538
1579
  };
1539
1580
  }
1581
+ function resolveToolPolicy(env, file, defaults) {
1582
+ const deny = env.deny || file?.deny ? [.../* @__PURE__ */ new Set([...env.deny ?? [], ...file?.deny ?? []])] : void 0;
1583
+ return {
1584
+ default: defaults.default,
1585
+ allow: env.allow ?? file?.allow ?? defaults.allow,
1586
+ ...deny ? { deny } : {}
1587
+ };
1588
+ }
1589
+ function createDynamicToolGate(policyFor) {
1590
+ return {
1591
+ gate(toolName) {
1592
+ return createLocalToolGate(policyFor()).gate(toolName);
1593
+ }
1594
+ };
1595
+ }
1540
1596
 
1541
1597
  // src/data-plane.ts
1542
1598
  import { readFileSync as readFileSync7 } from "node:fs";
1543
1599
  import { WebSocket } from "node:http";
1544
- import { homedir as homedir2 } from "node:os";
1545
- import { join as join5 } from "node:path";
1600
+ import { homedir as homedir3 } from "node:os";
1601
+ import { join as join6 } from "node:path";
1546
1602
  var SEEN_MAX = 5e3;
1547
1603
  function errText(e) {
1548
1604
  return e instanceof Error ? e.message : String(e);
@@ -1585,8 +1641,8 @@ var SecureComsDataPlane = class {
1585
1641
  generation = 0;
1586
1642
  constructor(cfg, onMessage) {
1587
1643
  this.identityId = cfg.identityId;
1588
- const dir = cfg.connectionsDir ?? join5(homedir2(), ".securecoms", "connections");
1589
- this.connectionPath = join5(dir, `${cfg.identityId}.json`);
1644
+ const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
1645
+ this.connectionPath = join6(dir, `${cfg.identityId}.json`);
1590
1646
  this.establishedMs = cfg.establishedMs ?? 5e3;
1591
1647
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
1592
1648
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
@@ -1901,8 +1957,8 @@ ${longTerm}` });
1901
1957
  }
1902
1958
 
1903
1959
  // 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";
1960
+ import { existsSync as existsSync6, readFileSync as readFileSync8, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
1961
+ import { dirname as dirname3, join as join7 } from "node:path";
1906
1962
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1907
1963
  var PERSONA_MAX_BYTES = 8192;
1908
1964
  var PERSONA_SOURCES = [
@@ -1918,17 +1974,17 @@ var SAFETY_PREAMBLE = [
1918
1974
  "- Only use the tools you were given; do not invent capabilities."
1919
1975
  ].join("\n");
1920
1976
  function defaultPersonaDir() {
1921
- return join6(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1977
+ return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1922
1978
  }
1923
1979
  function assemblePrompt(parts) {
1924
1980
  return parts.map((p) => `${p.heading}
1925
1981
  ${p.body}`).join("\n\n");
1926
1982
  }
1927
- function loadPersona(dataDir) {
1983
+ function loadPersona(dirs) {
1928
1984
  const parts = [];
1929
1985
  for (const { file, heading } of PERSONA_SOURCES) {
1930
- const path = join6(dataDir, file);
1931
- if (!existsSync5(path)) continue;
1986
+ const path = findConfigFile(file, dirs);
1987
+ if (!path) continue;
1932
1988
  let body;
1933
1989
  try {
1934
1990
  body = readFileSync8(path, "utf8");
@@ -1945,25 +2001,66 @@ function loadPersona(dataDir) {
1945
2001
  }
1946
2002
  return parts.length ? assemblePrompt(parts) : null;
1947
2003
  }
1948
- function systemPromptFor(dataDir, fallback) {
1949
- return `${loadPersona(dataDir) ?? fallback}
2004
+ function systemPromptFor(dirs, fallback) {
2005
+ return `${loadPersona(dirs) ?? fallback}
1950
2006
 
1951
2007
  ${SAFETY_PREAMBLE}`;
1952
2008
  }
2009
+ function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2010
+ if (!existsSync6(managedDir)) return false;
2011
+ const persona = {};
2012
+ for (const { file } of PERSONA_SOURCES) {
2013
+ const path2 = join7(dataDir, file);
2014
+ if (!existsSync6(path2)) continue;
2015
+ try {
2016
+ persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
2017
+ } catch {
2018
+ }
2019
+ }
2020
+ const doc = {
2021
+ schema: "securecoms.local-config/1",
2022
+ written_at: now.toISOString(),
2023
+ has_search_key: hasSearchKey,
2024
+ persona
2025
+ };
2026
+ const path = join7(managedDir, "local-config.json");
2027
+ const tmp = `${path}.${process.pid}.tmp`;
2028
+ try {
2029
+ writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
2030
+ `, { mode: 384 });
2031
+ renameSync(tmp, path);
2032
+ } catch (e) {
2033
+ try {
2034
+ unlinkSync(tmp);
2035
+ } catch {
2036
+ }
2037
+ throw e;
2038
+ }
2039
+ return true;
2040
+ }
1953
2041
  function personaInit(dataDir, defaultsDir, writeFile, exists) {
1954
2042
  const written = [];
1955
2043
  const skipped = [];
1956
2044
  for (const { file } of PERSONA_SOURCES) {
1957
- const target = join6(dataDir, file);
2045
+ const target = join7(dataDir, file);
1958
2046
  if (exists(target)) {
1959
2047
  skipped.push(target);
1960
2048
  continue;
1961
2049
  }
1962
- writeFile(target, readFileSync8(join6(defaultsDir, file), "utf8"));
2050
+ writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
1963
2051
  written.push(target);
1964
2052
  }
1965
2053
  return { written, skipped };
1966
2054
  }
2055
+ function createLocalConfigReporter(managedDir, dataDir, now) {
2056
+ let lastWritten = null;
2057
+ return (hasSearchKey) => {
2058
+ if (lastWritten === hasSearchKey) return false;
2059
+ const wrote = writeLocalConfigReport(managedDir, dataDir, now(), hasSearchKey);
2060
+ if (wrote) lastWritten = hasSearchKey;
2061
+ return wrote;
2062
+ };
2063
+ }
1967
2064
 
1968
2065
  // src/warning-filter.ts
1969
2066
  function installWarningFilter(proc = process) {
@@ -1978,7 +2075,7 @@ function installWarningFilter(proc = process) {
1978
2075
 
1979
2076
  // src/index.ts
1980
2077
  if (process.argv.includes("--version")) {
1981
- const pkg = JSON.parse(readFileSync9(join7(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2078
+ const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
1982
2079
  console.log(pkg.version);
1983
2080
  process.exit(0);
1984
2081
  }
@@ -2009,10 +2106,10 @@ function realServiceDeps() {
2009
2106
  const r = spawnSync(cmd, args, { encoding: "utf8" });
2010
2107
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
2011
2108
  },
2012
- writeFile: (p, data) => writeFileSync3(p, data, { mode: 420 }),
2109
+ writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
2013
2110
  removeFile: (p) => rmSync(p, { force: true }),
2014
2111
  mkdirp: (p) => mkdirSync3(p, { recursive: true }),
2015
- fileExists: (p) => existsSync6(p),
2112
+ fileExists: (p) => existsSync7(p),
2016
2113
  canRead: (p) => {
2017
2114
  try {
2018
2115
  accessSync(p, constants.R_OK);
@@ -2027,7 +2124,7 @@ function realServiceDeps() {
2027
2124
  function realResolveNodeDeps() {
2028
2125
  return {
2029
2126
  realpath: (p) => realpathSync(p),
2030
- fileExists: (p) => existsSync6(p),
2127
+ fileExists: (p) => existsSync7(p),
2031
2128
  nodeMajorOf: (p) => {
2032
2129
  const r = spawnSync(p, ["--version"], { encoding: "utf8" });
2033
2130
  if (r.status !== 0) return null;
@@ -2047,7 +2144,7 @@ function runServiceVerb(verb) {
2047
2144
  console.error(`securecoms-agent: ${MISSING_IDENTITY_MESSAGE}`);
2048
2145
  process.exit(2);
2049
2146
  }
2050
- const home = homedir3();
2147
+ const home = homedir4();
2051
2148
  try {
2052
2149
  const execPath = chooseVerbExecPath(
2053
2150
  verb,
@@ -2123,13 +2220,18 @@ async function main() {
2123
2220
  const r = personaInit(
2124
2221
  cfgForPersona.dataDir,
2125
2222
  defaultPersonaDir(),
2126
- (p, d) => writeFileSync3(p, d, { mode: 384 }),
2127
- (p) => existsSync6(p)
2223
+ (p, d) => writeFileSync4(p, d, { mode: 384 }),
2224
+ (p) => existsSync7(p)
2128
2225
  );
2129
2226
  for (const p of r.written) console.log(`wrote ${p}`);
2130
2227
  for (const p of r.skipped) console.log(`kept ${p} (already exists)`);
2131
2228
  } else {
2132
- console.log(systemPromptFor(cfgForPersona.dataDir, DEFAULT_SYSTEM_PROMPT));
2229
+ console.log(
2230
+ systemPromptFor(
2231
+ configSearchPath(cfgForPersona.identityId, cfgForPersona.dataDir),
2232
+ DEFAULT_SYSTEM_PROMPT
2233
+ )
2234
+ );
2133
2235
  }
2134
2236
  process.exit(0);
2135
2237
  }
@@ -2155,7 +2257,7 @@ async function main() {
2155
2257
  const memoryFor = (channelId) => {
2156
2258
  let mem = memories.get(channelId);
2157
2259
  if (!mem) {
2158
- mem = new AgentMemory(join7(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2260
+ mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2159
2261
  memories.set(channelId, mem);
2160
2262
  }
2161
2263
  return mem;
@@ -2170,24 +2272,45 @@ async function main() {
2170
2272
  return null;
2171
2273
  }
2172
2274
  };
2275
+ const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
2276
+ const managedDir = managedConfigDir(cfg.identityId);
2277
+ const hasSearchKey = () => hasReadableKeyFile([cfg.braveKeyFile, cfg.tavilyKeyFile], (p) => readFileSync9(p, "utf8"));
2278
+ migrateMemoryIntoManaged(managedDir, cfg.dataDir);
2279
+ const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
2280
+ reportLocalConfig(hasSearchKey());
2281
+ const currentSkillsConfig = () => {
2282
+ const managed = findConfigFile("skills.json", [managedDir]);
2283
+ return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
2284
+ };
2173
2285
  const builtinCtx = {
2174
2286
  dataDir: cfg.dataDir,
2287
+ configDirs,
2175
2288
  readKeyFile,
2176
2289
  fetch,
2177
2290
  resolve: systemResolver(),
2178
- skillsConfig: loadSkillsConfig(cfg.skillsConfigPath, cfg.dataDir),
2291
+ get skillsConfig() {
2292
+ return currentSkillsConfig();
2293
+ },
2179
2294
  now: () => /* @__PURE__ */ new Date(),
2180
2295
  log
2181
2296
  };
2182
2297
  const skills = new SkillInvoker(cfg.skillsDir, builtinCtx);
2183
2298
  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)) {
2299
+ const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
2300
+ default: cfg.toolGate.default,
2301
+ allow: DEFAULT_TOOL_ALLOW
2302
+ });
2303
+ const gate = createDynamicToolGate(policyFor);
2304
+ const offeredTools = () => {
2305
+ const sc = currentSkillsConfig();
2306
+ const g = createLocalToolGate(policyFor());
2307
+ const searchKey = hasSearchKey();
2308
+ reportLocalConfig(searchKey);
2309
+ return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
2310
+ };
2311
+ for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
2188
2312
  console.log(`unavailable: ${held.name} (missing ${held.missing.join(", ")})`);
2189
2313
  }
2190
- const gate = createLocalToolGate(cfg.toolGate);
2191
2314
  const plane = createSecureComsDataPlane(
2192
2315
  { identityId: cfg.identityId, log: (level, msg) => log(level, msg) },
2193
2316
  buildHandler({
@@ -2196,16 +2319,16 @@ async function main() {
2196
2319
  skills,
2197
2320
  gate,
2198
2321
  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()))
2322
+ // All of these are read PER TURN: a persona edit, a new remembered fact, a
2323
+ // key file dropped in, or configuration saved in the web app takes effect
2324
+ // on the next message, with no restart.
2325
+ systemPromptFor: () => systemPromptFor(configDirs, DEFAULT_SYSTEM_PROMPT),
2326
+ longTermMemory: () => readLongTermMemory(configDirs),
2327
+ tools: offeredTools
2205
2328
  })
2206
2329
  );
2207
2330
  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}`
2331
+ `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
2209
2332
  );
2210
2333
  await plane.start();
2211
2334
  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.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"