@ai-ecoverse/slicc-vault 1.9.2 → 1.10.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The separate-origin vault at `vault.sliccy.ai`. It holds the sealed secrets, OAuth tokens, and model and MCP credentials of SLICC seven, so that no value ever reaches the model provider. The design is `secrets-tokens-design.md` (revision 4) in slicc-agent-plan. This README is the spec of what is built.
4
4
 
5
- - **Built:** the key store, records, origin namespacing, auto-unlock with an optional passkey or passphrase lock, the embedding API with its `page` and `agent` roles, the page's `put`, `seal`, `remove`, `restore`, `cancel` and `lock`, the scrub state, the lock button and its popup, the attested policy (rules, `[exec]`, `[exits]`, `[limits]`), the injector and `forward` with the response scrub (S1), the agent's `redact`, `redactStream`, `endpoints`, `model` and `mcp` (S3, S4), `records`, `grant` and `refresh`, the model and MCP refusal, exec tickets (`materialize` and `materializeConsume`), `import` (backups, KeePass, six's `secrets.env`) and `export`, sign-in tokens with their grants and refresh through `egress`, and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
5
+ - **Built:** the key store, records, origin namespacing, auto-unlock with an optional passkey or passphrase lock, the embedding API with its `page` and `agent` roles, the page's `put`, `seal`, `remove`, `restore`, `cancel` and `lock`, the scrub state, the lock button and its popup, the attested policy (rules, `[exec]`, `[exits]`, `[limits]`), the injector and `forward` with the response scrub (S1), the agent's `redact`, `redactStream`, `endpoints`, `model` and `mcp` (S3, S4), `records`, `grant` and `refresh`, the model and MCP refusal, exec tickets (`materialize` and `materializeConsume`), `sessionMaterialize` for kernel-verified TLS sessions, `import` (backups, KeePass, six's `secrets.env`) and `export`, sign-in tokens with their grants and refresh through `egress`, and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
6
6
  - **The vault has no network stack.** It never fetches anything but its own `config.json` and `embedders.json`: bios sends every request the vault prepares.
7
7
 
8
8
  ## Documents and hosting
@@ -102,13 +102,17 @@ The vault trusts a file only when:
102
102
  - `onRootFs` is true, and `users` is `enforce`;
103
103
  - `bytes` is UTF-8 and parses.
104
104
 
105
- 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 }], 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.
105
+ 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.
106
106
 
107
107
  **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.
108
108
 
109
109
  **`/etc/fnox/config.toml`**, the injection policy, read the way fnox reads it:
110
110
  - `[secrets]`: entries whose `provider` (or the file's `default_provider`) is `seven`, and whose `value` is a placeholder.
111
- - `[[proxy.rules]]`: `secret` (a seven entry of `[secrets]`), `domain` (one exact host, lower-cased), `header` (lower-cased, default `authorization`), `methods` (upper-cased; empty means any) and `paths` (each starting with `/`; empty means any). A rule that breaks any of this is dropped with a warning; the rest of the file still counts.
111
+ - `[[proxy.rules]]`: `secret` (a seven entry of `[secrets]`), `domain` (one exact host, lower-cased), `header` (lower-cased, default `authorization`), `methods` (upper-cased; empty means any) and `paths` (each starting with `/`; empty means any). A rule that breaks any of this is dropped with a warning; the rest of the file still counts. Rules are numbered from 1 in file order, dropped ones included; a `session` audit entry names its rule by that number.
112
+ - **Session rules** (§3.19) add `protocol` and `port` (an integer from 1 to 65535), and are used only by `sessionMaterialize`, never by `forward`; a rule without `protocol` is used only by `forward`:
113
+ - `protocol = "http1"`: `header`, `methods` and `paths` as above; the position is the header. A `position` is a warning.
114
+ - `protocol = "smtp"`, with `position = "auth-plain"` or `"auth-login"`, or `protocol = "pg"`, with `position = "password-cleartext"`: no `header`, `methods` or `paths` (a warning otherwise). Its `header` reads `''`.
115
+ - `port` or `position` without `protocol`, another `protocol`, or a missing or wrong `port` or `position` is a warning, and the rule is dropped.
112
116
  - A record of kind `secret` is `active` when the file's `[secrets]` entry of its name holds the record's own placeholder and at least one rule names it. A placeholder from another vault or an old record doesn't activate anything.
113
117
  - Nothing else in the file counts: no imports, profiles or other providers.
114
118
 
@@ -121,7 +125,7 @@ GITHUB_TOKEN = { argv = [["/usr/bin/gh", "release", "upload"]] }
121
125
  OPENAI_API_KEY = { shell = true }
122
126
 
123
127
  [exits]
124
- NPM_TOKEN = ["page", "device:lars-air"]
128
+ NPM_TOKEN = ["page", "device:7d1e4b0a9c2f5e83"]
125
129
 
126
130
  [limits]
127
131
  materialize_per_minute = 10
@@ -130,7 +134,7 @@ shell_per_day = 5000
130
134
  ```
131
135
 
132
136
  - `[exec]`: `false` is an explicit off, `true` is `exec: 'any'`, exactly `{ argv = [[…]] }` is `'argv'` (each prefix an absolute program path), and exactly `{ shell = true }` is `'shell'`. Anything else, a table with other or extra keys included, is a warning, and `exec` stays `false`. VAULT-1d acts on them.
133
- - `[exits]` is **optional**: a secret without a line may use every exit; a line restricts it to the exits listed (`page`, `proxy`, `tailscale`, `device:<name>`). Unknown names are dropped from the line with a warning, which leaves the line narrower, never wider.
137
+ - `[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, never its display name, which can be renamed or shared by two devices; 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.
134
138
  - `[limits]` overrides the defaults above with positive integers.
135
139
 
136
140
  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.
@@ -160,7 +164,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
160
164
  - On `port2` it answers `{ v: 1, op: 'welcome', role, status }`, or `{ v: 1, op: 'refused', error }` for another protocol version or an unknown role.
161
165
  - 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).
162
166
  - **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).
