npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ai-ecoverse/slicc-vault

v1.20.0

Published

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

Readme

slicc-vault

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.

  • 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), sessionMaterialize for kernel-verified TLS sessions, 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.
  • 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.

Documents and hosting

The build (npm run build) writes a flat static tree to dist/site/:

| file | what it is | |---|---| | index.html, frame.js | the core frame. bios embeds it as a hidden iframe and talks to it over a MessagePort | | button.html, button.js | the lock button. A popup can only open from a click inside a vault document, so bios and spectrum embed this wherever the lock is offered | | popup.html, popup.js | the vault window, a top-level vault.sliccy.ai document, used only for the passkey or passphrase lock | | vault.css, fonts/ | Spectrum 2 from the CSS-only @spectrum-css packages, and Adobe Clean | | config.json | { version, embedders }. embedders is vault.sliccy.ai's fallback list, ["https://seven.sliccy.ai"] |

The bundles hold this repo's code, @ai-ecoverse/slicc-scrub, and @ai-ecoverse/jerk-oauth (exact-pinned, for token refresh): its wasm is inlined into a separate chunk under chunks/ (about 970 KB) that loads only on the first refresh or MCP grant, never fetched. 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:

  • Content-Security-Policy with script-src 'self', no inline scripts or styles, and frame-ancestors listing the seven origins;
  • Cross-Origin-Embedder-Policy: require-corp, Cross-Origin-Resource-Policy: same-site, Cross-Origin-Opener-Policy: same-origin;
  • Origin-Agent-Cluster: ?1, nosniff, Referrer-Policy: no-referrer, no-cache with an ETag, and no-store on the HTML documents.

To stay out of the back/forward cache, index.html and button.html add an unload listener, which keeps an embedder's page out of it too, and all three documents reload on a pageshow with persisted; bfcache.test.mjs proves index.html is never restored.

At start, a vault document that is the top-level document (popup.html, or index.html or button.html opened as a tab) calls navigator.mediaDevices.setCaptureHandleConfig({ handle: 'slicc-vault', exposeOrigin: true, permittedOrigins }), with permittedOrigins the embedders the document loaded that are valid exact origins, serialized (a :* source or an invalid one is left out; none means an empty list, never '*'), so seven sees the handle and refuses to capture a vault tab. A framed document, or a browser without the API, sets none.

GET /embedders.json is answered by the Worker, not by this tree. It returns { "embedders": [...] }, the same sources as frame-ancestors. The vault reads it at start and accepts only those origins. When it's missing or malformed, only vault.sliccy.ai falls back to config.json; on every other host (vault-preview.sliccy.ai, branch previews, local) that means no embedders, so it fails closed. An embedder source is http(s)://host, optionally with :port or :*, matched exactly.

