@securecoms/agent-runtime 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +17 -1
  2. package/dist/bin.js +253 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,7 +90,8 @@ All configuration is via `SECURECOMS_*` environment variables (see
90
90
  | `SECURECOMS_LLM_ENDPOINT` | `http://127.0.0.1:11434` | Base URL of an OpenAI-compatible LLM server, with or without a trailing `/v1` |
91
91
  | `SECURECOMS_LLM_MODEL` | `llama3` | Model name to request |
92
92
  | `SECURECOMS_LLM_API_KEY` | unset | API key for the LLM endpoint, if it requires one |
93
- | `SECURECOMS_LLM_API_KEY_FILE` | unset (or `--llm-key-file <path>`) | File holding the API key (trimmed on read). Preferred: it wins over `SECURECOMS_LLM_API_KEY`, and it is the only key form the service installer accepts. An unreadable or empty file exits 2 |
93
+ | `SECURECOMS_LLM_API_KEY_FILE` | unset (or `--llm-key-file <path>`) | File holding the API key (trimmed on read). Preferred: it wins over `SECURECOMS_LLM_API_KEY`, and it is the only key form the service installer accepts. An unreadable or empty file, or `--llm-key-file` given anywhere (argv or env), makes the runtime an EXPLICIT source; a resolution failure fails that turn only — the agent boots and logs `llm: source unavailable — <reason>`, it does not exit |
94
+ | `SECURECOMS_LLM_SOURCE` | unset | Any value fails the LLM source closed with no read of the managed files. Reserved for a future bridge-provided source; not a customer-facing setting today |
94
95
  | `SECURECOMS_BRAVE_KEY_FILE` | unset (or `--brave-key-file <path>`) | File holding a Brave Search API key. A path, never a value — required for `web_search` |
95
96
  | `SECURECOMS_TAVILY_KEY_FILE` | unset (or `--tavily-key-file <path>`) | File holding a Tavily API key. Used only when the Brave key file is absent |
96
97
  | `SECURECOMS_SKILLS_DIR` | this package's own `skills/` | Directory of skill manifests to load |
@@ -101,6 +102,21 @@ All configuration is via `SECURECOMS_*` environment variables (see
101
102
  | `SECURECOMS_TOOL_DENY` | unset | Comma-separated tool deny-list (wins over allow) |
102
103
  | `SECURECOMS_TOOL_DEFAULT` | `deny` | Fallback policy for a tool matched by neither list |
103
104
 
105
+ ### The LLM source: explicit vs managed
106
+
107
+ An explicit source (any of `SECURECOMS_LLM_ENDPOINT`, `SECURECOMS_LLM_API_KEY`,
108
+ `SECURECOMS_LLM_API_KEY_FILE`, or `--llm-key-file` on argv) always wins and is
109
+ read once per turn straight from the env/cfg above — the managed files are
110
+ never touched. With none of those set, the runtime instead looks for
111
+ `~/.securecoms/runtime-config/<identityId>/llm-source.json` (`{provider,
112
+ model}`) plus, when that provider needs one, `llm.key` and `llm.key.host`
113
+ beside it — written by the desktop app (or, later, the bridge), never by this
114
+ runtime. The endpoint is always derived from a compiled provider table, never
115
+ read from the managed file, and `llm.key` is refused unless its
116
+ `llm.key.host` sidecar names the same host the provider resolves to. Any
117
+ resolution failure in either tier fails just that turn; it never crashes the
118
+ process.
119
+
104
120
  ## Memory
105
121
 
106
122
  Memory is local only, never sent anywhere but the configured LLM endpoint,
package/dist/bin.js CHANGED
@@ -3,9 +3,9 @@
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 join8 } from "node:path";
6
+ import { dirname as dirname4, join as join9 } from "node:path";
7
7
  import { spawnSync } from "node:child_process";
8
- import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
8
+ import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync3, realpathSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
9
9
  import { homedir as homedir4, userInfo } from "node:os";
10
10
 
11
11
  // src/config.ts
@@ -88,6 +88,7 @@ function loadConfig(argv, env) {
88
88
  llmEndpoint: env.SECURECOMS_LLM_ENDPOINT || "http://127.0.0.1:11434",
89
89
  llmModel: env.SECURECOMS_LLM_MODEL || "llama3",
90
90
  llmApiKey: env.SECURECOMS_LLM_API_KEY,
91
+ llmSource: env.SECURECOMS_LLM_SOURCE,
91
92
  llmApiKeyFile: parseStringFlag(argv, "--llm-key-file", env, "SECURECOMS_LLM_API_KEY_FILE"),
92
93
  braveKeyFile: parseStringFlag(argv, "--brave-key-file", env, "SECURECOMS_BRAVE_KEY_FILE"),
93
94
  tavilyKeyFile: parseStringFlag(argv, "--tavily-key-file", env, "SECURECOMS_TAVILY_KEY_FILE"),
@@ -103,25 +104,6 @@ function loadConfig(argv, env) {
103
104
  };
104
105
  }
105
106
 
106
- // src/llm-key.ts
107
- var LlmKeyFileError = class extends Error {
108
- };
109
- function resolveLlmApiKey(cfg, readFile) {
110
- const path = cfg.llmApiKeyFile;
111
- if (!path) return cfg.llmApiKey;
112
- let raw;
113
- try {
114
- raw = readFile(path);
115
- } catch (e) {
116
- throw new LlmKeyFileError(
117
- `cannot read LLM key file ${path}: ${e instanceof Error ? e.message : String(e)}`
118
- );
119
- }
120
- const key = raw.trim();
121
- if (!key) throw new LlmKeyFileError(`cannot read LLM key file ${path}: file is empty`);
122
- return key;
123
- }
124
-
125
107
  // src/service/args.ts
126
108
  function parseVerb(argv) {
127
109
  const v = argv[2];
@@ -499,9 +481,179 @@ var LLMDispatcher = class {
499
481
  }
500
482
  };
501
483
 
484
+ // src/llm-source.ts
485
+ import { join as join2 } from "node:path";
486
+
487
+ // src/llm-providers.json
488
+ var llm_providers_default = {
489
+ openrouter: { chat_base: "https://openrouter.ai/api/v1", models_url: "https://openrouter.ai/api/v1/models", models_id_path: "data[].id", validate_url: "https://openrouter.ai/api/v1/key", needs_key: true },
490
+ anthropic: { chat_base: "https://openrouter.ai/api/v1", models_url: "https://openrouter.ai/api/v1/models", models_id_path: "data[].id", validate_url: "https://openrouter.ai/api/v1/key", needs_key: true },
491
+ openai: { chat_base: "https://api.openai.com/v1", models_url: "https://api.openai.com/v1/models", models_id_path: "data[].id", validate_url: "https://api.openai.com/v1/models", needs_key: true },
492
+ local: { chat_base: "http://127.0.0.1:11434", models_url: "http://127.0.0.1:11434/api/tags", models_id_path: "models[].name", validate_url: null, needs_key: false }
493
+ };
494
+
495
+ // src/llm-key.ts
496
+ var LlmKeyFileError = class extends Error {
497
+ };
498
+ function resolveLlmApiKey(cfg, readFile) {
499
+ const path = cfg.llmApiKeyFile;
500
+ if (!path) return cfg.llmApiKey;
501
+ let raw;
502
+ try {
503
+ raw = readFile(path);
504
+ } catch (e) {
505
+ throw new LlmKeyFileError(
506
+ `cannot read LLM key file ${path}: ${e instanceof Error ? e.message : String(e)}`
507
+ );
508
+ }
509
+ const key = raw.trim();
510
+ if (!key) throw new LlmKeyFileError(`cannot read LLM key file ${path}: file is empty`);
511
+ return key;
512
+ }
513
+
514
+ // src/llm-source.ts
515
+ var PROVIDER_TABLE = Object.freeze(Object.fromEntries(
516
+ Object.entries(llm_providers_default).map(([p, e]) => [p, Object.freeze({ endpoint: e.chat_base, needsKey: e.needs_key })])
517
+ ));
518
+ var LlmSourceUnavailableError = class extends Error {
519
+ };
520
+ function providerOrigin(provider) {
521
+ const entry = PROVIDER_TABLE[provider];
522
+ if (!entry) throw new LlmSourceUnavailableError(`unknown LLM provider ${JSON.stringify(provider)}`);
523
+ const u = new URL(entry.endpoint);
524
+ return `${u.protocol}//${u.host}`.toLowerCase();
525
+ }
526
+ var explicitEndpointSet = (env) => Boolean(env.SECURECOMS_LLM_ENDPOINT || env.SECURECOMS_LLM_API_KEY_FILE || env.SECURECOMS_LLM_API_KEY);
527
+ function isExplicitSource(cfg, env) {
528
+ return explicitEndpointSet(env) || Boolean(cfg.llmApiKeyFile) || Boolean(cfg.llmApiKey);
529
+ }
530
+ function resolveLlmSource(cfg, explicit, deps) {
531
+ if (cfg.llmSource) {
532
+ throw new LlmSourceUnavailableError(
533
+ `SECURECOMS_LLM_SOURCE=${JSON.stringify(cfg.llmSource)} is not a supported LLM source`
534
+ );
535
+ }
536
+ if (explicit) {
537
+ return {
538
+ endpoint: cfg.llmEndpoint,
539
+ model: cfg.llmModel,
540
+ apiKey: resolveLlmApiKey(cfg, deps.readFile),
541
+ provider: "env",
542
+ origin: "env"
543
+ };
544
+ }
545
+ const managedDir = join2(deps.home, ".securecoms", "runtime-config", cfg.identityId);
546
+ const sourcePath = join2(managedDir, "llm-source.json");
547
+ let raw;
548
+ try {
549
+ raw = deps.readFile(sourcePath);
550
+ } catch (e) {
551
+ if (e.code !== "ENOENT") {
552
+ throw new LlmSourceUnavailableError(
553
+ `cannot read ${sourcePath}: ${e instanceof Error ? e.message : String(e)}`
554
+ );
555
+ }
556
+ }
557
+ if (raw !== void 0) {
558
+ let parsed;
559
+ try {
560
+ parsed = JSON.parse(raw);
561
+ } catch (e) {
562
+ throw new LlmSourceUnavailableError(
563
+ `invalid JSON in ${sourcePath}: ${e instanceof Error ? e.message : String(e)}`
564
+ );
565
+ }
566
+ const { provider, model } = parsed;
567
+ if (typeof provider !== "string" || typeof model !== "string") {
568
+ throw new LlmSourceUnavailableError(`${sourcePath} is missing "provider" or "model"`);
569
+ }
570
+ const entry = PROVIDER_TABLE[provider];
571
+ if (!entry) {
572
+ throw new LlmSourceUnavailableError(
573
+ `unknown LLM provider ${JSON.stringify(provider)} in ${sourcePath}`
574
+ );
575
+ }
576
+ let apiKey;
577
+ if (entry.needsKey) {
578
+ const keyPath = join2(managedDir, "llm.key");
579
+ const hostPath = join2(managedDir, "llm.key.host");
580
+ if (!deps.exists(keyPath)) {
581
+ throw new LlmSourceUnavailableError(
582
+ `${keyPath} does not exist \u2014 no key has been written for this managed source, refusing to read it`
583
+ );
584
+ }
585
+ let mode;
586
+ try {
587
+ ({ mode } = deps.statFile(keyPath));
588
+ } catch (e) {
589
+ throw new LlmSourceUnavailableError(
590
+ `cannot stat ${keyPath}: ${e instanceof Error ? e.message : String(e)}`
591
+ );
592
+ }
593
+ if ((mode & 63) !== 0) {
594
+ throw new LlmSourceUnavailableError(
595
+ `${keyPath} is readable by other users on this machine \u2014 tighten its permissions to owner-only`
596
+ );
597
+ }
598
+ const expectedOrigin = providerOrigin(provider);
599
+ if (!deps.exists(hostPath)) {
600
+ throw new LlmSourceUnavailableError(
601
+ `${keyPath} has no ${hostPath} sidecar recording which origin it belongs to \u2014 refusing to send it to ${expectedOrigin}`
602
+ );
603
+ }
604
+ const actualOrigin = deps.readFile(hostPath).trim();
605
+ if (actualOrigin !== expectedOrigin) {
606
+ throw new LlmSourceUnavailableError(
607
+ `${hostPath} is bound to ${actualOrigin}, but provider ${JSON.stringify(provider)} requires ${expectedOrigin} \u2014 refusing to send the key across origins`
608
+ );
609
+ }
610
+ apiKey = resolveLlmApiKey({ llmApiKeyFile: keyPath }, deps.readFile);
611
+ }
612
+ return {
613
+ endpoint: entry.endpoint,
614
+ model,
615
+ ...apiKey !== void 0 ? { apiKey } : {},
616
+ provider,
617
+ origin: "managed"
618
+ };
619
+ }
620
+ return {
621
+ endpoint: cfg.llmEndpoint,
622
+ model: cfg.llmModel,
623
+ provider: "default",
624
+ origin: "default"
625
+ };
626
+ }
627
+
628
+ // src/llm-shim.ts
629
+ function endpointHost(endpoint) {
630
+ try {
631
+ return new URL(endpoint).host;
632
+ } catch {
633
+ return "(unparseable endpoint)";
634
+ }
635
+ }
636
+ function buildLlmShim(cfg, explicit, deps) {
637
+ return {
638
+ async complete(messages, tools) {
639
+ const src = resolveLlmSource(cfg, explicit, deps);
640
+ const dispatcher = new LLMDispatcher(src.endpoint, src.model, { apiKey: src.apiKey });
641
+ return dispatcher.complete(messages, tools);
642
+ }
643
+ };
644
+ }
645
+ function bootLine(cfg, explicit, deps) {
646
+ try {
647
+ const src = resolveLlmSource(cfg, explicit, deps);
648
+ return `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm source=${src.origin} ${endpointHost(src.endpoint)} (${src.model})`;
649
+ } catch (e) {
650
+ return `llm: source unavailable \u2014 ${e instanceof Error ? e.message : String(e)}`;
651
+ }
652
+ }
653
+
502
654
  // src/memory.ts
503
655
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
504
- import { join as join2 } from "node:path";
656
+ import { join as join3 } from "node:path";
505
657
  var MAX_HISTORY = 50;
506
658
  var AgentMemory = class {
507
659
  history = [];
@@ -566,10 +718,10 @@ ${memories}`;
566
718
  }
567
719
  }
568
720
  get historyPath() {
569
- return join2(this.dataDir, "conversation-history.json");
721
+ return join3(this.dataDir, "conversation-history.json");
570
722
  }
571
723
  get memoriesPath() {
572
- return join2(this.dataDir, "memories.json");
724
+ return join3(this.dataDir, "memories.json");
573
725
  }
574
726
  loadHistory() {
575
727
  try {
@@ -609,7 +761,7 @@ ${memories}`;
609
761
 
610
762
  // src/skill-invoker.ts
611
763
  import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync5, statSync } from "node:fs";
612
- import { join as join5 } from "node:path";
764
+ import { join as join6 } from "node:path";
613
765
 
614
766
  // src/skill-md.ts
615
767
  var SkillMdError = class extends Error {
@@ -770,16 +922,16 @@ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
770
922
  // src/config-dirs.ts
771
923
  import { existsSync as existsSync2 } from "node:fs";
772
924
  import { homedir as homedir2 } from "node:os";
773
- import { join as join3 } from "node:path";
925
+ import { join as join4 } from "node:path";
774
926
  function managedConfigDir(identityId, home = homedir2()) {
775
- return join3(home, ".securecoms", "runtime-config", identityId);
927
+ return join4(home, ".securecoms", "runtime-config", identityId);
776
928
  }
777
929
  function configSearchPath(identityId, dataDir, home = homedir2()) {
778
930
  return [managedConfigDir(identityId, home), dataDir];
779
931
  }
780
932
  function findConfigFile(name, dirs) {
781
933
  for (const dir of dirs) {
782
- const path = join3(dir, name);
934
+ const path = join4(dir, name);
783
935
  if (existsSync2(path)) return path;
784
936
  }
785
937
  return null;
@@ -830,7 +982,18 @@ function loadSkillsConfigFile(path, dataDir) {
830
982
  ...strings(rawTools.allow) ? { allow: strings(rawTools.allow) } : {},
831
983
  ...strings(rawTools.deny) ? { deny: strings(rawTools.deny) } : {}
832
984
  } : void 0;
833
- return { endpoints, databases, flags, ...tools ? { tools } : {} };
985
+ let search;
986
+ const s = src.search;
987
+ if (s && typeof s === "object") {
988
+ if (s.provider !== "brave" && s.provider !== "tavily") {
989
+ console.warn(`[skills-config] search in ${path} has an unknown provider; skipped`);
990
+ } else if (typeof s.keyFile !== "string" || s.keyFile === "") {
991
+ console.warn(`[skills-config] search in ${path} has no keyFile; skipped`);
992
+ } else {
993
+ search = { provider: s.provider, keyFile: abs2(s.keyFile, dataDir) };
994
+ }
995
+ }
996
+ return { endpoints, databases, flags, ...tools ? { tools } : {}, ...search ? { search } : {} };
834
997
  }
835
998
 
836
999
  // src/builtins/current-time.ts
@@ -1107,7 +1270,7 @@ var httpRequest = async (args, ctx) => {
1107
1270
 
1108
1271
  // src/long-term-memory.ts
1109
1272
  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";
1273
+ import { join as join5 } from "node:path";
1111
1274
  var MEMORY_FILE = "MEMORY.md";
1112
1275
  var MEMORY_MAX_BYTES = 8192;
1113
1276
  var FACT_MAX_CHARS = 300;
@@ -1118,13 +1281,13 @@ function readLongTermMemory(dirs) {
1118
1281
  return body || null;
1119
1282
  }
1120
1283
  function memoryTarget(dirs) {
1121
- for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
1284
+ for (const d of dirs) if (existsSync4(join5(d, MEMORY_FILE))) return d;
1122
1285
  for (const d of dirs) if (existsSync4(d)) return d;
1123
1286
  return dirs[dirs.length - 1];
1124
1287
  }
1125
1288
  function migrateMemoryIntoManaged(managedDir, dataDir) {
1126
- const managed = join4(managedDir, MEMORY_FILE);
1127
- const local = join4(dataDir, MEMORY_FILE);
1289
+ const managed = join5(managedDir, MEMORY_FILE);
1290
+ const local = join5(dataDir, MEMORY_FILE);
1128
1291
  if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
1129
1292
  copyFileSync(local, managed);
1130
1293
  return true;
@@ -1142,7 +1305,7 @@ function rememberFact(dirs, fact, now) {
1142
1305
  `, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
1143
1306
  lines = lines.slice(1);
1144
1307
  }
1145
- writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
1308
+ writeFileSync2(join5(dir, MEMORY_FILE), `${lines.join("\n")}
1146
1309
  `, { mode: 384 });
1147
1310
  return line;
1148
1311
  }
@@ -1404,11 +1567,11 @@ var SkillInvoker = class {
1404
1567
  const flat = [], manifests = [], skillMds = [];
1405
1568
  for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
1406
1569
  if (entry.isFile() && entry.name.endsWith(".json")) {
1407
- flat.push(join5(this.skillsDir, entry.name));
1570
+ flat.push(join6(this.skillsDir, entry.name));
1408
1571
  } else if (entry.isDirectory()) {
1409
- const m = join5(this.skillsDir, entry.name, "manifest.json");
1572
+ const m = join6(this.skillsDir, entry.name, "manifest.json");
1410
1573
  if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
1411
- const s = join5(this.skillsDir, entry.name, "SKILL.md");
1574
+ const s = join6(this.skillsDir, entry.name, "SKILL.md");
1412
1575
  if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
1413
1576
  }
1414
1577
  }
@@ -1598,7 +1761,7 @@ function createDynamicToolGate(policyFor) {
1598
1761
  import { readFileSync as readFileSync7 } from "node:fs";
1599
1762
  import { WebSocket } from "node:http";
1600
1763
  import { homedir as homedir3 } from "node:os";
1601
- import { join as join6 } from "node:path";
1764
+ import { join as join7 } from "node:path";
1602
1765
  var SEEN_MAX = 5e3;
1603
1766
  function errText(e) {
1604
1767
  return e instanceof Error ? e.message : String(e);
@@ -1641,8 +1804,8 @@ var SecureComsDataPlane = class {
1641
1804
  generation = 0;
1642
1805
  constructor(cfg, onMessage) {
1643
1806
  this.identityId = cfg.identityId;
1644
- const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
1645
- this.connectionPath = join6(dir, `${cfg.identityId}.json`);
1807
+ const dir = cfg.connectionsDir ?? join7(homedir3(), ".securecoms", "connections");
1808
+ this.connectionPath = join7(dir, `${cfg.identityId}.json`);
1646
1809
  this.establishedMs = cfg.establishedMs ?? 5e3;
1647
1810
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
1648
1811
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
@@ -1958,7 +2121,7 @@ ${longTerm}` });
1958
2121
 
1959
2122
  // src/persona.ts
1960
2123
  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";
2124
+ import { dirname as dirname3, join as join8 } from "node:path";
1962
2125
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1963
2126
  var PERSONA_MAX_BYTES = 8192;
1964
2127
  var PERSONA_SOURCES = [
@@ -1974,7 +2137,7 @@ var SAFETY_PREAMBLE = [
1974
2137
  "- Only use the tools you were given; do not invent capabilities."
1975
2138
  ].join("\n");
1976
2139
  function defaultPersonaDir() {
1977
- return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
2140
+ return join8(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1978
2141
  }
1979
2142
  function assemblePrompt(parts) {
1980
2143
  return parts.map((p) => `${p.heading}
@@ -2006,11 +2169,11 @@ function systemPromptFor(dirs, fallback) {
2006
2169
 
2007
2170
  ${SAFETY_PREAMBLE}`;
2008
2171
  }
2009
- function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2172
+ function writeLocalConfigReport(managedDir, dataDir, now, flags) {
2010
2173
  if (!existsSync6(managedDir)) return false;
2011
2174
  const persona = {};
2012
2175
  for (const { file } of PERSONA_SOURCES) {
2013
- const path2 = join7(dataDir, file);
2176
+ const path2 = join8(dataDir, file);
2014
2177
  if (!existsSync6(path2)) continue;
2015
2178
  try {
2016
2179
  persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
@@ -2020,10 +2183,11 @@ function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2020
2183
  const doc = {
2021
2184
  schema: "securecoms.local-config/1",
2022
2185
  written_at: now.toISOString(),
2023
- has_search_key: hasSearchKey,
2186
+ has_search_key: flags.hasSearchKey,
2187
+ has_llm_key: flags.hasLlmKey,
2024
2188
  persona
2025
2189
  };
2026
- const path = join7(managedDir, "local-config.json");
2190
+ const path = join8(managedDir, "local-config.json");
2027
2191
  const tmp = `${path}.${process.pid}.tmp`;
2028
2192
  try {
2029
2193
  writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
@@ -2042,25 +2206,30 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
2042
2206
  const written = [];
2043
2207
  const skipped = [];
2044
2208
  for (const { file } of PERSONA_SOURCES) {
2045
- const target = join7(dataDir, file);
2209
+ const target = join8(dataDir, file);
2046
2210
  if (exists(target)) {
2047
2211
  skipped.push(target);
2048
2212
  continue;
2049
2213
  }
2050
- writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
2214
+ writeFile(target, readFileSync8(join8(defaultsDir, file), "utf8"));
2051
2215
  written.push(target);
2052
2216
  }
2053
2217
  return { written, skipped };
2054
2218
  }
2055
2219
  function createLocalConfigReporter(managedDir, dataDir, now) {
2056
2220
  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;
2221
+ return (flags) => {
2222
+ if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey) {
2223
+ return false;
2224
+ }
2225
+ const wrote = writeLocalConfigReport(managedDir, dataDir, now(), flags);
2226
+ if (wrote) lastWritten = { ...flags };
2061
2227
  return wrote;
2062
2228
  };
2063
2229
  }
2230
+ function hasLlmKeyFor(source) {
2231
+ return source.origin === "managed" && source.apiKey !== void 0;
2232
+ }
2064
2233
 
2065
2234
  // src/warning-filter.ts
2066
2235
  function installWarningFilter(proc = process) {
@@ -2075,7 +2244,7 @@ function installWarningFilter(proc = process) {
2075
2244
 
2076
2245
  // src/index.ts
2077
2246
  if (process.argv.includes("--version")) {
2078
- const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2247
+ const pkg = JSON.parse(readFileSync9(join9(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2079
2248
  console.log(pkg.version);
2080
2249
  process.exit(0);
2081
2250
  }
@@ -2092,12 +2261,13 @@ function makeLogger(cfg) {
2092
2261
  function safeChannelSegment(channelId) {
2093
2262
  return channelId.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
2094
2263
  }
2095
- function endpointHost(endpoint) {
2096
- try {
2097
- return new URL(endpoint).host;
2098
- } catch {
2099
- return "(unparseable endpoint)";
2100
- }
2264
+ function realLlmSourceDeps() {
2265
+ return {
2266
+ home: homedir4(),
2267
+ readFile: (p) => readFileSync9(p, "utf8"),
2268
+ statFile: (p) => ({ mode: statSync2(p).mode }),
2269
+ exists: (p) => existsSync7(p)
2270
+ };
2101
2271
  }
2102
2272
  function realServiceDeps() {
2103
2273
  return {
@@ -2244,26 +2414,28 @@ async function main() {
2244
2414
  return;
2245
2415
  }
2246
2416
  const log = makeLogger(cfg);
2247
- let apiKey;
2248
- try {
2249
- apiKey = resolveLlmApiKey(cfg, (p) => readFileSync9(p, "utf8"));
2250
- } catch (e) {
2251
- console.error(`securecoms-agent: ${e instanceof Error ? e.message : String(e)}`);
2252
- process.exit(2);
2253
- return;
2254
- }
2255
- const llm = new LLMDispatcher(cfg.llmEndpoint, cfg.llmModel, { apiKey });
2417
+ const llm = buildLlmShim(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps());
2256
2418
  const memories = /* @__PURE__ */ new Map();
2257
2419
  const memoryFor = (channelId) => {
2258
2420
  let mem = memories.get(channelId);
2259
2421
  if (!mem) {
2260
- mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2422
+ mem = new AgentMemory(join9(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2261
2423
  memories.set(channelId, mem);
2262
2424
  }
2263
2425
  return mem;
2264
2426
  };
2427
+ const managedDir = managedConfigDir(cfg.identityId);
2428
+ const currentSkillsConfig = () => {
2429
+ const managed = findConfigFile("skills.json", [managedDir]);
2430
+ return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
2431
+ };
2432
+ const keyFilePath = (which) => {
2433
+ const owner = currentSkillsConfig().search;
2434
+ if (owner && owner.provider === which) return owner.keyFile;
2435
+ return which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
2436
+ };
2265
2437
  const readKeyFile = (which) => {
2266
- const path = which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
2438
+ const path = keyFilePath(which);
2267
2439
  if (!path) return null;
2268
2440
  try {
2269
2441
  return readFileSync9(path, "utf8").trim() || null;
@@ -2273,15 +2445,17 @@ async function main() {
2273
2445
  }
2274
2446
  };
2275
2447
  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"));
2448
+ const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) => readFileSync9(p, "utf8"));
2449
+ const hasLlmKey = () => {
2450
+ try {
2451
+ return hasLlmKeyFor(resolveLlmSource(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps()));
2452
+ } catch {
2453
+ return false;
2454
+ }
2455
+ };
2278
2456
  migrateMemoryIntoManaged(managedDir, cfg.dataDir);
2279
2457
  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
- };
2458
+ reportLocalConfig({ hasSearchKey: hasSearchKey(), hasLlmKey: hasLlmKey() });
2285
2459
  const builtinCtx = {
2286
2460
  dataDir: cfg.dataDir,
2287
2461
  configDirs,
@@ -2305,7 +2479,7 @@ async function main() {
2305
2479
  const sc = currentSkillsConfig();
2306
2480
  const g = createLocalToolGate(policyFor());
2307
2481
  const searchKey = hasSearchKey();
2308
- reportLocalConfig(searchKey);
2482
+ reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey() });
2309
2483
  return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
2310
2484
  };
2311
2485
  for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
@@ -2328,7 +2502,7 @@ async function main() {
2328
2502
  })
2329
2503
  );
2330
2504
  console.log(
2331
- `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
2505
+ `${bootLine(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps())}, ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
2332
2506
  );
2333
2507
  await plane.start();
2334
2508
  const shutdown = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"