163
- - **Values:** only `forward` (inside the head it returns, where an accepted rule allows it), `materializeConsume` (for an `[exec]` grant) 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.
167
+ - **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.
164
168
 
165
169
  | op (page role) | fields | reply |
166
170
  |---|---|---|
@@ -180,6 +184,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
180
184
  | `policy` | `files` | the attested policy files, below (Policy) |
181
185
  | `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
182
186
  | `materializeConsume` | `ticket`, `stamp`, `path`, `resolved`, `argv`, `cwd`, `interpreters`, `trusted` | the values for the child's environment, below (Exec) |
187
+ | `sessionMaterialize` | `stamp`, `session`, `host`, `port`, `protocol`, `position`, `verified`, `handshakeAt`, `exit`, `placeholders`, `head?` | `{ values }` for a kernel-verified TLS session, below (Sessions) |
183
188
  | `import` | `kind`, `bytes`, `password?`, `pick?` | below (Import and export) |
184
189
  | `export` | `passphrase` | `{ bundle, count }`, below |
185
190
  | anything else | | `ENOTSUP` |
@@ -299,7 +304,7 @@ const channel = new MessageChannel();
299
304
  port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }, [channel.port2]);
300
305
  ```
301
306
 
302
- - `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:<name>`); `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.
307
+ - `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.
303
308
  - **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`.
304
309
  - **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.
305
310
 
@@ -310,12 +315,41 @@ port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }
310
315
  - **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.
311
316
  - **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.
312
317
 
318
+ ## Sessions
319
+
320
+ **`sessionMaterialize`** (§3.19, a page op that bios relays unchanged from the kernel) hands the kernel's TLS writer the secrets for one kernel-terminated TLS session (A2), for protocols `forward` can't carry: an http1 header on a kernel-made connection, SMTP AUTH, or pg's cleartext password. The kernel asks only after its own handshake verified the host (SNI, chain and hostname, whatever the caller's flags), never on a plaintext leg; it substitutes only at the position, uses each value once within 60 s, keeps it out of every process, fd, file, log and telemetry, and scrubs the session's read side (S2) for the values and their encodings. The vault picks the rules, so the kernel never parses `/etc/fnox/config.toml`.
321
+
322
+ ```js
323
+ port.postMessage({ v: 1, id, op: 'sessionMaterialize', stamp, session, host, port: 587, protocol: 'smtp', position: 'auth-plain', verified: true, handshakeAt, exit, placeholders });
324
+ ```
325
+
326
+ The fields are the kernel's session attestation:
327
+ - `stamp`: the kernel's stamp of the process that owns the socket; `users` must be `enforce` (else `EACCES` "kernel users must be enforce").
328
+ - `session`: the kernel's session id, 16 to 128 characters of `A-Z`, `a-z`, `0-9`, `_` and `-`.
329
+ - `host`: the name the kernel verified the leaf against, compared lower-cased without one trailing dot; `port`; `protocol` (`http1`, `smtp` or `pg`).
330
+ - `position`: `header:<name>` (lower-cased) for http1, else `auth-plain`, `auth-login` or `password-cleartext`.
331
+ - `verified`: the kernel's own verdict on the chain and the name; `handshakeAt`: when the handshake finished (epoch ms).
332
+ - `exit`: the exit the session leaves by (`page`, `proxy`, `tailscale` or `device:<id>`, with the device's `DeviceId`).
333
+ - `placeholders`: 1 to 16 placeholders, every one found at the position, in one request. `head` (http1 only, required there): `{ method, path }`, the request line's method and target.
334
+ - A malformed field is `EINVAL`, and so is a `protocol` other than these or a `position` that isn't one of its protocol's; neither burns the session.
335
+
336
+ Once the fields check out, the vault burns the session in IndexedDB, in one transaction, so no vault frame of the namespace, now or after a reload, answers it again until 60 s after the later of the request and `handshakeAt`. Then it refuses the whole request with `EACCES` "session refused: …" when:
337
+ - the session already had a request, answered or refused: "this session already had its request";
338
+ - `verified` isn't true: "the kernel didn't verify the session's host";
339
+ - `handshakeAt` is in the future or more than 60 s ago: "the session is past its 60 s deadline";
340
+ - the vault is locked (`EACCES` "vault is locked: …"), at the start or by the point of release (below);
341
+ - for http1: an ambiguous path (as in Forward), or a URL under a model or MCP endpoint (Model and MCP endpoints).
342
+
343
+ Each placeholder then gets its value only when it is a `secret` record's, holding the placeholder `[secrets]` names, neither removed nor expired, with a session rule of that secret whose `protocol`, `domain`, `port` and position are the session's (and, for http1, whose `methods` and `paths` allow `head`), and whose `[exits]` line, if any, lists `exit`. A session can carry several secrets this way, each by its own rule. Just before answering, the records, the accepted policy and the registered endpoints are read again, at a fresh clock, in the transaction that audits the values. A value whose record was removed, expired or rotated meanwhile, or whose rule or `[exits]` line no longer allows it, is withheld. A session past its deadline by then, or an http1 URL now under a registered endpoint, refuses the whole request. That transaction is the point of release: if the vault locked before it, it writes nothing and refuses (`vault is locked`).
344
+
345
+ It answers `{ values: { [placeholder]: value } }` for those, and leaves the others out, so the kernel sends them as written. When none gets a value, it's `EACCES` with the first placeholder's reason: "NAME isn't a secret", "a placeholder of no record of this vault", "NAME's placeholder isn't the one /etc/fnox/config.toml names", "NAME was removed", "NAME expired", "no session rule in /etc/fnox/config.toml lets NAME be used", "no session rule lets NAME at PROTOCOL POSITION on HOST:PORT" (with " for METHOD PATH" for http1), "NAME isn't allowed on EXIT", or "the secret or its rule changed meanwhile". Each value handed out adds a `session` audit entry `{ name, session, domain, port, protocol, position, rule, exit, uid, tag }`, `rule` being the number of the rule that allowed it, and sets the record's `usedAt`. It never includes the value. Stored values never hold CR, LF or NUL (Records), so a value can't break out of its position.
346
+
313
347
  ## Model and MCP endpoints
314
348
 
315
349
  The vault never injects a secret into a model or MCP endpoint, whatever a rule says.
316
350
  - **The built-in list** is generated at build time (`scripts/endpoints.mjs`, into the gitignored `src/endpoints.json`) from every provider base URL in pi-ai's catalog (`@earendil-works/pi-ai`, pinned in `devDependencies`), plus `bedrock-runtime.*` and `bedrock-runtime-fips.*.amazonaws.com`, Azure OpenAI and AI services, and Adobe's LLM proxy. Each entry is a host (with `*` for one label) and a path prefix (`*` for one segment, from templated segments).
317
351
  - **Registered endpoints:** the agent's `endpoints` op adds model base URLs and MCP servers for its namespace. They only ever add.
318
- - **At policy parse:** a rule that can reach only refused URLs (its domain matches, and the endpoint has no path prefix or every path of the rule lies under it) is dropped and listed in `list.refused` with the reason `NAME: host is a model or MCP endpoint; the vault never injects there`. Other rules on the same host stay.
352
+ - **At policy parse:** a rule that can reach only refused URLs (its domain matches, and the endpoint has no path prefix or every path of the rule lies under it), and an `smtp` or `pg` session rule whose domain matches any endpoint's host, is dropped and listed in `list.refused` with the reason `NAME: host is a model or MCP endpoint; the vault never injects there`. Other rules on the same host stay.
319
353
  - **At request time:** `forward` refuses any URL under the refused set, before rules: `403 host is a model or MCP endpoint; the vault never injects there`.
320
354
  - **Kernel programs calling a registered MCP server (§3.16.1):** a `forward` whose URL lies under a registered MCP server isn't refused but handled for that server only. Only `mcp` records the server lists in `names` are filled in; a `secret`, `grant` or `model` placeholder is refused ("secrets are never sent to model or MCP endpoints"). 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.
321
355
  - **`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.