main is served at vault.sliccy.ai, and every other branch at vault-<label>.sliccy.ai. CI is one caller workflow, ci.yml, that runs slicc-shared-web's pipeline.yml at the same v2 tag as the @ai-ecoverse/slicc-shared-web devDependency, with secrets: inherit, on pull requests, on the merge queue (merge_group) and on main: lint everywhere; on pull requests and in the merge queue the integration tests (test/integration/*.test.mjs, split by timings across the pipeline's shards, Chrome for Testing from slicc-cdp install-browser, headed on Xvfb); on pull requests a Claude and a Codex review; on main semantic-release, with no tests. ci / integration and ci / review are the checks to require. The pipeline picks its own runners (its self-hosted pool, a hosted runner for a fork's pull request and for the release), and a pull request from a fork gets no review. Releases run only on main: the pipeline's release job runs only on a push to it, in the production environment, and publishes vault.sliccy.ai there. On a pull request the pipeline's deploy job publishes the branch preview in the staging environment and hands its URL to the tests and reviewers as STAGING_URL; it publishes nothing to npm and makes no tag or release, and the merge queue deploys nothing. ci.yml is the only workflow, so nothing deploys outside the pipeline. No workflow names a self-hosted runner label. workflows.test.mjs keeps all of this so. seven embeds only vault.sliccy.ai.

Storage and keys

Everything lives in the vault origin's IndexedDB, database slicc-vault, version 1:

| store | key | holds | |---|---|---| | meta | "keys" | the key state below | | records | [ns, kind, name], unique index on placeholder | records | | requests | id | requests that wait for a human | | audit | seq (auto-increment) | the audit log, per namespace | | intents | nonce | hand-offs from a button to the popup, valid for 10 minutes |

The data key is one AES-GCM 256 key. Each record's value is encrypted under it with a fresh 12-byte IV and the additional data slicc-vault/1 NUL ns NUL kind NUL name, so a ciphertext can't be moved to another record.

The key state is one of:

  • auto (the default): the data key is stored as a non-extractable CryptoKey. Every vault document loads it at start, so there's never a prompt.
  • passkey: the data key is stored only wrapped (AES-GCM) under a key-encryption key from WebAuthn PRF. The passkey's RP id is the vault's host, the PRF input is a random 32-byte salt, and the KEK is HKDF-SHA-256 over the PRF output with that salt and the info slicc-vault/kek/1.
  • passphrase: the same, with the KEK from PBKDF2-SHA-256, 600,000 iterations, over a random 16-byte salt.

Locking:

  • While protected, a document starts locked and asks the other open vault documents (over the origin's BroadcastChannel slicc-vault) for the unwrapped key. Unlocking in the popup unwraps the key as non-extractable and hands it to every open vault document the same way.
  • Lock now drops the key from every document. Closing every vault document has the same effect.
  • Protecting makes a new data key, re-encrypts every record under it, and stores it wrapped, in one transaction. If a record changed meanwhile, protecting fails with EAGAIN and changes nothing. Removing protection stores the current key as non-extractable again.
  • Every data key has an id, stored with it. A seal checks that id in the same transaction that writes the record, so a value encrypted under a key that was replaced meanwhile is refused with EAGAIN rather than stored unreadable.
  • While locked, seal answers EACCES with a message starting vault is locked. Ops that need no value (placeholder, list, status, ...) still work.

Records

{ ns, kind: "secret" | "grant" | "model" | "mcp", name, placeholder, sealed: { iv, data },
  source, createdAt, updatedAt, usedAt?, expiresAt?, refresh?, oauth?, provider?, server?, settings?, url?, sealedBy?, removedAt?, removedWith? }
  • ns is the embedder's origin, below.
  • name matches ^[A-Za-z_][A-Za-z0-9_]*$, at most 128 characters.
  • 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.
  • 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.
  • 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. In list, a grant shows the server or provider of the live tokens of the same source that name it as their refresh, when they all agree (and none when they don't, whatever the grant stored), for display and lookup only; nothing is copied onto the grant. 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".
  • source is settings (entered in spectrum), process, jerk:<provider>@<device>, oauth:<provider> or import:<kind>.
  • state is computed, never stored: removed when removedAt is set, active when the accepted policy has a rule for the record (below), otherwise inert.
  • 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. Removing a token removes the grant its refresh names (when the grant has the token's own source, and no other live token of that source still names it) in the same transaction, and restoring it restores that grant too, only when that grant was removed by this token's removal (the vault records which token took it as removedWith, <kind>\u0000<name> of that token, cleared on restore) and is still within the 30 days; the one remove or restore audit entry names it as grant. Removing an already removed record, or restoring a live one, succeeds and changes nothing (its 30 days don't restart, and nothing is audited).
  • Times are epoch milliseconds everywhere.

Backup reminder

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).

Policy

The rules come only from two root-owned kernel files. bios reads them with the kernel's page-only kernel.attest(path) (KS-7), at boot and whenever either file changes, and passes the results through unchanged:

port.postMessage({ v: 1, id, op: 'policy', files: [await kernel.attest('/etc/fnox/config.toml'), await kernel.attest('/etc/slicc/secrets-policy.toml')] });

An attestation is exactly { path, bytes, uid, gid, mode, ino, mtimeMs, sha256, users, onRootFs, parents }, nothing more or less:

| field | format | |---|---| | path | the canonical absolute path the kernel read (it refuses symlinks and mount points) | | bytes | the file's bytes, a Uint8Array, read in the same step as the stats | | uid, gid, mode | integers; mode includes the type bits (0o100644) | | ino, mtimeMs | numbers | | sha256 | 64 lower-case hex characters | | users | the kernel's users mode: off, report or enforce | | onRootFs | true only on the root file system, under no mount | | parents | every directory from / down to the file's, each exactly { path, uid, gid, mode } |

The vault trusts a file only when:

  • path is /etc/fnox/config.toml or /etc/slicc/secrets-policy.toml, and the object has exactly the fields above in those formats;
  • the SHA-256 the vault computes over bytes equals sha256 (the field alone is never trusted);
  • uid is 0, and mode is a regular file with mode & 0o022 of 0;
  • parents lists exactly the directories above path, in order from /, each with uid 0, a directory type and mode & 0o022 of 0;
  • onRootFs is true, and users is enforce;
  • bytes is UTF-8 and parses.

Otherwise the vault keeps the policy it last accepted for that file, so the rules it would have changed stay as they were (new ones stay inert), and warns, e.g. rules not trusted: /etc/fnox/config.toml: kernel users must be enforce. The reply is { accepted, files: [{ path, sha256, accepted, warnings, absent?, paused? }], rules: [{ secret, domain, header, methods, paths, protocol?, position?, port? }], warnings }, with rules the rules now in effect. status.policy repeats files from the last policy call, so Settings can show why rules aren't trusted.

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.

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.

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.

/etc/fnox/config.toml, the injection policy, read the way fnox reads it:

  • [secrets]: entries whose provider (or the file's default_provider) is seven, and whose value is a placeholder.
  • [[proxy.rules]]: secret (a seven entry of [secrets]), domain (one exact host, lower-cased), header (lower-cased, default authorization), methods (upper-cased; empty means any) and paths (each starting with /; empty means any). A rule that breaks any of this is dropped with a warning; the rest of the file still counts. Rules are numbered from 1 in file order, dropped ones included; a session audit entry names its rule by that number.
  • Session rules (§3.19) add protocol and port (an integer from 1 to 65535), and are used only by sessionMaterialize, never by forward; a rule without protocol is used only by forward:
    • protocol = "http1": header, methods and paths as above; the position is the header. A position is a warning.
    • protocol = "smtp", with position = "auth-plain" or "auth-login", or protocol = "pg", with position = "password-cleartext" or "scram-sha-256" (each allowed only where a rule names it): no header, methods or paths (a warning otherwise). Its header reads ''.
    • Trust anchors (§3.19.6), on any session rule: anchors (base64 DER certificates, each starting with a SEQUENCE and at most 8 KB), pins (base64 SHA-256 of a public key, 32 bytes) and roots (true or false), at most 8 anchors and 8 pins. A rule with anchors or pins accepts only sessions the kernel verified against them, never the public roots, unless it says roots = true; a rule without them accepts the public roots, and roots = false there is a warning. The vault checks only their format: the kernel checks that an anchor is a CA (CA:true and keyCertSign) and the chain. Like every rule, they come only from the attested root-owned file, so only an approved sudo write sets them: no program, agent or page op can.
    • port, position, anchors, pins or roots without protocol, another protocol, or a missing or wrong port, position, anchors, pins or roots is a warning, and the rule is dropped.
  • A record of kind secret is active when the file's [secrets] entry of its name holds the record's own placeholder and at least one rule names it. A placeholder from another vault or an old record doesn't activate anything.
  • Nothing else in the file counts: no imports, profiles or other providers.

/etc/slicc/secrets-policy.toml, read by the vault only:

[exec]
NPM_TOKEN = true
GITHUB_TOKEN = { argv = [["/usr/bin/gh", "release", "upload"]] }
OPENAI_API_KEY = { shell = true }

[exits]
NPM_TOKEN = ["page", "device:7d1e4b0a9c2f5e83"]

[limits]
materialize_per_minute = 10
materialize_per_day = 200
shell_per_day = 5000
  • [exec]: false is an explicit off, true is exec: 'any', exactly { argv = [[…]] } is 'argv' (each prefix an absolute program path), and exactly { shell = true } is 'shell'. Anything else, a table with other or extra keys included, is a warning, and exec stays false. VAULT-1d acts on them.
  • [exits] is optional: a secret without a line may use every exit; a line restricts it to the exits listed (page, proxy, tailscale, device:<id>). <id> is the device's DeviceId, the stable id of its link certificate (a tray's 64 hex characters, or local:… for a CLI linked on the same computer), never its display name, which can be renamed or shared by two devices: one or more of A-Z, a-z, 0-9, ., _, : and -. An entry is compared exactly with the exit bios sends. Unknown names are dropped from the line with a warning, which leaves the line narrower, never wider.
  • [limits] overrides the defaults above with positive integers.

Accepted policy is kept per namespace in IndexedDB, and loaded when an embedder says hello. Each file is audited once per SHA-256 and verdict (the vault remembers the last 64 per file) as a policy entry, with accepted, any warning, and its changes against the previously accepted file: rules added or removed, exec modes, exits lines and limits. A pause and its end are always audited: the entry that starts one has absent: true and sha256: null. Any change to the accepted policy, or a new audit entry, pushes changed with secrets, exits and audit, and other vault documents of the namespace reload the policy. Each policy call reads, merges and writes the stored policy in one transaction, so two pages can't undo each other's update.

Scrub state

scrub { needed, version, ready } tells bios and the agent whether output must pass the vault's scrubber before it reaches the model. It's per namespace:

  • needed is false only when the namespace holds no plaintext at all: no record of any kind or state, removed ones included until they're purged.
  • version counts every change to the namespace's set of values: each seal, rotation, removal, restore and purge.
  • ready is false only when values exist, the vault is locked, and this frame has no compiled scrubber for the namespace: a reload with the lock on, before unlock. The scrubber (@ai-ecoverse/slicc-scrub, over every value of the namespace, records of every kind and state) is compiled whenever the frame holds the key and the set of values changed, and is kept in memory for the frame's life, so locking stops value use but never scrubbing.

It's in status, in placeholders replies and in every placeholders push. After put and seal, the push with the new pair goes out before the reply.

Namespaces

Every record, request, audit entry and scrub version belongs to the origin that embeds the vault. The core frame takes it from the event.origin of the hello, and the button from location.ancestorOrigins[0] (or the first message's origin when the browser hides ancestors). A branch preview of bios therefore gets its own empty vault and never sees seven's records. The popup shows the namespace of the button that opened it. The lock itself is one per vault.

The embedding API (core frame)

The embedder (bios) loads https://vault.sliccy.ai/ in an iframe and, after load, posts a hello carrying a port:

iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role: 'page', sudoGid }, 'https://vault.sliccy.ai', [port2]);
  • 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.
  • On port2 it answers { v: 1, op: 'welcome', role, status }, or { v: 1, op: 'refused', error } for another protocol version or an unknown role.
  • 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).
  • 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).
  • 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.

| op (page role) | fields | reply | |---|---|---| | sealed | req (the /dev/sealed request object, or its JSON text), stamp (the kernel's { pid, ppid, uid, gid, tag, users, groups?, user? }) | { reply }, the §3.7 reply, below | | status | — | { status: { version, protocol, capabilities, origin, locked, lock, scrub, waiting, exportedAt, backupReminder, policy } }. capabilities is { forward: 1 }, in every status (the welcome's, this reply's and the status event's): every credentialed forward needs a manual exit, an MCP one a complete scrubbed body, its response is scrubbed, and a trusted missing policy file revokes. Without it, bios must not forward | | placeholders | — | { placeholders, scrub }: every placeholder of a record that isn't removed, active and inert, sorted | | list | — | { list: { secrets, mcp, refused, audit, auditMore, exits } } for Settings › Secrets. mcp is [{ name, server, state }], sorted by name, the mcp records bound to a server that can be used now: not removed, and state ready or refreshing (expired with a refresh source). An expired one with no refresh source or with a dead grant, a bridge token (source process), one with no server, one whose mcp:<server> is longer than a request's 128-character provider, and one whose name or server holds a value are left out, and so is every record while the vault is locked before its first unlock | | audit | before? (a seq), limit? (1–1000, default 100) | { audit, more, next? }, newest first; pass next as before for the next page | | 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, except over a process record that isn't a secret, which gets a new one (below, seal in /dev/sealed). 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 | | 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) | | 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 | | refresh | name | { state: 'refreshed', expiresAt? } or { state: 'requested' }, below | | remove, restore | name, kind? (default secret) | { name, placeholder, state }. A record removed more than 30 days ago can't be restored (ENOENT) | | cancel | request | { state }; the request's /dev/sealed status becomes cancelled | | lock | — | {}; drops the key everywhere when the lock is on | | dismissBackupReminder | — | { backupReminder }; the reminder becomes dismissed for this namespace | | policy | files | the attested policy files, below (Policy) | | 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 | | forward | head, bodyPresent, body?, owner, exit, rawTraits, with a MessagePort transferred alongside | { mode, head, bodyComplete?, body? }, { needsBody: true } or { refused }, below (Forward) | | materializeConsume | ticket, stamp, path, resolved, argv, cwd, interpreters, trusted | the values for the child's environment, below (Exec) | | 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) | | sessionAnchors | stamp, session, host, port, protocol, exit | { anchors, pins }, below (Sessions) | | import | kind, bytes, password?, pick? | below (Import and export) | | export | passphrase | { bundle, count }, below | | anything else | | ENOTSUP |

