@ai-ecoverse/slicc-vault 1.0.0 → 1.2.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 +74 -6
- package/dist/types/contract.d.ts +47 -1
- package/package.json +2 -2
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, 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]`), and the vault side of the `/dev/sealed` ops `seal`, `placeholder`, `list`, `test`, `request`, `status`, `cancel` and `materialize`.
|
|
6
|
+
- **Not built yet:** `forward` and the scrubber (`test` always refuses), exec, `import` and `export`, and the agent's ops. They answer as described below.
|
|
7
7
|
|
|
8
8
|
## Documents and hosting
|
|
9
9
|
|
|
@@ -64,10 +64,76 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
|
|
|
64
64
|
- **A value** is at least 8 bytes of UTF-8, without CR, LF or NUL, and contains no placeholder. Anything shorter is refused with `EINVAL` "values shorter than 8 bytes can't be protected from the model", because the scrubber couldn't tell it apart from ordinary output.
|
|
65
65
|
- **`placeholder`** is `fnox_proxy_` plus 24 lower-case hex characters from `crypto.getRandomValues`, unique across the whole vault and stable for the record's life. All placeholders have the same length, so none contains another.
|
|
66
66
|
- **`source`** is `settings` (entered in spectrum), `process`, `jerk:<provider>@<device>`, `oauth:<provider>` or `import:<kind>`.
|
|
67
|
-
- **`state`** is computed, never stored: `removed` when `removedAt` is set, `active` when the accepted policy has a rule
|
|
67
|
+
- **`state`** is computed, never stored: `removed` when `removedAt` is set, `active` when the accepted policy has a rule for the record (below), otherwise `inert`.
|
|
68
68
|
- **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.
|
|
69
69
|
- **Times** are epoch milliseconds everywhere.
|
|
70
70
|
|
|
71
|
+
## Backup reminder
|
|
72
|
+
|
|
73
|
+
`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). `export` arrives with VAULT-1e; until then `exportedAt` stays `null`.
|
|
74
|
+
|
|
75
|
+
## Policy
|
|
76
|
+
|
|
77
|
+
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:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
port.postMessage({ v: 1, id, op: 'policy', files: [await kernel.attest('/etc/fnox/config.toml'), await kernel.attest('/etc/slicc/secrets-policy.toml')] });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**An attestation** is exactly `{ path, bytes, uid, gid, mode, ino, mtimeMs, sha256, users, onRootFs, parents }`, nothing more or less:
|
|
84
|
+
|
|
85
|
+
| field | format |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `path` | the canonical absolute path the kernel read (it refuses symlinks and mount points) |
|
|
88
|
+
| `bytes` | the file's bytes, a `Uint8Array`, read in the same step as the stats |
|
|
89
|
+
| `uid`, `gid`, `mode` | integers; `mode` includes the type bits (`0o100644`) |
|
|
90
|
+
| `ino`, `mtimeMs` | numbers |
|
|
91
|
+
| `sha256` | 64 lower-case hex characters |
|
|
92
|
+
| `users` | the kernel's users mode: `off`, `report` or `enforce` |
|
|
93
|
+
| `onRootFs` | true only on the root file system, under no mount |
|
|
94
|
+
| `parents` | every directory from `/` down to the file's, each exactly `{ path, uid, gid, mode }` |
|
|
95
|
+
|
|
96
|
+
The vault trusts a file only when:
|
|
97
|
+
- `path` is `/etc/fnox/config.toml` or `/etc/slicc/secrets-policy.toml`, and the object has exactly the fields above in those formats;
|
|
98
|
+
- the SHA-256 the vault computes over `bytes` equals `sha256` (the field alone is never trusted);
|
|
99
|
+
- `uid` is 0, and `mode` is a regular file with `mode & 0o022` of 0;
|
|
100
|
+
- `parents` lists exactly the directories above `path`, in order from `/`, each with uid 0, a directory type and `mode & 0o022` of 0;
|
|
101
|
+
- `onRootFs` is true, and `users` is `enforce`;
|
|
102
|
+
- `bytes` is UTF-8 and parses.
|
|
103
|
+
|
|
104
|
+
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 }], rules: [{ secret, domain, header, methods, paths }], 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.
|
|
105
|
+
|
|
106
|
+
**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.
|
|
107
|
+
|
|
108
|
+
**`/etc/fnox/config.toml`**, the injection policy, read the way fnox reads it:
|
|
109
|
+
- `[secrets]`: entries whose `provider` (or the file's `default_provider`) is `seven`, and whose `value` is a placeholder.
|
|
110
|
+
- `[[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.
|
|
111
|
+
- 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.
|
|
112
|
+
- Nothing else in the file counts: no imports, profiles or other providers.
|
|
113
|
+
|
|
114
|
+
**`/etc/slicc/secrets-policy.toml`**, read by the vault only:
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
[exec]
|
|
118
|
+
NPM_TOKEN = true
|
|
119
|
+
GITHUB_TOKEN = { argv = [["/usr/bin/gh", "release", "upload"]] }
|
|
120
|
+
OPENAI_API_KEY = { shell = true }
|
|
121
|
+
|
|
122
|
+
[exits]
|
|
123
|
+
NPM_TOKEN = ["page", "device:lars-air"]
|
|
124
|
+
|
|
125
|
+
[limits]
|
|
126
|
+
materialize_per_minute = 10
|
|
127
|
+
materialize_per_day = 200
|
|
128
|
+
shell_per_day = 5000
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `[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.
|
|
132
|
+
- `[exits]` is **optional**: a secret without a line may use every exit; a line restricts it to the exits listed (`page`, `proxy`, `tailscale`, `device:<name>`). Unknown names are dropped from the line with a warning, which leaves the line narrower, never wider.
|
|
133
|
+
- `[limits]` overrides the defaults above with positive integers.
|
|
134
|
+
|
|
135
|
+
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`. 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.
|
|
136
|
+
|
|
71
137
|
## Scrub state
|
|
72
138
|
|
|
73
139
|
`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:
|
|
@@ -97,7 +163,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
97
163
|
| op (page role) | fields | reply |
|
|
98
164
|
|---|---|---|
|
|
99
165
|
| `sealed` | `req` (the `/dev/sealed` request object, or its JSON text), `stamp` (`{ pid, ppid, uid, gid, tag }` from the kernel) | `{ reply }`, the §3.7 reply, below |
|
|
100
|
-
| `status` | — | `{ status: { version, protocol, origin, locked, lock, scrub, waiting } }` |
|
|
166
|
+
| `status` | — | `{ status: { version, protocol, origin, locked, lock, scrub, waiting, exportedAt, backupReminder } }` |
|
|
101
167
|
| `placeholders` | — | `{ placeholders, scrub }`: every placeholder of a record that isn't removed, active and inert, sorted |
|
|
102
168
|
| `list` | — | `{ list: { secrets, refused, audit, auditMore, exits } }` for Settings › Secrets |
|
|
103
169
|
| `audit` | `before?` (a `seq`), `limit?` (1–1000, default 100) | `{ audit, more, next? }`, newest first; pass `next` as `before` for the next page |
|
|
@@ -106,6 +172,8 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
106
172
|
| `remove`, `restore` | `name`, `kind?` (default `secret`) | `{ name, placeholder, state }`. A record removed more than 30 days ago can't be restored (`ENOENT`) |
|
|
107
173
|
| `cancel` | `request` | `{ state }`; the request's `/dev/sealed` `status` becomes `cancelled` |
|
|
108
174
|
| `lock` | — | `{}`; drops the key everywhere when the lock is on |
|
|
175
|
+
| `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
|
|
176
|
+
| `policy` | `files` | the attested policy files, below (Policy) |
|
|
109
177
|
| `import`, `export`, anything else | | `ENOTSUP` |
|
|
110
178
|
|
|
111
179
|
`put` and `seal` need the vault unlocked. They answer `EACCES` with a message starting `vault is locked` otherwise.
|
|
@@ -125,7 +193,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
|
|
|
125
193
|
|---|---|---|
|
|
126
194
|
| `seal` | `name`, `value` | `{ placeholder, state }`. A new name creates a `process` record. An existing one is rotated and keeps its placeholder, but only when its value came from a process; a value from Settings, jerk, OAuth or an import is replaced only by uid 0, and anyone else gets `EEXIST` |
|
|
127
195
|
| `placeholder` | `name` | `{ placeholder, state }`, including `removed`; `ENOENT` when there's no record |
|
|
128
|
-
| `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed |
|
|
196
|
+
| `list` | — | `{ secrets: [{ name, placeholder, state, exec, rules, expiresAt? }] }`, records of kind `secret` that aren't removed, with their rules and `[exec]` mode |
|
|
129
197
|
| `test` | `name`, `url`, `method?`, `header?` | `{ verdict, reason }`. Without policy it's always `refuse`; `ENOENT` for an unknown name |
|
|
130
198
|
| `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
|
|
131
199
|
| `request` | `kind: "oauth"`, `provider`, `oauth` (an object) | the same |
|
|
@@ -195,7 +263,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
|
|
|
195
263
|
- **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
|
|
196
264
|
- **`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.
|
|
197
265
|
- **`AuditEntry`**: `forward` is a use of a value in a request, with the exit that carried it; **`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.
|
|
198
|
-
- **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
|
|
266
|
+
- **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint (empty until VAULT-1b).
|
|
199
267
|
- **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
|
|
200
268
|
- **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
|
|
201
269
|
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -77,7 +77,11 @@ export interface VaultStatus {
|
|
|
77
77
|
lock: LockMode;
|
|
78
78
|
scrub: ScrubState;
|
|
79
79
|
waiting: PendingRequest[];
|
|
80
|
+
exportedAt: EpochMs | null;
|
|
81
|
+
backupReminder: BackupReminder;
|
|
82
|
+
policy: PolicyFileState[];
|
|
80
83
|
}
|
|
84
|
+
export type BackupReminder = 'due' | 'dismissed' | null;
|
|
81
85
|
export type Availability = 'connecting' | 'waiting' | 'locked' | 'ready';
|
|
82
86
|
export type PolicyFile = '/etc/fnox/config.toml' | '/etc/slicc/secrets-policy.toml';
|
|
83
87
|
export type PolicyChange = {
|
|
@@ -162,6 +166,38 @@ export interface ExitPolicyView {
|
|
|
162
166
|
exits: ExitName[];
|
|
163
167
|
}[];
|
|
164
168
|
}
|
|
169
|
+
export interface Attestation {
|
|
170
|
+
path: string;
|
|
171
|
+
bytes: Uint8Array;
|
|
172
|
+
uid: number;
|
|
173
|
+
gid: number;
|
|
174
|
+
mode: number;
|
|
175
|
+
ino: number;
|
|
176
|
+
mtimeMs: EpochMs;
|
|
177
|
+
sha256: string;
|
|
178
|
+
users: string;
|
|
179
|
+
onRootFs: boolean;
|
|
180
|
+
parents: {
|
|
181
|
+
path: string;
|
|
182
|
+
uid: number;
|
|
183
|
+
gid: number;
|
|
184
|
+
mode: number;
|
|
185
|
+
}[];
|
|
186
|
+
}
|
|
187
|
+
export interface PolicyFileState {
|
|
188
|
+
path: string;
|
|
189
|
+
sha256: string | null;
|
|
190
|
+
accepted: boolean;
|
|
191
|
+
warnings: string[];
|
|
192
|
+
}
|
|
193
|
+
export interface PolicyResult {
|
|
194
|
+
accepted: boolean;
|
|
195
|
+
files: PolicyFileState[];
|
|
196
|
+
rules: (Rule & {
|
|
197
|
+
secret: string;
|
|
198
|
+
})[];
|
|
199
|
+
warnings: string[];
|
|
200
|
+
}
|
|
165
201
|
export interface VaultList {
|
|
166
202
|
secrets: SecretView[];
|
|
167
203
|
refused: RefusedRule[];
|
|
@@ -263,6 +299,11 @@ export type Op = {
|
|
|
263
299
|
passphrase: string;
|
|
264
300
|
} | {
|
|
265
301
|
op: 'lock';
|
|
302
|
+
} | {
|
|
303
|
+
op: 'dismissBackupReminder';
|
|
304
|
+
} | {
|
|
305
|
+
op: 'policy';
|
|
306
|
+
files: Attestation[];
|
|
266
307
|
};
|
|
267
308
|
export type OpReply = {
|
|
268
309
|
op: 'sealed';
|
|
@@ -291,7 +332,12 @@ export type OpReply = {
|
|
|
291
332
|
bundle: Uint8Array;
|
|
292
333
|
} | {
|
|
293
334
|
op: 'lock';
|
|
294
|
-
}
|
|
335
|
+
} | {
|
|
336
|
+
op: 'dismissBackupReminder';
|
|
337
|
+
backupReminder: BackupReminder;
|
|
338
|
+
} | ({
|
|
339
|
+
op: 'policy';
|
|
340
|
+
} & PolicyResult);
|
|
295
341
|
export type Request = Op & {
|
|
296
342
|
v: Protocol;
|
|
297
343
|
id: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-vault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"release": "semantic-release"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@ai-ecoverse/slicc-shared-web": "1.13.
|
|
49
|
+
"@ai-ecoverse/slicc-shared-web": "1.13.1",
|
|
50
50
|
"@biomejs/biome": "2.5.15",
|
|
51
51
|
"@happy-dom/global-registrator": "20.14.5",
|
|
52
52
|
"@spectrum-css/button": "14.2.0",
|