@ai-ecoverse/slicc-vault 1.1.0 → 1.3.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
@@ -2,8 +2,9 @@
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, and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
6
- - **Not built yet:** policy (every record is `inert`, and `test` always refuses), `forward` and the scrubber, exec, `import` and `export`, and the agent's ops. They answer as described below.
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` and `endpoints` (S3), the model and MCP refusal, and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
6
+ - **Not built yet:** exec (`materialize`), `import` and `export`, and the agent's `model`, `mcp`, `records`, `grant` and `refresh`. They answer as described below.
7
+ - **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
8
 
8
9
  ## Documents and hosting
9
10
 
@@ -64,7 +65,7 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
64
65
  - **A value** is at least 8 bytes of UTF-8, without CR, LF or NUL, and contains no placeholder. Anything shorter is refused with `EINVAL` "values shorter than 8 bytes can't be protected from the model", because the scrubber couldn't tell it apart from ordinary output.
65
66
  - **`placeholder`** is `fnox_proxy_` plus 24 lower-case hex characters from `crypto.getRandomValues`, unique across the whole vault and stable for the record's life. All placeholders have the same length, so none contains another.
66
67
  - **`source`** is `settings` (entered in spectrum), `process`, `jerk:<provider>@<device>`, `oauth:<provider>` or `import:<kind>`.
67
- - **`state`** is computed, never stored: `removed` when `removedAt` is set, `active` when the accepted policy has a rule naming the record, otherwise `inert`. With no policy yet, every record is `inert`.
68
+ - **`state`** is computed, never stored: `removed` when `removedAt` is set, `active` when the accepted policy has a rule for the record (below), otherwise `inert`.
68
69
  - **Removing** sets `removedAt`. A removed record keeps its placeholder, can be restored with `restore`, and is deleted for good 30 days later. Sealing a removed name brings it back with a new value and the same placeholder.
69
70
  - **Times** are epoch milliseconds everywhere.
70
71
 
@@ -72,12 +73,74 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
72
73
 
73
74
  `status.exportedAt` is the time of the namespace's last successful `export`, or `null` if it never exported. `status.backupReminder` is `due` once the namespace holds a `secret` record and has never exported, `dismissed` after `dismissBackupReminder`, and `null` otherwise (no secrets yet, or exported). `export` arrives with VAULT-1e; until then `exportedAt` stays `null`.
74
75
 
76
+ ## Policy
77
+
78
+ The rules come only from two root-owned kernel files. bios reads them with the kernel's page-only `kernel.attest(path)` (KS-7), at boot and whenever either file changes, and passes the results through unchanged:
79
+
80
+ ```js
81
+ port.postMessage({ v: 1, id, op: 'policy', files: [await kernel.attest('/etc/fnox/config.toml'), await kernel.attest('/etc/slicc/secrets-policy.toml')] });
82
+ ```
83
+
84
+ **An attestation** is exactly `{ path, bytes, uid, gid, mode, ino, mtimeMs, sha256, users, onRootFs, parents }`, nothing more or less:
85
+
86
+ | field | format |
87
+ |---|---|
88
+ | `path` | the canonical absolute path the kernel read (it refuses symlinks and mount points) |
89
+ | `bytes` | the file's bytes, a `Uint8Array`, read in the same step as the stats |
90
+ | `uid`, `gid`, `mode` | integers; `mode` includes the type bits (`0o100644`) |
91
+ | `ino`, `mtimeMs` | numbers |
92
+ | `sha256` | 64 lower-case hex characters |
93
+ | `users` | the kernel's users mode: `off`, `report` or `enforce` |
94
+ | `onRootFs` | true only on the root file system, under no mount |
95
+ | `parents` | every directory from `/` down to the file's, each exactly `{ path, uid, gid, mode }` |
96
+
97
+ The vault trusts a file only when:
98
+ - `path` is `/etc/fnox/config.toml` or `/etc/slicc/secrets-policy.toml`, and the object has exactly the fields above in those formats;
99
+ - the SHA-256 the vault computes over `bytes` equals `sha256` (the field alone is never trusted);
100
+ - `uid` is 0, and `mode` is a regular file with `mode & 0o022` of 0;
101
+ - `parents` lists exactly the directories above `path`, in order from `/`, each with uid 0, a directory type and `mode & 0o022` of 0;
102
+ - `onRootFs` is true, and `users` is `enforce`;
103
+ - `bytes` is UTF-8 and parses.
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.
106
+
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
+
109
+ **`/etc/fnox/config.toml`**, the injection policy, read the way fnox reads it:
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.
112
+ - 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
+ - Nothing else in the file counts: no imports, profiles or other providers.
114
+
115
+ **`/etc/slicc/secrets-policy.toml`**, read by the vault only:
116
+
117
+ ```toml
118
+ [exec]
119
+ NPM_TOKEN = true
120
+ GITHUB_TOKEN = { argv = [["/usr/bin/gh", "release", "upload"]] }
121
+ OPENAI_API_KEY = { shell = true }
122
+
123
+ [exits]
124
+ NPM_TOKEN = ["page", "device:lars-air"]
125
+
126
+ [limits]
127
+ materialize_per_minute = 10
128
+ materialize_per_day = 200
129
+ shell_per_day = 5000
130
+ ```
131
+
132
+ - `[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.
134
+ - `[limits]` overrides the defaults above with positive integers.
135
+
136
+ 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.
137
+
75
138
  ## Scrub state
76
139
 
77
140
  `scrub { needed, version, ready }` tells bios and the agent whether output must pass the vault's scrubber before it reaches the model. It's per namespace:
78
141
  - `needed` is false only when the namespace holds no plaintext at all: no record of any kind or state, removed ones included until they're purged.
79
142
  - `version` counts every change to the namespace's set of values: each seal, rotation, removal, restore and purge.
80
- - `ready` is false only when values exist but this vault frame never held the data key since it loaded, which is a reload with the lock on, before unlock. Locking again later doesn't clear it, because the compiled scrubber stays in memory for the frame's life.
143
+ - `ready` is false only when values exist, the vault is locked, and this frame has no compiled scrubber for the namespace: a reload with the lock on, before unlock. The scrubber (`@ai-ecoverse/slicc-scrub`, over every value of the namespace, records of every kind and state) is compiled whenever the frame holds the key and the set of values changed, and is kept in memory for the frame's life, so locking stops value use but never scrubbing.
81
144
 
82
145
  It's in `status`, in `placeholders` replies and in every `placeholders` push. After `put` and `seal`, the push with the new pair goes out **before** the reply.
83
146
 
@@ -96,12 +159,13 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
96
159
  - 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.
97
160
  - On `port2` it answers `{ v: 1, op: 'welcome', role, status }`, or `{ v: 1, op: 'refused', error }` for another protocol version or an unknown role.
98
161
  - 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).
