@securecoms/agent-runtime 0.7.0 → 0.9.1

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.
Files changed (3) hide show
  1. package/README.md +87 -7
  2. package/dist/bin.js +2890 -297
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -79,6 +79,66 @@ process (from `--identity`/`SECURECOMS_IDENTITY`) must match both the
79
79
  identity recorded in the connection file and the identity the local daemon
80
80
  reports on its health endpoint. Any mismatch refuses the connection outright.
81
81
 
82
+ ## Execution boundaries
83
+
84
+ Every model request and response is limited to 128 KiB and the complete
85
+ provider exchange has a 180-second deadline. The full model/tool turn has a
86
+ five-minute deadline. The same cancellation signal reaches model fetches,
87
+ network skills, SQLite subprocesses, and Postgres sockets when a turn times
88
+ out, its WebSocket disconnects, or the runtime stops.
89
+
90
+ Turns are serialized per channel because channel memory is mutable. One agent
91
+ may run at most four turns across different channels; additional work is
92
+ refused with a retryable busy response instead of entering an unbounded queue.
93
+ Network skills retain their independent 10-second and 2-MiB fetch boundaries,
94
+ and tool results retain the common 20,000-byte response boundary.
95
+
96
+ ### Retry idempotency for external side effects
97
+
98
+ The reachable execution inventory is intentionally narrow. `echo`,
99
+ `current_time`, `web_search`, `fetch_url`, `sql_query`, and GET-only
100
+ `http_request` calls use read grants. The only currently supported
101
+ state-changing surface is a configured `http_request` using POST, PUT, or
102
+ DELETE. Custom scripts and every other current built-in fail closed if they
103
+ are presented with a side-effect grant.
104
+
105
+ A state-changing HTTP call is offered and executed only when an active
106
+ per-turn grant binds all of the following: the tool, a canonical SHA-256 of
107
+ the exact arguments, a confirmation id, an idempotency key, and an expiry.
108
+ The runtime checks this binding both before execution and again inside the
109
+ skill invoker. A read grant never authorizes a state-changing method, and a
110
+ side-effect grant never authorizes GET.
111
+
112
+ Before the external request starts, a per-agent mode-0600 SQLite ledger
113
+ durably binds the idempotency key to the confirmed operation. A completed
114
+ retry returns `already_applied` without repeating the request. A process loss,
115
+ cancelled request, transport failure, or crash between the external response
116
+ and ledger completion leaves an ambiguous claim; retries then fail closed and
117
+ require reconciliation. The ledger stores only identifiers, hashes, states,
118
+ and timestamps—not request bodies, responses, or message content—and refuses
119
+ new operations after 10,000 claims rather than growing without bound.
120
+
121
+ After a crash or host replacement, list unresolved effects without exposing
122
+ request or response content:
123
+
124
+ ```
125
+ securecoms-agent side-effects list --identity <identity_id> [--data-dir <dir>]
126
+ ```
127
+
128
+ Verify the outcome against the external system using its own operation or
129
+ idempotency record, then bind the decision to every identifier printed by the
130
+ list command:
131
+
132
+ ```
133
+ securecoms-agent side-effects resolve --identity <identity_id> [--data-dir <dir>] \
134
+ --idempotency-key <key> --operation-sha256 <digest> \
135
+ --confirmation-id <id> --outcome completed
136
+ ```
137
+
138
+ Use `--outcome not_applied` only after independently proving the effect did not
139
+ occur; that removes the pre-effect claim and permits one exact retry. `completed`
140
+ keeps the durable suppression and all later retries return `already_applied`.
141
+
82
142
  ## Configuration
83
143
 
