@securecoms/agent-runtime 0.1.0 → 0.2.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,12 @@ 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`.
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,163 @@ 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
+ By default `skills.json` lives at `<dataDir>/skills.json`; point elsewhere
237
+ with `SECURECOMS_SKILLS_CONFIG` or `--skills-config <path>`. A missing file
238
+ means no endpoints, no databases and no flags — not an error.
239
+
240
+ ## Persona and memory
241
+
242
+ Three optional files in the data dir make up an agent's persona, read fresh
243
+ on **every turn** — no restart needed after an edit:
244
+
245
+ | File | Heading |
246
+ | --- | --- |
247
+ | `IDENTITY.md` | `## Identity` |
248
+ | `SOUL.md` | `## Soul` |
249
+ | `AGENTS.md` | `## Operating rules` |
250
+
251
+ Whichever of the three exist are joined, in that fixed order, under their
252
+ fixed headings; each is capped at 8 kB (truncated, with a warning, if
253
+ larger). If none exist, a built-in default prompt is used instead. Either
254
+ way, a fixed, non-removable safety preamble is appended last, so it is the
255
+ part of the prompt read most recently.
256
+
257
+ `securecoms-agent persona init [--identity <id>] [--data-dir <dir>]` copies
258
+ the shipped default persona files into the data dir — it never overwrites a
259
+ file that already exists, so a customized `IDENTITY.md` is safe to keep
260
+ across a re-init. `securecoms-agent persona show` prints the assembled system
261
+ prompt exactly as a turn would see it, useful for checking an edit landed.
262
+
263
+ Separately, `MEMORY.md` in the data dir is **agent-wide** (not per-persona,
264
+ not per-channel): the `remember` skill appends a dated, one-line fact to it,
265
+ and its contents are injected as a system message on every turn across every
266
+ channel. It is capped at 8 kB; once it overflows, the **oldest** lines are
267
+ dropped first, on the theory that the newest remembered facts are the ones
268
+ still likely to be true.
121
269
 
122
270
  ## Provenance
123
271