@ai-ecoverse/slicc-vault 1.13.0 → 1.15.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 +15 -5
- package/dist/types/contract.d.ts +22 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
174
174
|
| `placeholders` | — | `{ placeholders, scrub }`: every placeholder of a record that isn't removed, active and inert, sorted |
|
|
175
175
|
| `list` | — | `{ list: { secrets, refused, audit, auditMore, exits } }` for Settings › Secrets |
|
|
176
176
|
| `audit` | `before?` (a `seq`), `limit?` (1–1000, default 100) | `{ audit, more, next? }`, newest first; pass `next` as `before` for the next page |
|
|
177
|
-
| `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` |
|
|
177
|
+
| `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. Without `value`, for an existing `model` or `mcp` record, it only updates `provider`, `server` or `settings` (say, a new Cloudflare account id), keeping the value and placeholder (`ENOENT` if there's no such live record; like every `put`, it needs the vault unlocked), audited as a `seal` entry with `rebound: true`; a new `server` clears a first-use pin, and a new `provider` drops the record's custom base URLs. 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` |
|
|
178
178
|
| `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) |
|
|
179
179
|
| `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 |
|
|
180
180
|
| `refresh` | `name` | `{ state: 'refreshed', expiresAt? }` or `{ state: 'requested' }`, below |
|
|
@@ -183,7 +183,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
183
183
|
| `lock` | — | `{}`; drops the key everywhere when the lock is on |
|
|
184
184
|
| `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
|
|
185
185
|
| `policy` | `files` | the attested policy files, below (Policy) |
|
|
186
|
-
| `endpoints` | `model?`: https base URLs, or `{ name, url }`, a custom base URL for the `model` record `name`; `mcp?`: as the agent's; `remove?`: `{ model?: (url \| { name, url })[], mcp?: { server, url }[] }` | `{ ok: true }`; the user's own registrations, trusted (Model and MCP endpoints). The whole request is checked first and applied in one transaction, so an `EINVAL` anywhere changes nothing. `unpin?`: names of `mcp` records to unpin (needs the vault unlocked). `remove` takes back the page's own entries, never the agent's; removing the model record drops its custom URLs too. A `{ name, url }` needs a live `model` record of that name, and a plain `https:` URL on 443 without `*` or `{…}` and not under a page-registered MCP server, else `EINVAL` |
|
|
186
|
+
| `endpoints` | `model?`: https base URLs, or `{ name, url }`, a custom base URL for the `model` record `name`; `mcp?`: as the agent's; `remove?`: `{ model?: (url \| { name, url })[], mcp?: { server, url }[] }` | `{ ok: true }`; the user's own registrations, trusted (Model and MCP endpoints). The whole request is checked first and applied in one transaction, so an `EINVAL` anywhere changes nothing; a field other than `model`, `mcp`, `remove` and `unpin` is `EINVAL` too. `unpin?`: names of `mcp` records to unpin (needs the vault unlocked). `remove` takes back the page's own entries, never the agent's; removing the model record drops its custom URLs too. A `{ name, url }` needs a live `model` record of that name, and a plain `https:` URL on 443 without `*` or `{…}` and not under a page-registered MCP server, else `EINVAL` |
|
|
187
187
|
| `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
|
|
188
188
|
| `materializeConsume` | `ticket`, `stamp`, `path`, `resolved`, `argv`, `cwd`, `interpreters`, `trusted` | the values for the child's environment, below (Exec) |
|
|
189
189
|
| `sessionMaterialize` | `stamp`, `session`, `host`, `port`, `protocol`, `position`, `verified`, `handshakeAt`, `exit`, `verifiedBy?`, `anchor?`, then `placeholders` (and `head` for http1) at every position but `scram-sha-256`, which takes `scram` and no `placeholders` | `{ values }` for a kernel-verified TLS session, below (Sessions) |
|
|
@@ -360,7 +360,8 @@ The candidates are the placeholders that would pass the per-placeholder checks a
|
|
|
360
360
|
## Model and MCP endpoints
|
|
361
361
|
|
|
362
362
|
The vault never injects a secret into a model or MCP endpoint, whatever a rule says.
|
|
363
|
-
- **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`, the version slicc-agent runs), plus `bedrock-runtime.*` and `bedrock-runtime-fips.*.amazonaws.com`, Vertex AI's global `aiplatform.googleapis.com` (`google-vertex`), Azure OpenAI, AI services and Cognitive Services' OpenAI path (`*.openai.azure.com`, `*.services.ai.azure.com`, `*.cognitiveservices.azure.com/openai`, so Speech, Translator and the other Cognitive Services stay usable by rules), Adobe's LLM proxy (for the provider `adobe`), and the model base URLs pi-ai has retired but that still serve models (`opencode.ai/zen`, `api.kimi.com/coding`): the refused set never shrinks when pi-ai drops a URL, though no key goes there. `*` is exactly one label. The published package's `piAi` field in `package.json` names the pi-ai version the vault was built against, so slicc-agent can check that both run the same catalog; Renovate bumps it with the pin, labelled `pi-ai-lockstep` and never automerged. Each entry is a host (with `*` for one label) and a path prefix (`*` for one segment, from templated segments).
|
|
363
|
+
- **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`, the version slicc-agent runs), plus `bedrock-runtime.*` and `bedrock-runtime-fips.*.amazonaws.com`, Vertex AI's global `aiplatform.googleapis.com` (`google-vertex`), Azure OpenAI, AI services and Cognitive Services' OpenAI path (`*.openai.azure.com`, `*.services.ai.azure.com`, `*.cognitiveservices.azure.com/openai`, so Speech, Translator and the other Cognitive Services stay usable by rules), Adobe's LLM proxy (for the provider `adobe`), and the model base URLs pi-ai has retired but that still serve models (`opencode.ai/zen`, `api.kimi.com/coding`): the refused set never shrinks when pi-ai drops a URL, though no key goes there. `*` is exactly one label (or segment). The published package's `piAi` field in `package.json` names the pi-ai version the vault was built against, so slicc-agent can check that both run the same catalog; Renovate bumps it with the pin, labelled `pi-ai-lockstep` and never automerged. Each entry is a host (with `*` for one label) and a path prefix (`*` for one segment, from templated segments).
|
|
364
|
+
- **Tenants (V3):** a built-in wildcard named after a tenant setting is bound to the key's own record. `gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/…`, `api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai…` and the Azure hosts `{AZURE_OPENAI_RESOURCE_NAME}.openai.azure.com`, `.services.ai.azure.com` and `.cognitiveservices.azure.com/openai` fill a key only where each label or segment equals the record's `settings` entry of that name (Azure's host label case-insensitively, Cloudflare's path segments exactly), when the record has one. A record without them still reaches any tenant on that vendor's domain, as before; re-saving its `settings` binds it. Other wildcards (Bedrock's region, Vertex's location) aren't tenants and stay open.
|
|
364
365
|
- **Registered endpoints** only ever add, per namespace, and who registers them decides what they do:
|
|
365
366
|
- **The page's `endpoints` op** (bios and spectrum, for what the user typed in Settings) adds model base URLs, a custom base URL for one model record as `{ name, url }` (an Azure APIM gateway or a Cloudflare gateway the owner typed for that account, say), and MCP servers. These are trusted: they join the refused set below. A `{ name, url }` is the only way a key reaches a URL outside the built-in list, and only that record's key: another record of the same provider never goes there. The vault stores the record's provider with it, and drops it when the record is removed.
|
|
366
367
|
- **The agent's `endpoints` op** adds MCP servers, which only say where the agent's own `mcp` credentials may go (below). The vault never sets an agent-supplied `tokenUrl` for a server name a typed-in token is bound to. Once the page has registered a server name, every agent entry under that name is ignored, stored or new: the agent can't add to its `names`, set its `tokenUrl`, or register it at another URL. Its model base URLs and `{ provider, url }` entries are kept as URLs only: they never let a key go anywhere, and never refuse a secret rule. So a compromised agent worker can neither send a provider's key to a URL of its choosing nor make a user's secret rules inert by registering their host.
|
|
@@ -395,7 +396,7 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
|
|
|
395
396
|
| `model` | `provider?`, `request`: `{ method, url, headers: [name, value][], body?: string \| bytes }` | `{ request, hits, version }`, the request prepared for the agent's transport, below |
|
|
396
397
|
| `mcp` | `server`, `request` (the same shape) | `{ request, hits, version }`, below |
|
|
397
398
|
| `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 |
|
|
398
|
-
| `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 |
|
|
399
|
+
| `grant` | `provider`, `name`, `response`, `settings?` for a model-provider sign-in (below), or `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 |
|
|
399
400
|
| `refresh` | `server` or `provider`, `name?` | `{ state: 'refreshed', expiresAt? }`: refreshes that server's or provider's token in the vault, as Tokens and refresh describes (an `expiresAt` is left out when its digits are a value before or after the refresh, the replaced and the new token included, or when any other value changed while it ran); `ENOENT` when it has none, `EINVAL` when it has several and no `name` picks one |
|
|
400
401
|
|
|
401
402
|
**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.
|
|
@@ -410,9 +411,18 @@ The vault never injects a secret into a model or MCP endpoint, whatever a rule s
|
|
|
410
411
|
|
|
411
412
|
**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.
|
|
412
413
|
- `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.
|
|
413
|
-
- 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.
|
|
414
|
+
- 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. A live `<name>_GRANT` that isn't this token's own (a sign-in's, say) is `EEXIST`, checked in the writing transaction; a re-grant without a `refresh_token` removes the token's old grant unless another live credential shares it.
|
|
414
415
|
- 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.
|
|
415
416
|
|
|
417
|
+
**Model-provider sign-ins (`grant { provider }`, §3.12.1).** For the subscription sign-ins `xai`, `kimi-coding` and `openai-codex`, the worker runs the device-code flow and hands the vault the final token response: `grant { provider, name, response, settings? }`, with `name` the account's record name. The worker holds that response only in memory and never logs, stores, licks or transcribes it.
|
|
418
|
+
- **The token endpoint and client id come from the vault's own table** (`src/providers.ts`, taken from pi-ai's OAuth modules and checked against the installed pi-ai by a test): `https://auth.x.ai/oauth2/token`, `https://auth.kimi.com/api/oauth/token`, `https://auth.openai.com/oauth/token`. A `server` or `oauth` in the request is `EINVAL`, and any other provider is `ENOTSUP`.
|
|
419
|
+
- **The response** is checked like an MCP grant's, except that a missing `token_type` is taken as `Bearer` (none of these providers' clients reads it); one that's present and not `Bearer` is still refused.
|
|
420
|
+
- **The records:** the access token becomes a `model` record of `provider`, source `oauth:<provider>`, with its `expiresAt`; the refresh token, if any, a `grant` record `<name>_GRANT` that refreshes through `egress` as usual. Both are written in one transaction. The reply is `{ records: [{ kind, name, placeholder }], expiresAt? }`.
|
|
421
|
+
- **Its name and settings** (its own or, without any, those it inherits from the name's record, removed or not, checked in the writing transaction, which is `EAGAIN` if a value was sealed meanwhile) may hold neither a returned token nor a stored value (`EINVAL`), and `<name>_GRANT` must be free or this record's own grant, checked in the transaction that writes them (`EEXIST`); a removed grant still counts as another credential's while that removed credential can be restored. Over an earlier OAuth key whose grant has another name, that grant is removed unless shared. `response` is a string or bytes (any `ArrayBufferView`).
|
|
422
|
+
- **Under an existing name:** a record of another provider, even a removed one, is `EEXIST`. An earlier sign-in of the same provider is rotated, keeping its placeholder; if the new response has no `refresh_token`, the old grant is removed with it unless another live credential still refreshes from it. A key the user pasted (any source but `oauth:`), live or removed within the 30 days it can still be restored (one removed longer ago is gone, whatever its provider), is kept restorable: the sign-in replaces it (same placeholder), and `restore` of that name brings the pasted key back with its own source, settings, expiry and refresh link, and removes the sign-in's grant (unless shared). `list` marks such a record `replaced: true`. The stashed key stays encrypted under the vault key (re-sealed when the lock changes), is scrubbed like any value, and is what `export` writes for that name with the pasted key's own settings and expiry (a backup carries the pasted key, not the sign-in).
|
|
423
|
+
- **Remove** of the `model` record removes its grant too; `restore` restores both (for a record without a pasted key underneath).
|
|
424
|
+
- **Refresh failures:** the provider refusing the refresh is `EACCES "refresh failed: <code>"`; after `invalid_grant`, the grant is dead (not asked again), `records` shows the key `expired` and it isn't filled any more (`EACCES` "NAME expired; sign in again"), whatever its `expiresAt`, until a new sign-in. A network failure (`ENOTCONN`, `ETIMEDOUT`) keeps it `refreshing`.
|
|
425
|
+
|
|
416
426
|
**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").
|
|
417
427
|
|
|
418
428
|
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.
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ export interface SecretView {
|
|
|
90
90
|
server?: string;
|
|
91
91
|
url?: string;
|
|
92
92
|
pinned?: true;
|
|
93
|
+
replaced?: true;
|
|
93
94
|
settings?: Record<string, string>;
|
|
94
95
|
domains: string[];
|
|
95
96
|
rules: Rule[];
|
|
@@ -205,6 +206,7 @@ export interface SealEntry extends AuditBase {
|
|
|
205
206
|
type: 'seal';
|
|
206
207
|
source: RecordSource;
|
|
207
208
|
rotated: boolean;
|
|
209
|
+
rebound?: true;
|
|
208
210
|
}
|
|
209
211
|
export interface RemoveEntry extends AuditBase {
|
|
210
212
|
type: 'remove' | 'restore';
|
|
@@ -451,6 +453,12 @@ export type Op = {
|
|
|
451
453
|
value: string;
|
|
452
454
|
kind?: 'secret' | 'model' | 'mcp';
|
|
453
455
|
request?: string;
|
|
456
|
+
} & RecordBinding) | ({
|
|
457
|
+
op: 'put';
|
|
458
|
+
name: string;
|
|
459
|
+
value?: never;
|
|
460
|
+
kind: 'model' | 'mcp';
|
|
461
|
+
request?: never;
|
|
454
462
|
} & RecordBinding) | {
|
|
455
463
|
op: 'cancel';
|
|
456
464
|
request: string;
|
|
@@ -597,6 +605,12 @@ export type AgentOp = {
|
|
|
597
605
|
response: string | Uint8Array;
|
|
598
606
|
oauth: OAuthConfig;
|
|
599
607
|
name?: string;
|
|
608
|
+
} | {
|
|
609
|
+
op: 'grant';
|
|
610
|
+
provider: 'xai' | 'kimi-coding' | 'openai-codex';
|
|
611
|
+
name: string;
|
|
612
|
+
response: string | Uint8Array;
|
|
613
|
+
settings?: Record<string, string>;
|
|
600
614
|
} | {
|
|
601
615
|
op: 'refresh';
|
|
602
616
|
server: string;
|
|
@@ -645,6 +659,14 @@ export type AgentReply = {
|
|
|
645
659
|
placeholder: string;
|
|
646
660
|
}[];
|
|
647
661
|
expiresAt?: EpochMs;
|
|
662
|
+
} | {
|
|
663
|
+
op: 'grant';
|
|
664
|
+
records: {
|
|
665
|
+
kind: RecordKind;
|
|
666
|
+
name: string;
|
|
667
|
+
placeholder: string;
|
|
668
|
+
}[];
|
|
669
|
+
expiresAt?: EpochMs;
|
|
648
670
|
} | {
|
|
649
671
|
op: 'refresh';
|
|
650
672
|
state: 'refreshed' | 'requested';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-vault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.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.3.0",
|
|
6
6
|
"type": "module",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@ai-ecoverse/jerk-oauth": "1.0.0",
|
|
51
|
-
"@ai-ecoverse/slicc-kernel": "1.
|
|
51
|
+
"@ai-ecoverse/slicc-kernel": "1.132.1",
|
|
52
52
|
"@ai-ecoverse/slicc-scrub": "1.0.0",
|
|
53
|
-
"@ai-ecoverse/slicc-shared-web": "1.
|
|
54
|
-
"@ai-ecoverse/wasm-bash": "5.3.0-
|
|
53
|
+
"@ai-ecoverse/slicc-shared-web": "1.25.1",
|
|
54
|
+
"@ai-ecoverse/wasm-bash": "5.3.0-21",
|
|
55
55
|
"@ai-ecoverse/wasm-coreutils": "9.12.0-10",
|
|
56
56
|
"@biomejs/biome": "2.5.15",
|
|
57
57
|
"@earendil-works/pi-ai": "1.3.0",
|