@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 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>] [--exec-path <node>]
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`), so an operator who already exports the run-mode
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 rendered plist/unit **without writing anything or
36
- calling `launchctl`/`systemctl`** — use it to inspect an install before making
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
- Drop a manifest under `SECURECOMS_SKILLS_DIR`, either as a flat
107
- `<skillsDir>/<name>.json` file or as `<skillsDir>/<dir>/manifest.json`. A
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
- - `builtin:<name>` — one of the runtime's built-in handlers (currently
112
- `echo`, `timestamp`).
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 is skipped with a warning, and a
119
- skill invocation that throws is turned into a `tool_failed` tool message
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