@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/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
2
|
import { routes } from "./routes.js";
|
|
3
|
+
import { MEMBER_ID, KEY_PREFIX, keyAgentId } from "./members.js";
|
|
3
4
|
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
5
|
const merge = (a, b) => {
|
|
5
6
|
for (const [k, v] of Object.entries(b))
|
|
@@ -58,6 +59,24 @@ export function reconcileConfig(saved, o) {
|
|
|
58
59
|
...routes().filter(route => route.offer && route.plugin).map(route => route.plugin)])];
|
|
59
60
|
c.plugins.entries ??= {};
|
|
60
61
|
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
62
|
+
if (o.browser) {
|
|
63
|
+
// Replace, never merge caller-controlled profiles/targets. The dead default fails closed without our hook.
|
|
64
|
+
c.browser = { enabled: true, defaultProfile: 'byokit-none', evaluateEnabled: false,
|
|
65
|
+
tabCleanup: { enabled: false }, ssrfPolicy: { dangerouslyAllowPrivateNetwork: true },
|
|
66
|
+
profiles: { 'byokit-none': { cdpUrl: 'http://127.0.0.1:1', attachOnly: true }, ...o.browser.profiles } };
|
|
67
|
+
c.plugins.allow = [...new Set([...c.plugins.allow, 'browser'])];
|
|
68
|
+
c.plugins.entries.browser = { enabled: true };
|
|
69
|
+
c.plugins.entries[o.pluginId].hooks.allowConversationAccess = true;
|
|
70
|
+
c.tools ??= {};
|
|
71
|
+
const browserTools = [...new Set([...(c.tools.alsoAllow ?? []), 'browser', 'request_sign_in', ...o.browser.tools])];
|
|
72
|
+
// The stock schema rejects allow + alsoAllow in one scope; an explicit closed allow must absorb additions.
|
|
73
|
+
if (Array.isArray(c.tools.allow)) {
|
|
74
|
+
c.tools.allow = [...new Set([...c.tools.allow, ...browserTools])];
|
|
75
|
+
delete c.tools.alsoAllow;
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
c.tools.alsoAllow = browserTools;
|
|
79
|
+
}
|
|
61
80
|
c.security ??= {};
|
|
62
81
|
c.security.installPolicy = { enabled: true, exec: {
|
|
63
82
|
source: 'exec', command: process.execPath, args: [o.policyPath],
|
|
@@ -69,8 +88,119 @@ export function reconcileConfig(saved, o) {
|
|
|
69
88
|
} };
|
|
70
89
|
return c;
|
|
71
90
|
}
|
|
91
|
+
/** Caller-owned task namespaces, including the engine key-agent rewrite; never an entire member. */
|
|
92
|
+
export function appRecoveryPrefixes(ownership) {
|
|
93
|
+
if (ownership === undefined)
|
|
94
|
+
return [];
|
|
95
|
+
const invalid = () => new Error('invalid appOwnedSessions.keyPrefixes: use agent:<member>:<task-prefix>, never the main session');
|
|
96
|
+
const prefixes = ownership?.keyPrefixes;
|
|
97
|
+
if (!Array.isArray(prefixes))
|
|
98
|
+
throw invalid();
|
|
99
|
+
return [...new Set(prefixes.flatMap(prefix => {
|
|
100
|
+
const match = typeof prefix === 'string' && /^agent:([a-z0-9-]+):(.*)$/.exec(prefix);
|
|
101
|
+
if (!match || `agent:${match[1]}:main`.startsWith(prefix))
|
|
102
|
+
throw invalid();
|
|
103
|
+
return MEMBER_ID.test(match[1]) && !match[1].startsWith(KEY_PREFIX)
|
|
104
|
+
? [prefix, `agent:${keyAgentId(match[1])}:${match[2]}`] : [prefix];
|
|
105
|
+
}))];
|
|
106
|
+
}
|
|
107
|
+
/** A closed explicit global allow is required, not a group deny or an OS sandbox claim. Every nested
|
|
108
|
+
* agent, delegate and provider policy must stay inside it. The runtime also checks tools.effective. */
|
|
109
|
+
export function browserToolPolicySafe(config, tools) {
|
|
110
|
+
const safe = new Set(['browser', 'request_sign_in', ...tools]);
|
|
111
|
+
const allowed = (value) => Array.isArray(value) && value.every(t => typeof t === 'string' && safe.has(t));
|
|
112
|
+
if (!object(config) || !object(config.tools) || !allowed(config.tools.allow) || !config.tools.allow.length)
|
|
113
|
+
return false;
|
|
114
|
+
const visit = (value) => {
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.every(visit);
|
|
117
|
+
if (!object(value))
|
|
118
|
+
return true;
|
|
119
|
+
for (const [key, child] of Object.entries(value)) {
|
|
120
|
+
if (key === 'tools') {
|
|
121
|
+
if (!object(child))
|
|
122
|
+
return false;
|
|
123
|
+
if (child.profile !== undefined || (child.allow !== undefined && !allowed(child.allow))
|
|
124
|
+
|| (child.alsoAllow !== undefined && !allowed(child.alsoAllow)))
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (!visit(child))
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// byProvider entries are tool policies too, not named `tools`.
|
|
131
|
+
if (object(value.byProvider) && Object.values(value.byProvider).some(p => !object(p) || p.profile !== undefined
|
|
132
|
+
|| (p.allow !== undefined && !allowed(p.allow)) || (p.alsoAllow !== undefined && !allowed(p.alsoAllow))))
|
|
133
|
+
return false;
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
return visit(config);
|
|
137
|
+
}
|
|
138
|
+
// The pinned config.get redacts the entire token-bearing CDP URL. Only an applied, stable host-owned
|
|
139
|
+
// config snapshot can substitute its exact endpoint; an arbitrary masked/changed URL never counts as an ack.
|
|
140
|
+
export function browserProfileAcknowledged(profile, owned, endpoint, snapshot, stableFile) {
|
|
141
|
+
if (!object(profile) || !object(owned) || profile.attachOnly !== true || owned.attachOnly !== true
|
|
142
|
+
|| owned.cdpUrl !== endpoint || !stableFile || typeof snapshot.configRevisionHash !== 'string'
|
|
143
|
+
|| !snapshot.configRevisionHash || snapshot.configRevisionHash !== snapshot.appliedConfigHash)
|
|
144
|
+
return false;
|
|
145
|
+
return profile.cdpUrl === endpoint || profile.cdpUrl === '__OPENCLAW_REDACTED__';
|
|
146
|
+
}
|
|
147
|
+
// A failed resume is not permission for engine recovery. Only a fresh exact kit run may replace it.
|
|
148
|
+
export function browserSessionMayRun(requests, key, registeredRun = false) {
|
|
149
|
+
return !requests.some(r => r.sessionKey === key && (r.state !== 'settled'
|
|
150
|
+
|| (r.settled?.resume && (r.settled.resume.state !== 'failed' || !registeredRun))));
|
|
151
|
+
}
|
|
72
152
|
export function memoryLimited(config, member) {
|
|
73
153
|
const c = config;
|
|
74
154
|
const entry = c.agents?.entries?.[member];
|
|
75
155
|
return (entry?.memory ?? c.memory)?.search?.provider === 'none';
|
|
76
156
|
}
|
|
157
|
+
const KEY_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
158
|
+
const UNSAFE_SEGMENT = new Set(['__proto__', 'constructor', 'prototype']);
|
|
159
|
+
/** One dotted path, validated at the boundary: a caller-supplied walk must never reach a prototype. */
|
|
160
|
+
export function configKeyPath(key) {
|
|
161
|
+
const segments = typeof key === 'string' && key.length ? key.split('.') : [];
|
|
162
|
+
if (!segments.length || segments.some(part => !KEY_SEGMENT.test(part) || UNSAFE_SEGMENT.has(part)))
|
|
163
|
+
throw new Error(`invalid config key: ${String(key)}`);
|
|
164
|
+
return segments;
|
|
165
|
+
}
|
|
166
|
+
/** The value at one dotted path, or undefined when it is absent. A copy, so a caller cannot hold kit state. */
|
|
167
|
+
export function readConfigKey(config, segments) {
|
|
168
|
+
let node = config;
|
|
169
|
+
for (const segment of segments) {
|
|
170
|
+
if (!object(node) || !(segment in node))
|
|
171
|
+
return undefined;
|
|
172
|
+
node = node[segment];
|
|
173
|
+
}
|
|
174
|
+
return structuredClone(node);
|
|
175
|
+
}
|
|
176
|
+
/** Writes one dotted path, creating only the missing objects on the way, and returns the value it replaced.
|
|
177
|
+
* undefined removes the key, and with it the empty objects this call created, so a removal restores the file it
|
|
178
|
+
* read. Every other key keeps its place, so one narrow set stays one narrow set. */
|
|
179
|
+
export function writeConfigKey(config, segments, value) {
|
|
180
|
+
const chain = [config], created = [];
|
|
181
|
+
for (const segment of segments.slice(0, -1)) {
|
|
182
|
+
const parent = chain[chain.length - 1];
|
|
183
|
+
if (parent[segment] === undefined) {
|
|
184
|
+
parent[segment] = {};
|
|
185
|
+
created.push(true);
|
|
186
|
+
}
|
|
187
|
+
else if (!object(parent[segment]))
|
|
188
|
+
throw new Error(`config key is not an object: ${segments.join('.')}`);
|
|
189
|
+
else
|
|
190
|
+
created.push(false);
|
|
191
|
+
chain.push(parent[segment]);
|
|
192
|
+
}
|
|
193
|
+
const leaf = chain[chain.length - 1], last = segments[segments.length - 1];
|
|
194
|
+
const previous = readConfigKey(leaf, [last]);
|
|
195
|
+
if (value !== undefined) {
|
|
196
|
+
leaf[last] = structuredClone(value);
|
|
197
|
+
return previous;
|
|
198
|
+
}
|
|
199
|
+
delete leaf[last];
|
|
200
|
+
for (let depth = chain.length - 1; depth > 0; depth--) {
|
|
201
|
+
if (!created[depth - 1] || Object.keys(chain[depth]).length)
|
|
202
|
+
break;
|
|
203
|
+
delete chain[depth - 1][segments[depth - 1]];
|
|
204
|
+
}
|
|
205
|
+
return previous;
|
|
206
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type AgentUsageReading } from './usage.ts';
|
|
2
|
+
import type { SessionsUsageParams } from '@openclaw/gateway-protocol';
|
|
3
|
+
import type { Member } from './types.ts';
|
|
4
|
+
export type EngineStartedKind = 'workshop-review';
|
|
5
|
+
export type EngineStartedCharge = {
|
|
6
|
+
chargeId: string;
|
|
7
|
+
member: Member;
|
|
8
|
+
kind: EngineStartedKind;
|
|
9
|
+
state: 'counted' | 'reported-missing' | 'pending' | 'interrupted';
|
|
10
|
+
startedAt: number;
|
|
11
|
+
endedAt?: number;
|
|
12
|
+
outcome?: 'nothing' | 'proposed' | 'applied' | 'failed';
|
|
13
|
+
provider: string;
|
|
14
|
+
model: string;
|
|
15
|
+
billing: 'subscription' | 'api' | 'local' | 'unknown';
|
|
16
|
+
authProfileId?: string;
|
|
17
|
+
tokens?: {
|
|
18
|
+
input?: number;
|
|
19
|
+
output?: number;
|
|
20
|
+
cacheRead?: number;
|
|
21
|
+
cacheWrite?: number;
|
|
22
|
+
reasoningTokens?: number;
|
|
23
|
+
total: number;
|
|
24
|
+
};
|
|
25
|
+
tokenSource: 'engine-reported';
|
|
26
|
+
cost: {
|
|
27
|
+
state: 'missing';
|
|
28
|
+
};
|
|
29
|
+
bootId: string;
|
|
30
|
+
seq: number;
|
|
31
|
+
origin?: {
|
|
32
|
+
sessionKey: string;
|
|
33
|
+
runId?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type AgentDayUsage = {
|
|
37
|
+
member: Member;
|
|
38
|
+
window: {
|
|
39
|
+
startMs: number;
|
|
40
|
+
endMs: number;
|
|
41
|
+
} & ({
|
|
42
|
+
mode: 'utc';
|
|
43
|
+
} | {
|
|
44
|
+
mode: 'time-zone';
|
|
45
|
+
timeZone: string;
|
|
46
|
+
});
|
|
47
|
+
transcripts: Omit<AgentUsageReading, 'window'> & {
|
|
48
|
+
window: {
|
|
49
|
+
startDate: string;
|
|
50
|
+
endDate: string;
|
|
51
|
+
} & ({
|
|
52
|
+
mode: 'utc';
|
|
53
|
+
} | {
|
|
54
|
+
mode: 'time-zone';
|
|
55
|
+
timeZone: string;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
engineStarted: {
|
|
59
|
+
state: 'available' | 'unavailable';
|
|
60
|
+
coverageSince?: number;
|
|
61
|
+
charges: EngineStartedCharge[];
|
|
62
|
+
unreadableLines: number;
|
|
63
|
+
};
|
|
64
|
+
coverage: 'transcripts+workshop-review';
|
|
65
|
+
complete: boolean;
|
|
66
|
+
knownTotalTokens?: number;
|
|
67
|
+
};
|
|
68
|
+
/** Only accept the kit's bounded, content-free RPC result; malformed facts make the reading unavailable. */
|
|
69
|
+
export declare function engineStartedOf(raw: unknown, member: Member, window: AgentDayUsage['window']): AgentDayUsage['engineStarted'] & {
|
|
70
|
+
complete: boolean;
|
|
71
|
+
};
|
|
72
|
+
/** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
|
|
73
|
+
export declare function readAgentDayUsage(client: {
|
|
74
|
+
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
75
|
+
call(method: 'sessions.usage', params: SessionsUsageParams): Promise<unknown>;
|
|
76
|
+
}, member: Member, window: AgentDayUsage['window']): Promise<AgentDayUsage>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Portable additive day reader. No host files, pricing guesses or side totals for persisted turns.
|
|
2
|
+
import { agentUsageOf } from "./usage.js";
|
|
3
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
|
+
const count = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
5
|
+
const text = (v) => typeof v === 'string' && v.length > 0 && v.length <= 512;
|
|
6
|
+
const integer = (v) => count(v) && Number.isSafeInteger(v);
|
|
7
|
+
function dates(window) {
|
|
8
|
+
if (!integer(window.startMs) || !integer(window.endMs) || window.startMs > window.endMs)
|
|
9
|
+
throw new Error('Invalid usage window');
|
|
10
|
+
if (window.mode !== 'utc' && window.mode !== 'time-zone')
|
|
11
|
+
throw new Error('Gateway timezone is unsupported');
|
|
12
|
+
const zone = window.mode === 'utc' ? 'UTC' : window.timeZone;
|
|
13
|
+
const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
14
|
+
const day = (at) => {
|
|
15
|
+
const p = Object.fromEntries(formatter.formatToParts(at).map(p => [p.type, p.value]));
|
|
16
|
+
return `${p.year}-${p.month}-${p.day}`;
|
|
17
|
+
};
|
|
18
|
+
return { startDate: day(window.startMs), endDate: day(window.endMs) };
|
|
19
|
+
}
|
|
20
|
+
/** Only accept the kit's bounded, content-free RPC result; malformed facts make the reading unavailable. */
|
|
21
|
+
export function engineStartedOf(raw, member, window) {
|
|
22
|
+
const unavailable = { state: 'unavailable', charges: [], unreadableLines: 0, complete: false };
|
|
23
|
+
if (!object(raw) || raw.agentId !== member || raw.startMs !== window.startMs || raw.endMs !== window.endMs || raw.state !== 'available' ||
|
|
24
|
+
!Array.isArray(raw.facts) || !integer(raw.unreadableLines) || !count(raw.coverageSince) || typeof raw.complete !== 'boolean')
|
|
25
|
+
return unavailable;
|
|
26
|
+
const phases = new Map();
|
|
27
|
+
for (const fact of raw.facts) {
|
|
28
|
+
if (!object(fact) || fact.v !== 1 || fact.agentId !== member || fact.kind !== 'workshop-review' ||
|
|
29
|
+
!['started', 'ended'].includes(String(fact.phase)) || !text(fact.chargeId) || !text(fact.bootId) ||
|
|
30
|
+
!text(fact.provider) || !text(fact.model) || !integer(fact.seq) || fact.seq < 1 || !count(fact.at) || !count(fact.startedAt) ||
|
|
31
|
+
(fact.authProfileId !== undefined && !text(fact.authProfileId)) ||
|
|
32
|
+
(fact.outcome !== undefined && !['nothing', 'proposed', 'applied', 'failed'].includes(String(fact.outcome))))
|
|
33
|
+
return unavailable;
|
|
34
|
+
const key = `${fact.chargeId}\0${fact.phase}`;
|
|
35
|
+
const previous = phases.get(key);
|
|
36
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(fact))
|
|
37
|
+
return unavailable;
|
|
38
|
+
phases.set(key, fact);
|
|
39
|
+
}
|
|
40
|
+
const ids = new Set([...phases.values()].map(f => f.chargeId));
|
|
41
|
+
const charges = [];
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const ended = phases.get(`${id}\0ended`), started = phases.get(`${id}\0started`);
|
|
44
|
+
if (ended && started && ['agentId', 'bootId', 'kind', 'provider', 'model', 'authProfileId', 'startedAt'].some(k => started[k] !== ended[k]))
|
|
45
|
+
return unavailable;
|
|
46
|
+
const fact = ended ?? started;
|
|
47
|
+
const at = ended ? fact.at : fact.startedAt;
|
|
48
|
+
if (at < window.startMs || at > window.endMs)
|
|
49
|
+
continue;
|
|
50
|
+
let tokens;
|
|
51
|
+
if (object(fact.usage) && count(fact.usage.total) && ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoningTokens'].every(k => fact.usage[k] === undefined || count(fact.usage[k]))) {
|
|
52
|
+
tokens = { total: fact.usage.total };
|
|
53
|
+
for (const k of ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoningTokens'])
|
|
54
|
+
if (count(fact.usage[k]))
|
|
55
|
+
tokens[k] = fact.usage[k];
|
|
56
|
+
}
|
|
57
|
+
const charge = { chargeId: id, member, kind: 'workshop-review', state: ended ? (tokens ? 'counted' : 'reported-missing') :
|
|
58
|
+
fact.bootId === raw.liveBootId ? 'pending' : 'interrupted', startedAt: fact.startedAt,
|
|
59
|
+
provider: fact.provider, model: fact.model, billing: 'unknown', tokenSource: 'engine-reported',
|
|
60
|
+
cost: { state: 'missing' }, bootId: fact.bootId, seq: fact.seq };
|
|
61
|
+
if (ended)
|
|
62
|
+
charge.endedAt = fact.at;
|
|
63
|
+
if (fact.outcome !== undefined)
|
|
64
|
+
charge.outcome = fact.outcome;
|
|
65
|
+
if (fact.authProfileId !== undefined)
|
|
66
|
+
charge.authProfileId = fact.authProfileId;
|
|
67
|
+
if (tokens && ended)
|
|
68
|
+
charge.tokens = tokens;
|
|
69
|
+
if (object(fact.origin) && text(fact.origin.sessionKey) && (fact.origin.runId === undefined || text(fact.origin.runId)))
|
|
70
|
+
charge.origin = { sessionKey: fact.origin.sessionKey, ...(fact.origin.runId ? { runId: fact.origin.runId } : {}) };
|
|
71
|
+
charges.push(charge);
|
|
72
|
+
}
|
|
73
|
+
charges.sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt) || a.chargeId.localeCompare(b.chargeId));
|
|
74
|
+
return { state: 'available', coverageSince: raw.coverageSince, charges, unreadableLines: raw.unreadableLines,
|
|
75
|
+
complete: raw.complete && raw.unreadableLines === 0 && window.startMs >= raw.coverageSince && charges.every(c => c.state === 'counted') };
|
|
76
|
+
}
|
|
77
|
+
/** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
|
|
78
|
+
export async function readAgentDayUsage(client, member, window) {
|
|
79
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(member))
|
|
80
|
+
throw new Error('Invalid member');
|
|
81
|
+
const day = dates(window);
|
|
82
|
+
const [transcriptRaw, ledgerRaw] = await Promise.all([
|
|
83
|
+
client.call('sessions.usage', { agentId: member, ...day, mode: window.mode === 'utc' ? 'utc' : 'specific',
|
|
84
|
+
...(window.mode === 'time-zone' ? { timeZone: window.timeZone } : {}), groupBy: 'instance', limit: 1 }).catch(() => undefined),
|
|
85
|
+
client.callDynamic('byokit.usage.engineStarted', { agentId: member, startMs: window.startMs, endMs: window.endMs }).catch(() => undefined),
|
|
86
|
+
]);
|
|
87
|
+
const transcripts = { ...agentUsageOf(transcriptRaw, member, day), window: { ...day,
|
|
88
|
+
...(window.mode === 'utc' ? { mode: 'utc' } : { mode: 'time-zone', timeZone: window.timeZone }) } };
|
|
89
|
+
// The pinned RPC reports calendar-day totals, not arbitrary millisecond slices. Never claim an exact partial-day total.
|
|
90
|
+
const clock = new Intl.DateTimeFormat('en-GB', { timeZone: window.mode === 'utc' ? 'UTC' : window.timeZone,
|
|
91
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' });
|
|
92
|
+
if (clock.format(window.startMs) !== '00:00:00' || window.startMs % 1000 !== 0 ||
|
|
93
|
+
clock.format(window.endMs) !== '23:59:59' || window.endMs % 1000 !== 999) {
|
|
94
|
+
transcripts.state = 'unavailable';
|
|
95
|
+
delete transcripts.totals;
|
|
96
|
+
}
|
|
97
|
+
const { complete, ...engineStarted } = engineStartedOf(ledgerRaw, member, window);
|
|
98
|
+
const result = { member, window, transcripts, engineStarted, coverage: 'transcripts+workshop-review',
|
|
99
|
+
complete: complete && transcripts.state === 'available' };
|
|
100
|
+
if (transcripts.totals && engineStarted.state === 'available')
|
|
101
|
+
result.knownTotalTokens = transcripts.totals.totalTokens +
|
|
102
|
+
engineStarted.charges.reduce((n, c) => n + (c.state === 'counted' ? c.tokens.total : 0), 0);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
package/dist/device.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { BrowserDevice, SignInRefusedWhy } from './browser.ts';
|
|
1
2
|
import type { DeviceLink } from '@byokit/link';
|
|
3
|
+
import type { RouteView } from './routes.ts';
|
|
2
4
|
import { type AccountView } from './words.ts';
|
|
3
|
-
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState,
|
|
5
|
+
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView } from './types.ts';
|
|
4
6
|
export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
|
|
5
7
|
export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
|
|
6
8
|
/** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
|
|
@@ -13,6 +15,12 @@ export type OpenClawLinkEvent = {
|
|
|
13
15
|
event: 'approval';
|
|
14
16
|
change: 'added' | 'resolved';
|
|
15
17
|
approval: Approval;
|
|
18
|
+
} | {
|
|
19
|
+
event: 'byokit.browser';
|
|
20
|
+
payload: {
|
|
21
|
+
member: string;
|
|
22
|
+
kind: 'state' | 'signin';
|
|
23
|
+
};
|
|
16
24
|
};
|
|
17
25
|
/** One `oc.sessions` row: `sessions.list` filtered to the member's `agent:<member>:` keys. */
|
|
18
26
|
export type SessionRow = {
|
|
@@ -55,9 +63,25 @@ export declare class LinkRefused extends Error {
|
|
|
55
63
|
readonly sealed = true;
|
|
56
64
|
constructor(message: string);
|
|
57
65
|
}
|
|
66
|
+
export declare class BrowserRefused extends LinkRefused {
|
|
67
|
+
readonly why: SignInRefusedWhy;
|
|
68
|
+
constructor(why: SignInRefusedWhy);
|
|
69
|
+
}
|
|
70
|
+
export type SignInNotice = {
|
|
71
|
+
source: 'signin';
|
|
72
|
+
id: string;
|
|
73
|
+
gen: number;
|
|
74
|
+
member: string;
|
|
75
|
+
site: string;
|
|
76
|
+
};
|
|
77
|
+
/** A sealed hint, never an authorization or a lease. Re-fetch signIns() before taking any action. */
|
|
78
|
+
export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
|
|
79
|
+
/** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
|
|
80
|
+
export declare function browserDevice(link: DeviceLink): BrowserDevice;
|
|
58
81
|
export declare function openclawDevice(link: DeviceLink): {
|
|
82
|
+
browser: BrowserDevice;
|
|
59
83
|
state(): Promise<DeviceState>;
|
|
60
|
-
routes(): Promise<
|
|
84
|
+
routes(): Promise<RouteView[]>;
|
|
61
85
|
signIn: {
|
|
62
86
|
start(p: string, via: 'browser' | 'code'): Promise<SignInView>;
|
|
63
87
|
view(p: string): Promise<AccountView>;
|
|
@@ -79,6 +103,12 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
79
103
|
registerNotices(seed: Uint8Array): Promise<void>;
|
|
80
104
|
openNotice(data: Record<string, unknown>, seed: Uint8Array): Approval | null;
|
|
81
105
|
call<M extends GatewayMethod>(method: M, params: GatewayParams<M>): Promise<GatewayResult<M>>;
|
|
106
|
+
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
82
107
|
};
|
|
83
108
|
export { openNotice } from './notices.ts';
|
|
109
|
+
export { readAgentUsage, agentUsageOf } from './usage.ts';
|
|
110
|
+
export { readAgentDayUsage } from './day-usage.ts';
|
|
111
|
+
export type { AgentDayUsage, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
|
|
112
|
+
export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageWindow } from './usage.ts';
|
|
113
|
+
export type { LiveSource, BrowserState, SignInMethodHint, SignInReason, SignInChoice, SettledState, SettledReason, ResumeState, NeedSignIn, TakeoverLease, LiveViewState, LiveFrame, LiveInput, ThumbnailResult, SignInRefusedWhy, BrowserOptions, SiteVerifier, BrowserHost, BrowserDevice } from './browser.ts';
|
|
84
114
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
package/dist/device.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
// (test/portable.test.ts guards it). Only type imports from @byokit/link; the seal crypto bundles cleanly.
|
|
3
|
-
import { boxKeyPairFromSeed } from '@byokit/seal';
|
|
1
|
+
import { openBoxFromSeed, boxKeyPairFromSeed } from '@byokit/seal';
|
|
4
2
|
import { toAccountView } from "./words.js";
|
|
5
|
-
import { b64urlEncode, openNotice } from "./notices.js";
|
|
3
|
+
import { b64urlDecode, b64urlEncode, openNotice } from "./notices.js";
|
|
6
4
|
/**
|
|
7
5
|
* The host ended a stream with its own words (e.g. a refusal). Portable twin of link's PublicLinkError:
|
|
8
6
|
* same shape ({ code: 'failed', sealed: true }) so a screen can show .message the same way. A device-side
|
|
@@ -16,6 +14,10 @@ export class LinkRefused extends Error {
|
|
|
16
14
|
this.name = 'LinkRefused';
|
|
17
15
|
}
|
|
18
16
|
}
|
|
17
|
+
export class BrowserRefused extends LinkRefused {
|
|
18
|
+
why;
|
|
19
|
+
constructor(why) { super(`Sign-in refused: ${why}`); this.why = why; }
|
|
20
|
+
}
|
|
19
21
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
22
|
/**
|
|
21
23
|
* Newline-delimited JSON frames off a link stream, in order. A host end with an error becomes a
|
|
@@ -115,8 +117,223 @@ function liveStream(open) {
|
|
|
115
117
|
},
|
|
116
118
|
};
|
|
117
119
|
}
|
|
120
|
+
/** A sealed hint, never an authorization or a lease. Re-fetch signIns() before taking any action. */
|
|
121
|
+
export function openSignInNotice(data, seed) {
|
|
122
|
+
if (data.v !== 1 || typeof data.sealed !== 'string' || !(seed instanceof Uint8Array) || seed.length !== 32)
|
|
123
|
+
return null;
|
|
124
|
+
const bundle = b64urlDecode(data.sealed);
|
|
125
|
+
const bytes = bundle && openBoxFromSeed(bundle, seed);
|
|
126
|
+
if (!bytes)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const v = JSON.parse(new TextDecoder().decode(bytes));
|
|
130
|
+
if (!isRecord(v) || v.source !== 'signin' || typeof v.id !== 'string' || !Number.isSafeInteger(v.gen)
|
|
131
|
+
|| typeof v.member !== 'string' || typeof v.site !== 'string')
|
|
132
|
+
return null;
|
|
133
|
+
return { source: 'signin', id: v.id, gen: v.gen, member: v.member, site: v.site };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
bytes.fill(0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function decodeBrowserFrame(value) {
|
|
143
|
+
if (!isRecord(value) || typeof value.jpeg !== 'string' || value.jpeg.length > 8 * 1024 * 1024
|
|
144
|
+
|| !Number.isSafeInteger(value.seq) || !Number.isSafeInteger(value.w) || !Number.isSafeInteger(value.h)
|
|
145
|
+
|| value.w < 1 || value.h < 1 || typeof value.at !== 'number' || !Number.isFinite(value.at))
|
|
146
|
+
throw new LinkRefused('Invalid browser frame');
|
|
147
|
+
const jpeg = b64urlDecode(value.jpeg);
|
|
148
|
+
if (!jpeg)
|
|
149
|
+
throw new LinkRefused('Invalid browser frame');
|
|
150
|
+
return { seq: value.seq, w: value.w, h: value.h, at: value.at, jpeg };
|
|
151
|
+
}
|
|
152
|
+
/** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
|
|
153
|
+
export function browserDevice(link) {
|
|
154
|
+
const call = async (op, args = {}) => {
|
|
155
|
+
const it = liveStream(() => link.stream(`oc.browser.${op}`, args))[Symbol.asyncIterator]();
|
|
156
|
+
try {
|
|
157
|
+
const reply = await it.next();
|
|
158
|
+
if (!reply.done && isRecord(reply.value) && typeof reply.value.refused === 'string')
|
|
159
|
+
throw new BrowserRefused(reply.value.refused);
|
|
160
|
+
if (reply.done || !isRecord(reply.value) || !('value' in reply.value))
|
|
161
|
+
throw new LinkRefused('Browser request ended');
|
|
162
|
+
return reply.value.value;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await it.return?.();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
state: member => call('state', { member }),
|
|
170
|
+
signIns: () => call('signins'),
|
|
171
|
+
takeover: (id, gen, o) => call('takeover', { id, gen, ...o }),
|
|
172
|
+
confirmOrigin: async (lease, origin) => { await call('confirmorigin', { lease, origin }); },
|
|
173
|
+
done: lease => call('done', { lease }),
|
|
174
|
+
notNow: (id, gen) => call('notnow', { id, gen }),
|
|
175
|
+
reopen: (id, gen) => call('reopen', { id, gen }),
|
|
176
|
+
retry: (id, gen) => call('retry', { id, gen }),
|
|
177
|
+
cancel: (id, gen) => call('cancel', { id, gen }),
|
|
178
|
+
forget: async (member, site) => { await call('forget', { member, site }); },
|
|
179
|
+
thumbnail: async (source) => {
|
|
180
|
+
const result = await call('thumb', { source });
|
|
181
|
+
return result.state === 'ok' ? { state: 'ok', frame: decodeBrowserFrame(result.frame) } : result;
|
|
182
|
+
},
|
|
183
|
+
live: (source, o) => {
|
|
184
|
+
let stream;
|
|
185
|
+
let closed = false;
|
|
186
|
+
let ended = false;
|
|
187
|
+
let inputBusy = false;
|
|
188
|
+
let inputBytes = 0;
|
|
189
|
+
const inputs = [];
|
|
190
|
+
let text = '';
|
|
191
|
+
let frame;
|
|
192
|
+
let state = { source, mode: o?.lease ? 'control' : 'observe', phase: 'connecting' };
|
|
193
|
+
const frameWaiters = new Set();
|
|
194
|
+
const stateWaiters = new Set();
|
|
195
|
+
const wake = () => {
|
|
196
|
+
for (const w of [...frameWaiters, ...stateWaiters])
|
|
197
|
+
w();
|
|
198
|
+
frameWaiters.clear();
|
|
199
|
+
stateWaiters.clear();
|
|
200
|
+
};
|
|
201
|
+
const close = () => {
|
|
202
|
+
closed = ended = true;
|
|
203
|
+
frame = undefined;
|
|
204
|
+
state = undefined;
|
|
205
|
+
text = '';
|
|
206
|
+
inputs.length = 0;
|
|
207
|
+
inputBytes = 0;
|
|
208
|
+
stream?.end('closed');
|
|
209
|
+
wake();
|
|
210
|
+
};
|
|
211
|
+
const finish = (error) => {
|
|
212
|
+
if (closed || ended)
|
|
213
|
+
return;
|
|
214
|
+
ended = true;
|
|
215
|
+
frame = undefined;
|
|
216
|
+
text = '';
|
|
217
|
+
inputs.length = 0;
|
|
218
|
+
inputBytes = 0;
|
|
219
|
+
state = { source, mode: o?.lease ? 'control' : 'observe',
|
|
220
|
+
phase: error === 'unreachable' ? 'reconnecting' : error ? 'failed' : 'ended',
|
|
221
|
+
...(error === 'removed' ? { why: 'revoked' } : {}) };
|
|
222
|
+
wake();
|
|
223
|
+
};
|
|
224
|
+
const flushInput = async () => {
|
|
225
|
+
if (inputBusy || !stream || closed || ended)
|
|
226
|
+
return;
|
|
227
|
+
inputBusy = true;
|
|
228
|
+
try {
|
|
229
|
+
while (inputs.length && !closed && !ended) {
|
|
230
|
+
const value = inputs.shift();
|
|
231
|
+
await stream.write(value);
|
|
232
|
+
if (!closed && !ended)
|
|
233
|
+
inputBytes -= new TextEncoder().encode(value).byteLength;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
finish('failed');
|
|
238
|
+
stream.end('failed');
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
inputBusy = false;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const decoder = new TextDecoder();
|
|
245
|
+
void link.stream('oc.browser.live', { source, mode: o?.lease ? 'control' : 'observe', ...o }).then(s => {
|
|
246
|
+
if (closed) {
|
|
247
|
+
s.end();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
stream = s;
|
|
251
|
+
s.onData = chunk => {
|
|
252
|
+
if (closed || ended)
|
|
253
|
+
return;
|
|
254
|
+
try {
|
|
255
|
+
text += decoder.decode(chunk, { stream: true });
|
|
256
|
+
if (text.length > 9 * 1024 * 1024)
|
|
257
|
+
throw new Error();
|
|
258
|
+
let at;
|
|
259
|
+
while ((at = text.indexOf('\n')) >= 0) {
|
|
260
|
+
const v = JSON.parse(text.slice(0, at));
|
|
261
|
+
text = text.slice(at + 1);
|
|
262
|
+
if (!isRecord(v))
|
|
263
|
+
throw new Error();
|
|
264
|
+
if ('frame' in v)
|
|
265
|
+
frame = decodeBrowserFrame(v.frame);
|
|
266
|
+
else if (isRecord(v.state)) {
|
|
267
|
+
state = v.state;
|
|
268
|
+
if (state.phase !== 'live')
|
|
269
|
+
frame = undefined;
|
|
270
|
+
if (state.phase === 'ended' || state.phase === 'failed') {
|
|
271
|
+
ended = true;
|
|
272
|
+
inputs.length = 0;
|
|
273
|
+
inputBytes = 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else
|
|
277
|
+
throw new Error();
|
|
278
|
+
}
|
|
279
|
+
wake();
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
finish('failed');
|
|
283
|
+
s.end('failed');
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
s.onEnd = finish;
|
|
287
|
+
}, () => finish('failed'));
|
|
288
|
+
const channel = (take, waiters) => ({
|
|
289
|
+
[Symbol.asyncIterator]() {
|
|
290
|
+
return {
|
|
291
|
+
async next() {
|
|
292
|
+
for (;;) {
|
|
293
|
+
if (closed)
|
|
294
|
+
return { value: undefined, done: true };
|
|
295
|
+
const value = take();
|
|
296
|
+
if (value !== undefined)
|
|
297
|
+
return { value, done: false };
|
|
298
|
+
if (ended)
|
|
299
|
+
return { value: undefined, done: true };
|
|
300
|
+
await new Promise(resolve => waiters.add(resolve));
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async return() { close(); return { value: undefined, done: true }; },
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
states: channel(() => { const s = state; state = undefined; return s; }, stateWaiters),
|
|
309
|
+
frames: channel(() => { const f = frame; frame = undefined; return f; }, frameWaiters),
|
|
310
|
+
input: i => {
|
|
311
|
+
// Ordered, bounded memory-only input while online. Never replay secret input after a drop.
|
|
312
|
+
if (closed || ended || !o?.lease)
|
|
313
|
+
return;
|
|
314
|
+
if (!stream) {
|
|
315
|
+
finish('failed');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const value = `${JSON.stringify(i)}\n`;
|
|
319
|
+
const bytes = new TextEncoder().encode(value).byteLength;
|
|
320
|
+
if (inputBytes + bytes > 65536) {
|
|
321
|
+
finish('failed');
|
|
322
|
+
stream.end('failed');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
inputs.push(value);
|
|
326
|
+
inputBytes += bytes;
|
|
327
|
+
void flushInput();
|
|
328
|
+
},
|
|
329
|
+
close,
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
118
334
|
export function openclawDevice(link) {
|
|
119
335
|
return {
|
|
336
|
+
browser: browserDevice(link),
|
|
120
337
|
state: () => link.request('oc.state'),
|
|
121
338
|
routes: () => link.request('oc.routes'),
|
|
122
339
|
signIn: {
|
|
@@ -186,10 +403,13 @@ export function openclawDevice(link) {
|
|
|
186
403
|
await link.request('oc.notices.register', { boxPublicKey: b64urlEncode(boxKeyPairFromSeed(seed).publicKey) });
|
|
187
404
|
},
|
|
188
405
|
openNotice,
|
|
406
|
+
callDynamic: (method, params) => link.request('oc.call', { method, params }),
|
|
189
407
|
call: (method, params) => link.request('oc.call', { method, params }),
|
|
190
408
|
};
|
|
191
409
|
}
|
|
192
410
|
// Portable, re-exported by ./device (7.3).
|
|
193
411
|
export { openNotice } from "./notices.js";
|
|
412
|
+
export { readAgentUsage, agentUsageOf } from "./usage.js";
|
|
413
|
+
export { readAgentDayUsage } from "./day-usage.js";
|
|
194
414
|
// The kit's sentences, so a phone or browser shows the same words the computer does (5.14).
|
|
195
415
|
export { stateWords, toAccountView, words } from "./words.js";
|