put and seal need the vault unlocked. They answer EACCES with a message starting vault is locked otherwise.

Events on page ports:

  • { v: 1, event: 'status', status } whenever requests, records, the scrub state or the lock change;
  • { v: 1, event: 'placeholders', placeholders, scrub } whenever either changes;
  • { v: 1, event: 'changed', parts }, with parts among secrets, audit and exits, so a view knows what to reload;
  • { v: 1, event: 'refresh', name, source, expiresAt }, when a J1 lease is due (below);
  • { v: 1, event: 'egress', request }, with a MessagePort, when the vault needs bios to send a request it built (below).

Errors use errno names: EINVAL, ENOENT, EACCES, EEXIST, EAGAIN, ETIMEDOUT, ENOTCONN, and ENOTSUP for ops this version doesn't have.

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.

/dev/sealed, the vault side

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.

| op | fields | reply | |---|---|---| | 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 | | 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) | | list | — | { secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }, records of kind secret that aren't removed, with their rules and [exec] mode | | 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 | | request | kind: "value", name, prompt? (≤ 200 characters) | { state: "pending", id } | | request | kind: "oauth", provider, oauth (an object) | the same | | request | kind: "jerk-seal", provider | the same | | request | kind: "jerk", jop (list, status, create), provider? (needed but for list), scope?, params? (an object) | the same, or { state: 'done', id, exit: 2, message } for a J2 refusal | | request | kind: "unseal", provider | the same, or the J2 refusal | | status | id | { state }, with placeholder and record once the request is done, jerk for a sign-in or lease that reported it, and the latest answer fields (progress, exit, user, scopes, message, rows, placeholder). A jerk status of mcp:<server> is decided below (Root and mcp records) | | cancel | id | { state }: cancelled, or the state it had already reached | | materialize | names (or shellGrants: true), path, argv, cwd, interpreter? | { ticket, expiresAt, real: { NAME: true \| false } }, below (Exec) |

  • Requests expire 300 s after they're made. Each uid has at most one pending request per namespace that waits for a human (value, oauth, jerk-seal, and jerk with jop: "create"); a second one is EAGAIN. status and cancel see only the caller's own requests (uid 0 sees all), and others get ENOENT. Only requests that wait for a human show in status.waiting.
  • A value request is answered by the page's put with its request id. A sign-in or lease (oauth, jerk-seal) is answered by seal with request (Tokens and refresh). Every other kind is answered by the page's answer. cancel ends any of them and pushes { v: 1, event: 'cancelled', request, kind } to the page, which passes it on to J1 where it can.
  • J-8's jerk requests (§3.7.1): the vault only keeps their state; bios learns of them from the relay and answers. jop: "create" and unseal are for root and cones only (J2): the kernel stamp must have users: 'enforce', and either uid 0 or the sudo group among its groups. bios names that group's gid as sudoGid in its hello; a page hello without it clears the namespace's gid. A stamp without groups, a vault without sudoGid, or any other mode refuses: the request is done with exit: 2 at once, before bios sees anything. The tag is kept for audit and display only.
  • Root and mcp records: the placeholder of an mcp record a process didn't seal is for root only, since a process holding one can call that server through forward past a role's tool limits. Whoever seals a bridge token holds its placeholder. Root is a stamp with uid 0 and users: 'enforce', not mayManage's sudo group: a process a human approved (sudo, including standing approvals), or the judge the owner chose to delegate sudo to (slicc-agent's sudo.approver = agent, whose default is user, a person). That root can already do more than read a placeholder. A cone or scoop isn't root, and neither is any uid under another mode, where the stamp proves nothing. Records match as kind mcp, source other than process, with server equal to the name given.
    • placeholder with server, for root: only a usable mcp record bound to that server answers, as in list.mcp (live, ready or refreshing, its name and server holding no value, so none while the vault is locked before its first unlock), with { placeholder, state, kind: 'mcp' }. Given a name, it's the record of that name, else ENOENT "no record named NAME"; a secret of that name or a removed record never answers. Names in these refusals are scrubbed over every value. Without a name, it's the server's one record: EINVAL "several mcp records are bound to SERVER; ask for one by name" for more than one, and ENOENT for none. A record's name is never derived from its server's. Every other caller, and root without server, gets the secret answer, without kind.
    • placeholder with name and server answers for root when such a record, not removed, has that name and server. Anyone else, a wrong server, a removed record, a bridge token and a record with no server get the reply the op gives without server, the same as for no record. A seal under a taken name still answers EEXIST, as above.
    • status of a done jerk status request for mcp:<server> with no placeholder from a secret record: when both the request's stored stamp and the caller are root, and bios answered exit 0, and exactly one usable record (the rule of list.mcp) is bound to that server, the reply adds its placeholder and record. Several usable ones are EINVAL for root ("several mcp records are bound to SERVER; ask for one by name"), as for the agent's refresh; placeholder by name needs no such rule. When both are root and a record is usable but bios didn't answer exit 0, bios's reply stands. Every other reader, and any reader with no usable record, gets { state: 'done', exit: 3, message: "no token for mcp:<server> in seven's vault: run jerk mcp:<server> create" }, whatever bios answered. The page and bios never see this rule: bios answers from list.mcp, before the caller is checked.
    • Neither reply carries a value, and neither read is audited, like every read of a placeholder; each use of one is, as mcp (Model and MCP endpoints). Where the value is filled is Which MCP credential goes where (§3.16.1): for a Settings record, an https: URL under the server URL the page registered; with no page server for that name, it's pinned on first use to the agent's URL for that server, until the page registers one; for a sign-in (oauth:mcp), any registered https: URL of that server.

