@securecoms/agent-runtime 0.2.0 → 0.4.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/PROVENANCE.md +9 -0
- package/README.md +66 -3
- package/dist/bin.js +205 -66
- package/package.json +1 -1
package/PROVENANCE.md
CHANGED
|
@@ -35,3 +35,12 @@ recognisable here. **No code was cloned from any of them.** The frontmatter
|
|
|
35
35
|
parser in `src/skill-md.ts` is original and deliberately accepts only the
|
|
36
36
|
documented subset; the built-ins are written against this package's own
|
|
37
37
|
`BuiltinContext`.
|
|
38
|
+
|
|
39
|
+
## Managed configuration directory (2026-09-09)
|
|
40
|
+
|
|
41
|
+
`src/config-dirs.ts` and the search-path form of the persona, memory and
|
|
42
|
+
skills.json readers are original to this package. The convention
|
|
43
|
+
`~/.securecoms/runtime-config/<identity-id>/` is SecureComs' own: the runtime
|
|
44
|
+
computes the path from the home directory and its own identity rather than
|
|
45
|
+
being told it, so no path crosses a process boundary and nothing can redirect
|
|
46
|
+
a configuration read. No code was cloned for this round.
|
package/README.md
CHANGED
|
@@ -233,13 +233,20 @@ fresh on every `http_request` call — never cached, never logged. A Postgres
|
|
|
233
233
|
database needs the optional `pg` package installed separately
|
|
234
234
|
(`npm i -g pg`); SQLite needs nothing extra (`node:sqlite`).
|
|
235
235
|
|
|
236
|
+
`skills.json` may also carry a `tools` block — `{"tools": {"allow": [...],
|
|
237
|
+
"deny": [...]}}` — the owner's on/off choices for the curated skills. A skill
|
|
238
|
+
that is switched off is not offered to the model at all. The environment wins:
|
|
239
|
+
see "Managed configuration" below.
|
|
240
|
+
|
|
236
241
|
By default `skills.json` lives at `<dataDir>/skills.json`; point elsewhere
|
|
237
|
-
with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A
|
|
238
|
-
|
|
242
|
+
with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A managed copy
|
|
243
|
+
(see below) wins over both. A missing file means no endpoints, no databases
|
|
244
|
+
and no flags — not an error.
|
|
239
245
|
|
|
240
246
|
## Persona and memory
|
|
241
247
|
|
|
242
|
-
Three optional files
|
|
248
|
+
Three optional files make up an agent's persona, looked up along the config
|
|
249
|
+
search path (see "Managed configuration" below) and read fresh
|
|
243
250
|
on **every turn** — no restart needed after an edit:
|
|
244
251
|
|
|
245
252
|
| File | Heading |
|
|
@@ -267,6 +274,62 @@ channel. It is capped at 8 kB; once it overflows, the **oldest** lines are
|
|
|
267
274
|
dropped first, on the theory that the newest remembered facts are the ones
|
|
268
275
|
still likely to be true.
|
|
269
276
|
|
|
277
|
+
## Managed configuration (from the web app)
|
|
278
|
+
|
|
279
|
+
When an agent is managed from the SecureComs web app, the owner's saved
|
|
280
|
+
configuration is written to disk by the customer's daemon at:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
~/.securecoms/runtime-config/<identity-id>/
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The runtime reads its configuration along a **search path**: that managed
|
|
287
|
+
directory first, then the data dir. The lookup is per FILE, so an owner who
|
|
288
|
+
saved only `IDENTITY.md` in the app keeps the machine's own `SOUL.md`; a file
|
|
289
|
+
with no managed copy still comes from the data dir, and an agent with no
|
|
290
|
+
managed directory at all behaves exactly as it did before. `skills.json` is
|
|
291
|
+
looked up the same way, and re-read on every turn — a new endpoint or a skill
|
|
292
|
+
switched off in the app takes effect on the next message, with no restart.
|
|
293
|
+
|
|
294
|
+
Two deliberate exceptions:
|
|
295
|
+
|
|
296
|
+
- **The environment still wins for the tool policy.** `SECURECOMS_TOOL_ALLOW`
|
|
297
|
+
overrides the `allow` list in the `tools` block of `skills.json`, so an
|
|
298
|
+
install that pins its allow list in its service unit is unaffected by a save
|
|
299
|
+
in the app. `SECURECOMS_TOOL_DENY` is UNIONED with the file's `deny` list
|
|
300
|
+
rather than replacing it: deny is a safety mechanism, so one entry in the
|
|
301
|
+
service unit must not silently un-deny every tool the owner switched off in
|
|
302
|
+
the app. Everything the environment denies stays denied either way.
|
|
303
|
+
- **Writes go to the data dir**, not the managed directory — except `MEMORY.md`,
|
|
304
|
+
which is written wherever the agent already keeps it. An existing
|
|
305
|
+
`<dataDir>/MEMORY.md` is copied into the managed directory once, at startup,
|
|
306
|
+
so the owner can read and prune it in the app.
|
|
307
|
+
|
|
308
|
+
The runtime also writes `local-config.json` into the managed directory when one
|
|
309
|
+
exists: what this agent's OWN persona files say, and a single boolean for
|
|
310
|
+
whether it has a readable search key. It is written at startup and rewritten
|
|
311
|
+
whenever the search-key answer changes, so a key file dropped in (or taken
|
|
312
|
+
away) mid-run is reported without a restart. It is a report the daemon passes
|
|
313
|
+
upward — nothing in the runtime reads it back, and it holds no key value.
|
|
314
|
+
|
|
315
|
+
### A known limitation: the generated tool paragraph
|
|
316
|
+
|
|
317
|
+
The `AGENTS.md` the app delivers ends in a generated paragraph naming the tools
|
|
318
|
+
this agent has. The backend derives that list from the saved configuration plus
|
|
319
|
+
the search-key boolean above; it does not see the running process. Three things
|
|
320
|
+
can therefore make it diverge from what the runtime actually offers:
|
|
321
|
+
|
|
322
|
+
- a `SECURECOMS_TOOL_ALLOW` or `SECURECOMS_TOOL_DENY` set in the service unit,
|
|
323
|
+
which the backend cannot see;
|
|
324
|
+
- a customer skill folder whose `requires:` is narrower than the shipped one, so
|
|
325
|
+
the runtime withholds a tool the backend counted;
|
|
326
|
+
- a custom skill the backend knows nothing about, so it is offered but unnamed.
|
|
327
|
+
|
|
328
|
+
In any of those cases the paragraph is advisory rather than exact. The gate is
|
|
329
|
+
still authoritative — a tool named in the paragraph but denied by policy is
|
|
330
|
+
still refused at call time — so the risk is a wrong claim in conversation, not
|
|
331
|
+
a wrong permission.
|
|
332
|
+
|
|
270
333
|
## Provenance
|
|
271
334
|
|
|
272
335
|
See [PROVENANCE.md](./PROVENANCE.md) for which files were ported from
|
package/dist/bin.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
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 join8 } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, existsSync as
|
|
9
|
-
import { homedir as
|
|
8
|
+
import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9
|
+
import { homedir as homedir4, userInfo } from "node:os";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
12
|
import { homedir } from "node:os";
|
|
@@ -95,7 +95,11 @@ function loadConfig(argv, env) {
|
|
|
95
95
|
dataDir,
|
|
96
96
|
skillsConfigPath: parseStringFlag(argv, "--skills-config", env, "SECURECOMS_SKILLS_CONFIG") ?? join(dataDir, "skills.json"),
|
|
97
97
|
logLevel,
|
|
98
|
-
toolGate
|
|
98
|
+
toolGate,
|
|
99
|
+
toolGateEnv: {
|
|
100
|
+
...parsedAllow !== void 0 ? { allow: parsedAllow } : {},
|
|
101
|
+
...deny !== void 0 ? { deny } : {}
|
|
102
|
+
}
|
|
99
103
|
};
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -604,8 +608,8 @@ ${memories}`;
|
|
|
604
608
|
};
|
|
605
609
|
|
|
606
610
|
// src/skill-invoker.ts
|
|
607
|
-
import { readFileSync as readFileSync6, readdirSync, existsSync as
|
|
608
|
-
import { join as
|
|
611
|
+
import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync5, statSync } from "node:fs";
|
|
612
|
+
import { join as join5 } from "node:path";
|
|
609
613
|
|
|
610
614
|
// src/skill-md.ts
|
|
611
615
|
var SkillMdError = class extends Error {
|
|
@@ -760,13 +764,33 @@ function resolveRequires(req, env) {
|
|
|
760
764
|
}
|
|
761
765
|
|
|
762
766
|
// src/skills-config.ts
|
|
763
|
-
import { existsSync as
|
|
767
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
764
768
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
769
|
+
|
|
770
|
+
// src/config-dirs.ts
|
|
771
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
772
|
+
import { homedir as homedir2 } from "node:os";
|
|
773
|
+
import { join as join3 } from "node:path";
|
|
774
|
+
function managedConfigDir(identityId, home = homedir2()) {
|
|
775
|
+
return join3(home, ".securecoms", "runtime-config", identityId);
|
|
776
|
+
}
|
|
777
|
+
function configSearchPath(identityId, dataDir, home = homedir2()) {
|
|
778
|
+
return [managedConfigDir(identityId, home), dataDir];
|
|
779
|
+
}
|
|
780
|
+
function findConfigFile(name, dirs) {
|
|
781
|
+
for (const dir of dirs) {
|
|
782
|
+
const path = join3(dir, name);
|
|
783
|
+
if (existsSync2(path)) return path;
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/skills-config.ts
|
|
765
789
|
var EMPTY_SKILLS_CONFIG = { endpoints: {}, databases: {}, flags: [] };
|
|
766
790
|
var empty = () => ({ endpoints: {}, databases: {}, flags: [] });
|
|
767
791
|
var abs2 = (p, dataDir) => isAbsolute2(p) ? p : resolve2(dataDir, p);
|
|
768
|
-
function
|
|
769
|
-
if (!
|
|
792
|
+
function loadSkillsConfigFile(path, dataDir) {
|
|
793
|
+
if (!existsSync3(path)) return empty();
|
|
770
794
|
let raw;
|
|
771
795
|
try {
|
|
772
796
|
raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -800,7 +824,24 @@ function loadSkillsConfig(path, dataDir) {
|
|
|
800
824
|
} else console.warn(`[skills-config] database "${name}" in ${path} needs sqlite or postgresUrlFile; skipped`);
|
|
801
825
|
}
|
|
802
826
|
const flags = Array.isArray(src.flags) ? src.flags.filter((f) => typeof f === "string") : [];
|
|
803
|
-
|
|
827
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
|
|
828
|
+
const rawTools = src.tools ?? void 0;
|
|
829
|
+
const tools = rawTools ? {
|
|
830
|
+
...strings(rawTools.allow) ? { allow: strings(rawTools.allow) } : {},
|
|
831
|
+
...strings(rawTools.deny) ? { deny: strings(rawTools.deny) } : {}
|
|
832
|
+
} : void 0;
|
|
833
|
+
let search;
|
|
834
|
+
const s = src.search;
|
|
835
|
+
if (s && typeof s === "object") {
|
|
836
|
+
if (s.provider !== "brave" && s.provider !== "tavily") {
|
|
837
|
+
console.warn(`[skills-config] search in ${path} has an unknown provider; skipped`);
|
|
838
|
+
} else if (typeof s.keyFile !== "string" || s.keyFile === "") {
|
|
839
|
+
console.warn(`[skills-config] search in ${path} has no keyFile; skipped`);
|
|
840
|
+
} else {
|
|
841
|
+
search = { provider: s.provider, keyFile: abs2(s.keyFile, dataDir) };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return { endpoints, databases, flags, ...tools ? { tools } : {}, ...search ? { search } : {} };
|
|
804
845
|
}
|
|
805
846
|
|
|
806
847
|
// src/builtins/current-time.ts
|
|
@@ -1076,33 +1117,43 @@ var httpRequest = async (args, ctx) => {
|
|
|
1076
1117
|
};
|
|
1077
1118
|
|
|
1078
1119
|
// src/long-term-memory.ts
|
|
1079
|
-
import { existsSync as
|
|
1080
|
-
import { join as
|
|
1120
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1121
|
+
import { join as join4 } from "node:path";
|
|
1081
1122
|
var MEMORY_FILE = "MEMORY.md";
|
|
1082
1123
|
var MEMORY_MAX_BYTES = 8192;
|
|
1083
1124
|
var FACT_MAX_CHARS = 300;
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
function readLongTermMemory(dataDir) {
|
|
1088
|
-
const p = memoryPath(dataDir);
|
|
1089
|
-
if (!existsSync3(p)) return null;
|
|
1125
|
+
function readLongTermMemory(dirs) {
|
|
1126
|
+
const p = findConfigFile(MEMORY_FILE, dirs);
|
|
1127
|
+
if (!p) return null;
|
|
1090
1128
|
const body = readFileSync4(p, "utf8").trim();
|
|
1091
1129
|
return body || null;
|
|
1092
1130
|
}
|
|
1093
|
-
function
|
|
1131
|
+
function memoryTarget(dirs) {
|
|
1132
|
+
for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
|
|
1133
|
+
for (const d of dirs) if (existsSync4(d)) return d;
|
|
1134
|
+
return dirs[dirs.length - 1];
|
|
1135
|
+
}
|
|
1136
|
+
function migrateMemoryIntoManaged(managedDir, dataDir) {
|
|
1137
|
+
const managed = join4(managedDir, MEMORY_FILE);
|
|
1138
|
+
const local = join4(dataDir, MEMORY_FILE);
|
|
1139
|
+
if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
|
|
1140
|
+
copyFileSync(local, managed);
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
function rememberFact(dirs, fact, now) {
|
|
1094
1144
|
const clean = fact.replace(/\s+/g, " ").trim().slice(0, FACT_MAX_CHARS);
|
|
1095
1145
|
if (!clean) throw new Error("remember needs a non-empty fact");
|
|
1096
1146
|
const line = `- ${now.toISOString().slice(0, 10)} ${clean}`;
|
|
1097
|
-
|
|
1098
|
-
|
|
1147
|
+
const dir = memoryTarget(dirs);
|
|
1148
|
+
mkdirSync2(dir, { recursive: true });
|
|
1149
|
+
const existing = readLongTermMemory(dirs);
|
|
1099
1150
|
let lines = existing ? existing.split("\n") : [];
|
|
1100
1151
|
lines.push(line);
|
|
1101
1152
|
while (Buffer.byteLength(`${lines.join("\n")}
|
|
1102
1153
|
`, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
|
|
1103
1154
|
lines = lines.slice(1);
|
|
1104
1155
|
}
|
|
1105
|
-
writeFileSync2(
|
|
1156
|
+
writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
|
|
1106
1157
|
`, { mode: 384 });
|
|
1107
1158
|
return line;
|
|
1108
1159
|
}
|
|
@@ -1110,7 +1161,7 @@ function rememberFact(dataDir, fact, now) {
|
|
|
1110
1161
|
// src/builtins/remember.ts
|
|
1111
1162
|
var remember = async (args, ctx) => {
|
|
1112
1163
|
try {
|
|
1113
|
-
return JSON.stringify({ remembered: rememberFact(ctx.
|
|
1164
|
+
return JSON.stringify({ remembered: rememberFact(ctx.configDirs, String(args.fact ?? ""), ctx.now()) });
|
|
1114
1165
|
} catch (e) {
|
|
1115
1166
|
return toolError(e instanceof Error ? e.message : String(e));
|
|
1116
1167
|
}
|
|
@@ -1314,6 +1365,7 @@ function offlineContext() {
|
|
|
1314
1365
|
};
|
|
1315
1366
|
return {
|
|
1316
1367
|
dataDir: "",
|
|
1368
|
+
configDirs: [""],
|
|
1317
1369
|
readKeyFile: () => null,
|
|
1318
1370
|
fetch: refuse,
|
|
1319
1371
|
resolve: async () => refuse(),
|
|
@@ -1356,19 +1408,19 @@ var SkillInvoker = class {
|
|
|
1356
1408
|
* `<skillsDir>/<subdir>/manifest.json`.
|
|
1357
1409
|
*/
|
|
1358
1410
|
loadSkills() {
|
|
1359
|
-
if (!
|
|
1411
|
+
if (!existsSync5(this.skillsDir)) {
|
|
1360
1412
|
console.warn(`[SkillInvoker] Skills directory not found: ${this.skillsDir}`);
|
|
1361
1413
|
return;
|
|
1362
1414
|
}
|
|
1363
1415
|
const flat = [], manifests = [], skillMds = [];
|
|
1364
1416
|
for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
|
|
1365
1417
|
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
1366
|
-
flat.push(
|
|
1418
|
+
flat.push(join5(this.skillsDir, entry.name));
|
|
1367
1419
|
} else if (entry.isDirectory()) {
|
|
1368
|
-
const m =
|
|
1369
|
-
if (
|
|
1370
|
-
const s =
|
|
1371
|
-
if (
|
|
1420
|
+
const m = join5(this.skillsDir, entry.name, "manifest.json");
|
|
1421
|
+
if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
|
|
1422
|
+
const s = join5(this.skillsDir, entry.name, "SKILL.md");
|
|
1423
|
+
if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
|
|
1372
1424
|
}
|
|
1373
1425
|
}
|
|
1374
1426
|
for (const p of [...flat.sort(), ...manifests.sort()]) this.loadManifestFile(p);
|
|
@@ -1537,12 +1589,27 @@ function createLocalToolGate(policy) {
|
|
|
1537
1589
|
}
|
|
1538
1590
|
};
|
|
1539
1591
|
}
|
|
1592
|
+
function resolveToolPolicy(env, file, defaults) {
|
|
1593
|
+
const deny = env.deny || file?.deny ? [.../* @__PURE__ */ new Set([...env.deny ?? [], ...file?.deny ?? []])] : void 0;
|
|
1594
|
+
return {
|
|
1595
|
+
default: defaults.default,
|
|
1596
|
+
allow: env.allow ?? file?.allow ?? defaults.allow,
|
|
1597
|
+
...deny ? { deny } : {}
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function createDynamicToolGate(policyFor) {
|
|
1601
|
+
return {
|
|
1602
|
+
gate(toolName) {
|
|
1603
|
+
return createLocalToolGate(policyFor()).gate(toolName);
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1540
1607
|
|
|
1541
1608
|
// src/data-plane.ts
|
|
1542
1609
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1543
1610
|
import { WebSocket } from "node:http";
|
|
1544
|
-
import { homedir as
|
|
1545
|
-
import { join as
|
|
1611
|
+
import { homedir as homedir3 } from "node:os";
|
|
1612
|
+
import { join as join6 } from "node:path";
|
|
1546
1613
|
var SEEN_MAX = 5e3;
|
|
1547
1614
|
function errText(e) {
|
|
1548
1615
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -1585,8 +1652,8 @@ var SecureComsDataPlane = class {
|
|
|
1585
1652
|
generation = 0;
|
|
1586
1653
|
constructor(cfg, onMessage) {
|
|
1587
1654
|
this.identityId = cfg.identityId;
|
|
1588
|
-
const dir = cfg.connectionsDir ??
|
|
1589
|
-
this.connectionPath =
|
|
1655
|
+
const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
|
|
1656
|
+
this.connectionPath = join6(dir, `${cfg.identityId}.json`);
|
|
1590
1657
|
this.establishedMs = cfg.establishedMs ?? 5e3;
|
|
1591
1658
|
this.initialDelayMs = cfg.initialDelayMs ?? 250;
|
|
1592
1659
|
this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
|
|
@@ -1901,8 +1968,8 @@ ${longTerm}` });
|
|
|
1901
1968
|
}
|
|
1902
1969
|
|
|
1903
1970
|
// src/persona.ts
|
|
1904
|
-
import { existsSync as
|
|
1905
|
-
import { dirname as dirname3, join as
|
|
1971
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1972
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
1906
1973
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1907
1974
|
var PERSONA_MAX_BYTES = 8192;
|
|
1908
1975
|
var PERSONA_SOURCES = [
|
|
@@ -1918,17 +1985,17 @@ var SAFETY_PREAMBLE = [
|
|
|
1918
1985
|
"- Only use the tools you were given; do not invent capabilities."
|
|
1919
1986
|
].join("\n");
|
|
1920
1987
|
function defaultPersonaDir() {
|
|
1921
|
-
return
|
|
1988
|
+
return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
|
|
1922
1989
|
}
|
|
1923
1990
|
function assemblePrompt(parts) {
|
|
1924
1991
|
return parts.map((p) => `${p.heading}
|
|
1925
1992
|
${p.body}`).join("\n\n");
|
|
1926
1993
|
}
|
|
1927
|
-
function loadPersona(
|
|
1994
|
+
function loadPersona(dirs) {
|
|
1928
1995
|
const parts = [];
|
|
1929
1996
|
for (const { file, heading } of PERSONA_SOURCES) {
|
|
1930
|
-
const path =
|
|
1931
|
-
if (!
|
|
1997
|
+
const path = findConfigFile(file, dirs);
|
|
1998
|
+
if (!path) continue;
|
|
1932
1999
|
let body;
|
|
1933
2000
|
try {
|
|
1934
2001
|
body = readFileSync8(path, "utf8");
|
|
@@ -1945,25 +2012,66 @@ function loadPersona(dataDir) {
|
|
|
1945
2012
|
}
|
|
1946
2013
|
return parts.length ? assemblePrompt(parts) : null;
|
|
1947
2014
|
}
|
|
1948
|
-
function systemPromptFor(
|
|
1949
|
-
return `${loadPersona(
|
|
2015
|
+
function systemPromptFor(dirs, fallback) {
|
|
2016
|
+
return `${loadPersona(dirs) ?? fallback}
|
|
1950
2017
|
|
|
1951
2018
|
${SAFETY_PREAMBLE}`;
|
|
1952
2019
|
}
|
|
2020
|
+
function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
|
|
2021
|
+
if (!existsSync6(managedDir)) return false;
|
|
2022
|
+
const persona = {};
|
|
2023
|
+
for (const { file } of PERSONA_SOURCES) {
|
|
2024
|
+
const path2 = join7(dataDir, file);
|
|
2025
|
+
if (!existsSync6(path2)) continue;
|
|
2026
|
+
try {
|
|
2027
|
+
persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
|
|
2028
|
+
} catch {
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const doc = {
|
|
2032
|
+
schema: "securecoms.local-config/1",
|
|
2033
|
+
written_at: now.toISOString(),
|
|
2034
|
+
has_search_key: hasSearchKey,
|
|
2035
|
+
persona
|
|
2036
|
+
};
|
|
2037
|
+
const path = join7(managedDir, "local-config.json");
|
|
2038
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2039
|
+
try {
|
|
2040
|
+
writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
|
|
2041
|
+
`, { mode: 384 });
|
|
2042
|
+
renameSync(tmp, path);
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
try {
|
|
2045
|
+
unlinkSync(tmp);
|
|
2046
|
+
} catch {
|
|
2047
|
+
}
|
|
2048
|
+
throw e;
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
1953
2052
|
function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
1954
2053
|
const written = [];
|
|
1955
2054
|
const skipped = [];
|
|
1956
2055
|
for (const { file } of PERSONA_SOURCES) {
|
|
1957
|
-
const target =
|
|
2056
|
+
const target = join7(dataDir, file);
|
|
1958
2057
|
if (exists(target)) {
|
|
1959
2058
|
skipped.push(target);
|
|
1960
2059
|
continue;
|
|
1961
2060
|
}
|
|
1962
|
-
writeFile(target, readFileSync8(
|
|
2061
|
+
writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
|
|
1963
2062
|
written.push(target);
|
|
1964
2063
|
}
|
|
1965
2064
|
return { written, skipped };
|
|
1966
2065
|
}
|
|
2066
|
+
function createLocalConfigReporter(managedDir, dataDir, now) {
|
|
2067
|
+
let lastWritten = null;
|
|
2068
|
+
return (hasSearchKey) => {
|
|
2069
|
+
if (lastWritten === hasSearchKey) return false;
|
|
2070
|
+
const wrote = writeLocalConfigReport(managedDir, dataDir, now(), hasSearchKey);
|
|
2071
|
+
if (wrote) lastWritten = hasSearchKey;
|
|
2072
|
+
return wrote;
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
1967
2075
|
|
|
1968
2076
|
// src/warning-filter.ts
|
|
1969
2077
|
function installWarningFilter(proc = process) {
|
|
@@ -1978,7 +2086,7 @@ function installWarningFilter(proc = process) {
|
|
|
1978
2086
|
|
|
1979
2087
|
// src/index.ts
|
|
1980
2088
|
if (process.argv.includes("--version")) {
|
|
1981
|
-
const pkg = JSON.parse(readFileSync9(
|
|
2089
|
+
const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
|
|
1982
2090
|
console.log(pkg.version);
|
|
1983
2091
|
process.exit(0);
|
|
1984
2092
|
}
|
|
@@ -2009,10 +2117,10 @@ function realServiceDeps() {
|
|
|
2009
2117
|
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
2010
2118
|
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
2011
2119
|
},
|
|
2012
|
-
writeFile: (p, data) =>
|
|
2120
|
+
writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
|
|
2013
2121
|
removeFile: (p) => rmSync(p, { force: true }),
|
|
2014
2122
|
mkdirp: (p) => mkdirSync3(p, { recursive: true }),
|
|
2015
|
-
fileExists: (p) =>
|
|
2123
|
+
fileExists: (p) => existsSync7(p),
|
|
2016
2124
|
canRead: (p) => {
|
|
2017
2125
|
try {
|
|
2018
2126
|
accessSync(p, constants.R_OK);
|
|
@@ -2027,7 +2135,7 @@ function realServiceDeps() {
|
|
|
2027
2135
|
function realResolveNodeDeps() {
|
|
2028
2136
|
return {
|
|
2029
2137
|
realpath: (p) => realpathSync(p),
|
|
2030
|
-
fileExists: (p) =>
|
|
2138
|
+
fileExists: (p) => existsSync7(p),
|
|
2031
2139
|
nodeMajorOf: (p) => {
|
|
2032
2140
|
const r = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
2033
2141
|
if (r.status !== 0) return null;
|
|
@@ -2047,7 +2155,7 @@ function runServiceVerb(verb) {
|
|
|
2047
2155
|
console.error(`securecoms-agent: ${MISSING_IDENTITY_MESSAGE}`);
|
|
2048
2156
|
process.exit(2);
|
|
2049
2157
|
}
|
|
2050
|
-
const home =
|
|
2158
|
+
const home = homedir4();
|
|
2051
2159
|
try {
|
|
2052
2160
|
const execPath = chooseVerbExecPath(
|
|
2053
2161
|
verb,
|
|
@@ -2123,13 +2231,18 @@ async function main() {
|
|
|
2123
2231
|
const r = personaInit(
|
|
2124
2232
|
cfgForPersona.dataDir,
|
|
2125
2233
|
defaultPersonaDir(),
|
|
2126
|
-
(p, d) =>
|
|
2127
|
-
(p) =>
|
|
2234
|
+
(p, d) => writeFileSync4(p, d, { mode: 384 }),
|
|
2235
|
+
(p) => existsSync7(p)
|
|
2128
2236
|
);
|
|
2129
2237
|
for (const p of r.written) console.log(`wrote ${p}`);
|
|
2130
2238
|
for (const p of r.skipped) console.log(`kept ${p} (already exists)`);
|
|
2131
2239
|
} else {
|
|
2132
|
-
console.log(
|
|
2240
|
+
console.log(
|
|
2241
|
+
systemPromptFor(
|
|
2242
|
+
configSearchPath(cfgForPersona.identityId, cfgForPersona.dataDir),
|
|
2243
|
+
DEFAULT_SYSTEM_PROMPT
|
|
2244
|
+
)
|
|
2245
|
+
);
|
|
2133
2246
|
}
|
|
2134
2247
|
process.exit(0);
|
|
2135
2248
|
}
|
|
@@ -2155,13 +2268,23 @@ async function main() {
|
|
|
2155
2268
|
const memoryFor = (channelId) => {
|
|
2156
2269
|
let mem = memories.get(channelId);
|
|
2157
2270
|
if (!mem) {
|
|
2158
|
-
mem = new AgentMemory(
|
|
2271
|
+
mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
|
|
2159
2272
|
memories.set(channelId, mem);
|
|
2160
2273
|
}
|
|
2161
2274
|
return mem;
|
|
2162
2275
|
};
|
|
2276
|
+
const managedDir = managedConfigDir(cfg.identityId);
|
|
2277
|
+
const currentSkillsConfig = () => {
|
|
2278
|
+
const managed = findConfigFile("skills.json", [managedDir]);
|
|
2279
|
+
return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
|
|
2280
|
+
};
|
|
2281
|
+
const keyFilePath = (which) => {
|
|
2282
|
+
const owner = currentSkillsConfig().search;
|
|
2283
|
+
if (owner && owner.provider === which) return owner.keyFile;
|
|
2284
|
+
return which === "brave" ? cfg.braveKeyFile : cfg.tavilyKeyFile;
|
|
2285
|
+
};
|
|
2163
2286
|
const readKeyFile = (which) => {
|
|
2164
|
-
const path = which
|
|
2287
|
+
const path = keyFilePath(which);
|
|
2165
2288
|
if (!path) return null;
|
|
2166
2289
|
try {
|
|
2167
2290
|
return readFileSync9(path, "utf8").trim() || null;
|
|
@@ -2170,24 +2293,40 @@ async function main() {
|
|
|
2170
2293
|
return null;
|
|
2171
2294
|
}
|
|
2172
2295
|
};
|
|
2296
|
+
const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
|
|
2297
|
+
const hasSearchKey = () => hasReadableKeyFile([keyFilePath("brave"), keyFilePath("tavily")], (p) => readFileSync9(p, "utf8"));
|
|
2298
|
+
migrateMemoryIntoManaged(managedDir, cfg.dataDir);
|
|
2299
|
+
const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
|
|
2300
|
+
reportLocalConfig(hasSearchKey());
|
|
2173
2301
|
const builtinCtx = {
|
|
2174
2302
|
dataDir: cfg.dataDir,
|
|
2303
|
+
configDirs,
|
|
2175
2304
|
readKeyFile,
|
|
2176
2305
|
fetch,
|
|
2177
2306
|
resolve: systemResolver(),
|
|
2178
|
-
skillsConfig
|
|
2307
|
+
get skillsConfig() {
|
|
2308
|
+
return currentSkillsConfig();
|
|
2309
|
+
},
|
|
2179
2310
|
now: () => /* @__PURE__ */ new Date(),
|
|
2180
2311
|
log
|
|
2181
2312
|
};
|
|
2182
2313
|
const skills = new SkillInvoker(cfg.skillsDir, builtinCtx);
|
|
2183
2314
|
skills.loadSkills();
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2315
|
+
const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
|
|
2316
|
+
default: cfg.toolGate.default,
|
|
2317
|
+
allow: DEFAULT_TOOL_ALLOW
|
|
2318
|
+
});
|
|
2319
|
+
const gate = createDynamicToolGate(policyFor);
|
|
2320
|
+
const offeredTools = () => {
|
|
2321
|
+
const sc = currentSkillsConfig();
|
|
2322
|
+
const g = createLocalToolGate(policyFor());
|
|
2323
|
+
const searchKey = hasSearchKey();
|
|
2324
|
+
reportLocalConfig(searchKey);
|
|
2325
|
+
return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
|
|
2326
|
+
};
|
|
2327
|
+
for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
|
|
2188
2328
|
console.log(`unavailable: ${held.name} (missing ${held.missing.join(", ")})`);
|
|
2189
2329
|
}
|
|
2190
|
-
const gate = createLocalToolGate(cfg.toolGate);
|
|
2191
2330
|
const plane = createSecureComsDataPlane(
|
|
2192
2331
|
{ identityId: cfg.identityId, log: (level, msg) => log(level, msg) },
|
|
2193
2332
|
buildHandler({
|
|
@@ -2196,16 +2335,16 @@ async function main() {
|
|
|
2196
2335
|
skills,
|
|
2197
2336
|
gate,
|
|
2198
2337
|
log,
|
|
2199
|
-
// All
|
|
2200
|
-
//
|
|
2201
|
-
// message, with no restart.
|
|
2202
|
-
systemPromptFor: () => systemPromptFor(
|
|
2203
|
-
longTermMemory: () => readLongTermMemory(
|
|
2204
|
-
tools:
|
|
2338
|
+
// All of these are read PER TURN: a persona edit, a new remembered fact, a
|
|
2339
|
+
// key file dropped in, or configuration saved in the web app takes effect
|
|
2340
|
+
// on the next message, with no restart.
|
|
2341
|
+
systemPromptFor: () => systemPromptFor(configDirs, DEFAULT_SYSTEM_PROMPT),
|
|
2342
|
+
longTermMemory: () => readLongTermMemory(configDirs),
|
|
2343
|
+
tools: offeredTools
|
|
2205
2344
|
})
|
|
2206
2345
|
);
|
|
2207
2346
|
console.log(
|
|
2208
|
-
`securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${
|
|
2347
|
+
`securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
|
|
2209
2348
|
);
|
|
2210
2349
|
await plane.start();
|
|
2211
2350
|
const shutdown = () => {
|