84
144
  All configuration is via `SECURECOMS_*` environment variables (see
@@ -101,6 +161,9 @@ All configuration is via `SECURECOMS_*` environment variables (see
101
161
  | `SECURECOMS_TOOL_ALLOW` | `echo`, `web_search`, `fetch_url`, `http_request`, `sql_query`, `current_time` | Comma-separated tool allow-list |
102
162
  | `SECURECOMS_TOOL_DENY` | unset | Comma-separated tool deny-list (wins over allow) |
103
163
  | `SECURECOMS_TOOL_DEFAULT` | `deny` | Fallback policy for a tool matched by neither list |
164
+ | `SECURECOMS_LLM_MAX_OUTPUT_TOKENS` | `8192` | Output-token cap sent on every model request (`max_tokens`; `max_completion_tokens` for `api.openai.com`). Whole number ≥ 1 |
165
+ | `SECURECOMS_LLM_DAILY_CALL_LIMIT` | `2000` | Model requests per UTC day. When spent, each channel gets one "daily usage limit" reply and the provider is not called again until 00:00 UTC. `0` = unlimited |
166
+ | `SECURECOMS_LLM_DAILY_TOKEN_LIMIT` | `5000000` | Tokens per UTC day, same behaviour as the call limit. `0` = unlimited. Each request first reserves its worst case (its size in bytes plus the output-token cap), is refused if that does not fit, and the reserve is saved to disk before the request is sent (if it cannot be saved, the request is refused). The reserve is refunded down to the provider-reported usage, kept whole when the provider may have billed without reporting (timeout, oversize or unreadable answer, no usage in the answer), and refunded whole only when the request never left (DNS failure, connection refused). A non-zero limit smaller than the largest possible request (128 KiB plus the output cap) is refused at startup. Counters persist in `<dataDir>/llm-budget-<identity>.json` (counts only, no content). See *Daily budget: limits of the guarantee* below |
104
167
 
105
168
  ### The LLM source: explicit vs managed
106
169
 
@@ -184,6 +247,10 @@ required. `handler` is one of:
184
247
  A manifest or `SKILL.md` with any other handler shape, or a missing required
185
248
  field, is skipped with a warning, and a skill invocation that throws is
186
249
  turned into a `tool_failed` tool message rather than dropping the turn.
250
+ Every built-in and script result crosses the same 20,000-byte response
251
+ boundary. Oversized plain text ends with `…[truncated]`; oversized JSON and
252
+ errors remain valid JSON with `truncated: true` instead of being sliced into
253
+ an invalid envelope.
187
254
 
188
255
  ### Gating a skill on configuration
189
256
 
@@ -207,7 +274,7 @@ the line — for example `requires: [endpoint:crm]` in
207
274
 
208
275
  Six built-in handlers ship with the runtime. All network built-ins refuse
209
276
  `localhost` and any private, loopback or link-local resolved address, and
210
- every tool result is capped at 20 kB (truncated, not an error). A redirect to a
277
+ every tool result is capped at 20,000 UTF-8 bytes (truncated, not an error). A redirect to a
211
278
  different origin is followed without the caller's headers and without the
212
279
  request body, so a configured header value or a search key can never be handed
213
280
  to a host the operator did not name.
@@ -341,12 +408,12 @@ the local allow/deny gate before showing tools to the model, then repeats the
341
408
  same channel-grant check immediately before execution. Missing or malformed
342
409
  claims, inactive/unknown membership, and absent grants all fail closed.
343
410
 
344
- This staged release issues no channel grants yet, so no generic tool is offered
345
- even when it is installed and enabled. Read grants can enable only the curated
346
- read-only tools; `http_request` is additionally limited to GET at execution.
347
- Side-effect grant fields are reserved in the protocol, but side-effect calls
348
- remain denied until exact-operation confirmation, expiry, and idempotency are
349
- implemented end to end.
411
+ The live bridge currently issues no channel grants, so no generic tool is
412
+ offered even when it is installed and enabled. Read grants can enable only the
413
+ curated read-only surfaces. The runtime now enforces the exact-operation,
414
+ confirmation, expiry, and durable replay contract described above for future
415
+ side-effect grants; that path remains dormant until an upstream confirmation
416
+ producer deliberately issues such a grant.
350
417
 
351
418
  ### The generated tool paragraph is an installation summary
352
419
 
@@ -374,3 +441,16 @@ time, so a configured but ungranted or denied tool is still refused.
374
441
  See [PROVENANCE.md](./PROVENANCE.md) for which files were ported from
375
442
  `agentvault-federal`'s reference `agent-runtime` package and how the
376
443
  AgentVault package-dependency cord was cut.
444
+
445
+ ## Daily budget: limits of the guarantee
446
+
447
+ - **Reported usage is clamped to the request's worst case.** A provider that bills beyond the output cap it was sent, or adds hidden system or tool tokens, is counted at most the reserve for that request.
448
+ - **Around 00:00 UTC.** A request reserved just before midnight is charged to the day it started. Its settlement after midnight refunds nothing into the new day, and the new day starts from zero. So for a few seconds, requests reserved on both days can be in flight at once (up to about twice the daily cap). The clock only moves the budget forward: a clock stepped back across midnight keeps the later day's counters.
449
+ - **One runtime per identity per data directory.** `<dataDir>/llm-budget-<identity>.lock` names the running process as JSON: its PID, host name, boot identity (Linux `boot_id`, otherwise the boot time) and, on Linux, the process start time.
450
+ - A runtime that cannot hold the lock **exits at startup with code 3** and logs `llm.budget_lock_held`, before it connects to the bridge, so it never displaces the running one. The same applies to `_foreign_host`, `_unreadable`, `_busy` (a holder mid-write) and `_failed` (the lock could not be written). Every one of these events names the lock file (`lock_file`, `lock_path`).
451
+ - A lock left by a process that is gone, or that was written during an earlier boot, is taken over. The boot is identified exactly (Linux `boot_id`, macOS `kern.bootsessionuuid`), so a clock step does not change it.
452
+ - **`llm.budget_lock_foreign_host`** means the lock was written under another host name. The remedy is in the event: **if no other host uses this data directory, delete the lock file it names.** One case is recovered automatically, a container that crashed and came back under a new container-id host name: the same kernel (`boot_id`) wrote the lock, and its holder stopped refreshing it (every running holder touches the file every minute) more than 10 minutes ago. Another machine, or this machine after a reboot under a new name, is never taken over automatically. A running holder checks before every request that the lock is still the one it wrote, and stops (`llm.budget_lock_lost`) if not.
453
+ - A clean exit removes the lock even if the host name changed while the runtime ran.
454
+ - On macOS a PID that is reused *within* one boot keeps the lock "held" until the stale lock file is removed by hand. Linux detects this case from the process start time.
455
+ - **The state file.** A missing file starts a fresh day. A file that exists but cannot be read (for example, left by a `sudo` run) refuses LLM calls (`llm.budget_state_read_failed`) until it can be read, and is never overwritten. A file dated more than one day ahead (a clock that was wrong when it was saved) is ignored (`llm.budget_state_future_day`), so it cannot hold the cap until that date.
456
+ - Runtimes on different hosts, or with different data directories, keep separate budgets. The budget is per runtime instance, and a data directory must never be shared between hosts.