@byokit/openclaw 0.5.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +205 -15
  3. package/SECURITY.md +6 -6
  4. package/dist/auth-store.d.ts +6 -0
  5. package/dist/auth-store.js +58 -15
  6. package/dist/bridge.d.ts +10 -2
  7. package/dist/bridge.js +61 -14
  8. package/dist/browser/broker.d.ts +63 -0
  9. package/dist/browser/broker.js +1003 -0
  10. package/dist/browser/host.d.ts +98 -0
  11. package/dist/browser/host.js +945 -0
  12. package/dist/browser/resume.d.ts +10 -0
  13. package/dist/browser/resume.js +23 -0
  14. package/dist/browser/store.d.ts +25 -0
  15. package/dist/browser/store.js +151 -0
  16. package/dist/browser/verify.d.ts +17 -0
  17. package/dist/browser/verify.js +38 -0
  18. package/dist/browser.d.ts +212 -0
  19. package/dist/browser.js +4 -0
  20. package/dist/config.d.ts +28 -0
  21. package/dist/config.js +130 -0
  22. package/dist/day-usage.d.ts +78 -0
  23. package/dist/day-usage.js +104 -0
  24. package/dist/device.d.ts +38 -5
  25. package/dist/device.js +224 -4
  26. package/dist/engine-patches.d.ts +36 -0
  27. package/dist/engine-patches.js +320 -0
  28. package/dist/engine-status.d.ts +13 -0
  29. package/dist/engine-status.js +31 -0
  30. package/dist/engine.d.ts +13 -1
  31. package/dist/engine.js +99 -52
  32. package/dist/index.d.ts +9 -1
  33. package/dist/index.js +2 -0
  34. package/dist/kit.d.ts +28 -3
  35. package/dist/kit.js +319 -12
  36. package/dist/link.d.ts +16 -5
  37. package/dist/link.js +333 -6
  38. package/dist/routes.d.ts +19 -8
  39. package/dist/routes.js +44 -11
  40. package/dist/routes.json +3775 -356
  41. package/dist/runs.js +8 -2
  42. package/dist/signin.d.ts +1 -1
  43. package/dist/signin.js +19 -4
  44. package/dist/testing/browser.d.ts +50 -0
  45. package/dist/testing/browser.js +166 -0
  46. package/dist/testing/contract.js +1 -2
  47. package/dist/testing/fake-gateway.d.ts +14 -7
  48. package/dist/testing/fake-gateway.js +8 -6
  49. package/dist/testing/index.d.ts +3 -2
  50. package/dist/testing/index.js +1 -0
  51. package/dist/testing/model-stub.d.ts +15 -1
  52. package/dist/testing/model-stub.js +11 -11
  53. package/dist/types.d.ts +50 -3
  54. package/dist/usage-boots.d.ts +8 -0
  55. package/dist/usage-boots.js +26 -0
  56. package/dist/usage.d.ts +53 -0
  57. package/dist/usage.js +60 -0
  58. package/dist/words.json +42 -0
  59. package/engine/OPENCLAW-LICENSE +24 -0
  60. package/engine/patches.json +59 -0
  61. package/engine/patches.schema.json +41 -0
  62. package/package.json +12 -4
  63. package/plugin/index.js +78 -5
  64. package/plugin/usage.js +125 -0
