@byokit/openclaw 0.5.1 → 0.6.1
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 +37 -3
- package/README.md +172 -2
- package/dist/auth-store.js +1 -1
- package/dist/bridge.d.ts +10 -2
- package/dist/bridge.js +61 -14
- package/dist/browser/broker.d.ts +63 -0
- package/dist/browser/broker.js +990 -0
- package/dist/browser/host.d.ts +98 -0
- package/dist/browser/host.js +945 -0
- package/dist/browser/resume.d.ts +10 -0
- package/dist/browser/resume.js +23 -0
- package/dist/browser/store.d.ts +25 -0
- package/dist/browser/store.js +151 -0
- package/dist/browser/verify.d.ts +17 -0
- package/dist/browser/verify.js +38 -0
- package/dist/browser.d.ts +212 -0
- package/dist/browser.js +4 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +130 -0
- package/dist/day-usage.d.ts +76 -0
- package/dist/day-usage.js +104 -0
- package/dist/device.d.ts +32 -2
- package/dist/device.js +224 -4
- package/dist/engine-patches.d.ts +36 -0
- package/dist/engine-patches.js +320 -0
- package/dist/engine-status.d.ts +13 -0
- package/dist/engine-status.js +31 -0
- package/dist/engine.d.ts +13 -1
- package/dist/engine.js +99 -52
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/kit.d.ts +28 -3
- package/dist/kit.js +319 -12
- package/dist/link.d.ts +5 -2
- package/dist/link.js +333 -4
- package/dist/routes.d.ts +19 -8
- package/dist/routes.js +44 -11
- package/dist/routes.json +3775 -356
- package/dist/runs.js +8 -2
- package/dist/signin.d.ts +1 -1
- package/dist/signin.js +19 -4
- package/dist/testing/browser.d.ts +50 -0
- package/dist/testing/browser.js +166 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +3 -1
- package/dist/types.d.ts +50 -3
- package/dist/usage-boots.d.ts +8 -0
- package/dist/usage-boots.js +26 -0
- package/dist/usage.d.ts +51 -0
- package/dist/usage.js +61 -0
- package/dist/words.json +42 -0
- package/engine/OPENCLAW-LICENSE +24 -0
- package/engine/patches.json +59 -0
- package/engine/patches.schema.json +41 -0
- package/package.json +12 -4
- package/plugin/index.js +78 -5
- 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
|
|
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, ...
|
|
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.
|
|
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'
|
|
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
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { fakeGateway, 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
4
|
export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall } from './model-stub.ts';
|
package/dist/testing/index.js
CHANGED
|
@@ -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";
|
|
@@ -30,6 +30,7 @@ export declare const stubHolding: (key?: string) => boolean;
|
|
|
30
30
|
export type ModelStubOptions = {
|
|
31
31
|
idPattern?: RegExp;
|
|
32
32
|
routingMarker?: string;
|
|
33
|
+
onCall?: (call: StubCall) => void;
|
|
33
34
|
};
|
|
34
35
|
export declare function startModelStub(script?: string[], o?: ModelStubOptions): Promise<ModelStub>;
|
|
35
36
|
export declare function useModelStub(kit: OpenClawKit, stub: ModelStub): Promise<void>;
|
|
@@ -76,7 +76,9 @@ export function startModelStub(script = [], o = {}) {
|
|
|
76
76
|
res.writeHead(400).end();
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
const call = { authorization: String(req.headers.authorization ?? ''), path: req.url ?? '', body };
|
|
80
|
+
calls.push(call);
|
|
81
|
+
o.onCall?.(call); // Fixture evidence is persisted before any response/hold awaits.
|
|
80
82
|
if (req.url === '/api/embed' || req.url === '/api/embeddings') {
|
|
81
83
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
82
84
|
return res.end(JSON.stringify({
|
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,26 @@
|
|
|
1
|
+
// Internal lifecycle records. Accounting UUIDs never participate in process ownership or liveness.
|
|
2
|
+
import { closeSync, fsyncSync, openSync, writeSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function appendUsageBoot(dir, record) {
|
|
5
|
+
let fd;
|
|
6
|
+
try {
|
|
7
|
+
fd = openSync(join(dir, 'boots.jsonl'), 'a', 0o600);
|
|
8
|
+
const line = Buffer.from(JSON.stringify(record) + '\n');
|
|
9
|
+
if (writeSync(fd, line) !== line.length)
|
|
10
|
+
throw new Error();
|
|
11
|
+
fsyncSync(fd);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error('Usage boot record could not be made durable');
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
if (fd !== undefined) {
|
|
18
|
+
try {
|
|
19
|
+
closeSync(fd);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error('Usage boot record could not be made durable');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SessionsUsageParams } from '@openclaw/gateway-protocol';
|
|
2
|
+
import type { CallOptions, Member } from './types.ts';
|
|
3
|
+
export type UsageWindow = {
|
|
4
|
+
startDate: string;
|
|
5
|
+
endDate: string;
|
|
6
|
+
};
|
|
7
|
+
/** Engine-reported counters. Price totals are not a bill, and zero with missingCostEntries is unknown cost. */
|
|
8
|
+
export type LedgerUsageTotals = {
|
|
9
|
+
input: number;
|
|
10
|
+
output: number;
|
|
11
|
+
cacheRead: number;
|
|
12
|
+
cacheWrite: number;
|
|
13
|
+
totalTokens: number;
|
|
14
|
+
totalCost: number;
|
|
15
|
+
inputCost: number;
|
|
16
|
+
outputCost: number;
|
|
17
|
+
cacheReadCost: number;
|
|
18
|
+
cacheWriteCost: number;
|
|
19
|
+
missingCostEntries: number;
|
|
20
|
+
missingCostByModel?: Record<string, number>;
|
|
21
|
+
};
|
|
22
|
+
export type UsageCache = {
|
|
23
|
+
status: 'fresh' | 'refreshing' | 'partial' | 'stale';
|
|
24
|
+
cachedFiles: number;
|
|
25
|
+
pendingFiles: number;
|
|
26
|
+
staleFiles: number;
|
|
27
|
+
refreshedAt?: number;
|
|
28
|
+
};
|
|
29
|
+
export type AgentUsageReading = {
|
|
30
|
+
member: Member;
|
|
31
|
+
window: UsageWindow & {
|
|
32
|
+
mode: 'utc';
|
|
33
|
+
};
|
|
34
|
+
receivedAt: number;
|
|
35
|
+
/** Result assembly time, not latest model call time. */
|
|
36
|
+
updatedAt?: number;
|
|
37
|
+
cache?: UsageCache;
|
|
38
|
+
/** Detached Workshop reviews are omitted; other internal turns only count when persisted. */
|
|
39
|
+
coverage: 'retained-transcripts-only';
|
|
40
|
+
state: 'available' | 'unavailable';
|
|
41
|
+
/** Observed ledger counters only. Absent on missing, malformed, refreshing or stale data. */
|
|
42
|
+
totals?: LedgerUsageTotals;
|
|
43
|
+
/** Complete original response, including session rows, day aggregates and unknown extensions. */
|
|
44
|
+
raw: unknown;
|
|
45
|
+
};
|
|
46
|
+
/** Normalize an explicitly agent-scoped UTC response. No missing datum is replaced with zero. */
|
|
47
|
+
export declare function agentUsageOf(raw: unknown, member: Member, window: UsageWindow, receivedAt?: number): AgentUsageReading;
|
|
48
|
+
/** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
|
|
49
|
+
export declare function readAgentUsage(client: {
|
|
50
|
+
call(method: 'sessions.usage', params: SessionsUsageParams, options?: CallOptions): Promise<unknown>;
|
|
51
|
+
}, member: Member, window: UsageWindow, options?: CallOptions): Promise<AgentUsageReading>;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const count = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
3
|
+
const buckets = ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens', 'totalCost', 'inputCost', 'outputCost',
|
|
4
|
+
'cacheReadCost', 'cacheWriteCost', 'missingCostEntries'];
|
|
5
|
+
function totalsOf(v) {
|
|
6
|
+
if (!record(v) || !buckets.every(k => count(v[k])))
|
|
7
|
+
return;
|
|
8
|
+
if (v.missingCostByModel !== undefined && (!record(v.missingCostByModel) || !Object.values(v.missingCostByModel).every(count)))
|
|
9
|
+
return;
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
function cacheOf(v) {
|
|
13
|
+
if (!record(v) || !['fresh', 'refreshing', 'partial', 'stale'].includes(String(v.status)) ||
|
|
14
|
+
!['cachedFiles', 'pendingFiles', 'staleFiles'].every(k => count(v[k])) ||
|
|
15
|
+
(v.refreshedAt !== undefined && !count(v.refreshedAt)))
|
|
16
|
+
return;
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
function validate(member, window) {
|
|
20
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(member))
|
|
21
|
+
throw new Error('Invalid member');
|
|
22
|
+
for (const day of [window.startDate, window.endDate]) {
|
|
23
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !Number.isFinite(Date.parse(day)) || new Date(day).toISOString().slice(0, 10) !== day)
|
|
24
|
+
throw new Error('Expected a valid YYYY-MM-DD calendar date');
|
|
25
|
+
}
|
|
26
|
+
if (window.startDate > window.endDate)
|
|
27
|
+
throw new Error('Usage startDate must not follow endDate');
|
|
28
|
+
}
|
|
29
|
+
/** Normalize an explicitly agent-scoped UTC response. No missing datum is replaced with zero. */
|
|
30
|
+
export function agentUsageOf(raw, member, window, receivedAt = Date.now()) {
|
|
31
|
+
validate(member, window);
|
|
32
|
+
const result = { member, window: { ...window, mode: 'utc' }, receivedAt,
|
|
33
|
+
coverage: 'retained-transcripts-only', state: 'unavailable', raw };
|
|
34
|
+
if (!record(raw))
|
|
35
|
+
return result;
|
|
36
|
+
if (count(raw.updatedAt))
|
|
37
|
+
result.updatedAt = raw.updatedAt;
|
|
38
|
+
const cache = cacheOf(raw.cacheStatus);
|
|
39
|
+
if (cache)
|
|
40
|
+
result.cache = cache;
|
|
41
|
+
if (!cache || cache.status !== 'fresh' || cache.pendingFiles !== 0 || cache.staleFiles !== 0 ||
|
|
42
|
+
raw.startDate !== window.startDate || raw.endDate !== window.endDate || !record(raw.aggregates) ||
|
|
43
|
+
!Array.isArray(raw.aggregates.byAgent))
|
|
44
|
+
return result;
|
|
45
|
+
const agents = raw.aggregates.byAgent;
|
|
46
|
+
// An absent agent row is not evidence of a zero-use day. Do not read another agent's/global totals.
|
|
47
|
+
if (agents.length !== 1 || !record(agents[0]) || agents[0].agentId !== member)
|
|
48
|
+
return result;
|
|
49
|
+
const totals = totalsOf(agents[0].totals);
|
|
50
|
+
if (!totals)
|
|
51
|
+
return result;
|
|
52
|
+
result.state = 'available';
|
|
53
|
+
result.totals = totals;
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/** Works with kit.call or the portable device.call; retains the complete existing pass-through. RPC errors reject. */
|
|
57
|
+
export async function readAgentUsage(client, member, window, options) {
|
|
58
|
+
validate(member, window);
|
|
59
|
+
const raw = await client.call('sessions.usage', { agentId: member, startDate: window.startDate, endDate: window.endDate, mode: 'utc', groupBy: 'instance', limit: 1 }, options);
|
|
60
|
+
return agentUsageOf(raw, member, window);
|
|
61
|
+
}
|
package/dist/words.json
CHANGED
|
@@ -17,6 +17,48 @@
|
|
|
17
17
|
"signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
|
|
18
18
|
"signin.cancelled": "Sign-in cancelled. You can start again whenever you are ready.",
|
|
19
19
|
"signin.expired": "The sign-in took too long. Start it again.",
|
|
20
|
+
"signin.title": "Sign in to {site}",
|
|
21
|
+
"signin.takeover": "Take over to sign in",
|
|
22
|
+
"signin.notNow": "Not now",
|
|
23
|
+
"signin.cancel": "Cancel sign-in",
|
|
24
|
+
"signin.reopen": "Continue signing in",
|
|
25
|
+
"signin.retry": "Try signing in again",
|
|
26
|
+
"signin.confirmOrigin": "Continue on {origin}",
|
|
27
|
+
"signin.done": "Done signing in",
|
|
28
|
+
"signin.confirmSite": "First time {name} signs in to {site}. Type the site name to continue.",
|
|
29
|
+
"signin.offOrigin": "You're now on {origin}, not {site}. Continue only if you expected this.",
|
|
30
|
+
"signin.agentNote": "{name} says: “{note}”",
|
|
31
|
+
"signin.waiting": "{name} needs you to sign in to {site}.",
|
|
32
|
+
"signin.parked": "Sign-in to {site} is waiting for you.",
|
|
33
|
+
"signin.checking": "Sign-in details entered",
|
|
34
|
+
"signin.verified": "Signed in to {site}",
|
|
35
|
+
"signin.stillSignedOut": "{site} still shows a sign-in page. Try again?",
|
|
36
|
+
"browser.signin.expired": "The sign-in request for {site} timed out.",
|
|
37
|
+
"browser.signin.cancelled": "Sign-in to {site} was cancelled.",
|
|
38
|
+
"browser.signin.notice": "A browser sign-in is waiting for you.",
|
|
39
|
+
"signin.originMismatch": "You finished on a different site than {site}, so {name} won't continue.",
|
|
40
|
+
"signin.browserGone": "{name}'s browser closed during sign-in.",
|
|
41
|
+
"signin.superseded": "A newer request replaced this one.",
|
|
42
|
+
"signin.runReplaced": "This task moved on, so the sign-in request was closed.",
|
|
43
|
+
"signin.resumeFailed": "Signed in, but {name} couldn't continue. Try again.",
|
|
44
|
+
"signin.resumeUnknown": "Signed in, but we can't tell whether {name} continued. Check the task before retrying.",
|
|
45
|
+
"signin.noVerifier": "Sign-in details entered for {site}. {name} can't confirm you're signed in.",
|
|
46
|
+
"signin.insecureRemote": "{site} isn't secure, so taking over to sign in is off.",
|
|
47
|
+
"signin.gate": "Waiting for the person to sign in to {site}.",
|
|
48
|
+
"signin.resume": "The person signed in to {site}. Continue the task.",
|
|
49
|
+
"signin.private": "Private while someone signs in",
|
|
50
|
+
"browser.recovering": "Reconnecting to {name}'s browser…",
|
|
51
|
+
"browser.blocked.noBrowser": "No browser is set up for {name}.",
|
|
52
|
+
"browser.blocked.exhausted": "{name}'s browser stopped and could not be restarted.",
|
|
53
|
+
"browser.blocked.detached": "{name}'s browser isn't connected.",
|
|
54
|
+
"browser.blocked.unsafe": "An agent here can run commands or read files, so the browser stays off.",
|
|
55
|
+
"browser.blocked.gateOff": "Browser handoff needs the tool gate on.",
|
|
56
|
+
"browser.blocked.unprotected": "Signing in for {name} isn't available yet.",
|
|
57
|
+
"live.reconnecting": "Reconnecting…",
|
|
58
|
+
"live.browser": "Browser live view",
|
|
59
|
+
"live.label": "Browser live view",
|
|
60
|
+
"live.ended": "Browser live view ended.",
|
|
61
|
+
"live.failed": "Browser live view couldn't connect.",
|
|
20
62
|
"approval.ask": "{helper} wants to {summary}. Allow it?",
|
|
21
63
|
"approval.expired": "Nobody answered in time, so this wasn't allowed.",
|
|
22
64
|
"approval.notice": "Something is waiting for your yes.",
|