Tokens and refresh

The vault has no network stack, so sign-ins run in seven's page (route (b), §3.11) and the vault keeps only the results.

  • A sign-in's tokens: bios runs the sign-in, does the code exchange, and hands the results to seal, keeping no copy:
    • the refresh token as { kind: 'grant', name, value, source: 'oauth:<provider>', oauth: { tokenUrl, clientId, scope?, resource? } }. A grant is never injected and never exported; tokenUrl must be https:, without userinfo (user:pw@, user@, even an empty :@, also when hidden by whitespace or backslashes the URL parser normalises: any @ in the authority refuses), and a resource must be an https: URL (or http: on 127.0.0.1, localhost or [::1]) without userinfo or fragment;
    • 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).
    • 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.
  • A J1 lease: { name, value, source: 'jerk:<provider>@<device>', expiresAt, refresh: 'jerk-seal' }.
  • When a token is due (5 minutes before expiresAt): the core frame checks once a minute, and right after a page says hello, 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? }.
    • 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' }).
    • An OAuth token is refreshed in the vault with jerk-oauth: refreshTokenRequestBytes builds POST <tokenUrl, serialized> with grant_type=refresh_token, the refresh token, client_id and any scope and resource (form-encoded), from a byte buffer it zeroes, and the vault 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 checks the response with its own rules below and then with jerk-oauth's parseRefreshResponse (with the clock as whole seconds; a clock that isn't a safe integer is EACCES: refresh failed: the vault's clock is invalid), 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 whole number from 0 to ten years is EACCES: refresh failed: the IdP returned an invalid expires_in (likewise a refresh_token that is present but not a string, a token_type that isn't Bearer in any case or is missing, and access_token, refresh_token, token_type or expires_in given twice: EACCES: refresh failed: the IdP returned NAME twice); 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; a code that isn't 1–128 characters of RFC 6749's error charset reads "the IdP sent an unreadable error", and error_description is never shown. Any other response jerk-oauth refuses is EACCES: refresh failed: the IdP response is invalid. MCP grant responses are parsed the same way. Without a connected page it's ENOTCONN.
  • egress carries the refresh token to the page, in transit only; the response goes back to the vault, never to the kernel.

