@ai-ecoverse/slicc-vault 1.18.1 → 1.19.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`), 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
 
@@ -174,7 +178,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
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) |
@@ -214,7 +218,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
214
218
  | op | fields | reply |
215
219
  |---|---|---|
216
220
  | `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) |
221
+ | `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
222
  | `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
219
223
  | `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 |
220
224
  | `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
@@ -230,6 +234,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
230
234
  - 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
235
  - **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
236
  - **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.
237
+ - `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
238
  - `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
239
  - `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
240
  - 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 +309,7 @@ The vault burns the ticket, then requires that:
304
309
  - `{ shell = true }`: any program and chain, the explicit exception;
305
310
  - no real record has expired meanwhile.
306
311
 
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).
312
+ 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
313
 
309
314
  ## Forward
310
315
 
@@ -312,19 +317,27 @@ bios's exit router asks the vault to inject a request the kernel forwarded (an o
312
317
 
313
318
  ```js
314
319
  const channel = new MessageChannel();
315
- port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }, [channel.port2]);
320
+ port.postMessage({ v: 1, id, op: 'forward', head, bodyPresent, owner, exit, rawTraits, streamCredit }, [channel.port2]);
316
321
  ```
317
322
 
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`.
323
+ - `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.
324
+ - **The reply** says which `mode` the vault chose; bios never guesses it:
325
+ - `{ mode: 'secret', head }`: the same head with the values filled in. It has no body, whatever bios sent.
326
+ - `{ 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.
327
+ - `{ 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`.
328
+ - `{ refused: { status, reason } }`: `403` with a one-line reason, `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.
329
+ - Malformed fields, and an MCP body above 32 MiB, are `EINVAL`.
330
+ - **The port:** on every reply but a `mode` one, the vault closes the transferred port.
320
331
  - **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.
332
+ - **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.
333
+ - **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
334
 
322
335
  **The injector**, fnox's proxy semantics:
323
336
  - **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.
337
+ - **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
338
  - **Paths** in rules are globs: `*` stays within a segment, `**` crosses segments, `?` is one character.
326
339
  - **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.
340
+ - **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
341
 
329
342
  ## Sessions
330
343
 
@@ -385,10 +398,10 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
385
398
  - **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
399
  - **kernel bridge tokens** (§3.16.3, with their `url`): only on that URL;
387
400
  - **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.
401
+ - **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
402
  - **`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
403
 
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.
404
+ **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
405
 
393
406
  ## The agent's ops
394
407
 
@@ -487,7 +500,7 @@ The button writes its intent to `intents` under a random 128-bit nonce and opens
487
500
 
488
501
  ## Types for bios and spectrum
489
502
 
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:
503
+ 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
504
 
492
505
  - **`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
506
  - **`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).
@@ -118,9 +118,13 @@ export interface ScrubState {
118
118
  version: number;
119
119
  ready: boolean;
120
120
  }
121
+ export interface Capabilities {
122
+ forward: 1;
123
+ }
121
124
  export interface VaultStatus {
122
125
  version: string;
123
126
  protocol: Protocol;
127
+ capabilities: Capabilities;
124
128
  origin: string;
125
129
  locked: boolean;
126
130
  lock: LockMode;
@@ -220,7 +224,8 @@ export interface PolicyEntry {
220
224
  uid: null;
221
225
  tag: null;
222
226
  file: PolicyFile;
223
- sha256: string;
227
+ sha256: string | null;
228
+ absent?: true;
224
229
  accepted: boolean;
225
230
  changes: PolicyChange[];
226
231
  warning?: string;
@@ -266,8 +271,15 @@ export interface RawTraits {
266
271
  encodedBodies?: boolean;
267
272
  }
268
273
  export type ForwardResult = {
274
+ mode: 'secret';
275
+ head: RequestHead;
276
+ } | {
277
+ mode: 'mcp';
269
278
  head: RequestHead;
279
+ bodyComplete: true;
270
280
  body?: Uint8Array;
281
+ } | {
282
+ needsBody: true;
271
283
  } | {
272
284
  refused: {
273
285
  status: number;
@@ -290,6 +302,10 @@ export type ResponsePart = {
290
302
  op: 'error';
291
303
  message: string;
292
304
  };
305
+ export type ResponseAck = ResponsePart | {
306
+ op: 'credit';
307
+ bytes: number;
308
+ };
293
309
  export interface McpServerEntry {
294
310
  server: string;
295
311
  url: string;
@@ -357,11 +373,25 @@ export interface Attestation {
357
373
  mode: number;
358
374
  }[];
359
375
  }
376
+ export interface MissingPolicyFile {
377
+ path: string;
378
+ absent: true;
379
+ users: string;
380
+ onRootFs: boolean;
381
+ parents: {
382
+ path: string;
383
+ uid: number;
384
+ gid: number;
385
+ mode: number;
386
+ }[];
387
+ }
360
388
  export interface PolicyFileState {
361
389
  path: string;
362
390
  sha256: string | null;
363
391
  accepted: boolean;
364
392
  warnings: string[];
393
+ absent?: true;
394
+ paused?: true;
365
395
  }
366
396
  export interface PolicyResult {
367
397
  accepted: boolean;
@@ -512,7 +542,7 @@ export type Op = {
512
542
  op: 'dismissBackupReminder';
513
543
  } | {
514
544
  op: 'policy';
515
- files: Attestation[];
545
+ files: (Attestation | MissingPolicyFile)[];
516
546
  } | {
517
547
  op: 'endpoints';
518
548
  model?: (string | {
@@ -534,10 +564,12 @@ export type Op = {
534
564
  } | {
535
565
  op: 'forward';
536
566
  head: RequestHead;
567
+ bodyPresent: boolean;
537
568
  body?: Uint8Array | string;
538
569
  owner: Stamp;
539
570
  exit: ExitName;
540
571
  rawTraits: RawTraits;
572
+ streamCredit?: boolean;
541
573
  } | {
542
574
  op: 'materializeConsume';
543
575
  ticket: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.18.1",
3
+ "version": "1.19.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",
@@ -47,11 +47,11 @@
47
47
  "release": "semantic-release"
48
48
  },
49
49
  "dependencies": {
50
- "@ai-ecoverse/jerk-oauth": "1.3.8",
50
+ "@ai-ecoverse/jerk-oauth": "1.3.9",
51
51
  "@ai-ecoverse/slicc-scrub": "1.1.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@ai-ecoverse/slicc-cdp": "1.43.0",
54
+ "@ai-ecoverse/slicc-cdp": "1.43.1",
55
55
  "@ai-ecoverse/slicc-kernel": "1.170.3",
56
56
  "@ai-ecoverse/slicc-shared-web": "2.13.7",
57
57
  "@ai-ecoverse/wasm-bash": "5.3.0-23",