@ai-ecoverse/slicc-vault 1.18.2 → 1.20.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/README.md CHANGED
@@ -106,7 +106,11 @@ The vault trusts a file only when:
106
106
  - `onRootFs` is true, and `users` is `enforce`;
107
107
  - `bytes` is UTF-8 and parses.
108
108
 
109
- Otherwise the vault keeps the policy it last accepted for that file, so the rules it would have changed stay as they were (new ones stay inert), and warns, e.g. `rules not trusted: /etc/fnox/config.toml: kernel users must be enforce`. The reply is `{ accepted, files: [{ path, sha256, accepted, warnings }], rules: [{ secret, domain, header, methods, paths, protocol?, position?, port? }], warnings }`, with `rules` the rules now in effect. `status.policy` repeats `files` from the last `policy` call, so Settings can show why rules aren't trusted.
109
+ Otherwise the vault keeps the policy it last accepted for that file, so the rules it would have changed stay as they were (new ones stay inert), and warns, e.g. `rules not trusted: /etc/fnox/config.toml: kernel users must be enforce`. The reply is `{ accepted, files: [{ path, sha256, accepted, warnings, absent?, paused? }], rules: [{ secret, domain, header, methods, paths, protocol?, position?, port? }], warnings }`, with `rules` the rules now in effect. `status.policy` repeats `files` from the last `policy` call, so Settings can show why rules aren't trusted.
110
+
111
+ **A missing file** is attested by the kernel as exactly `{ path, absent: true, users, onRootFs, parents }`, with no other field (so no `bytes`, `sha256`, `uid`, `mode`, `gid`, `ino` or `mtimeMs`). The vault trusts it only when `path` is one of the two files, `absent` is exactly `true`, `onRootFs` is true, `users` is `enforce`, and `parents` lists exactly the directories above `path`, by the same rules as for a file. A list that stops above a missing directory isn't trusted. Anything else with `absent` is an untrusted attestation, as above: the last accepted policy, or a pause, stays as it was.
112
+
113
+ A trusted one clears the accepted policy of that file and pauses it: while either file is paused, `forward` of a secret, `materialize`, `materializeConsume`, `sessionMaterialize`, `sessionAnchors` and the `test` verdict all refuse with `policy file missing: paused` (`EACCES`, or a `403` from `forward`) and `cause: 'policy-paused'`, so deleting `secrets-policy.toml` never falls back to permissive defaults. `forward` of an `mcp` record uses no rule and goes on. The pause is kept with the accepted policy, and ends when a trusted attestation of that file with `bytes` arrives. Until then `files` and `status.policy` show the file with `absent: true` and `paused: true`, in every `policy` reply, whether or not the call named it.
110
114
 
111
115
  **The KS-7 conformance fixtures** are `test/integration/fixtures/attest/*.json`: `{ description, trusted, reason?, attestation }`, with `bytes` as base64, written by `make.mjs` next to them. `good-*` must be trusted, and every `bad-*` must be refused with a warning containing `reason`. The vault's own tests run them through the `policy` op, and the kernel's tests can check that `kernel.attest` produces the good shape.
112
116
 
@@ -142,7 +146,7 @@ shell_per_day = 5000
142
146
  - `[exits]` is **optional**: a secret without a line may use every exit; a line restricts it to the exits listed (`page`, `proxy`, `tailscale`, `device:<id>`). `<id>` is the device's `DeviceId`, the stable id of its link certificate (a tray's 64 hex characters, or `local:…` for a CLI linked on the same computer), never its display name, which can be renamed or shared by two devices: one or more of `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` and `-`. An entry is compared exactly with the `exit` bios sends. Unknown names are dropped from the line with a warning, which leaves the line narrower, never wider.
143
147
  - `[limits]` overrides the defaults above with positive integers.
144
148
 
145
- Accepted policy is kept per namespace in IndexedDB, and loaded when an embedder says hello. Each file is audited once per SHA-256 and verdict (the vault remembers the last 64 per file) as a `policy` entry, with `accepted`, any `warning`, and its `changes` against the previously accepted file: rules `added` or `removed`, `exec` modes, `exits` lines and `limits`. Any change to the accepted policy, or a new audit entry, pushes `changed` with `secrets`, `exits` and `audit`, and other vault documents of the namespace reload the policy. Each `policy` call reads, merges and writes the stored policy in one transaction, so two pages can't undo each other's update.
149
+ Accepted policy is kept per namespace in IndexedDB, and loaded when an embedder says hello. Each file is audited once per SHA-256 and verdict (the vault remembers the last 64 per file) as a `policy` entry, with `accepted`, any `warning`, and its `changes` against the previously accepted file: rules `added` or `removed`, `exec` modes, `exits` lines and `limits`. A pause and its end are always audited: the entry that starts one has `absent: true` and `sha256: null`. Any change to the accepted policy, or a new audit entry, pushes `changed` with `secrets`, `exits` and `audit`, and other vault documents of the namespace reload the policy. Each `policy` call reads, merges and writes the stored policy in one transaction, so two pages can't undo each other's update.
146
150
 
147
151
  ## Scrub state