Import and export

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.

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:

{ "format": "slicc-vault-bundle", "v": 1,
  "kdf": { "name": "PBKDF2", "hash": "SHA-256", "iterations": 600000, "salt": "<base64url>" },
  "cipher": { "name": "AES-GCM", "iv": "<base64url>" },
  "data": "<base64url>" }

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.

import { kind, bytes, password?, pick? } runs in two steps:

  • 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.
  • 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.

| kind | password | what becomes a candidate | |---|---|---| | 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") | | 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 | | 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 |

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.

Exec

fnox exec (§3.5) asks for a one-shot ticket, and the kernel redeems it when it execs the child.

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. While the caller's stamp doesn't say users: 'enforce', it's EACCES "kernel users must be enforce" in every [exec] mode, shell included (§3.16.2): with users off, every process is effectively root and the stamp proves nothing.

  • 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.
  • 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.
  • { argv = [[…]] }: real only when [path, ...argv[1..]] starts with one of the listed prefixes. A listed shell or interpreter is allowed.
  • { shell = true }: always real, with no program refusal.
  • 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).
  • A locked vault answers EACCES (vault is locked).
  • 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.
  • The reply never carries a value. The ticket is 32 hex characters and expires after 30 s.

materializeConsume (a page op, from bios for the kernel at exec) takes { ticket, stamp, path, resolved, argv, cwd, interpreters, trusted }, all from the kernel and relayed unchanged:

  • stamp is the exec'ing process.
  • resolved is the kernel-normalised file that actually runs: path with every symlink resolved.
  • 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.
  • trusted is true when the program and every chain entry are root-owned regular files, not group- or other-writable, under root-owned, non-writable parents (the attestation rule). Under users off it's always false, and the consume is refused anyway.