package/dist/runs.js CHANGED
@@ -84,6 +84,10 @@ export function createRuns(ctx) {
84
84
  // Member boundary first: a member never speaks in another member's session, refused before any request.
85
85
  if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
86
86
  throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
87
+ // Same validation as the pin's NonEmptyString; preserve the caller's bytes (including whitespace).
88
+ if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
89
+ throw new Error('refused: idempotencyKey must be a non-empty string');
90
+ const idempotencyKey = spec.idempotencyKey ?? randomUUID();
87
91
  const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
88
92
  const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
89
93
  const picked = spec.model === undefined ? undefined : account(spec.model);
@@ -92,7 +96,7 @@ export function createRuns(ctx) {
92
96
  throw new Error(`refused: "${tool}" is not one of this kit's tools`);
93
97
  const agentId = keyAgent ?? (await ctx.ensure(spec.member)).agentId;
94
98
  const sessionKey = keyAgent ? `agent:${keyAgent}:${spec.sessionKey.slice(`agent:${spec.member}:`.length)}` : spec.sessionKey;
95
- const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools) : undefined;
99
+ const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
96
100
  let last;
97
101
  let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
98
102
  let ended = false;
@@ -155,7 +159,7 @@ export function createRuns(ctx) {
155
159
  agentId,
156
160
  sessionKey,
157
161
  message: spec.message,
158
- idempotencyKey: randomUUID(),
162
+ idempotencyKey,
159
163
  ...(picked ?? {}),
160
164
  ...(system ? { extraSystemPrompt: system } : {}),
161
165
  ...(spec.images ? { attachments: spec.images.map((image) => ({ mimeType: image.mimeType, content: image.data })) } : {}),
@@ -166,6 +170,8 @@ export function createRuns(ctx) {
166
170
  runId = started.runId;
167
171
  const result = await ctx.request('agent.wait', { runId, timeoutMs: WAIT_MS }, { timeoutMs: WAIT_CLIENT_MS });
168
172
  if (result.status === 'ok') {
173
+ // A cached `in_flight` replay has no final subscription. Keep the existing wait/stream fallback:
174
+ // re-sending `agent` to fetch its final could dispatch again if the bounded cache was evicted.
169
175
  const done = await Promise.race([final.catch(() => undefined), delay(FINAL_GRACE_MS, undefined, { ref: false })]);
170
176
  ended = true; // no later stream event may supersede the final callback
171
177
  const frame = isRecord(done) && isRecord(done.result) ? done.result : {};
package/dist/signin.d.ts CHANGED
@@ -9,7 +9,7 @@ export type SignInCtx = {
9
9
  };
10
10
  /**
11
11
  * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
12
- * `paste` hands a returned browser address to a waiting wizard text step. Every exit short of done cancels this
12
+ * `paste` hands a returned browser address or sign-in token to a waiting wizard text step. Every exit short of done cancels this
13
13
  * session and its callback listener only.
14
14
  */
15
15
  export declare function signIn(ctx: SignInCtx, member: Member, o: SignInOptions, on: (v: SignInView) => void): {
package/dist/signin.js CHANGED
@@ -19,7 +19,7 @@ const cut = (value) => (value instanceof Error ? value.message : String(value)).
19
19
  const pullOptions = (signal) => ({ timeoutMs: PULL_MS, signal });
20
20
  /**
21
21
  * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
22
- * `paste` hands a returned browser address to a waiting wizard text step. Every exit short of done cancels this
22
+ * `paste` hands a returned browser address or sign-in token to a waiting wizard text step. Every exit short of done cancels this
23
23
  * session and its callback listener only.
24
24
  */
25
25
  export function signIn(ctx, member, o, on) {
@@ -68,10 +68,15 @@ export function signIn(ctx, member, o, on) {
68
68
  let settle;
69
69
  let pasteIn;
70
70
  let returned;
71
+ // Once a secret step is seen, no later gateway prose, link or code is safe to display.
72
+ let sensitive = o.authChoice === 'setup-token' || o.authChoice === 'apiKey';
73
+ const secretFailure = () => o.authChoice === 'apiKey' ? 'API key activation failed' : 'Sign-in failed. Try again.';
71
74
  const done = new Promise((resolve) => { settle = resolve; });
72
75
  const say = (v) => {
73
76
  if (!over && !recovering && !signal.aborted)
74
- on({ state: 'waiting', via, ...v });
77
+ on({ state: 'waiting', via, ...(sensitive
78
+ ? { ...(v.prompt ? { prompt: v.prompt } : {}), ...(v.error ? { error: secretFailure() } : {}) }
79
+ : v) });
75
80
  };
76
81
  const closeCallback = async () => {
77
82
  const held = server;
@@ -90,6 +95,8 @@ export function signIn(ctx, member, o, on) {
90
95
  returned = undefined;
91
96
  pasteIn = undefined;
92
97
  await closeCallback();
98
+ if (sensitive && view.state === 'failed' && view.why === 'failed')
99
+ view = { ...view, error: secretFailure() };
93
100
  on(view);
94
101
  settle(view);
95
102
  };
@@ -218,6 +225,10 @@ export function signIn(ctx, member, o, on) {
218
225
  const pull = () => ctx.request('wizard.next', { sessionId }, options());
219
226
  const answer = (step, value) => ctx.request('wizard.next', { sessionId, answer: { stepId: step.id, ...(value === undefined ? {} : { value }) } }, options());
220
227
  const waitForPaste = () => new Promise((resolve) => {
228
+ if (signal.aborted) {
229
+ resolve(undefined);
230
+ return;
231
+ }
221
232
  if (returned !== undefined) {
222
233
  const text = returned;
223
234
  returned = undefined;
@@ -255,7 +266,9 @@ export function signIn(ctx, member, o, on) {
255
266
  }
256
267
  const current = step;
257
268
  step = undefined;
258
- if (current.deviceCode) {
269
+ if (current.type === 'text' && current.sensitive)
270
+ sensitive = true;
271
+ if (current.deviceCode && current.type !== 'text') {
259
272
  stopApproval();
260
273
  approval = true;
261
274
  sawDeviceCode = true;
@@ -278,7 +291,9 @@ export function signIn(ctx, member, o, on) {
278
291
  }
279
292
  if (current.type !== 'progress')
280
293
  stopApproval();
281
- if (current.type === 'text' && !current.sensitive) {
294
+ if (current.type === 'text') {
295
+ if (current.sensitive)
296
+ say({ prompt: 'Sign-in token' });
282
297
  const value = await waitForPaste();
283
298
  if (value === undefined)
284
299
  return signal.aborted ? cancelled() : expired();
@@ -0,0 +1,50 @@
1
+ import type { BrowserHost, BrowserOptions, LiveFrame, NeedSignIn } from '../browser.ts';
2
+ import { type BrowserHostController, type HostBroker } from '../browser/host.ts';
3
+ import { type SignInStore } from '../browser/store.ts';
4
+ import type { Probe } from '../browser/verify.ts';
5
+ export declare function memorySignInStore(): SignInStore;
6
+ export type BrowserFixture = {
7
+ /** Set the synthetic address bar; no network call. */
8
+ navigate(member: string, origin: string): void;
9
+ authenticated(member: string, value: boolean): void;
10
+ frame(member: string, frame?: LiveFrame): void;
11
+ dropStreams(member: string): void;
12
+ advance(ms: number): Promise<void>;
13
+ resumeOutcome(value: 'accepted' | 'refused' | 'unknown' | 'throw'): void;
14
+ closePrivate(member: string, fn: (() => Promise<void>) | undefined): void;
15
+ probe(member: string, fn: ((p: Probe) => Promise<'ok' | 'fail' | 'timeout'>) | undefined): void;
16
+ readonly dispatches: {
17
+ member: string;
18
+ sessionKey: string;
19
+ idempotencyKey: string;
20
+ message: string;
21
+ }[];
22
+ readonly parked: {
23
+ member: string;
24
+ sessionKey: string;
25
+ }[];
26
+ readonly calls: string[];
27
+ broker(member: string): HostBroker;
28
+ inputCount(member: string): number;
29
+ privateOpen(member: string): boolean;
30
+ fenced(member: string): boolean;
31
+ };
32
+ export type FakeBrowserHost = BrowserHostController & {
33
+ readonly fixture: BrowserFixture;
34
+ };
35
+ export type FakeBrowserHostOptions = {
36
+ members?: string[];
37
+ options?: Partial<BrowserOptions>;
38
+ store?: SignInStore;
39
+ authorize?: (grant: string, member: string, control: boolean) => boolean;
40
+ ping?: (member: string, kind: 'state' | 'signin') => void;
41
+ };
42
+ export declare function fakeBrowserHost(o?: FakeBrowserHostOptions): Promise<FakeBrowserHost>;
43
+ export declare const createBrowserHostFake: typeof fakeBrowserHost;
44
+ /** Reusable public-host contract, fixture controls kept outside BrowserHost. Runs against any host adapter. */
45
+ export declare function browserHostContract(f: {
46
+ host: BrowserHost;
47
+ raise(): Promise<NeedSignIn>;
48
+ signedIn(): void;
49
+ dispatches(): number;
50
+ }): Promise<void>;
@@ -0,0 +1,166 @@
1
+ import { fixtureBrowserHost, secureOrigin } from "../browser/host.js";
2
+ import { emptySignIns, validateSignIns } from "../browser/store.js";
3
+ function channel() {
4
+ let pending;
5
+ let value;
6
+ let ended = false;
7
+ return {
8
+ push(next) { if (ended)
9
+ return; if (pending) {
10
+ const resolve = pending;
11
+ pending = undefined;
12
+ resolve({ done: false, value: next });
13
+ }
14
+ else
15
+ value = next; },
16
+ close() { ended = true; value = undefined; pending?.({ done: true, value: undefined }); pending = undefined; },
17
+ iterable: { [Symbol.asyncIterator]() {
18
+ return {
19
+ next() {
20
+ if (value !== undefined) {
21
+ const next = value;
22
+ value = undefined;
23
+ return Promise.resolve({ done: false, value: next });
24
+ }
25
+ if (ended)
26
+ return Promise.resolve({ done: true, value: undefined });
27
+ return new Promise(resolve => { pending = resolve; });
28
+ },
29
+ return() { ended = true; pending?.({ done: true, value: undefined }); return Promise.resolve({ done: true, value: undefined }); },
30
+ };
31
+ } },
32
+ };
33
+ }
34
+ export function memorySignInStore() {
35
+ let data = emptySignIns();
36
+ return { read: () => structuredClone(data), write: next => { data = validateSignIns(next); } };
37
+ }
38
+ export async function fakeBrowserHost(o = {}) {
39
+ const members = o.members ?? ['ada'];
40
+ const states = new Map(members.map(member => [member, { origin: 'http://127.0.0.1:2820', privateOrigin: undefined,
41
+ fenced: false, signedIn: false, binding: undefined,
42
+ confirmed: new Set(), viewers: new Set(), inputs: 0,
43
+ closer: undefined, prober: undefined }]));
44
+ const get = (member) => { const s = states.get(member); if (!s)
45
+ throw new Error('unknown fixture member'); return s; };
46
+ const privateState = (member) => {
47
+ const s = get(member);
48
+ return s.privateOrigin ? { origin: s.privateOrigin, secure: secureOrigin(s.privateOrigin), offOrigin: !s.binding
49
+ || !(s.privateOrigin === s.binding.origin || s.binding.knownIdps.includes(s.privateOrigin) || s.confirmed.has(s.privateOrigin)) } : undefined;
50
+ };
51
+ const calls = [];
52
+ const dispatches = [];
53
+ const parked = [];
54
+ let now = Date.now();
55
+ let outcome = 'accepted';
56
+ let seq = 0;
57
+ const frame = () => ({ seq: ++seq, at: now, w: 1, h: 1, jpeg: Uint8Array.of(255, 216, 255, 217) });
58
+ const brokers = new Map(members.map(member => {
59
+ const s = get(member);
60
+ return [member, {
61
+ endpoint: () => ({ cdpUrl: 'ws://127.0.0.1:2820/devtools/browser?token=synthetic' }),
62
+ async fence(on) { calls.push(`${member}:fence:${on}`); s.fenced = on; },
63
+ agentTab: () => 'agent',
64
+ async originOf(target) { return target === 'private' ? s.privateOrigin ?? s.origin : s.origin; },
65
+ async openPrivate(url) { calls.push(`${member}:open`); s.privateOrigin = new URL(url).origin; return 'private'; },
66
+ async closePrivate() { calls.push(`${member}:close`); await s.closer?.(); s.privateOrigin = undefined; },
67
+ async probe(url, verify) {
68
+ calls.push(`${member}:probe`);
69
+ const p = { url, status: s.signedIn ? 200 : 401, exists: async () => s.signedIn };
70
+ return s.prober ? s.prober(p) : await verify(p) ? 'ok' : 'fail';
71
+ },
72
+ attachViewer({ lease }) {
73
+ const frames = channel();
74
+ const liveStates = channel();
75
+ const v = { frames, states: liveStates, lease, close: () => { frames.close(); liveStates.close(); s.viewers.delete(v); } };
76
+ s.viewers.add(v);
77
+ frames.push(frame());
78
+ const state = privateState(member);
79
+ if (lease && state)
80
+ liveStates.push(state);
81
+ return { frames: frames.iterable, states: liveStates.iterable,
82
+ input(_i) {
83
+ // Do not retain, log, event or echo input values, even in this fixture.
84
+ const state = privateState(member);
85
+ if (lease && s.binding?.epoch === lease.epoch && s.binding.nonce === lease.nonce && state?.secure && !state.offOrigin)
86
+ s.inputs++;
87
+ }, close: v.close };
88
+ },
89
+ async navigateAgent(url) { calls.push(`${member}:navigate`); if (url !== 'reload')
90
+ s.origin = new URL(url).origin; },
91
+ async close() { s.viewers.forEach(v => v.close()); s.privateOrigin = undefined; },
92
+ bindLease(l) { calls.push(`${member}:bind:${l ? 'on' : 'off'}`); s.binding = l; s.confirmed.clear(); },
93
+ confirmOrigin(l, origin) {
94
+ if (s.binding?.epoch !== l.epoch || s.binding.nonce !== l.nonce || s.privateOrigin !== origin)
95
+ return false;
96
+ s.confirmed.add(origin);
97
+ const state = privateState(member);
98
+ if (state)
99
+ s.viewers.forEach(v => v.states.push(state));
100
+ return true;
101
+ },
102
+ privateState: () => privateState(member),
103
+ async clearSite(origins) { if (s.fenced)
104
+ throw new Error('fixture fenced'); calls.push(`${member}:clear:${origins.length}`); s.signedIn = false; },
105
+ }];
106
+ }));
107
+ const defaultVerifier = { origin: 'http://127.0.0.1:2820', url: 'http://127.0.0.1:2820/account', status: 200, selector: '#signed-in' };
108
+ const host = await fixtureBrowserHost({ brokers, store: o.store ?? memorySignInStore(),
109
+ options: { executablePath: '/synthetic/chromium', members, verifiers: [defaultVerifier], ...o.options },
110
+ authorize: o.authorize ?? ((grant, member, control) => members.includes(member) && (grant === 'control' || grant === 'other' || (!control && grant === 'view'))),
111
+ ping: o.ping,
112
+ park: async (member, sessionKey) => { parked.push({ member, sessionKey }); },
113
+ resume: async (input) => { dispatches.push(input); if (outcome === 'throw')
114
+ throw new Error('synthetic transport drop'); return outcome; },
115
+ siteOf: origin => new URL(origin).hostname,
116
+ now: () => now,
117
+ });
118
+ const fixture = {
119
+ navigate(member, origin) {
120
+ const s = get(member);
121
+ const exact = new URL(origin).origin;
122
+ if (s.privateOrigin)
123
+ s.privateOrigin = exact;
124
+ else
125
+ s.origin = exact;
126
+ const state = privateState(member);
127
+ if (state)
128
+ s.viewers.forEach(v => v.states.push(state));
129
+ },
130
+ authenticated(member, value) { get(member).signedIn = value; },
131
+ frame(member, value) { get(member).viewers.forEach(v => v.frames.push(value ?? frame())); },
132
+ dropStreams(member) { get(member).viewers.forEach(v => v.close()); },
133
+ async advance(ms) { now += ms; await host.sweep(); },
134
+ resumeOutcome(value) { outcome = value; },
135
+ closePrivate(member, fn) { get(member).closer = fn; },
136
+ probe(member, fn) { get(member).prober = fn; },
137
+ dispatches, parked, calls,
138
+ broker: member => { const broker = brokers.get(member); if (!broker)
139
+ throw new Error('unknown fixture member'); return broker; },
140
+ inputCount: member => get(member).inputs,
141
+ privateOpen: member => !!get(member).privateOrigin,
142
+ fenced: member => get(member).fenced,
143
+ };
144
+ return Object.assign(host, { fixture });
145
+ }
146
+ export const createBrowserHostFake = fakeBrowserHost;
147
+ /** Reusable public-host contract, fixture controls kept outside BrowserHost. Runs against any host adapter. */
148
+ export async function browserHostContract(f) {
149
+ const require = (value) => { if (!value)
150
+ throw new Error('browser host contract failed'); };
151
+ const r = await f.raise();
152
+ require(r.state === 'waiting');
153
+ require((await f.host.thumbnail({ kind: 'browser', member: r.member }, { grant: 'view' })).state === 'private');
154
+ const lease = await f.host.takeover(r.id, r.gen, { grant: 'control', confirmSite: r.site });
155
+ f.signedIn();
156
+ const settled = await f.host.done(lease);
157
+ require(settled.state === 'settled' && settled.settled?.state === 'verified' && !!settled.settled.resume?.key);
158
+ require(f.dispatches() === 1);
159
+ try {
160
+ await f.host.done(lease);
161
+ }
162
+ catch {
163
+ return;
164
+ }
165
+ throw new Error('browser host contract failed');
166
+ }
@@ -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
- // The generated method table arrives with O2; until then kit.call's typed surface accepts no method names, so the
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
- type Handler = (params: any, bridgeSock: string) => unknown;
4
- /** Pre-registered handlers for `fakeGateway(script)`, on top of the defaults (a same-name entry replaces one). */
5
- export type FakeScript = Record<string, Handler>;
6
- export declare function fakeGateway(script?: FakeScript): {
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: Handler): void;
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 ?? p.systemRunPlan?.agentId,
335
- sessionKey: p.sessionKey ?? p.systemRunPlan?.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 ?? {}, bridgeSock);
382
+ return await handler(isPlainObject(params) ? params : {}, bridgeSock);
381
383
  };
382
384
  const hello = () => ({
383
385
  protocol: PROTOCOL_VERSION,
@@ -1,3 +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
+ export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore, type FakeBrowserHost, type FakeBrowserHostOptions, type BrowserFixture } from './browser.ts';
2
3
  export { openclawContract, type ContractFixture } from './contract.ts';
3
- 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,4 +1,5 @@
1
1
  // The kit's fakes, contract suite and scripted model (5.11). Built in O7.
2
2
  export { fakeGateway } from "./fake-gateway.js";
3
+ export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore } from "./browser.js";
3
4
  export { openclawContract } from "./contract.js";
4
5
  export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls } from "./model-stub.js";
@@ -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: any;
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: {
@@ -30,6 +43,7 @@ export declare const stubHolding: (key?: string) => boolean;
30
43
  export type ModelStubOptions = {
31
44
  idPattern?: RegExp;
32
45
  routingMarker?: string;
46
+ onCall?: (call: StubCall) => void;
33
47
  };
34
48
  export declare function startModelStub(script?: string[], o?: ModelStubOptions): Promise<ModelStub>;
35
49
  export declare function useModelStub(kit: OpenClawKit, stub: ModelStub): Promise<void>;
@@ -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) ? m.content.map((c) => c.text ?? '').join('') : '';
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];
@@ -76,7 +77,9 @@ export function startModelStub(script = [], o = {}) {
76
77
  res.writeHead(400).end();
77
78
  return;
78
79
  }
79
- calls.push({ authorization: String(req.headers.authorization ?? ''), path: req.url ?? '', body });
80
+ const call = { authorization: String(req.headers.authorization ?? ''), path: req.url ?? '', body };
81
+ calls.push(call);
82
+ o.onCall?.(call); // Fixture evidence is persisted before any response/hold awaits.
80
83
  if (req.url === '/api/embed' || req.url === '/api/embeddings') {
81
84
  res.writeHead(200, { 'content-type': 'application/json' });
82
85
  return res.end(JSON.stringify({
@@ -95,7 +98,7 @@ export function startModelStub(script = [], o = {}) {
95
98
  const phrased = (text) => /hit the limit|no helpers in plan|sign me out|ask permission/i.test(text);
96
99
  const lastUser = [...users].reverse().find((m) => structural(words(m)))
97
100
  ?? [...users].reverse().find((m) => phrased(words(m)))
98
- ?? users.at(-1);
101
+ ?? users.at(-1) ?? {};
99
102
  const said = words(lastUser);
100
103
  const anchor = messages.lastIndexOf(lastUser);
101
104
  const results = anchor < 0 ? [] : messages.slice(anchor + 1).filter((m) => m.role === 'tool');
@@ -111,7 +114,7 @@ export function startModelStub(script = [], o = {}) {
111
114
  if (said.startsWith(routingMarker)) {
112
115
  const options = [...said.matchAll(/^- ([a-z0-9-]+):/gm)].map((m) => m[1]);
113
116
  const pick = /\[route ([a-z0-9-]+|\?)\]/.exec(said)?.[1] ?? 'chief';
114
- 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)]))));
115
118
  }
116
119
  if (/sign me out/i.test(said)) {
117
120
  res.writeHead(401, { 'content-type': 'application/json' });
@@ -165,21 +168,18 @@ export function startModelStub(script = [], o = {}) {
165
168
  });
166
169
  });
167
170
  }
168
- const plain = (res, body, text) => {
171
+ const plain = (res, model, text) => {
169
172
  const id = randomUUID();
170
173
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' });
171
- const chunk = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: body.model, choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
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`);
172
175
  chunk({ role: 'assistant', content: text });
173
176
  chunk({}, 'stop');
174
177
  res.end('data: [DONE]\n\n');
175
178
  };
176
179
  // provider id 'byokit-stub', model 'test': the stub becomes every agent's primary model.
177
180
  export async function useModelStub(kit, stub) {
178
- // The generated method table arrives with O2; until then kit.call's typed surface accepts no method names, so
179
- // configure through this string-typed view of the same runtime path.
180
- const call = kit.call.bind(kit);
181
- const current = await call('config.get');
182
- 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({
183
183
  models: { providers: { 'byokit-stub': {
184
184
  baseUrl: stub.url, apiKey: 'byokit-stub', api: 'openai-completions',
185
185
  models: [{ id: 'test', name: 'Test', reasoning: true, input: ['text'],
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OutputSchema } from './output.ts';
2
+ import type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
2
3
  export type { OutputSchema, SchemaOutput } from './output.ts';
3
4
  export type Member = string;
4
5
  export interface ToolSpec {
@@ -29,6 +30,9 @@ export interface ToolHost {
29
30
  }
30
31
  export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | undefined> extends RunRef {
31
32
  message: string;
33
+ /** Stable per-dispatch action key; omit for a fresh UUID. Gateway-wide across agents/sessions for `agent`.
34
+ * Reuse only for the same input/attempt. In-memory bounded cache, not exactly-once across engine restarts. */
35
+ idempotencyKey?: string;
32
36
  schema?: S;
33
37
  system?: string;
34
38
  images?: {
@@ -121,8 +125,9 @@ export type Decision = {
121
125
  };
122
126
  export type KitState = {
123
127
  phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
124
- why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running';
128
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch';
125
129
  retryAt?: number;
130
+ patchSet?: string | null;
126
131
  };
127
132
  export type Hello = {
128
133
  protocol: number;
@@ -136,13 +141,48 @@ export type Route = {
136
141
  choice: string;
137
142
  provider: string;
138
143
  plugin: string;
139
- billing: 'subscription' | 'api' | 'local';
144
+ billing: 'subscription' | 'api' | 'local' | 'free' | 'unknown';
140
145
  auth?: 'cli' | 'api_key' | 'token';
141
146
  terms?: 'allowed' | 'grey';
142
147
  termsUrl?: string;
143
- via: 'browser' | 'code';
148
+ via: 'browser' | 'code' | 'paste' | 'key' | 'setup_token' | 'cli' | 'plan_key' | 'cloud' | 'local' | 'endpoint';
144
149
  prerequisite: string | null;
145
150
  offer: boolean;
151
+ id?: string;
152
+ name?: string;
153
+ company?: string;
154
+ aliases?: string[];
155
+ offerPolicy?: 'default' | 'explicit';
156
+ legacy?: {
157
+ provider: string;
158
+ via: 'browser' | 'code';
159
+ };
160
+ billingFrom?: 'source' | 'host';
161
+ group?: 'models' | 'services';
162
+ platforms?: {
163
+ node: 'yes' | 'host' | 'no';
164
+ browser: 'yes' | 'host' | 'no';
165
+ rn: 'yes' | 'host' | 'no';
166
+ };
167
+ needs?: {
168
+ binary?: string;
169
+ plugin?: string;
170
+ client?: string;
171
+ };
172
+ install?: {
173
+ npmSpec?: string;
174
+ clawhubSpec?: string;
175
+ minHostVersion?: string;
176
+ };
177
+ upstream?: {
178
+ surface: 'openclaw';
179
+ id: string;
180
+ method?: string;
181
+ revision: string;
182
+ flow: 'present' | 'absent';
183
+ };
184
+ readiness?: 'ready' | 'needs_binary' | 'needs_plugin' | 'needs_host' | 'needs_client' | 'unsupported_platform' | 'no_upstream_flow';
185
+ why?: string;
146
186
  reason: string;
147
187
  source: string;
148
188
  revision: string;
@@ -173,5 +213,12 @@ export type CallOptions = {
173
213
  timeoutMs?: number | null;
174
214
  signal?: AbortSignal;
175
215
  };
216
+ /** Kit-owned invalidation only: no origin, URL, input value or lease secret. */
217
+ export type BrowserPing = {
218
+ member: Member;
219
+ kind: 'state' | 'signin';
220
+ };
221
+ export type KitEventName = GatewayEventName | 'byokit.browser';
222
+ export type KitEventPayload<E extends KitEventName> = E extends 'byokit.browser' ? BrowserPing : GatewayEventPayload<Extract<E, GatewayEventName>>;
176
223
  export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
177
224
  export type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
@@ -0,0 +1,8 @@
1
+ export declare function appendUsageBoot(dir: string, record: {
2
+ bootId: string;
3
+ } & ({
4
+ startedAt: number;
5
+ } | {
6
+ failedAt: number;
7
+ spawned: false;
8
+ })): void;