@@ -324,9 +358,9 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
324
358
 
325
359
  | op | fields | reply |
326
360
  |---|---|---|
327
- | `redact` | `texts`: strings and `Uint8Array`s | `{ texts, hits, version }`: each text scrubbed, in the same order and type; `hits` totals `{ name, form, count }` |
361
+ | `redact` | `texts`: strings and bytes | `{ texts, hits, version }`: each text scrubbed, in the same order, a string for a string and a `Uint8Array` for bytes; `hits` totals `{ name, form, count }` |
328
362
  | `redactStream` | `stream` (an id) | `{ stream, idleMs, version }`; at most 64 streams per port (`EAGAIN`) |
329
- | `push` | `stream`, `chunk` (bytes) | `{ chunk }`: what can be released, scrubbed |
363
+ | `push` | `stream`, `chunk` (bytes) | `{ chunk }`: what can be released, scrubbed, as a `Uint8Array` |
330
364
  | `idle` | `stream` | `{ chunk }`: a held tail under 16 bytes, after `idleMs` without input |
331
365
  | `end` | `stream` | `{ chunk, hits }`: the rest, and the stream is gone |
332
366
  | `drop` | `stream` | `{}` |
@@ -352,6 +386,8 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
352
386
  - The access token is sealed as an `mcp` record bound to `server`, named `name` or `<SERVER>_ACCESS_TOKEN` (upper-cased, other characters as `_`, `MCP_` in front of a leading digit), with source `oauth:mcp` and `expiresAt` from `expires_in`. A `refresh_token` is sealed as a `grant` named `<name>_GRANT` with `oauth` and the same `server`, and becomes the token's refresh source.
