@ai-ecoverse/slicc-vault 1.1.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 +68 -5
- package/dist/types/contract.d.ts +39 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The separate-origin vault at `vault.sliccy.ai`. It holds the sealed secrets, OAuth tokens, and model and MCP credentials of SLICC seven, so that no value ever reaches the model provider. The design is `secrets-tokens-design.md` (revision 4) in slicc-agent-plan. This README is the spec of what is built.
|
|
4
4
|
|
|
5
|
-
- **Built:** the key store, records, origin namespacing, auto-unlock with an optional passkey or passphrase lock, the embedding API with its `page` and `agent` roles, the page's `put`, `seal`, `remove`, `restore`, `cancel` and `lock`, the scrub state, the lock button and its popup, 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,7 +64,7 @@ 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
|
|
|
@@ -72,6 +72,68 @@ Everything lives in the vault origin's IndexedDB, database `slicc-vault`, versio
|
|
|
72
72
|
|
|
73
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
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
|
+
|
|
75
137
|
## Scrub state
|
|
76
138
|
|
|
77
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:
|
|
@@ -111,6 +173,7 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
111
173
|
| `cancel` | `request` | `{ state }`; the request's `/dev/sealed` `status` becomes `cancelled` |
|
|
112
174
|
| `lock` | — | `{}`; drops the key everywhere when the lock is on |
|
|
113
175
|
| `dismissBackupReminder` | — | `{ backupReminder }`; the reminder becomes `dismissed` for this namespace |
|
|
176
|
+
| `policy` | `files` | the attested policy files, below (Policy) |
|
|
114
177
|
| `import`, `export`, anything else | | `ENOTSUP` |
|
|
115
178
|
|
|
116
179
|
`put` and `seal` need the vault unlocked. They answer `EACCES` with a message starting `vault is locked` otherwise.
|
|
@@ -130,7 +193,7 @@ bios relays each kernel request unchanged as the `sealed` op, with the kernel's
|
|
|
130
193
|
|---|---|---|
|
|
131
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` |
|
|
132
195
|
| `placeholder` | `name` | `{ placeholder, state }`, including `removed`; `ENOENT` when there's no record |
|
|
133
|
-
| `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 |
|
|
134
197
|
| `test` | `name`, `url`, `method?`, `header?` | `{ verdict, reason }`. Without policy it's always `refuse`; `ENOENT` for an unknown name |
|
|
135
198
|
| `request` | `kind: "value"`, `name`, `prompt?` (≤ 200 characters) | `{ state: "pending", id }` |
|
|
136
199
|
| `request` | `kind: "oauth"`, `provider`, `oauth` (an object) | the same |
|
|
@@ -200,7 +263,7 @@ The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built fr
|
|
|
200
263
|
- **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
|
|
201
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.
|
|
202
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.
|
|
203
|
-
- **`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).
|
|
204
267
|
- **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
|
|
205
268
|
- **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
|
|
206
269
|
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export interface VaultStatus {
|
|
|
79
79
|
waiting: PendingRequest[];
|
|
80
80
|
exportedAt: EpochMs | null;
|
|
81
81
|
backupReminder: BackupReminder;
|
|
82
|
+
policy: PolicyFileState[];
|
|
82
83
|
}
|
|
83
84
|
export type BackupReminder = 'due' | 'dismissed' | null;
|
|
84
85
|
export type Availability = 'connecting' | 'waiting' | 'locked' | 'ready';
|
|
@@ -165,6 +166,38 @@ export interface ExitPolicyView {
|
|
|
165
166
|
exits: ExitName[];
|
|
166
167
|
}[];
|
|
167
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
|
+
}
|
|
168
201
|
export interface VaultList {
|
|
169
202
|
secrets: SecretView[];
|
|
170
203
|
refused: RefusedRule[];
|
|
@@ -268,6 +301,9 @@ export type Op = {
|
|
|
268
301
|
op: 'lock';
|
|
269
302
|
} | {
|
|
270
303
|
op: 'dismissBackupReminder';
|
|
304
|
+
} | {
|
|
305
|
+
op: 'policy';
|
|
306
|
+
files: Attestation[];
|
|
271
307
|
};
|
|
272
308
|
export type OpReply = {
|
|
273
309
|
op: 'sealed';
|
|
@@ -299,7 +335,9 @@ export type OpReply = {
|
|
|
299
335
|
} | {
|
|
300
336
|
op: 'dismissBackupReminder';
|
|
301
337
|
backupReminder: BackupReminder;
|
|
302
|
-
}
|
|
338
|
+
} | ({
|
|
339
|
+
op: 'policy';
|
|
340
|
+
} & PolicyResult);
|
|
303
341
|
export type Request = Op & {
|
|
304
342
|
v: Protocol;
|
|
305
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",
|