@ai-ecoverse/slicc-vault 1.7.2 → 1.8.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 +33 -12
- package/dist/types/contract.d.ts +79 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +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` and `
|
|
6
|
-
- **Not built yet:** the agent's `model`, `mcp`, `records`, `grant` and `refresh`. 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`, `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`.
|
|
7
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.
|
|
8
7
|
|
|
9
8
|
## Documents and hosting
|
|
@@ -57,13 +56,14 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
|
|
|
57
56
|
|
|
58
57
|
```
|
|
59
58
|
{ ns, kind: "secret" | "grant" | "model" | "mcp", name, placeholder, sealed: { iv, data },
|
|
60
|
-
source, createdAt, updatedAt, usedAt?, expiresAt?, refresh?, removedAt? }
|
|
59
|
+
source, createdAt, updatedAt, usedAt?, expiresAt?, refresh?, oauth?, provider?, server?, settings?, removedAt? }
|
|
61
60
|
```
|
|
62
61
|
|
|
63
62
|
- **`ns`** is the embedder's origin, below.
|
|
64
63
|
- **`name`** matches `^[A-Za-z_][A-Za-z0-9_]*$`, at most 128 characters.
|
|
65
64
|
- **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.
|
|
66
65
|
- **`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
|
+
- **`provider`** (a `model` record's pi-ai provider id, required, `^[a-z0-9][a-z0-9._-]{0,63}$`), **`server`** (an `mcp` record's MCP server name, optional) and **`settings`** (non-secret `NAME = text` pairs for a `model` or `mcp` record, at most 16, each at most 256 characters, e.g. `AWS_REGION`) bind a record to what may use it. A reseal without them keeps the stored ones, and one with them rebinds the record; a new `model` record without a provider is refused with `EINVAL` "a model record needs its provider".
|
|
67
67
|
- **`source`** is `settings` (entered in spectrum), `process`, `jerk:<provider>@<device>`, `oauth:<provider>` or `import:<kind>`.
|
|
68
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`.
|
|
69
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.
|
|
@@ -159,8 +159,8 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
159
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.
|
|
160
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.
|
|
161
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).
|
|
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
|
|
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. No other op, event or reply carries
|
|
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`, 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.
|
|
164
164
|
|
|
165
165
|
| op (page role) | fields | reply |
|
|
166
166
|
|---|---|---|
|
|
@@ -169,8 +169,8 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
169
169
|
| `placeholders` | — | `{ placeholders, scrub }`: every placeholder of a record that isn't removed, active and inert, sorted |
|
|
170
170
|
| `list` | — | `{ list: { secrets, refused, audit, auditMore, exits } }` for Settings › Secrets |
|
|
171
171
|
| `audit` | `before?` (a `seq`), `limit?` (1–1000, default 100) | `{ audit, more, next? }`, newest first; pass `next` as `before` for the next page |
|
|
172
|
-
| `put` | `name`, `value`, `request?` | `{ name, placeholder, state }`. A value typed in Settings, with source `settings`; on an existing name it's a rotation that keeps the placeholder. With `request`, it answers that value request, which must still be pending and ask for `name` (else `EINVAL`, and nothing is stored), so its `/dev/sealed` `status` becomes `done` |
|
|
173
|
-
| `seal` | `name`, `value`, `source`, `kind?` (default `secret`), `expiresAt?`, `refresh?`, `oauth?`, `request?`, `jerk?` | `{ name, placeholder, state }`. bios's, for J1 leases and sign-in tokens, below (Tokens and refresh) |
|
|
172
|
+
| `put` | `name`, `value`, `kind?` (`secret`, the default, `model` or `mcp`), `provider?`, `server?`, `settings?`, `request?` | `{ name, placeholder, state }`. A value typed in Settings (Settings › Accounts puts model keys as `kind: 'model'` with their `provider`), with source `settings`; on an existing name it's a rotation that keeps the placeholder. With `request`, it answers that value request, which must still be pending and ask for `name`, and the record must be a `secret` (else `EINVAL`, and nothing is stored), so its `/dev/sealed` `status` becomes `done` |
|
|
173
|
+
| `seal` | `name`, `value`, `source`, `kind?` (default `secret`), `expiresAt?`, `refresh?`, `oauth?`, `provider?`, `server?`, `settings?`, `request?`, `jerk?` | `{ name, placeholder, state }`. bios's, for J1 leases and sign-in tokens, below (Tokens and refresh) |
|
|
174
174
|
| `answer` | `request`, `state` (`progress` or `done`), `progress?` (≤ 200), `exit?`, `user?`, `scopes?`, `message?` (≤ 500), `rows?`, `placeholder?` | `{ state }`. The answer to a `jerk` or `unseal` request (or a sign-in that failed: for `oauth` and `jerk-seal`, `done` needs a nonzero `exit`, since only `seal` completes them); fields merge into the request, and `status` returns them. `rows` (for `jop: "list"`) are `{ provider, name?, env, device?, signedIn?, user?, scopes? }`, with any other field dropped, so no token can pass. For a `jop: "status"` answered `done` with `exit` 0, the vault adds the placeholder of the provider's own `oauth:<provider>` or `jerk:<provider>@…` record; a `placeholder` given by bios must be one of those records of the provider |
|
|
175
175
|
| `refresh` | `name` | `{ state: 'refreshed', expiresAt? }` or `{ state: 'requested' }`, below |
|
|
176
176
|
| `remove`, `restore` | `name`, `kind?` (default `secret`) | `{ name, placeholder, state }`. A record removed more than 30 days ago can't be restored (`ENOENT`) |
|
|
@@ -201,7 +201,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
|
|
|
201
201
|
|
|
202
202
|
| op | fields | reply |
|
|
203
203
|
|---|---|---|
|
|
204
|
-
| `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` |
|
|
204
|
+
| `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`). A page `put` or `seal` of the same name turns it into an ordinary `mcp` record, without the URL. 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` |
|
|
205
205
|
| `placeholder` | `name` | `{ placeholder, state }`, including `removed`; `ENOENT` when there's no record |
|
|
206
206
|
| `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
|
|
207
207
|
| `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 |
|
|
@@ -226,7 +226,7 @@ The vault has no network stack, so sign-ins run in seven's page (route (b), §3.
|
|
|
226
226
|
- the access token as `{ name, value, source: 'oauth:<provider>', expiresAt, refresh: '<grant name>' }`. A grant name needs an `oauth:` source and `jerk-seal` a `jerk:` one (`EINVAL`), and a refresh uses the grant only when its source is the token's own (else the grant is missing).
|
|
227
227
|
- With `request` (only for a `secret`; the id of a pending `oauth` request for the same provider as `oauth:<provider>`, or a `jerk-seal` request for the provider of `jerk:<provider>@…`) and `jerk` (`{ exit, user?, scopes?, expires_at? }`), `seal` also answers that request, in the same transaction that stores the record (a request that ended meanwhile refuses with `EINVAL`, and nothing is stored): its `/dev/sealed` `status` becomes `done`, with `jerk`, the record's `placeholder` and `record` state.
|
|
228
228
|
- **A J1 lease:** `{ name, value, source: 'jerk:<provider>@<device>', expiresAt, refresh: 'jerk-seal' }`.
|
|
229
|
-
- **When a token is due** (5 minutes before `expiresAt`): the core frame checks once a minute, for every connected namespace, and `refresh { name }` asks for it at once. Vault frames of the same storage (several tabs) take a Web Lock per refresh source, so one grant is sent to its IdP by one frame at a time; a frame that finds the token changed while it waited (refreshed or sealed again) sends nothing and answers with the stored token's `{ state: 'refreshed', expiresAt? }`.
|
|
229
|
+
- **When a token is due** (5 minutes before `expiresAt`): the core frame checks once a minute, for every connected namespace and for `secret`, `model` and `mcp` records alike, and `refresh { name }` asks for it at once. Vault frames of the same storage (several tabs) take a Web Lock per refresh source, so one grant is sent to its IdP by one frame at a time; a frame that finds the token changed while it waited (refreshed or sealed again) sends nothing and answers with the stored token's `{ state: 'refreshed', expiresAt? }`.
|
|
230
230
|
- A `jerk-seal` token is announced once per `expiresAt` (kept in IndexedDB, so across frames), and again every 10 minutes while it stays unsealed, in case no live page heard it, with the `refresh` event, and bios runs J1 `seal` again (`{ state: 'requested' }`).
|
|
231
231
|
- An OAuth token is refreshed in the vault: it builds `POST tokenUrl` with `grant_type=refresh_token`, the refresh token, `client_id` and any `scope` and `resource` (form-encoded), and sends it through bios with the `egress` event. bios sends the request through its exits and posts `{ status, headers, body }` (or `{ error }`) on the event's port within 30 s. The event goes to the page port of the namespace that connected last. A port that doesn't answer in time (`ETIMEDOUT`), closed or not, keeps its ops and events but gets no more `egress`, and the next refresh goes to the page port before it. The vault reads `access_token`, `expires_in` and any new `refresh_token`, checks them as values, and rotates the records in one transaction with the `refresh` audit entry (their placeholders stay; if the token or its grant was changed, removed or restored during the round trip, nothing is written and it answers `EAGAIN`). An `expires_in` of 0 makes the token due at once, and one that isn't a non-negative finite number is `EACCES: refresh failed: the IdP returned an invalid expires_in` (likewise a `refresh_token` that is present but not a string, and a `token_type` other than `Bearer`); a response carrying `error` fails even with a token; a response without `expires_in` leaves the new token without an expiry, so it isn't refreshed again until a new one is sealed. It answers `{ state: 'refreshed', expiresAt? }`, without `expiresAt` when the token has none. A failed refresh is `EACCES: refresh failed: <the IdP's error code>`, scrubbed. Without a connected page it's `ENOTCONN`.
|
|
232
232
|
- `egress` carries the refresh token to the page, in transit only; the response goes back to the vault, never to the kernel.
|
|
@@ -252,7 +252,7 @@ Spectrum's Settings › Secrets reads the file and the password, and bios hands
|
|
|
252
252
|
|
|
253
253
|
| `kind` | `password` | what becomes a candidate |
|
|
254
254
|
|---|---|---|
|
|
255
|
-
| `bundle` | the export passphrase | each record, with its kind, source (`import:bundle` when it isn't a valid source), expiry and **placeholder**, so the policy files still match. A placeholder used by another record of the namespace, or by another embedder of this vault, is refused ("this app already has this secret under another name", "another app that uses this vault already has this secret; use it there, or add it here by hand"), as is a name whose record here has another placeholder ("this app already has a different secret with this name") |
|
|
255
|
+
| `bundle` | the export passphrase | each record, with its kind, source (`import:bundle` when it isn't a valid source), expiry, `provider`, `server` and `settings` (a `model` record without a provider is refused with "a model record needs its provider"), and **placeholder**, so the policy files still match. A placeholder used by another record of the namespace, or by another embedder of this vault, is refused ("this app already has this secret under another name", "another app that uses this vault already has this secret; use it there, or add it here by hand"), as is a name whose record here has another placeholder ("this app already has a different secret with this name") |
|
|
256
256
|
| `keepass` | the master password (KDBX 3 or 4, as kdbxweb reads them; Argon2 through hash-wasm) | each entry with a password, named after its title (upper-cased, other characters as `_`, a leading digit prefixed with `_`; a name already taken by an earlier entry gets `_2`, `_3`, …), source `import:keepass` |
|
|
257
257
|
| `six` | — | each `NAME=value` line of six's `secrets.env` (`export` and quotes allowed), source `import:six`. `NAME_DOMAINS` becomes `suggestedRules`: a `[[proxy.rules]]` stanza per exact domain, for the human to add with `sudo`, and a note for each wildcard, which isn't carried over |
|
|
258
258
|
|
|
@@ -330,7 +330,27 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
|
|
|
330
330
|
| `idle` | `stream` | `{ chunk }`: a held tail under 16 bytes, after `idleMs` without input |
|
|
331
331
|
| `end` | `stream` | `{ chunk, hits }`: the rest, and the stream is gone |
|
|
332
332
|
| `drop` | `stream` | `{}` |
|
|
333
|
-
| `endpoints` | `model?`: https base URLs; `mcp?`: `{ server, url, names? }` or a URL string | `{ ok: true }`;
|
|
333
|
+
| `endpoints` | `model?`: https base URLs, or `{ provider, url }` for a provider's custom base URL; `mcp?`: `{ server, url, names?, tokenUrl? }` or a URL string | `{ ok: true }`; registrations only add: a server's first `tokenUrl` stays, for every URL it registers. New entries re-check the policy and push `changed` |
|
|
334
|
+
| `model` | `provider?`, `request`: `{ method, url, headers: [name, value][], body?: string \| bytes }` | `{ request, hits, version }`, the request prepared for the agent's transport, below |
|
|
335
|
+
| `mcp` | `server`, `request` (the same shape) | `{ request, hits, version }`, below |
|
|
336
|
+
| `records` | — | `{ records: [{ kind, name, placeholder, state, provider?, server?, expiresAt?, settings? }] }`: the namespace's `model` and `mcp` records that aren't removed, `state` being `ready`, `expired` or `refreshing` (expired with a refresh source), and `settings` scrubbed over every value (an entry whose name holds a value is dropped; so is a record whose name, provider or server holds one, and an `expiresAt` whose digits are a value). Never a value, a `secret` or a `grant`; the records are read and scrubbed under one scrub version (retried, then `EAGAIN` while values keep changing); `EACCES` while the vault is locked before its first unlock |
|
|
337
|
+
| `grant` | `server`, `response` (the token endpoint's body, a string or bytes), `oauth` (`{ tokenUrl, clientId, scope?, resource? }`), `name?` (a record name; anything else is `EINVAL`) | `{ response, records: [{ kind, name, placeholder }], expiresAt? }`, below |
|
|
338
|
+
| `refresh` | `server` or `provider`, `name?` | `{ state: 'refreshed', expiresAt? }`: refreshes that server's or provider's token in the vault, as Tokens and refresh describes; `ENOENT` when it has none, `EINVAL` when it has several and no `name` picks one |
|
|
339
|
+
|
|
340
|
+
**Preparing model and MCP requests (§3.12).** Every model and MCP request of the agent worker goes through `model` or `mcp`, with or without a credential; the response comes back to the worker directly.
|
|
341
|
+
- **The head carries no value:** checked with the scrubber from before and after any refresh or rotation during the request, a raw value (in any form the scrubber knows) in the method, the URL, a header name or a header value is `EACCES` ("the request head carries a value; send its placeholder instead").
|
|
342
|
+
- **The body** (a string or bytes, at most 32 MiB of UTF-8) is scrubbed over every value of the namespace (S4), with both of those scrubbers, so neither a token refreshed for this request nor a value rotated meanwhile slips through and keeps its type; placeholders in it are left alone. The URL must be `https:`, or for `mcp` the kernel loopback `http://<port>.kernel.localhost/…` (port 9222 refused) under that server's own loopback registration; anything else is `EACCES` ("model and MCP requests go out over https only").
|
|
343
|
+
- **The credential goes in by placeholder.** The worker sets a provider's API key (or `mcp.json`'s `${NAME}`) to the record's placeholder from `records`, and the vault replaces it with the value **only in the headers and the URL** (URL-encoded there), never in the body. Every placeholder of the namespace in the headers or URL must belong to the target, else `EACCES` ("NAME isn't a credential of TARGET; secrets are never sent to model or MCP endpoints"): for `model`, a `model` record of that `provider`; for `mcp`, an `mcp` record bound to that `server` or listed in its `names`; a bridge token sealed by a program only when the server's registration has exactly its `url` and the request is under that `url` itself (never at the server's `tokenUrl`). A `secret` or `grant` is never filled in, nor a credential whose value contains or equals another record's value in any form the scrubber knows except a 16-character fragment (`EACCES`). Every string in the replies of `model`/`mcp` (outside the prepared head), `records`, `grant` and `refresh`, and every error message, is scrubbed over every value, and a number whose digits are a value is left out.
|
|
344
|
+
- **Its own URLs only.** `model` fills a key only under that provider's built-in base URL (pi-ai's catalog, generated with the provider ids) or a `{ provider, url }` registered with `endpoints`; `mcp` only under that server's registered URL (same scheme and port), at exactly its `tokenUrl`, or at its kernel loopback URL. Elsewhere a filled placeholder is `EACCES` ("NAME is sent only to TARGET's own endpoints"). A request without such a placeholder is only scrubbed, wherever it goes over `https:`, so OAuth discovery and client registration work.
|
|
345
|
+
- **`provider` is optional:** without it, the provider is the one of the single `model` placeholder in the headers and URL; with none or several, nothing is filled.
|
|
346
|
+
- **An expired record** with a refresh source is refreshed first, once per prepared request however often its placeholder appears, through `egress` (`EACCES` "NAME expired; refreshing" if that fails); one without is `EACCES` "NAME expired". A removed record is `EACCES`. A locked vault before its first unlock is `EACCES` ("unlock the vault to continue").
|
|
347
|
+
- **Consistency:** if the namespace's values change while a request is prepared (other than by its own refresh), it's `EAGAIN` ("prepare the request again"), and a credential that a refresh rebinds to another provider or server is `EACCES`; `grant`'s kept fields also have the returned tokens replaced by their placeholders directly, and its record names and `expiresAt` are scrubbed against a value stored meanwhile.
|
|
348
|
+
- **The reply carries the value,** in `request.headers` or `request.url`, for the worker's transport to send at once. It is never logged, stored, retried from storage or shown; after a 401 the worker calls `refresh` once and prepares the request again.
|
|
349
|
+
|
|
350
|
+
**MCP OAuth (`grant`).** The worker registers the server with its `tokenUrl`, sends the code exchange through `mcp` (to exactly `tokenUrl`, with no credential), and hands the token endpoint's body to `grant`, holding it only in transit.
|
|
351
|
+
- `oauth.tokenUrl` must be `https:` and the server's registered `tokenUrl` (`EACCES` otherwise). A server or record name that holds a stored value or one of the returned tokens, in any encoded form, is `EINVAL`. The body is checked as for a refresh (an object with a string `access_token`, no `error`, `token_type` Bearer if present, a valid `expires_in`), and every token must be a valid value (`EINVAL` otherwise), before anything is stored.
|
|
352
|
+
- 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 becomes the token's refresh source.
|
|
353
|
+
- 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.
|
|
334
354
|
|
|
335
355
|
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.
|
|
336
356
|
|
|
@@ -392,7 +412,8 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
|
|
|
392
412
|
- **`Rule`**: `domain` is an exact lower-case host; `header` a lower-case header name; **empty `methods` or `paths` mean any**.
|
|
393
413
|
- **`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.
|
|
394
414
|
- **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
|
|
395
|
-
-
|
|
415
|
+
- **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
|
+
- **`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`.
|
|
396
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 …".
|
|
397
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.
|
|
398
419
|
- **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface SecretView {
|
|
|
45
45
|
sourceDevice: DeviceId | null;
|
|
46
46
|
provider?: string;
|
|
47
47
|
account?: string;
|
|
48
|
+
server?: string;
|
|
49
|
+
settings?: Record<string, string>;
|
|
48
50
|
domains: string[];
|
|
49
51
|
rules: Rule[];
|
|
50
52
|
exits?: ExitName[];
|
|
@@ -207,6 +209,32 @@ export interface McpServerEntry {
|
|
|
207
209
|
server: string;
|
|
208
210
|
url: string;
|
|
209
211
|
names?: string[];
|
|
212
|
+
tokenUrl?: string;
|
|
213
|
+
}
|
|
214
|
+
export interface RecordBinding {
|
|
215
|
+
provider?: string;
|
|
216
|
+
server?: string;
|
|
217
|
+
settings?: Record<string, string>;
|
|
218
|
+
}
|
|
219
|
+
export interface ModelEndpoint {
|
|
220
|
+
provider: string;
|
|
221
|
+
url: string;
|
|
222
|
+
}
|
|
223
|
+
export interface OutgoingRequest {
|
|
224
|
+
method: string;
|
|
225
|
+
url: string;
|
|
226
|
+
headers: [string, string][];
|
|
227
|
+
body?: string | Uint8Array;
|
|
228
|
+
}
|
|
229
|
+
export interface AgentRecord {
|
|
230
|
+
kind: 'model' | 'mcp';
|
|
231
|
+
name: string;
|
|
232
|
+
placeholder: string;
|
|
233
|
+
state: 'ready' | 'expired' | 'refreshing';
|
|
234
|
+
provider?: string;
|
|
235
|
+
server?: string;
|
|
236
|
+
expiresAt?: EpochMs;
|
|
237
|
+
settings?: Record<string, string>;
|
|
210
238
|
}
|
|
211
239
|
export interface ScrubHit {
|
|
212
240
|
name: string;
|
|
@@ -334,12 +362,13 @@ export type Op = {
|
|
|
334
362
|
op: 'audit';
|
|
335
363
|
before?: number;
|
|
336
364
|
limit?: number;
|
|
337
|
-
} | {
|
|
365
|
+
} | ({
|
|
338
366
|
op: 'put';
|
|
339
367
|
name: string;
|
|
340
368
|
value: string;
|
|
369
|
+
kind?: 'secret' | 'model' | 'mcp';
|
|
341
370
|
request?: string;
|
|
342
|
-
} | {
|
|
371
|
+
} & RecordBinding) | {
|
|
343
372
|
op: 'cancel';
|
|
344
373
|
request: string;
|
|
345
374
|
} | {
|
|
@@ -350,7 +379,7 @@ export type Op = {
|
|
|
350
379
|
op: 'restore';
|
|
351
380
|
name: string;
|
|
352
381
|
kind?: RecordKind;
|
|
353
|
-
} | {
|
|
382
|
+
} | ({
|
|
354
383
|
op: 'seal';
|
|
355
384
|
name: string;
|
|
356
385
|
value: string;
|
|
@@ -361,7 +390,7 @@ export type Op = {
|
|
|
361
390
|
oauth?: OAuthConfig;
|
|
362
391
|
request?: string;
|
|
363
392
|
jerk?: JerkResult;
|
|
364
|
-
} | {
|
|
393
|
+
} & RecordBinding) | {
|
|
365
394
|
op: 'refresh';
|
|
366
395
|
name: string;
|
|
367
396
|
} | ({
|
|
@@ -426,8 +455,32 @@ export type AgentOp = {
|
|
|
426
455
|
stream: string;
|
|
427
456
|
} | {
|
|
428
457
|
op: 'endpoints';
|
|
429
|
-
model?: string[];
|
|
458
|
+
model?: (string | ModelEndpoint)[];
|
|
430
459
|
mcp?: (string | McpServerEntry)[];
|
|
460
|
+
} | {
|
|
461
|
+
op: 'model';
|
|
462
|
+
provider?: string;
|
|
463
|
+
request: OutgoingRequest;
|
|
464
|
+
} | {
|
|
465
|
+
op: 'mcp';
|
|
466
|
+
server: string;
|
|
467
|
+
request: OutgoingRequest;
|
|
468
|
+
} | {
|
|
469
|
+
op: 'records';
|
|
470
|
+
} | {
|
|
471
|
+
op: 'grant';
|
|
472
|
+
server: string;
|
|
473
|
+
response: string | Uint8Array;
|
|
474
|
+
oauth: OAuthConfig;
|
|
475
|
+
name?: string;
|
|
476
|
+
} | {
|
|
477
|
+
op: 'refresh';
|
|
478
|
+
server: string;
|
|
479
|
+
name?: string;
|
|
480
|
+
} | {
|
|
481
|
+
op: 'refresh';
|
|
482
|
+
provider: string;
|
|
483
|
+
name?: string;
|
|
431
484
|
};
|
|
432
485
|
export type AgentReply = {
|
|
433
486
|
op: 'redact';
|
|
@@ -451,6 +504,27 @@ export type AgentReply = {
|
|
|
451
504
|
} | {
|
|
452
505
|
op: 'endpoints';
|
|
453
506
|
ok: true;
|
|
507
|
+
} | {
|
|
508
|
+
op: 'model' | 'mcp';
|
|
509
|
+
request: OutgoingRequest;
|
|
510
|
+
hits: ScrubHit[];
|
|
511
|
+
version: number;
|
|
512
|
+
} | {
|
|
513
|
+
op: 'records';
|
|
514
|
+
records: AgentRecord[];
|
|
515
|
+
} | {
|
|
516
|
+
op: 'grant';
|
|
517
|
+
response: string | Uint8Array;
|
|
518
|
+
records: {
|
|
519
|
+
kind: RecordKind;
|
|
520
|
+
name: string;
|
|
521
|
+
placeholder: string;
|
|
522
|
+
}[];
|
|
523
|
+
expiresAt?: EpochMs;
|
|
524
|
+
} | {
|
|
525
|
+
op: 'refresh';
|
|
526
|
+
state: 'refreshed' | 'requested';
|
|
527
|
+
expiresAt?: EpochMs;
|
|
454
528
|
};
|
|
455
529
|
export type OpReply = {
|
|
456
530
|
op: 'sealed';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-vault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|