99
- - **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`, `end`, `drop`, `model`, `mcp`, `endpoints`, `records`, `grant` and `refresh` (all `ENOTSUP` in this version), and anything else is `EACCES`. The agent port gets no events.
162
+ - **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` (the last five `ENOTSUP` in this version), and anything else is `EACCES`. The agent port gets no events. Its ops are below (The agent's ops).
163
+ - **Values:** only `forward` puts a value into the page, inside the head it returns, and only where an accepted rule allows it. No other op, event or reply carries one, and nothing on the agent port ever does.
100
164
 
101
165
  | op (page role) | fields | reply |
102
166
  |---|---|---|
103
167
  | `sealed` | `req` (the `/dev/sealed` request object, or its JSON text), `stamp` (`{ pid, ppid, uid, gid, tag }` from the kernel) | `{ reply }`, the §3.7 reply, below |
104
- | `status` | — | `{ status: { version, protocol, origin, locked, lock, scrub, waiting, exportedAt, backupReminder } }` |
168
+ | `status` | — | `{ status: { version, protocol, origin, locked, lock, scrub, waiting, exportedAt, backupReminder, policy } }` |
105
169
  | `placeholders` | — | `{ placeholders, scrub }`: every placeholder of a record that isn't removed, active and inert, sorted |
106
170
  | `list` | — | `{ list: { secrets, refused, audit, auditMore, exits } }` for Settings › Secrets |
107
171
  | `audit` | `before?` (a `seq`), `limit?` (1–1000, default 100) | `{ audit, more, next? }`, newest first; pass `next` as `before` for the next page |
@@ -111,6 +175,8 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
111
175
  | `cancel` | `request` | `{ state }`; the request's `/dev/sealed` `status` becomes `cancelled` |
112
176
  | `lock` | — | `{}`; drops the key everywhere when the lock is on |
113
177
  | `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
178
+ | `policy` | `files` | the attested policy files, below (Policy) |
179
+ | `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
114
180
  | `import`, `export`, anything else | | `ENOTSUP` |
115
181
 
116
182
  `put` and `seal` need the vault unlocked. They answer `EACCES` with a message starting `vault is locked` otherwise.
@@ -130,8 +196,8 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
130
196
  |---|---|---|
131
197
  | `seal` | `name`, `value` | `{ 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` |
132
198
  | `placeholder` | `name` | `{ placeholder, state }`, including `removed`; `ENOENT` when there's no record |
133
- | `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed |
134
- | `test` | `name`, `url`, `method?`, `header?` | `{ verdict, reason }`. Without policy it's always `refuse`; `ENOENT` for an unknown name |
199
+ | `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
200
+ | `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 |
135
201
  | `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
136
202
  | `request` | `kind: "oauth"`, `provider`, `oauth` (an object) | the same |
137
203
  | `request` | `kind: "jerk-seal"`, `provider` | the same |
@@ -142,6 +208,52 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
142
208
  - **Requests** expire 300 s after they're made. Each uid has at most one pending request per namespace; a second one is `EAGAIN`. `status` and `cancel` see only the caller's own requests (uid 0 sees all), and others get `ENOENT`.
143
209
  - A value request shows up in `status.waiting` and is answered by the page's `put` with its `request` id, or ended by `cancel`. `oauth` and `jerk-seal` requests can only be cancelled in this version.
144
210
 
211
+ ## Forward
212
+
213
+ bios's exit router asks the vault to inject a request the kernel forwarded (an owner-bearing request that carries a placeholder), then sends it itself:
214
+
215
+ ```js
216
+ const channel = new MessageChannel();
217
+ port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }, [channel.port2]);
218
+ ```
219
+
220
+ - `head` is `{ method, url, headers: [name, value][] }`; `owner` the kernel's `{ pid, ppid, uid, gid, tag }`; `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.
221
+ - **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`.
222
+ - **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.
223
+
224
+ **The injector**, fnox's proxy semantics:
225
+ - **Hosts** are compared lower-cased, without a trailing dot, in rules, the refusal list and MCP servers alike.
226
+ - **Refused before anything is sent:** 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.
227
+ - **Paths** in rules are globs: `*` stays within a segment, `**` crosses segments, `?` is one character.
228
+ - **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.
229
+ - **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.
230
+
231
+ ## Model and MCP endpoints
232
+
233
+ The vault never injects a secret into a model or MCP endpoint, whatever a rule says.
234
+ - **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).
235
+ - **Registered endpoints:** the agent's `endpoints` op adds model base URLs and MCP servers for its namespace. They only ever add.
236
+ - **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.
237
+ - **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`.
238
+ - **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.
239
+ - **`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.
240
+
241
+ ## The agent's ops
242
+
243
+ | op | fields | reply |
244
+ |---|---|---|
245
+ | `redact` | `texts`: strings and `Uint8Array`s | `{ texts, hits, version }`: each text scrubbed, in the same order and type; `hits` totals `{ name, form, count }` |
246
+ | `redactStream` | `stream` (an id) | `{ stream, idleMs, version }`; at most 64 streams per port (`EAGAIN`) |
247
+ | `push` | `stream`, `chunk` (bytes) | `{ chunk }`: what can be released, scrubbed |
248
+ | `idle` | `stream` | `{ chunk }`: a held tail under 16 bytes, after `idleMs` without input |
249
+ | `end` | `stream` | `{ chunk, hits }`: the rest, and the stream is gone |
250
+ | `drop` | `stream` | `{}` |
251
+ | `endpoints` | `model?`: https base URLs; `mcp?`: `{ server, url, names? }` or a URL string | `{ ok: true }`; new entries re-check the policy and push `changed` |
252
+
253
+ 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.
254
+
255
+ `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.
256
+
145
257
  ## The button (`button.html`)
146
258
 
147
259
  A popup needs a click inside a vault document, so the way into the lock popup is this button. The embedder loads `https://vault.sliccy.ai/button.html` in an iframe and, after `load`, posts:
@@ -199,7 +311,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
199
311
  - **`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.
200
312
  - **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
201
313
  - **`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.
202
- - **`AuditEntry`**: `forward` is a use of a value in a request, with the exit that carried it; **`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.
314
+ - **`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.
203
315
  - **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
204
316
  - **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
205
317
  - **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
@@ -79,6 +79,7 @@ export interface VaultStatus {
79
79
  waiting: PendingRequest[];
80
80
  exportedAt: EpochMs | null;
81
81
  backupReminder: BackupReminder;
82
+ policy: PolicyFileState[];
82
83
  }
83
84
  export type BackupReminder = 'due' | 'dismissed' | null;
84
85
  export type Availability = 'connecting' | 'waiting' | 'locked' | 'ready';
@@ -120,6 +121,14 @@ export interface ForwardEntry extends AuditBase {
120
121
  rule: Rule;
121
122
  exit: ExitName;
122
123
  }
124
+ export interface McpEntry extends AuditBase {
125
+ type: 'mcp';
126
+ server: string;
127
+ domain: string;
128
+ method: string;
129
+ path: string;
130
+ exit: ExitName;
131
+ }
123
132
  export interface RevealEntry extends AuditBase {
124
133
  type: 'materialize';
125
134
  exec: ExecMode;
@@ -151,7 +160,51 @@ export interface PolicyEntry {
151
160
  changes: PolicyChange[];
152
161
  warning?: string;
153
162
  }
154
- export type AuditEntry = ForwardEntry | RevealEntry | SealEntry | RemoveEntry | PolicyEntry;
163
+ export type AuditEntry = ForwardEntry | McpEntry | RevealEntry | SealEntry | RemoveEntry | PolicyEntry;
164
+ export interface RequestHead {
165
+ method: string;
166
+ url: string;
167
+ headers: [string, string][];
168
+ }
169
+ export interface RawTraits {
170
+ manualRedirects?: boolean;
171
+ encodedBodies?: boolean;
172
+ }
173
+ export type ForwardResult = {
174
+ head: RequestHead;
175
+ body?: Uint8Array;
176
+ } | {
177
+ refused: {
178
+ status: number;
179
+ reason: string;
180
+ };
181
+ };
182
+ export type ResponsePart = {
183
+ op: 'response';
184
+ status: number;
185
+ statusText: string;
186
+ url: string;
187
+ redirected?: boolean;
188
+ headers: [string, string][];
189
+ } | {
190
+ op: 'body';
191
+ chunk: Uint8Array;
192
+ } | {
193
+ op: 'end';
194
+ } | {
195
+ op: 'error';
196
+ message: string;
197
+ };
198
+ export interface McpServerEntry {
199
+ server: string;
200
+ url: string;
201
+ names?: string[];
202
+ }
203
+ export interface ScrubHit {
204
+ name: string;
205
+ form: string;
206
+ count: number;
207
+ }
155
208
  export interface AuditPage {
156
209
  audit: AuditEntry[];
157
210
  more: boolean;
@@ -165,6 +218,38 @@ export interface ExitPolicyView {
165
218
  exits: ExitName[];
166
219
  }[];
167
220
  }
221
+ export interface Attestation {
222
+ path: string;
223
+ bytes: Uint8Array;
224
+ uid: number;
225
+ gid: number;
226
+ mode: number;
227
+ ino: number;
228
+ mtimeMs: EpochMs;
229
+ sha256: string;
230
+ users: string;
231
+ onRootFs: boolean;
232
+ parents: {
233
+ path: string;
234
+ uid: number;
235
+ gid: number;
236
+ mode: number;
237
+ }[];
238
+ }
239
+ export interface PolicyFileState {
240
+ path: string;
241
+ sha256: string | null;
242
+ accepted: boolean;
243
+ warnings: string[];
244
+ }
245
+ export interface PolicyResult {
246
+ accepted: boolean;
247
+ files: PolicyFileState[];
248
+ rules: (Rule & {
249
+ secret: string;
250
+ })[];
251
+ warnings: string[];
252
+ }
168
253
  export interface VaultList {
169
254
  secrets: SecretView[];
170
255
  refused: RefusedRule[];
@@ -268,6 +353,63 @@ export type Op = {
268
353
  op: 'lock';
269
354
  } | {
270
355
  op: 'dismissBackupReminder';
356
+ } | {
357
+ op: 'policy';
358
+ files: Attestation[];
359
+ } | {
360
+ op: 'forward';
361
+ head: RequestHead;
362
+ body?: Uint8Array | string;
363
+ owner: Stamp;
364
+ exit: ExitName;
365
+ rawTraits: RawTraits;
366
+ };
367
+ export type AgentOp = {
368
+ op: 'redact';
369
+ texts: (string | Uint8Array)[];
370
+ } | {
371
+ op: 'redactStream';
372
+ stream: string;
373
+ } | {
374
+ op: 'push';
375
+ stream: string;
376
+ chunk: Uint8Array;
377
+ } | {
378
+ op: 'idle';
379
+ stream: string;
380
+ } | {
381
+ op: 'end';
382
+ stream: string;
383
+ } | {
384
+ op: 'drop';
385
+ stream: string;
386
+ } | {
387
+ op: 'endpoints';
388
+ model?: string[];
389
+ mcp?: (string | McpServerEntry)[];
390
+ };
391
+ export type AgentReply = {
392
+ op: 'redact';
393
+ texts: (string | Uint8Array)[];
394
+ hits: ScrubHit[];
395
+ version: number;
396
+ } | {
397
+ op: 'redactStream';
398
+ stream: string;
399
+ idleMs: number;
400
+ version: number;
401
+ } | {
402
+ op: 'push' | 'idle';
403
+ chunk: Uint8Array;
404
+ } | {
405
+ op: 'end';
406
+ chunk: Uint8Array;
407
+ hits: ScrubHit[];
408
+ } | {
409
+ op: 'drop';
410
+ } | {
411
+ op: 'endpoints';
412
+ ok: true;
271
413
  };
272
414
  export type OpReply = {
273
415
  op: 'sealed';
@@ -299,7 +441,11 @@ export type OpReply = {
299
441
  } | {
300
442
  op: 'dismissBackupReminder';
301
443
  backupReminder: BackupReminder;
302
- };
444
+ } | ({
445
+ op: 'policy';
446
+ } & PolicyResult) | ({
447
+ op: 'forward';
448
+ } & ForwardResult);
303
449
  export type Request = Op & {
304
450
  v: Protocol;
305
451
  id: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.1.0",
3
+ "version": "1.3.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
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -31,13 +31,13 @@
31
31
  "dist/types"
32
32
  ],
33
33
  "scripts": {
34
- "prepare": "[ ! -e .git ] || lefthook install",
35
- "build": "node build.mjs && tsc -p tsconfig.build.json",
34
+ "prepare": "node scripts/endpoints.mjs && { [ ! -e .git ] || lefthook install; }",
35
+ "build": "node scripts/endpoints.mjs && node build.mjs && tsc -p tsconfig.build.json",
36
36
  "prepack": "npm run build",
37
37
  "start": "npm run build && node test/integration/serve.mjs",
38
38
  "pretest": "npm run build && playwright-core install --no-shell chromium",
39
39
  "test": "node --test --test-timeout=300000 --test-global-setup=node_modules/@ai-ecoverse/slicc-shared-web/harness/global.mjs 'test/integration/*.test.mjs'",
40
- "test:unit": "NODE_NO_WARNINGS=1 mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --import fake-indexeddb/auto --test --test-force-exit 'test/unit/**/*.test.mjs'",
40
+ "test:unit": "node scripts/endpoints.mjs && NODE_NO_WARNINGS=1 mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --import fake-indexeddb/auto --test --test-force-exit 'test/unit/**/*.test.mjs'",
41
41
  "typecheck": "tsc",
42
42
  "lint": "slicc-lint",
43
43
  "format": "biome check --write .",
@@ -46,8 +46,10 @@
46
46
  "release": "semantic-release"
47
47
  },
48
48
  "devDependencies": {
49
+ "@ai-ecoverse/slicc-scrub": "1.0.0",
49
50
  "@ai-ecoverse/slicc-shared-web": "1.13.1",
50
51
  "@biomejs/biome": "2.5.15",
52
+ "@earendil-works/pi-ai": "1.0.4",
51
53
  "@happy-dom/global-registrator": "20.14.5",
52
54
  "@spectrum-css/button": "14.2.0",
53
55
  "@spectrum-css/textfield": "8.2.0",