The vault burns the ticket, then requires that:

  • stamp.users is enforce (else "kernel users must be enforce"), 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;
  • resolved is an absolute path (missing or malformed refuses), interpreters 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);
  • 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;
  • under that name's mode, the chain passes:
    • true: no entry of path, resolved and interpreters is on the refusal list, so neither a #!/usr/bin/env node script nor a symlink /tmp/l → /bin/bash passes, and trusted is true ("the program or its interpreter can be changed by a non-root user" otherwise), so a renamed copy of a shell, which its non-root maker owns, doesn't pass either;
    • { argv }: the listed program is path, and when path is a link to a refused program the rule must also list that resolved program by its real path ("list it in [exec] by its real path"), so a listed /tmp/l that is repointed to /bin/bash refuses. Whatever chain the kernel resolved for it is allowed, so a rule for npm publish allows /usr/bin/npm's node. But when resolved or the chain is a refused program and trusted is false, it refuses ("the program or its interpreter can be changed by a non-root user"), and env and the other wrappers in the chain always refuse;
    • { shell = true }: any program and chain, the explicit exception;
  • no real record has expired meanwhile.

Any failure is EACCES: ticket refused: …, and the ticket stays burned, except for a policy that is paused or that changes before the values go out: the vault checks that in the transaction that marks the entries consumed, refuses with policy file missing: paused or ticket refused: the policy changed meanwhile, releases nothing, and keeps the ticket for its 30 s. 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).

