@byokit/openclaw 0.9.0 → 0.11.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/CHANGELOG.md +31 -0
- package/README.md +55 -26
- package/SECURITY.md +6 -5
- package/dist/accounts.d.ts +16 -0
- package/dist/accounts.js +222 -0
- package/dist/auth-status.d.ts +1 -0
- package/dist/auth-store.d.ts +30 -5
- package/dist/auth-store.js +265 -52
- package/dist/classify.js +2 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +17 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/device.d.ts +1 -1
- package/dist/engine.d.ts +5 -2
- package/dist/engine.js +112 -23
- package/dist/generated/methods.d.ts +7 -1
- package/dist/generated/params.d.ts +20 -0
- package/dist/generated/params.js +1 -1
- package/dist/generated/report.json +5 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/kit.d.ts +8 -1
- package/dist/kit.js +39 -3
- package/dist/link.d.ts +1 -1
- package/dist/link.js +3 -3
- package/dist/locks.d.ts +5 -0
- package/dist/locks.js +85 -0
- package/dist/members.d.ts +1 -0
- package/dist/members.js +12 -2
- package/dist/pick.d.ts +11 -0
- package/dist/pick.js +145 -0
- package/dist/signin.d.ts +1 -0
- package/dist/signin.js +13 -1
- package/dist/testing/contract.js +0 -1
- package/dist/testing/fake-gateway.js +220 -7
- package/dist/types.d.ts +88 -1
- package/dist/words.js +3 -0
- package/dist/words.json +38 -1
- package/engine/package-lock.json +84 -84
- package/engine/package.json +1 -1
- package/engine/patches.json +17 -17
- package/package.json +8 -8
- package/plugin/accounts.js +224 -0
- package/plugin/index.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.11.0 (2026-10-12)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/ui 0.7.0.
|
|
8
|
+
- Dependency update: pins @byokit/secrets 0.5.1.
|
|
9
|
+
- Dependency update: pins @byokit/discover 0.8.0.
|
|
10
|
+
- Dependency update: pins @byokit/relay 0.6.1.
|
|
11
|
+
- Dependency update: pins @byokit/pair 0.10.0.
|
|
12
|
+
|
|
13
|
+
- FEAT: `byokit.accounts` seals an agent's own sign-ins (list/seal/ready/adopt/clear), loaded as the `plugin/accounts.js` gateway method; a failed `adopt` rolls the model and both profile stores back so no partial move or dangling pin survives, a self-adopt is refused, and the fake gateway mirrors every branch.
|
|
14
|
+
|
|
15
|
+
## 0.10.0 (2026-10-11)
|
|
16
|
+
|
|
17
|
+
- Dependency update: pins @byokit/discover 0.7.0.
|
|
18
|
+
|
|
19
|
+
- FIX (behavior change for installs with two or more members): a member no longer runs on another member's sign-in. The kit now forces `agents.defaults.authInheritance.agentId = 'byokit-base'` on every boot and config patch, an id it never creates, signs in to or runs, so the inherited base every agent reads through is empty. Before, the pinned engine made the first agent the inherited owner once a second agent was created; while an install's shared auth store had not yet moved into `state/openclaw.sqlite`, every later member's turns read that member's sign-ins through. From this release a member runs only on its own sign-ins: one that never signed in reads signed out, and an explicit pick of a provider it is not signed in to ends `{ ok: false, kind: 'signed-out' }` before any model request. `signOut(member, provider)` now passes `models.authLogout` the `profileIds` of that member agent's own profiles; without them the engine removed the provider from every owner store, the shared or inherited store other members read included. With none of its own it sends nothing. New members follow D9 (`/^[a-z](?!.*--)[a-z0-9-]{0,23}$/`, never starting `byokit-`); a member that already exists under the older rule, such as `a--b`, keeps working. `main`, `openclaw`, `crestodian` and `byokit-base` are never a member, even though `main` exists on every install: `ensureMember('main')` used to hand back the engine's own agent. The kit's fake gateway lists one `<provider>:default` profile per signed-in provider and checks `profileIds` on sign-out.
|
|
20
|
+
- FIX: restoring an engine home no longer pays one directory fsync per sealed object. `restore()` writes each plaintext file exactly as before (temp write, file fsync, atomic rename) but records the directories whose entries changed — the containing directory of every restored file and the parent of every directory it creates — and fsyncs each distinct directory once, after all files are written and before it removes the `restoring` marker. Every restored file is content-durable, and each rename and new directory entry is durable before the marker goes, so a crash mid-restore still keeps the sealed store intact. `seal()` and the stop path keep `put()` and its per-file directory fsync, byte-for-byte unchanged; nothing new is written to disk.
|
|
21
|
+
- FIX: A large engine home can no longer abort the process at seal time. AuthStore.collect() stays narrowed to credential state (credential paths under `state` plus non-cache `home` paths; tool caches are skipped; other `state` stores are sealed as objects outside the cap), and saved sign-in data is capped: over the fixed limit, `prepare()`, `start()`, `stop()` and an engine exit reject with exported `AuthStoreSealSizeError` (`code: 'auth-store-seal-size'`, fields `size`, a lower bound at the refusal, and `cap`). On that refusal the last good saved store is kept as it was and no live file is deleted; nothing else is promised. The kit reports `{ phase: 'failed', why: 'auth-store-seal-size', sealSize }` with words giving the size and limit in MB, and a refused `stop()` still releases the lock. `@byokit/secrets` encrypt() also emits its envelope through bounded chunk-wise escaping instead of a second full-size `JSON.stringify`, so `encrypt()` builds no snapshot-sized JSON string; the envelope is assembled as bytes, still transiently two byte copies. The README's include rule replaces the former "auth-store seal size demand" known limitation.
|
|
22
|
+
- FIX: an engine store that is already sealed unchanged in this process is no longer resealed on `stop()`, `prepare()` or an offline migration. `AuthStore` keeps an in-memory `object file name -> sha256 of the sealed plaintext payload` map, filled by `restore()` from each object it decrypts and refreshed after every successful per-object `seal()`; `sealObjects()` skips a re-seal when the object's plaintext hash is unchanged and its sealed file is still on disk, so the encrypt, verify decrypt and fsyncs are not repeated for transcripts and media the engine left alone. A changed object, a new object and a fresh process (empty map) seal exactly as before. The map is memory-only: no hash, size or mtime is written to disk or logs, so the at-rest contract (no plaintext, no new on-disk metadata) is unchanged.
|
|
23
|
+
- FIX: the credential seal no longer carries engine transcripts or other non-credential state. Credential files under `state` and `home` still go into the bounded `auth-store.sealed` blob, but every other file under `state` (the shared and per-agent SQLite databases with their WAL/SHM/journal sidecars, exported transcripts, legacy session stores, media, delivery-queue media, logs, caches and tmp) is sealed as its own encrypted file under `auth-store.objects/`, outside the 128 MiB total, and restored byte for byte on start; a single such file over 128 MiB refuses with `AuthStoreSealSizeError` naming it. After stop and remove no plaintext copy of those `state` files remains; regenerable caches under `home` stay on disk unsealed. A host whose transcripts and databases alone pass the cap now starts and stops normally instead of refusing with `AuthStoreSealSizeError`. Existing v1 snapshots that hold engine stores restore and reseal in the new layout. Rolling back to a kit older than this release keeps the blob credentials but not the engine databases (they are now objects), so the user signs in again. An over-cap engine store now reports its file name in the kit's refusal state, not as sign-in data.
|
|
24
|
+
- FIX: the engine is pinned to the OpenClaw 2026.8.35 extended-stable line, down from 2026.8.1 (and gateway-client back from the 2026.9.6 dependabot bump); `ENGINE_VERSION`, the shipped `engine/package.json` + integrity lock, `@openclaw/gateway-protocol`/`@openclaw/gateway-client` and the generated tables all carry 2026.8.35. The provider auth surface is byte-identical to 2026.8.1 (verified per-extension), so the route table is unchanged. Compatibility the new pin needs: the Workshop usage seam is matched by its stable filename prefix (the content hash changes per release), the pending package-lifecycle marker is completed with upstream's own postinstall while the set is writable (plugin installs disabled), and `openclaw.setup.activate.start` is registered in the runtime method table.
|
|
25
|
+
- FEAT: `@byokit/openclaw` names the multi-account shapes its later account surfaces build on — `Account`, `AccountId`, `AccountRef`, `SignInState`, `Room`, `RoomSpan`, `ModelInfo`, `RunSelection`, `Defaults`, `Considered`, `PickWhy`, `AccountPick` and `MoveResult` — restated structurally from `@byokit/accounts` (never imported), including the kit-only `bound`/`paid` failure states. The section 5.15 account words (Auto choice, room, pick explanations, reading age and the `account.*` refusals) join `words.json`, so they flow through the exact-sentence and plain-words tests.
|
|
26
|
+
- FEAT: `@byokit/openclaw` exports a pure start-of-run picker — `consider`, `chooseAccount` and `resolveSelection` — restated structurally from `@byokit/accounts`' `multi.ts` and never importing it. `resolveSelection` resolves `{ account: 'auto' | 'default' | id, model, needs }` with an optional kit-only `bound` list of the accounts a conversation may use, returning an `AccountPick` with its `considered` rows. Accounts outside `bound` are never candidates (a default outside `bound` is treated as not ready, its row `out: 'bound'`), and an explicit signed-out id in `bound` is returned as chosen and never replaced. The shared `fixtures/conformance/auto-pick-typescript.json` gains the kit-only bound rows, which `@byokit/accounts`' `multi.test.ts` skips.
|
|
27
|
+
- FIX: a failed engine install or post-build verification now retains a size-capped, environment-free diagnosis at
|
|
28
|
+
`<stateDir>/logs/engine-install-drift.json` before the failed temporary set is deleted: the first failed
|
|
29
|
+
root-manifest or package check (its path, expected vs actual version, or the read error), the npm path and
|
|
30
|
+
version, and the npm stderr tail. Previously the temp was deleted with the comparator's reason swallowed, so a
|
|
31
|
+
natural `drift-after-build` failure could not be diagnosed after the fact.
|
|
32
|
+
- FIX: explicitly offered sign-ins such as OpenRouter are no longer blocked by the engine plugin allow list. `KitOptions.offered` names the accounts an app offers; the kit adds the plugin of every route those name to `plugins.allow`, so an opted-in sign-in is not a dead end.
|
|
33
|
+
- FIX: the kit's `classify` now reads the ChatGPT/Codex "Next reset in N min/minutes/hour/hours" wording as well as "try again in ~N min/h", so `RunEnd.until` carries the stated reset for a subscription refusal instead of being empty; wording with no number stays without an `until` for the app's fallback.
|
|
34
|
+
- Dependency update: depend on `@byokit/pair` 0.9.0 instead of its deprecated `@byokit/link` shim.
|
|
35
|
+
|
|
5
36
|
## 0.9.0 (2026-10-09)
|
|
6
37
|
|
|
7
38
|
- Dependency update: pins @byokit/relay 0.6.0.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center"><strong>The OpenClaw runtime kit: one object that runs the pinned engine for your app.</strong><br/>
|
|
10
|
-
It drives the pinned OpenClaw engine (<code>openclaw@2026.8.
|
|
10
|
+
It drives the pinned OpenClaw engine (<code>openclaw@2026.8.35</code>, protocol 4) through one kit object. The
|
|
11
11
|
aggregator's full operator surface stays available as typed pass-through calls (<code>call</code> for every operator
|
|
12
12
|
method, <code>callDynamic</code> for the rest), with plain-words helpers for members, sign-in, runs, approvals and
|
|
13
13
|
config. For apps built on OpenClaw, where the engine holds the subscriptions.</p>
|
|
@@ -20,7 +20,7 @@ npm install @byokit/openclaw
|
|
|
20
20
|
|
|
21
21
|
[](https://www.npmjs.com/package/@byokit/openclaw) · [Latest release](https://github.com/umeranjum17/byokit/releases?q=openclaw-v) · [All releases](https://github.com/umeranjum17/byokit/releases)
|
|
22
22
|
|
|
23
|
-
Node 22.18 or later. The kit brings the engine with it: the first `start()` installs the pinned `openclaw@2026.8.
|
|
23
|
+
Node 22.18 or later. The kit brings the engine with it: the first `start()` installs the pinned `openclaw@2026.8.35`
|
|
24
24
|
under your `stateDir` (a few minutes, needs the network once), and later starts run from there. For a whole app, a
|
|
25
25
|
phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
|
|
26
26
|
[`examples/openclaw-kit`](../../examples/openclaw-kit).
|
|
@@ -30,7 +30,7 @@ phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
|
|
|
30
30
|
`engine/patches.json` declares the exact upstream tarball integrity, commit, unique edits and before/after file
|
|
31
31
|
hashes. The Gateway Workshop review patch writes content-free, durable usage facts only when the kit sets its
|
|
32
32
|
accounting environment. The separately bundled worker and all other detached kinds remain uncovered.
|
|
33
|
-
The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage);
|
|
33
|
+
The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage-d19);
|
|
34
34
|
apps do not need consumer tool aliases.
|
|
35
35
|
The full upstream MIT notice ships in [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
|
|
36
36
|
|
|
@@ -43,7 +43,10 @@ patch id (`null` after a provenance failure); a refusal is `why: 'engine-patch'`
|
|
|
43
43
|
|
|
44
44
|
Rollback selects a verified stock or older set; known sets and offline clones require no registry access.
|
|
45
45
|
Drift leaves the old bytes untouched and builds a replacement sibling; rebuilding stock may need the registry.
|
|
46
|
-
|
|
46
|
+
When an install or post-build verification fails, the kit retains a size-capped, environment-free diagnosis at
|
|
47
|
+
`<stateDir>/logs/engine-install-drift.json` — the first failed root-manifest or package check (path, expected vs
|
|
48
|
+
actual version, or the read error), the npm path and version, and the npm stderr tail — before the failed temporary
|
|
49
|
+
tree is deleted. Pre-change kits still run the untouched base install. No garbage collection: each retained set can occupy about
|
|
47
50
|
889 MB on this pin (reflinks may reduce physical storage). Whole-tree verification also adds preparation I/O;
|
|
48
51
|
read-only modes protect against accidental writes, not a malicious host app that owns the files.
|
|
49
52
|
|
|
@@ -93,7 +96,7 @@ async function dispatch(kit: OpenClawKit, taskId: string, attempt: number, messa
|
|
|
93
96
|
```
|
|
94
97
|
|
|
95
98
|
Omitting `idempotencyKey` preserves a fresh UUID per call. Supplied keys are non-empty strings passed unchanged.
|
|
96
|
-
**Not exactly-once:** engine 2026.8.
|
|
99
|
+
**Not exactly-once:** engine 2026.8.35 caches `agent` keys gateway-wide, across agents and sessions, without comparing
|
|
97
100
|
inputs. A collision silently returns the first run's result/error; a task id alone is not a safe key. Reconnects
|
|
98
101
|
retain the in-memory cache; engine process restarts lose it. Inactive entries expire after five minutes (60-second
|
|
99
102
|
cleanup tick), or earlier under the 1,000-entry cache limit; active and pending accepted runs are exempt.
|
|
@@ -185,7 +188,7 @@ RPC data is retained; existing typed `call` pass-through is unchanged. Windows a
|
|
|
185
188
|
refresh it. The engine response cache lasts 30 seconds; pending/stale/unknown data leaves `totals` absent.
|
|
186
189
|
An absent agent row is unavailable, not proof of zero usage. Reset/deletion/retention can reduce counters.
|
|
187
190
|
|
|
188
|
-
**Coverage is always `retained-transcripts-only`.** Stock 2026.8.
|
|
191
|
+
**Coverage is always `retained-transcripts-only`.** Stock 2026.8.35's detached Skill Workshop reviews are omitted
|
|
189
192
|
from its ledger; actual crash-recovery resumes and threshold memory flushes are counted through persisted
|
|
190
193
|
transcripts, never through a second side total. This reader cannot enforce a complete all-turn budget. The app owns budget,
|
|
191
194
|
share and unavailable/partial-data policy. Do not add run results to these totals: they already overlap. Engine
|
|
@@ -325,8 +328,7 @@ streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app,
|
|
|
325
328
|
|
|
326
329
|
Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
|
|
327
330
|
apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
|
|
328
|
-
`local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry.
|
|
329
|
-
in the app configuration and pass its `choice` explicitly to `kit.signIn()`.
|
|
331
|
+
`local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry. Pass the accounts the app offers in `KitOptions.offered` (the provider, auth choice or alias each route carries, e.g. `'openrouter'`); the kit allow-lists the plugin of every route those name, so an opted-in sign-in is not blocked. An app can still name a plugin directly in `config.plugins.allow`, and pass the route's `choice` explicitly to `kit.signIn()`.
|
|
330
332
|
Offered provider plugins are allowed by default alongside the app's plugin ids.
|
|
331
333
|
|
|
332
334
|
## API at a glance
|
|
@@ -335,7 +337,7 @@ Entries:
|
|
|
335
337
|
|
|
336
338
|
- `.` is the host side: `OpenClawKit`, engine supervision, config invariants, approvals (Node).
|
|
337
339
|
- `./device` is the portable client for phones and browsers (no Node imports).
|
|
338
|
-
- `./link` is the host-side `@byokit/
|
|
340
|
+
- `./link` is the host-side `@byokit/pair` adapter: member-checked ops, sealed approval push (Node).
|
|
339
341
|
- `./testing` holds `fakeGateway`, the `openclawContract` suite and the scripted model stub.
|
|
340
342
|
|
|
341
343
|
| Export | What it does |
|
|
@@ -348,7 +350,7 @@ Entries:
|
|
|
348
350
|
| `openclawDevice(link)` (`./device`) | Portable client: state, routes, sign-in, runs, steer, abort, approvals, events, sealed notices, pass-through `call`. `OpenClawDevice` and `DeviceEndFrame` name what it returns |
|
|
349
351
|
| `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
|
|
350
352
|
| `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds [`@byokit/ui`](../ui/README.md)'s `phaseOf` |
|
|
351
|
-
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/
|
|
353
|
+
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/pair` `Host`, checked per member, plus `onAction` for relay push actions. `OpenClawLinkOptions` / `OpenClawLinkHost` name both sides |
|
|
352
354
|
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
|
|
353
355
|
| `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests. `fakeGateway`'s script is typed per method (`FakeScript`, `FakeHandler`), and a recorded `StubCall` carries a typed `StubRequest` |
|
|
354
356
|
|
|
@@ -356,8 +358,8 @@ Entries:
|
|
|
356
358
|
|
|
357
359
|
Every method and event the pin publishes has a real type: `kit.call('sessions.list', params)` is checked in and
|
|
358
360
|
returns the engine's declared result, with no cast. For the slots the pin itself leaves undeclared the table stays
|
|
359
|
-
`unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.
|
|
360
|
-
of
|
|
361
|
+
`unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.35` that is 302
|
|
362
|
+
of 394 methods' params and 193 of their results, plus 21 of 55 event payloads; the other slots are not missing from
|
|
361
363
|
the kit, they are not published by the engine, and typing them here would be a guess that does not match the
|
|
362
364
|
gateway. `callDynamic` stays the same shape for a method the pin never declared.
|
|
363
365
|
|
|
@@ -423,8 +425,9 @@ itself — no manual file surgery:
|
|
|
423
425
|
|
|
424
426
|
- It stops a leftover Gateway only after verifying it is a same-user orphan of this root (matching pid, identity,
|
|
425
427
|
start time, exe/cwd and env), and only when the auth-store lock's owner is dead; then it seals the leftover live
|
|
426
|
-
`state/` and `home/`
|
|
427
|
-
unverifiable pid, another writer alive, shutdown timeout) fails
|
|
428
|
+
credential state (`state/` and the non-cache `home/` paths, see below) back into the store under the acquired
|
|
429
|
+
lock. Anything ambiguous (a live lock owner, an unverifiable pid, another writer alive, shutdown timeout) fails
|
|
430
|
+
closed with `EngineAlreadyRunningError` instead.
|
|
428
431
|
- A stale `auth-store.lock/` with a dead owner is recovered through a `recovery/` marker; interrupted
|
|
429
432
|
`cleanup`/`restoring` transitions are completed from the authenticated snapshot; empty live trees are treated as
|
|
430
433
|
debris.
|
|
@@ -438,9 +441,26 @@ and `install-home/`. None of these is safe to edit by hand, and `auth-store.seal
|
|
|
438
441
|
restore an authentic backup under the lock instead (see the credential sealing section). Keep backups before any
|
|
439
442
|
manual intervention.
|
|
440
443
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
Resealing after the kill seals every file under the engine `state` tree and the credential paths under `home`.
|
|
445
|
+
Credential files under `state` and `home` go into the bounded credential blob. Every other file under `state` is
|
|
446
|
+
sealed as its own object under `auth-store.objects/`, outside that bound: the shared and per-agent SQLite databases
|
|
447
|
+
(with their `-wal`/`-shm`/`-journal` sidecars, which mix credentials with session and transcript rows), exported
|
|
448
|
+
transcript artifacts (`state/transcripts`), legacy session stores (`state/sessions`, `state/agents/<agentId>/sessions`),
|
|
449
|
+
the media stores (`state/media`, `state/delivery-queue-media`), the control-UI and other caches (`state/cache`,
|
|
450
|
+
`state/completions`), logs (`state/logs`) and gateway temp/lock files (`state/tmp`). However large a long-used host's
|
|
451
|
+
transcripts and media grow in total, they never count toward the bound; each such file is capped individually at the same
|
|
452
|
+
limit (below). In `home` the XDG cache and npm cache homes
|
|
453
|
+
(`home/.cache`, `home/.npm`) and the transcript, log and cache subtrees of the CLIs the engine runs (`home/.codex`
|
|
454
|
+
sessions/log/cache/`.tmp`/`history.jsonl`, `home/.claude` projects/todos/shell-snapshots/statsig/file-history/`history.jsonl`)
|
|
455
|
+
stay on disk unsealed. Unknown paths are still sealed in the blob (a credential location we do not know about must
|
|
456
|
+
fail loudly, never drop silently). The blob is bounded: if the collected credential files together pass a fixed
|
|
457
|
+
limit, `prepare()`, `start()`, `stop()` and an engine exit reject with exported `AuthStoreSealSizeError`
|
|
458
|
+
(`code: 'auth-store-seal-size'`, fields `size` and `cap`). On that refusal the last good saved store is kept as it
|
|
459
|
+
was and no live file is deleted; nothing else is promised. A refused `stop()` still releases the lock. Its `size` is a
|
|
460
|
+
lower bound (the running total at the refusal). A single object file over the same limit refuses the same way, naming
|
|
461
|
+
the file in the error message, before any file is removed. Do not move or delete files under `state/` or `home/` to get under the
|
|
462
|
+
limit: the next successful seal would drop them. If your app hits this, report the error's size and cap to the byokit
|
|
463
|
+
maintainers; the limit was reached in real use.
|
|
444
464
|
|
|
445
465
|
## App-owned task recovery
|
|
446
466
|
|
|
@@ -466,15 +486,16 @@ that decision. It needs the published kit's bundled engine patch, not a separate
|
|
|
466
486
|
|
|
467
487
|
## Phones and browsers
|
|
468
488
|
|
|
469
|
-
The host side serves member-checked ops over `@byokit/
|
|
489
|
+
The host side serves member-checked ops over `@byokit/pair`; the phone or browser uses the portable client.
|
|
490
|
+
(This kit depends on [`@byokit/pair`](../pair), and its examples import from there.)
|
|
470
491
|
|
|
471
492
|
```ts
|
|
472
|
-
import { Host, keyPair, type DeviceLink } from '@byokit/
|
|
493
|
+
import { Host, keyPair, type DeviceLink } from '@byokit/pair';
|
|
473
494
|
import { OpenClawKit } from '@byokit/openclaw';
|
|
474
495
|
import { openclawLink, serve } from '@byokit/openclaw/link';
|
|
475
496
|
import { openclawDevice } from '@byokit/openclaw/device';
|
|
476
497
|
|
|
477
|
-
// On the computer (Node): member-checked ops over @byokit/
|
|
498
|
+
// On the computer (Node): member-checked ops over @byokit/pair, served per reach.
|
|
478
499
|
declare const kit: OpenClawKit;
|
|
479
500
|
declare const ask: (question: string) => Promise<boolean>; // your own prompt
|
|
480
501
|
const api = openclawLink(kit, { memberOf: (grant) => (grant.meta as { member?: string } | undefined)?.member });
|
|
@@ -505,9 +526,14 @@ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-ki
|
|
|
505
526
|
|
|
506
527
|
The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
|
|
507
528
|
credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
|
|
508
|
-
archives. `authSeal` therefore protects credential state —
|
|
509
|
-
|
|
510
|
-
|
|
529
|
+
archives. `authSeal` therefore protects credential state — config/credential paths under `home` and every credential
|
|
530
|
+
path under the isolated `state` tree — in one bounded blob, `auth-store.sealed`. Every other file under `state`
|
|
531
|
+
(the shared state database `state/state/openclaw.sqlite` and the per-agent databases
|
|
532
|
+
`state/agents/<agentId>/agent/openclaw-agent.sqlite`, which mix credentials with transcript rows, plus transcripts,
|
|
533
|
+
media, logs, caches and session stores) is sealed as its own file under `auth-store.objects/`, outside the bound, so
|
|
534
|
+
a long-used host's total history never makes `start()` or `stop()` refuse; only a single file over the cap does. Regenerable tool caches in `home` stay on disk
|
|
535
|
+
unsealed. Only a credential blob, or a single engine store file, over the cap still refuses `start()` and `stop()` with
|
|
536
|
+
the typed size error instead of aborting the process. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
|
|
511
537
|
uses a persistent private file key for new stores when no non-interactive keyring is available.
|
|
512
538
|
Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
|
|
513
539
|
`kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
|
|
@@ -548,13 +574,16 @@ every retained sealed migration archive before retiring an automatic key. Rotate
|
|
|
548
574
|
separately; dual-wrap rotation retains old wrapping keys for backups.
|
|
549
575
|
|
|
550
576
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
551
|
-
only
|
|
552
|
-
disk unsealed. The adapter authenticates the snapshot
|
|
577
|
+
only sealed files: `auth-store.sealed` for the credential paths, and `auth-store.objects/` for the other engine stores
|
|
578
|
+
under `state`; regenerable caches under `home` stay on disk unsealed. The adapter authenticates the snapshot
|
|
553
579
|
before any restoration; a missing adapter rejects. A store the key cannot open (a wrong key,
|
|
554
580
|
damaged or tampered bytes) or whose payload is not a credential snapshot stays unchanged at
|
|
555
581
|
`auth-store.sealed`. Both `prepare()` and `start()` reject with exported `AuthStoreUnreadableError`
|
|
556
582
|
(`code: 'auth-store-unreadable'`, `reason: 'auth-failed' | 'invalid-snapshot'`). The kit reports
|
|
557
583
|
`{ phase: 'failed', why: 'auth-store-unreadable' }`; show `stateWords(kit.state)`, not a sign-in prompt.
|
|
584
|
+
Saved sign-in data over the cap rejects with `AuthStoreSealSizeError`, and the kit reports
|
|
585
|
+
`{ phase: 'failed', why: 'auth-store-seal-size', sealSize: { size, cap, file? } }` (`file` is set when an engine store is the one over the cap); the refusal rules are in
|
|
586
|
+
[Retained home killed without stop()](#retained-home-killed-without-stop).
|
|
558
587
|
Restore access to the original seal/key (and the original service/stateDir binding if the adapter uses
|
|
559
588
|
one), then retry `start()` with that adapter. Do not generate/rotate a key to repair an unreadable store.
|
|
560
589
|
If the bytes are damaged, stop all writers, acquire the host writer lock, and restore an authentic
|
|
@@ -604,7 +633,7 @@ retained-login cleanup and sealed approval limits.
|
|
|
604
633
|
|
|
605
634
|
## Status
|
|
606
635
|
|
|
607
|
-
Pinned to OpenClaw `2026.8.
|
|
636
|
+
Pinned to OpenClaw `2026.8.35` (protocol 4); `ENGINE_VERSION` and `PROTOCOL_VERSION` carry the pin. Signatures are
|
|
608
637
|
frozen (docs/runtime-kits.md §5).
|
|
609
638
|
|
|
610
639
|
## Links
|
package/SECURITY.md
CHANGED
|
@@ -41,7 +41,7 @@ for the public error and host repair procedure. Other sealing failures still rej
|
|
|
41
41
|
| Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
|
|
42
42
|
| Device identity | `openclaw/device.json` | Ed25519 private key stored as **unencrypted PKCS#8 PEM** in JSON, newly created 0600. The transport accepts both kit and legacy paired shapes without rewriting. Theft can impersonate the operator identity. |
|
|
43
43
|
| Provider profiles | `openclaw/state/` and `openclaw/home/`, including agent/shared SQLite databases, journals and migration JSON | Access/refresh tokens or API keys are **plaintext while running**; without `authSeal` they also remain plaintext while stopped. Migration stages `auth-profiles.json` at 0600 in a newly created 0700 directory. Theft may authorize provider calls and refresh. |
|
|
44
|
-
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (
|
|
44
|
+
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (credential paths under `state` plus config/credential paths under `home`) while stopped; other engine `state` stores are sealed as objects under `auth-store.objects/`; regenerable caches stay on disk unsealed. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
|
|
45
45
|
| Saved configuration, logs and sessions | `openclaw/openclaw.json`, `logs/`, engine state | May contain app-supplied keys, prompts, tool inputs, sign-in URLs/codes or provider diagnostics. Treat the whole tree as secret. Engine stdout/stderr is appended to a newly created 0600 log; it is not a guaranteed credential-redaction layer. |
|
|
46
46
|
| Notice seed and decrypted approvals | Device app storage and memory | The app stores the 32-byte seed; the kit receives it only for key derivation/decryption. Seed theft exposes notices for its box key. |
|
|
47
47
|
|
|
@@ -52,8 +52,8 @@ collects regular files and file symlinks whose fully resolved targets are regula
|
|
|
52
52
|
files within the isolated engine root; links restore as regular files at the link
|
|
53
53
|
paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
|
|
54
54
|
skipped without reading their contents. Collected POSIX modes are normalized;
|
|
55
|
-
other paths still need host protection.
|
|
56
|
-
|
|
55
|
+
other paths still need host protection. Regenerable caches under `home` stay on disk
|
|
56
|
+
unsealed after successful sealing; other skipped entries are not preserved in the snapshot or the objects. Never use a shared/writable tree or point
|
|
57
57
|
the kit at another product's state. Review existing permissions before adopting a
|
|
58
58
|
tree; use an OS-protected app directory, encrypted disk and restricted backups.
|
|
59
59
|
Do not log `doctorContext().env`, transport arguments, auth records or sign-in
|
|
@@ -61,8 +61,9 @@ callbacks. JavaScript strings and engine memory are not reliably zeroized.
|
|
|
61
61
|
|
|
62
62
|
The engine needs plaintext credentials and PEM during operation. With `authSeal`
|
|
63
63
|
(a host-injected `SealingAdapter` from `@byokit/secrets`), `src/auth-store.ts` seals
|
|
64
|
-
credential state —
|
|
65
|
-
|
|
64
|
+
credential state — credential paths under `state` plus config/credential paths under `home`, including SQLite journals — into one bounded blob,
|
|
65
|
+
and every other engine `state` store into its own object under `auth-store.objects/`, on successful `prepare()` and after
|
|
66
|
+
the engine exits on `stop()`; regenerable caches stay on disk unsealed. The scope is defined in [credential sealing](README.md#credential-sealing-and-threat-model). It verifies the adapter round-trip, writes via an
|
|
66
67
|
exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
|
|
67
68
|
removing live plaintext. `start()` authenticates and validates snapshot paths before
|
|
68
69
|
restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createLocks } from './locks.ts';
|
|
2
|
+
import type { Account, AccountId, Defaults, GatewayTransport, Member } from './types.ts';
|
|
3
|
+
export declare function createAccounts(ctx: {
|
|
4
|
+
request: GatewayTransport['request'];
|
|
5
|
+
root: string;
|
|
6
|
+
ensure(member: Member): Promise<{
|
|
7
|
+
agentId: string;
|
|
8
|
+
workspace: string;
|
|
9
|
+
}>;
|
|
10
|
+
locks: ReturnType<typeof createLocks>;
|
|
11
|
+
}): {
|
|
12
|
+
list(member: Member): Promise<Account[]>;
|
|
13
|
+
rename(member: Member, id: AccountId, name: string): Promise<Account>;
|
|
14
|
+
defaults(member: Member): Promise<Defaults>;
|
|
15
|
+
setDefaults(member: Member, d: Defaults): Promise<void>;
|
|
16
|
+
};
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// The per-member account index (5.15, O14 step S5): `<root>/accounts/<member>.json` (0600, whole-file temp
|
|
2
|
+
// plus rename) is the only kit state: `{ v: 1, accounts: { [id]: { agent, name, route, addedAt, until?, model?,
|
|
3
|
+
// who? } }, defaults }`. No credential, token, email or engine line. Listing, rename and defaults live here;
|
|
4
|
+
// add, remove, models, room, pick and move arrive in later steps.
|
|
5
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { KEY_PREFIX, MEMBER_ID } from "./members.js";
|
|
9
|
+
import { routes, routeFor } from "./routes.js";
|
|
10
|
+
const ACCOUNT_AGENT = /^[a-z][a-z0-9-]{0,31}--[0-9a-f]{6}$/;
|
|
11
|
+
const usable = (status) => status === 'ok' || status === 'expiring' || status === 'static';
|
|
12
|
+
const empty = () => ({ v: 1, accounts: {}, defaults: {} });
|
|
13
|
+
const validEntry = (v) => !!v && typeof v === 'object'
|
|
14
|
+
&& typeof v.agent === 'string' && typeof v.name === 'string'
|
|
15
|
+
&& typeof v.route === 'string' && Number.isFinite(v.addedAt)
|
|
16
|
+
&& (v.until === undefined || Number.isFinite(v.until))
|
|
17
|
+
&& (v.model === undefined || typeof v.model === 'string')
|
|
18
|
+
&& (v.who === undefined || typeof v.who === 'string');
|
|
19
|
+
export function createAccounts(ctx) {
|
|
20
|
+
const path = (member) => join(ctx.root, 'accounts', `${member}.json`);
|
|
21
|
+
const check = (member) => {
|
|
22
|
+
if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
|
|
23
|
+
throw new Error(`invalid member id: ${member}`);
|
|
24
|
+
};
|
|
25
|
+
const read = (member) => {
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = readFileSync(path(member), 'utf8');
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return empty();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const file = JSON.parse(text);
|
|
36
|
+
if (!file || typeof file !== 'object' || file.v !== 1)
|
|
37
|
+
return empty();
|
|
38
|
+
const accounts = {};
|
|
39
|
+
for (const [id, entry] of Object.entries(file.accounts ?? {}))
|
|
40
|
+
if (validEntry(entry))
|
|
41
|
+
accounts[id] = entry;
|
|
42
|
+
const raw = file.defaults && typeof file.defaults === 'object' ? file.defaults : {};
|
|
43
|
+
const defaults = {};
|
|
44
|
+
if (typeof raw.account === 'string')
|
|
45
|
+
defaults.account = raw.account;
|
|
46
|
+
if (typeof raw.model === 'string')
|
|
47
|
+
defaults.model = raw.model;
|
|
48
|
+
if (typeof raw.auto === 'boolean')
|
|
49
|
+
defaults.auto = raw.auto;
|
|
50
|
+
return { v: 1, accounts, defaults };
|
|
51
|
+
};
|
|
52
|
+
const write = (member, file) => {
|
|
53
|
+
mkdirSync(join(ctx.root, 'accounts'), { recursive: true, mode: 0o700 });
|
|
54
|
+
const tmp = `${path(member)}.${randomUUID()}.tmp`;
|
|
55
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + '\n');
|
|
56
|
+
chmodSync(tmp, 0o600);
|
|
57
|
+
try {
|
|
58
|
+
renameSync(tmp, path(member));
|
|
59
|
+
chmodSync(path(member), 0o600);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rmSync(tmp, { force: true });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const metaOf = (choice) => {
|
|
66
|
+
const row = routes().find((r) => r.choice === choice);
|
|
67
|
+
if (row && (row.billing === 'subscription' || row.billing === 'api'))
|
|
68
|
+
return { label: row.label, billing: row.billing };
|
|
69
|
+
return { label: choice, billing: 'subscription' };
|
|
70
|
+
};
|
|
71
|
+
const synthesizedRoute = (provider) => routeFor(provider, 'browser')?.choice
|
|
72
|
+
?? routes().find((r) => r.provider === provider && r.billing === 'subscription')?.choice
|
|
73
|
+
?? provider;
|
|
74
|
+
const rosterOf = () => ctx.request('agents.list')
|
|
75
|
+
.then((r) => new Set(r.agents.map((a) => a.id)), () => new Set());
|
|
76
|
+
const profilesOf = (agentId) => ctx.request('byokit.accounts', { action: 'list', agentId })
|
|
77
|
+
.then((r) => Array.isArray(r.profiles) ? r.profiles : [], () => []);
|
|
78
|
+
const stateOf = (route, status, until) => {
|
|
79
|
+
if (!routes().some((r) => r.choice === route))
|
|
80
|
+
return 'not_included';
|
|
81
|
+
if (until !== undefined && until > Date.now())
|
|
82
|
+
return 'resting';
|
|
83
|
+
if (!status)
|
|
84
|
+
return 'signed_out';
|
|
85
|
+
return usable(status) ? 'ready' : status === 'expired' ? 'needs_again' : 'signed_out';
|
|
86
|
+
};
|
|
87
|
+
// Lock-free row builder over one snapshot: mutates `file` (synthesized entries, `who` backfill, dropped
|
|
88
|
+
// dangling default) and reports whether it did. Callers own the locking.
|
|
89
|
+
const build = async (member, agentId, file, roster, memberProfiles) => {
|
|
90
|
+
let dirty = false;
|
|
91
|
+
const byProvider = new Map();
|
|
92
|
+
for (const p of memberProfiles) {
|
|
93
|
+
if (typeof p.provider !== 'string' || !p.provider)
|
|
94
|
+
continue;
|
|
95
|
+
byProvider.set(p.provider, [...(byProvider.get(p.provider) ?? []), p]);
|
|
96
|
+
}
|
|
97
|
+
const memberRows = [];
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (const [provider, profiles] of [...byProvider.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
100
|
+
let entry = file.accounts[provider];
|
|
101
|
+
if (!entry || entry.agent !== agentId) {
|
|
102
|
+
const route = entry?.agent === agentId && typeof entry.route === 'string' ? entry.route : synthesizedRoute(provider);
|
|
103
|
+
const same = Object.values(file.accounts).filter((e) => e.agent === agentId).length;
|
|
104
|
+
entry = { agent: agentId, name: `${metaOf(route).label} ${same + 1}`, route, addedAt: 0 };
|
|
105
|
+
file.accounts[provider] = entry;
|
|
106
|
+
dirty = true;
|
|
107
|
+
}
|
|
108
|
+
if (!entry.who && typeof profiles[0]?.who === 'string') {
|
|
109
|
+
entry.who = profiles[0].who;
|
|
110
|
+
dirty = true;
|
|
111
|
+
}
|
|
112
|
+
const meta = metaOf(entry.route);
|
|
113
|
+
memberRows.push({ id: provider, provider, route: entry.route, name: entry.name, label: meta.label,
|
|
114
|
+
billing: meta.billing, state: stateOf(entry.route, profiles[0]?.status, entry.until),
|
|
115
|
+
...(entry.until !== undefined ? { until: entry.until } : {}), addedAt: entry.addedAt });
|
|
116
|
+
seen.add(provider);
|
|
117
|
+
}
|
|
118
|
+
for (const [id, entry] of Object.entries(file.accounts)) {
|
|
119
|
+
if (entry.agent !== agentId || id.includes('.') || seen.has(id))
|
|
120
|
+
continue;
|
|
121
|
+
const stale = metaOf(entry.route);
|
|
122
|
+
memberRows.push({ id, provider: id, route: entry.route, name: entry.name, label: stale.label,
|
|
123
|
+
billing: stale.billing, state: stateOf(entry.route, undefined, entry.until),
|
|
124
|
+
...(entry.until !== undefined ? { until: entry.until } : {}), addedAt: entry.addedAt });
|
|
125
|
+
}
|
|
126
|
+
// Account agents: only what the index names, with exactly `<member>--<hex6>` shape. An unindexed
|
|
127
|
+
// agent of that shape is never listed, and an entry whose agent is gone is not listed either.
|
|
128
|
+
const agentRows = [];
|
|
129
|
+
for (const [id, entry] of Object.entries(file.accounts)) {
|
|
130
|
+
if (entry.agent === agentId || !ACCOUNT_AGENT.test(entry.agent) || !entry.agent.startsWith(`${member}--`))
|
|
131
|
+
continue;
|
|
132
|
+
if (!roster.has(entry.agent))
|
|
133
|
+
continue;
|
|
134
|
+
const provider = id.split('.')[0];
|
|
135
|
+
const meta = metaOf(entry.route);
|
|
136
|
+
const profiles = await profilesOf(entry.agent);
|
|
137
|
+
agentRows.push({ id, provider, route: entry.route, name: entry.name, label: meta.label,
|
|
138
|
+
billing: meta.billing, state: stateOf(entry.route, profiles[0]?.status, entry.until),
|
|
139
|
+
...(entry.until !== undefined ? { until: entry.until } : {}), addedAt: entry.addedAt });
|
|
140
|
+
}
|
|
141
|
+
memberRows.sort((a, b) => a.addedAt - b.addedAt || (a.id < b.id ? -1 : 1));
|
|
142
|
+
agentRows.sort((a, b) => a.addedAt - b.addedAt);
|
|
143
|
+
const rows = [...memberRows, ...agentRows];
|
|
144
|
+
if (file.defaults.account !== undefined && !rows.some((r) => r.id === file.defaults.account)) {
|
|
145
|
+
delete file.defaults.account;
|
|
146
|
+
dirty = true;
|
|
147
|
+
}
|
|
148
|
+
return { rows, dirty };
|
|
149
|
+
};
|
|
150
|
+
const gather = (agentId) => Promise.all([rosterOf(), profilesOf(agentId)]);
|
|
151
|
+
const buildRows = async (member, agentId) => {
|
|
152
|
+
const file = read(member);
|
|
153
|
+
const [roster, profiles] = await gather(agentId);
|
|
154
|
+
return { ...(await build(member, agentId, file, roster, profiles)), file };
|
|
155
|
+
};
|
|
156
|
+
async function list(member) {
|
|
157
|
+
check(member);
|
|
158
|
+
const { agentId } = await ctx.ensure(member);
|
|
159
|
+
const first = await ctx.locks.shared(agentId, () => buildRows(member, agentId));
|
|
160
|
+
if (!first.dirty)
|
|
161
|
+
return first.rows;
|
|
162
|
+
// A write is needed (synthesized entry, `who` backfill or dropped default): redo it under the
|
|
163
|
+
// exclusive lock so two first-lists cannot interleave their read-modify-write.
|
|
164
|
+
return ctx.locks.exclusive([agentId], async () => {
|
|
165
|
+
const { rows, dirty, file } = await buildRows(member, agentId);
|
|
166
|
+
if (dirty)
|
|
167
|
+
write(member, file);
|
|
168
|
+
return rows;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function rename(member, id, name) {
|
|
172
|
+
check(member);
|
|
173
|
+
if (typeof name !== 'string')
|
|
174
|
+
throw new Error('renameAccount: name is empty');
|
|
175
|
+
const trimmed = name.trim();
|
|
176
|
+
if (!trimmed || trimmed.length > 100 || /[\u0000-\u001f\u007f]/.test(trimmed))
|
|
177
|
+
throw new Error('renameAccount: name is empty');
|
|
178
|
+
const { agentId } = await ctx.ensure(member);
|
|
179
|
+
if (!(await list(member)).some((r) => r.id === id))
|
|
180
|
+
throw new Error(`unknown account: ${id}`);
|
|
181
|
+
await ctx.locks.exclusive([agentId], async () => {
|
|
182
|
+
const file = read(member);
|
|
183
|
+
const entry = file.accounts[id];
|
|
184
|
+
if (!entry)
|
|
185
|
+
throw new Error(`unknown account: ${id}`);
|
|
186
|
+
entry.name = trimmed;
|
|
187
|
+
write(member, file);
|
|
188
|
+
});
|
|
189
|
+
const done = (await list(member)).find((r) => r.id === id);
|
|
190
|
+
if (!done)
|
|
191
|
+
throw new Error(`unknown account: ${id}`);
|
|
192
|
+
return done;
|
|
193
|
+
}
|
|
194
|
+
async function defaults(member) {
|
|
195
|
+
await list(member);
|
|
196
|
+
return read(member).defaults;
|
|
197
|
+
}
|
|
198
|
+
async function setDefaults(member, d) {
|
|
199
|
+
check(member);
|
|
200
|
+
if (!d || typeof d !== 'object')
|
|
201
|
+
throw new Error('setDefaults: defaults are invalid');
|
|
202
|
+
if (d.account !== undefined && typeof d.account !== 'string')
|
|
203
|
+
throw new Error('setDefaults: defaults are invalid');
|
|
204
|
+
if (d.model !== undefined && typeof d.model !== 'string')
|
|
205
|
+
throw new Error('setDefaults: defaults are invalid');
|
|
206
|
+
if (d.auto !== undefined && typeof d.auto !== 'boolean')
|
|
207
|
+
throw new Error('setDefaults: defaults are invalid');
|
|
208
|
+
const { agentId } = await ctx.ensure(member);
|
|
209
|
+
return ctx.locks.exclusive([agentId], async () => {
|
|
210
|
+
const file = read(member);
|
|
211
|
+
const next = {};
|
|
212
|
+
if (d.account !== undefined)
|
|
213
|
+
next.account = d.account;
|
|
214
|
+
if (d.model !== undefined)
|
|
215
|
+
next.model = d.model;
|
|
216
|
+
if (d.auto !== undefined)
|
|
217
|
+
next.auto = d.auto;
|
|
218
|
+
write(member, { ...file, defaults: next });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { list, rename, defaults, setDefaults };
|
|
222
|
+
}
|
package/dist/auth-status.d.ts
CHANGED
package/dist/auth-store.d.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
-
/** True for the
|
|
3
|
-
export declare function
|
|
2
|
+
/** True for the non-credential paths: under `home` they are never sealed; under `state` they are sealed as objects. */
|
|
3
|
+
export declare function nonCredential(name: string): boolean;
|
|
4
4
|
/** Opening failed; the original snapshot stays in place for repair and retry. */
|
|
5
5
|
export declare class AuthStoreUnreadableError extends Error {
|
|
6
6
|
readonly code = "auth-store-unreadable";
|
|
7
7
|
readonly reason: 'auth-failed' | 'invalid-snapshot';
|
|
8
8
|
constructor(reason: 'auth-failed' | 'invalid-snapshot');
|
|
9
9
|
}
|
|
10
|
+
/** Total raw credential bytes one snapshot may seal. The sealed payload is one runtime string, so collect()
|
|
11
|
+
* refuses a larger snapshot before reading it and the process never aborts inside the sealer. Credential
|
|
12
|
+
* state is normally a few MiB; the cap leaves several times that headroom. The same bound applies to each
|
|
13
|
+
* non-credential engine store under `state` sealed as its own object; their total is not bounded. */
|
|
14
|
+
export declare const SEAL_CAP_BYTES: number;
|
|
15
|
+
/** The saved sign-in data (or one object file, when `file` names it) is over `SEAL_CAP_BYTES`. On a refusal the
|
|
16
|
+
* last good saved store is kept as it was and no live file is deleted. `size` is a lower bound for the whole
|
|
17
|
+
* store (the running total when the cap was crossed) and the exact size when `file` is set. */
|
|
18
|
+
export declare class AuthStoreSealSizeError extends Error {
|
|
19
|
+
readonly code = "auth-store-seal-size";
|
|
20
|
+
readonly size: number;
|
|
21
|
+
readonly cap: number;
|
|
22
|
+
readonly file?: string;
|
|
23
|
+
constructor(size: number, cap: number, file?: string);
|
|
24
|
+
}
|
|
10
25
|
export declare class AuthStore {
|
|
11
26
|
private active;
|
|
12
27
|
private owned;
|
|
13
28
|
private queue;
|
|
14
29
|
private readonly file;
|
|
30
|
+
private readonly objects;
|
|
15
31
|
private readonly lock;
|
|
16
32
|
private readonly cleanup;
|
|
17
33
|
private readonly restoring;
|
|
34
|
+
private readonly objectHashes;
|
|
18
35
|
constructor(privateOptions: {
|
|
19
36
|
root: string;
|
|
20
37
|
stateDir: string;
|
|
@@ -26,13 +43,21 @@ export declare class AuthStore {
|
|
|
26
43
|
private serial;
|
|
27
44
|
private acquire;
|
|
28
45
|
private release;
|
|
46
|
+
private open;
|
|
47
|
+
private seal;
|
|
29
48
|
private read;
|
|
49
|
+
private readObject;
|
|
50
|
+
private inventory;
|
|
30
51
|
private collect;
|
|
31
|
-
/** Remove the live
|
|
32
|
-
*
|
|
33
|
-
* and the directories still holding them are left in place. */
|
|
52
|
+
/** Remove the live sealed trees: all of `state` (every file there is sealed, as a blob entry or an object),
|
|
53
|
+
* and in `home` exactly the sealed paths — regenerable caches and the directories still holding them are left in place. */
|
|
34
54
|
private remove;
|
|
55
|
+
private sealObjects;
|
|
35
56
|
private persist;
|
|
57
|
+
/** `mkdir -p` for a restore target that records the parent of every directory it creates: a new directory
|
|
58
|
+
* changes its parent's entries, so the parent needs a directory fsync too. Stops at the store root (it exists)
|
|
59
|
+
* and never fsyncs now; restore() fsyncs the collected directories once after all files are written. */
|
|
60
|
+
private ensureRestoreDirs;
|
|
36
61
|
private restore;
|
|
37
62
|
archives(): Promise<void>;
|
|
38
63
|
prepare(): Promise<void>;
|