@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.
- package/README.md +17 -1
- package/dist/bin.js +271 -73
- 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];
|
|
@@ -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}
|
|
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
|
|
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
|
|
761
|
+
return join3(this.dataDir, "conversation-history.json");
|
|
570
762
|
}
|
|
571
763
|
get memoriesPath() {
|
|
572
|
-
return
|
|
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
|
|
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
|
|
965
|
+
import { join as join4 } from "node:path";
|
|
774
966
|
function managedConfigDir(identityId, home = homedir2()) {
|
|
775
|
-
return
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
1138
|
-
const local =
|
|
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(
|
|
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(
|
|
1610
|
+
flat.push(join6(this.skillsDir, entry.name));
|
|
1419
1611
|
} else if (entry.isDirectory()) {
|
|
1420
|
-
const m =
|
|
1612
|
+
const m = join6(this.skillsDir, entry.name, "manifest.json");
|
|
1421
1613
|
if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
|
|
1422
|
-
const s =
|
|
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
|
|
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 ??
|
|
1656
|
-
this.connectionPath =
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 (
|
|
2069
|
-
if (lastWritten === hasSearchKey)
|
|
2070
|
-
|
|
2071
|
-
|
|
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(
|
|
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
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 = () => {
|