Forward

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:

const channel = new MessageChannel();
port.postMessage({ v: 1, id, op: 'forward', head, bodyPresent, owner, exit, rawTraits, streamCredit }, [channel.port2]);
  • head is { method, url, headers: [name, value][] }; owner the kernel's { pid, ppid, uid, gid, tag, users, groups?, user? } (users must be enforce, else nothing is injected); exit the exit bios chose (page, proxy, tailscale or device:<id>, with the device's DeviceId); rawTraits that exit's { manualRedirects?, encodedBodies? }; bodyPresent whether the request has a body, sent on every forward. body (bytes or a string) is read only for MCP requests (below); for every other request it stays in bios, because bodies are never injected.
  • The reply says which mode the vault chose; bios never guesses it:
    • { mode: 'secret', head }: the same head with the values filled in. It has no body, whatever bios sent.
    • { 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.
    • { 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.
    • { 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.
    • Malformed fields, and an MCP body above 32 MiB, are EINVAL.
  • The port: on every reply but a mode one, the vault closes the transferred port.
  • 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.
  • When the vault's values change (a value sealed, rotated, removed or restored, in any vault document), the scrub of a response in flight is out of date, so the response is cancelled. In order for each message from bios and for the idle timer, the vault reads the scrub version and sends what follows from that message inside the same read transaction, so no write can commit between the check and the send. If it moved, nothing derived from that message goes out: any held tail is dropped, and the vault posts one { op: 'error', message: "the vault's values changed; send the request again" } and closes the port. No credit follows. bios cancels the send.
  • Flow control is opt-in: with streamCredit: true, and only then, the vault follows the scrubbed output of each { op: 'body', chunk: Uint8Array } it gets with exactly one { op: 'credit', bytes }, bytes being that chunk's byteLength, even when the output is empty because the scrub holds a tail back. Nothing else earns a credit: not the response head, an idle flush, end, error, or a chunk that isn't a Uint8Array.

The injector, fnox's proxy semantics:

  • Hosts are compared lower-cased, without a trailing dot, in rules, the refusal list and MCP servers alike.
  • Refused before anything is sent: an owner whose users isn't enforce ("kernel users must be enforce", §3.16.2; this covers kernel programs calling a registered MCP server, and the /dev/sealed test verdict, too); a placeholder in the URL or in a header name; credentials in the URL; a scheme other than https: or a port other than 443 (except §3.16.3's kernel servers, below); an ambiguous path (./.. segments, \, %2e, %2f, %5c, %25, checked on the raw URL); an exit with encoded bodies; no placeholder of this vault at all; a record that is removed, expired ("NAME expired; refreshing") or not a secret; a locked vault; no rule (in /etc/fnox/config.toml) whose exact domain, header, method and path allow it; a paused policy ("policy file missing: paused"); an exit the secret's [exits] line doesn't list; the same placeholder twice in one header value; any credential, secret or mcp, in any header, authorization included, on an exit whose raw manualRedirects isn't true ("NAME needs an exit that doesn't follow redirects"); a value that contains another stored value ("NAME's value contains another stored value"); CR, LF or NUL after substitution. The /dev/sealed test verdict goes through the same checks, on an exit it takes to be manual.
  • Paths in rules are globs: * stays within a segment, ** crosses segments, ? is one character.
  • Injected: each placeholder is replaced inside the rule's header value, so Bearer … and token … templates work. In Authorization: Basic, the credential is decoded, the user and password parts substituted (a substituted user may not contain :), and re-encoded. A Basic value that holds a placeholder but isn't valid base64 of UTF-8 is refused.
  • Audited: each injection adds a forward audit entry { name, domain, method, path, rule, exit, uid, tag }, all its text, the rule's included, scrubbed over every value before it's stored, and sets the record's usedAt. Never headers, bodies or values.

Sessions

sessionMaterialize (§3.19, a page op that bios relays unchanged from the kernel) hands the kernel's TLS writer the secrets for one kernel-terminated TLS session (A2), for protocols forward can't carry: an http1 header on a kernel-made connection, SMTP AUTH, or pg's cleartext password or SCRAM-SHA-256. The kernel asks only after its own handshake verified the host (SNI, chain and hostname, whatever the caller's flags), never on a plaintext leg; it substitutes only at the position, uses each value once within 60 s, keeps it out of every process, fd, file, log and telemetry, and scrubs the session's read side (S2) for the values and their encodings. The vault picks the rules, so the kernel never parses /etc/fnox/config.toml.

port.postMessage({ v: 1, id, op: 'sessionMaterialize', stamp, session, host, port: 587, protocol: 'smtp', position: 'auth-plain', verified: true, handshakeAt, exit, placeholders });

The fields are the kernel's session attestation:

  • stamp: the kernel's stamp of the process that owns the socket; users must be enforce (else EACCES "kernel users must be enforce").
  • session: the kernel's session id, 16 to 128 characters of A-Z, a-z, 0-9, _ and -.
  • host: the name the kernel verified the leaf against, compared lower-cased without one trailing dot; port; protocol (http1, smtp or pg).
  • position: header:<name> (lower-cased) for http1, else auth-plain, auth-login, password-cleartext or scram-sha-256 (below).
  • verified: the kernel's own verdict on the chain and the name; handshakeAt: when the handshake finished (epoch ms).
  • verifiedBy: roots (the default; the kernel's bundled public roots) or anchored (a rule's anchors or pins, below), with anchor: base64 of the SHA-256 of the whole DER of the anchor that verified the chain, as sessionAnchors gave it, or the pin itself. anchor with roots, or a missing or malformed one with anchored, is EINVAL.
  • exit: the exit the session leaves by (page, proxy, tailscale or device:<id>, with the device's DeviceId).
  • placeholders (every position but scram-sha-256, which takes scram instead, below): 1 to 16 placeholders, every one found at the position, in one request. head (http1 only, required there): { method, path }, the request line's method and target.
  • A malformed field is EINVAL, and so is a protocol other than these or a position that isn't one of its protocol's; neither burns the session.

Once the fields check out, the vault burns the session in IndexedDB, in one transaction, so no vault frame of the namespace, now or after a reload, answers it again until 60 s after the later of the request and handshakeAt. Then it refuses the whole request with EACCES "session refused: …" when:

  • the session already had a request, answered or refused: "this session already had its request";
  • verified isn't true: "the kernel didn't verify the session's host";
  • handshakeAt is in the future or more than 60 s ago: "the session is past its 60 s deadline";
  • the vault is locked (EACCES "vault is locked: …"), at the start or by the point of release (below);
  • for http1: an ambiguous path (as in Forward), or a URL under a model or MCP endpoint (Model and MCP endpoints).

Each placeholder then gets its value only when it is a secret record's, holding the placeholder [secrets] names, neither removed nor expired, with a session rule of that secret whose protocol, domain, port and position are the session's (and, for http1, whose methods and paths allow head), and whose [exits] line, if any, lists exit. A session can carry several secrets this way, each by its own rule. Just before answering, the records, the accepted policy and the registered endpoints are read again, at a fresh clock, in the transaction that audits the values. A value whose record was removed, expired or rotated meanwhile, or whose rule or [exits] line no longer allows it, is withheld. A session past its deadline by then, or an http1 URL now under a registered endpoint, refuses the whole request. That transaction is the point of release: if the vault locked before it, it writes nothing and refuses (vault is locked).

It answers { values: { [placeholder]: value } } for those, and leaves the others out, so the kernel sends them as written. When none gets a value, it's EACCES with the first placeholder's reason: "NAME isn't a secret", "a placeholder of no record of this vault", "NAME's placeholder isn't the one /etc/fnox/config.toml names", "NAME was removed", "NAME expired", "no session rule in /etc/fnox/config.toml lets NAME be used", "no session rule lets NAME at PROTOCOL POSITION on HOST:PORT" (with " for METHOD PATH" for http1), "NAME isn't allowed on EXIT", or "the secret or its rule changed meanwhile". Each value handed out adds a session audit entry { name, session, domain, port, protocol, position, rule, verifiedBy, anchor?, exit, uid, tag }, rule being the number of the rule that allowed it and verifiedBy and anchor how the kernel trusted the session (anchor only when anchored: the anchor's DER SHA-256 or the pin, never a value; entries written before 1.12.1 have neither, which means unknown, never roots), and sets the record's usedAt. It never includes the value. Stored values never hold CR, LF or NUL (Records), so a value can't break out of its position.

sessionAnchors (§3.19.6) is value-free: when the kernel's