@securecoms/agent-runtime 0.4.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 +229 -71
  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;
@@ -1118,7 +1270,7 @@ var httpRequest = async (args, ctx) => {
1118
1270
 
1119
1271
  // src/long-term-memory.ts
1120
1272
  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";
1273
+ import { join as join5 } from "node:path";
1122
1274
  var MEMORY_FILE = "MEMORY.md";
1123
1275
  var MEMORY_MAX_BYTES = 8192;
1124
1276
  var FACT_MAX_CHARS = 300;
@@ -1129,13 +1281,13 @@ function readLongTermMemory(dirs) {
1129
1281
  return body || null;
1130
1282
  }
1131
1283
  function memoryTarget(dirs) {
1132
- 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;
1133
1285
  for (const d of dirs) if (existsSync4(d)) return d;
1134
1286
  return dirs[dirs.length - 1];
1135
1287
  }
1136
1288
  function migrateMemoryIntoManaged(managedDir, dataDir) {
1137
- const managed = join4(managedDir, MEMORY_FILE);
1138
- const local = join4(dataDir, MEMORY_FILE);
1289
+ const managed = join5(managedDir, MEMORY_FILE);
1290
+ const local = join5(dataDir, MEMORY_FILE);
1139
1291
  if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
1140
1292
  copyFileSync(local, managed);
1141
1293
  return true;
@@ -1153,7 +1305,7 @@ function rememberFact(dirs, fact, now) {
1153
1305
  `, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
1154
1306
  lines = lines.slice(1);
1155
1307
  }
1156
- writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
1308
+ writeFileSync2(join5(dir, MEMORY_FILE), `${lines.join("\n")}
1157
1309
  `, { mode: 384 });
1158
1310
  return line;
1159
1311
  }
@@ -1415,11 +1567,11 @@ var SkillInvoker = class {
1415
1567
  const flat = [], manifests = [], skillMds = [];
1416
1568
  for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
1417
1569
  if (entry.isFile() && entry.name.endsWith(".json")) {
1418
- flat.push(join5(this.skillsDir, entry.name));
1570
+ flat.push(join6(this.skillsDir, entry.name));
1419
1571
  } else if (entry.isDirectory()) {
1420
- const m = join5(this.skillsDir, entry.name, "manifest.json");
1572
+ const m = join6(this.skillsDir, entry.name, "manifest.json");
1421
1573
  if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
1422
- const s = join5(this.skillsDir, entry.name, "SKILL.md");
1574
+ const s = join6(this.skillsDir, entry.name, "SKILL.md");
1423
1575
  if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
1424
1576
  }
1425
1577
  }
@@ -1609,7 +1761,7 @@ function createDynamicToolGate(policyFor) {
1609
1761
  import { readFileSync as readFileSync7 } from "node:fs";
1610
1762
  import { WebSocket } from "node:http";
1611
1763
  import { homedir as homedir3 } from "node:os";
1612
- import { join as join6 } from "node:path";
1764
+ import { join as join7 } from "node:path";
1613
1765
  var SEEN_MAX = 5e3;
1614
1766
  function errText(e) {
1615
1767
  return e instanceof Error ? e.message : String(e);
@@ -1652,8 +1804,8 @@ var SecureComsDataPlane = class {
1652
1804
  generation = 0;
1653
1805
  constructor(cfg, onMessage) {
1654
1806
  this.identityId = cfg.identityId;
1655
- const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
1656
- this.connectionPath = join6(dir, `${cfg.identityId}.json`);
1807
+ const dir = cfg.connectionsDir ?? join7(homedir3(), ".securecoms", "connections");
1808
+ this.connectionPath = join7(dir, `${cfg.identityId}.json`);
1657
1809
  this.establishedMs = cfg.establishedMs ?? 5e3;
1658
1810
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
1659
1811
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
@@ -1969,7 +2121,7 @@ ${longTerm}` });
1969
2121
 
1970
2122
  // src/persona.ts
1971
2123
  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";
2124
+ import { dirname as dirname3, join as join8 } from "node:path";
1973
2125
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1974
2126
  var PERSONA_MAX_BYTES = 8192;
1975
2127
  var PERSONA_SOURCES = [
@@ -1985,7 +2137,7 @@ var SAFETY_PREAMBLE = [
1985
2137
  "- Only use the tools you were given; do not invent capabilities."
1986
2138
  ].join("\n");
1987
2139
  function defaultPersonaDir() {
1988
- return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
2140
+ return join8(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1989
2141
  }
1990
2142
  function assemblePrompt(parts) {
1991
2143
  return parts.map((p) => `${p.heading}
@@ -2017,11 +2169,11 @@ function systemPromptFor(dirs, fallback) {
2017
2169
 
2018
2170
  ${SAFETY_PREAMBLE}`;
2019
2171
  }
2020
- function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2172
+ function writeLocalConfigReport(managedDir, dataDir, now, flags) {
2021
2173
  if (!existsSync6(managedDir)) return false;
2022
2174
  const persona = {};
2023
2175
  for (const { file } of PERSONA_SOURCES) {
2024
- const path2 = join7(dataDir, file);
2176
+ const path2 = join8(dataDir, file);
2025
2177
  if (!existsSync6(path2)) continue;
2026
2178
  try {
2027
2179
  persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
@@ -2031,10 +2183,11 @@ function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2031
2183
  const doc = {
2032
2184
  schema: "securecoms.local-config/1",
2033
2185
  written_at: now.toISOString(),
2034
- has_search_key: hasSearchKey,
2186
+ has_search_key: flags.hasSearchKey,
2187
+ has_llm_key: flags.hasLlmKey,
2035
2188
  persona
2036
2189
  };
2037
- const path = join7(managedDir, "local-config.json");
2190
+ const path = join8(managedDir, "local-config.json");
2038
2191
  const tmp = `${path}.${process.pid}.tmp`;
2039
2192
  try {
2040
2193
  writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
@@ -2053,25 +2206,30 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
2053
2206
  const written = [];
2054
2207
  const skipped = [];
2055
2208
  for (const { file } of PERSONA_SOURCES) {
2056
- const target = join7(dataDir, file);
2209
+ const target = join8(dataDir, file);
2057
2210
  if (exists(target)) {
2058
2211
  skipped.push(target);
2059
2212
  continue;
2060
2213
  }
2061
- writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
2214
+ writeFile(target, readFileSync8(join8(defaultsDir, file), "utf8"));
2062
2215
  written.push(target);
2063
2216
  }
2064
2217
  return { written, skipped };
2065
2218
  }
2066
2219
  function createLocalConfigReporter(managedDir, dataDir, now) {
2067
2220
  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;
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 };
2072
2227
  return wrote;
2073
2228
  };
2074
2229
  }
2230
+ function hasLlmKeyFor(source) {
2231
+ return source.origin === "managed" && source.apiKey !== void 0;
2232
+ }
2075
2233
 
2076
2234
  // src/warning-filter.ts
2077
2235
  function installWarningFilter(proc = process) {
@@ -2086,7 +2244,7 @@ function installWarningFilter(proc = process) {
2086
2244
 
2087
2245
  // src/index.ts
2088
2246
  if (process.argv.includes("--version")) {
2089
- 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"));
2090
2248
  console.log(pkg.version);
2091
2249
  process.exit(0);
2092
2250
  }
@@ -2103,12 +2261,13 @@ function makeLogger(cfg) {
2103
2261
  function safeChannelSegment(channelId) {
2104
2262
  return channelId.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
2105
2263
  }
2106
- function endpointHost(endpoint) {
2107
- try {
2108
- return new URL(endpoint).host;
2109
- } catch {
2110
- return "(unparseable endpoint)";
2111
- }
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
+ };
2112
2271
  }
2113
2272
  function realServiceDeps() {
2114
2273
  return {
@@ -2255,20 +2414,12 @@ async function main() {
2255
2414
  return;
2256
2415
  }
2257
2416
  const log = makeLogger(cfg);
2258
- let apiKey;
2259
- try {
2260
- apiKey = resolveLlmApiKey(cfg, (p) => readFileSync9(p, "utf8"));
2261
- } catch (e) {
2262
- console.error(`securecoms-agent: ${e instanceof Error ? e.message : String(e)}`);
2263
- process.exit(2);
2264
- return;
2265
- }
2266
- const llm = new LLMDispatcher(cfg.llmEndpoint, cfg.llmModel, { apiKey });
2417
+ const llm = buildLlmShim(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps());
2267
2418
  const memories = /* @__PURE__ */ new Map();
2268
2419
  const memoryFor = (channelId) => {
2269
2420
  let mem = memories.get(channelId);
2270
2421
  if (!mem) {
2271
- mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2422
+ mem = new AgentMemory(join9(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2272
2423
  memories.set(channelId, mem);
2273
2424
  }
2274
2425
  return mem;
@@ -2295,9 +2446,16 @@ async function main() {
2295
2446
  };
2296
2447
  const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
2297
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
+ };
2298
2456
  migrateMemoryIntoManaged(managedDir, cfg.dataDir);
2299
2457
  const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
2300
- reportLocalConfig(hasSearchKey());
2458
+ reportLocalConfig({ hasSearchKey: hasSearchKey(), hasLlmKey: hasLlmKey() });
2301
2459
  const builtinCtx = {
2302
2460
  dataDir: cfg.dataDir,
2303
2461
  configDirs,
@@ -2321,7 +2479,7 @@ async function main() {
2321
2479
  const sc = currentSkillsConfig();
2322
2480
  const g = createLocalToolGate(policyFor());
2323
2481
  const searchKey = hasSearchKey();
2324
- reportLocalConfig(searchKey);
2482
+ reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey() });
2325
2483
  return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
2326
2484
  };
2327
2485
  for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
@@ -2344,7 +2502,7 @@ async function main() {
2344
2502
  })
2345
2503
  );
2346
2504
  console.log(
2347
- `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}`
2348
2506
  );
2349
2507
  await plane.start();
2350
2508
  const shutdown = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"