@ai-ecoverse/slicc-vault 1.3.0 → 1.5.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,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, 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.
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:** `import` and `export`, and 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
@@ -160,7 +160,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
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
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.
163
+ - **Values:** only `forward` (inside the head it returns, where an accepted rule allows it) and `materializeConsume` (for an `[exec]` grant) put a value into the page. No other op, event or reply carries one, and nothing on the agent port ever does.
164
164
 
165
165
  | op (page role) | fields | reply |
166
166
  |---|---|---|
@@ -177,6 +177,7 @@ 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`, `trusted` | the values for the child's environment, below (Exec) |
180
181
  | `import`, `export`, anything else | | `ENOTSUP` |
181
182
 
182
183
  `put` and `seal` need the vault unlocked. They answer `EACCES` with a message starting `vault is locked` otherwise.
@@ -203,11 +204,42 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
203
204
  | `request` | `kind: "jerk-seal"`, `provider` | the same |
204
205
  | `status` | `id` | `{ state }`, with `placeholder` and `record` once a value request is `done` |
205
206
  | `cancel` | `id` | `{ state }`: `cancelled`, or the state it had already reached |
206
- | `materialize` | `names`, `path`, `argv`, `cwd` | checked, then always `EACCES` in this version |
207
+ | `materialize` | `names` (or `shellGrants: true`), `path`, `argv`, `cwd`, `interpreter?` | `{ ticket, expiresAt, real: { NAME: true \| false } }`, below (Exec) |
207
208
 
208
209
  - **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`.
209
210
  - 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.
210
211
 
212
+ ## Exec
213
+
214
+ `fnox exec` (§3.5) asks for a one-shot ticket, and the kernel redeems it when it execs the child.
215
+
216
+ **`materialize`** (a `/dev/sealed` op) takes `names`, `path` (the absolute program fnox will exec), `argv` (exact, `argv[0]` included), `cwd`, and optionally `interpreter`: fnox's claim of the absolute shebang or binfmt interpreter of `path`, omitted for a native program. It's only a claim; the kernel's chain at consume decides. `shellGrants: true` (`fnox exec --shell-grants`) adds every `shell = true` secret to `names`, which may then be omitted.
217
+ - Each name must be a `secret` record (`ENOENT` otherwise) that hasn't expired (`EACCES: NAME expired; refreshing`). `real` says which will get their value: removed records and secrets without an `[exec]` grant stay `false` and keep their placeholder.
218
+ - **`true`** (exec `any`): refused with `EACCES` when `path`, or `interpreter`, is a shell (`sh`, `bash`, `dash`, `zsh`, `fish`, `busybox`, `jsh`), an interpreter (`python*`, `node`, `deno`, `bun`, `perl`, `ruby`, `php`, `lua`, `wasmer`, `awk` and `gawk`/`mawk`/`nawk`, `sed`, `tclsh`, `expect`, `R`, `Rscript`, `irb`) or an env dumper or wrapper (`env`, `printenv`, `xargs`, `sudo`, `su`, `nohup`, `timeout`, `time`, `nice`, `strace`), by basename.
219
+ - **`{ argv = [[…]] }`**: `real` only when `[path, ...argv[1..]]` starts with one of the listed prefixes. A listed shell or interpreter is allowed.
220
+ - **`{ shell = true }`**: always `real`, with no program refusal.
221
+ - **Limits,** per secret and uid, counted from the audit log: `materialize_per_minute` (10) and `materialize_per_day` (200), and `shell_per_day` (5,000, with no per-minute limit) for shell grants. Over a limit is `EAGAIN: materialize limit for NAME (10/min)`.
222
+ - A locked vault answers `EACCES` (`vault is locked`).
223
+ - 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
+ - The reply never carries a value. The ticket is 32 hex characters and expires after 30 s.
225
+
226
+ **`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:
227
+ - `stamp` is the exec'ing process.
228
+ - `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.
229
+ - `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).
230
+
231
+ The vault burns the ticket, then requires that:
232
+ - 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;
233
+ - `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);
234
+ - 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;
235
+ - under that name's mode, the chain passes:
236
+ - `true`: no entry of `path` and `interpreters` is on the refusal list, so a `#!/usr/bin/env node` script needs an `{ argv }` rule;
237
+ - `{ 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;
238
+ - `{ shell = true }`: any chain;
239
+ - no real record has expired meanwhile.
240
+
241
+ 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).
242
+
211
243
  ## Forward
212
244
 
213
245
  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:
@@ -363,6 +363,15 @@ export type Op = {
363
363
  owner: Stamp;
364
364
  exit: ExitName;
365
365
  rawTraits: RawTraits;
366
+ } | {
367
+ op: 'materializeConsume';
368
+ ticket: string;
369
+ stamp: Stamp;
370
+ path: string;
371
+ argv: string[];
372
+ cwd: string;
373
+ interpreters: string[];
374
+ trusted: boolean;
366
375
  };
367
376
  export type AgentOp = {
368
377
  op: 'redact';
@@ -445,7 +454,11 @@ export type OpReply = {
445
454
  op: 'policy';
446
455
  } & PolicyResult) | ({
447
456
  op: 'forward';
448
- } & ForwardResult);
457
+ } & ForwardResult) | {
458
+ op: 'materializeConsume';
459
+ values: Record<string, string>;
460
+ placeholders: Record<string, string>;
461
+ };
449
462
  export type Request = Op & {
450
463
  v: Protocol;
451
464
  id: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-vault",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",