353
387
  - Both records are written in one transaction, so `grant` stores all or nothing. The reply's `response` is JSON, a string or bytes like the input, with only `access_token` and `refresh_token` (as their placeholders), any string `token_type`, a numeric `expires_in` that isn't a stored value, and string `scope` scrubbed over every value; every other field, `id_token` included, is dropped. A `grant` whose access-token name is already bound to another server is `EEXIST`, checked in the same transaction.
354
388
 
389
+ **Bytes** are any `ArrayBufferView` (`Uint8Array`, `Int8Array`, `DataView`, a `Float32Array`, a Node `Buffer`), read as exactly its own bytes from `byteOffset` for `byteLength`, never the rest of its buffer. Anything else, a bare `ArrayBuffer` included, is `EINVAL` ("chunk must be bytes", or "texts must be a list of strings and bytes").
390
+
355
391
  A stream is compiled against the namespace's values when it opens. If a value is added, rotated or removed while it's open, its next `push`, `idle` or `end` answers `EAGAIN` and the stream is gone: open it again and push the output from its start.
356
392
 
357
393
  `redact` and `redactStream` need a compiled scrubber: with the lock on, before the first unlock since the frame loaded, they answer `EACCES` ("unlock the vault to continue"). They never return a value: matches are replaced, everything else passes through.
@@ -408,14 +444,14 @@ The button writes its intent to `intents` under a random 128-bit nonce and opens
408
444
 
409
445
  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`, `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:
410
446
 
