@securecoms/agent-runtime 0.2.0 → 0.3.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 +188 -65
- 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,13 @@ 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
|
+
return { endpoints, databases, flags, ...tools ? { tools } : {} };
|
|
804
834
|
}
|
|
805
835
|
|
|
806
836
|
// src/builtins/current-time.ts
|
|
@@ -1076,33 +1106,43 @@ var httpRequest = async (args, ctx) => {
|
|
|
1076
1106
|
};
|
|
1077
1107
|
|
|
1078
1108
|
// src/long-term-memory.ts
|
|
1079
|
-
import { existsSync as
|
|
1080
|
-
import { join as
|
|
1109
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1110
|
+
import { join as join4 } from "node:path";
|
|
1081
1111
|
var MEMORY_FILE = "MEMORY.md";
|
|
1082
1112
|
var MEMORY_MAX_BYTES = 8192;
|
|
1083
1113
|
var FACT_MAX_CHARS = 300;
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
function readLongTermMemory(dataDir) {
|
|
1088
|
-
const p = memoryPath(dataDir);
|
|
1089
|
-
if (!existsSync3(p)) return null;
|
|
1114
|
+
function readLongTermMemory(dirs) {
|
|
1115
|
+
const p = findConfigFile(MEMORY_FILE, dirs);
|
|
1116
|
+
if (!p) return null;
|
|
1090
1117
|
const body = readFileSync4(p, "utf8").trim();
|
|
1091
1118
|
return body || null;
|
|
1092
1119
|
}
|
|
1093
|
-
function
|
|
1120
|
+
function memoryTarget(dirs) {
|
|
1121
|
+
for (const d of dirs) if (existsSync4(join4(d, MEMORY_FILE))) return d;
|
|
1122
|
+
for (const d of dirs) if (existsSync4(d)) return d;
|
|
1123
|
+
return dirs[dirs.length - 1];
|
|
1124
|
+
}
|
|
1125
|
+
function migrateMemoryIntoManaged(managedDir, dataDir) {
|
|
1126
|
+
const managed = join4(managedDir, MEMORY_FILE);
|
|
1127
|
+
const local = join4(dataDir, MEMORY_FILE);
|
|
1128
|
+
if (!existsSync4(managedDir) || existsSync4(managed) || !existsSync4(local)) return false;
|
|
1129
|
+
copyFileSync(local, managed);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
function rememberFact(dirs, fact, now) {
|
|
1094
1133
|
const clean = fact.replace(/\s+/g, " ").trim().slice(0, FACT_MAX_CHARS);
|
|
1095
1134
|
if (!clean) throw new Error("remember needs a non-empty fact");
|
|
1096
1135
|
const line = `- ${now.toISOString().slice(0, 10)} ${clean}`;
|
|
1097
|
-
|
|
1098
|
-
|
|
1136
|
+
const dir = memoryTarget(dirs);
|
|
1137
|
+
mkdirSync2(dir, { recursive: true });
|
|
1138
|
+
const existing = readLongTermMemory(dirs);
|
|
1099
1139
|
let lines = existing ? existing.split("\n") : [];
|
|
1100
1140
|
lines.push(line);
|
|
1101
1141
|
while (Buffer.byteLength(`${lines.join("\n")}
|
|
1102
1142
|
`, "utf8") > MEMORY_MAX_BYTES && lines.length > 1) {
|
|
1103
1143
|
lines = lines.slice(1);
|
|
1104
1144
|
}
|
|
1105
|
-
writeFileSync2(
|
|
1145
|
+
writeFileSync2(join4(dir, MEMORY_FILE), `${lines.join("\n")}
|
|
1106
1146
|
`, { mode: 384 });
|
|
1107
1147
|
return line;
|
|
1108
1148
|
}
|
|
@@ -1110,7 +1150,7 @@ function rememberFact(dataDir, fact, now) {
|
|
|
1110
1150
|
// src/builtins/remember.ts
|
|
1111
1151
|
var remember = async (args, ctx) => {
|
|
1112
1152
|
try {
|
|
1113
|
-
return JSON.stringify({ remembered: rememberFact(ctx.
|
|
1153
|
+
return JSON.stringify({ remembered: rememberFact(ctx.configDirs, String(args.fact ?? ""), ctx.now()) });
|
|
1114
1154
|
} catch (e) {
|
|
1115
1155
|
return toolError(e instanceof Error ? e.message : String(e));
|
|
1116
1156
|
}
|
|
@@ -1314,6 +1354,7 @@ function offlineContext() {
|
|
|
1314
1354
|
};
|
|
1315
1355
|
return {
|
|
1316
1356
|
dataDir: "",
|
|
1357
|
+
configDirs: [""],
|
|
1317
1358
|
readKeyFile: () => null,
|
|
1318
1359
|
fetch: refuse,
|
|
1319
1360
|
resolve: async () => refuse(),
|
|
@@ -1356,19 +1397,19 @@ var SkillInvoker = class {
|
|
|
1356
1397
|
* `<skillsDir>/<subdir>/manifest.json`.
|
|
1357
1398
|
*/
|
|
1358
1399
|
loadSkills() {
|
|
1359
|
-
if (!
|
|
1400
|
+
if (!existsSync5(this.skillsDir)) {
|
|
1360
1401
|
console.warn(`[SkillInvoker] Skills directory not found: ${this.skillsDir}`);
|
|
1361
1402
|
return;
|
|
1362
1403
|
}
|
|
1363
1404
|
const flat = [], manifests = [], skillMds = [];
|
|
1364
1405
|
for (const entry of readdirSync(this.skillsDir, { withFileTypes: true })) {
|
|
1365
1406
|
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
1366
|
-
flat.push(
|
|
1407
|
+
flat.push(join5(this.skillsDir, entry.name));
|
|
1367
1408
|
} else if (entry.isDirectory()) {
|
|
1368
|
-
const m =
|
|
1369
|
-
if (
|
|
1370
|
-
const s =
|
|
1371
|
-
if (
|
|
1409
|
+
const m = join5(this.skillsDir, entry.name, "manifest.json");
|
|
1410
|
+
if (existsSync5(m) && statSync(m).isFile()) manifests.push(m);
|
|
1411
|
+
const s = join5(this.skillsDir, entry.name, "SKILL.md");
|
|
1412
|
+
if (existsSync5(s) && statSync(s).isFile()) skillMds.push(s);
|
|
1372
1413
|
}
|
|
1373
1414
|
}
|
|
1374
1415
|
for (const p of [...flat.sort(), ...manifests.sort()]) this.loadManifestFile(p);
|
|
@@ -1537,12 +1578,27 @@ function createLocalToolGate(policy) {
|
|
|
1537
1578
|
}
|
|
1538
1579
|
};
|
|
1539
1580
|
}
|
|
1581
|
+
function resolveToolPolicy(env, file, defaults) {
|
|
1582
|
+
const deny = env.deny || file?.deny ? [.../* @__PURE__ */ new Set([...env.deny ?? [], ...file?.deny ?? []])] : void 0;
|
|
1583
|
+
return {
|
|
1584
|
+
default: defaults.default,
|
|
1585
|
+
allow: env.allow ?? file?.allow ?? defaults.allow,
|
|
1586
|
+
...deny ? { deny } : {}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function createDynamicToolGate(policyFor) {
|
|
1590
|
+
return {
|
|
1591
|
+
gate(toolName) {
|
|
1592
|
+
return createLocalToolGate(policyFor()).gate(toolName);
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1540
1596
|
|
|
1541
1597
|
// src/data-plane.ts
|
|
1542
1598
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1543
1599
|
import { WebSocket } from "node:http";
|
|
1544
|
-
import { homedir as
|
|
1545
|
-
import { join as
|
|
1600
|
+
import { homedir as homedir3 } from "node:os";
|
|
1601
|
+
import { join as join6 } from "node:path";
|
|
1546
1602
|
var SEEN_MAX = 5e3;
|
|
1547
1603
|
function errText(e) {
|
|
1548
1604
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -1585,8 +1641,8 @@ var SecureComsDataPlane = class {
|
|
|
1585
1641
|
generation = 0;
|
|
1586
1642
|
constructor(cfg, onMessage) {
|
|
1587
1643
|
this.identityId = cfg.identityId;
|
|
1588
|
-
const dir = cfg.connectionsDir ??
|
|
1589
|
-
this.connectionPath =
|
|
1644
|
+
const dir = cfg.connectionsDir ?? join6(homedir3(), ".securecoms", "connections");
|
|
1645
|
+
this.connectionPath = join6(dir, `${cfg.identityId}.json`);
|
|
1590
1646
|
this.establishedMs = cfg.establishedMs ?? 5e3;
|
|
1591
1647
|
this.initialDelayMs = cfg.initialDelayMs ?? 250;
|
|
1592
1648
|
this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
|
|
@@ -1901,8 +1957,8 @@ ${longTerm}` });
|
|
|
1901
1957
|
}
|
|
1902
1958
|
|
|
1903
1959
|
// src/persona.ts
|
|
1904
|
-
import { existsSync as
|
|
1905
|
-
import { dirname as dirname3, join as
|
|
1960
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1961
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
1906
1962
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1907
1963
|
var PERSONA_MAX_BYTES = 8192;
|
|
1908
1964
|
var PERSONA_SOURCES = [
|
|
@@ -1918,17 +1974,17 @@ var SAFETY_PREAMBLE = [
|
|
|
1918
1974
|
"- Only use the tools you were given; do not invent capabilities."
|
|
1919
1975
|
].join("\n");
|
|
1920
1976
|
function defaultPersonaDir() {
|
|
1921
|
-
return
|
|
1977
|
+
return join7(dirname3(fileURLToPath2(import.meta.url)), "..", "persona");
|
|
1922
1978
|
}
|
|
1923
1979
|
function assemblePrompt(parts) {
|
|
1924
1980
|
return parts.map((p) => `${p.heading}
|
|
1925
1981
|
${p.body}`).join("\n\n");
|
|
1926
1982
|
}
|
|
1927
|
-
function loadPersona(
|
|
1983
|
+
function loadPersona(dirs) {
|
|
1928
1984
|
const parts = [];
|
|
1929
1985
|
for (const { file, heading } of PERSONA_SOURCES) {
|
|
1930
|
-
const path =
|
|
1931
|
-
if (!
|
|
1986
|
+
const path = findConfigFile(file, dirs);
|
|
1987
|
+
if (!path) continue;
|
|
1932
1988
|
let body;
|
|
1933
1989
|
try {
|
|
1934
1990
|
body = readFileSync8(path, "utf8");
|
|
@@ -1945,25 +2001,66 @@ function loadPersona(dataDir) {
|
|
|
1945
2001
|
}
|
|
1946
2002
|
return parts.length ? assemblePrompt(parts) : null;
|
|
1947
2003
|
}
|
|
1948
|
-
function systemPromptFor(
|
|
1949
|
-
return `${loadPersona(
|
|
2004
|
+
function systemPromptFor(dirs, fallback) {
|
|
2005
|
+
return `${loadPersona(dirs) ?? fallback}
|
|
1950
2006
|
|
|
1951
2007
|
${SAFETY_PREAMBLE}`;
|
|
1952
2008
|
}
|
|
2009
|
+
function writeLocalConfigReport(managedDir, dataDir, now, hasSearchKey) {
|
|
2010
|
+
if (!existsSync6(managedDir)) return false;
|
|
2011
|
+
const persona = {};
|
|
2012
|
+
for (const { file } of PERSONA_SOURCES) {
|
|
2013
|
+
const path2 = join7(dataDir, file);
|
|
2014
|
+
if (!existsSync6(path2)) continue;
|
|
2015
|
+
try {
|
|
2016
|
+
persona[file] = readFileSync8(path2, "utf8").slice(0, PERSONA_MAX_BYTES);
|
|
2017
|
+
} catch {
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const doc = {
|
|
2021
|
+
schema: "securecoms.local-config/1",
|
|
2022
|
+
written_at: now.toISOString(),
|
|
2023
|
+
has_search_key: hasSearchKey,
|
|
2024
|
+
persona
|
|
2025
|
+
};
|
|
2026
|
+
const path = join7(managedDir, "local-config.json");
|
|
2027
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2028
|
+
try {
|
|
2029
|
+
writeFileSync3(tmp, `${JSON.stringify(doc, null, 2)}
|
|
2030
|
+
`, { mode: 384 });
|
|
2031
|
+
renameSync(tmp, path);
|
|
2032
|
+
} catch (e) {
|
|
2033
|
+
try {
|
|
2034
|
+
unlinkSync(tmp);
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
throw e;
|
|
2038
|
+
}
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
1953
2041
|
function personaInit(dataDir, defaultsDir, writeFile, exists) {
|
|
1954
2042
|
const written = [];
|
|
1955
2043
|
const skipped = [];
|
|
1956
2044
|
for (const { file } of PERSONA_SOURCES) {
|
|
1957
|
-
const target =
|
|
2045
|
+
const target = join7(dataDir, file);
|
|
1958
2046
|
if (exists(target)) {
|
|
1959
2047
|
skipped.push(target);
|
|
1960
2048
|
continue;
|
|
1961
2049
|
}
|
|
1962
|
-
writeFile(target, readFileSync8(
|
|
2050
|
+
writeFile(target, readFileSync8(join7(defaultsDir, file), "utf8"));
|
|
1963
2051
|
written.push(target);
|
|
1964
2052
|
}
|
|
1965
2053
|
return { written, skipped };
|
|
1966
2054
|
}
|
|
2055
|
+
function createLocalConfigReporter(managedDir, dataDir, now) {
|
|
2056
|
+
let lastWritten = null;
|
|
2057
|
+
return (hasSearchKey) => {
|
|
2058
|
+
if (lastWritten === hasSearchKey) return false;
|
|
2059
|
+
const wrote = writeLocalConfigReport(managedDir, dataDir, now(), hasSearchKey);
|
|
2060
|
+
if (wrote) lastWritten = hasSearchKey;
|
|
2061
|
+
return wrote;
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
1967
2064
|
|
|
1968
2065
|
// src/warning-filter.ts
|
|
1969
2066
|
function installWarningFilter(proc = process) {
|
|
@@ -1978,7 +2075,7 @@ function installWarningFilter(proc = process) {
|
|
|
1978
2075
|
|
|
1979
2076
|
// src/index.ts
|
|
1980
2077
|
if (process.argv.includes("--version")) {
|
|
1981
|
-
const pkg = JSON.parse(readFileSync9(
|
|
2078
|
+
const pkg = JSON.parse(readFileSync9(join8(dirname4(dirname4(fileURLToPath3(import.meta.url))), "package.json"), "utf8"));
|
|
1982
2079
|
console.log(pkg.version);
|
|
1983
2080
|
process.exit(0);
|
|
1984
2081
|
}
|
|
@@ -2009,10 +2106,10 @@ function realServiceDeps() {
|
|
|
2009
2106
|
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
2010
2107
|
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
2011
2108
|
},
|
|
2012
|
-
writeFile: (p, data) =>
|
|
2109
|
+
writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
|
|
2013
2110
|
removeFile: (p) => rmSync(p, { force: true }),
|
|
2014
2111
|
mkdirp: (p) => mkdirSync3(p, { recursive: true }),
|
|
2015
|
-
fileExists: (p) =>
|
|
2112
|
+
fileExists: (p) => existsSync7(p),
|
|
2016
2113
|
canRead: (p) => {
|
|
2017
2114
|
try {
|
|
2018
2115
|
accessSync(p, constants.R_OK);
|
|
@@ -2027,7 +2124,7 @@ function realServiceDeps() {
|
|
|
2027
2124
|
function realResolveNodeDeps() {
|
|
2028
2125
|
return {
|
|
2029
2126
|
realpath: (p) => realpathSync(p),
|
|
2030
|
-
fileExists: (p) =>
|
|
2127
|
+
fileExists: (p) => existsSync7(p),
|
|
2031
2128
|
nodeMajorOf: (p) => {
|
|
2032
2129
|
const r = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
2033
2130
|
if (r.status !== 0) return null;
|
|
@@ -2047,7 +2144,7 @@ function runServiceVerb(verb) {
|
|
|
2047
2144
|
console.error(`securecoms-agent: ${MISSING_IDENTITY_MESSAGE}`);
|
|
2048
2145
|
process.exit(2);
|
|
2049
2146
|
}
|
|
2050
|
-
const home =
|
|
2147
|
+
const home = homedir4();
|
|
2051
2148
|
try {
|
|
2052
2149
|
const execPath = chooseVerbExecPath(
|
|
2053
2150
|
verb,
|
|
@@ -2123,13 +2220,18 @@ async function main() {
|
|
|
2123
2220
|
const r = personaInit(
|
|
2124
2221
|
cfgForPersona.dataDir,
|
|
2125
2222
|
defaultPersonaDir(),
|
|
2126
|
-
(p, d) =>
|
|
2127
|
-
(p) =>
|
|
2223
|
+
(p, d) => writeFileSync4(p, d, { mode: 384 }),
|
|
2224
|
+
(p) => existsSync7(p)
|
|
2128
2225
|
);
|
|
2129
2226
|
for (const p of r.written) console.log(`wrote ${p}`);
|
|
2130
2227
|
for (const p of r.skipped) console.log(`kept ${p} (already exists)`);
|
|
2131
2228
|
} else {
|
|
2132
|
-
console.log(
|
|
2229
|
+
console.log(
|
|
2230
|
+
systemPromptFor(
|
|
2231
|
+
configSearchPath(cfgForPersona.identityId, cfgForPersona.dataDir),
|
|
2232
|
+
DEFAULT_SYSTEM_PROMPT
|
|
2233
|
+
)
|
|
2234
|
+
);
|
|
2133
2235
|
}
|
|
2134
2236
|
process.exit(0);
|
|
2135
2237
|
}
|
|
@@ -2155,7 +2257,7 @@ async function main() {
|
|
|
2155
2257
|
const memoryFor = (channelId) => {
|
|
2156
2258
|
let mem = memories.get(channelId);
|
|
2157
2259
|
if (!mem) {
|
|
2158
|
-
mem = new AgentMemory(
|
|
2260
|
+
mem = new AgentMemory(join8(cfg.dataDir, "channels", safeChannelSegment(channelId)));
|
|
2159
2261
|
memories.set(channelId, mem);
|
|
2160
2262
|
}
|
|
2161
2263
|
return mem;
|
|
@@ -2170,24 +2272,45 @@ async function main() {
|
|
|
2170
2272
|
return null;
|
|
2171
2273
|
}
|
|
2172
2274
|
};
|
|
2275
|
+
const configDirs = configSearchPath(cfg.identityId, cfg.dataDir);
|
|
2276
|
+
const managedDir = managedConfigDir(cfg.identityId);
|
|
2277
|
+
const hasSearchKey = () => hasReadableKeyFile([cfg.braveKeyFile, cfg.tavilyKeyFile], (p) => readFileSync9(p, "utf8"));
|
|
2278
|
+
migrateMemoryIntoManaged(managedDir, cfg.dataDir);
|
|
2279
|
+
const reportLocalConfig = createLocalConfigReporter(managedDir, cfg.dataDir, () => /* @__PURE__ */ new Date());
|
|
2280
|
+
reportLocalConfig(hasSearchKey());
|
|
2281
|
+
const currentSkillsConfig = () => {
|
|
2282
|
+
const managed = findConfigFile("skills.json", [managedDir]);
|
|
2283
|
+
return managed ? loadSkillsConfigFile(managed, cfg.dataDir) : loadSkillsConfigFile(cfg.skillsConfigPath, cfg.dataDir);
|
|
2284
|
+
};
|
|
2173
2285
|
const builtinCtx = {
|
|
2174
2286
|
dataDir: cfg.dataDir,
|
|
2287
|
+
configDirs,
|
|
2175
2288
|
readKeyFile,
|
|
2176
2289
|
fetch,
|
|
2177
2290
|
resolve: systemResolver(),
|
|
2178
|
-
skillsConfig
|
|
2291
|
+
get skillsConfig() {
|
|
2292
|
+
return currentSkillsConfig();
|
|
2293
|
+
},
|
|
2179
2294
|
now: () => /* @__PURE__ */ new Date(),
|
|
2180
2295
|
log
|
|
2181
2296
|
};
|
|
2182
2297
|
const skills = new SkillInvoker(cfg.skillsDir, builtinCtx);
|
|
2183
2298
|
skills.loadSkills();
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2299
|
+
const policyFor = () => resolveToolPolicy(cfg.toolGateEnv, currentSkillsConfig().tools, {
|
|
2300
|
+
default: cfg.toolGate.default,
|
|
2301
|
+
allow: DEFAULT_TOOL_ALLOW
|
|
2302
|
+
});
|
|
2303
|
+
const gate = createDynamicToolGate(policyFor);
|
|
2304
|
+
const offeredTools = () => {
|
|
2305
|
+
const sc = currentSkillsConfig();
|
|
2306
|
+
const g = createLocalToolGate(policyFor());
|
|
2307
|
+
const searchKey = hasSearchKey();
|
|
2308
|
+
reportLocalConfig(searchKey);
|
|
2309
|
+
return skills.available(requiresEnvFrom(sc, searchKey)).filter((t) => g.gate(t.function.name).allowed);
|
|
2310
|
+
};
|
|
2311
|
+
for (const held of skills.unavailable(requiresEnvFrom(currentSkillsConfig(), hasSearchKey()))) {
|
|
2188
2312
|
console.log(`unavailable: ${held.name} (missing ${held.missing.join(", ")})`);
|
|
2189
2313
|
}
|
|
2190
|
-
const gate = createLocalToolGate(cfg.toolGate);
|
|
2191
2314
|
const plane = createSecureComsDataPlane(
|
|
2192
2315
|
{ identityId: cfg.identityId, log: (level, msg) => log(level, msg) },
|
|
2193
2316
|
buildHandler({
|
|
@@ -2196,16 +2319,16 @@ async function main() {
|
|
|
2196
2319
|
skills,
|
|
2197
2320
|
gate,
|
|
2198
2321
|
log,
|
|
2199
|
-
// All
|
|
2200
|
-
//
|
|
2201
|
-
// message, with no restart.
|
|
2202
|
-
systemPromptFor: () => systemPromptFor(
|
|
2203
|
-
longTermMemory: () => readLongTermMemory(
|
|
2204
|
-
tools:
|
|
2322
|
+
// All of these are read PER TURN: a persona edit, a new remembered fact, a
|
|
2323
|
+
// key file dropped in, or configuration saved in the web app takes effect
|
|
2324
|
+
// on the next message, with no restart.
|
|
2325
|
+
systemPromptFor: () => systemPromptFor(configDirs, DEFAULT_SYSTEM_PROMPT),
|
|
2326
|
+
longTermMemory: () => readLongTermMemory(configDirs),
|
|
2327
|
+
tools: offeredTools
|
|
2205
2328
|
})
|
|
2206
2329
|
);
|
|
2207
2330
|
console.log(
|
|
2208
|
-
`securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${
|
|
2331
|
+
`securecoms-agent ${cfg.identityId.slice(0, 8)} \u2192 llm ${endpointHost(cfg.llmEndpoint)} (${cfg.llmModel}), ${offeredTools().length} skills, tool gate default=${cfg.toolGate.default}`
|
|
2209
2332
|
);
|
|
2210
2333
|
await plane.start();
|
|
2211
2334
|
const shutdown = () => {
|