@byokit/openclaw 0.3.1 → 0.3.3
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 +19 -0
- package/README.md +23 -2
- package/SECURITY.md +203 -0
- package/dist/auth-store.js +11 -1
- package/dist/config.js +3 -1
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +29 -3
- package/dist/kit.js +1 -1
- package/dist/link.js +1 -1
- package/dist/routes.d.ts +2 -1
- package/dist/routes.js +2 -1
- package/dist/routes.json +3 -3
- package/dist/types.d.ts +1 -1
- package/dist/words.js +1 -0
- package/dist/words.json +1 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.3 (2026-09-30)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/secrets 0.5.0.
|
|
8
|
+
|
|
9
|
+
- SECURITY: Opt-in dual-wrap credential upgrades are authenticated and atomically replaced under the store lock; protection is only as strong as the owner-only host-key file, which must stay out of sealed-store backups.
|
|
10
|
+
- FIX: A locked or unresponsive keyring no longer throws from prepare or start. The kit reports a locked saved sign-in in plain words, preserves the sealed store and retries normally on a later start after unlock; dual-wrapped stores open through their host key without prompting.
|
|
11
|
+
|
|
12
|
+
- FIX: ChatGPT subscription device pairing was blocked by the plugin allowlist unless the app added the provider plugin itself. The kit now merges every offered route's provider plugin with the bridge and caller-provided plugins, without adding plugins for unoffered routes.
|
|
13
|
+
- Label the explicit Anthropic route as API key (billed per use), keeping API and plan routes off by default.
|
|
14
|
+
|
|
15
|
+
## 0.3.2 (2026-09-30)
|
|
16
|
+
|
|
17
|
+
- Dependency update: pins @byokit/secrets 0.4.0.
|
|
18
|
+
|
|
19
|
+
- SECURITY: View-only devices can no longer resolve approvals through relay push actions.
|
|
20
|
+
- FIX: Existing authSeal calls using osKeyringSeal automatically pick up persistent headless sealing, including locked and unresponsive keyring fallback, without host changes.
|
|
21
|
+
- Document the credential, migration and sealed-approval threat model in SECURITY.md, shipped with the package.
|
|
22
|
+
- Run the shared engine isolation contract in npm test with an offline child; keep the pinned-engine check in test:engine.
|
|
23
|
+
|
|
5
24
|
## 0.3.1 (2026-09-30)
|
|
6
25
|
|
|
7
26
|
|
package/README.md
CHANGED
|
@@ -171,7 +171,17 @@ person's back (memory search is never a paid provider).
|
|
|
171
171
|
The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
|
|
172
172
|
credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
|
|
173
173
|
archives. `authSeal` therefore protects the complete isolated `state` and `home` directories, including SQLite
|
|
174
|
-
journals. It uses the
|
|
174
|
+
journals. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
|
|
175
|
+
uses a persistent private file key for new stores when no non-interactive keyring is available.
|
|
176
|
+
Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
|
|
177
|
+
`kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
|
|
178
|
+
recovery, and keep the sealed snapshot unchanged without launching the engine. Call `start()` again
|
|
179
|
+
after unlock to restore the same credentials, even if a fallback key directory now exists.
|
|
180
|
+
|
|
181
|
+
For hosts whose keyring stays locked, opt into `osKeyringSeal({ service, dualWrap: true })`.
|
|
182
|
+
While unlocked, reading an existing keyring-only snapshot atomically upgrades it to dual-wrap.
|
|
183
|
+
Later starts and stops can use the host wrap without prompting. Default off: **protection is only
|
|
184
|
+
as strong as the owner-only host-key file**, which must stay outside sealed-store backups.
|
|
175
185
|
|
|
176
186
|
```ts
|
|
177
187
|
import { osKeyringSeal, hostKeySeal } from '@byokit/secrets';
|
|
@@ -185,7 +195,8 @@ await kit.prepare(); // seals an existing plaintext store and migration archives
|
|
|
185
195
|
await kit.start(); // authenticates and restores the isolated files for the engine
|
|
186
196
|
await kit.stop(); // waits for exit, verifies an atomic sealed snapshot, then removes plaintext
|
|
187
197
|
|
|
188
|
-
//
|
|
198
|
+
// Inspect the adapter's write mode: 'keyring', 'host-key-file' or opt-in 'dual-wrap'.
|
|
199
|
+
// Hosts can also explicitly supply a 32-byte key kept outside stateDir and its backups:
|
|
189
200
|
declare const hostKey: Uint8Array;
|
|
190
201
|
const server = new OpenClawKit({
|
|
191
202
|
stateDir: './server-state',
|
|
@@ -193,6 +204,13 @@ const server = new OpenClawKit({
|
|
|
193
204
|
});
|
|
194
205
|
```
|
|
195
206
|
|
|
207
|
+
The automatic key lives in the platform state directory, separate from the engine store. A key
|
|
208
|
+
on the same disk protects copied stores/backups only when the key directory is excluded; it does
|
|
209
|
+
not protect against code running as the same OS user. See [secrets' rotation and threat model](../secrets/README.md#servers-and-headless-node).
|
|
210
|
+
Stop the kit, acquire the host writer lock and call `seal.rotate()` with `auth-store.sealed` plus
|
|
211
|
+
every retained sealed migration archive before retiring an automatic key. Rotate each envelope mode
|
|
212
|
+
separately; dual-wrap rotation retains old wrapping keys for backups.
|
|
213
|
+
|
|
196
214
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
197
215
|
only a sealed snapshot, `auth-store.sealed`, for those directories. The adapter authenticates the snapshot
|
|
198
216
|
before any restoration; a wrong key, tampering, or a missing adapter rejects. Files restored for the engine
|
|
@@ -224,6 +242,9 @@ install/cache files and app workspaces are outside this adapter's sealing scope;
|
|
|
224
242
|
`doctorContext()` is an advanced escape hatch: launching an external doctor bypasses this lifecycle; use the
|
|
225
243
|
kit's migration method for sealed stores.
|
|
226
244
|
|
|
245
|
+
See [SECURITY.md](SECURITY.md) for the credential and approval threat model, private-directory requirements,
|
|
246
|
+
retained-login cleanup and sealed approval limits.
|
|
247
|
+
|
|
227
248
|
## Status
|
|
228
249
|
|
|
229
250
|
Pinned to OpenClaw `2026.8.1` (protocol 4); `ENGINE_VERSION` and `PROTOCOL_VERSION` carry the pin. Signatures are
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# OpenClaw kit security
|
|
2
|
+
|
|
3
|
+
## Scope and trust boundaries
|
|
4
|
+
|
|
5
|
+
This is the credential and approval threat model for `@byokit/openclaw`, reviewed
|
|
6
|
+
against engine **2026.8.1** on **2026-09-30**. The authoritative runtime design is
|
|
7
|
+
[runtime-kits.md](../../docs/runtime-kits.md), especially §§4.2, 5.5–5.9 and 7.1–7.3.
|
|
8
|
+
This review covers the kit's supervisor, migration, transport and sealed approval
|
|
9
|
+
path, including the credential-store lifecycle in kit 0.3.0; it is not an independent audit of the engine or its transitive dependencies.
|
|
10
|
+
|
|
11
|
+
The host app, its member mapping, configuration, plugin allowlist, tools and chosen
|
|
12
|
+
executables are trusted. The app must give each installation a private, app-owned
|
|
13
|
+
`stateDir`, with one writer, and protect its parent directory from other users.
|
|
14
|
+
An explicitly supplied `engineDir`, retained-login path and install-policy roots
|
|
15
|
+
are also trusted app choices. Untrusted inputs include model output, tool requests,
|
|
16
|
+
paired devices and relay/push payloads. Members separate application accounts;
|
|
17
|
+
they are not operating-system sandboxes within the shared engine process.
|
|
18
|
+
|
|
19
|
+
The kit protects against accidental discovery of another tool's credentials,
|
|
20
|
+
unauthorized device operations, wrong-member approvals and disclosure of approval
|
|
21
|
+
content to the relay. It cannot protect credentials from root, another process
|
|
22
|
+
running as the app's user, a compromised host/engine/plugin, or readable backups.
|
|
23
|
+
The isolated environment and tool gate are not an OS filesystem/network sandbox.
|
|
24
|
+
The app must sandbox `ToolHost.call` and judge requests in `ToolHost.gate`.
|
|
25
|
+
|
|
26
|
+
A locked or unresponsive keyring leaves the sealed snapshot unchanged. `prepare()` and `start()`
|
|
27
|
+
resolve with `phase: 'locked'`, do not restore plaintext or launch the engine, and a later start
|
|
28
|
+
retries after unlock. Opt-in dual wrapping opens through an owner-only host key without prompting;
|
|
29
|
+
its protection is only as strong as that file, which must stay outside sealed-store backups.
|
|
30
|
+
Unlocked keyring-only snapshots upgrade atomically under the store's exclusive lock after verifying
|
|
31
|
+
the replacement decrypts to identical contents. Other sealing failures still reject.
|
|
32
|
+
|
|
33
|
+
## Secrets at rest and in memory
|
|
34
|
+
|
|
35
|
+
| Asset | Location relative to `stateDir` | Protection and consequence of theft |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
|
|
38
|
+
| 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. |
|
|
39
|
+
| 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. |
|
|
40
|
+
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts the complete state/home trees while stopped. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
|
|
41
|
+
| 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. |
|
|
42
|
+
| 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. |
|
|
43
|
+
|
|
44
|
+
New supervisor directories use 0700 and new secret files use 0600. These creation
|
|
45
|
+
modes alone do **not** repair all pre-existing permissions, reject every symlink,
|
|
46
|
+
encrypt disk contents, or enforce Windows ACLs. The optional credential snapshot
|
|
47
|
+
collects regular files and file symlinks whose fully resolved targets are regular
|
|
48
|
+
files within the isolated engine root; links restore as regular files at the link
|
|
49
|
+
paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
|
|
50
|
+
skipped without reading their contents. Collected POSIX modes are normalized;
|
|
51
|
+
other paths still need host protection. Skipped entries are not preserved in the
|
|
52
|
+
snapshot and are removed with the live trees after successful sealing. Never use a shared/writable tree or point
|
|
53
|
+
the kit at another product's state. Review existing permissions before adopting a
|
|
54
|
+
tree; use an OS-protected app directory, encrypted disk and restricted backups.
|
|
55
|
+
Do not log `doctorContext().env`, transport arguments, auth records or sign-in
|
|
56
|
+
callbacks. JavaScript strings and engine memory are not reliably zeroized.
|
|
57
|
+
|
|
58
|
+
The engine needs plaintext credentials and PEM during operation. With `authSeal`
|
|
59
|
+
(a host-injected `SealingAdapter` from `@byokit/secrets`), `src/auth-store.ts` seals
|
|
60
|
+
all of state/home, including SQLite journals, on successful `prepare()` and after
|
|
61
|
+
the engine exits on `stop()`. It verifies the adapter round-trip, writes via an
|
|
62
|
+
exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
|
|
63
|
+
removing live plaintext. `start()` authenticates and validates snapshot paths before
|
|
64
|
+
restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
|
|
65
|
+
A live store owner or orphan gateway prevents a second kit from racing its writer.
|
|
66
|
+
|
|
67
|
+
This protects stopped stores, not a running engine or doctor. Await `stop()` on
|
|
68
|
+
orderly shutdown. An abrupt host exit can leave live plaintext; the next prepare
|
|
69
|
+
seals leftovers only after the orphan writer has exited. Interrupted restore or
|
|
70
|
+
removal recovers from the authenticated snapshot. A sealing failure retains
|
|
71
|
+
recoverable files and rejects; the host must resolve it and retry. Snapshot size
|
|
72
|
+
and memory cost grow with the complete state/home trees. Keys, gateway token,
|
|
73
|
+
device PEM, inline config secrets, logs, install/cache files and app workspaces
|
|
74
|
+
are outside this seal. OS-keyring or host-key adapters must keep the key separate
|
|
75
|
+
from state/backups. There is no rollback protection. File removal cannot erase old
|
|
76
|
+
blocks, snapshots, swap or backups. A sealed approval provides separate transit
|
|
77
|
+
confidentiality and does not seal the credential tree.
|
|
78
|
+
After suspected compromise, revoke provider credentials with the provider, stop
|
|
79
|
+
the app's own engine, revoke device grants and recreate its operator identity and
|
|
80
|
+
token in a fresh private state directory. Deleting files alone does not revoke
|
|
81
|
+
already copied credentials or erase snapshots/backups.
|
|
82
|
+
|
|
83
|
+
## Engine isolation and installation
|
|
84
|
+
|
|
85
|
+
`src/engine.ts` builds the gateway/doctor environment explicitly: HOME, XDG,
|
|
86
|
+
provider-tool homes, tmp, config and state paths point into the app's tree. It does
|
|
87
|
+
not forward shell API keys, `NODE_OPTIONS` or shell environment snapshots. The
|
|
88
|
+
gateway binds `127.0.0.1` on its own port, uses token authentication and a signed
|
|
89
|
+
device identity, and disables Control UI, discovery, channels, automatic updates
|
|
90
|
+
and default remote memory search. Loopback is a reachability restriction, not a
|
|
91
|
+
substitute for authentication against other local processes.
|
|
92
|
+
|
|
93
|
+
Installation is a separate boundary: `npm ci --ignore-scripts` consumes the
|
|
94
|
+
committed engine lockfile, verifies the exact engine version, and uses an isolated
|
|
95
|
+
install HOME/cache. It needs the registry/network once and inherits the host PATH
|
|
96
|
+
to find npm; `npmPath` and `enginePath` must resolve to trusted executables. Version
|
|
97
|
+
and integrity pins reduce drift, but do not prove dependency safety. Trusted app
|
|
98
|
+
config can add plugins, providers and other destinations. The kit is not a
|
|
99
|
+
general egress filter. Builtin tools are gated by default; `gateBuiltins: false`
|
|
100
|
+
weakens that boundary deliberately. The install policy refuses dependency
|
|
101
|
+
installs, and trusted skills/roots remain the app's responsibility.
|
|
102
|
+
|
|
103
|
+
## Retained-login migration
|
|
104
|
+
|
|
105
|
+
`src/migrate.ts` reads only the explicit `{ path }` or `{ record }` supplied by the
|
|
106
|
+
app. It never searches HOME for credentials. Member ids are validated before
|
|
107
|
+
writing. Before the gateway starts, it stages legacy profiles in the member's
|
|
108
|
+
own profile store, then runs the offline doctor to canonicalize them. An existing
|
|
109
|
+
profile store is not overwritten; a failed doctor removes only staging this call
|
|
110
|
+
wrote. Tests exercise these conditions and path traversal refusal.
|
|
111
|
+
|
|
112
|
+
With `authSeal`, offline migration restores the credential snapshot and reseals
|
|
113
|
+
the staged/imported store even after a failed doctor. The public `doctorContext()`
|
|
114
|
+
escape hatch does not enforce that lifecycle; use the kit migration method.
|
|
115
|
+
A zero doctor exit is insufficient to retire a login. Only a gateway status report
|
|
116
|
+
that every expected provider is signed in confirms migration. Confirmation removes
|
|
117
|
+
the explicitly supplied path and its retained plaintext/sealed copies, then writes
|
|
118
|
+
an empty `<path>.moved-to-engine.canonicalized` marker. No new plaintext retained
|
|
119
|
+
archive is created. A record source remains the app's responsibility to delete.
|
|
120
|
+
|
|
121
|
+
Prepare scans only the app-owned tree for engine migration archives and old retained
|
|
122
|
+
copies, excluding install/cache/tmp/plugin/workspace roots and symlinks. With an
|
|
123
|
+
adapter these become verified `*.sealed` archives; without one, engine archives and
|
|
124
|
+
confirmed retained copies are removed. An unconfirmed retained copy stays available
|
|
125
|
+
for migration unless sealed. A sealed archive is not restored as live engine input;
|
|
126
|
+
an unconfirmed sealed retained source can be read by explicitly passing its original
|
|
127
|
+
path and the matching adapter. External sources are handled only when the host
|
|
128
|
+
explicitly supplies them. Archive log events contain no credential bytes.
|
|
129
|
+
|
|
130
|
+
Protect the original directory and backups; cleanup neither revokes credentials nor
|
|
131
|
+
erases historical copies. Never pass an owner's unrelated auth file or run concurrent
|
|
132
|
+
migrations. Staging uses a predictable pid suffix and a rename; hostile directories
|
|
133
|
+
remain outside the trusted-directory contract. The sealing store's ownership and
|
|
134
|
+
symlink checks do not establish an OS sandbox or general protection for other paths.
|
|
135
|
+
|
|
136
|
+
## Approvals, devices and relay notices
|
|
137
|
+
|
|
138
|
+
The bridge denies unknown runs and gates every tool by default. Registered app
|
|
139
|
+
tools use single-use permits or tickets bound to session, tool and exact input;
|
|
140
|
+
run teardown discards their authorizations. Missing/failed gates deny. Parked
|
|
141
|
+
asks expire, deny when the caller disconnects, and are removed on shutdown.
|
|
142
|
+
Native exec/plugin decisions use `allow-once`, not a persistent allowlist.
|
|
143
|
+
The engine remains authoritative for native approval expiration and resolution.
|
|
144
|
+
|
|
145
|
+
`openclawLink` checks the app's member mapping and session prefix before member
|
|
146
|
+
operations, checks pending approval ownership before deciding, and denies mutation
|
|
147
|
+
to view grants. Raw `oc.call` is denied unless the app's `passThrough` explicitly
|
|
148
|
+
allows the method; that callback must also enforce parameter/member scope. The
|
|
149
|
+
local `kit.call`/`kit.decide` surface has operator authority and is trusted host
|
|
150
|
+
code. Unattributed approvals never match a valid member id.
|
|
151
|
+
|
|
152
|
+
Notice registration sends only a 32-byte box public key over the authenticated
|
|
153
|
+
device link. `sealNotice` encrypts the complete approval with `@byokit/seal`'s
|
|
154
|
+
sealed box; each device receives separate ciphertext. A device with no registered
|
|
155
|
+
box key gets only the generic title. The relay sees destination, approval id,
|
|
156
|
+
generic title, action labels, timing, urgency and ciphertext size; it does not
|
|
157
|
+
need approval plaintext. Wrong keys, altered ciphertext, invalid JSON, versions
|
|
158
|
+
and approval shapes are rejected by `openNotice`.
|
|
159
|
+
|
|
160
|
+
Sealed boxes do **not** authenticate the sender: anyone with the public key can
|
|
161
|
+
encrypt a notice. They also provide no freshness/replay check; timestamps are
|
|
162
|
+
data, and a notice is never authorization. Treat it as a prompt to reload current
|
|
163
|
+
approvals over the authenticated link. `onAction` checks a remembered grant,
|
|
164
|
+
current member mapping, control authority, allow/deny action and pending member
|
|
165
|
+
approval. The relay action transport must authenticate the device and reject
|
|
166
|
+
replays before invoking it; a caller-supplied device id is not proof. Grant
|
|
167
|
+
revocation must make `memberOf` return undefined for revoked grants, since the
|
|
168
|
+
adapter retains grants/box keys seen earlier. Push failures do not resolve asks.
|
|
169
|
+
|
|
170
|
+
## Review record and regression evidence
|
|
171
|
+
|
|
172
|
+
Review completed 2026-09-30 by source inspection and offline regression coverage:
|
|
173
|
+
|
|
174
|
+
- [x] Plaintext identity/profiles, creation modes, logs, backup and same-user risks documented.
|
|
175
|
+
- [x] Shared isolation contract (`test/isolation-contract.ts`) runs in `npm test`
|
|
176
|
+
through `test/isolation.test.ts` with an offline child, exercising the actual
|
|
177
|
+
Engine spawn, its received environment/cwd, loopback listener, creation modes,
|
|
178
|
+
safe config and unchanged decoy HOME files. It checks its log for the token,
|
|
179
|
+
private PEM and inherited key canary; this is not a universal log-redaction proof.
|
|
180
|
+
- [x] `test/engine/isolation.test.ts` runs the same contract against the pinned
|
|
181
|
+
engine in `npm run test:engine` / CI's `openclaw-engine` job. It requires a
|
|
182
|
+
registry install; the default test run remains offline. Neither test claims
|
|
183
|
+
complete syscall tracing or an OS sandbox.
|
|
184
|
+
- [x] Migration failure, existing profiles, private staging, member validation,
|
|
185
|
+
confirmation and verified-source removal covered by `test/migrate.test.ts`.
|
|
186
|
+
- [x] Optional credential sealing, wrong keys, tampering, concurrent owners, safe file-link
|
|
187
|
+
materialization and skipped runtime/unsafe entries, interrupted transitions and archive cleanup covered by
|
|
188
|
+
`test/engine-unit.test.ts`. The seal excludes gateway/device secrets and live state.
|
|
189
|
+
- [x] Bridge fail-closed, timeout/disconnect and one-use input binding covered by
|
|
190
|
+
`test/bridge.test.ts` and `test/plugin.test.ts`.
|
|
191
|
+
- [x] Member/role checks, sealed/generic relay content, wrong-member push refusal
|
|
192
|
+
covered by `test/link.test.ts`; wrong-key and ciphertext tampering rejection
|
|
193
|
+
covered by `test/device.test.ts`.
|
|
194
|
+
|
|
195
|
+
The review found that a remembered **view** grant could resolve an approval via
|
|
196
|
+
`onAction`, although `oc.decide` already refused it. The push path now applies the
|
|
197
|
+
same role restriction, with a same-member view-grant regression case. The risks
|
|
198
|
+
above remain explicit limits; optional stopped-store encryption does not claim
|
|
199
|
+
live credential confidentiality or general hostile-directory safety.
|
|
200
|
+
|
|
201
|
+
Report a suspected vulnerability privately through the repository's GitHub
|
|
202
|
+
security advisory channel when available. Do not put tokens, private keys,
|
|
203
|
+
retained login files, pairing grants or unredacted logs in a public issue.
|
package/dist/auth-store.js
CHANGED
|
@@ -149,7 +149,17 @@ export class AuthStore {
|
|
|
149
149
|
if (!existsSync(this.file))
|
|
150
150
|
return undefined;
|
|
151
151
|
regular(this.file);
|
|
152
|
-
|
|
152
|
+
const seal = this.o.seal;
|
|
153
|
+
const bytes = readFileSync(this.file);
|
|
154
|
+
const text = seal.decryptString(bytes);
|
|
155
|
+
const saved = snapshot(text);
|
|
156
|
+
const upgraded = seal.upgrade?.(bytes);
|
|
157
|
+
if (upgraded) {
|
|
158
|
+
if (seal.decryptString(Buffer.from(upgraded)) !== text)
|
|
159
|
+
throw new Error('credential upgrade verification failed');
|
|
160
|
+
put(this.file, upgraded);
|
|
161
|
+
}
|
|
162
|
+
return saved;
|
|
153
163
|
}
|
|
154
164
|
collect() {
|
|
155
165
|
const s = { v: 1, dirs: [], files: [] };
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
|
+
import { routes } from "./routes.js";
|
|
2
3
|
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
4
|
const merge = (a, b) => {
|
|
4
5
|
for (const [k, v] of Object.entries(b))
|
|
@@ -53,7 +54,8 @@ export function reconcileConfig(saved, o) {
|
|
|
53
54
|
c.plugins ??= {};
|
|
54
55
|
c.plugins.load ??= {};
|
|
55
56
|
c.plugins.load.paths = [...new Set([...(c.plugins.load.paths ?? []).filter((p) => !p.includes('byokit-openclaw-bridge')), o.pluginDir])];
|
|
56
|
-
c.plugins.allow = [...new Set([...(c.plugins.allow ?? []), o.pluginId
|
|
57
|
+
c.plugins.allow = [...new Set([...(c.plugins.allow ?? []), o.pluginId,
|
|
58
|
+
...routes().filter(route => route.offer && route.plugin).map(route => route.plugin)])];
|
|
57
59
|
c.plugins.entries ??= {};
|
|
58
60
|
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
59
61
|
c.security ??= {};
|
package/dist/engine.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare class Engine {
|
|
|
16
16
|
private starting?;
|
|
17
17
|
private readonly authStore;
|
|
18
18
|
private stopping;
|
|
19
|
+
private credentialsLocked;
|
|
19
20
|
private repaired;
|
|
20
21
|
private prepared?;
|
|
21
22
|
private port;
|
|
@@ -40,7 +41,8 @@ export declare class Engine {
|
|
|
40
41
|
port: number;
|
|
41
42
|
token: string;
|
|
42
43
|
identityPath: string;
|
|
43
|
-
}>;
|
|
44
|
+
} | undefined>;
|
|
45
|
+
private locked;
|
|
44
46
|
private startOnce;
|
|
45
47
|
private launch;
|
|
46
48
|
withAuthStore<T>(task: () => Promise<T>): Promise<T>;
|
package/dist/engine.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Engine {
|
|
|
52
52
|
starting;
|
|
53
53
|
authStore;
|
|
54
54
|
stopping = false;
|
|
55
|
+
credentialsLocked = false;
|
|
55
56
|
repaired = false;
|
|
56
57
|
prepared;
|
|
57
58
|
port = 0;
|
|
@@ -94,7 +95,15 @@ export class Engine {
|
|
|
94
95
|
throw new Error('authSeal required for sealed credential store');
|
|
95
96
|
for (const d of [this.root, ...(!this.o.authSeal ? [join(this.root, 'home'), join(this.root, 'state')] : []), join(this.root, 'tmp'), join(this.root, 'install-home'), join(this.root, 'npm-cache'), join(this.o.stateDir, 'logs'), this.dir])
|
|
96
97
|
mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
97
|
-
|
|
98
|
+
try {
|
|
99
|
+
await this.authStore.prepare();
|
|
100
|
+
this.credentialsLocked = false;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (this.locked(error))
|
|
104
|
+
return;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
98
107
|
if (this.o.spawnEngine) {
|
|
99
108
|
const versionPath = join(this.dir, 'node_modules', 'openclaw', 'package.json');
|
|
100
109
|
if (!existsSync(this.entry) || !installMatches(this.dir)) {
|
|
@@ -183,9 +192,25 @@ export class Engine {
|
|
|
183
192
|
this.starting = undefined; }).catch(() => { });
|
|
184
193
|
return pending;
|
|
185
194
|
}
|
|
195
|
+
locked(error) {
|
|
196
|
+
if (!(error instanceof Error) || error.name !== 'KeystoreError' || !('code' in error) || error.code !== 'keyring-locked')
|
|
197
|
+
return false;
|
|
198
|
+
this.credentialsLocked = true;
|
|
199
|
+
this.state('locked');
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
186
202
|
async startOnce() {
|
|
187
203
|
await this.prepare();
|
|
188
|
-
|
|
204
|
+
if (this.credentialsLocked)
|
|
205
|
+
return undefined;
|
|
206
|
+
try {
|
|
207
|
+
await this.authStore.start();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (this.locked(error))
|
|
211
|
+
return undefined;
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
189
214
|
try {
|
|
190
215
|
return this.launch();
|
|
191
216
|
}
|
|
@@ -275,7 +300,8 @@ export class Engine {
|
|
|
275
300
|
throw new Error('engine did not stop; credential store still in use');
|
|
276
301
|
this.child = undefined;
|
|
277
302
|
rmSync(join(this.root, 'gateway.pid'), { force: true });
|
|
278
|
-
|
|
303
|
+
if (!this.credentialsLocked)
|
|
304
|
+
await this.authStore.stop();
|
|
279
305
|
rmSync(this.bridgeSock, { force: true });
|
|
280
306
|
this.state('stopped');
|
|
281
307
|
}
|
package/dist/kit.js
CHANGED
|
@@ -468,7 +468,7 @@ export class OpenClawKit {
|
|
|
468
468
|
let transport;
|
|
469
469
|
try {
|
|
470
470
|
const ctx = await this.engine.start();
|
|
471
|
-
if (this.stopping)
|
|
471
|
+
if (!ctx || this.stopping)
|
|
472
472
|
return;
|
|
473
473
|
this.setState({ phase: 'starting' });
|
|
474
474
|
transport = (this.o.transport ?? gatewayTransport)({ ...ctx, bridgeSock: this.engine.bridgeSock });
|
package/dist/link.js
CHANGED
|
@@ -86,7 +86,7 @@ export function openclawLink(kit, o) {
|
|
|
86
86
|
const onAction = async (a) => {
|
|
87
87
|
const grant = seen.get(a.device);
|
|
88
88
|
const member = grant ? o.memberOf(grant) : undefined;
|
|
89
|
-
if (member === undefined)
|
|
89
|
+
if (member === undefined || grant?.role === 'view')
|
|
90
90
|
throw refused();
|
|
91
91
|
if (a.action !== 'allow' && a.action !== 'deny')
|
|
92
92
|
throw refused();
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Route } from './types.ts';
|
|
2
2
|
/** The pin's auth-choice table. `provider` is the id a person's account is known by (the doc's matrix), which for
|
|
3
3
|
* MiniMax's portal routes is the account-visible `minimax`, not the wizard's internal `minimax-portal`. `plugin` is
|
|
4
|
-
* the bundled plugin
|
|
4
|
+
* the bundled plugin needed for that route to start (5.6); the kit allows offered routes, and the app allows any
|
|
5
|
+
* unoffered route it explicitly uses. Only the pin's bundled provider contracts are
|
|
5
6
|
* listed: install-catalog plugins (external providers needing their own install) are not contracted by the pin and
|
|
6
7
|
* are deliberately absent. */
|
|
7
8
|
export declare function routes(): Route[];
|
package/dist/routes.js
CHANGED
|
@@ -3,7 +3,8 @@ import routesJson from './routes.json' with { type: 'json' };
|
|
|
3
3
|
const table = routesJson;
|
|
4
4
|
/** The pin's auth-choice table. `provider` is the id a person's account is known by (the doc's matrix), which for
|
|
5
5
|
* MiniMax's portal routes is the account-visible `minimax`, not the wizard's internal `minimax-portal`. `plugin` is
|
|
6
|
-
* the bundled plugin
|
|
6
|
+
* the bundled plugin needed for that route to start (5.6); the kit allows offered routes, and the app allows any
|
|
7
|
+
* unoffered route it explicitly uses. Only the pin's bundled provider contracts are
|
|
7
8
|
* listed: install-catalog plugins (external providers needing their own install) are not contracted by the pin and
|
|
8
9
|
* are deliberately absent. */
|
|
9
10
|
export function routes() {
|
package/dist/routes.json
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"via": "browser",
|
|
118
118
|
"prerequisite": "the Claude CLI, signed in inside the engine's own folder",
|
|
119
119
|
"offer": false,
|
|
120
|
-
"reason": "
|
|
120
|
+
"reason": "Requires an explicit app opt-in; the pinned Gateway does not expose this plan route",
|
|
121
121
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"via": "browser",
|
|
129
129
|
"prerequisite": null,
|
|
130
130
|
"offer": false,
|
|
131
|
-
"reason": "API
|
|
131
|
+
"reason": "API key (billed per use); shown only when an app explicitly asks.",
|
|
132
132
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
@@ -348,7 +348,7 @@
|
|
|
348
348
|
"via": "browser",
|
|
349
349
|
"prerequisite": null,
|
|
350
350
|
"offer": false,
|
|
351
|
-
"reason": "
|
|
351
|
+
"reason": "Requires an explicit app opt-in; the pinned Gateway does not expose this plan route",
|
|
352
352
|
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
353
353
|
},
|
|
354
354
|
{
|
package/dist/types.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export type Decision = {
|
|
|
108
108
|
answer?: unknown;
|
|
109
109
|
};
|
|
110
110
|
export type KitState = {
|
|
111
|
-
phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update';
|
|
111
|
+
phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
|
|
112
112
|
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version';
|
|
113
113
|
retryAt?: number;
|
|
114
114
|
};
|
package/dist/words.js
CHANGED
package/dist/words.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"engine.installing": "Getting things ready on this computer. The first time takes a few minutes.",
|
|
3
3
|
"engine.starting": "Starting up…",
|
|
4
4
|
"engine.repairing": "Fixing a small problem with the setup. This takes a moment.",
|
|
5
|
+
"engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
|
|
5
6
|
"engine.ready": "Ready.",
|
|
6
7
|
"engine.restarting": "Something stopped. Starting it again by itself.",
|
|
7
8
|
"engine.failed": "This computer couldn't start the helper. Restart the app to try again.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"policy",
|
|
46
46
|
"README.md",
|
|
47
47
|
"LICENSE",
|
|
48
|
-
"CHANGELOG.md"
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"SECURITY.md"
|
|
49
50
|
],
|
|
50
51
|
"scripts": {
|
|
51
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"@byokit/relay": "0.4.2",
|
|
56
57
|
"@byokit/reach": "0.4.0",
|
|
57
58
|
"@byokit/seal": "0.2.0",
|
|
58
|
-
"@byokit/secrets": "0.
|
|
59
|
+
"@byokit/secrets": "0.5.0",
|
|
59
60
|
"@openclaw/gateway-client": "2026.8.1",
|
|
60
61
|
"@openclaw/gateway-protocol": "2026.8.1",
|
|
61
62
|
"ws": "8.21.3"
|