@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.
- package/README.md +17 -1
- package/dist/bin.js +253 -79
- 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
|
|
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
|
|
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
|
|
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
|
|
721
|
+
return join3(this.dataDir, "conversation-history.json");
|
|
570
722
|
}
|
|
571
723
|
get memoriesPath() {
|
|
572
|
-
return
|
|
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
|
|
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
|
|
925
|
+
import { join as join4 } from "node:path";
|
|
774
926
|
function managedConfigDir(identityId, home = homedir2()) {
|
|
775
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
1127
|
-
const local =
|
|
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(
|
|
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(
|
|
1570
|
+
flat.push(join6(this.skillsDir, entry.name));
|
|
1408
1571
|
} else if (entry.isDirectory()) {
|
|
1409
|
-
const m =
|
|
1572
|
+
const m = join6(this.skillsDir, entry.name, "manifest.json");
|
|
1410
1573
|
if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
|
|
1411
|
-
const s =
|
|
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
|
|
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 ??
|
|
1645
|
-
this.connectionPath =
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 (
|
|
2058
|
-
if (lastWritten === hasSearchKey)
|
|
2059
|
-
|
|
2060
|
-
|
|
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(
|
|
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
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
2277
|
-
const
|
|
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
|
-
|
|
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 = () => {
|