148
152
 
@@ -167,14 +171,14 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
167
171
 
168
172
  - The frame answers only when the message comes from its own parent window, from an allowed origin, matching `location.ancestorOrigins[0]` when the browser exposes it. Anything else gets no answer at all.
169
173
  - On `port2` it answers `{ v: 1, op: 'welcome', role, status }`, or `{ v: 1, op: 'refused', error }` for another protocol version or an unknown role.
170
- - Everything after that goes over the port. The namespace and the role are fixed by the hello. Requests are `{ v: 1, id, op, ...fields }`; replies are `{ v: 1, id, op, ...fields }` or `{ v: 1, id, error: { code, message } }`. Each port is answered in order (FIFO).
174
+ - Everything after that goes over the port. The namespace and the role are fixed by the hello. Requests are `{ v: 1, id, op, ...fields }`; replies are `{ v: 1, id, op, ...fields }` or `{ v: 1, id, error: { code, message, cause? } }`. Each port is answered in order (FIFO).
171
175
  - **Roles:** `page` (the default) is bios and spectrum. bios may hand spectrum a page port of its own, or relay; the messages are the same. `agent` is the port bios hands the agent worker: it may use only `redact`, `redactStream`, `push`, `idle`, `end`, `drop`, `endpoints`, `model`, `mcp`, `records`, `grant` and `refresh`, and anything else is `EACCES`. The agent port gets no events. Its ops are below (The agent's ops).
172
176
  - **Values:** only `forward` (inside the head it returns, where an accepted rule allows it), `materializeConsume` (for an `[exec]` grant), `sessionMaterialize` (for a session rule, to the kernel's TLS writer, §3.19) and `egress` (a refresh request) put a value into the page. On the agent port only `model` and `mcp` do: their reply's prepared request carries the provider's or server's credential in its headers or URL, only for that provider's or server's own URLs, for the agent worker's transport to send at once. It must never be logged, stored, retried from storage or shown. No other op, event or reply carries a value, and no value ever reaches the model.
173
177
 
174
178
  | op (page role) | fields | reply |
175
179
  |---|---|---|
176
180
  | `sealed` | `req` (the `/dev/sealed` request object, or its JSON text), `stamp` (the kernel's `{ pid, ppid, uid, gid, tag, users, groups?, user? }`) | `{ reply }`, the §3.7 reply, below |
177
- | `status` | — | `{ status: { version, protocol, origin, locked, lock, scrub, waiting, exportedAt, backupReminder, policy } }` |
181
+ | `status` | — | `{ status: { version, protocol, capabilities, origin, locked, lock, scrub, waiting, exportedAt, backupReminder, policy } }`. `capabilities` is `{ forward: 1 }`, in every `status` (the welcome's, this reply's and the `status` event's): every credentialed `forward` needs a manual exit, an MCP one a complete scrubbed body, its response is scrubbed, and a trusted missing policy file revokes. Without it, bios must not forward |
178
182
  | `placeholders` | — | `{ placeholders, scrub }`: every placeholder of a record that isn't removed, active and inert, sorted |
179
183
  | `list` | — | `{ list: { secrets, mcp, refused, audit, auditMore, exits } }` for Settings › Secrets. `mcp` is `[{ name, server, state }]`, sorted by name, the `mcp` records bound to a server that can be used now: not removed, and `state` `ready` or `refreshing` (expired with a refresh source). An expired one with no refresh source or with a dead grant, a bridge token (source `process`), one with no `server`, one whose `mcp:<server>` is longer than a request's 128-character `provider`, and one whose name or server holds a value are left out, and so is every record while the vault is locked before its first unlock |
180
184
  | `audit` | `before?` (a `seq`), `limit?` (1–1000, default 100) | `{ audit, more, next? }`, newest first; pass `next` as `before` for the next page |
@@ -188,7 +192,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
188
192
  | `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
189
193
  | `policy` | `files` | the attested policy files, below (Policy) |
190
194
  | `endpoints` | `model?`: https base URLs, or `{ name, url }`, a custom base URL for the `model` record `name`; `mcp?`: as the agent's; `remove?`: `{ model?: (url \| { name, url })[], mcp?: { server, url }[] }` | `{ ok: true }`; the user's own registrations, trusted (Model and MCP endpoints). The whole request is checked first and applied in one transaction, so an `EINVAL` anywhere changes nothing; a field other than `model`, `mcp`, `remove` and `unpin` is `EINVAL` too. `unpin?`: names of `mcp` records to unpin (needs the vault unlocked). `remove` takes back the page's own entries, never the agent's; removing the model record drops its custom URLs too. A `{ name, url }` needs a live `model` record of that name, and a plain `https:` URL on 443 without `*` or `{…}` and not under a page-registered MCP server, else `EINVAL` |
191
- | `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
195
+ | `forward` | `head`, `bodyPresent`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | `{ mode, head, bodyComplete?, body? }`, `{ needsBody: true }` or `{ refused }`, below (Forward) |
192
196
  | `materializeConsume` | `ticket`, `stamp`, `path`, `resolved`, `argv`, `cwd`, `interpreters`, `trusted` | the values for the child's environment, below (Exec) |
193
197
  | `sessionMaterialize` | `stamp`, `session`, `host`, `port`, `protocol`, `position`, `verified`, `handshakeAt`, `exit`, `verifiedBy?`, `anchor?`, then `placeholders` (and `head` for http1) at every position but `scram-sha-256`, which takes `scram` and no `placeholders` | `{ values }` for a kernel-verified TLS session, below (Sessions) |
194
198
  | `sessionAnchors` | `stamp`, `session`, `host`, `port`, `protocol`, `exit` | `{ anchors, pins }`, below (Sessions) |
@@ -207,16 +211,18 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
207
211
 
208
212
  **Errors** use errno names: `EINVAL`, `ENOENT`, `EACCES`, `EEXIST`, `EAGAIN`, `ETIMEDOUT`, `ENOTCONN`, and `ENOTSUP` for ops this version doesn't have.
209
213
 
214
+ **`cause`** is the machine-readable reason for a refusal; match on it, never on `message`. Its one value is `policy-paused`: the policy is paused (see Policy) and the vault may well be unlocked. `forward` (in `refused`), `materialize`, `materializeConsume`, `sessionMaterialize`, `sessionAnchors` (in `error`) and `test` (beside `verdict`) can return it, with `EACCES` or status `403` and `policy file missing: paused`. Every other refusal has no `cause`, a locked vault's included. `status.policy[]` says which file to restore: its entries `{ path, sha256, accepted, warnings, absent?: true, paused?: true }`, in page `status` replies and `status` events, name the missing file in `path`.
215
+
210
216
  ## `/dev/sealed`, the vault side
211
217
 
212
- bios relays each kernel request unchanged as the `sealed` op, with the kernel's stamp. The vault validates and answers `{ v: 1, ...fields }` or `{ v: 1, error: { code, message } }`. A request over 64 KiB, one that isn't a single JSON object, one with `v` other than 1, and an unknown `op` are `EINVAL`, as is a missing or malformed stamp.
218
+ bios relays each kernel request unchanged as the `sealed` op, with the kernel's stamp. The vault validates and answers `{ v: 1, ...fields }` or `{ v: 1, error: { code, message, cause? } }`. A request over 64 KiB, one that isn't a single JSON object, one with `v` other than 1, and an unknown `op` are `EINVAL`, as is a missing or malformed stamp.
213
219
 
214
220
  | op | fields | reply |
215
221
  |---|---|---|
216
222
  | `seal` | `name`, `value`, and for an MCP bridge `kind: "mcp"`, `server`, `url` | `{ placeholder, state }`. A new name creates a `process` record. An existing one is rotated and keeps its placeholder, but only when its value came from a process; a value from Settings, jerk, OAuth or an import is replaced only by uid 0, and anyone else gets `EEXIST`. **A bridge token** (§3.16.3, e.g. mcpcat's `--bearer`) is an `mcp` record bound to `server` and `url`, which must be a kernel-loopback URL (`http://<port>.kernel.localhost/…`, never 9222; anything else is `EACCES`). Any write that isn't a process's over a `process` record of another kind than `secret` (a page `put` or `seal`, a sign-in, an import) makes it an ordinary record, without the URL, under a new placeholder, so whoever sealed it keeps no placeholder of the new value; a `secret` keeps its placeholder, which every uid can read and the rules name. It records the sealing uid, and resealing it, even after it was removed, is allowed only for a `process` `mcp` record sealed by the same uid, or by uid 0; anything else, a user-entered record included, is `EEXIST` |
217
- | `placeholder` | `name`, `server?` | `{ placeholder, state }` of the `secret` record, including `removed`; `ENOENT` when there's no record. For root, with `server`, the live `mcp` record of that name bound to that server comes first, below (Root and `mcp` records) |
223
+ | `placeholder` | `name`, `server?` | `{ placeholder, state }` of the `secret` record, including `removed`; `ENOENT` when there's no record. For root, `server` changes the answer, and `name` is optional, below (Root and `mcp` records) |
218
224
  | `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
219
- | `test` | `name`, `url`, `method?` (`GET`), `header?` (`authorization`), `exit?` (`page`) | `{ verdict: 'inject' \| 'refuse', reason }`: the injector's verdict for `Bearer <placeholder>` in that header, without sending anything; `ENOENT` for an unknown name |
225
+ | `test` | `name`, `url`, `method?` (`GET`), `header?` (`authorization`), `exit?` (`page`) | `{ verdict: 'inject' \| 'refuse', reason, cause? }`: the injector's verdict for `Bearer <placeholder>` in that header, without sending anything, with `cause: 'policy-paused'` when the policy is paused; `ENOENT` for an unknown name |
220
226
  | `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
221
227
  | `request` | `kind: "oauth"`, `provider`, `oauth` (an object) | the same |
222
228
  | `request` | `kind: "jerk-seal"`, `provider` | the same |
@@ -230,6 +236,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
230
236
  - A value request is answered by the page's `put` with its `request` id. A sign-in or lease (`oauth`, `jerk-seal`) is answered by `seal` with `request` (Tokens and refresh). Every other kind is answered by the page's `answer`. `cancel` ends any of them and pushes `{ v: 1, event: 'cancelled', request, kind }` to the page, which passes it on to J1 where it can.
231
237
  - **J-8's jerk requests** (§3.7.1): the vault only keeps their state; bios learns of them from the relay and answers. `jop: "create"` and `unseal` are for root and cones only (J2): the kernel stamp must have `users: 'enforce'`, and either uid 0 or the `sudo` group among its `groups`. bios names that group's gid as `sudoGid` in its hello; a page hello without it clears the namespace's gid. A stamp without `groups`, a vault without `sudoGid`, or any other mode refuses: the request is `done` with `exit: 2` at once, before bios sees anything. The tag is kept for audit and display only.
232
238
  - **Root and `mcp` records:** the placeholder of an `mcp` record a process didn't seal is for root only, since a process holding one can call that server through `forward` past a role's tool limits. Whoever seals a bridge token holds its placeholder. Root is a stamp with uid 0 and `users: 'enforce'`, not `mayManage`'s `sudo` group: a process a human approved (sudo, including standing approvals), or the judge the owner chose to delegate sudo to (slicc-agent's `sudo.approver = agent`, whose default is `user`, a person). That root can already do more than read a placeholder. A cone or scoop isn't root, and neither is any uid under another mode, where the stamp proves nothing. Records match as kind `mcp`, source other than `process`, with `server` equal to the name given.
239
+ - `placeholder` with `server`, for root: only a usable `mcp` record bound to that server answers, as in `list.mcp` (live, `ready` or `refreshing`, its name and server holding no value, so none while the vault is locked before its first unlock), with `{ placeholder, state, kind: 'mcp' }`. Given a `name`, it's the record of that name, else `ENOENT` "no record named NAME"; a `secret` of that name or a removed record never answers. Names in these refusals are scrubbed over every value. Without a `name`, it's the server's one record: `EINVAL` "several mcp records are bound to SERVER; ask for one by name" for more than one, and `ENOENT` for none. A record's name is never derived from its server's. Every other caller, and root without `server`, gets the `secret` answer, without `kind`.
233
240
  - `placeholder` with `name` and `server` answers for root when such a record, not removed, has that name and server. Anyone else, a wrong server, a removed record, a bridge token and a record with no `server` get the reply the op gives without `server`, the same as for no record. A `seal` under a taken name still answers `EEXIST`, as above.
234
241
  - `status` of a `done` `jerk` `status` request for `mcp:<server>` with no placeholder from a `secret` record: when both the request's stored stamp and the caller are root, and bios answered exit 0, and exactly one usable record (the rule of `list.mcp`) is bound to that server, the reply adds its `placeholder` and `record`. Several usable ones are `EINVAL` for root ("several mcp records are bound to SERVER; ask for one by name"), as for the agent's `refresh`; `placeholder` by name needs no such rule. When both are root and a record is usable but bios didn't answer exit 0, bios's reply stands. Every other reader, and any reader with no usable record, gets `{ state: 'done', exit: 3, message: "no token for mcp:<server> in seven's vault: run jerk mcp:<server> create" }`, whatever bios answered. The page and bios never see this rule: bios answers from `list.mcp`, before the caller is checked.
235
242
  - Neither reply carries a value, and neither read is audited, like every read of a placeholder; each use of one is, as `mcp` (Model and MCP endpoints). Where the value is filled is Which MCP credential goes where (§3.16.1): for a Settings record, an `https:` URL under the server URL the page registered; with no page server for that name, it's pinned on first use to the agent's URL for that server, until the page registers one; for a sign-in (`oauth:mcp`), any registered `https:` URL of that server.
@@ -304,7 +311,7 @@ The vault burns the ticket, then requires that:
304
311
  - `{ shell = true }`: any program and chain, the explicit exception;
305
312
  - no real record has expired meanwhile.
306
313
 
307
- Any failure is `EACCES: ticket refused: …`, and the ticket stays burned. It answers `{ values: { NAME: value }, placeholders: { NAME: placeholder } }` for the names that were real and still exist, and marks their audit entries `consumed`. The kernel swaps each env var whose value is exactly one of those placeholders, and compiles its own scrubber over these values for the child's output (S2).
314
+ Any failure is `EACCES: ticket refused: …`, and the ticket stays burned, except for a policy that is paused or that changes before the values go out: the vault checks that in the transaction that marks the entries `consumed`, refuses with `policy file missing: paused` or `ticket refused: the policy changed meanwhile`, releases nothing, and keeps the ticket for its 30 s. It answers `{ values: { NAME: value }, placeholders: { NAME: placeholder } }` for the names that were real and still exist, and marks their audit entries `consumed`. The kernel swaps each env var whose value is exactly one of those placeholders, and compiles its own scrubber over these values for the child's output (S2).
308
315
 
309
316
  ## Forward
310
317
 
@@ -312,19 +319,27 @@ bios's exit router asks the vault to inject a request the kernel forwarded (an o
312
319
 
313
320
  ```js
314
321
  const channel = new MessageChannel();
315
- port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }, [channel.port2]);
322
+ port.postMessage({ v: 1, id, op: 'forward', head, bodyPresent, owner, exit, rawTraits, streamCredit }, [channel.port2]);
316
323
  ```
317
324
 
318
- - `head` is `{ method, url, headers: [name, value][] }`; `owner` the kernel's `{ pid, ppid, uid, gid, tag, users, groups?, user? }` (`users` must be `enforce`, else nothing is injected); `exit` the exit bios chose (`page`, `proxy`, `tailscale` or `device:<id>`, with the device's `DeviceId`); `rawTraits` that exit's `{ manualRedirects?, encodedBodies? }`. `body` (bytes or a string) is needed only for MCP requests (below); for every other request it stays in bios, because bodies are never injected.
319
- - **The reply** is `{ head }`, the same head with the values filled in, or `{ refused: { status, reason } }`: `403` with a one-line reason, or `502` for an exit with encoded bodies. A refused request must not be sent anywhere. Malformed fields are `EINVAL`.
325
+ - `head` is `{ method, url, headers: [name, value][] }`; `owner` the kernel's `{ pid, ppid, uid, gid, tag, users, groups?, user? }` (`users` must be `enforce`, else nothing is injected); `exit` the exit bios chose (`page`, `proxy`, `tailscale` or `device:<id>`, with the device's `DeviceId`); `rawTraits` that exit's `{ manualRedirects?, encodedBodies? }`; `bodyPresent` whether the request has a body, sent on every `forward`. `body` (bytes or a string) is read only for MCP requests (below); for every other request it stays in bios, because bodies are never injected.
326
+ - **The reply** says which `mode` the vault chose; bios never guesses it:
327
+ - `{ mode: 'secret', head }`: the same head with the values filled in. It has no body, whatever bios sent.
328
+ - `{ mode: 'mcp', head, bodyComplete: true, body? }`: the same, with `body` the complete request body scrubbed over every value, the only bytes bios may send. A request with `bodyPresent: false` gets no `body` field; with `bodyPresent: true` it always does, an empty `Uint8Array` for an empty body.
329
+ - `{ needsBody: true }`: an MCP request with `bodyPresent: true` and no `body`. Nothing else happened: no head, no `mcp` audit entry, no `usedAt`, no pin. Like any `forward`, it may run the one-time upgrade of legacy endpoints, with its `endpoints` audit entry. bios collects the complete body and calls `forward` again, with a new `MessageChannel`.
330
+ - `{ refused: { status, reason, cause? } }`: `403` with a one-line reason, and `cause: 'policy-paused'` when the policy is paused, `400` for an MCP request whose `bodyPresent` isn't a boolean ("an MCP request must say whether it has a body") or is `false` with a `body`, or `502` for an exit with encoded bodies. A refused request must not be sent anywhere.
331
+ - Malformed fields, and an MCP body above 32 MiB, are `EINVAL`.
332
+ - **The port:** on every reply but a `mode` one, the vault closes the transferred port.
320
333
  - **The response** goes back over `channel.port1`, scrubbed: bios posts `{ op: 'response', status, statusText, url, redirected?, headers }`, then any number of `{ op: 'body', chunk: Uint8Array }`, then `{ op: 'end' }` or `{ op: 'error', message }`. The vault answers with the same messages, with `statusText`, `url`, every header name and value, the body and the error scrubbed over every value of the namespace. A value in its raw form comes back as its placeholder, anything else as `[redacted:NAME]`. The body is scrubbed as a stream; after 250 ms without input, a held tail shorter than 16 bytes is let through.
334
+ - **When the vault's values change** (a value sealed, rotated, removed or restored, in any vault document), the scrub of a response in flight is out of date, so the response is cancelled. In order for each message from bios and for the idle timer, the vault reads the scrub version and sends what follows from that message inside the same read transaction, so no write can commit between the check and the send. If it moved, nothing derived from that message goes out: any held tail is dropped, and the vault posts one `{ op: 'error', message: "the vault's values changed; send the request again" }` and closes the port. No credit follows. bios cancels the send.
335
+ - **Flow control** is opt-in: with `streamCredit: true`, and only then, the vault follows the scrubbed output of each `{ op: 'body', chunk: Uint8Array }` it gets with exactly one `{ op: 'credit', bytes }`, `bytes` being that chunk's `byteLength`, even when the output is empty because the scrub holds a tail back. Nothing else earns a credit: not the response head, an idle flush, `end`, `error`, or a chunk that isn't a `Uint8Array`.
321
336
 
322
337
  **The injector**, fnox's proxy semantics:
323
338
  - **Hosts** are compared lower-cased, without a trailing dot, in rules, the refusal list and MCP servers alike.
324
- - **Refused before anything is sent:** an `owner` whose `users` isn't `enforce` ("kernel users must be enforce", §3.16.2; this covers kernel programs calling a registered MCP server, and the `/dev/sealed` `test` verdict, too); a placeholder in the URL or in a header name; credentials in the URL; a scheme other than `https:` or a port other than 443 (except §3.16.3's kernel servers, below); an ambiguous path (`.`/`..` segments, `\`, `%2e`, `%2f`, `%5c`, `%25`, checked on the raw URL); an exit with encoded bodies; no placeholder of this vault at all; a record that is removed, expired ("NAME expired; refreshing") or not a `secret`; a locked vault; no rule (in `/etc/fnox/config.toml`) whose exact domain, header, method and path allow it; an exit the secret's `[exits]` line doesn't list; the same placeholder twice in one header value; a header other than `authorization` on an exit that follows redirects (`manualRedirects` false); CR, LF or NUL after substitution.
339
+ - **Refused before anything is sent:** an `owner` whose `users` isn't `enforce` ("kernel users must be enforce", §3.16.2; this covers kernel programs calling a registered MCP server, and the `/dev/sealed` `test` verdict, too); a placeholder in the URL or in a header name; credentials in the URL; a scheme other than `https:` or a port other than 443 (except §3.16.3's kernel servers, below); an ambiguous path (`.`/`..` segments, `\`, `%2e`, `%2f`, `%5c`, `%25`, checked on the raw URL); an exit with encoded bodies; no placeholder of this vault at all; a record that is removed, expired ("NAME expired; refreshing") or not a `secret`; a locked vault; no rule (in `/etc/fnox/config.toml`) whose exact domain, header, method and path allow it; a paused policy ("policy file missing: paused"); an exit the secret's `[exits]` line doesn't list; the same placeholder twice in one header value; any credential, `secret` or `mcp`, in any header, `authorization` included, on an exit whose raw `manualRedirects` isn't `true` ("NAME needs an exit that doesn't follow redirects"); a value that contains another stored value ("NAME's value contains another stored value"); CR, LF or NUL after substitution. The `/dev/sealed` `test` verdict goes through the same checks, on an exit it takes to be manual.
325
340
  - **Paths** in rules are globs: `*` stays within a segment, `**` crosses segments, `?` is one character.
326
341
  - **Injected:** each placeholder is replaced inside the rule's header value, so `Bearer …` and `token …` templates work. In `Authorization: Basic`, the credential is decoded, the user and password parts substituted (a substituted user may not contain `:`), and re-encoded. A Basic value that holds a placeholder but isn't valid base64 of UTF-8 is refused.
327
- - **Audited:** each injection adds a `forward` audit entry `{ name, domain, method, path, rule, exit, uid, tag }` and sets the record's `usedAt`. Never headers, bodies or values.
342
+ - **Audited:** each injection adds a `forward` audit entry `{ name, domain, method, path, rule, exit, uid, tag }`, all its text, the rule's included, scrubbed over every value before it's stored, and sets the record's `usedAt`. Never headers, bodies or values.
328
343
 
329
344
  ## Sessions
330
345
 
@@ -385,10 +400,10 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
385
400
  - **the agent's sign-ins** (`grant`, source `oauth:mcp`): on any registered `https:` URL of that server, since the agent already held the token;
386
401
  - **kernel bridge tokens** (§3.16.3, with their `url`): only on that URL;
387
402
  - **never** on a kernel loopback URL unless it is a bridge token, and never on a URL under a built-in or page-registered model endpoint.
388
- - **Kernel programs calling a registered MCP server:** a `forward` that carries no `secret` placeholder and whose URL lies under a registered MCP server isn't refused but handled for that server only, a page server winning over an agent one on the same URL. Only `mcp` records bound to that server (by `server`, or listed in its `names`) and held as above are filled in; a `grant` or `model` placeholder is refused. A request that carries a `secret` placeholder is a secret request, whatever the agent registered: the refused set and the rules decide, so an agent's MCP registration can't make a user's rule inert, and a secret never goes over a kernel loopback `http:` URL ("credentials are sent only over https on port 443"). The reply carries `body`, the request body scrubbed over every value, which bios sends instead of its own copy; the response is scrubbed as above; each call is audited as `mcp` with `server`, uid and tag.
403
+ - **Kernel programs calling a registered MCP server:** a `forward` that carries no `secret` placeholder and whose URL lies under a registered MCP server isn't refused but handled for that server only, a page server winning over an agent one on the same URL. Only `mcp` records bound to that server (by `server`, or listed in its `names`) and held as above are filled in; a `grant` or `model` placeholder is refused. A request that carries a `secret` placeholder is a secret request, whatever the agent registered: the refused set and the rules decide, so an agent's MCP registration can't make a user's rule inert, and a secret never goes over a kernel loopback `http:` URL ("credentials are sent only over https on port 443"). The reply is in `mode` `mcp`, and carries `body`, the complete request body scrubbed over every value, which bios sends instead of its own copy; the response is scrubbed as above; each call is audited as `mcp` with `server`, uid and tag.
389
404
  - **`http://<port>.kernel.localhost/<path>`** (port 1–65535, never 9222, no explicit port) is the one `http:` form an MCP server may have (§3.16.3). Everything else must be `https:` on 443.
390
405
 
391
- **Owner changes and requests in flight.** The vault re-reads the endpoints, the records and the scrub version as a last step before it hands a prepared head (agent `model`/`mcp`) or a forwarded one (kernel `forward`) back, so an owner change that has committed by then refuses it. Otherwise, a request prepared before an owner change (the page removing, rotating, unpinning or re-registering something) may complete under the earlier state; the next request sees the change. This applies only to the owner's own changes: what the agent or a kernel program registers or sends never widens a binding, whatever the timing.
406
+ **Owner changes and requests in flight.** The vault re-reads the endpoints, the records and the scrub version as a last step before it hands a prepared head (agent `model`/`mcp`) or a forwarded one (kernel `forward`) back, so an owner change that has committed by then refuses it; a `forward` runs the injector again on what it re-read and writes its audit entry only if that gave the same head and nothing has changed since, in the audit's own transaction. A scrub version that moved at any point since the `forward` began refuses it too, so an MCP body is never scrubbed without a value held when it began. So a rule, exit, policy, record or endpoint change that has committed by then refuses it with `403 the endpoints changed meanwhile; send it again`, and leaves no audit entry and no `usedAt`. Otherwise, a request prepared before an owner change (the page removing, rotating, unpinning or re-registering something) may complete under the earlier state; the next request sees the change. This applies only to the owner's own changes: what the agent or a kernel program registers or sends never widens a binding, whatever the timing.
392
407
 
393
408
  ## The agent's ops
394
409
 
@@ -487,7 +502,7 @@ The button writes its intent to `intents` under a random 128-bit nonce and opens
487
502
 
488
503
  ## Types for bios and spectrum
489
504
 
490
- The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built from `src/contract.ts`: the port API (`Hello`, `Greeting`, `Request`, `Reply`, `VaultEvent`, `Role`), the button (`ButtonConfig`, `ButtonMessage`, `ButtonEvent`), the popup (`PopupIntent`, `PopupState`), the page's results (`SealResult`, `ImportResult`), and the data Settings shows (`SecretView`, `McpView`, `VaultStatus`, `ScrubState`, `PendingRequest`, `AuditEntry`, `ExitPolicyView`, `VaultList`). `Availability` (`connecting`, `waiting`, `locked`, `ready`) is for bios to tell spectrum "waiting for the vault" and "unlock the vault to continue". They mean:
505
+ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built from `src/contract.ts`: the port API (`Hello`, `Greeting`, `Request`, `Reply`, `VaultEvent`, `Role`, and `ResponsePart` and `ResponseAck` for a `forward` port), the button (`ButtonConfig`, `ButtonMessage`, `ButtonEvent`), the popup (`PopupIntent`, `PopupState`), the page's results (`SealResult`, `ImportResult`), and the data Settings shows (`SecretView`, `McpView`, `VaultStatus`, `ScrubState`, `PendingRequest`, `AuditEntry`, `ExitPolicyView`, `VaultList`). `Availability` (`connecting`, `waiting`, `locked`, `ready`) is for bios to tell spectrum "waiting for the vault" and "unlock the vault to continue". They mean:
491
506
 
492
507
  - **`EpochMs`**: milliseconds since the epoch. **`DeviceId`**: the stable id of a linked device's link certificate, the same id bios uses for linked devices and trays, in `sourceDevice`, `device:<id>` exits (and `[exits]` lines) and `ExitPolicyView.devices[].device`; never a display name.
493
508
  - **`Rule`**: `domain` is an exact lower-case host; `header` a lower-case header name; **empty `methods` or `paths` mean any**. A rule with `protocol` (`http1`, `smtp`, `pg`) is a session rule, with its `port` and, for smtp and pg, its `position`; show it as "PROTOCOL on host:port". `anchors`, `pins` and `roots` are its trust anchors (Policy).
@@ -500,6 +515,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
500
515
  - **`AuditEntry`**: `endpoints` is vault-originated: the upgrade's `purged` (show it as "Removed custom model URLs an agent set") and `adopted` ("Kept the MCP servers your tokens use"), a `pinned` MCP token ("pinned to <host>", with an Unpin action) and an `unpinned` one; `forward` is a use of a value in a request, with the exit that carried it; `mcp` a kernel program's call to a registered MCP server; `session` a value handed to the kernel for a verified TLS session (show it as "Sent over PROTOCOL to host", with "trusted by public roots" or "trusted by its pinned anchor" from `verifiedBy`, and nothing when an older entry has no `verifiedBy`); **`materialize` is shown as "Revealed to a program"**; `seal`, `remove`, `restore` and `policy` (with its `changes`) record changes. Every entry has a `seq` for paging.
501
516
  - **`SessionTarget`**: a `sessionMaterialize` request's protocol, position and what goes with it: `placeholders` (and `head` for http1) for every position but `scram-sha-256`, which takes `scram` (a `ScramExchange`) and no `placeholders`.
502
517
  - **`VaultList.mcp`** lists usable `mcp` records for bios's jerk `status`, without a placeholder or source (`list`); treat every entry as usable, whatever its `state`.
518
+ - **`ErrorCause`**: the machine-readable `cause` on `VaultError`, on `ForwardResult`'s `refused` and on `TestVerdict` (the `/dev/sealed` `test` reply). Its one value is `policy-paused`.
503
519
  - **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
504
520
  - **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
505
521
  - **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
@@ -21,9 +21,11 @@ export interface Stamp {
21
21
  users?: string;
22
22
  groups?: number[];
23
23
  }
24
+ export type ErrorCause = 'policy-paused';
24
25
  export interface VaultError {
25
26
  code: ErrorCode;
26
27
  message: string;
28
+ cause?: ErrorCause;
27
29
  }
28
30
  export type SessionProtocol = 'http1' | 'smtp' | 'pg';
29
31
  export type SessionPosition = 'auth-plain' | 'auth-login' | 'password-cleartext' | 'scram-sha-256';
@@ -118,9 +120,13 @@ export interface ScrubState {
118
120
  version: number;
119
121
  ready: boolean;
120
122
  }
123
+ export interface Capabilities {
124
+ forward: 1;
125
+ }
121
126
  export interface VaultStatus {
122
127
  version: string;
123
128
  protocol: Protocol;
129
+ capabilities: Capabilities;
124
130
  origin: string;
125
131
  locked: boolean;
126
132
  lock: LockMode;
@@ -220,7 +226,8 @@ export interface PolicyEntry {
220
226
  uid: null;
221
227
  tag: null;
222
228
  file: PolicyFile;
223
- sha256: string;
229
+ sha256: string | null;
230
+ absent?: true;
224
231
  accepted: boolean;
225
232
  changes: PolicyChange[];
226
233
  warning?: string;
@@ -266,14 +273,27 @@ export interface RawTraits {
266
273
  encodedBodies?: boolean;
267
274
  }
268
275
  export type ForwardResult = {
276
+ mode: 'secret';
277
+ head: RequestHead;
278
+ } | {
279
+ mode: 'mcp';
269
280
  head: RequestHead;
281
+ bodyComplete: true;
270
282
  body?: Uint8Array;
283
+ } | {
284
+ needsBody: true;
271
285
  } | {
272
286
  refused: {
273
287
  status: number;
274
288
  reason: string;
289
+ cause?: ErrorCause;
275
290
  };
276
291
  };
292
+ export interface TestVerdict {
293
+ verdict: 'inject' | 'refuse';
294
+ reason: string;
295
+ cause?: ErrorCause;
296
+ }
277
297
  export type ResponsePart = {
278
298
  op: 'response';
279
299
  status: number;
@@ -290,6 +310,10 @@ export type ResponsePart = {
290
310
  op: 'error';
291
311
  message: string;
292
312
  };
313
+ export type ResponseAck = ResponsePart | {
314
+ op: 'credit';
315
+ bytes: number;
316
+ };
293
317
  export interface McpServerEntry {
294
318
  server: string;
295
319
  url: string;
@@ -357,11 +381,25 @@ export interface Attestation {
357
381
  mode: number;
358
382
  }[];
359
383
  }
384
+ export interface MissingPolicyFile {
385
+ path: string;
386
+ absent: true;
387
+ users: string;
388
+ onRootFs: boolean;
389
+ parents: {
390
+ path: string;
391
+ uid: number;
392
+ gid: number;
393
+ mode: number;
394
+ }[];
395
+ }
360
396
  export interface PolicyFileState {
361
397
  path: string;
362
398
  sha256: string | null;
363
399
  accepted: boolean;
364
400
  warnings: string[];
401
+ absent?: true;
402
+ paused?: true;
365
403
  }
366
404
  export interface PolicyResult {
367
405
  accepted: boolean;
@@ -512,7 +550,7 @@ export type Op = {
512
550
  op: 'dismissBackupReminder';
513
551
  } | {
514
552
  op: 'policy';
515
- files: Attestation[];
553
+ files: (Attestation | MissingPolicyFile)[];
516
554
  } | {
517
555
  op: 'endpoints';
518
556
  model?: (string | {
@@ -534,10 +572,12 @@ export type Op = {
534
572
  } | {
535
573
  op: 'forward';
536
574
  head: RequestHead;
575
+ bodyPresent: boolean;
537
576
  body?: Uint8Array | string;
538
577
  owner: Stamp;
539
578
  exit: ExitName;
540
579
  rawTraits: RawTraits;
580
+ streamCredit?: boolean;
541
581
  } | {
542
582
  op: 'materializeConsume';
543
583
  ticket: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.18.2",
3
+ "version": "1.20.0",
4
4
  "description": "The separate-origin vault at vault.sliccy.ai: sealed secrets, OAuth tokens, and model and MCP credentials for SLICC seven, used only by its injector",
5
5
  "piAi": "1.4.0",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@ai-ecoverse/slicc-cdp": "1.43.1",
55
- "@ai-ecoverse/slicc-kernel": "1.170.3",
55
+ "@ai-ecoverse/slicc-kernel": "1.170.4",
56
56
  "@ai-ecoverse/slicc-shared-web": "2.13.7",
57
57
  "@ai-ecoverse/wasm-bash": "5.3.0-23",
58
58
  "@ai-ecoverse/wasm-coreutils": "9.12.0-15",