@securecoms/agent-runtime 0.1.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 +18 -0
- package/README.md +226 -15
- package/dist/bin.js +1128 -63
- package/package.json +10 -1
- package/persona/AGENTS.md +17 -0
- package/persona/IDENTITY.md +13 -0
- package/persona/SOUL.md +15 -0
- package/skills/current_time/SKILL.md +11 -0
- package/skills/fetch_url/SKILL.md +13 -0
- package/skills/http_request/SKILL.md +21 -0
- package/skills/remember/SKILL.md +13 -0
- package/skills/sql_query/SKILL.md +21 -0
- package/skills/web_search/SKILL.md +15 -0
package/PROVENANCE.md
CHANGED
|
@@ -26,3 +26,21 @@ own transport, so no row is seeded for it here.
|
|
|
26
26
|
|
|
27
27
|
This table is complete as of 2026-09-08 and should be re-reviewed if the
|
|
28
28
|
source repo's files move or are re-cut.
|
|
29
|
+
|
|
30
|
+
## SKILL.md format (2026-09-09)
|
|
31
|
+
|
|
32
|
+
The `SKILL.md` + YAML-frontmatter skill layout follows the convention used by
|
|
33
|
+
Hermes Agent, OpenClaw and Claude Code, so a skill written for one of those is
|
|
34
|
+
recognisable here. **No code was cloned from any of them.** The frontmatter
|
|
35
|
+
parser in `src/skill-md.ts` is original and deliberately accepts only the
|
|
36
|
+
documented subset; the built-ins are written against this package's own
|
|
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
|
@@ -20,7 +20,8 @@ connection this process serves.
|
|
|
20
20
|
```
|
|
21
21
|
securecoms-agent install --identity <identity_id> [--llm-endpoint <url>] \
|
|
22
22
|
[--llm-model <model>] [--llm-key-file <path>] [--skills-dir <dir>] \
|
|
23
|
-
[--data-dir <dir>] [--
|
|
23
|
+
[--data-dir <dir>] [--brave-key-file <path>] [--tavily-key-file <path>] \
|
|
24
|
+
[--skills-config <path>] [--exec-path <node>]
|
|
24
25
|
securecoms-agent install --identity <identity_id> --dry-run
|
|
25
26
|
securecoms-agent uninstall --identity <identity_id>
|
|
26
27
|
```
|
|
@@ -28,13 +29,15 @@ securecoms-agent uninstall --identity <identity_id>
|
|
|
28
29
|
Every install flag also reads its `SECURECOMS_*` variable from the environment
|
|
29
30
|
when the flag is absent (`SECURECOMS_IDENTITY`, `SECURECOMS_LLM_ENDPOINT`,
|
|
30
31
|
`SECURECOMS_LLM_MODEL`, `SECURECOMS_LLM_API_KEY_FILE`, `SECURECOMS_SKILLS_DIR`,
|
|
31
|
-
`SECURECOMS_DATA_DIR`
|
|
32
|
+
`SECURECOMS_DATA_DIR`, `SECURECOMS_BRAVE_KEY_FILE`, `SECURECOMS_TAVILY_KEY_FILE`,
|
|
33
|
+
`SECURECOMS_SKILLS_CONFIG`), so an operator who already exports the run-mode
|
|
32
34
|
environment can just run `securecoms-agent install`. The flag always wins.
|
|
33
35
|
`--exec-path` is the one exception: it is never taken from the environment.
|
|
34
36
|
`--dry-run` prints the resolved Node path, the service file path, the log
|
|
35
|
-
path, the key file, and the
|
|
36
|
-
|
|
37
|
-
it
|
|
37
|
+
path, the key file, the brave/tavily key files and the skills config path —
|
|
38
|
+
each as a PATH only, never a key value — and the rendered plist/unit
|
|
39
|
+
**without writing anything or calling `launchctl`/`systemctl`** — use it to
|
|
40
|
+
inspect an install before making it.
|
|
38
41
|
|
|
39
42
|
`install` writes a per-user service that starts the agent at login and
|
|
40
43
|
restarts it on crash — a launchd LaunchAgent at
|
|
@@ -88,10 +91,13 @@ All configuration is via `SECURECOMS_*` environment variables (see
|
|
|
88
91
|
| `SECURECOMS_LLM_MODEL` | `llama3` | Model name to request |
|
|
89
92
|
| `SECURECOMS_LLM_API_KEY` | unset | API key for the LLM endpoint, if it requires one |
|
|
90
93
|
| `SECURECOMS_LLM_API_KEY_FILE` | unset (or `--llm-key-file <path>`) | File holding the API key (trimmed on read). Preferred: it wins over `SECURECOMS_LLM_API_KEY`, and it is the only key form the service installer accepts. An unreadable or empty file exits 2 |
|
|
94
|
+
| `SECURECOMS_BRAVE_KEY_FILE` | unset (or `--brave-key-file <path>`) | File holding a Brave Search API key. A path, never a value — required for `web_search` |
|
|
95
|
+
| `SECURECOMS_TAVILY_KEY_FILE` | unset (or `--tavily-key-file <path>`) | File holding a Tavily API key. Used only when the Brave key file is absent |
|
|
91
96
|
| `SECURECOMS_SKILLS_DIR` | this package's own `skills/` | Directory of skill manifests to load |
|
|
92
97
|
| `SECURECOMS_DATA_DIR` | `~/.securecoms/agent-runtime` | Root directory for per-channel state |
|
|
98
|
+
| `SECURECOMS_SKILLS_CONFIG` | `<dataDir>/skills.json` (or `--skills-config <path>`) | Path to `skills.json` — named endpoints, databases and flags |
|
|
93
99
|
| `SECURECOMS_LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, `error` |
|
|
94
|
-
| `SECURECOMS_TOOL_ALLOW` | `echo` | Comma-separated tool allow-list |
|
|
100
|
+
| `SECURECOMS_TOOL_ALLOW` | `echo`, `web_search`, `fetch_url`, `http_request`, `sql_query`, `current_time`, `remember` | Comma-separated tool allow-list |
|
|
95
101
|
| `SECURECOMS_TOOL_DENY` | unset | Comma-separated tool deny-list (wins over allow) |
|
|
96
102
|
| `SECURECOMS_TOOL_DEFAULT` | `deny` | Fallback policy for a tool matched by neither list |
|
|
97
103
|
|
|
@@ -103,21 +109,226 @@ history and captured memories never leak into another channel's turn.
|
|
|
103
109
|
|
|
104
110
|
## Adding a skill
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
manifest needs `name`, `description`, a JSON-Schema `parameters` object, and
|
|
109
|
-
a `handler`:
|
|
112
|
+
A skill is either a JSON manifest or a `SKILL.md` file, dropped under
|
|
113
|
+
`SECURECOMS_SKILLS_DIR` in one of three layouts:
|
|
110
114
|
|
|
111
|
-
- `
|
|
112
|
-
|
|
115
|
+
- flat `<skillsDir>/<name>.json`
|
|
116
|
+
- `<skillsDir>/<dir>/manifest.json`
|
|
117
|
+
- `<skillsDir>/<dir>/SKILL.md`
|
|
118
|
+
|
|
119
|
+
All three keep working side by side. The loader reads them in that order —
|
|
120
|
+
flat `.json`, then `manifest.json`, then `SKILL.md` — sorted within each
|
|
121
|
+
bucket for a deterministic result; if two files declare the same skill name,
|
|
122
|
+
the one loaded later (later bucket, or later alphabetically) wins, with a
|
|
123
|
+
warning naming both paths.
|
|
124
|
+
|
|
125
|
+
A `SKILL.md` is YAML frontmatter (between two `---` lines) followed by
|
|
126
|
+
free-text guidance appended to the model-facing description (capped at 1 kB).
|
|
127
|
+
The frontmatter accepts a deliberately small subset of YAML — anything else
|
|
128
|
+
throws with the file and line, rather than silently misreading it:
|
|
129
|
+
|
|
130
|
+
- scalars (`key: value`, optionally quoted)
|
|
131
|
+
- flow lists (`key: [a, b, c]`)
|
|
132
|
+
- flow maps (`key: { a: 1, b: 2 }`)
|
|
133
|
+
- nested mappings (an indented block under a bare `key:`), two-space indents
|
|
134
|
+
only, no tabs
|
|
135
|
+
- comments (`#`) and blank lines
|
|
136
|
+
- block scalars (`|`, `>`), anchors, aliases and other YAML constructs are
|
|
137
|
+
**not** supported — use a JSON manifest for anything that needs them
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
---
|
|
143
|
+
name: web_search
|
|
144
|
+
description: Search the web for recent information
|
|
145
|
+
parameters:
|
|
146
|
+
type: object
|
|
147
|
+
properties:
|
|
148
|
+
query: { type: string }
|
|
149
|
+
count: { type: number }
|
|
150
|
+
required: [query]
|
|
151
|
+
handler: builtin:web_search
|
|
152
|
+
requires: [search_provider]
|
|
153
|
+
---
|
|
154
|
+
Use this when the user asks about something time-sensitive or outside your
|
|
155
|
+
training data. Prefer a specific query over a broad one.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`name`, `description`, `parameters` (a nested mapping) and `handler` are
|
|
159
|
+
required. `handler` is one of:
|
|
160
|
+
|
|
161
|
+
- `builtin:<name>` — one of the runtime's built-in handlers (see "Curated
|
|
162
|
+
skills" below).
|
|
113
163
|
- `script:<path>` — an executable file, path relative to `skillsDir` and
|
|
114
164
|
contained within it (symlinks included — a link that resolves outside
|
|
115
165
|
`skillsDir` is refused). It runs with its arguments as JSON on stdin, cwd
|
|
116
166
|
set to `skillsDir`, and a minimal environment (no inherited secrets).
|
|
117
167
|
|
|
118
|
-
A manifest with any other handler shape
|
|
119
|
-
|
|
120
|
-
rather than dropping the turn.
|
|
168
|
+
A manifest or `SKILL.md` with any other handler shape, or a missing required
|
|
169
|
+
field, is skipped with a warning, and a skill invocation that throws is
|
|
170
|
+
turned into a `tool_failed` tool message rather than dropping the turn.
|
|
171
|
+
|
|
172
|
+
### Gating a skill on configuration
|
|
173
|
+
|
|
174
|
+
A manifest or `SKILL.md` may carry `requires: [...]`, and the skill is offered
|
|
175
|
+
to the model only when every entry is configured: `endpoint:<name>` and
|
|
176
|
+
`database:<name>` name entries in `skills.json`, `search_provider` means a
|
|
177
|
+
readable, non-empty search key file, `any_endpoint` / `any_database` mean at
|
|
178
|
+
least one endpoint or database is configured, and anything else is matched
|
|
179
|
+
against `skills.json`'s `flags`.
|
|
180
|
+
|
|
181
|
+
The shipped `http_request` and `sql_query` skills declare `requires:
|
|
182
|
+
[any_endpoint]` and `requires: [any_database]`: the endpoint and database names
|
|
183
|
+
belong to the customer, so the shipped copies cannot name one, but neither is
|
|
184
|
+
offered to the model until `skills.json` gives it something to reach. With no
|
|
185
|
+
endpoints configured, startup prints `unavailable: http_request (missing
|
|
186
|
+
any_endpoint)`. To narrow one to a particular system, copy the folder and change
|
|
187
|
+
the line — for example `requires: [endpoint:crm]` in
|
|
188
|
+
`skills/http_request/SKILL.md`.
|
|
189
|
+
|
|
190
|
+
## Curated skills
|
|
191
|
+
|
|
192
|
+
Seven built-in handlers ship with the runtime. All network built-ins refuse
|
|
193
|
+
`localhost` and any private, loopback or link-local resolved address, and
|
|
194
|
+
every tool result is capped at 20 kB (truncated, not an error). A redirect to a
|
|
195
|
+
different origin is followed without the caller's headers and without the
|
|
196
|
+
request body, so a configured header value or a search key can never be handed
|
|
197
|
+
to a host the operator did not name.
|
|
198
|
+
|
|
199
|
+
| Skill | Arguments | Notes |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| `echo` | `text` | Returns the text unchanged. No `requires`. |
|
|
202
|
+
| `current_time` | `timezone` (an IANA name such as `America/New_York`; defaults to UTC) | The server clock, ISO 8601. No `requires`. |
|
|
203
|
+
| `fetch_url` | `url` | Fetches a page and returns extracted text (HTML is stripped to text). Up to 3 redirects, 10 s timeout, 2 MB response cap. |
|
|
204
|
+
| `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. |
|
|
205
|
+
| `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. |
|
|
206
|
+
| `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. |
|
|
207
|
+
| `remember` | `fact` (≤300 chars) | Appends a dated line to the agent-wide `MEMORY.md`. |
|
|
208
|
+
|
|
209
|
+
## skills.json
|
|
210
|
+
|
|
211
|
+
`skills.json` names the endpoints and databases the curated skills are
|
|
212
|
+
allowed to reach, plus arbitrary boolean `flags` for gating. It carries paths,
|
|
213
|
+
never secrets — a header value or a Postgres connection string is read from
|
|
214
|
+
the file it names, at call time, and never logged. Relative paths (a
|
|
215
|
+
`sqlite`, `postgresUrlFile` or `headersFile` value) resolve against the data
|
|
216
|
+
dir, not the current working directory.
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"endpoints": {
|
|
221
|
+
"crm": { "baseUrl": "https://crm.example.com/api", "headersFile": "crm-headers.json" }
|
|
222
|
+
},
|
|
223
|
+
"databases": {
|
|
224
|
+
"reports": { "sqlite": "reports.db" },
|
|
225
|
+
"warehouse": { "postgresUrlFile": "warehouse-url.txt" }
|
|
226
|
+
},
|
|
227
|
+
"flags": ["beta_tools"]
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A `headersFile` is a flat JSON object of header name to string value, read
|
|
232
|
+
fresh on every `http_request` call — never cached, never logged. A Postgres
|
|
233
|
+
database needs the optional `pg` package installed separately
|
|
234
|
+
(`npm i -g pg`); SQLite needs nothing extra (`node:sqlite`).
|
|
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
|
+
|
|
241
|
+
By default `skills.json` lives at `<dataDir>/skills.json`; point elsewhere
|
|
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.
|
|
245
|
+
|
|
246
|
+
## Persona and memory
|
|
247
|
+
|
|
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
|
|
250
|
+
on **every turn** — no restart needed after an edit:
|
|
251
|
+
|
|
252
|
+
| File | Heading |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `IDENTITY.md` | `## Identity` |
|
|
255
|
+
| `SOUL.md` | `## Soul` |
|
|
256
|
+
| `AGENTS.md` | `## Operating rules` |
|
|
257
|
+
|
|
258
|
+
Whichever of the three exist are joined, in that fixed order, under their
|
|
259
|
+
fixed headings; each is capped at 8 kB (truncated, with a warning, if
|
|
260
|
+
larger). If none exist, a built-in default prompt is used instead. Either
|
|
261
|
+
way, a fixed, non-removable safety preamble is appended last, so it is the
|
|
262
|
+
part of the prompt read most recently.
|
|
263
|
+
|
|
264
|
+
`securecoms-agent persona init [--identity <id>] [--data-dir <dir>]` copies
|
|
265
|
+
the shipped default persona files into the data dir — it never overwrites a
|
|
266
|
+
file that already exists, so a customized `IDENTITY.md` is safe to keep
|
|
267
|
+
across a re-init. `securecoms-agent persona show` prints the assembled system
|
|
268
|
+
prompt exactly as a turn would see it, useful for checking an edit landed.
|
|
269
|
+
|
|
270
|
+
Separately, `MEMORY.md` in the data dir is **agent-wide** (not per-persona,
|
|
271
|
+
not per-channel): the `remember` skill appends a dated, one-line fact to it,
|
|
272
|
+
and its contents are injected as a system message on every turn across every
|
|
273
|
+
channel. It is capped at 8 kB; once it overflows, the **oldest** lines are
|
|
274
|
+
dropped first, on the theory that the newest remembered facts are the ones
|
|
275
|
+
still likely to be true.
|
|
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.
|
|
121
332
|
|
|
122
333
|
## Provenance
|
|
123
334
|
|