411
- - **`EpochMs`**: milliseconds since the epoch. **`DeviceId`**: the same id bios uses for linked devices and trays, in `sourceDevice`, `device:<id>` exits and `ExitPolicyView.devices[].device`.
412
- - **`Rule`**: `domain` is an exact lower-case host; `header` a lower-case header name; **empty `methods` or `paths` mean any**.
447
+ - **`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.
448
+ - **`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".
413
449
  - **`exec`**: `false` means the value is never revealed to a program; `any`, `argv` (with `execArgv`, the allowed argv prefixes) and `shell` are the `[exec]` modes under which `fnox exec` may hand it to a program.
414
450
  - **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
415
451
  - **Bindings in `list`:** read under one scrub version (retried, then `EAGAIN`), `provider`, `server` and `settings` are scrubbed over every value (a setting whose name holds one is dropped), and left out while the vault is locked before its first unlock.
416
452
  - **`kind`**: `secret` (a credential programs use through rules), **`grant`** (a sign-in's refresh token, never injected; show it as "Sign-in"), `model` (a model provider key), `mcp` (an MCP server credential). `provider` and `account` name the service and the signed-in account where one is known; a `model` record always has its `provider`, an `mcp` record may have its `server`, and either may carry non-secret `settings`.
417
- - **Who did it:** `uid` and `tag` are the kernel stamp for entries a program or the agent caused (`/dev/sealed` seals, `forward`, `mcp`, `materialize`). Every entry from a page-role op (`put`, `seal`, `remove`, `restore`, `import`, `policy`) and from the vault itself (`refresh`) has `uid: null, tag: null`, older entries included. Spectrum shows page ops as "by you", `refresh` as "automatically", and `policy` entries with no "by …".
418
- - **`AuditEntry`**: `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; **`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.
453
+ - **Who did it:** `uid` and `tag` are the kernel stamp for entries a program or the agent caused (`/dev/sealed` seals, `forward`, `mcp`, `materialize`, `session`). Every entry from a page-role op (`put`, `seal`, `remove`, `restore`, `import`, `policy`) and from the vault itself (`refresh`) has `uid: null, tag: null`, older entries included. Spectrum shows page ops as "by you", `refresh` as "automatically", and `policy` entries with no "by …".
454
+ - **`AuditEntry`**: `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"); **`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.
419
455
  - **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
420
456
  - **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
421
457
  - **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
@@ -430,7 +466,7 @@ npm test # Chromium integration tests
430
466
  npm start # vault on http://vault.sliccy.localhost:8790, a test embedder on seven.sliccy.localhost
431
467
  ```
432
468
 
433
- - **Integration tests** (`test/integration/`) run the built vault on a local server that sends the production headers, at `http://vault.sliccy.localhost:<port>`, with test embedders on `seven.`, `preview.` and `evil.sliccy.localhost`. Chromium treats `*.sliccy.localhost` as one site and as a secure context, so the embedded frame gets the same first-party storage it gets under `sliccy.ai`. They use slicc-shared-web's CDP harness, plus a second CDP session for popups and the virtual WebAuthn authenticator. `kernel.test.mjs` runs exec tickets end to end against the real kernel (`@ai-ecoverse/slicc-kernel/node` with wasm-bash and wasm-coreutils, test dependencies only): its `/dev/sealed` hook relays to the built vault, so a `materialize`, the spawn with `SLICC_SEALED_TICKET`, the kernel's `materializeConsume`, the value swap and S2's scrubbing of the child's output all run as in seven, and with users `off` nothing is revealed.
469
+ - **Integration tests** (`test/integration/`) run the built vault on a local server that sends the production headers, at `http://vault.sliccy.localhost:<port>`, with test embedders on `seven.`, `preview.` and `evil.sliccy.localhost`. Chromium treats `*.sliccy.localhost` as one site and as a secure context, so the embedded frame gets the same first-party storage it gets under `sliccy.ai`. They use slicc-shared-web's CDP harness, plus a second CDP session for popups and the virtual WebAuthn authenticator. They collect no browser coverage (nothing reads it, and with `coverage: []` the harness attaches nothing); `SLICC_COVERAGE=1 npm test` turns it on locally. The unit tests' diff-coverage gate is separate. `kernel.test.mjs` runs exec tickets end to end against the real kernel (`@ai-ecoverse/slicc-kernel/node` with wasm-bash and wasm-coreutils, test dependencies only): its `/dev/sealed` hook relays to the built vault, so a `materialize`, the spawn with `SLICC_SEALED_TICKET`, the kernel's `materializeConsume`, the value swap and S2's scrubbing of the child's output all run as in seven, and with users `off` nothing is revealed.
434
470
  - **Unit tests** stay in the gitignored `test/unit/` (`npm run test:unit`), and the pre-commit hook requires them to cover every changed line of `src/`.
435
471
  - The Biome, TypeScript, lefthook and Renovate configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web). There are no comments in any file.
