@byokit/openclaw 0.3.2 → 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 +10 -0
- package/README.md +13 -4
- package/SECURITY.md +7 -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/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 +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## 0.3.2 (2026-09-30)
|
|
6
16
|
|
|
7
17
|
- Dependency update: pins @byokit/secrets 0.4.0.
|
package/README.md
CHANGED
|
@@ -172,8 +172,16 @@ The pinned engine has no supported hook for sealing OAuth profile writes. Its `a
|
|
|
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
174
|
journals. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
177
185
|
|
|
178
186
|
```ts
|
|
179
187
|
import { osKeyringSeal, hostKeySeal } from '@byokit/secrets';
|
|
@@ -187,7 +195,7 @@ await kit.prepare(); // seals an existing plaintext store and migration archives
|
|
|
187
195
|
await kit.start(); // authenticates and restores the isolated files for the engine
|
|
188
196
|
await kit.stop(); // waits for exit, verifies an atomic sealed snapshot, then removes plaintext
|
|
189
197
|
|
|
190
|
-
// Inspect the adapter's mode
|
|
198
|
+
// Inspect the adapter's write mode: 'keyring', 'host-key-file' or opt-in 'dual-wrap'.
|
|
191
199
|
// Hosts can also explicitly supply a 32-byte key kept outside stateDir and its backups:
|
|
192
200
|
declare const hostKey: Uint8Array;
|
|
193
201
|
const server = new OpenClawKit({
|
|
@@ -200,7 +208,8 @@ The automatic key lives in the platform state directory, separate from the engin
|
|
|
200
208
|
on the same disk protects copied stores/backups only when the key directory is excluded; it does
|
|
201
209
|
not protect against code running as the same OS user. See [secrets' rotation and threat model](../secrets/README.md#servers-and-headless-node).
|
|
202
210
|
Stop the kit, acquire the host writer lock and call `seal.rotate()` with `auth-store.sealed` plus
|
|
203
|
-
every retained sealed migration archive before retiring an automatic key.
|
|
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.
|
|
204
213
|
|
|
205
214
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
206
215
|
only a sealed snapshot, `auth-store.sealed`, for those directories. The adapter authenticates the snapshot
|
package/SECURITY.md
CHANGED
|
@@ -23,6 +23,13 @@ running as the app's user, a compromised host/engine/plugin, or readable backups
|
|
|
23
23
|
The isolated environment and tool gate are not an OS filesystem/network sandbox.
|
|
24
24
|
The app must sandbox `ToolHost.call` and judge requests in `ToolHost.gate`.
|
|
25
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
|
+
|
|
26
33
|
## Secrets at rest and in memory
|
|
27
34
|
|
|
28
35
|
| Asset | Location relative to `stateDir` | Protection and consequence of theft |
|
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/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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@byokit/relay": "0.4.2",
|
|
57
57
|
"@byokit/reach": "0.4.0",
|
|
58
58
|
"@byokit/seal": "0.2.0",
|
|
59
|
-
"@byokit/secrets": "0.
|
|
59
|
+
"@byokit/secrets": "0.5.0",
|
|
60
60
|
"@openclaw/gateway-client": "2026.8.1",
|
|
61
61
|
"@openclaw/gateway-protocol": "2026.8.1",
|
|
62
62
|
"ws": "8.21.3"
|