@byokit/openclaw 0.6.1 → 0.6.2
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 +33 -13
- package/SECURITY.md +6 -6
- package/dist/auth-store.d.ts +6 -0
- package/dist/auth-store.js +57 -14
- package/dist/browser/broker.js +17 -4
- package/dist/day-usage.d.ts +5 -3
- package/dist/device.d.ts +6 -3
- package/dist/index.d.ts +5 -2
- package/dist/link.d.ts +15 -7
- package/dist/link.js +0 -2
- package/dist/testing/contract.js +1 -2
- package/dist/testing/fake-gateway.d.ts +14 -7
- package/dist/testing/fake-gateway.js +8 -6
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/model-stub.d.ts +14 -1
- package/dist/testing/model-stub.js +8 -10
- package/dist/usage.d.ts +4 -2
- package/dist/usage.js +0 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.2 (2026-10-05)
|
|
6
|
+
|
|
7
|
+
FIX: The browser broker now holds its CDP endpoint back until Chromium's initial tab exists, so `/json/list` can never serve an empty browser to an attaching engine on a cold or slow start; a browser that never lists a tab fails the launch honestly instead.
|
|
8
|
+
- Remove the fake Gateway's own happy-path self-tests for default member/auth and the device-code wizard script from `test/fake.test.ts`. They asserted the synthetic fixture's own response shapes and exact number of progress pulls, not product behaviour. Member creation, caching and validation, device-code sign-in through `kit.signIn`/`kit.signedIn`, `health` pass-through and sign-out remain covered by the shared public consumer contract (`src/testing/contract.ts`) and the packed Chromium journey in `examples/openclaw-kit/e2e.test.ts`. Every negative, cancellation, denial, gate, drop and refusal assertion is kept unchanged, and no test was added.
|
|
9
|
+
- The quickstart's example output is what the example actually prints today: the offered-route list was stale
|
|
10
|
+
(it still showed `anthropic-cli` and a browser `setup-token` and missed `opencode-go`). The block is now the
|
|
11
|
+
real output of the runnable fake-Gateway example, followed by a "read next" pointer to the run contract, the
|
|
12
|
+
kit example and SECURITY.md.
|
|
13
|
+
FIX: The sealed credential store now seals credential state only — the complete `state` tree plus config/credential paths under `home` — instead of the whole engine home, so a real signed-in home with tool caches no longer produces a sealed payload past the runtime string limit and aborts boot. Regenerable caches, transcripts and logs (`home/.cache`, `home/.npm`, and the `sessions`/`log`/`cache`/`.tmp`/`history.jsonl` subtrees of `.codex`, the `projects`/`todos`/`shell-snapshots`/`statsig`/`file-history`/`history.jsonl` subtrees of `.claude`) stay on disk unsealed; unknown `home` paths stay sealed. The sealed payload is built exactly once and verified by decrypting the sealed bytes. Existing `v: 1` snapshots still restore completely and re-seal once as `v: 2` with a log line; no credential is ever dropped.
|
|
14
|
+
- Every type an entry hands a caller is now nameable from that entry, and none of them carries `any`.
|
|
15
|
+
`.` exports `GatewayMethods`, `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient` and `DayUsageClient`;
|
|
16
|
+
`./device` exports `OpenClawDevice` and `DeviceEndFrame`; `./link` exports `OpenClawLinkOptions`,
|
|
17
|
+
`OpenClawLinkHost`, `OpenClawServeOptions` and `OpenClawServeHandle`; `./testing` exports `FakeGateway`,
|
|
18
|
+
`FakeHandler`, `FakeParams` and `StubRequest`, types `fakeGateway`'s script per method, and records each
|
|
19
|
+
stubbed request as a typed `StubRequest` instead of `any`. An app can write its own generic wrapper over
|
|
20
|
+
`call` and annotate what `routes()`, `openclawDevice()` and `openclawLink()` return without a cast. The
|
|
21
|
+
Gateway slots the pinned engine does not declare stay `unknown` and are listed per release in
|
|
22
|
+
`src/generated/report.json`; the README now says so beside the API table.
|
|
23
|
+
|
|
5
24
|
## 0.6.1 (2026-10-04)
|
|
6
25
|
|
|
7
26
|
FIX: Let apps declare `appOwnedSessions.keyPrefixes` before Gateway startup so caller-requeued task sessions do not also receive an engine-started recovery turn. Preserve session history, policy gates, API-key session mapping and stock recovery for other namespaces.
|
package/README.md
CHANGED
|
@@ -288,17 +288,23 @@ state: ready | protocol: 4
|
|
|
288
288
|
event: {"type":"text","text":"fake: Say hello."}
|
|
289
289
|
event: {"type":"text","text":"fake: Say hello."}
|
|
290
290
|
end: {"ok":true,"text":"fake: Say hello.","usage":{"input":10,"output":16,"total":26}}
|
|
291
|
-
route:
|
|
292
|
-
route: openai-device-code subscription code
|
|
293
|
-
route: xai-oauth subscription code
|
|
291
|
+
route: setup-token subscription setup_token
|
|
294
292
|
route: github-copilot subscription code
|
|
295
293
|
route: github-copilot-enterprise subscription code
|
|
296
294
|
route: minimax-global-oauth subscription code
|
|
297
295
|
route: minimax-cn-oauth subscription code
|
|
298
|
-
route:
|
|
299
|
-
route:
|
|
296
|
+
route: openai subscription browser
|
|
297
|
+
route: openai-device-code subscription code
|
|
298
|
+
route: opencode-go subscription plan_key
|
|
299
|
+
route: xai-oauth subscription code
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
+
The block above is the whole quickstart: copy it into a `.mts` file next to your app's
|
|
303
|
+
`node_modules` and `node` it. No engine, no account, no network. Then read
|
|
304
|
+
[the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for retries and
|
|
305
|
+
streams, [`examples/openclaw-kit`](../../examples/openclaw-kit) for a whole app, and
|
|
306
|
+
[SECURITY.md](SECURITY.md) before you store anything the engine returns.
|
|
307
|
+
|
|
302
308
|
Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
|
|
303
309
|
apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
|
|
304
310
|
`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. For an opted-in route, allow its `plugin` id
|
|
@@ -319,12 +325,23 @@ Entries:
|
|
|
319
325
|
| `OpenClawKit` (`.`) | Prepares, starts, supervises and stops the engine. `call` / `callDynamic` pass through to the Gateway, `onEvent` listens. Helpers: `ensureMember`, `routes`, `providers`, `signedIn`, `signIn`, `signOut`, `migrateRetainedLogin`, `confirmRetainedLogin`, `toolNames`, `run`, `steer`, `abort`, `approvals`, `onApproval`, `decide`, `allowOnce`, `disallowOnce`, `patchConfig`, `getConfigKey`, `setConfigKey`, `memoryLimited`, `doctorContext` |
|
|
320
326
|
| `ENGINE_VERSION`, `PROTOCOL_VERSION`, `OPERATOR_SCOPES` (`.`) | The pinned engine version, its protocol and the operator scopes the kit connects with |
|
|
321
327
|
| `KitOptions`, `RunSpec`, `RunEvent`, `RunEnd`, `RunUsage`, `PlanWindow`, `Route`, `Approval`, `Decision`, `ToolSpec`, `ToolHost`, `KitState`, ... (`.`) | Public types (docs/runtime-kits.md §5.2) |
|
|
322
|
-
| `
|
|
328
|
+
| `GatewayMethods`, `GatewayMethod`, `GatewayParams`, `GatewayResult`, `GatewayEventName`, `GatewayEventPayload` (`.`) | The generated pass-through tables, so an app can write its own generic wrapper over `call` / `onEvent` once |
|
|
329
|
+
| `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
|
|
330
|
+
| `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 |
|
|
323
331
|
| `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
|
|
324
332
|
| `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds `@byokit/ui-core`'s `phaseOf` |
|
|
325
|
-
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions |
|
|
326
|
-
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs |
|
|
327
|
-
| `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests |
|
|
333
|
+
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions. `OpenClawLinkOptions` / `OpenClawLinkHost` name both sides |
|
|
334
|
+
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
|
|
335
|
+
| `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` |
|
|
336
|
+
|
|
337
|
+
### What the pass-through cannot type
|
|
338
|
+
|
|
339
|
+
Every method and event the pin publishes has a real type: `kit.call('sessions.list', params)` is checked in and
|
|
340
|
+
returns the engine's declared result, with no cast. For the slots the pin itself leaves undeclared the table stays
|
|
341
|
+
`unknown`, and `src/generated/report.json` names every one of them per release. On `openclaw@2026.8.1` that is 301
|
|
342
|
+
of 393 methods' params and 192 of their results, plus 21 of 55 event payloads; the other slots are not missing from
|
|
343
|
+
the kit, they are not published by the engine, and typing them here would be a guess that does not match the
|
|
344
|
+
gateway. `callDynamic` stays the same shape for a method the pin never declared.
|
|
328
345
|
|
|
329
346
|
## Owned browser (unqualified handoff)
|
|
330
347
|
|
|
@@ -419,8 +436,9 @@ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-ki
|
|
|
419
436
|
|
|
420
437
|
The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
|
|
421
438
|
credential JSON in agent SQLite databases and a shared state database, and doctor imports leave migration
|
|
422
|
-
archives. `authSeal` therefore protects the complete isolated `state`
|
|
423
|
-
|
|
439
|
+
archives. `authSeal` therefore protects credential state — the complete isolated `state` tree plus
|
|
440
|
+
config/credential paths under `home`, including SQLite journals — while regenerable tool caches,
|
|
441
|
+
transcripts and logs stay on disk unsealed. It uses the injected `SealingAdapter` from `@byokit/secrets`. `osKeyringSeal()` automatically
|
|
424
442
|
uses a persistent private file key for new stores when no non-interactive keyring is available.
|
|
425
443
|
Opening follows the saved envelope's mode. A locked or unresponsive keyring-only store leaves
|
|
426
444
|
`kit.state.phase === 'locked'`: `prepare()` and `start()` resolve, show `stateWords(kit.state)` for
|
|
@@ -461,7 +479,8 @@ every retained sealed migration archive before retiring an automatic key. Rotate
|
|
|
461
479
|
separately; dual-wrap rotation retains old wrapping keys for backups.
|
|
462
480
|
|
|
463
481
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
464
|
-
only a sealed snapshot, `auth-store.sealed`, for
|
|
482
|
+
only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
|
|
483
|
+
disk unsealed. The adapter authenticates the snapshot
|
|
465
484
|
before any restoration; a wrong key, tampering, or a missing adapter rejects. Files restored for the engine
|
|
466
485
|
have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
|
|
467
486
|
regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
|
|
@@ -474,7 +493,8 @@ inspection or a rewritten command line), another host is alive, or shutdown time
|
|
|
474
493
|
`{ phase: 'failed', why: 'engine-already-running' }`; show `stateWords(kit.state)` and retry after the other
|
|
475
494
|
session stops. Failed-start cleanup and `stop()` on an instance with no ownership preserve the other
|
|
476
495
|
writer's pid, lock, sealed snapshot and live state.
|
|
477
|
-
|
|
496
|
+
The sealed payload is built once and verified by decrypting the sealed bytes, so startup and stop cost
|
|
497
|
+
grows with credential state, not session history.
|
|
478
498
|
|
|
479
499
|
The migration doctor temporarily opens the same store and reseals it even when import fails. Once the gateway
|
|
480
500
|
verifies every provider, `confirmRetainedLogin()` removes the explicitly passed legacy source and writes only
|
package/SECURITY.md
CHANGED
|
@@ -37,7 +37,7 @@ the replacement decrypts to identical contents. Other sealing failures still rej
|
|
|
37
37
|
| Gateway token | `openclaw/token` | Random 32-byte bearer secret; newly created file is 0600. Treat it as operator access. |
|
|
38
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
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
|
|
40
|
+
| Sealed credential snapshot | `openclaw/auth-store.sealed` and retired `*.sealed` archives | With host-injected `authSeal`, authenticates and encrypts credential state (the complete `state` tree plus config/credential paths under `home`) while stopped; regenerable caches stay on disk unsealed. Key security belongs to the adapter/host; an older authentic snapshot can be replayed. |
|
|
41
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
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
43
|
|
|
@@ -48,8 +48,8 @@ collects regular files and file symlinks whose fully resolved targets are regula
|
|
|
48
48
|
files within the isolated engine root; links restore as regular files at the link
|
|
49
49
|
paths. Outside-root, dangling and directory links, sockets, FIFOs and devices are
|
|
50
50
|
skipped without reading their contents. Collected POSIX modes are normalized;
|
|
51
|
-
other paths still need host protection.
|
|
52
|
-
snapshot and
|
|
51
|
+
other paths still need host protection. Excluded caches are not preserved in the
|
|
52
|
+
snapshot and stay on disk unsealed after successful sealing; other skipped entries are not preserved in the snapshot. Never use a shared/writable tree or point
|
|
53
53
|
the kit at another product's state. Review existing permissions before adopting a
|
|
54
54
|
tree; use an OS-protected app directory, encrypted disk and restricted backups.
|
|
55
55
|
Do not log `doctorContext().env`, transport arguments, auth records or sign-in
|
|
@@ -57,8 +57,8 @@ callbacks. JavaScript strings and engine memory are not reliably zeroized.
|
|
|
57
57
|
|
|
58
58
|
The engine needs plaintext credentials and PEM during operation. With `authSeal`
|
|
59
59
|
(a host-injected `SealingAdapter` from `@byokit/secrets`), `src/auth-store.ts` seals
|
|
60
|
-
|
|
61
|
-
the engine exits on `stop()
|
|
60
|
+
credential state — the complete `state` tree plus config/credential paths under `home`, including SQLite journals — on successful `prepare()` and after
|
|
61
|
+
the engine exits on `stop()`, while regenerable caches stay on disk unsealed. It verifies the adapter round-trip, writes via an
|
|
62
62
|
exclusive temporary file, fsyncs/renames the snapshot and verifies it again before
|
|
63
63
|
removing live plaintext. `start()` authenticates and validates snapshot paths before
|
|
64
64
|
restoring files. Missing/wrong keys or tampering reject without plaintext fallback.
|
|
@@ -69,7 +69,7 @@ orderly shutdown. An abrupt host exit can leave live plaintext; the next prepare
|
|
|
69
69
|
seals leftovers only after the orphan writer has exited. Interrupted restore or
|
|
70
70
|
removal recovers from the authenticated snapshot. A sealing failure retains
|
|
71
71
|
recoverable files and rejects; the host must resolve it and retry. Snapshot size
|
|
72
|
-
and memory cost grow with
|
|
72
|
+
and memory cost grow with sealed credential state; the payload is built once and excluded caches never enter it. Keys, gateway token,
|
|
73
73
|
device PEM, inline config secrets, logs, install/cache files and app workspaces
|
|
74
74
|
are outside this seal. OS-keyring or host-key adapters must keep the key separate
|
|
75
75
|
from state/backups. There is no rollback protection. File removal cannot erase old
|
package/dist/auth-store.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
+
/** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
|
|
3
|
+
export declare function cached(name: string): boolean;
|
|
2
4
|
export declare class AuthStore {
|
|
3
5
|
private active;
|
|
4
6
|
private owned;
|
|
@@ -20,6 +22,10 @@ export declare class AuthStore {
|
|
|
20
22
|
private release;
|
|
21
23
|
private read;
|
|
22
24
|
private collect;
|
|
25
|
+
/** Remove the live credential trees: `state` entirely (it is all credential state, including the
|
|
26
|
+
* runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
|
|
27
|
+
* and the directories still holding them are left in place. */
|
|
28
|
+
private remove;
|
|
23
29
|
private persist;
|
|
24
30
|
private restore;
|
|
25
31
|
archives(): Promise<void>;
|
package/dist/auth-store.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The pin persists OAuth JSON in both shared and agent SQLite. There is no public persistence hook.
|
|
2
|
-
// Seal
|
|
3
|
-
|
|
2
|
+
// Seal only credential state — the engine's complete `state` tree plus every config/credential path in
|
|
3
|
+
// `home` — never the regenerable tool caches, transcripts and logs a signed-in home accumulates.
|
|
4
|
+
// Sealing those too once produced a snapshot past the runtime string limit and aborted boot.
|
|
5
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
|
|
4
6
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
7
|
import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
|
|
6
8
|
const encoder = new TextEncoder();
|
|
@@ -45,13 +47,27 @@ function put(path, bytes) {
|
|
|
45
47
|
rmSync(tmp, { force: true });
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
// Credential state is what restores a working signed-in session. Regenerable tool caches, transcripts
|
|
51
|
+
// and logs never do: the XDG cache and npm cache homes the kit's engine environment pins, plus the
|
|
52
|
+
// transcript/log/cache subtrees of the Codex and Claude Code CLIs it runs in `home`. Unknown paths stay
|
|
53
|
+
// sealed — a credential location we do not know about must fail loudly, never drop silently.
|
|
54
|
+
const HOME_CACHES = {
|
|
55
|
+
home: ['.cache', '.npm'],
|
|
56
|
+
'home/.codex': ['sessions', 'log', 'cache', '.tmp', 'history.jsonl'],
|
|
57
|
+
'home/.claude': ['projects', 'todos', 'shell-snapshots', 'statsig', 'file-history', 'history.jsonl'],
|
|
58
|
+
};
|
|
59
|
+
/** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
|
|
60
|
+
export function cached(name) {
|
|
61
|
+
const parent = name.includes('/') ? name.slice(0, name.lastIndexOf('/')) : '';
|
|
62
|
+
return (HOME_CACHES[parent] ?? []).includes(name.slice(name.lastIndexOf('/') + 1));
|
|
63
|
+
}
|
|
48
64
|
function safePath(path) {
|
|
49
65
|
return typeof path === 'string' && /^(state|home)(\/[^/]+)*$/.test(path)
|
|
50
66
|
&& !path.split('/').some((part) => part === '.' || part === '..' || part.includes('\\') || part.includes('\0'));
|
|
51
67
|
}
|
|
52
68
|
function snapshot(text) {
|
|
53
69
|
const s = JSON.parse(text);
|
|
54
|
-
if (s?.v !== 1 || !Array.isArray(s.dirs) || !Array.isArray(s.files))
|
|
70
|
+
if ((s?.v !== 1 && s?.v !== 2) || !Array.isArray(s.dirs) || !Array.isArray(s.files))
|
|
55
71
|
throw new Error('invalid sealed credential store');
|
|
56
72
|
const paths = new Set();
|
|
57
73
|
for (const path of s.dirs) {
|
|
@@ -169,10 +185,12 @@ export class AuthStore {
|
|
|
169
185
|
return saved;
|
|
170
186
|
}
|
|
171
187
|
collect() {
|
|
172
|
-
const s = { v:
|
|
188
|
+
const s = { v: 2, dirs: [], files: [] };
|
|
173
189
|
const root = realpathSync(this.o.root);
|
|
174
190
|
const walk = (path) => {
|
|
175
191
|
const name = relative(this.o.root, path).split('\\').join('/');
|
|
192
|
+
if (cached(name))
|
|
193
|
+
return; // tool caches are never credential state; skipping a directory skips its subtree
|
|
176
194
|
const stat = lstatSync(path);
|
|
177
195
|
if (stat.isDirectory()) {
|
|
178
196
|
chmodSync(path, 0o700);
|
|
@@ -208,31 +226,56 @@ export class AuthStore {
|
|
|
208
226
|
walk(join(this.o.root, dir));
|
|
209
227
|
return s;
|
|
210
228
|
}
|
|
229
|
+
/** Remove the live credential trees: `state` entirely (it is all credential state, including the
|
|
230
|
+
* runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
|
|
231
|
+
* and the directories still holding them are left in place. */
|
|
232
|
+
remove(s) {
|
|
233
|
+
rmSync(join(this.o.root, 'state'), { recursive: true, force: true });
|
|
234
|
+
for (const [path] of s.files)
|
|
235
|
+
if (path.startsWith('home/'))
|
|
236
|
+
rmSync(join(this.o.root, path), { force: true });
|
|
237
|
+
for (const path of [...s.dirs].sort((a, b) => b.split('/').length - a.split('/').length)) {
|
|
238
|
+
if (!path.startsWith('home/'))
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
rmdirSync(join(this.o.root, path));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
const code = error.code;
|
|
245
|
+
if (code !== 'ENOTEMPTY' && code !== 'ENOENT')
|
|
246
|
+
throw error; // a directory still holding caches stays
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
211
250
|
async persist() {
|
|
212
251
|
if (!this.o.seal)
|
|
213
252
|
return;
|
|
214
253
|
// Always authenticate an earlier snapshot before considering a leftover live store after a crash.
|
|
215
254
|
const previous = await this.read();
|
|
216
255
|
if (previous && (existsSync(this.cleanup) || existsSync(this.restoring))) {
|
|
217
|
-
|
|
218
|
-
rmSync(join(this.o.root, dir), { recursive: true, force: true });
|
|
256
|
+
this.remove(previous);
|
|
219
257
|
removeMarker(this.cleanup);
|
|
220
258
|
removeMarker(this.restoring);
|
|
221
259
|
this.o.log?.('interrupted credential transition recovered');
|
|
222
260
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
261
|
+
const saved = this.collect();
|
|
262
|
+
// Live trees with nothing sealable are debris (crash leftovers, or only the caches a previous stop
|
|
263
|
+
// left in place) — keep the sealed credentials instead of replacing them with an empty snapshot.
|
|
264
|
+
// A running engine always leaves files behind (its SQLite stores), so a genuine sign-out still re-seals.
|
|
265
|
+
if (!saved.files.length && previous)
|
|
225
266
|
return;
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
267
|
+
// The payload is built exactly once; verification decrypts the sealed bytes and compares to it.
|
|
268
|
+
const payload = JSON.stringify(saved);
|
|
269
|
+
const sealed = this.o.seal.encryptString(payload);
|
|
270
|
+
if (this.o.seal.decryptString(Buffer.from(sealed)) !== payload)
|
|
229
271
|
throw new Error('credential seal verification failed');
|
|
230
272
|
put(this.file, sealed);
|
|
231
|
-
if (this.o.seal.decryptString(readFileSync(this.file)) !==
|
|
273
|
+
if (this.o.seal.decryptString(readFileSync(this.file)) !== payload)
|
|
232
274
|
throw new Error('credential seal verification failed');
|
|
275
|
+
if (previous?.v === 1)
|
|
276
|
+
this.o.log?.('sealed credential store re-sealed: tool caches no longer sealed');
|
|
233
277
|
put(this.cleanup, encoder.encode('1'));
|
|
234
|
-
|
|
235
|
-
rmSync(join(this.o.root, dir), { recursive: true, force: true });
|
|
278
|
+
this.remove(saved);
|
|
236
279
|
removeMarker(this.cleanup);
|
|
237
280
|
}
|
|
238
281
|
async restore() {
|
package/dist/browser/broker.js
CHANGED
|
@@ -668,10 +668,23 @@ export async function launchBroker(o) {
|
|
|
668
668
|
await send('Browser.getVersion');
|
|
669
669
|
await send('Browser.setDownloadBehavior', { behavior: 'deny', eventsEnabled: false });
|
|
670
670
|
await send('Target.setDiscoverTargets', { discover: true });
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
671
|
+
// Chromium creates the initial tab after the pipe already answers, so a cold start can list zero
|
|
672
|
+
// page targets here. Never expose the endpoint before the agent tab exists: a discovery /json/list
|
|
673
|
+
// would honestly report an empty browser and break every engine that lists targets at attach.
|
|
674
|
+
const tabUntil = Date.now() + 30_000;
|
|
675
|
+
let pages = [];
|
|
676
|
+
for (;;) {
|
|
677
|
+
const result = await send('Target.getTargets');
|
|
678
|
+
for (const t of result.targetInfos)
|
|
679
|
+
targets.set(t.targetId, t);
|
|
680
|
+
pages = result.targetInfos.filter(t => t.type === 'page');
|
|
681
|
+
if (pages.length > 0 || Date.now() >= tabUntil)
|
|
682
|
+
break;
|
|
683
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
684
|
+
}
|
|
685
|
+
lastAgent = pages[0]?.targetId;
|
|
686
|
+
if (!lastAgent)
|
|
687
|
+
throw failure();
|
|
675
688
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve()); });
|
|
676
689
|
port = server.address().port;
|
|
677
690
|
}
|
package/dist/day-usage.d.ts
CHANGED
|
@@ -69,8 +69,10 @@ export type AgentDayUsage = {
|
|
|
69
69
|
export declare function engineStartedOf(raw: unknown, member: Member, window: AgentDayUsage['window']): AgentDayUsage['engineStarted'] & {
|
|
70
70
|
complete: boolean;
|
|
71
71
|
};
|
|
72
|
-
/**
|
|
73
|
-
export
|
|
72
|
+
/** What `readAgentDayUsage` needs from a reader: the kit's `call` plus its own string-typed RPC. */
|
|
73
|
+
export type DayUsageClient = {
|
|
74
74
|
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
75
75
|
call(method: 'sessions.usage', params: SessionsUsageParams): Promise<unknown>;
|
|
76
|
-
}
|
|
76
|
+
};
|
|
77
|
+
/** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
|
|
78
|
+
export declare function readAgentDayUsage(client: DayUsageClient, member: Member, window: AgentDayUsage['window']): Promise<AgentDayUsage>;
|
package/dist/device.d.ts
CHANGED
|
@@ -27,7 +27,8 @@ export type SessionRow = {
|
|
|
27
27
|
sessionKey: string;
|
|
28
28
|
[k: string]: unknown;
|
|
29
29
|
};
|
|
30
|
-
|
|
30
|
+
/** The frame `oc.run`'s stream ends with: the run's `RunEnd`, beside its events. */
|
|
31
|
+
export type DeviceEndFrame<T = unknown> = {
|
|
31
32
|
type: 'end';
|
|
32
33
|
end: RunEnd<T>;
|
|
33
34
|
};
|
|
@@ -78,7 +79,8 @@ export type SignInNotice = {
|
|
|
78
79
|
export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
|
|
79
80
|
/** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
|
|
80
81
|
export declare function browserDevice(link: DeviceLink): BrowserDevice;
|
|
81
|
-
|
|
82
|
+
/** `openclawDevice`'s device surface: every `oc.*` op the computer answers, typed. */
|
|
83
|
+
export type OpenClawDevice = {
|
|
82
84
|
browser: BrowserDevice;
|
|
83
85
|
state(): Promise<DeviceState>;
|
|
84
86
|
routes(): Promise<RouteView[]>;
|
|
@@ -90,7 +92,7 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
90
92
|
};
|
|
91
93
|
signOut(p: string): Promise<void>;
|
|
92
94
|
sessions(): Promise<SessionRow[]>;
|
|
93
|
-
run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent |
|
|
95
|
+
run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | DeviceEndFrame<SchemaOutput<S>>>;
|
|
94
96
|
steer(k: string, t: string, o?: {
|
|
95
97
|
auth?: 'apiKey';
|
|
96
98
|
}): Promise<void>;
|
|
@@ -105,6 +107,7 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
105
107
|
call<M extends GatewayMethod>(method: M, params: GatewayParams<M>): Promise<GatewayResult<M>>;
|
|
106
108
|
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
107
109
|
};
|
|
110
|
+
export declare function openclawDevice(link: DeviceLink): OpenClawDevice;
|
|
108
111
|
export { openNotice } from './notices.ts';
|
|
109
112
|
export { readAgentUsage, agentUsageOf } from './usage.ts';
|
|
110
113
|
export { readAgentDayUsage } from './day-usage.ts';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
2
|
export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
3
|
+
export type { GatewayMethods } from './generated/methods.ts';
|
|
4
|
+
export type { JsonValue } from './output.ts';
|
|
5
|
+
export type { RouteFacts, RouteView } from './routes.ts';
|
|
3
6
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
4
7
|
export type { AddKeyResult } from './keys.ts';
|
|
5
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
6
9
|
export { EngineAlreadyRunningError } from './engine-status.ts';
|
|
7
10
|
export { readAgentDayUsage } from './day-usage.ts';
|
|
8
|
-
export type { AgentDayUsage, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
|
|
11
|
+
export type { AgentDayUsage, DayUsageClient, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
|
|
9
12
|
export { readAgentUsage, agentUsageOf } from './usage.ts';
|
|
10
|
-
export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageWindow } from './usage.ts';
|
|
13
|
+
export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageClient, UsageWindow } from './usage.ts';
|
|
11
14
|
export type { LiveSource, BrowserState, SignInMethodHint, SignInReason, SignInChoice, SettledState, SettledReason, ResumeState, NeedSignIn, TakeoverLease, LiveViewState, LiveFrame, LiveInput, ThumbnailResult, SignInRefusedWhy, BrowserOptions, SiteVerifier, BrowserHost, BrowserDevice } from './browser.ts';
|
package/dist/link.d.ts
CHANGED
|
@@ -5,23 +5,31 @@ import { type ServeIngress, type Via } from '@byokit/reach';
|
|
|
5
5
|
import type { PushAction, RelayClient } from '@byokit/relay';
|
|
6
6
|
import type { OpenClawKit } from './kit.ts';
|
|
7
7
|
import type { Member } from './types.ts';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}, o: {
|
|
8
|
+
/** `openclawLink`'s options: which member a grant acts for, the D8 pass-through predicate, sealed relay push. */
|
|
9
|
+
export type OpenClawLinkOptions = {
|
|
11
10
|
memberOf: (grant: Grant) => Member | undefined;
|
|
12
11
|
passThrough?: (method: string, grant: Grant) => boolean;
|
|
13
12
|
relay?: RelayClient;
|
|
14
|
-
}
|
|
13
|
+
};
|
|
14
|
+
/** What `openclawLink` hands the link host: its request/stream handlers plus the kit's push entry point. */
|
|
15
|
+
export type OpenClawLinkHost = Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
|
|
15
16
|
onAction(a: PushAction): Promise<unknown>;
|
|
16
17
|
};
|
|
17
|
-
export declare function
|
|
18
|
+
export declare function openclawLink(kit: OpenClawKit & {
|
|
19
|
+
browser?: BrowserHost;
|
|
20
|
+
}, o: OpenClawLinkOptions): OpenClawLinkHost;
|
|
21
|
+
/** `serve`'s options: the link host to accept on, and the ingress to publish or replace. */
|
|
22
|
+
export type OpenClawServeOptions = {
|
|
18
23
|
host: Host;
|
|
19
24
|
port: number;
|
|
20
25
|
via?: Via;
|
|
21
26
|
previous?: ServeIngress;
|
|
22
27
|
http?: (req: IncomingMessage, res: ServerResponse) => void;
|
|
23
|
-
}
|
|
28
|
+
};
|
|
29
|
+
/** The running ingress: its urls, the entry that reaches the engine, and an orderly close. */
|
|
30
|
+
export type OpenClawServeHandle = {
|
|
24
31
|
urls: string[];
|
|
25
32
|
ingress?: ServeIngress;
|
|
26
33
|
close(): Promise<void>;
|
|
27
|
-
}
|
|
34
|
+
};
|
|
35
|
+
export declare function serve(o: OpenClawServeOptions): Promise<OpenClawServeHandle>;
|
package/dist/link.js
CHANGED
|
@@ -618,8 +618,6 @@ export function openclawLink(kit, o) {
|
|
|
618
618
|
};
|
|
619
619
|
return { handle, stream, allow: () => true, onAction, onGrantRemoved: grant => revoke(grant.id) };
|
|
620
620
|
}
|
|
621
|
-
// ponytail: this ~40-line serve() wiring is deliberately duplicated per runtime kit (D3);
|
|
622
|
-
// extract it into a shared package when a third runtime kit appears.
|
|
623
621
|
export async function serve(o) {
|
|
624
622
|
const r = await reach({ port: o.port, via: o.via, previous: o.previous });
|
|
625
623
|
const server = createServer(o.http ??
|
package/dist/testing/contract.js
CHANGED
|
@@ -10,8 +10,7 @@ import assert from 'node:assert/strict';
|
|
|
10
10
|
import { PROTOCOL_VERSION } from "../constants.js";
|
|
11
11
|
import { GATEWAY_CAPS } from "../transport.js";
|
|
12
12
|
import { releaseStub, stubHolding } from "./model-stub.js";
|
|
13
|
-
|
|
14
|
-
// suite drives pass-through through this string-typed view of the same runtime path.
|
|
13
|
+
/** The contract drives pass-through by method name, so results arrive untyped and are narrowed per case. */
|
|
15
14
|
const call = (kit, method, params) => kit.call(method, params);
|
|
16
15
|
const REQUIRED_METHODS = [
|
|
17
16
|
'agent', 'agent.wait', 'agents.create', 'agents.list', 'chat.abort', 'config.get', 'config.patch',
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { GatewayTransport } from '../types.ts';
|
|
1
|
+
import type { GatewayMethod, GatewayParams, GatewayTransport } from '../types.ts';
|
|
2
2
|
import type { KitOptions } from '../kit.ts';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
3
|
+
/** A scripted handler's params: the generated type when the pin publishes one, else the raw params object. */
|
|
4
|
+
export type FakeParams<M extends GatewayMethod> = unknown extends GatewayParams<M> ? Record<string, unknown> : GatewayParams<M>;
|
|
5
|
+
/** A scripted handler for one method, with that method's params typed. */
|
|
6
|
+
export type FakeHandler<M extends GatewayMethod = GatewayMethod> = (params: FakeParams<M>, bridgeSock: string) => unknown;
|
|
7
|
+
/** Pre-registered handlers for `fakeGateway(script)`, on top of the defaults (a same-name entry replaces one).
|
|
8
|
+
* Only methods the pinned engine publishes are scriptable, exactly as `kit.call` and `kit.callDynamic` divide them. */
|
|
9
|
+
export type FakeScript = {
|
|
10
|
+
[M in GatewayMethod]?: FakeHandler<M>;
|
|
11
|
+
};
|
|
12
|
+
/** The fake's control surface: the transport to hand a kit, the calls it saw, and its triggers. */
|
|
13
|
+
export type FakeGateway = {
|
|
7
14
|
factory: NonNullable<KitOptions['transport']>;
|
|
8
15
|
calls: {
|
|
9
16
|
method: string;
|
|
@@ -12,8 +19,8 @@ export declare function fakeGateway(script?: FakeScript): {
|
|
|
12
19
|
emit(event: string, payload?: unknown): void;
|
|
13
20
|
failNext(method: string, message: string): void;
|
|
14
21
|
drop(why: string): void;
|
|
15
|
-
handle(method: string, fn:
|
|
22
|
+
handle(method: string, fn: FakeHandler): void;
|
|
16
23
|
} & {
|
|
17
24
|
transport: GatewayTransport;
|
|
18
25
|
};
|
|
19
|
-
export
|
|
26
|
+
export declare function fakeGateway(script?: FakeScript): FakeGateway;
|
|
@@ -245,12 +245,12 @@ export function fakeGateway(script) {
|
|
|
245
245
|
'openclaw.setup.activate': (p) => {
|
|
246
246
|
if (p.kind !== 'api-key')
|
|
247
247
|
throw new Error('only the API-key setup path is simulated');
|
|
248
|
-
const route = routes().find((r) => r.choice === p.authChoice && r.keyEntry);
|
|
248
|
+
const route = routes().find((r) => r.choice === String(p.authChoice) && r.keyEntry);
|
|
249
249
|
if (!route)
|
|
250
250
|
return { ok: false, status: 'unavailable' };
|
|
251
251
|
const model = `${route.provider}/fake-key-model`;
|
|
252
|
-
keys.set(p.agentId, { provider: route.provider, key: p.apiKey, model, sealed: false });
|
|
253
|
-
signInAgent(p.agentId, route.provider);
|
|
252
|
+
keys.set(String(p.agentId), { provider: route.provider, key: String(p.apiKey), model, sealed: false });
|
|
253
|
+
signInAgent(String(p.agentId), route.provider);
|
|
254
254
|
return { ok: true, status: 'ok', modelRef: model };
|
|
255
255
|
},
|
|
256
256
|
'openclaw.setup.auth.start': (p) => {
|
|
@@ -325,14 +325,15 @@ export function fakeGateway(script) {
|
|
|
325
325
|
'exec.approval.request': (p) => {
|
|
326
326
|
const id = String(p.id ?? `exec-${randomUUID()}`);
|
|
327
327
|
const now = Date.now();
|
|
328
|
+
const plan = isPlainObject(p.systemRunPlan) ? p.systemRunPlan : {};
|
|
328
329
|
emit('exec.approval.requested', {
|
|
329
330
|
approvalKind: 'exec', id, createdAtMs: now, expiresAtMs: now + 180_000,
|
|
330
331
|
request: {
|
|
331
332
|
command: p.command,
|
|
332
333
|
// Like the real engine, which normalizes the exec policy mode into request.ask (B8).
|
|
333
334
|
ask: p.ask ?? 'on-miss',
|
|
334
|
-
agentId: p.agentId ??
|
|
335
|
-
sessionKey: p.sessionKey ??
|
|
335
|
+
agentId: p.agentId ?? plan.agentId,
|
|
336
|
+
sessionKey: p.sessionKey ?? plan.sessionKey,
|
|
336
337
|
},
|
|
337
338
|
});
|
|
338
339
|
return { id };
|
|
@@ -366,6 +367,7 @@ export function fakeGateway(script) {
|
|
|
366
367
|
};
|
|
367
368
|
for (const [method, handler] of Object.entries(defaults))
|
|
368
369
|
handlers.set(method, handler);
|
|
370
|
+
// A script handler is already per-method typed; the loose internal map only dispatches it.
|
|
369
371
|
for (const [method, handler] of Object.entries(script ?? {}))
|
|
370
372
|
handlers.set(method, handler);
|
|
371
373
|
const dispatch = async (method, params, bridgeSock) => {
|
|
@@ -377,7 +379,7 @@ export function fakeGateway(script) {
|
|
|
377
379
|
const handler = handlers.get(method);
|
|
378
380
|
if (!handler)
|
|
379
381
|
throw new Error(`unknown method: ${method}`);
|
|
380
|
-
return await handler(params
|
|
382
|
+
return await handler(isPlainObject(params) ? params : {}, bridgeSock);
|
|
381
383
|
};
|
|
382
384
|
const hello = () => ({
|
|
383
385
|
protocol: PROTOCOL_VERSION,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { fakeGateway, type FakeScript } from './fake-gateway.ts';
|
|
1
|
+
export { fakeGateway, type FakeGateway, type FakeHandler, type FakeParams, type FakeScript } from './fake-gateway.ts';
|
|
2
2
|
export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore, type FakeBrowserHost, type FakeBrowserHostOptions, type BrowserFixture } from './browser.ts';
|
|
3
3
|
export { openclawContract, type ContractFixture } from './contract.ts';
|
|
4
|
-
export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall } from './model-stub.ts';
|
|
4
|
+
export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall, type StubRequest } from './model-stub.ts';
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import type { OpenClawKit } from '../kit.ts';
|
|
2
|
+
export type StubRequest = {
|
|
3
|
+
model?: unknown;
|
|
4
|
+
input?: unknown;
|
|
5
|
+
messages?: {
|
|
6
|
+
role?: string;
|
|
7
|
+
content?: unknown;
|
|
8
|
+
name?: unknown;
|
|
9
|
+
tool_call_id?: unknown;
|
|
10
|
+
}[];
|
|
11
|
+
stream_options?: {
|
|
12
|
+
include_usage?: boolean;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
2
15
|
export type StubCall = {
|
|
3
16
|
authorization: string;
|
|
4
17
|
path: string;
|
|
5
|
-
body:
|
|
18
|
+
body: StubRequest;
|
|
6
19
|
};
|
|
7
20
|
/** The token usage every stub reply reports when asked (`stream_options.include_usage`), per model call. */
|
|
8
21
|
export declare const STUB_USAGE: {
|
|
@@ -58,7 +58,8 @@ export const releaseStub = (key, reply = '') => {
|
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
export const stubHolding = (key) => (key ? holds.has(key) : holds.size > 0);
|
|
61
|
-
const words = (m) => typeof m?.content === 'string' ? m.content : Array.isArray(m?.content)
|
|
61
|
+
const words = (m) => typeof m?.content === 'string' ? m.content : Array.isArray(m?.content)
|
|
62
|
+
? m.content.map((c) => (typeof c?.text === 'string' ? c.text : '')).join('') : '';
|
|
62
63
|
export function startModelStub(script = [], o = {}) {
|
|
63
64
|
const calls = [];
|
|
64
65
|
const queue = [...script];
|
|
@@ -97,7 +98,7 @@ export function startModelStub(script = [], o = {}) {
|
|
|
97
98
|
const phrased = (text) => /hit the limit|no helpers in plan|sign me out|ask permission/i.test(text);
|
|
98
99
|
const lastUser = [...users].reverse().find((m) => structural(words(m)))
|
|
99
100
|
?? [...users].reverse().find((m) => phrased(words(m)))
|
|
100
|
-
?? users.at(-1);
|
|
101
|
+
?? users.at(-1) ?? {};
|
|
101
102
|
const said = words(lastUser);
|
|
102
103
|
const anchor = messages.lastIndexOf(lastUser);
|
|
103
104
|
const results = anchor < 0 ? [] : messages.slice(anchor + 1).filter((m) => m.role === 'tool');
|
|
@@ -113,7 +114,7 @@ export function startModelStub(script = [], o = {}) {
|
|
|
113
114
|
if (said.startsWith(routingMarker)) {
|
|
114
115
|
const options = [...said.matchAll(/^- ([a-z0-9-]+):/gm)].map((m) => m[1]);
|
|
115
116
|
const pick = /\[route ([a-z0-9-]+|\?)\]/.exec(said)?.[1] ?? 'chief';
|
|
116
|
-
return plain(res, body, JSON.stringify(Object.fromEntries(options.map((o) => [o, pick === '?' ? 1 / options.length : o === pick ? 0.9 : 0.1 / (options.length - 1)]))));
|
|
117
|
+
return plain(res, body.model, JSON.stringify(Object.fromEntries(options.map((o) => [o, pick === '?' ? 1 / options.length : o === pick ? 0.9 : 0.1 / (options.length - 1)]))));
|
|
117
118
|
}
|
|
118
119
|
if (/sign me out/i.test(said)) {
|
|
119
120
|
res.writeHead(401, { 'content-type': 'application/json' });
|
|
@@ -167,21 +168,18 @@ export function startModelStub(script = [], o = {}) {
|
|
|
167
168
|
});
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
|
-
const plain = (res,
|
|
171
|
+
const plain = (res, model, text) => {
|
|
171
172
|
const id = randomUUID();
|
|
172
173
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' });
|
|
173
|
-
const chunk = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model
|
|
174
|
+
const chunk = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
|
|
174
175
|
chunk({ role: 'assistant', content: text });
|
|
175
176
|
chunk({}, 'stop');
|
|
176
177
|
res.end('data: [DONE]\n\n');
|
|
177
178
|
};
|
|
178
179
|
// provider id 'byokit-stub', model 'test': the stub becomes every agent's primary model.
|
|
179
180
|
export async function useModelStub(kit, stub) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const call = kit.call.bind(kit);
|
|
183
|
-
const current = await call('config.get');
|
|
184
|
-
await call('config.patch', { baseHash: current.hash, raw: JSON.stringify({
|
|
181
|
+
const current = await kit.call('config.get', {});
|
|
182
|
+
await kit.call('config.patch', { baseHash: current.hash, raw: JSON.stringify({
|
|
185
183
|
models: { providers: { 'byokit-stub': {
|
|
186
184
|
baseUrl: stub.url, apiKey: 'byokit-stub', api: 'openai-completions',
|
|
187
185
|
models: [{ id: 'test', name: 'Test', reasoning: true, input: ['text'],
|
package/dist/usage.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type AgentUsageReading = {
|
|
|
46
46
|
/** Normalize an explicitly agent-scoped UTC response. No missing datum is replaced with zero. */
|
|
47
47
|
export declare function agentUsageOf(raw: unknown, member: Member, window: UsageWindow, receivedAt?: number): AgentUsageReading;
|
|
48
48
|
/** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
|
|
49
|
-
|
|
49
|
+
/** What `readAgentUsage` needs from a reader: the kit (`call`) or a device (`callDynamic` + `call`) both fit. */
|
|
50
|
+
export type UsageClient = {
|
|
50
51
|
call(method: 'sessions.usage', params: SessionsUsageParams, options?: CallOptions): Promise<unknown>;
|
|
51
|
-
}
|
|
52
|
+
};
|
|
53
|
+
export declare function readAgentUsage(client: UsageClient, member: Member, window: UsageWindow, options?: CallOptions): Promise<AgentUsageReading>;
|
package/dist/usage.js
CHANGED
|
@@ -53,7 +53,6 @@ export function agentUsageOf(raw, member, window, receivedAt = Date.now()) {
|
|
|
53
53
|
result.totals = totals;
|
|
54
54
|
return result;
|
|
55
55
|
}
|
|
56
|
-
/** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
|
|
57
56
|
export async function readAgentUsage(client, member, window, options) {
|
|
58
57
|
validate(member, window);
|
|
59
58
|
const raw = await client.call('sessions.usage', { agentId: member, startDate: window.startDate, endDate: window.endDate, mode: 'utc', groupBy: 'instance', limit: 1 }, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|