@securecoms/agent-runtime 0.5.0 → 0.7.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 +52 -29
- package/dist/bin.js +445 -255
- package/package.json +2 -2
- package/skills/remember/SKILL.md +0 -13
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ All configuration is via `SECURECOMS_*` environment variables (see
|
|
|
98
98
|
| `SECURECOMS_DATA_DIR` | `~/.securecoms/agent-runtime` | Root directory for per-channel state |
|
|
99
99
|
| `SECURECOMS_SKILLS_CONFIG` | `<dataDir>/skills.json` (or `--skills-config <path>`) | Path to `skills.json` — named endpoints, databases and flags |
|
|
100
100
|
| `SECURECOMS_LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, `error` |
|
|
101
|
-
| `SECURECOMS_TOOL_ALLOW` | `echo`, `web_search`, `fetch_url`, `http_request`, `sql_query`, `current_time
|
|
101
|
+
| `SECURECOMS_TOOL_ALLOW` | `echo`, `web_search`, `fetch_url`, `http_request`, `sql_query`, `current_time` | Comma-separated tool allow-list |
|
|
102
102
|
| `SECURECOMS_TOOL_DENY` | unset | Comma-separated tool deny-list (wins over allow) |
|
|
103
103
|
| `SECURECOMS_TOOL_DEFAULT` | `deny` | Fallback policy for a tool matched by neither list |
|
|
104
104
|
|
|
@@ -205,7 +205,7 @@ the line — for example `requires: [endpoint:crm]` in
|
|
|
205
205
|
|
|
206
206
|
## Curated skills
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
Six built-in handlers ship with the runtime. All network built-ins refuse
|
|
209
209
|
`localhost` and any private, loopback or link-local resolved address, and
|
|
210
210
|
every tool result is capped at 20 kB (truncated, not an error). A redirect to a
|
|
211
211
|
different origin is followed without the caller's headers and without the
|
|
@@ -220,7 +220,6 @@ to a host the operator did not name.
|
|
|
220
220
|
| `web_search` | `query`, `count` (1–10, default 5) | `requires: [search_provider]` — needs a readable, non-empty `SECURECOMS_BRAVE_KEY_FILE` or `SECURECOMS_TAVILY_KEY_FILE`; Brave is used first when both are set. |
|
|
221
221
|
| `http_request` | `name` (a configured `endpoint:<name>`), `path` (a query string is kept, a `#fragment` is dropped), `method` (GET/POST/PUT/DELETE), `body` | `requires: [any_endpoint]`. The model names an endpoint and a path, never a host; a path that would leave the endpoint's `baseUrl` origin is refused. |
|
|
222
222
|
| `sql_query` | `name` (a configured `database:<name>`), `sql` (read-only `SELECT` only; a `;` or a write keyword anywhere in the text — inside a string literal or a comment included — is refused) | `requires: [any_database]`. SQLite or Postgres (`pg` required separately for Postgres). Results capped at 200 rows. |
|
|
223
|
-
| `remember` | `fact` (≤300 chars) | Appends a dated line to the agent-wide `MEMORY.md`. |
|
|
224
223
|
|
|
225
224
|
## skills.json
|
|
226
225
|
|
|
@@ -283,12 +282,16 @@ file that already exists, so a customized `IDENTITY.md` is safe to keep
|
|
|
283
282
|
across a re-init. `securecoms-agent persona show` prints the assembled system
|
|
284
283
|
prompt exactly as a turn would see it, useful for checking an edit landed.
|
|
285
284
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
Legacy `MEMORY.md` files are not active configuration. Earlier runtime versions
|
|
286
|
+
injected one agent-wide file into every channel, which crossed channel
|
|
287
|
+
audiences. On startup the runtime moves each legacy file to a mode-0600
|
|
288
|
+
`MEMORY.quarantined*.md` file in the same directory without overwriting an
|
|
289
|
+
existing quarantine. The quarantined content is never injected or writable by
|
|
290
|
+
a tool. The runtime inventories that quarantine plus inactive per-channel JSON
|
|
291
|
+
stores for the owner-review surface. Review offers a passphrase-encrypted
|
|
292
|
+
export and exact-source deletion; the 90-day clock begins only after the
|
|
293
|
+
machine successfully reports the source. Explicit fact-to-channel assignment
|
|
294
|
+
is a separate migration step and never guesses or fans out an audience.
|
|
292
295
|
|
|
293
296
|
## Managed configuration (from the web app)
|
|
294
297
|
|
|
@@ -307,7 +310,7 @@ managed directory at all behaves exactly as it did before. `skills.json` is
|
|
|
307
310
|
looked up the same way, and re-read on every turn — a new endpoint or a skill
|
|
308
311
|
switched off in the app takes effect on the next message, with no restart.
|
|
309
312
|
|
|
310
|
-
|
|
313
|
+
One deliberate exception:
|
|
311
314
|
|
|
312
315
|
- **The environment still wins for the tool policy.** `SECURECOMS_TOOL_ALLOW`
|
|
313
316
|
overrides the `allow` list in the `tools` block of `skills.json`, so an
|
|
@@ -316,35 +319,55 @@ Two deliberate exceptions:
|
|
|
316
319
|
rather than replacing it: deny is a safety mechanism, so one entry in the
|
|
317
320
|
service unit must not silently un-deny every tool the owner switched off in
|
|
318
321
|
the app. Everything the environment denies stays denied either way.
|
|
319
|
-
- **Writes go to the data dir**, not the managed directory — except `MEMORY.md`,
|
|
320
|
-
which is written wherever the agent already keeps it. An existing
|
|
321
|
-
`<dataDir>/MEMORY.md` is copied into the managed directory once, at startup,
|
|
322
|
-
so the owner can read and prune it in the app.
|
|
323
322
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
whether it has a readable search key. It is written at startup and rewritten
|
|
327
|
-
whenever the search-key answer changes, so a key file dropped in (or taken
|
|
328
|
-
away) mid-run is reported without a restart. It is a report the daemon passes
|
|
329
|
-
upward — nothing in the runtime reads it back, and it holds no key value.
|
|
323
|
+
Runtime-owned writes go to the data directory, not the managed directory.
|
|
324
|
+
Legacy `MEMORY.md` is handled only by the quarantine behavior described above.
|
|
330
325
|
|
|
331
|
-
|
|
326
|
+
The runtime also writes mode-0600 `local-config.json` into the managed directory
|
|
327
|
+
when one exists: what this agent's OWN persona files say, booleans for readable
|
|
328
|
+
search/managed-LLM keys, and a bounded inventory of inert legacy-memory files.
|
|
329
|
+
It is written at startup and rewritten whenever those answers or the inventory
|
|
330
|
+
change. The daemon revalidates current regular-file bytes, removes local paths,
|
|
331
|
+
and only then reports it upward. Nothing in the runtime reads the report back,
|
|
332
|
+
and it holds no key value.
|
|
333
|
+
|
|
334
|
+
### Per-turn channel authorization
|
|
335
|
+
|
|
336
|
+
Installation configuration is only a ceiling. On every inbound message the
|
|
337
|
+
bridge supplies an immutable, server-derived authorization claim containing
|
|
338
|
+
the sender audience, membership state, and channel tool grants. The runtime
|
|
339
|
+
intersects that claim with installed skills, configuration requirements, and
|
|
340
|
+
the local allow/deny gate before showing tools to the model, then repeats the
|
|
341
|
+
same channel-grant check immediately before execution. Missing or malformed
|
|
342
|
+
claims, inactive/unknown membership, and absent grants all fail closed.
|
|
343
|
+
|
|
344
|
+
This staged release issues no channel grants yet, so no generic tool is offered
|
|
345
|
+
even when it is installed and enabled. Read grants can enable only the curated
|
|
346
|
+
read-only tools; `http_request` is additionally limited to GET at execution.
|
|
347
|
+
Side-effect grant fields are reserved in the protocol, but side-effect calls
|
|
348
|
+
remain denied until exact-operation confirmation, expiry, and idempotency are
|
|
349
|
+
implemented end to end.
|
|
350
|
+
|
|
351
|
+
### The generated tool paragraph is an installation summary
|
|
332
352
|
|
|
333
353
|
The `AGENTS.md` the app delivers ends in a generated paragraph naming the tools
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
354
|
+
configured for the installation, and explicitly tells the model that the
|
|
355
|
+
per-turn offered list is authoritative. The backend derives the installation
|
|
356
|
+
list from saved configuration plus the search-key boolean above; it does not
|
|
357
|
+
see the running process or a turn's channel grants. These things can therefore
|
|
358
|
+
make the installation summary differ from what the runtime offers:
|
|
337
359
|
|
|
338
360
|
- a `SECURECOMS_TOOL_ALLOW` or `SECURECOMS_TOOL_DENY` set in the service unit,
|
|
339
361
|
which the backend cannot see;
|
|
340
362
|
- a customer skill folder whose `requires:` is narrower than the shipped one, so
|
|
341
363
|
the runtime withholds a tool the backend counted;
|
|
342
|
-
- a custom skill the backend knows nothing about, so it
|
|
364
|
+
- a custom skill the backend knows nothing about, so it can be eligible but
|
|
365
|
+
unnamed;
|
|
366
|
+
- the current channel has no matching server-issued read grant.
|
|
343
367
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
a wrong permission.
|
|
368
|
+
The paragraph is therefore an advisory configuration summary, not an authority
|
|
369
|
+
claim. Channel authorization and the local gate are both enforced at call
|
|
370
|
+
time, so a configured but ungranted or denied tool is still refused.
|
|
348
371
|
|
|
349
372
|
## Provenance
|
|
350
373
|
|
package/dist/bin.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8 } 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 join8 } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, existsSync as
|
|
8
|
+
import { accessSync, constants, existsSync as existsSync6, mkdirSync, realpathSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9
9
|
import { homedir as homedir4, userInfo } from "node:os";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
@@ -57,8 +57,7 @@ var DEFAULT_TOOL_ALLOW = [
|
|
|
57
57
|
"fetch_url",
|
|
58
58
|
"http_request",
|
|
59
59
|
"sql_query",
|
|
60
|
-
"current_time"
|
|
61
|
-
"remember"
|
|
60
|
+
"current_time"
|
|
62
61
|
];
|
|
63
62
|
function loadConfig(argv, env) {
|
|
64
63
|
const identityId = parseIdentity(argv, env);
|
|
@@ -440,10 +439,16 @@ var LLMDispatcher = class {
|
|
|
440
439
|
endpoint;
|
|
441
440
|
model;
|
|
442
441
|
apiKey;
|
|
442
|
+
chatPath;
|
|
443
|
+
chatAuth;
|
|
444
|
+
authHeaderName;
|
|
443
445
|
constructor(endpoint, model, opts = {}) {
|
|
444
446
|
this.endpoint = normalizeEndpoint(endpoint);
|
|
445
447
|
this.model = model;
|
|
446
448
|
this.apiKey = opts.apiKey;
|
|
449
|
+
this.chatPath = opts.chatPath ?? "/v1/chat/completions";
|
|
450
|
+
this.chatAuth = opts.chatAuth ?? "bearer";
|
|
451
|
+
this.authHeaderName = opts.authHeaderName ?? "x-api-key";
|
|
447
452
|
}
|
|
448
453
|
async complete(messages, tools) {
|
|
449
454
|
const body = {
|
|
@@ -454,10 +459,12 @@ var LLMDispatcher = class {
|
|
|
454
459
|
if (tools && tools.length > 0) {
|
|
455
460
|
body.tools = tools;
|
|
456
461
|
}
|
|
457
|
-
const url = `${this.endpoint}
|
|
462
|
+
const url = `${this.endpoint}${this.chatPath}`;
|
|
458
463
|
const headers = { "Content-Type": "application/json" };
|
|
459
|
-
if (this.apiKey) {
|
|
464
|
+
if (this.apiKey && this.chatAuth === "bearer") {
|
|
460
465
|
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
466
|
+
} else if (this.apiKey && this.chatAuth === "x-api-key") {
|
|
467
|
+
headers[this.authHeaderName] = this.apiKey;
|
|
461
468
|
}
|
|
462
469
|
const resp = await fetch(url, {
|
|
463
470
|
method: "POST",
|
|
@@ -465,8 +472,8 @@ var LLMDispatcher = class {
|
|
|
465
472
|
body: JSON.stringify(body)
|
|
466
473
|
});
|
|
467
474
|
if (!resp.ok) {
|
|
468
|
-
|
|
469
|
-
throw new Error(`LLM request failed (${resp.status})
|
|
475
|
+
await resp.body?.cancel();
|
|
476
|
+
throw new Error(`LLM request failed (${resp.status})`);
|
|
470
477
|
}
|
|
471
478
|
const data = await resp.json();
|
|
472
479
|
const choice = data.choices?.[0];
|
|
@@ -486,10 +493,13 @@ import { join as join2 } from "node:path";
|
|
|
486
493
|
|
|
487
494
|
// src/llm-providers.json
|
|
488
495
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
496
|
+
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 },
|
|
497
|
+
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 },
|
|
498
|
+
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 },
|
|
499
|
+
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 },
|
|
500
|
+
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 },
|
|
501
|
+
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 },
|
|
502
|
+
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 }
|
|
493
503
|
};
|
|
494
504
|
|
|
495
505
|
// src/llm-key.ts
|
|
@@ -513,7 +523,12 @@ function resolveLlmApiKey(cfg, readFile) {
|
|
|
513
523
|
|
|
514
524
|
// src/llm-source.ts
|
|
515
525
|
var PROVIDER_TABLE = Object.freeze(Object.fromEntries(
|
|
516
|
-
Object.entries(llm_providers_default).map(([p, e]) => [p, Object.freeze({
|
|
526
|
+
Object.entries(llm_providers_default).map(([p, e]) => [p, Object.freeze({
|
|
527
|
+
endpoint: e.chat_base,
|
|
528
|
+
needsKey: e.needs_key,
|
|
529
|
+
chatPath: e.chat_path,
|
|
530
|
+
chatAuth: e.chat_auth
|
|
531
|
+
})])
|
|
517
532
|
));
|
|
518
533
|
var LlmSourceUnavailableError = class extends Error {
|
|
519
534
|
};
|
|
@@ -523,6 +538,9 @@ function providerOrigin(provider) {
|
|
|
523
538
|
const u = new URL(entry.endpoint);
|
|
524
539
|
return `${u.protocol}//${u.host}`.toLowerCase();
|
|
525
540
|
}
|
|
541
|
+
function providerSlug(provider) {
|
|
542
|
+
return providerOrigin(provider).replace("://", "_").replace(/:/g, "-");
|
|
543
|
+
}
|
|
526
544
|
var explicitEndpointSet = (env) => Boolean(env.SECURECOMS_LLM_ENDPOINT || env.SECURECOMS_LLM_API_KEY_FILE || env.SECURECOMS_LLM_API_KEY);
|
|
527
545
|
function isExplicitSource(cfg, env) {
|
|
528
546
|
return explicitEndpointSet(env) || Boolean(cfg.llmApiKeyFile) || Boolean(cfg.llmApiKey);
|
|
@@ -539,7 +557,9 @@ function resolveLlmSource(cfg, explicit, deps) {
|
|
|
539
557
|
model: cfg.llmModel,
|
|
540
558
|
apiKey: resolveLlmApiKey(cfg, deps.readFile),
|
|
541
559
|
provider: "env",
|
|
542
|
-
origin: "env"
|
|
560
|
+
origin: "env",
|
|
561
|
+
chatPath: "/v1/chat/completions",
|
|
562
|
+
chatAuth: "bearer"
|
|
543
563
|
};
|
|
544
564
|
}
|
|
545
565
|
const managedDir = join2(deps.home, ".securecoms", "runtime-config", cfg.identityId);
|
|
@@ -567,6 +587,11 @@ function resolveLlmSource(cfg, explicit, deps) {
|
|
|
567
587
|
if (typeof provider !== "string" || typeof model !== "string") {
|
|
568
588
|
throw new LlmSourceUnavailableError(`${sourcePath} is missing "provider" or "model"`);
|
|
569
589
|
}
|
|
590
|
+
if (!Object.hasOwn(llm_providers_default, provider)) {
|
|
591
|
+
throw new LlmSourceUnavailableError(
|
|
592
|
+
`unknown LLM provider ${JSON.stringify(provider)} in ${sourcePath}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
570
595
|
const entry = PROVIDER_TABLE[provider];
|
|
571
596
|
if (!entry) {
|
|
572
597
|
throw new LlmSourceUnavailableError(
|
|
@@ -575,53 +600,63 @@ function resolveLlmSource(cfg, explicit, deps) {
|
|
|
575
600
|
}
|
|
576
601
|
let apiKey;
|
|
577
602
|
if (entry.needsKey) {
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
|
|
603
|
+
const expectedOrigin = providerOrigin(provider);
|
|
604
|
+
const slug = providerSlug(provider);
|
|
605
|
+
const keyPath = join2(managedDir, `llm.key.${slug}`);
|
|
606
|
+
const hostPath = join2(managedDir, `llm.key.${slug}.host`);
|
|
607
|
+
const legacyKey = join2(managedDir, "llm.key");
|
|
608
|
+
const legacyHost = join2(managedDir, "llm.key.host");
|
|
609
|
+
const useLegacy = !deps.exists(keyPath) && deps.exists(legacyKey) && deps.exists(legacyHost);
|
|
610
|
+
const boundKey = useLegacy ? legacyKey : keyPath;
|
|
611
|
+
const boundHost = useLegacy ? legacyHost : hostPath;
|
|
612
|
+
if (!deps.exists(boundKey)) {
|
|
581
613
|
throw new LlmSourceUnavailableError(
|
|
582
614
|
`${keyPath} does not exist \u2014 no key has been written for this managed source, refusing to read it`
|
|
583
615
|
);
|
|
584
616
|
}
|
|
585
617
|
let mode;
|
|
586
618
|
try {
|
|
587
|
-
({ mode } = deps.statFile(
|
|
619
|
+
({ mode } = deps.statFile(boundKey));
|
|
588
620
|
} catch (e) {
|
|
589
621
|
throw new LlmSourceUnavailableError(
|
|
590
|
-
`cannot stat ${
|
|
622
|
+
`cannot stat ${boundKey}: ${e instanceof Error ? e.message : String(e)}`
|
|
591
623
|
);
|
|
592
624
|
}
|
|
593
625
|
if ((mode & 63) !== 0) {
|
|
594
626
|
throw new LlmSourceUnavailableError(
|
|
595
|
-
`${
|
|
627
|
+
`${boundKey} is readable by other users on this machine \u2014 tighten its permissions to owner-only`
|
|
596
628
|
);
|
|
597
629
|
}
|
|
598
|
-
|
|
599
|
-
if (!deps.exists(hostPath)) {
|
|
630
|
+
if (!deps.exists(boundHost)) {
|
|
600
631
|
throw new LlmSourceUnavailableError(
|
|
601
|
-
`${
|
|
632
|
+
`${boundKey} has no ${boundHost} sidecar recording which origin it belongs to \u2014 refusing to send it to ${expectedOrigin}`
|
|
602
633
|
);
|
|
603
634
|
}
|
|
604
|
-
const actualOrigin = deps.readFile(
|
|
635
|
+
const actualOrigin = deps.readFile(boundHost).trim();
|
|
605
636
|
if (actualOrigin !== expectedOrigin) {
|
|
606
637
|
throw new LlmSourceUnavailableError(
|
|
607
|
-
`${
|
|
638
|
+
`${boundHost} is bound to ${actualOrigin}, but provider ${JSON.stringify(provider)} requires ${expectedOrigin} \u2014 refusing to send the key across origins`
|
|
608
639
|
);
|
|
609
640
|
}
|
|
610
|
-
apiKey = resolveLlmApiKey({ llmApiKeyFile:
|
|
641
|
+
apiKey = resolveLlmApiKey({ llmApiKeyFile: boundKey }, deps.readFile);
|
|
611
642
|
}
|
|
612
643
|
return {
|
|
613
644
|
endpoint: entry.endpoint,
|
|
614
645
|
model,
|
|
615
646
|
...apiKey !== void 0 ? { apiKey } : {},
|
|
616
647
|
provider,
|
|
617
|
-
origin: "managed"
|
|
648
|
+
origin: "managed",
|
|
649
|
+
chatPath: entry.chatPath,
|
|
650
|
+
chatAuth: entry.chatAuth
|
|
618
651
|
};
|
|
619
652
|
}
|
|
620
653
|
return {
|
|
621
654
|
endpoint: cfg.llmEndpoint,
|
|
622
655
|
model: cfg.llmModel,
|
|
623
656
|
provider: "default",
|
|
624
|
-
origin: "default"
|
|
657
|
+
origin: "default",
|
|
658
|
+
chatPath: "/v1/chat/completions",
|
|
659
|
+
chatAuth: "bearer"
|
|
625
660
|
};
|
|
626
661
|
}
|
|
627
662
|
|
|
@@ -637,7 +672,11 @@ function buildLlmShim(cfg, explicit, deps) {
|
|
|
637
672
|
return {
|
|
638
673
|
async complete(messages, tools) {
|
|
639
674
|
const src = resolveLlmSource(cfg, explicit, deps);
|
|
640
|
-
const dispatcher = new LLMDispatcher(src.endpoint, src.model, {
|
|
675
|
+
const dispatcher = new LLMDispatcher(src.endpoint, src.model, {
|
|
676
|
+
apiKey: src.apiKey,
|
|
677
|
+
chatPath: src.chatPath,
|
|
678
|
+
chatAuth: src.chatAuth
|
|
679
|
+
});
|
|
641
680
|
return dispatcher.complete(messages, tools);
|
|
642
681
|
}
|
|
643
682
|
};
|
|
@@ -652,116 +691,59 @@ function bootLine(cfg, explicit, deps) {
|
|
|
652
691
|
}
|
|
653
692
|
|
|
654
693
|
// src/memory.ts
|
|
655
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
656
|
-
import { join as join3 } from "node:path";
|
|
657
694
|
var MAX_HISTORY = 50;
|
|
695
|
+
var MAX_FACTS = 200;
|
|
658
696
|
var AgentMemory = class {
|
|
659
697
|
history = [];
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
698
|
+
facts = [];
|
|
699
|
+
constructor(snapshot) {
|
|
700
|
+
if (snapshot) this.replace(snapshot);
|
|
701
|
+
}
|
|
702
|
+
/** Replace the working view at the start of every authenticated turn. */
|
|
703
|
+
replace(snapshot) {
|
|
704
|
+
this.history = snapshot.recent.slice(-MAX_HISTORY).map((turn) => ({
|
|
705
|
+
role: turn.role === "peer" ? "user" : "assistant",
|
|
706
|
+
content: turn.text
|
|
707
|
+
}));
|
|
708
|
+
this.facts = snapshot.facts.slice(-MAX_FACTS);
|
|
709
|
+
}
|
|
710
|
+
/** Add a message to this turn's in-process history only. */
|
|
668
711
|
addMessage(msg) {
|
|
669
712
|
this.history.push(msg);
|
|
670
713
|
if (this.history.length > MAX_HISTORY * 2) {
|
|
671
714
|
this.history = this.history.slice(-MAX_HISTORY);
|
|
672
715
|
}
|
|
673
|
-
this.saveHistory();
|
|
674
716
|
}
|
|
675
717
|
/** Get recent conversation history for LLM context. */
|
|
676
718
|
getHistory() {
|
|
677
719
|
return this.history.slice(-MAX_HISTORY);
|
|
678
720
|
}
|
|
679
|
-
/** Clear
|
|
721
|
+
/** Clear this process-local view; durable deletion belongs to the bridge. */
|
|
680
722
|
clear() {
|
|
681
723
|
this.history = [];
|
|
682
|
-
this.
|
|
724
|
+
this.facts = [];
|
|
683
725
|
}
|
|
684
|
-
/**
|
|
685
|
-
* Recall relevant memories to prepend to the LLM context.
|
|
686
|
-
* Case-insensitive keyword-overlap match over captured entries: query is
|
|
687
|
-
* split into words of length >= 3, entries are scored by number of
|
|
688
|
-
* matching words, and the top 5 with score > 0 are returned.
|
|
689
|
-
*/
|
|
726
|
+
/** Case-insensitive keyword-overlap recall over protected channel facts. */
|
|
690
727
|
async recall(query) {
|
|
691
|
-
const queryWords = query.toLowerCase().split(/\W+/).filter((
|
|
728
|
+
const queryWords = query.toLowerCase().split(/\W+/).filter((word) => word.length >= 3);
|
|
692
729
|
if (queryWords.length === 0) return null;
|
|
693
|
-
const scored = this.
|
|
694
|
-
const contentLower =
|
|
730
|
+
const scored = this.facts.map((content) => {
|
|
731
|
+
const contentLower = content.toLowerCase();
|
|
695
732
|
const score = queryWords.reduce(
|
|
696
|
-
(acc,
|
|
733
|
+
(acc, word) => contentLower.includes(word) ? acc + 1 : acc,
|
|
697
734
|
0
|
|
698
735
|
);
|
|
699
|
-
return {
|
|
700
|
-
}).filter((
|
|
736
|
+
return { content, score };
|
|
737
|
+
}).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
701
738
|
if (scored.length === 0) return null;
|
|
702
|
-
const memories = scored.map((s) => `- ${s.memory.content}`).join("\n");
|
|
703
739
|
return `Relevant context from memory:
|
|
704
|
-
${
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Capture a notable fact to local memory (best-effort).
|
|
708
|
-
*/
|
|
709
|
-
async capture(content, kind) {
|
|
710
|
-
try {
|
|
711
|
-
this.memories.push({
|
|
712
|
-
content,
|
|
713
|
-
category: kind,
|
|
714
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
715
|
-
});
|
|
716
|
-
this.saveMemories();
|
|
717
|
-
} catch {
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
get historyPath() {
|
|
721
|
-
return join3(this.dataDir, "conversation-history.json");
|
|
722
|
-
}
|
|
723
|
-
get memoriesPath() {
|
|
724
|
-
return join3(this.dataDir, "memories.json");
|
|
725
|
-
}
|
|
726
|
-
loadHistory() {
|
|
727
|
-
try {
|
|
728
|
-
if (existsSync(this.historyPath)) {
|
|
729
|
-
const raw = readFileSync(this.historyPath, "utf-8");
|
|
730
|
-
this.history = JSON.parse(raw);
|
|
731
|
-
}
|
|
732
|
-
} catch {
|
|
733
|
-
this.history = [];
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
saveHistory() {
|
|
737
|
-
try {
|
|
738
|
-
mkdirSync(this.dataDir, { recursive: true });
|
|
739
|
-
writeFileSync(this.historyPath, JSON.stringify(this.history));
|
|
740
|
-
} catch {
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
loadMemories() {
|
|
744
|
-
try {
|
|
745
|
-
if (existsSync(this.memoriesPath)) {
|
|
746
|
-
const raw = readFileSync(this.memoriesPath, "utf-8");
|
|
747
|
-
this.memories = JSON.parse(raw);
|
|
748
|
-
}
|
|
749
|
-
} catch {
|
|
750
|
-
this.memories = [];
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
saveMemories() {
|
|
754
|
-
try {
|
|
755
|
-
mkdirSync(this.dataDir, { recursive: true });
|
|
756
|
-
writeFileSync(this.memoriesPath, JSON.stringify(this.memories));
|
|
757
|
-
} catch {
|
|
758
|
-
}
|
|
740
|
+
${scored.map((entry) => `- ${entry.content}`).join("\n")}`;
|
|
759
741
|
}
|
|
760
742
|
};
|
|
761
743
|
|
|
762
744
|
// src/skill-invoker.ts
|
|
763
|
-
import { readFileSync as
|
|
764
|
-
import { join as
|
|
745
|
+
import { readFileSync as readFileSync4, readdirSync, existsSync as existsSync3, statSync } from "node:fs";
|
|
746
|
+
import { join as join4 } from "node:path";
|
|
765
747
|
|
|
766
748
|
// src/skill-md.ts
|
|
767
749
|
var SkillMdError = class extends Error {
|
|
@@ -916,23 +898,23 @@ function resolveRequires(req, env) {
|
|
|
916
898
|
}
|
|
917
899
|
|
|
918
900
|
// src/skills-config.ts
|
|
919
|
-
import { existsSync as
|
|
901
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
920
902
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
921
903
|
|
|
922
904
|
// src/config-dirs.ts
|
|
923
|
-
import { existsSync
|
|
905
|
+
import { existsSync } from "node:fs";
|
|
924
906
|
import { homedir as homedir2 } from "node:os";
|
|
925
|
-
import { join as
|
|
907
|
+
import { join as join3 } from "node:path";
|
|
926
908
|
function managedConfigDir(identityId, home = homedir2()) {
|
|
927
|
-
return
|
|
909
|
+
return join3(home, ".securecoms", "runtime-config", identityId);
|
|
928
910
|
}
|
|
929
911
|
function configSearchPath(identityId, dataDir, home = homedir2()) {
|
|
930
912
|
return [managedConfigDir(identityId, home), dataDir];
|
|
931
913
|
}
|
|
932
914
|
function findConfigFile(name, dirs) {
|
|
933
915
|
for (const dir of dirs) {
|
|
934
|
-
const path =
|
|
935
|
-
if (
|
|
916
|
+
const path = join3(dir, name);
|
|
917
|
+
if (existsSync(path)) return path;
|
|
936
918
|
}
|
|
937
919
|
return null;
|
|
938
920
|
}
|
|
@@ -942,10 +924,10 @@ var EMPTY_SKILLS_CONFIG = { endpoints: {}, databases: {}, flags: [] };
|
|
|
942
924
|
var empty = () => ({ endpoints: {}, databases: {}, flags: [] });
|
|
943
925
|
var abs2 = (p, dataDir) => isAbsolute2(p) ? p : resolve2(dataDir, p);
|
|
944
926
|
function loadSkillsConfigFile(path, dataDir) {
|
|
945
|
-
if (!
|
|
927
|
+
if (!existsSync2(path)) return empty();
|
|
946
928
|
let raw;
|
|
947
929
|
try {
|
|
948
|
-
raw = JSON.parse(
|
|
930
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
949
931
|
} catch (e) {
|
|
950
932
|
console.warn(`[skills-config] ignoring ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
951
933
|
return empty();
|
|
@@ -1200,7 +1182,7 @@ var fetchUrl = async (args, ctx) => {
|
|
|
1200
1182
|
};
|
|
1201
1183
|
|
|
1202
1184
|
// src/builtins/http-request.ts
|
|
1203
|
-
import { readFileSync as
|
|
1185
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1204
1186
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE"]);
|
|
1205
1187
|
var httpRequest = async (args, ctx) => {
|
|
1206
1188
|
const name = typeof args.name === "string" ? args.name : "";
|
|
@@ -1243,7 +1225,7 @@ var httpRequest = async (args, ctx) => {
|
|
|
1243
1225
|
const headers = { accept: "application/json" };
|
|
1244
1226
|
if (endpoint.headersFile) {
|
|
1245
1227
|
try {
|
|
1246
|
-
for (const [k, v] of Object.entries(JSON.parse(
|
|
1228
|
+
for (const [k, v] of Object.entries(JSON.parse(readFileSync2(endpoint.headersFile, "utf8")))) {
|
|
1247
1229
|
if (typeof v === "string") headers[k] = v;
|
|
1248
1230
|
}
|
|
1249
1231
|
} catch {
|
|
@@ -1268,59 +1250,8 @@ var httpRequest = async (args, ctx) => {
|
|
|
1268
1250
|
}
|
|
1269
1251
|
};
|
|
1270
1252
|
|
|
1271
|
-
// src/long-term-memory.ts
|
|
1272
|
-
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1273
|
-
import { join as join5 } from "node:path";
|
|
1274
|
-
var MEMORY_FILE = "MEMORY.md";
|
|
1275
|
-
var MEMORY_MAX_BYTES = 8192;
|
|
1276
|
-
var FACT_MAX_CHARS = 300;
|
|
1277
|
-
function readLongTermMemory(dirs) {
|
|
1278
|
-
const p = findConfigFile(MEMORY_FILE, dirs);
|
|
1279
|
-
if (!p) return null;
|
|
1280
|
-
const body = readFileSync4(p, "utf8").trim();
|
|
1281
|
-
return body || null;
|
|
1282
|
-
}
|
|
1283
|
-
function memoryTarget(dirs) {
|
|
1284
|
-
for (const d of dirs) if (existsSync4(join5(d, MEMORY_FILE))) return d;
|
|
1285
|
-
for (const d of dirs) if (existsSync4(d)) return d;
|
|
1286
|
-
return dirs[dirs.length - 1];
|
|
1287
|
-
}
|
|
1288
|
-
function migrateMemoryIntoManaged(managedDir, dataDir) {
|
|
1289
|
-
const managed = join5(managedDir, MEMORY_FILE);
|
|
1290
|
-
const local = join5(dataDir, MEMORY_FILE);
|
|
1291
|
-
if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
|
|
1292
|
-
copyFileSync(local, managed);
|
|
1293
|
-
return true;
|
|
1294
|
-
}
|
|
1295
|
-
function rememberFact(dirs, fact, now) {
|
|
1296
|
-
const clean = fact.replace(/\s+/g, " ").trim().slice(0, FACT_MAX_CHARS);
|
|
1297
|
-
if (!clean) throw new Error("remember needs a non-empty fact");
|
|
1298
|
-
const line = `- ${now.toISOString().slice(0, 10)} ${clean}`;
|
|
1299
|
-
const dir = memoryTarget(dirs);
|
|
1300
|
-
mkdirSync2(dir, { recursive: true });
|
|
1301
|
-
const existing = readLongTermMemory(dirs);
|
|
1302
|
-
let lines = existing ? existing.split("\n") : [];
|
|
1303
|
-
lines.push(line);
|
|
1304
|
-
while (Buffer.byteLength(`${lines.join("\n")}
|
|
1305
|
-
`, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
|
|
1306
|
-
lines = lines.slice(1);
|
|
1307
|
-
}
|
|
1308
|
-
writeFileSync2(join5(dir, MEMORY_FILE), `${lines.join("\n")}
|
|
1309
|
-
`, { mode: 384 });
|
|
1310
|
-
return line;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// src/builtins/remember.ts
|
|
1314
|
-
var remember = async (args, ctx) => {
|
|
1315
|
-
try {
|
|
1316
|
-
return JSON.stringify({ remembered: rememberFact(ctx.configDirs, String(args.fact ?? ""), ctx.now()) });
|
|
1317
|
-
} catch (e) {
|
|
1318
|
-
return toolError(e instanceof Error ? e.message : String(e));
|
|
1319
|
-
}
|
|
1320
|
-
};
|
|
1321
|
-
|
|
1322
1253
|
// src/builtins/sql-query.ts
|
|
1323
|
-
import { readFileSync as
|
|
1254
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1324
1255
|
|
|
1325
1256
|
// src/sql-guard.ts
|
|
1326
1257
|
var SqlNotReadOnlyError = class extends Error {
|
|
@@ -1421,7 +1352,7 @@ var sqlQuery = async (args, ctx) => {
|
|
|
1421
1352
|
const Client = pg.Client ?? pg.default?.Client;
|
|
1422
1353
|
if (!Client) return toolError(`the installed "pg" module does not export a Client`);
|
|
1423
1354
|
const client = new Client({
|
|
1424
|
-
connectionString:
|
|
1355
|
+
connectionString: readFileSync3(db.postgresUrlFile, "utf8").trim(),
|
|
1425
1356
|
// never logged (D4)
|
|
1426
1357
|
statement_timeout: SQL_TIMEOUT_MS
|
|
1427
1358
|
});
|
|
@@ -1533,8 +1464,7 @@ var BUILTINS = {
|
|
|
1533
1464
|
fetch_url: fetchUrl,
|
|
1534
1465
|
web_search: webSearch,
|
|
1535
1466
|
http_request: httpRequest,
|
|
1536
|
-
sql_query: sqlQuery
|
|
1537
|
-
remember
|
|
1467
|
+
sql_query: sqlQuery
|
|
1538
1468
|
};
|
|
1539
1469
|
|
|
1540
1470
|
// src/skill-invoker.ts
|
|
@@ -1560,19 +1490,19 @@ var SkillInvoker = class {
|
|
|
1560
1490
|
* `<skillsDir>/<subdir>/manifest.json`.
|
|
1561
1491
|
*/
|
|
1562
1492
|
loadSkills() {
|
|
1563
|
-
if (!
|
|
1493
|
+
if (!existsSync3(this.skillsDir)) {
|
|
1564
1494
|
console.warn(`[SkillInvoker] Skills directory not found: ${this.skillsDir}`);
|
|
1565
1495
|
return;
|
|
1566
1496
|
}
|
|
1567
1497
|
const flat = [], manifests = [], skillMds = [];
|
|
1568
1498
|
for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
|
|
1569
1499
|
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
1570
|
-
flat.push(
|
|
1500
|
+
flat.push(join4(this.skillsDir, entry.name));
|
|
1571
1501
|
} else if (entry.isDirectory()) {
|
|
1572
|
-
const m =
|
|
1573
|
-
if (
|
|
1574
|
-
const s =
|
|
1575
|
-
if (
|
|
1502
|
+
const m = join4(this.skillsDir, entry.name, "manifest.json");
|
|
1503
|
+
if (existsSync3(m) && statSync(m).isFile()) manifests.push(m);
|
|
1504
|
+
const s = join4(this.skillsDir, entry.name, "SKILL.md");
|
|
1505
|
+
if (existsSync3(s) && statSync(s).isFile()) skillMds.push(s);
|
|
1576
1506
|
}
|
|
1577
1507
|
}
|
|
1578
1508
|
for (const p of [...flat.sort(), ...manifests.sort()]) this.loadManifestFile(p);
|
|
@@ -1588,14 +1518,14 @@ var SkillInvoker = class {
|
|
|
1588
1518
|
}
|
|
1589
1519
|
loadSkillMdFile(path) {
|
|
1590
1520
|
try {
|
|
1591
|
-
this.register(parseSkillMd(
|
|
1521
|
+
this.register(parseSkillMd(readFileSync4(path, "utf-8"), path), path);
|
|
1592
1522
|
} catch (err) {
|
|
1593
1523
|
console.warn(`[SkillInvoker] Skipping ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1594
1524
|
}
|
|
1595
1525
|
}
|
|
1596
1526
|
loadManifestFile(path) {
|
|
1597
1527
|
try {
|
|
1598
|
-
const raw =
|
|
1528
|
+
const raw = readFileSync4(path, "utf-8");
|
|
1599
1529
|
const manifest = JSON.parse(raw);
|
|
1600
1530
|
if (typeof manifest.handler !== "string" || !(manifest.handler.startsWith("builtin:") || manifest.handler.startsWith("script:"))) {
|
|
1601
1531
|
console.warn(
|
|
@@ -1758,10 +1688,78 @@ function createDynamicToolGate(policyFor) {
|
|
|
1758
1688
|
}
|
|
1759
1689
|
|
|
1760
1690
|
// src/data-plane.ts
|
|
1761
|
-
import { readFileSync as
|
|
1691
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1762
1692
|
import { WebSocket } from "node:http";
|
|
1763
1693
|
import { homedir as homedir3 } from "node:os";
|
|
1764
|
-
import { join as
|
|
1694
|
+
import { join as join5 } from "node:path";
|
|
1695
|
+
var NO_TURN_MEMORY = Object.freeze({
|
|
1696
|
+
recent: Object.freeze([]),
|
|
1697
|
+
facts: Object.freeze([])
|
|
1698
|
+
});
|
|
1699
|
+
function parseTurnMemory(value) {
|
|
1700
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return NO_TURN_MEMORY;
|
|
1701
|
+
const raw = value;
|
|
1702
|
+
if (!Array.isArray(raw.recent) || !Array.isArray(raw.facts)) return NO_TURN_MEMORY;
|
|
1703
|
+
const recent = [];
|
|
1704
|
+
for (const value2 of raw.recent.slice(-50)) {
|
|
1705
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return NO_TURN_MEMORY;
|
|
1706
|
+
const turn = value2;
|
|
1707
|
+
if (turn.role !== "peer" && turn.role !== "agent" || typeof turn.text !== "string") {
|
|
1708
|
+
return NO_TURN_MEMORY;
|
|
1709
|
+
}
|
|
1710
|
+
recent.push(Object.freeze({ role: turn.role, text: turn.text }));
|
|
1711
|
+
}
|
|
1712
|
+
const facts = [];
|
|
1713
|
+
for (const fact of raw.facts.slice(-200)) {
|
|
1714
|
+
if (typeof fact !== "string") return NO_TURN_MEMORY;
|
|
1715
|
+
facts.push(fact);
|
|
1716
|
+
}
|
|
1717
|
+
return Object.freeze({ recent: Object.freeze(recent), facts: Object.freeze(facts) });
|
|
1718
|
+
}
|
|
1719
|
+
var AUDIENCES = /* @__PURE__ */ new Set([
|
|
1720
|
+
"owner_dm",
|
|
1721
|
+
"team_room",
|
|
1722
|
+
"partner_room",
|
|
1723
|
+
"other_human_dm",
|
|
1724
|
+
"agent_to_agent",
|
|
1725
|
+
"unknown"
|
|
1726
|
+
]);
|
|
1727
|
+
var NO_TURN_AUTHORIZATION = Object.freeze({
|
|
1728
|
+
version: 1,
|
|
1729
|
+
audience: "unknown",
|
|
1730
|
+
sender_membership: "unknown",
|
|
1731
|
+
tool_grants: Object.freeze([])
|
|
1732
|
+
});
|
|
1733
|
+
function parseTurnAuthorization(value) {
|
|
1734
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return NO_TURN_AUTHORIZATION;
|
|
1735
|
+
const raw = value;
|
|
1736
|
+
if (raw.version !== 1 || typeof raw.audience !== "string" || !AUDIENCES.has(raw.audience) || raw.sender_membership !== "active" && raw.sender_membership !== "unknown" || !Array.isArray(raw.tool_grants)) return NO_TURN_AUTHORIZATION;
|
|
1737
|
+
const grants = [];
|
|
1738
|
+
for (const value2 of raw.tool_grants) {
|
|
1739
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return NO_TURN_AUTHORIZATION;
|
|
1740
|
+
const grant = value2;
|
|
1741
|
+
if (typeof grant.tool !== "string" || !grant.tool || grant.tool.length > 100) return NO_TURN_AUTHORIZATION;
|
|
1742
|
+
if (grant.mode === "read") {
|
|
1743
|
+
grants.push(Object.freeze({ tool: grant.tool, mode: "read" }));
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (grant.mode !== "side_effect" || typeof grant.operation_sha256 !== "string" || !/^[0-9a-f]{64}$/.test(grant.operation_sha256) || typeof grant.confirmation_id !== "string" || !grant.confirmation_id || typeof grant.idempotency_key !== "string" || !grant.idempotency_key || typeof grant.expires_at !== "string" || !grant.expires_at) return NO_TURN_AUTHORIZATION;
|
|
1747
|
+
grants.push(Object.freeze({
|
|
1748
|
+
tool: grant.tool,
|
|
1749
|
+
mode: "side_effect",
|
|
1750
|
+
operation_sha256: grant.operation_sha256,
|
|
1751
|
+
confirmation_id: grant.confirmation_id,
|
|
1752
|
+
idempotency_key: grant.idempotency_key,
|
|
1753
|
+
expires_at: grant.expires_at
|
|
1754
|
+
}));
|
|
1755
|
+
}
|
|
1756
|
+
return Object.freeze({
|
|
1757
|
+
version: 1,
|
|
1758
|
+
audience: raw.audience,
|
|
1759
|
+
sender_membership: raw.sender_membership,
|
|
1760
|
+
tool_grants: Object.freeze(grants)
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1765
1763
|
var SEEN_MAX = 5e3;
|
|
1766
1764
|
function errText(e) {
|
|
1767
1765
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -1804,8 +1802,8 @@ var SecureComsDataPlane = class {
|
|
|
1804
1802
|
generation = 0;
|
|
1805
1803
|
constructor(cfg, onMessage) {
|
|
1806
1804
|
this.identityId = cfg.identityId;
|
|
1807
|
-
const dir = cfg.connectionsDir ??
|
|
1808
|
-
this.connectionPath =
|
|
1805
|
+
const dir = cfg.connectionsDir ?? join5(homedir3(), ".securecoms", "connections");
|
|
1806
|
+
this.connectionPath = join5(dir, `${cfg.identityId}.json`);
|
|
1809
1807
|
this.establishedMs = cfg.establishedMs ?? 5e3;
|
|
1810
1808
|
this.initialDelayMs = cfg.initialDelayMs ?? 250;
|
|
1811
1809
|
this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
|
|
@@ -1908,9 +1906,9 @@ var SecureComsDataPlane = class {
|
|
|
1908
1906
|
if (this.generation === generation) this.running = false;
|
|
1909
1907
|
}
|
|
1910
1908
|
async attemptOnce() {
|
|
1911
|
-
const conn = JSON.parse(
|
|
1909
|
+
const conn = JSON.parse(readFileSync5(this.connectionPath, "utf8"));
|
|
1912
1910
|
const httpBase = conn.bridge_url.replace(/^ws/, "http").replace(/\/v1\/ws$/, "");
|
|
1913
|
-
const token =
|
|
1911
|
+
const token = readFileSync5(conn.token_file, "utf8").trim();
|
|
1914
1912
|
const health = await (await fetch(`${httpBase}/healthz`)).json();
|
|
1915
1913
|
if (conn.identity_id !== health.identity_id || this.identityId !== health.identity_id) {
|
|
1916
1914
|
throw new Error(
|
|
@@ -1986,10 +1984,17 @@ var SecureComsDataPlane = class {
|
|
|
1986
1984
|
text: String(frame.text ?? ""),
|
|
1987
1985
|
senderId: String(frame.sender_id ?? ""),
|
|
1988
1986
|
channelId,
|
|
1989
|
-
messageId
|
|
1987
|
+
messageId,
|
|
1988
|
+
authorization: parseTurnAuthorization(frame.authorization),
|
|
1989
|
+
memory: parseTurnMemory(frame.memory)
|
|
1990
1990
|
});
|
|
1991
1991
|
if (reply != null) {
|
|
1992
|
-
ws.send(JSON.stringify({
|
|
1992
|
+
ws.send(JSON.stringify({
|
|
1993
|
+
type: "send",
|
|
1994
|
+
channel_id: channelId,
|
|
1995
|
+
text: String(reply),
|
|
1996
|
+
in_reply_to: messageId
|
|
1997
|
+
}));
|
|
1993
1998
|
}
|
|
1994
1999
|
}
|
|
1995
2000
|
} catch (e) {
|
|
@@ -2034,6 +2039,33 @@ async function runToolCalls(toolCalls, skills, context) {
|
|
|
2034
2039
|
// src/turn-loop.ts
|
|
2035
2040
|
var DEFAULT_SYSTEM_PROMPT = "You are a SecureComs agent. Respond helpfully and concisely.";
|
|
2036
2041
|
var DEFAULT_MAX_ITERATIONS = 5;
|
|
2042
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["echo", "web_search", "fetch_url", "sql_query", "current_time"]);
|
|
2043
|
+
function hasReadGrant(auth, toolName) {
|
|
2044
|
+
return auth.sender_membership === "active" && auth.tool_grants.some((grant) => grant.tool === toolName && grant.mode === "read");
|
|
2045
|
+
}
|
|
2046
|
+
function toolIsOffered(auth, toolName) {
|
|
2047
|
+
return hasReadGrant(auth, toolName) && (READ_ONLY_TOOLS.has(toolName) || toolName === "http_request");
|
|
2048
|
+
}
|
|
2049
|
+
function authorizeToolExecution(auth, toolName, argumentsJson, offeredNames) {
|
|
2050
|
+
if (auth.sender_membership !== "active") {
|
|
2051
|
+
return { allowed: false, reason: "Sender membership is not active" };
|
|
2052
|
+
}
|
|
2053
|
+
if (!offeredNames.has(toolName) || !toolIsOffered(auth, toolName)) {
|
|
2054
|
+
return { allowed: false, reason: `Tool "${toolName}" has no active channel read grant` };
|
|
2055
|
+
}
|
|
2056
|
+
if (toolName === "http_request") {
|
|
2057
|
+
try {
|
|
2058
|
+
const args = JSON.parse(argumentsJson);
|
|
2059
|
+
const method = typeof args.method === "string" ? args.method.toUpperCase() : "GET";
|
|
2060
|
+
if (method !== "GET") {
|
|
2061
|
+
return { allowed: false, reason: "State-changing HTTP calls require an exact confirmed operation grant" };
|
|
2062
|
+
}
|
|
2063
|
+
} catch {
|
|
2064
|
+
return { allowed: false, reason: "Tool arguments are not valid JSON" };
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return { allowed: true, reason: `Tool "${toolName}" has an active channel read grant` };
|
|
2068
|
+
}
|
|
2037
2069
|
function buildHandler(deps) {
|
|
2038
2070
|
const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
2039
2071
|
const log = deps.log ?? (() => {
|
|
@@ -2042,18 +2074,20 @@ function buildHandler(deps) {
|
|
|
2042
2074
|
return async (m) => {
|
|
2043
2075
|
const text = m.text?.trim() ? m.text : "";
|
|
2044
2076
|
if (!text) return null;
|
|
2045
|
-
log(
|
|
2077
|
+
log(
|
|
2078
|
+
"info",
|
|
2079
|
+
`message received (sender=${m.senderId.slice(0, 8)}, channel=${m.channelId.slice(0, 8)}, chars=${text.length})`
|
|
2080
|
+
);
|
|
2046
2081
|
const memory = memoryFor(m.channelId);
|
|
2082
|
+
memory.replace(m.memory);
|
|
2047
2083
|
const messages = [{ role: "system", content: deps.systemPromptFor() }];
|
|
2048
2084
|
const recalled = await memory.recall(text);
|
|
2049
2085
|
if (recalled) messages.push({ role: "system", content: recalled });
|
|
2050
|
-
const longTerm = deps.longTermMemory?.() ?? null;
|
|
2051
|
-
if (longTerm) messages.push({ role: "system", content: `Things you have remembered:
|
|
2052
|
-
${longTerm}` });
|
|
2053
2086
|
messages.push(...memory.getHistory());
|
|
2054
2087
|
messages.push({ role: "user", content: text });
|
|
2055
2088
|
memory.addMessage({ role: "user", content: text });
|
|
2056
|
-
const tools = deps.tools();
|
|
2089
|
+
const tools = deps.tools().filter((tool) => toolIsOffered(m.authorization, tool.function.name));
|
|
2090
|
+
const offeredNames = new Set(tools.map((tool) => tool.function.name));
|
|
2057
2091
|
let response = null;
|
|
2058
2092
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2059
2093
|
const result = await llm.complete(messages, tools.length > 0 ? tools : void 0);
|
|
@@ -2066,10 +2100,18 @@ ${longTerm}` });
|
|
|
2066
2100
|
content: result.content ?? "",
|
|
2067
2101
|
tool_calls: result.toolCalls
|
|
2068
2102
|
});
|
|
2069
|
-
const verdicts = result.toolCalls.map((call) =>
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2103
|
+
const verdicts = result.toolCalls.map((call) => {
|
|
2104
|
+
const channelVerdict = authorizeToolExecution(
|
|
2105
|
+
m.authorization,
|
|
2106
|
+
call.function.name,
|
|
2107
|
+
call.function.arguments,
|
|
2108
|
+
offeredNames
|
|
2109
|
+
);
|
|
2110
|
+
return {
|
|
2111
|
+
call,
|
|
2112
|
+
verdict: channelVerdict.allowed ? gate.gate(call.function.name) : channelVerdict
|
|
2113
|
+
};
|
|
2114
|
+
});
|
|
2073
2115
|
const allowed = verdicts.filter((v) => v.verdict.allowed).map((v) => v.call);
|
|
2074
2116
|
for (const v of verdicts) {
|
|
2075
2117
|
if (!v.verdict.allowed) {
|
|
@@ -2104,25 +2146,168 @@ ${longTerm}` });
|
|
|
2104
2146
|
return null;
|
|
2105
2147
|
}
|
|
2106
2148
|
memory.addMessage({ role: "assistant", content: response });
|
|
2107
|
-
|
|
2108
|
-
try {
|
|
2109
|
-
await memory.capture(
|
|
2110
|
-
`User asked: "${text.slice(0, 100)}". Agent responded: "${response.slice(0, 200)}"`,
|
|
2111
|
-
"conversation"
|
|
2112
|
-
);
|
|
2113
|
-
} catch (e) {
|
|
2114
|
-
log("warn", `memory capture failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
log("info", `response: ${response.slice(0, 80)}`);
|
|
2149
|
+
log("info", `response ready (chars=${response.length})`);
|
|
2118
2150
|
return response;
|
|
2119
2151
|
};
|
|
2120
2152
|
}
|
|
2121
2153
|
|
|
2122
2154
|
// src/persona.ts
|
|
2123
|
-
import { existsSync as
|
|
2124
|
-
import { dirname as dirname3, join as
|
|
2155
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7, renameSync as renameSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
2156
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
2125
2157
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2158
|
+
|
|
2159
|
+
// src/long-term-memory.ts
|
|
2160
|
+
import { createHash } from "node:crypto";
|
|
2161
|
+
import { chmodSync, existsSync as existsSync4, lstatSync, readFileSync as readFileSync6, readdirSync as readdirSync2, renameSync } from "node:fs";
|
|
2162
|
+
import { join as join6 } from "node:path";
|
|
2163
|
+
var MEMORY_FILE = "MEMORY.md";
|
|
2164
|
+
var QUARANTINE_MEMORY_FILE = "MEMORY.quarantined.md";
|
|
2165
|
+
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
2166
|
+
var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
2167
|
+
var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
2168
|
+
function quarantineOrder(a, b) {
|
|
2169
|
+
const index = (name) => {
|
|
2170
|
+
const match = /^MEMORY\.quarantined(?:\.(\d+))?\.md$/.exec(name);
|
|
2171
|
+
return match?.[1] ? Number(match[1]) + 1 : 0;
|
|
2172
|
+
};
|
|
2173
|
+
return index(a) - index(b) || a.localeCompare(b);
|
|
2174
|
+
}
|
|
2175
|
+
function sourceId(kind, channelId, path, bytes) {
|
|
2176
|
+
return createHash("sha256").update(kind).update("\0").update(channelId ?? "").update("\0").update(path).update("\0").update(bytes).digest("hex");
|
|
2177
|
+
}
|
|
2178
|
+
function readLegacySource(path, kind, contentBudget, channelId) {
|
|
2179
|
+
try {
|
|
2180
|
+
let st = lstatSync(path);
|
|
2181
|
+
if (st.isSymbolicLink() || !st.isFile()) return null;
|
|
2182
|
+
if ((st.mode & 511) !== 384) {
|
|
2183
|
+
chmodSync(path, 384);
|
|
2184
|
+
st = lstatSync(path);
|
|
2185
|
+
}
|
|
2186
|
+
if (st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES) {
|
|
2187
|
+
const marker = Buffer.from(`oversize:${st.size}:${st.mtimeMs}:${st.ctimeMs}`, "utf8");
|
|
2188
|
+
return {
|
|
2189
|
+
id: sourceId(kind, channelId, path, marker),
|
|
2190
|
+
kind,
|
|
2191
|
+
...channelId ? { channelId } : {},
|
|
2192
|
+
path,
|
|
2193
|
+
content: null,
|
|
2194
|
+
bytes: st.size,
|
|
2195
|
+
exportable: false
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
if (st.size > contentBudget) return null;
|
|
2199
|
+
const bytes = readFileSync6(path);
|
|
2200
|
+
if (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget) return null;
|
|
2201
|
+
return {
|
|
2202
|
+
id: sourceId(kind, channelId, path, bytes),
|
|
2203
|
+
kind,
|
|
2204
|
+
...channelId ? { channelId } : {},
|
|
2205
|
+
path,
|
|
2206
|
+
content: bytes.toString("utf8"),
|
|
2207
|
+
bytes: bytes.byteLength,
|
|
2208
|
+
exportable: true
|
|
2209
|
+
};
|
|
2210
|
+
} catch {
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function collectLegacyMemorySources(managedDir, dataDir) {
|
|
2215
|
+
const sources = [];
|
|
2216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2217
|
+
let contentBudget = LEGACY_MEMORY_REPORT_MAX_BYTES;
|
|
2218
|
+
const add = (path, kind, channelId) => {
|
|
2219
|
+
if (sources.length >= LEGACY_MEMORY_REPORT_MAX_SOURCES) return;
|
|
2220
|
+
const source = readLegacySource(path, kind, contentBudget, channelId);
|
|
2221
|
+
if (!source) return;
|
|
2222
|
+
sources.push(source);
|
|
2223
|
+
if (source.exportable) contentBudget -= source.bytes;
|
|
2224
|
+
};
|
|
2225
|
+
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
2226
|
+
let names;
|
|
2227
|
+
try {
|
|
2228
|
+
names = readdirSync2(dir);
|
|
2229
|
+
} catch {
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
for (const name of names.sort(quarantineOrder)) {
|
|
2233
|
+
if (!/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) continue;
|
|
2234
|
+
const path = join6(dir, name);
|
|
2235
|
+
if (seen.has(path)) continue;
|
|
2236
|
+
seen.add(path);
|
|
2237
|
+
add(path, "global-memory");
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const channelsDir = join6(dataDir, "channels");
|
|
2241
|
+
let channelNames;
|
|
2242
|
+
try {
|
|
2243
|
+
channelNames = readdirSync2(channelsDir);
|
|
2244
|
+
} catch {
|
|
2245
|
+
channelNames = [];
|
|
2246
|
+
}
|
|
2247
|
+
for (const channelId of channelNames.sort()) {
|
|
2248
|
+
const channelDir = join6(channelsDir, channelId);
|
|
2249
|
+
for (const [name, kind] of [
|
|
2250
|
+
["conversation-history.json", "conversation-history"],
|
|
2251
|
+
["memories.json", "captured-memory"]
|
|
2252
|
+
]) {
|
|
2253
|
+
add(join6(channelDir, name), kind, channelId);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return sources;
|
|
2257
|
+
}
|
|
2258
|
+
function legacyMemoryFingerprint(managedDir, dataDir) {
|
|
2259
|
+
const metadata = [];
|
|
2260
|
+
const add = (path) => {
|
|
2261
|
+
try {
|
|
2262
|
+
const st = lstatSync(path);
|
|
2263
|
+
if (!st.isFile() || st.isSymbolicLink()) return;
|
|
2264
|
+
metadata.push(`${path}\0${st.size}\0${st.mtimeMs}\0${st.ctimeMs}`);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
2269
|
+
try {
|
|
2270
|
+
for (const name of readdirSync2(dir).sort(quarantineOrder)) {
|
|
2271
|
+
if (/^MEMORY\.quarantined(?:\.\d+)?\.md$/.test(name)) add(join6(dir, name));
|
|
2272
|
+
}
|
|
2273
|
+
} catch {
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
try {
|
|
2277
|
+
const channelsDir = join6(dataDir, "channels");
|
|
2278
|
+
for (const channelId of readdirSync2(channelsDir).sort()) {
|
|
2279
|
+
add(join6(channelsDir, channelId, "conversation-history.json"));
|
|
2280
|
+
add(join6(channelsDir, channelId, "memories.json"));
|
|
2281
|
+
}
|
|
2282
|
+
} catch {
|
|
2283
|
+
}
|
|
2284
|
+
return createHash("sha256").update(metadata.join("\n")).digest("hex");
|
|
2285
|
+
}
|
|
2286
|
+
function nextQuarantinePath(dir) {
|
|
2287
|
+
const first = join6(dir, QUARANTINE_MEMORY_FILE);
|
|
2288
|
+
if (!existsSync4(first)) return first;
|
|
2289
|
+
for (let n = 1; ; n++) {
|
|
2290
|
+
const candidate = join6(dir, `MEMORY.quarantined.${n}.md`);
|
|
2291
|
+
if (!existsSync4(candidate)) return candidate;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function quarantineLegacyMemory(managedDir, dataDir) {
|
|
2295
|
+
const moved = [];
|
|
2296
|
+
for (const dir of /* @__PURE__ */ new Set([managedDir, dataDir])) {
|
|
2297
|
+
const source = join6(dir, MEMORY_FILE);
|
|
2298
|
+
if (!existsSync4(source)) continue;
|
|
2299
|
+
if (lstatSync(source).isSymbolicLink()) {
|
|
2300
|
+
throw new Error(`refusing to quarantine symlinked legacy memory: ${source}`);
|
|
2301
|
+
}
|
|
2302
|
+
const target = nextQuarantinePath(dir);
|
|
2303
|
+
renameSync(source, target);
|
|
2304
|
+
chmodSync(target, 384);
|
|
2305
|
+
moved.push(target);
|
|
2306
|
+
}
|
|
2307
|
+
return moved;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/persona.ts
|
|
2126
2311
|
var PERSONA_MAX_BYTES = 8192;
|
|
2127
2312
|
var PERSONA_SOURCES = [
|
|
2128
2313
|
{ file: "IDENTITY.md", heading: "## Identity" },
|
|
@@ -2137,7 +2322,7 @@ var SAFETY_PREAMBLE = [
|
|
|
2137
2322
|
"- Only use the tools you were given; do not invent capabilities."
|
|
2138
2323
|
].join("\n");
|
|
2139
2324
|
function defaultPersonaDir() {
|
|
2140
|
-
return
|
|
2325
|
+
return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
|
|
2141
2326
|
}
|
|
2142
2327
|
function assemblePrompt(parts) {
|
|
2143
2328
|
return parts.map((p) => `${p.heading}
|
|
@@ -2150,7 +2335,7 @@ function loadPersona(dirs) {
|
|
|
2150
2335
|
if (!path) continue;
|
|
2151
2336
|
let body;
|
|
2152
2337
|
try {
|
|
2153
|
-
body =
|
|
2338
|
+
body = readFileSync7(path, "utf8");
|
|
2154
2339
|
} catch {
|
|
2155
2340
|
console.warn(`[persona] cannot read ${path}; ignoring it`);
|
|
2156
2341
|
continue;
|
|
@@ -2170,29 +2355,32 @@ function systemPromptFor(dirs, fallback) {
|
|
|
2170
2355
|
${SAFETY_PREAMBLE}`;
|
|
2171
2356
|
}
|
|
2172
2357
|
function writeLocalConfigReport(managedDir, dataDir, now, flags) {
|
|
2173
|
-
if (!
|
|
2358
|
+
if (!existsSync5(managedDir)) return false;
|
|
2174
2359
|
const persona = {};
|
|
2175
2360
|
for (const { file } of PERSONA_SOURCES) {
|
|
2176
|
-
const path2 =
|
|
2177
|
-
if (!
|
|
2361
|
+
const path2 = join7(dataDir, file);
|
|
2362
|
+
if (!existsSync5(path2)) continue;
|
|
2178
2363
|
try {
|
|
2179
|
-
persona[file] =
|
|
2364
|
+
persona[file] = readFileSync7(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
|
|
2180
2365
|
} catch {
|
|
2181
2366
|
}
|
|
2182
2367
|
}
|
|
2183
2368
|
const doc = {
|
|
2184
|
-
schema: "securecoms.local-config/
|
|
2369
|
+
schema: "securecoms.local-config/2",
|
|
2185
2370
|
written_at: now.toISOString(),
|
|
2186
2371
|
has_search_key: flags.hasSearchKey,
|
|
2187
2372
|
has_llm_key: flags.hasLlmKey,
|
|
2188
|
-
persona
|
|
2373
|
+
persona,
|
|
2374
|
+
// Paths stay in this owner-only local handoff file. The daemon validates
|
|
2375
|
+
// each source again and strips `path` before reporting anything upstream.
|
|
2376
|
+
legacy_memory: collectLegacyMemorySources(managedDir, dataDir)
|
|
2189
2377
|
};
|
|
2190
|
-
const path =
|
|
2378
|
+
const path = join7(managedDir, "local-config.json");
|
|
2191
2379
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
2192
2380
|
try {
|
|
2193
|
-
|
|
2381
|
+
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
2194
2382
|
`, { mode: 384 });
|
|
2195
|
-
|
|
2383
|
+
renameSync2(tmp, path);
|
|
2196
2384
|
} catch (e) {
|
|
2197
2385
|
try {
|
|
2198
2386
|
unlinkSync(tmp);
|
|
@@ -2206,12 +2394,12 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
|
2206
2394
|
const written = [];
|
|
2207
2395
|
const skipped = [];
|
|
2208
2396
|
for (const { file } of PERSONA_SOURCES) {
|
|
2209
|
-
const target =
|
|
2397
|
+
const target = join7(dataDir, file);
|
|
2210
2398
|
if (exists(target)) {
|
|
2211
2399
|
skipped.push(target);
|
|
2212
2400
|
continue;
|
|
2213
2401
|
}
|
|
2214
|
-
writeFile(target,
|
|
2402
|
+
writeFile(target, readFileSync7(join7(defaultsDir, file), "utf8"));
|
|
2215
2403
|
written.push(target);
|
|
2216
2404
|
}
|
|
2217
2405
|
return { written, skipped };
|
|
@@ -2219,11 +2407,17 @@ function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
|
2219
2407
|
function createLocalConfigReporter(managedDir, dataDir, now) {
|
|
2220
2408
|
let lastWritten = null;
|
|
2221
2409
|
return (flags) => {
|
|
2222
|
-
|
|
2410
|
+
const legacyFingerprint = legacyMemoryFingerprint(managedDir, dataDir);
|
|
2411
|
+
if (lastWritten !== null && lastWritten.hasSearchKey === flags.hasSearchKey && lastWritten.hasLlmKey === flags.hasLlmKey && lastWritten.legacyFingerprint === legacyFingerprint) {
|
|
2223
2412
|
return false;
|
|
2224
2413
|
}
|
|
2225
2414
|
const wrote = writeLocalConfigReport(managedDir, dataDir, now(), flags);
|
|
2226
|
-
if (wrote)
|
|
2415
|
+
if (wrote) {
|
|
2416
|
+
lastWritten = {
|
|
2417
|
+
...flags,
|
|
2418
|
+
legacyFingerprint: legacyMemoryFingerprint(managedDir, dataDir)
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2227
2421
|
return wrote;
|
|
2228
2422
|
};
|
|
2229
2423
|
}
|
|
@@ -2244,7 +2438,7 @@ function installWarningFilter(proc = process) {
|
|
|
2244
2438
|
|
|
2245
2439
|
// src/index.ts
|
|
2246
2440
|
if (process.argv.includes("--version")) {
|
|
2247
|
-
const pkg = JSON.parse(
|
|
2441
|
+
const pkg = JSON.parse(readFileSync8(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
|
|
2248
2442
|
console.log(pkg.version);
|
|
2249
2443
|
process.exit(0);
|
|
2250
2444
|
}
|
|
@@ -2258,15 +2452,12 @@ function makeLogger(cfg) {
|
|
|
2258
2452
|
else console.log(line);
|
|
2259
2453
|
};
|
|
2260
2454
|
}
|
|
2261
|
-
function safeChannelSegment(channelId) {
|
|
2262
|
-
return channelId.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
|
|
2263
|
-
}
|
|
2264
2455
|
function realLlmSourceDeps() {
|
|
2265
2456
|
return {
|
|
2266
2457
|
home: homedir4(),
|
|
2267
|
-
readFile: (p) =>
|
|
2458
|
+
readFile: (p) => readFileSync8(p, "utf8"),
|
|
2268
2459
|
statFile: (p) => ({ mode: statSync2(p).mode }),
|
|
2269
|
-
exists: (p) =>
|
|
2460
|
+
exists: (p) => existsSync6(p)
|
|
2270
2461
|
};
|
|
2271
2462
|
}
|
|
2272
2463
|
function realServiceDeps() {
|
|
@@ -2276,10 +2467,10 @@ function realServiceDeps() {
|
|
|
2276
2467
|
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
2277
2468
|
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
2278
2469
|
},
|
|
2279
|
-
writeFile: (p, data) =>
|
|
2470
|
+
writeFile: (p, data) => writeFileSync2(p, data, { mode: 420 }),
|
|
2280
2471
|
removeFile: (p) => rmSync(p, { force: true }),
|
|
2281
|
-
mkdirp: (p) =>
|
|
2282
|
-
fileExists: (p) =>
|
|
2472
|
+
mkdirp: (p) => mkdirSync(p, { recursive: true }),
|
|
2473
|
+
fileExists: (p) => existsSync6(p),
|
|
2283
2474
|
canRead: (p) => {
|
|
2284
2475
|
try {
|
|
2285
2476
|
accessSync(p, constants.R_OK);
|
|
@@ -2294,7 +2485,7 @@ function realServiceDeps() {
|
|
|
2294
2485
|
function realResolveNodeDeps() {
|
|
2295
2486
|
return {
|
|
2296
2487
|
realpath: (p) => realpathSync(p),
|
|
2297
|
-
fileExists: (p) =>
|
|
2488
|
+
fileExists: (p) => existsSync6(p),
|
|
2298
2489
|
nodeMajorOf: (p) => {
|
|
2299
2490
|
const r = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
2300
2491
|
if (r.status !== 0) return null;
|
|
@@ -2386,12 +2577,12 @@ async function main() {
|
|
|
2386
2577
|
return;
|
|
2387
2578
|
}
|
|
2388
2579
|
if (personaVerb === "init") {
|
|
2389
|
-
|
|
2580
|
+
mkdirSync(cfgForPersona.dataDir, { recursive: true });
|
|
2390
2581
|
const r = personaInit(
|
|
2391
2582
|
cfgForPersona.dataDir,
|
|
2392
2583
|
defaultPersonaDir(),
|
|
2393
|
-
(p, d) =>
|
|
2394
|
-
(p) =>
|
|
2584
|
+
(p, d) => writeFileSync2(p, d, { mode: 384 }),
|
|
2585
|
+
(p) => existsSync6(p)
|
|
2395
2586
|
);
|
|
2396
2587
|
for (const p of r.written) console.log(`wrote ${p}`);
|
|
2397
2588
|
for (const p of r.skipped) console.log(`kept ${p} (already exists)`);
|
|
@@ -2419,7 +2610,7 @@ async function main() {
|
|
|
2419
2610
|
const memoryFor = (channelId) => {
|
|
2420
2611
|
let mem = memories.get(channelId);
|
|
2421
2612
|
if (!mem) {
|
|
2422
|
-
mem = new AgentMemory(
|
|
2613
|
+
mem = new AgentMemory();
|
|
2423
2614
|
memories.set(channelId, mem);
|
|
2424
2615
|
}
|
|
2425
2616
|
return mem;
|
|
@@ -2438,14 +2629,14 @@ async function main() {
|
|
|
2438
2629
|
const path = keyFilePath(which);
|
|
2439
2630
|
if (!path) return null;
|
|
2440
2631
|
try {
|
|
2441
|
-
return
|
|
2632
|
+
return readFileSync8(path, "utf8").trim() || null;
|
|
2442
2633
|
} catch {
|
|
2443
2634
|
log("warn", `cannot read the ${which} key file ${path}`);
|
|
2444
2635
|
return null;
|
|
2445
2636
|
}
|
|
2446
2637
|
};
|
|
2447
2638
|
const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
|
|
2448
|
-
const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) =>
|
|
2639
|
+
const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) => readFileSync8(p, "utf8"));
|
|
2449
2640
|
const hasLlmKey = () => {
|
|
2450
2641
|
try {
|
|
2451
2642
|
return hasLlmKeyFor(resolveLlmSource(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps()));
|
|
@@ -2453,7 +2644,7 @@ async function main() {
|
|
|
2453
2644
|
return false;
|
|
2454
2645
|
}
|
|
2455
2646
|
};
|
|
2456
|
-
|
|
2647
|
+
quarantineLegacyMemory(managedDir, cfg.dataDir);
|
|
2457
2648
|
const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
|
|
2458
2649
|
reportLocalConfig({ hasSearchKey: hasSearchKey(), hasLlmKey: hasLlmKey() });
|
|
2459
2650
|
const builtinCtx = {
|
|
@@ -2475,7 +2666,7 @@ async function main() {
|
|
|
2475
2666
|
allow: DEFAULT_TOOL_ALLOW
|
|
2476
2667
|
});
|
|
2477
2668
|
const gate = createDynamicToolGate(policyFor);
|
|
2478
|
-
const
|
|
2669
|
+
const installationEligibleTools = () => {
|
|
2479
2670
|
const sc = currentSkillsConfig();
|
|
2480
2671
|
const g = createLocalToolGate(policyFor());
|
|
2481
2672
|
const searchKey = hasSearchKey();
|
|
@@ -2493,16 +2684,15 @@ async function main() {
|
|
|
2493
2684
|
skills,
|
|
2494
2685
|
gate,
|
|
2495
2686
|
log,
|
|
2496
|
-
// All of these are read PER TURN: a persona edit, a
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2687
|
+
// All of these are read PER TURN: a persona edit, a key file dropped in,
|
|
2688
|
+
// or configuration saved in the web app takes effect on the next message,
|
|
2689
|
+
// with no restart.
|
|
2499
2690
|
systemPromptFor: () => systemPromptFor(configDirs, DEFAULT_SYSTEM_PROMPT),
|
|
2500
|
-
|
|
2501
|
-
tools: offeredTools
|
|
2691
|
+
tools: installationEligibleTools
|
|
2502
2692
|
})
|
|
2503
2693
|
);
|
|
2504
2694
|
console.log(
|
|
2505
|
-
`${bootLine(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps())}, ${
|
|
2695
|
+
`${bootLine(cfg, isExplicitSource(cfg, process.env), realLlmSourceDeps())}, ${installationEligibleTools().length} configured skills (channel grant required), tool gate default=${cfg.toolGate.default}`
|
|
2506
2696
|
);
|
|
2507
2697
|
await plane.start();
|
|
2508
2698
|
const shutdown = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@securecoms/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"securecoms-agent": "dist/bin.js"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/ws": "^8.5.12",
|
|
44
44
|
"esbuild": "0.24.0",
|
|
45
45
|
"typescript": "^5.4.0",
|
|
46
|
-
"vitest": "^2.
|
|
46
|
+
"vitest": "^3.2.6",
|
|
47
47
|
"ws": "^8.21.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/skills/remember/SKILL.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: remember
|
|
3
|
-
description: Save a durable fact to the agent's long-term memory.
|
|
4
|
-
handler: builtin:remember
|
|
5
|
-
parameters:
|
|
6
|
-
type: object
|
|
7
|
-
properties:
|
|
8
|
-
fact: { type: string, description: One durable fact of at most 300 characters. }
|
|
9
|
-
required: [fact]
|
|
10
|
-
---
|
|
11
|
-
Save a durable fact about the people or the business you work for — a renewal
|
|
12
|
-
date, a preference, a name. Not a passing detail from this conversation, and
|
|
13
|
-
never a password, key or token.
|