@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 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 same injected `SealingAdapter` as `@byokit/secrets`; the kit creates no sealing key.
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
- // A headless server supplies its own 32-byte key, held outside stateDir and its backups:
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.
@@ -149,7 +149,17 @@ export class AuthStore {
149
149
  if (!existsSync(this.file))
150
150
  return undefined;
151
151
  regular(this.file);
152
- return snapshot(this.o.seal.decryptString(readFileSync(this.file)));
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
- await this.authStore.prepare();
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
- await this.authStore.start();
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
- await this.authStore.stop();
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 the app must allow for that route to start (5.6). Only the pin's bundled provider contracts are
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 the app must allow for that route to start (5.6). Only the pin's bundled provider contracts are
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": "byokit never adds Claude plan sign-in",
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 billing, not a subscription; never offered (D12): no Anthropic API fallback.",
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": "byokit never adds Claude plan sign-in",
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
@@ -14,6 +14,7 @@ const PHASE_WORDS = {
14
14
  ready: 'engine.ready',
15
15
  restarting: 'engine.restarting',
16
16
  failed: 'engine.failed',
17
+ locked: 'engine.locked',
17
18
  'needs-update': 'engine.needsUpdate',
18
19
  };
19
20
  export function stateWords(s) {
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.1",
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.3.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"