@securecoms/agent-runtime 0.4.0 → 0.6.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 +271 -73
  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];
@@ -458,10 +440,16 @@ var LLMDispatcher = class {
458
440
  endpoint;
459
441
  model;
460
442
  apiKey;
443
+ chatPath;
444
+ chatAuth;
445
+ authHeaderName;
461
446
  constructor(endpoint, model, opts = {}) {
462
447
  this.endpoint = normalizeEndpoint(endpoint);
463
448
  this.model = model;
464
449
  this.apiKey = opts.apiKey;
450
+ this.chatPath = opts.chatPath ?? "/v1/chat/completions";
451
+ this.chatAuth = opts.chatAuth ?? "bearer";
452
+ this.authHeaderName = opts.authHeaderName ?? "x-api-key";
465
453
  }
466
454
  async complete(messages, tools) {
467
455
  const body = {
@@ -472,10 +460,12 @@ var LLMDispatcher = class {
472
460
  if (tools && tools.length > 0) {
473
461
  body.tools = tools;
474
462
  }
475
- const url = `${this.endpoint}/v1/chat/completions`;
463
+ const url = `${this.endpoint}${this.chatPath}`;
476
464
  const headers = { "Content-Type": "application/json" };
477
- if (this.apiKey) {
465
+ if (this.apiKey && this.chatAuth === "bearer") {
478
466
  headers.Authorization = `Bearer ${this.apiKey}`;
467
+ } else if (this.apiKey && this.chatAuth === "x-api-key") {
468
+ headers[this.authHeaderName] = this.apiKey;
479
469
  }
480
470
  const resp = await fetch(url, {
481
471
  method: "POST",
@@ -499,9 +489,211 @@ var LLMDispatcher = class {
499
489
  }
500
490
  };
501
491
 
492
+ // src/llm-source.ts
493
+ import { join as join2 } from "node:path";
494
+
495
+ // src/llm-providers.json
496
+ var llm_providers_default = {
497
+ openrouter: { chat_base: "https://openrouter.ai/api/v1", chat_path: "/v1/chat/completions", chat_auth: "bearer", models_url: "https://openrouter.ai/api/v1/models", models_id_path: "data[].id", validate_url: "https://openrouter.ai/api/v1/key", probe_auth: "bearer", extra_headers: null, catalog_prefix: "self", needs_key: true },
498
+ openai: { chat_base: "https://api.openai.com/v1", chat_path: "/v1/chat/completions", chat_auth: "bearer", models_url: "https://api.openai.com/v1/models", models_id_path: "data[].id", validate_url: "https://api.openai.com/v1/models", probe_auth: "bearer", extra_headers: null, catalog_prefix: "openai/", needs_key: true },
499
+ anthropic: { chat_base: "https://api.anthropic.com/v1", chat_path: "/v1/chat/completions", chat_auth: "bearer", models_url: "https://api.anthropic.com/v1/models", models_id_path: "data[].id", validate_url: "https://api.anthropic.com/v1/models", probe_auth: "x-api-key", extra_headers: { "anthropic-version": "2023-06-01" }, catalog_prefix: "anthropic/", needs_key: true },
500
+ gemini: { chat_base: "https://generativelanguage.googleapis.com/v1beta/openai", chat_path: "/chat/completions", chat_auth: "bearer", models_url: "https://generativelanguage.googleapis.com/v1beta/openai/models", models_id_path: "data[].id", validate_url: "https://generativelanguage.googleapis.com/v1beta/openai/models", probe_auth: "bearer", extra_headers: null, catalog_prefix: "google/", needs_key: true },
501
+ meta: { chat_base: "https://api.meta.ai/v1", chat_path: "/v1/chat/completions", chat_auth: "bearer", models_url: "https://api.meta.ai/v1/models", models_id_path: "data[].id", validate_url: "https://api.meta.ai/v1/models", probe_auth: "bearer", extra_headers: null, catalog_prefix: null, needs_key: true },
502
+ xai: { chat_base: "https://api.x.ai/v1", chat_path: "/v1/chat/completions", chat_auth: "bearer", models_url: "https://api.x.ai/v1/models", models_id_path: "data[].id", validate_url: "https://api.x.ai/v1/models", probe_auth: "bearer", extra_headers: null, catalog_prefix: "x-ai/", needs_key: true },
503
+ local: { chat_base: "http://127.0.0.1:11434", chat_path: "/v1/chat/completions", chat_auth: "none", models_url: "http://127.0.0.1:11434/api/tags", models_id_path: "models[].name", validate_url: null, probe_auth: "none", extra_headers: null, catalog_prefix: null, needs_key: false }
504
+ };
505
+
506
+ // src/llm-key.ts
507
+ var LlmKeyFileError = class extends Error {
508
+ };
509
+ function resolveLlmApiKey(cfg, readFile) {
510
+ const path = cfg.llmApiKeyFile;
511
+ if (!path) return cfg.llmApiKey;
512
+ let raw;
513
+ try {
514
+ raw = readFile(path);
515
+ } catch (e) {
516
+ throw new LlmKeyFileError(
517
+ `cannot read LLM key file ${path}: ${e instanceof Error ? e.message : String(e)}`
518
+ );
519
+ }
520
+ const key = raw.trim();
521
+ if (!key) throw new LlmKeyFileError(`cannot read LLM key file ${path}: file is empty`);
522
+ return key;
523
+ }
524
+
525
+ // src/llm-source.ts
526
+ var PROVIDER_TABLE = Object.freeze(Object.fromEntries(
527
+ Object.entries(llm_providers_default).map(([p, e]) => [p, Object.freeze({
528
+ endpoint: e.chat_base,
529
+ needsKey: e.needs_key,
530
+ chatPath: e.chat_path,
531
+ chatAuth: e.chat_auth
532
+ })])
533
+ ));
534
+ var LlmSourceUnavailableError = class extends Error {
535
+ };
536
+ function providerOrigin(provider) {
537
+ const entry = PROVIDER_TABLE[provider];
538
+ if (!entry) throw new LlmSourceUnavailableError(`unknown LLM provider ${JSON.stringify(provider)}`);
539
+ const u = new URL(entry.endpoint);
540
+ return `${u.protocol}//${u.host}`.toLowerCase();
541
+ }
542
+ function providerSlug(provider) {
543
+ return providerOrigin(provider).replace("://", "_").replace(/:/g, "-");
544
+ }
545
+ var explicitEndpointSet = (env) => Boolean(env.SECURECOMS_LLM_ENDPOINT || env.SECURECOMS_LLM_API_KEY_FILE || env.SECURECOMS_LLM_API_KEY);
546
+ function isExplicitSource(cfg, env) {
547
+ return explicitEndpointSet(env) || Boolean(cfg.llmApiKeyFile) || Boolean(cfg.llmApiKey);
548
+ }
549
+ function resolveLlmSource(cfg, explicit, deps) {
550
+ if (cfg.llmSource) {
551
+ throw new LlmSourceUnavailableError(
552
+ `SECURECOMS_LLM_SOURCE=${JSON.stringify(cfg.llmSource)} is not a supported LLM source`
553
+ );
554
+ }
555
+ if (explicit) {
556
+ return {
557
+ endpoint: cfg.llmEndpoint,
558
+ model: cfg.llmModel,
559
+ apiKey: resolveLlmApiKey(cfg, deps.readFile),
560
+ provider: "env",
561
+ origin: "env",
562
+ chatPath: "/v1/chat/completions",
563
+ chatAuth: "bearer"
564
+ };
565
+ }
566
+ const managedDir = join2(deps.home, ".securecoms", "runtime-config", cfg.identityId);
567
+ const sourcePath = join2(managedDir, "llm-source.json");
568
+ let raw;
569
+ try {
570
+ raw = deps.readFile(sourcePath);
571
+ } catch (e) {
572
+ if (e.code !== "ENOENT") {
573
+ throw new LlmSourceUnavailableError(
574
+ `cannot read ${sourcePath}: ${e instanceof Error ? e.message : String(e)}`
575
+ );
576
+ }
577
+ }
578
+ if (raw !== void 0) {
579
+ let parsed;
580
+ try {
581
+ parsed = JSON.parse(raw);
582
+ } catch (e) {
583
+ throw new LlmSourceUnavailableError(
584
+ `invalid JSON in ${sourcePath}: ${e instanceof Error ? e.message : String(e)}`
585
+ );
586
+ }
587
+ const { provider, model } = parsed;
588
+ if (typeof provider !== "string" || typeof model !== "string") {
589
+ throw new LlmSourceUnavailableError(`${sourcePath} is missing "provider" or "model"`);
590
+ }
591
+ if (!Object.hasOwn(llm_providers_default, provider)) {
592
+ throw new LlmSourceUnavailableError(
593
+ `unknown LLM provider ${JSON.stringify(provider)} in ${sourcePath}`
594
+ );
595
+ }
596
+ const entry = PROVIDER_TABLE[provider];
597
+ if (!entry) {
598
+ throw new LlmSourceUnavailableError(
599
+ `unknown LLM provider ${JSON.stringify(provider)} in ${sourcePath}`
600
+ );
601
+ }
602
+ let apiKey;
603
+ if (entry.needsKey) {
604
+ const expectedOrigin = providerOrigin(provider);
605
+ const slug = providerSlug(provider);
606
+ const keyPath = join2(managedDir, `llm.key.${slug}`);
607
+ const hostPath = join2(managedDir, `llm.key.${slug}.host`);
608
+ const legacyKey = join2(managedDir, "llm.key");
609
+ const legacyHost = join2(managedDir, "llm.key.host");
610
+ const useLegacy = !deps.exists(keyPath) && deps.exists(legacyKey) && deps.exists(legacyHost);
611
+ const boundKey = useLegacy ? legacyKey : keyPath;
612
+ const boundHost = useLegacy ? legacyHost : hostPath;
613
+ if (!deps.exists(boundKey)) {
614
+ throw new LlmSourceUnavailableError(
615
+ `${keyPath} does not exist \u2014 no key has been written for this managed source, refusing to read it`
616
+ );
617
+ }
618
+ let mode;
619
+ try {
620
+ ({ mode } = deps.statFile(boundKey));
621
+ } catch (e) {
622
+ throw new LlmSourceUnavailableError(
623
+ `cannot stat ${boundKey}: ${e instanceof Error ? e.message : String(e)}`
624
+ );
625
+ }
626
+ if ((mode & 63) !== 0) {
627
+ throw new LlmSourceUnavailableError(
628
+ `${boundKey} is readable by other users on this machine \u2014 tighten its permissions to owner-only`
629
+ );
630
+ }
631
+ if (!deps.exists(boundHost)) {
632
+ throw new LlmSourceUnavailableError(
633
+ `${boundKey} has no ${boundHost} sidecar recording which origin it belongs to \u2014 refusing to send it to ${expectedOrigin}`
634
+ );
635
+ }
636
+ const actualOrigin = deps.readFile(boundHost).trim();
637
+ if (actualOrigin !== expectedOrigin) {
638
+ throw new LlmSourceUnavailableError(
639
+ `${boundHost} is bound to ${actualOrigin}, but provider ${JSON.stringify(provider)} requires ${expectedOrigin} \u2014 refusing to send the key across origins`
640
+ );
641
+ }
642
+ apiKey = resolveLlmApiKey({ llmApiKeyFile: boundKey }, deps.readFile);
643
+ }
644
+ return {
645
+ endpoint: entry.endpoint,
646
+ model,
647
+ ...apiKey !== void 0 ? { apiKey } : {},
648
+ provider,
649
+ origin: "managed",
650
+ chatPath: entry.chatPath,
651
+ chatAuth: entry.chatAuth
652
+ };
653
+ }
654
+ return {
655
+ endpoint: cfg.llmEndpoint,
656
+ model: cfg.llmModel,
657
+ provider: "default",
658
+ origin: "default",
659
+ chatPath: "/v1/chat/completions",
660
+ chatAuth: "bearer"
661
+ };
662
+ }
663
+
664
+ // src/llm-shim.ts
665
+ function endpointHost(endpoint) {
666
+ try {
667
+ return new URL(endpoint).host;
668
+ } catch {
669
+ return "(unparseable endpoint)";
670
+ }
671
+ }
672
+ function buildLlmShim(cfg, explicit, deps) {
673
+ return {
674
+ async complete(messages, tools) {
675
+ const src = resolveLlmSource(cfg, explicit, deps);
676
+ const dispatcher = new LLMDispatcher(src.endpoint, src.model, {
677
+ apiKey: src.apiKey,
678
+ chatPath: src.chatPath,
679
+ chatAuth: src.chatAuth
680
+ });
681
+ return dispatcher.complete(messages, tools);
682
+ }
683
+ };
684
+ }
685
+ function bootLine(cfg, explicit, deps) {
686
+ try {
687
+ const src = resolveLlmSource(cfg, explicit, deps);
688
+ return `securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm source=${src.origin} ${endpointHost(src.endpoint)} (${src.model})`;
689
+ } catch (e) {
690
+ return `llm: source unavailable \u2014 ${e instanceof Error ? e.message : String(e)}`;
691
+ }
692
+ }
693
+
502
694
  // src/memory.ts
503
695
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
504
- import { join as join2 } from "node:path";
696
+ import { join as join3 } from "node:path";
505
697
  var MAX_HISTORY = 50;
506
698
  var AgentMemory = class {
507
699
  history = [];
@@ -566,10 +758,10 @@ ${memories}`;
566
758
  }
567
759
  }
568
760
  get historyPath() {
569
- return join2(this.dataDir, "conversation-history.json");
761
+ return join3(this.dataDir, "conversation-history.json");
570
762
  }
571
763
  get memoriesPath() {
572
- return join2(this.dataDir, "memories.json");
764
+ return join3(this.dataDir, "memories.json");
573
765
  }
574
766
  loadHistory() {
575
767
  try {
@@ -609,7 +801,7 @@ ${memories}`;
609
801
 
610
802
  // src/skill-invoker.ts
611
803
  import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync5, statSync } from "node:fs";
612
- import { join as join5 } from "node:path";
804
+ import { join as join6 } from "node:path";
613
805
 
614
806
  // src/skill-md.ts
615
807
  var SkillMdError = class extends Error {
@@ -770,16 +962,16 @@ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
770
962
  // src/config-dirs.ts
771
963
  import { existsSync as existsSync2 } from "node:fs";
772
964
  import { homedir as homedir2 } from "node:os";
773
- import { join as join3 } from "node:path";
965
+ import { join as join4 } from "node:path";
774
966
  function managedConfigDir(identityId, home = homedir2()) {
775
- return join3(home, ".securecoms", "runtime-config", identityId);
967
+ return join4(home, ".securecoms", "runtime-config", identityId);
776
968
  }
777
969
  function configSearchPath(identityId, dataDir, home = homedir2()) {
778
970
  return [managedConfigDir(identityId, home), dataDir];
779
971
  }
780
972
  function findConfigFile(name, dirs) {
781
973
  for (const dir of dirs) {
782
- const path = join3(dir, name);
974
+ const path = join4(dir, name);
783
975
  if (existsSync2(path)) return path;
784
976
  }
785
977
  return null;
@@ -1118,7 +1310,7 @@ var httpRequest = async (args, ctx) => {
1118
1310
 
1119
1311
  // src/long-term-memory.ts
1120
1312
  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";
1313
+ import { join as join5 } from "node:path";
1122
1314
  var MEMORY_FILE = "MEMORY.md";
1123
1315
  var MEMORY_MAX_BYTES = 8192;
1124
1316
  var FACT_MAX_CHARS = 300;
@@ -1129,13 +1321,13 @@ function readLongTermMemory(dirs) {
1129
1321
  return body || null;
1130
1322
  }
1131
1323
  function memoryTarget(dirs) {
1132
- for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
1324
+ for (const d of dirs) if (existsSync4(join5(d, MEMORY_FILE))) return d;
1133
1325
  for (const d of dirs) if (existsSync4(d)) return d;
1134
1326
  return dirs[dirs.length - 1];
1135
1327
  }
1136
1328
  function migrateMemoryIntoManaged(managedDir, dataDir) {
1137
- const managed = join4(managedDir, MEMORY_FILE);
1138
- const local = join4(dataDir, MEMORY_FILE);
1329
+ const managed = join5(managedDir, MEMORY_FILE);
1330
+ const local = join5(dataDir, MEMORY_FILE);
1139
1331
  if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
1140
1332
  copyFileSync(local, managed);
1141
1333
  return true;
@@ -1153,7 +1345,7 @@ function rememberFact(dirs, fact, now) {
1153
1345
  `, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
1154
1346
  lines = lines.slice(1);
1155
1347
  }
1156
- writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
1348
+ writeFileSync2(join5(dir, MEMORY_FILE), `${lines.join("\n")}
1157
1349
  `, { mode: 384 });
1158
1350
  return line;
1159
1351
  }
@@ -1415,11 +1607,11 @@ var SkillInvoker = class {
1415
1607
  const flat = [], manifests = [], skillMds = [];
1416
1608
  for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
1417
1609
  if (entry.isFile() && entry.name.endsWith(".json")) {
1418
- flat.push(join5(this.skillsDir, entry.name));
1610
+ flat.push(join6(this.skillsDir, entry.name));
1419
1611
  } else if (entry.isDirectory()) {
1420
- const m = join5(this.skillsDir, entry.name, "manifest.json");
1612
+ const m = join6(this.skillsDir, entry.name, "manifest.json");
1421
1613
  if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
1422
- const s = join5(this.skillsDir, entry.name, "SKILL.md");
1614
+ const s = join6(this.skillsDir, entry.name, "SKILL.md");
1423
1615
  if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
1424
1616
  }
1425
1617
  }
@@ -1609,7 +1801,7 @@ function createDynamicToolGate(policyFor) {
1609
1801
  import { readFileSync as readFileSync7 } from "node:fs";
1610
1802
  import { WebSocket } from "node:http";
1611
1803
  import { homedir as homedir3 } from "node:os";
1612
- import { join as join6 } from "node:path";
1804
+ import { join as join7 } from "node:path";
1613
1805
  var SEEN_MAX = 5e3;
1614
1806
  function errText(e) {
1615
1807
  return e instanceof Error ? e.message : String(e);
@@ -1652,8 +1844,8 @@ var SecureComsDataPlane = class {
1652
1844
  generation = 0;
1653
1845
  constructor(cfg, onMessage) {
1654
1846
  this.identityId = cfg.identityId;
1655
- const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
1656
- this.connectionPath = join6(dir, `${cfg.identityId}.json`);
1847
+ const dir = cfg.connectionsDir ?? join7(homedir3(), ".securecoms", "connections");
1848
+ this.connectionPath = join7(dir, `${cfg.identityId}.json`);
1657
1849
  this.establishedMs = cfg.establishedMs ?? 5e3;
1658
1850
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
1659
1851
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
@@ -1969,7 +2161,7 @@ ${longTerm}` });
1969
2161
 
1970
2162
  // src/persona.ts
1971
2163
  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";
2164
+ import { dirname as dirname3, join as join8 } from "node:path";
1973
2165
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1974
2166
  var PERSONA_MAX_BYTES = 8192;
1975
2167
  var PERSONA_SOURCES = [
@@ -1985,7 +2177,7 @@ var SAFETY_PREAMBLE = [
1985
2177
  "- Only use the tools you were given; do not invent capabilities."
1986
2178
  ].join("\n");
1987
2179
  function defaultPersonaDir() {
1988
- return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
2180
+ return join8(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
1989
2181
  }
1990
2182
  function assemblePrompt(parts) {
1991
2183
  return parts.map((p) => `${p.heading}
@@ -2017,11 +2209,11 @@ function systemPromptFor(dirs, fallback) {
2017
2209
 
2018
2210
  ${SAFETY_PREAMBLE}`;
2019
2211
  }
2020
- function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2212
+ function writeLocalConfigReport(managedDir, dataDir, now, flags) {
2021
2213
  if (!existsSync6(managedDir)) return false;
2022
2214
  const persona = {};
2023
2215
  for (const { file } of PERSONA_SOURCES) {
2024
- const path2 = join7(dataDir, file);
2216
+ const path2 = join8(dataDir, file);
2025
2217
  if (!existsSync6(path2)) continue;
2026
2218
  try {
2027
2219
  persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
@@ -2031,10 +2223,11 @@ function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
2031
2223
  const doc = {
2032
2224
  schema: "securecoms.local-config/1",
2033
2225
  written_at: now.toISOString(),
2034
- has_search_key: hasSearchKey,
2226
+ has_search_key: flags.hasSearchKey,
2227
+ has_llm_key: flags.hasLlmKey,
2035
2228
  persona
2036
2229
  };
2037
- const path = join7(managedDir, "local-config.json");
2230
+ const path = join8(managedDir, "local-config.json");
2038
2231
  const tmp = `${path}.${process.pid}.tmp`;
2039
2232
  try {
2040
2233
  writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
@@ -2053,25 +2246,30 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
2053
2246
  const written = [];
2054
2247
  const skipped = [];
2055
2248
  for (const { file } of PERSONA_SOURCES) {
2056
- const target = join7(dataDir, file);
2249
+ const target = join8(dataDir, file);
2057
2250
  if (exists(target)) {
2058
2251
  skipped.push(target);
2059
2252
  continue;
2060
2253
  }
2061
- writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
2254
+ writeFile(target, readFileSync8(join8(defaultsDir, file), "utf8"));
2062
2255
  written.push(target);
2063
2256
  }
2064
2257
  return { written, skipped };
2065
2258
  }
2066
2259
  function createLocalConfigReporter(managedDir, dataDir, now) {
2067
2260
  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;
2261
+ return (flags) => {
2262
+ if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey) {
2263
+ return false;
2264
+ }
2265
+ const wrote = writeLocalConfigReport(managedDir, dataDir, now(), flags);
2266
+ if (wrote) lastWritten = { ...flags };
2072
2267
  return wrote;
2073
2268
  };
2074
2269
  }
2270
+ function hasLlmKeyFor(source) {
2271
+ return source.origin === "managed" && source.apiKey !== void 0;
2272
+ }
2075
2273
 
2076
2274
  // src/warning-filter.ts
2077
2275
  function installWarningFilter(proc = process) {
@@ -2086,7 +2284,7 @@ function installWarningFilter(proc = process) {
2086
2284
 
2087
2285
  // src/index.ts
2088
2286
  if (process.argv.includes("--version")) {
2089
- const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2287
+ const pkg = JSON.parse(readFileSync9(join9(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
2090
2288
  console.log(pkg.version);
2091
2289
  process.exit(0);
2092
2290
  }
@@ -2103,12 +2301,13 @@ function makeLogger(cfg) {
2103
2301
  function safeChannelSegment(channelId) {
2104
2302
  return channelId.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
2105
2303
  }
2106
- function endpointHost(endpoint) {
2107
- try {
2108
- return new URL(endpoint).host;
2109
- } catch {
2110
- return "(unparseable endpoint)";
2111
- }
2304
+ function realLlmSourceDeps() {
2305
+ return {
2306
+ home: homedir4(),
2307
+ readFile: (p) => readFileSync9(p, "utf8"),
2308
+ statFile: (p) => ({ mode: statSync2(p).mode }),
2309
+ exists: (p) => existsSync7(p)
2310
+ };
2112
2311
  }
2113
2312
  function realServiceDeps() {
2114
2313
  return {
@@ -2255,20 +2454,12 @@ async function main() {
2255
2454
  return;
2256
2455
  }
2257
2456
  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 });
2457
+ const llm = buildLlmShim(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps());
2267
2458
  const memories = /* @__PURE__ */ new Map();
2268
2459
  const memoryFor = (channelId) => {
2269
2460
  let mem = memories.get(channelId);
2270
2461
  if (!mem) {
2271
- mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2462
+ mem = new AgentMemory(join9(cfg.dataDir, "channels", safeChannelSegment(channelId)));
2272
2463
  memories.set(channelId, mem);
2273
2464
  }
2274
2465
  return mem;
@@ -2295,9 +2486,16 @@ async function main() {
2295
2486
  };
2296
2487
  const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
2297
2488
  const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) => readFileSync9(p, "utf8"));
2489
+ const hasLlmKey = () => {
2490
+ try {
2491
+ return hasLlmKeyFor(resolveLlmSource(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps()));
2492
+ } catch {
2493
+ return false;
2494
+ }
2495
+ };
2298
2496
  migrateMemoryIntoManaged(managedDir, cfg.dataDir);
2299
2497
  const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
2300
- reportLocalConfig(hasSearchKey());
2498
+ reportLocalConfig({ hasSearchKey: hasSearchKey(), hasLlmKey: hasLlmKey() });
2301
2499
  const builtinCtx = {
2302
2500
  dataDir: cfg.dataDir,
2303
2501
  configDirs,
@@ -2321,7 +2519,7 @@ async function main() {
2321
2519
  const sc = currentSkillsConfig();
2322
2520
  const g = createLocalToolGate(policyFor());
2323
2521
  const searchKey = hasSearchKey();
2324
- reportLocalConfig(searchKey);
2522
+ reportLocalConfig({ hasSearchKey: searchKey, hasLlmKey: hasLlmKey() });
2325
2523
  return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
2326
2524
  };
2327
2525
  for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
@@ -2344,7 +2542,7 @@ async function main() {
2344
2542
  })
2345
2543
  );
2346
2544
  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}`
2545
+ `${bootLine(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps())}, ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
2348
2546
  );
2349
2547
  await plane.start();
2350
2548
  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.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"