436
472
 
@@ -25,11 +25,16 @@ export interface VaultError {
25
25
  code: ErrorCode;
26
26
  message: string;
27
27
  }
28
+ export type SessionProtocol = 'http1' | 'smtp' | 'pg';
29
+ export type SessionPosition = 'auth-plain' | 'auth-login' | 'password-cleartext';
28
30
  export interface Rule {
29
31
  domain: string;
30
32
  header: string;
31
33
  methods: string[];
32
34
  paths: string[];
35
+ protocol?: SessionProtocol;
36
+ position?: SessionPosition;
37
+ port?: number;
33
38
  }
34
39
  export interface RefusedRule {
35
40
  secret: string;
@@ -125,6 +130,16 @@ export interface ForwardEntry extends AuditBase {
125
130
  rule: Rule;
126
131
  exit: ExitName;
127
132
  }
133
+ export interface SessionEntry extends AuditBase {
134
+ type: 'session';
135
+ session: string;
136
+ domain: string;
137
+ port: number;
138
+ protocol: SessionProtocol;
139
+ position: string;
140
+ rule: number;
141
+ exit: ExitName;
142
+ }
128
143
  export interface McpEntry extends AuditBase {
129
144
  type: 'mcp';
130
145
  server: string;
@@ -167,7 +182,7 @@ export interface PolicyEntry {
167
182
  changes: PolicyChange[];
168
183
  warning?: string;
169
184
  }
170
- export type AuditEntry = ForwardEntry | McpEntry | RevealEntry | SealEntry | RemoveEntry | RefreshEntry | PolicyEntry;
185
+ export type AuditEntry = ForwardEntry | SessionEntry | McpEntry | RevealEntry | SealEntry | RemoveEntry | RefreshEntry | PolicyEntry;
171
186
  export interface RefreshEntry extends AuditBase {
172
187
  type: 'refresh';
173
188
  grant: string;
@@ -434,6 +449,22 @@ export type Op = {
434
449
  cwd: string;
435
450
  interpreters: string[];
436
451
  trusted: boolean;
452
+ } | {
453
+ op: 'sessionMaterialize';
454
+ stamp: Stamp;
455
+ session: string;
456
+ host: string;
457
+ port: number;
458
+ protocol: SessionProtocol;
459
+ position: string;
460
+ verified: boolean;
461
+ handshakeAt: EpochMs;
462
+ exit: ExitName;
463
+ placeholders: string[];
464
+ head?: {
465
+ method: string;
466
+ path: string;
467
+ };
437
468
  };
438
469
  export type AgentOp = {
439
470
  op: 'redact';
@@ -573,6 +604,9 @@ export type OpReply = {
573
604
  op: 'materializeConsume';
574
605
  values: Record<string, string>;
575
606
  placeholders: Record<string, string>;
607
+ } | {
608
+ op: 'sessionMaterialize';
609
+ values: Record<string, string>;
576
610
  };
577
611
  export type Request = Op & {
578
612
  v: Protocol;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
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
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -46,12 +46,12 @@
46
46
  "release": "semantic-release"
47
47
  },
48
48
  "devDependencies": {
49
- "@ai-ecoverse/jerk-oauth": "0.24.0",
50
- "@ai-ecoverse/slicc-kernel": "1.122.1",
49
+ "@ai-ecoverse/jerk-oauth": "1.0.0",
50
+ "@ai-ecoverse/slicc-kernel": "1.125.9",
51
51
  "@ai-ecoverse/slicc-scrub": "1.0.0",
52
- "@ai-ecoverse/slicc-shared-web": "1.23.1",
53
- "@ai-ecoverse/wasm-bash": "5.3.0-19",
54
- "@ai-ecoverse/wasm-coreutils": "9.12.0-9",
52
+ "@ai-ecoverse/slicc-shared-web": "1.24.2",
53
+ "@ai-ecoverse/wasm-bash": "5.3.0-20",
54
+ "@ai-ecoverse/wasm-coreutils": "9.12.0-10",
55
55
  "@biomejs/biome": "2.5.15",
56
56
  "@earendil-works/pi-ai": "1.1.0",
57
57
  "@happy-dom/global-registrator": "20.14.6",