@ai-ecoverse/slicc-vault 1.4.0 → 1.6.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 +51 -10
- package/dist/types/contract.d.ts +18 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
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 `endpoints` (S3), the model and MCP refusal, exec tickets (`materialize` and `materializeConsume`), and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
|
|
6
|
-
- **Not built yet:**
|
|
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, exec tickets (`materialize` and `materializeConsume`), `import` (backups, KeePass, six's `secrets.env`) and `export`, and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
|
|
6
|
+
- **Not built yet:** the agent's `model`, `mcp`, `records`, `grant` and `refresh`. They answer as described below.
|
|
7
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.
|
|
8
8
|
|
|
9
9
|
## Documents and hosting
|
|
@@ -18,7 +18,7 @@ The build (`npm run build`) writes a flat static tree to `dist/site/`:
|
|
|
18
18
|
| `vault.css`, `fonts/` | Spectrum 2 from the CSS-only `@spectrum-css` packages, and Adobe Clean |
|
|
19
19
|
| `config.json` | `{ version, embedders }`. `embedders` is the fallback list, `["https://seven.sliccy.ai"]` |
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The bundles hold this repo's code and `@ai-ecoverse/slicc-scrub`. The only third-party code is for KeePass imports: `kdbxweb` (with `@xmldom/xmldom` and `fflate`) and `hash-wasm`'s argon2, exact-pinned and bundled into a separate chunk under `chunks/` that the core frame loads only when a KeePass import starts; its wasm is inlined, never fetched. The sliccy-ai Worker serves the tree from the network on every load, never from OPFS, with:
|
|
22
22
|
- `Content-Security-Policy` with `script-src 'self'`, no inline scripts or styles, and `frame-ancestors` listing the seven origins;
|
|
23
23
|
- `Cross-Origin-Embedder-Policy: require-corp`, `Cross-Origin-Resource-Policy: same-site`, `Cross-Origin-Opener-Policy: same-origin`;
|
|
24
24
|
- `Origin-Agent-Cluster: ?1`, `nosniff`, `Referrer-Policy: no-referrer`, `no-cache` with an ETag.
|
|
@@ -71,7 +71,7 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
|
|
|
71
71
|
|
|
72
72
|
## Backup reminder
|
|
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).
|
|
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).
|
|
75
75
|
|
|
76
76
|
## Policy
|
|
77
77
|
|
|
@@ -177,8 +177,10 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
177
177
|
| `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
|
|
178
178
|
| `policy` | `files` | the attested policy files, below (Policy) |
|
|
179
179
|
| `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
|
|
180
|
-
| `materializeConsume` | `ticket`, `stamp`, `path`, `argv`, `cwd`, `interpreters` | the values for the child's environment, below (Exec) |
|
|
181
|
-
| `import`, `
|
|
180
|
+
| `materializeConsume` | `ticket`, `stamp`, `path`, `argv`, `cwd`, `interpreters`, `trusted` | the values for the child's environment, below (Exec) |
|
|
181
|
+
| `import` | `kind`, `bytes`, `password?`, `pick?` | below (Import and export) |
|
|
182
|
+
| `export` | `passphrase` | `{ bundle, count }`, below |
|
|
183
|
+
| anything else | | `ENOTSUP` |
|
|
182
184
|
|
|
183
185
|
`put` and `seal` need the vault unlocked. They answer `EACCES` with a message starting `vault is locked` otherwise.
|
|
184
186
|
|
|
@@ -209,6 +211,33 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
|
|
|
209
211
|
- **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`.
|
|
210
212
|
- 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.
|
|
211
213
|
|
|
214
|
+
## Import and export
|
|
215
|
+
|
|
216
|
+
Spectrum's Settings › Secrets reads the file and the password, and bios hands them over. Both need the vault unlocked. Like every page op, `export` trusts the page: under rev 4 seven's page is trusted with values in transit (§8), and a compromised seven origin is out of scope.
|
|
217
|
+
|
|
218
|
+
**`export { passphrase }`** returns `{ bundle: Uint8Array, count }`, a backup of the namespace's records of kind `secret`, `model` and `mcp` that aren't removed. Grants aren't included, because they need a fresh sign-in. The passphrase needs at least 12 characters. The bundle is UTF-8 JSON:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "format": "slicc-vault-bundle", "v": 1,
|
|
222
|
+
"kdf": { "name": "PBKDF2", "hash": "SHA-256", "iterations": 600000, "salt": "<base64url>" },
|
|
223
|
+
"cipher": { "name": "AES-GCM", "iv": "<base64url>" },
|
|
224
|
+
"data": "<base64url>" }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`data` is AES-GCM 256 (additional data `slicc-vault-bundle`) over `{ "records": [{ name, kind, value, placeholder, source, createdAt, expiresAt? }] }`. A successful export sets `status.exportedAt`.
|
|
228
|
+
|
|
229
|
+
**`import { kind, bytes, password?, pick? }`** runs in two steps:
|
|
230
|
+
- Without `pick` it answers `{ stage: 'pick', candidates: [{ name, kind, exists, suggestedRules?, refused? }] }`, for the human to choose from. `refused` says why a candidate can't be imported.
|
|
231
|
+
- With `pick` it seals the candidates picked, each given as `{ name, kind }` or, for kind `secret`, as just its name, and answers `{ stage: 'done', imported: [{ name, placeholder, state }], skipped }`. `skipped` lists picked names that were refused or not in the file, and `reasons` maps each to a short reason ("not a valid name", "values shorter than 8 bytes can't be protected from the model", "it isn't in the file", …), never a value or part of one. `outcomes` lists every picked candidate as `{ name, kind?, imported, reason? }`, which tells two candidates of the same name and different kinds apart (`kind` is absent only for a picked name that isn't in the file). Each record is stored in its own transaction, which also reserves a bundle's placeholder, so a record either lands whole or not at all; one that can't be stored (its placeholder was taken meanwhile, the vault was re-keyed) is listed in `skipped`, and the rest still import.
|
|
232
|
+
|
|
233
|
+
| `kind` | `password` | what becomes a candidate |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `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, as is a name whose record here has another placeholder |
|
|
236
|
+
| `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` |
|
|
237
|
+
| `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 |
|
|
238
|
+
|
|
239
|
+
Values are checked as at `put` (at least 8 bytes, no CR, LF or NUL, no placeholder). An existing name is rotated, keeping its placeholder. A wrong password or passphrase is `EACCES`.
|
|
240
|
+
|
|
212
241
|
## Exec
|
|
213
242
|
|
|
214
243
|
`fnox exec` (§3.5) asks for a one-shot ticket, and the kernel redeems it when it execs the child.
|
|
@@ -223,11 +252,22 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
|
|
|
223
252
|
- Each real name is audited as `materialize { name, exec, path, argv, argvSha256, cwd, pid, ppid, uid, tag, ticket, consumed: false }`. `argv` is joined with spaces, scrubbed, and cut to 200 characters; `argvSha256` is the SHA-256 of each argument followed by a NUL byte.
|
|
224
253
|
- The reply never carries a value. The ticket is 32 hex characters and expires after 30 s.
|
|
225
254
|
|
|
226
|
-
**`materializeConsume`** (a page op, from bios for the kernel at exec) takes `{ ticket, stamp, path, argv, cwd, interpreters }`,
|
|
255
|
+
**`materializeConsume`** (a page op, from bios for the kernel at exec) takes `{ ticket, stamp, path, argv, cwd, interpreters, trusted }`, all from the kernel and relayed unchanged:
|
|
256
|
+
- `stamp` is the exec'ing process.
|
|
257
|
+
- `interpreters` is the absolute, symlink-resolved shebang/binfmt chain the kernel resolved for `path`, empty for a native binary. A plain `#!/usr/bin/env X` (or `env -S`) is reported as the resolved `X`; `env` with any other option or a `NAME=value` assignment is reported as `env` itself.
|
|
258
|
+
- `trusted` is true when `path` and every chain entry are root-owned regular files, not group- or other-writable, under root-owned, non-writable parents (the attestation rule).
|
|
259
|
+
|
|
260
|
+
The vault burns the ticket, then requires that:
|
|
227
261
|
- it's unexpired and from this namespace, `stamp.ppid` is the pid that asked, and `path`, `argv` (element by element) and `cwd` are exactly the ones it was issued for;
|
|
228
|
-
- `interpreters` is a list of absolute paths
|
|
229
|
-
-
|
|
230
|
-
-
|
|
262
|
+
- `interpreters` is a list of absolute paths and `trusted` a boolean; a claimed `interpreter` from fnox equals `interpreters[0]` (without a claim, the kernel's chain is taken as it is);
|
|
263
|
+
- each real name is still granted by the policy as stored now (reloaded at materialize and at consume): a removed grant, or an `{ argv }` grant that no longer lists the command, refuses;
|
|
264
|
+
- under that name's mode, the chain passes:
|
|
265
|
+
- `true`: no entry of `path` and `interpreters` is on the refusal list, so a `#!/usr/bin/env node` script needs an `{ argv }` rule;
|
|
266
|
+
- `{ argv }`: the listed program is `path`, and whatever chain the kernel resolved for it is allowed, so a rule for `npm publish` allows `/usr/bin/npm`'s `node`. But when the chain holds a refused program and `trusted` is false, it refuses ("the script or its interpreter can be changed by a non-root user"), and `env` and the other wrappers in the chain always refuse;
|
|
267
|
+
- `{ shell = true }`: any chain;
|
|
268
|
+
- no real record has expired meanwhile.
|
|
269
|
+
|
|
270
|
+
Any failure is `EACCES: ticket refused: …`, and the ticket stays burned. It answers `{ values: { NAME: value }, placeholders: { NAME: placeholder } }` for the names that were real and still exist, and marks their audit entries `consumed`. The kernel swaps each env var whose value is exactly one of those placeholders, and compiles its own scrubber over these values for the child's output (S2).
|
|
231
271
|
|
|
232
272
|
## Forward
|
|
233
273
|
|
|
@@ -332,6 +372,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
|
|
|
332
372
|
- **`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.
|
|
333
373
|
- **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
|
|
334
374
|
- **`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.
|
|
375
|
+
- **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 …".
|
|
335
376
|
- **`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.
|
|
336
377
|
- **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
|
|
337
378
|
- **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -154,6 +154,8 @@ export interface PolicyEntry {
|
|
|
154
154
|
seq: number;
|
|
155
155
|
id: string;
|
|
156
156
|
at: EpochMs;
|
|
157
|
+
uid: null;
|
|
158
|
+
tag: null;
|
|
157
159
|
file: PolicyFile;
|
|
158
160
|
sha256: string;
|
|
159
161
|
accepted: boolean;
|
|
@@ -265,8 +267,10 @@ export interface SealResult {
|
|
|
265
267
|
export type ImportKind = 'bundle' | 'keepass' | 'six';
|
|
266
268
|
export interface ImportCandidate {
|
|
267
269
|
name: string;
|
|
270
|
+
kind: RecordKind;
|
|
268
271
|
exists: boolean;
|
|
269
272
|
suggestedRules?: string;
|
|
273
|
+
refused?: string;
|
|
270
274
|
}
|
|
271
275
|
export type ImportResult = {
|
|
272
276
|
stage: 'pick';
|
|
@@ -275,7 +279,15 @@ export type ImportResult = {
|
|
|
275
279
|
stage: 'done';
|
|
276
280
|
imported: SealResult[];
|
|
277
281
|
skipped: string[];
|
|
282
|
+
reasons?: Record<string, string>;
|
|
283
|
+
outcomes?: ImportOutcome[];
|
|
278
284
|
};
|
|
285
|
+
export interface ImportOutcome {
|
|
286
|
+
name: string;
|
|
287
|
+
kind?: RecordKind;
|
|
288
|
+
imported: boolean;
|
|
289
|
+
reason?: string;
|
|
290
|
+
}
|
|
279
291
|
export type PopupIntent = {
|
|
280
292
|
kind: 'unlock';
|
|
281
293
|
} | {
|
|
@@ -345,7 +357,10 @@ export type Op = {
|
|
|
345
357
|
kind: ImportKind;
|
|
346
358
|
bytes: Uint8Array;
|
|
347
359
|
password?: string;
|
|
348
|
-
pick?: string
|
|
360
|
+
pick?: (string | {
|
|
361
|
+
name: string;
|
|
362
|
+
kind: RecordKind;
|
|
363
|
+
})[];
|
|
349
364
|
} | {
|
|
350
365
|
op: 'export';
|
|
351
366
|
passphrase: string;
|
|
@@ -371,6 +386,7 @@ export type Op = {
|
|
|
371
386
|
argv: string[];
|
|
372
387
|
cwd: string;
|
|
373
388
|
interpreters: string[];
|
|
389
|
+
trusted: boolean;
|
|
374
390
|
};
|
|
375
391
|
export type AgentOp = {
|
|
376
392
|
op: 'redact';
|
|
@@ -444,6 +460,7 @@ export type OpReply = {
|
|
|
444
460
|
} & ImportResult) | {
|
|
445
461
|
op: 'export';
|
|
446
462
|
bundle: Uint8Array;
|
|
463
|
+
count: number;
|
|
447
464
|
} | {
|
|
448
465
|
op: 'lock';
|
|
449
466
|
} | {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-vault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"esbuild": "0.28.2",
|
|
58
58
|
"fake-indexeddb": "6.2.5",
|
|
59
59
|
"happy-dom": "20.14.5",
|
|
60
|
+
"hash-wasm": "4.12.0",
|
|
61
|
+
"kdbxweb": "2.1.1",
|
|
60
62
|
"lefthook": "2.1.17",
|
|
61
63
|
"monocart-coverage-reports": "2.13.0",
|
|
62
64
|
"playwright-core": "1.63.0",
|