@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.
- package/CHANGELOG.md +56 -3
- package/README.md +205 -15
- package/SECURITY.md +6 -6
- package/dist/auth-store.d.ts +6 -0
- package/dist/auth-store.js +58 -15
- 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 +1003 -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 +78 -0
- package/dist/day-usage.js +104 -0
- package/dist/device.d.ts +38 -5
- 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 +9 -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 +16 -5
- package/dist/link.js +333 -6
- 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/contract.js +1 -2
- package/dist/testing/fake-gateway.d.ts +14 -7
- package/dist/testing/fake-gateway.js +8 -6
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +1 -0
- package/dist/testing/model-stub.d.ts +15 -1
- package/dist/testing/model-stub.js +11 -11
- 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 +53 -0
- package/dist/usage.js +60 -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,78 @@
|
|
|
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
|
+
/** What `readAgentDayUsage` needs from a reader: the kit's `call` plus its own string-typed RPC. */
|
|
73
|
+
export type DayUsageClient = {
|
|
74
|
+
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
75
|
+
call(method: 'sessions.usage', params: SessionsUsageParams): Promise<unknown>;
|
|
76
|
+
};
|
|
77
|
+
/** Read the transcript and Workshop terms independently. A failed term never becomes zero. */
|
|
78
|
+
export declare function readAgentDayUsage(client: DayUsageClient, member: Member, window: AgentDayUsage['window']): Promise<AgentDayUsage>;
|
|
@@ -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,13 +15,20 @@ 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 = {
|
|
19
27
|
sessionKey: string;
|
|
20
28
|
[k: string]: unknown;
|
|
21
29
|
};
|
|
22
|
-
|
|
30
|
+
/** The frame `oc.run`'s stream ends with: the run's `RunEnd`, beside its events. */
|
|
31
|
+
export type DeviceEndFrame<T = unknown> = {
|
|
23
32
|
type: 'end';
|
|
24
33
|
end: RunEnd<T>;
|
|
25
34
|
};
|
|
@@ -55,9 +64,26 @@ export declare class LinkRefused extends Error {
|
|
|
55
64
|
readonly sealed = true;
|
|
56
65
|
constructor(message: string);
|
|
57
66
|
}
|
|
58
|
-
export declare
|
|
67
|
+
export declare class BrowserRefused extends LinkRefused {
|
|
68
|
+
readonly why: SignInRefusedWhy;
|
|
69
|
+
constructor(why: SignInRefusedWhy);
|
|
70
|
+
}
|
|
71
|
+
export type SignInNotice = {
|
|
72
|
+
source: 'signin';
|
|
73
|
+
id: string;
|
|
74
|
+
gen: number;
|
|
75
|
+
member: string;
|
|
76
|
+
site: string;
|
|
77
|
+
};
|
|
78
|
+
/** A sealed hint, never an authorization or a lease. Re-fetch signIns() before taking any action. */
|
|
79
|
+
export declare function openSignInNotice(data: Record<string, unknown>, seed: Uint8Array): SignInNotice | null;
|
|
80
|
+
/** Transient streams bypass link's durable request/answer cache: no lease nonce or frame is persisted. */
|
|
81
|
+
export declare function browserDevice(link: DeviceLink): BrowserDevice;
|
|
82
|
+
/** `openclawDevice`'s device surface: every `oc.*` op the computer answers, typed. */
|
|
83
|
+
export type OpenClawDevice = {
|
|
84
|
+
browser: BrowserDevice;
|
|
59
85
|
state(): Promise<DeviceState>;
|
|
60
|
-
routes(): Promise<
|
|
86
|
+
routes(): Promise<RouteView[]>;
|
|
61
87
|
signIn: {
|
|
62
88
|
start(p: string, via: 'browser' | 'code'): Promise<SignInView>;
|
|
63
89
|
view(p: string): Promise<AccountView>;
|
|
@@ -66,7 +92,7 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
66
92
|
};
|
|
67
93
|
signOut(p: string): Promise<void>;
|
|
68
94
|
sessions(): Promise<SessionRow[]>;
|
|
69
|
-
run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent |
|
|
95
|
+
run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | DeviceEndFrame<SchemaOutput<S>>>;
|
|
70
96
|
steer(k: string, t: string, o?: {
|
|
71
97
|
auth?: 'apiKey';
|
|
72
98
|
}): Promise<void>;
|
|
@@ -79,6 +105,13 @@ export declare function openclawDevice(link: DeviceLink): {
|
|
|
79
105
|
registerNotices(seed: Uint8Array): Promise<void>;
|
|
80
106
|
openNotice(data: Record<string, unknown>, seed: Uint8Array): Approval | null;
|
|
81
107
|
call<M extends GatewayMethod>(method: M, params: GatewayParams<M>): Promise<GatewayResult<M>>;
|
|
108
|
+
callDynamic(method: string, params?: unknown): Promise<unknown>;
|
|
82
109
|
};
|
|
110
|
+
export declare function openclawDevice(link: DeviceLink): OpenClawDevice;
|
|
83
111
|
export { openNotice } from './notices.ts';
|
|
112
|
+
export { readAgentUsage, agentUsageOf } from './usage.ts';
|
|
113
|
+
export { readAgentDayUsage } from './day-usage.ts';
|
|
114
|
+
export type { AgentDayUsage, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
|
|
115
|
+
export type { AgentUsageReading, LedgerUsageTotals, UsageCache, UsageWindow } from './usage.ts';
|
|
116
|
+
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
117
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|