@ai-ecoverse/slicc-vault 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ Otherwise the vault keeps the policy it last accepted for that file, so the rule
110
110
 
111
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
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.
113
+ A trusted one clears the accepted policy of that file and pauses it: while either file is paused, `forward` of a secret, `materialize`, `materializeConsume`, `sessionMaterialize`, `sessionAnchors` and the `test` verdict all refuse with `policy file missing: paused` (`EACCES`, or a `403` from `forward`) and `cause: 'policy-paused'`, so deleting `secrets-policy.toml` never falls back to permissive defaults. `forward` of an `mcp` record uses no rule and goes on. The pause is kept with the accepted policy, and ends when a trusted attestation of that file with `bytes` arrives. Until then `files` and `status.policy` show the file with `absent: true` and `paused: true`, in every `policy` reply, whether or not the call named it.
114
114
 
115
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.
116
116
 
@@ -171,7 +171,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
171
171
 
172
172
  - The frame answers only when the message comes from its own parent window, from an allowed origin, matching `location.ancestorOrigins[0]` when the browser exposes it. Anything else gets no answer at all.
173
173
  - On `port2` it answers `{ v: 1, op: 'welcome', role, status }`, or `{ v: 1, op: 'refused', error }` for another protocol version or an unknown role.
174
- - Everything after that goes over the port. The namespace and the role are fixed by the hello. Requests are `{ v: 1, id, op, ...fields }`; replies are `{ v: 1, id, op, ...fields }` or `{ v: 1, id, error: { code, message } }`. Each port is answered in order (FIFO).
174
+ - Everything after that goes over the port. The namespace and the role are fixed by the hello. Requests are `{ v: 1, id, op, ...fields }`; replies are `{ v: 1, id, op, ...fields }` or `{ v: 1, id, error: { code, message, cause? } }`. Each port is answered in order (FIFO).
175
175
  - **Roles:** `page` (the default) is bios and spectrum. bios may hand spectrum a page port of its own, or relay; the messages are the same. `agent` is the port bios hands the agent worker: it may use only `redact`, `redactStream`, `push`, `idle`, `end`, `drop`, `endpoints`, `model`, `mcp`, `records`, `grant` and `refresh`, and anything else is `EACCES`. The agent port gets no events. Its ops are below (The agent's ops).
176
176
  - **Values:** only `forward` (inside the head it returns, where an accepted rule allows it), `materializeConsume` (for an `[exec]` grant), `sessionMaterialize` (for a session rule, to the kernel's TLS writer, §3.19) and `egress` (a refresh request) put a value into the page. On the agent port only `model` and `mcp` do: their reply's prepared request carries the provider's or server's credential in its headers or URL, only for that provider's or server's own URLs, for the agent worker's transport to send at once. It must never be logged, stored, retried from storage or shown. No other op, event or reply carries a value, and no value ever reaches the model.
177
177
 
@@ -211,16 +211,18 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
211
211
 
212
212
  **Errors** use errno names: `EINVAL`, `ENOENT`, `EACCES`, `EEXIST`, `EAGAIN`, `ETIMEDOUT`, `ENOTCONN`, and `ENOTSUP` for ops this version doesn't have.
213
213
 
214
+ **`cause`** is the machine-readable reason for a refusal; match on it, never on `message`. Its one value is `policy-paused`: the policy is paused (see Policy) and the vault may well be unlocked. `forward` (in `refused`), `materialize`, `materializeConsume`, `sessionMaterialize`, `sessionAnchors` (in `error`) and `test` (beside `verdict`) can return it, with `EACCES` or status `403` and `policy file missing: paused`. Every other refusal has no `cause`, a locked vault's included. `status.policy[]` says which file to restore: its entries `{ path, sha256, accepted, warnings, absent?: true, paused?: true }`, in page `status` replies and `status` events, name the missing file in `path`.
215
+
214
216
  ## `/dev/sealed`, the vault side
215
217
 
216
- bios relays each kernel request unchanged as the `sealed` op, with the kernel's stamp. The vault validates and answers `{ v: 1, ...fields }` or `{ v: 1, error: { code, message } }`. A request over 64 KiB, one that isn't a single JSON object, one with `v` other than 1, and an unknown `op` are `EINVAL`, as is a missing or malformed stamp.
218
+ bios relays each kernel request unchanged as the `sealed` op, with the kernel's stamp. The vault validates and answers `{ v: 1, ...fields }` or `{ v: 1, error: { code, message, cause? } }`. A request over 64 KiB, one that isn't a single JSON object, one with `v` other than 1, and an unknown `op` are `EINVAL`, as is a missing or malformed stamp.
217
219
 
218
220
  | op | fields | reply |
219
221
  |---|---|---|
220
222
  | `seal` | `name`, `value`, and for an MCP bridge `kind: "mcp"`, `server`, `url` | `{ placeholder, state }`. A new name creates a `process` record. An existing one is rotated and keeps its placeholder, but only when its value came from a process; a value from Settings, jerk, OAuth or an import is replaced only by uid 0, and anyone else gets `EEXIST`. **A bridge token** (§3.16.3, e.g. mcpcat's `--bearer`) is an `mcp` record bound to `server` and `url`, which must be a kernel-loopback URL (`http://<port>.kernel.localhost/…`, never 9222; anything else is `EACCES`). Any write that isn't a process's over a `process` record of another kind than `secret` (a page `put` or `seal`, a sign-in, an import) makes it an ordinary record, without the URL, under a new placeholder, so whoever sealed it keeps no placeholder of the new value; a `secret` keeps its placeholder, which every uid can read and the rules name. It records the sealing uid, and resealing it, even after it was removed, is allowed only for a `process` `mcp` record sealed by the same uid, or by uid 0; anything else, a user-entered record included, is `EEXIST` |
221
223
  | `placeholder` | `name`, `server?` | `{ placeholder, state }` of the `secret` record, including `removed`; `ENOENT` when there's no record. For root, `server` changes the answer, and `name` is optional, below (Root and `mcp` records) |
222
224
  | `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
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 |
225
+ | `test` | `name`, `url`, `method?` (`GET`), `header?` (`authorization`), `exit?` (`page`) | `{ verdict: 'inject' \| 'refuse', reason, cause? }`: the injector's verdict for `Bearer <placeholder>` in that header, without sending anything, with `cause: 'policy-paused'` when the policy is paused; `ENOENT` for an unknown name |
224
226
  | `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
225
227
  | `request` | `kind: "oauth"`, `provider`, `oauth` (an object) | the same |
226
228
  | `request` | `kind: "jerk-seal"`, `provider` | the same |
@@ -325,7 +327,7 @@ port.postMessage({ v: 1, id, op: 'forward', head, bodyPresent, owner, exit, rawT
325
327
  - `{ mode: 'secret', head }`: the same head with the values filled in. It has no body, whatever bios sent.
326
328
  - `{ mode: 'mcp', head, bodyComplete: true, body? }`: the same, with `body` the complete request body scrubbed over every value, the only bytes bios may send. A request with `bodyPresent: false` gets no `body` field; with `bodyPresent: true` it always does, an empty `Uint8Array` for an empty body.
327
329
  - `{ needsBody: true }`: an MCP request with `bodyPresent: true` and no `body`. Nothing else happened: no head, no `mcp` audit entry, no `usedAt`, no pin. Like any `forward`, it may run the one-time upgrade of legacy endpoints, with its `endpoints` audit entry. bios collects the complete body and calls `forward` again, with a new `MessageChannel`.
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.
330
+ - `{ refused: { status, reason, cause? } }`: `403` with a one-line reason, and `cause: 'policy-paused'` when the policy is paused, `400` for an MCP request whose `bodyPresent` isn't a boolean ("an MCP request must say whether it has a body") or is `false` with a `body`, or `502` for an exit with encoded bodies. A refused request must not be sent anywhere.
329
331
  - Malformed fields, and an MCP body above 32 MiB, are `EINVAL`.
330
332
  - **The port:** on every reply but a `mode` one, the vault closes the transferred port.
331
333
  - **The response** goes back over `channel.port1`, scrubbed: bios posts `{ op: 'response', status, statusText, url, redirected?, headers }`, then any number of `{ op: 'body', chunk: Uint8Array }`, then `{ op: 'end' }` or `{ op: 'error', message }`. The vault answers with the same messages, with `statusText`, `url`, every header name and value, the body and the error scrubbed over every value of the namespace. A value in its raw form comes back as its placeholder, anything else as `[redacted:NAME]`. The body is scrubbed as a stream; after 250 ms without input, a held tail shorter than 16 bytes is let through.
@@ -513,6 +515,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
513
515
  - **`AuditEntry`**: `endpoints` is vault-originated: the upgrade's `purged` (show it as "Removed custom model URLs an agent set") and `adopted` ("Kept the MCP servers your tokens use"), a `pinned` MCP token ("pinned to <host>", with an Unpin action) and an `unpinned` one; `forward` is a use of a value in a request, with the exit that carried it; `mcp` a kernel program's call to a registered MCP server; `session` a value handed to the kernel for a verified TLS session (show it as "Sent over PROTOCOL to host", with "trusted by public roots" or "trusted by its pinned anchor" from `verifiedBy`, and nothing when an older entry has no `verifiedBy`); **`materialize` is shown as "Revealed to a program"**; `seal`, `remove`, `restore` and `policy` (with its `changes`) record changes. Every entry has a `seq` for paging.
514
516
  - **`SessionTarget`**: a `sessionMaterialize` request's protocol, position and what goes with it: `placeholders` (and `head` for http1) for every position but `scram-sha-256`, which takes `scram` (a `ScramExchange`) and no `placeholders`.
515
517
  - **`VaultList.mcp`** lists usable `mcp` records for bios's jerk `status`, without a placeholder or source (`list`); treat every entry as usable, whatever its `state`.
518
+ - **`ErrorCause`**: the machine-readable `cause` on `VaultError`, on `ForwardResult`'s `refused` and on `TestVerdict` (the `/dev/sealed` `test` reply). Its one value is `policy-paused`.
516
519
  - **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
517
520
  - **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
518
521
  - **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
@@ -21,9 +21,11 @@ export interface Stamp {
21
21
  users?: string;
22
22
  groups?: number[];
23
23
  }
24
+ export type ErrorCause = 'policy-paused';
24
25
  export interface VaultError {
25
26
  code: ErrorCode;
26
27
  message: string;
28
+ cause?: ErrorCause;
27
29
  }
28
30
  export type SessionProtocol = 'http1' | 'smtp' | 'pg';
29
31
  export type SessionPosition = 'auth-plain' | 'auth-login' | 'password-cleartext' | 'scram-sha-256';
@@ -284,8 +286,14 @@ export type ForwardResult = {
284
286
  refused: {
285
287
  status: number;
286
288
  reason: string;
289
+ cause?: ErrorCause;
287
290
  };
288
291
  };
292
+ export interface TestVerdict {
293
+ verdict: 'inject' | 'refuse';
294
+ reason: string;
295
+ cause?: ErrorCause;
296
+ }
289
297
  export type ResponsePart = {
290
298
  op: 'response';
291
299
  status: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "The separate-origin vault at vault.sliccy.ai: sealed secrets, OAuth tokens, and model and MCP credentials for SLICC seven, used only by its injector",
5
5
  "piAi": "1.4.0",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@ai-ecoverse/slicc-cdp": "1.43.1",
55
- "@ai-ecoverse/slicc-kernel": "1.170.3",
55
+ "@ai-ecoverse/slicc-kernel": "1.170.4",
56
56
  "@ai-ecoverse/slicc-shared-web": "2.13.7",
57
57
  "@ai-ecoverse/wasm-bash": "5.3.0-23",
58
58
  "@ai-ecoverse/wasm-coreutils": "9.12.0-15",