@byokit/openclaw 0.5.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +205 -15
  3. package/SECURITY.md +6 -6
  4. package/dist/auth-store.d.ts +6 -0
  5. package/dist/auth-store.js +58 -15
  6. package/dist/bridge.d.ts +10 -2
  7. package/dist/bridge.js +61 -14
  8. package/dist/browser/broker.d.ts +63 -0
  9. package/dist/browser/broker.js +1003 -0
  10. package/dist/browser/host.d.ts +98 -0
  11. package/dist/browser/host.js +945 -0
  12. package/dist/browser/resume.d.ts +10 -0
  13. package/dist/browser/resume.js +23 -0
  14. package/dist/browser/store.d.ts +25 -0
  15. package/dist/browser/store.js +151 -0
  16. package/dist/browser/verify.d.ts +17 -0
  17. package/dist/browser/verify.js +38 -0
  18. package/dist/browser.d.ts +212 -0
  19. package/dist/browser.js +4 -0
  20. package/dist/config.d.ts +28 -0
  21. package/dist/config.js +130 -0
  22. package/dist/day-usage.d.ts +78 -0
  23. package/dist/day-usage.js +104 -0
  24. package/dist/device.d.ts +38 -5
  25. package/dist/device.js +224 -4
  26. package/dist/engine-patches.d.ts +36 -0
  27. package/dist/engine-patches.js +320 -0
  28. package/dist/engine-status.d.ts +13 -0
  29. package/dist/engine-status.js +31 -0
  30. package/dist/engine.d.ts +13 -1
  31. package/dist/engine.js +99 -52
  32. package/dist/index.d.ts +9 -1
  33. package/dist/index.js +2 -0
  34. package/dist/kit.d.ts +28 -3
  35. package/dist/kit.js +319 -12
  36. package/dist/link.d.ts +16 -5
  37. package/dist/link.js +333 -6
  38. package/dist/routes.d.ts +19 -8
  39. package/dist/routes.js +44 -11
  40. package/dist/routes.json +3775 -356
  41. package/dist/runs.js +8 -2
  42. package/dist/signin.d.ts +1 -1
  43. package/dist/signin.js +19 -4
  44. package/dist/testing/browser.d.ts +50 -0
  45. package/dist/testing/browser.js +166 -0
  46. package/dist/testing/contract.js +1 -2
  47. package/dist/testing/fake-gateway.d.ts +14 -7
  48. package/dist/testing/fake-gateway.js +8 -6
  49. package/dist/testing/index.d.ts +3 -2
  50. package/dist/testing/index.js +1 -0
  51. package/dist/testing/model-stub.d.ts +15 -1
  52. package/dist/testing/model-stub.js +11 -11
  53. package/dist/types.d.ts +50 -3
  54. package/dist/usage-boots.d.ts +8 -0
  55. package/dist/usage-boots.js +26 -0
  56. package/dist/usage.d.ts +53 -0
  57. package/dist/usage.js +60 -0
  58. package/dist/words.json +42 -0
  59. package/engine/OPENCLAW-LICENSE +24 -0
  60. package/engine/patches.json +59 -0
  61. package/engine/patches.schema.json +41 -0
  62. package/package.json +12 -4
  63. package/plugin/index.js +78 -5
  64. package/plugin/usage.js +125 -0
@@ -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
+ }
@@ -0,0 +1,53 @@
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
+ /** What `readAgentUsage` needs from a reader: the kit (`call`) or a device (`callDynamic` + `call`) both fit. */
50
+ export type UsageClient = {
51
+ call(method: 'sessions.usage', params: SessionsUsageParams, options?: CallOptions): Promise<unknown>;
52
+ };
53
+ export declare function readAgentUsage(client: UsageClient, member: Member, window: UsageWindow, options?: CallOptions): Promise<AgentUsageReading>;
package/dist/usage.js ADDED
@@ -0,0 +1,60 @@
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
+ export async function readAgentUsage(client, member, window, options) {
57
+ validate(member, window);
58
+ const raw = await client.call('sessions.usage', { agentId: member, startDate: window.startDate, endDate: window.endDate, mode: 'utc', groupBy: 'instance', limit: 1 }, options);
59
+ return agentUsageOf(raw, member, window);
60
+ }
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.",
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Third-party notices for incorporated or adapted code are recorded in
24
+ THIRD_PARTY_NOTICES.md.
@@ -0,0 +1,59 @@
1
+ {
2
+ "v": 1,
3
+ "id": "171ce088cd516dcf",
4
+ "upstream": {
5
+ "name": "openclaw",
6
+ "version": "2026.8.1",
7
+ "integrity": "sha512-bSaFeaDFnQH/bU1vgKMac6eHkHHPHG0C/uwduXGI3eIS3lyiYSwmDU5ehhBUUhlPeV85tL5/KVwmoH48nX1tWw==",
8
+ "commit": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
9
+ "license": "MIT"
10
+ },
11
+ "files": [
12
+ {
13
+ "path": "dist/experience-review-default-6DPIIJds.js",
14
+ "before": "30f43da07b2520dd785df42ba417737a0f2cc0286922fe2189f06ecbafc1ab9c",
15
+ "after": "e9448c45a2d7f5e8ab7f1996269a4291d2453aac7641ea82cb82cc238e93cb43",
16
+ "edits": [
17
+ {
18
+ "find": "import { n as getRuntimeConfig } from \"./io.runtime-CNGn9TXj.js\";\n",
19
+ "replace": "import { n as getRuntimeConfig } from \"./io.runtime-CNGn9TXj.js\";\nimport { openSync as byokitOpen, writeSync as byokitWrite, fsyncSync as byokitFsync, closeSync as byokitClose } from \"node:fs\";\nimport { join as byokitJoin } from \"node:path\";\n"
20
+ },
21
+ {
22
+ "find": "async function runSkillExperienceReviewInner(candidate, deps) {\n",
23
+ "replace": "/** BYOKit R4-1: content-free durable Gateway Workshop facts, inert without kit env. */\nfunction byokitUsageFact(fact) {\n\tconst dir = process.env.BYOKIT_ENGINE_USAGE_LEDGER;\n\tconst bootId = process.env.BYOKIT_ENGINE_BOOT;\n\tif (!dir || !bootId) return;\n\tconst key = Symbol.for(\"byokit.engine-usage.v1\");\n\tconst live = globalThis[key] ??= { bootId, months: {}, inFlight: {} };\n\tconst month = new Date(fact.at).toISOString().slice(0, 7);\n\tconst counter = live.months[month] ??= { lastSeq: 0, failed: 0 };\n\tconst seq = ++counter.lastSeq;\n\tif (fact.phase === \"started\") live.inFlight[fact.chargeId] = { startedAt: fact.startedAt };\n\telse delete live.inFlight[fact.chargeId];\n\ttry {\n\t\tconst fd = byokitOpen(byokitJoin(dir, \"engine-started-\" + month + \".jsonl\"), \"a\", 0o600);\n\t\ttry {\n\t\t\tconst line = Buffer.from(JSON.stringify({ v: 1, ...fact, bootId, seq }) + \"\\n\");\n\t\t\tif (byokitWrite(fd, line) !== line.length) throw new Error(\"short accounting write\");\n\t\t\tbyokitFsync(fd);\n\t\t} finally { byokitClose(fd); }\n\t} catch { counter.failed++; }\n}\nasync function runSkillExperienceReviewInner(candidate, deps) {\n"
24
+ },
25
+ {
26
+ "find": "\tlet outcome;\n\tlet proposalId;\n\tlet usage;\n",
27
+ "replace": "\tlet outcome;\n\tlet proposalId;\n\tlet usage;\n\tlet byokitRunUsage;\n\tconst byokitFact = { chargeId: runId, agentId: foregroundPromptContext.agentId, kind: \"workshop-review\", provider: modelProviderId, model: modelId, ...candidate.ctx.authProfileId ? { authProfileId: candidate.ctx.authProfileId } : {}, startedAt: attemptedAtMs, origin: { sessionKey: foregroundSessionKey, ...candidate.ctx.runId ? { runId: candidate.ctx.runId } : {} } };\n"
28
+ },
29
+ {
30
+ "find": "\ttry {\n\t\tlet embeddedResult;\n",
31
+ "replace": "\tbyokitUsageFact({ ...byokitFact, phase: \"started\", at: attemptedAtMs });\n\ttry {\n\t\tlet embeddedResult;\n"
32
+ },
33
+ {
34
+ "find": "\t\t} finally {\n\t\t\tpreparedRunAdmission.close();\n\t\t}\n\t\tassertSkillReviewRunSucceeded(embeddedResult);\n",
35
+ "replace": "\t\t} finally {\n\t\t\tpreparedRunAdmission.close();\n\t\t}\n\t\tbyokitRunUsage = embeddedResult?.meta?.agentMeta?.usage;\n\t\tassertSkillReviewRunSucceeded(embeddedResult);\n"
36
+ },
37
+ {
38
+ "find": "\t} catch (error) {\n\t\trecordSkillExperienceReviewOutcome(workspaceDir, {\n",
39
+ "replace": "\t} catch (error) {\n\t\tbyokitUsageFact({ ...byokitFact, phase: \"ended\", at: Date.now(), outcome: \"failed\", ...byokitRunUsage ? { usage: byokitRunUsage } : {} });\n\t\trecordSkillExperienceReviewOutcome(workspaceDir, {\n"
40
+ },
41
+ {
42
+ "find": "\t} finally {\n\t\tclearAgentRunContext(runId);\n\t}\n\trecordSkillExperienceReviewOutcome(workspaceDir, {\n",
43
+ "replace": "\t} finally {\n\t\tclearAgentRunContext(runId);\n\t}\n\tbyokitUsageFact({ ...byokitFact, phase: \"ended\", at: Date.now(), outcome, ...byokitRunUsage ? { usage: byokitRunUsage } : {} });\n\trecordSkillExperienceReviewOutcome(workspaceDir, {\n"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "path": "dist/main-session-recovery-state-BWIrIyi_.js",
49
+ "before": "418a957496786dd7ed28df4a7a02e31e9567020e1be5804545e785baf965f225",
50
+ "after": "f1afea642c78d7979ed40ce43c102c41f790c5dbedaec0e6e7eb8d6f47bc0032",
51
+ "edits": [
52
+ {
53
+ "find": "function isMainRestartRecoveryCandidate(entry, sessionKey) {",
54
+ "replace": "// BYOKit: caller-owned task prefixes never acquire automatic restart recovery ownership.\nconst byokitAppOwnedSessionPrefixes = (() => {\n\ttry {\n\t\tconst prefixes = JSON.parse(process.env.BYOKIT_APP_OWNED_SESSION_PREFIXES ?? \"[]\");\n\t\treturn Array.isArray(prefixes) && prefixes.every((prefix) => {\n\t\t\tconst agent = typeof prefix === \"string\" && /^agent:([a-z0-9-]+):/.exec(prefix);\n\t\t\treturn agent && !`agent:${agent[1]}:main`.startsWith(prefix);\n\t\t}) ? prefixes : [];\n\t} catch { return []; }\n})();\nfunction isMainRestartRecoveryCandidate(entry, sessionKey) {\n\tif (byokitAppOwnedSessionPrefixes.some((prefix) => sessionKey.startsWith(prefix))) return false;"
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "BYOKit pinned OpenClaw patch set v1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["v", "id", "upstream", "files"],
7
+ "properties": {
8
+ "v": { "const": 1 },
9
+ "id": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
10
+ "upstream": {
11
+ "type": "object", "additionalProperties": false,
12
+ "required": ["name", "version", "integrity", "commit", "license"],
13
+ "properties": {
14
+ "name": { "const": "openclaw" },
15
+ "version": { "type": "string" },
16
+ "integrity": { "type": "string", "pattern": "^sha512-" },
17
+ "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
18
+ "license": { "const": "MIT" }
19
+ }
20
+ },
21
+ "files": { "type": "array", "items": { "$ref": "#/$defs/file" } }
22
+ },
23
+ "$defs": {
24
+ "file": {
25
+ "type": "object", "additionalProperties": false,
26
+ "required": ["path", "before", "after", "edits"],
27
+ "properties": {
28
+ "path": { "type": "string", "pattern": "^dist/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_.-]+\\.m?js$" },
29
+ "before": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
30
+ "after": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
31
+ "edits": {
32
+ "type": "array", "minItems": 1,
33
+ "items": {
34
+ "type": "object", "additionalProperties": false, "required": ["find", "replace"],
35
+ "properties": { "find": { "type": "string", "minLength": 1 }, "replace": { "type": "string", "minLength": 1 } }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.5.1",
3
+ "version": "0.6.2",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,6 +29,14 @@
29
29
  "types": "./dist/device.d.ts",
30
30
  "default": "./dist/device.js"
31
31
  },
32
+ "./usage": {
33
+ "types": "./dist/usage.d.ts",
34
+ "default": "./dist/usage.js"
35
+ },
36
+ "./day-usage": {
37
+ "types": "./dist/day-usage.d.ts",
38
+ "default": "./dist/day-usage.js"
39
+ },
32
40
  "./link": {
33
41
  "types": "./dist/link.d.ts",
34
42
  "default": "./dist/link.js"
@@ -53,11 +61,11 @@
53
61
  },
54
62
  "dependencies": {
55
63
  "@byokit/link": "0.7.0",
56
- "@byokit/relay": "0.5.1",
64
+ "@byokit/relay": "0.5.2",
57
65
  "@byokit/reach": "0.6.0",
58
- "@byokit/seal": "0.2.0",
66
+ "@byokit/seal": "0.3.0",
59
67
  "@byokit/secrets": "0.5.0",
60
- "@openclaw/gateway-client": "2026.8.1",
68
+ "@openclaw/gateway-client": "2026.9.6",
61
69
  "@openclaw/gateway-protocol": "2026.8.1",
62
70
  "ws": "8.21.3"
63
71
  },
package/plugin/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { connect } from 'node:net';
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { registerKeys } from './keys.js';
7
+ import { registerUsage } from './usage.js';
7
8
 
8
9
  /** TypeBox's Kind is a global symbol, so these schemas are valid TypeBox without importing typebox. */
9
10
  const Kind = Symbol.for('TypeBox.Kind');
@@ -14,6 +15,36 @@ const PERMIT_PARAM = table.permitParam;
14
15
  const TOOLS = new Map(table.tools.map((t) => [t.name, t]));
15
16
  // Absent (a table from before the flag) reads as true: fail closed.
16
17
  const GATE_BUILTINS = table.gateBuiltins !== false;
18
+ const BROWSER = table.browser === true;
19
+ let browserCapabilities = [];
20
+ function scrubBrowserCapabilities(value) {
21
+ let serialized = JSON.stringify(value).replace(/\b(?:wss?|https?):\/\/[^\s"'<>\\]+/g, url => {
22
+ try { return new URL(url).pathname.startsWith('/devtools/') ? '[browser transport omitted]' : url; }
23
+ catch { return url; }
24
+ });
25
+ for (const capability of browserCapabilities) serialized = serialized.replaceAll(capability, '[browser capability omitted]');
26
+ return JSON.parse(serialized);
27
+ }
28
+ const unsafe = new Set(['exec', 'process', 'code_execution', 'bash', 'terminal', 'read', 'write', 'edit', 'apply_patch', 'gateway']);
29
+ const browserActions = new Set(['profiles', 'importprofile', 'start', 'stop', 'doctor', 'evaluate']);
30
+
31
+ // Run before the app gate; never trust a caller's profile, target or nested routing.
32
+ function browserParams(params, member) {
33
+ if (!params || typeof params !== 'object' || Array.isArray(params)) throw new Error('browser action refused');
34
+ if (browserActions.has(String(params.action).toLowerCase()) || browserActions.has(String(params.kind).toLowerCase()))
35
+ throw new Error('browser action refused');
36
+ const out = { ...params };
37
+ // Stock merges hook params over the original: omission would retain a hostile node selection.
38
+ out.node = undefined;
39
+ out.profile = `byokit-${member}`;
40
+ out.target = 'host';
41
+ if (out.request !== undefined) out.request = browserParams(out.request, member);
42
+ if (out.actions !== undefined) {
43
+ if (!Array.isArray(out.actions)) throw new Error('browser action refused');
44
+ out.actions = out.actions.map(action => browserParams(action, member));
45
+ }
46
+ return out;
47
+ }
17
48
 
18
49
  /** Minimal JSON Schema -> TypeBox-marked schema converter; output is both valid TypeBox and valid JSON Schema. */
19
50
  function toTypeBox(schema) {
@@ -115,7 +146,25 @@ export default {
115
146
  jsonSchema: { type: 'object', additionalProperties: false, properties: {} },
116
147
  },
117
148
  register(api) {
118
- if (api.registerGatewayMethod) registerKeys(api);
149
+ if (api.registerGatewayMethod) { registerKeys(api); registerUsage(api); }
150
+ if (BROWSER) {
151
+ if (typeof api.registerAgentToolResultMiddleware !== 'function') throw new Error('browser result protection unavailable');
152
+ api.registerAgentToolResultMiddleware(async (event, ctx) => {
153
+ const stopped = { result: { content: [{ type: 'text', text: 'Browser session unavailable.' }],
154
+ details: { status: 'error' }, terminate: true } };
155
+ try {
156
+ const admission = await bridgeRequest({ kind: 'before-agent-run', key: ctx.sessionKey, runId: ctx.runId });
157
+ if (admission.allow !== true) return stopped;
158
+ const reply = await bridgeRequest({ kind: 'browser-capabilities' });
159
+ if (!Array.isArray(reply.capabilities) || reply.capabilities.some(value => typeof value !== 'string' || !value)) return stopped;
160
+ browserCapabilities = [...reply.capabilities].sort((a, b) => b.length - a.length);
161
+ return { result: scrubBrowserCapabilities(event.result) };
162
+ } catch { return stopped; }
163
+ }, { runtimes: ['openclaw', 'codex'] });
164
+ // Transcript-only safety net; live provider protection comes from the awaited middleware above.
165
+ api.on('tool_result_persist', event => ({ message: scrubBrowserCapabilities(event.message) }));
166
+ api.on('before_message_write', event => ({ message: scrubBrowserCapabilities(event.message) }));
167
+ }
119
168
  for (const spec of table.tools) {
120
169
  api.registerTool({
121
170
  name: spec.name,
@@ -151,16 +200,40 @@ export default {
151
200
  },
152
201
  });
153
202
  }
203
+ if (BROWSER) api.on('before_agent_run', async (_event, ctx) => {
204
+ // A kit-owned refusal seam, NOT an enable flag or an automatic-redispatch qualification.
205
+ const key = ctx?.sessionKey;
206
+ if (typeof key !== 'string' || !key) return { outcome: 'block', reason: 'run unavailable', message: 'run unavailable' };
207
+ try {
208
+ const reply = await bridgeRequest({ kind: 'before-agent-run', key, runId: ctx?.runId },
209
+ { timeoutMs: 10_000, signal: ctx?.abortSignal });
210
+ if (reply?.allow === true) return undefined;
211
+ } catch { /* missing host or timeout refuses before submission */ }
212
+ return { outcome: 'block', reason: 'run unavailable', message: 'run unavailable' };
213
+ });
154
214
  api.on('before_tool_call', async (event, ctx) => {
155
215
  // Engine builtins (web_fetch, memory, ...) are gated too unless the app opted out; they run in-engine and
156
216
  // never call back, so an allow passes them through unchanged.
157
217
  const builtin = !TOOLS.has(event.toolName);
158
- if (builtin && !GATE_BUILTINS) return undefined;
218
+ if (builtin && !GATE_BUILTINS && !BROWSER) return undefined;
159
219
  const key = ctx?.sessionKey;
160
220
  if (typeof key !== 'string' || !key) return { block: true, blockReason: "can't check this action right now" };
221
+ let params = event.params ?? {};
222
+ if (BROWSER) {
223
+ if (unsafe.has(event.toolName) || (builtin && event.toolName !== 'browser'))
224
+ return { block: true, blockReason: 'browser tool policy refused' };
225
+ if (event.toolName === 'browser') {
226
+ // Account agents retain their member identity. A delegate has its own agent id, never the parent's browser.
227
+ const agent = ctx?.agentId ?? /^agent:([^:]+):/.exec(key)?.[1];
228
+ const member = typeof agent === 'string' ? agent.replace(/^byokit-key-/, '') : '';
229
+ if (!/^[a-z][a-z0-9-]{0,31}$/.test(member)) return { block: true, blockReason: 'browser member unavailable' };
230
+ try { params = browserParams(params, member); }
231
+ catch { return { block: true, blockReason: 'browser action refused' }; }
232
+ }
233
+ }
161
234
  let decision;
162
235
  try {
163
- decision = await gate(key, event.toolName, event.params ?? {}, ctx?.abortSignal);
236
+ decision = await gate(key, event.toolName, params, ctx?.abortSignal);
164
237
  } catch (error) {
165
238
  return { block: true, blockReason: error instanceof Error ? error.message : "can't check this action right now" };
166
239
  }
@@ -170,12 +243,12 @@ export default {
170
243
  blockReason: typeof decision.reason === 'string' ? decision.reason : "can't check this action right now",
171
244
  };
172
245
  }
173
- if (builtin) return undefined;
246
+ if (builtin) return BROWSER && event.toolName === 'browser' ? { params } : undefined;
174
247
  // The bridge mints a permit for permitted tools and admits the rest ticket-side; either way the run key
175
248
  // rides along and execute sends back only what the gate gave it (N1).
176
249
  return {
177
250
  params: {
178
- ...(event.params ?? {}),
251
+ ...params,
179
252
  [RUN_PARAM]: key,
180
253
  ...(typeof decision.permit === 'string' ? { [PERMIT_PARAM]: decision.permit } : {}),
181
254
  },
@@ -0,0 +1,125 @@
1
+ // Gateway-only accounting. Reads requested UTC months, never transcript content or credentials.
2
+ import { readFileSync, openSync, writeSync, fsyncSync, closeSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const object = v => !!v && typeof v === 'object' && !Array.isArray(v);
5
+ const count = v => Number.isSafeInteger(v) && v >= 0;
6
+ export const USAGE_SYMBOL = Symbol.for('byokit.engine-usage.v1');
7
+ export function readEngineStarted(dir, live, params, read = readFileSync) {
8
+ const { agentId, startMs, endMs } = params ?? {};
9
+ if (typeof agentId !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(agentId) || !count(startMs) || !count(endMs) || startMs > endMs ||
10
+ endMs > 8.64e15) throw new Error('Invalid usage window');
11
+ const result = { state: 'unavailable', agentId, startMs, endMs, facts: [], unreadableLines: 0, complete: false };
12
+ if (!dir || !live || typeof live.bootId !== 'string' || !object(live.months) || !object(live.inFlight)) return result;
13
+ const lines = (path, absentOK) => {
14
+ try {
15
+ const text = read(path, 'utf8');
16
+ const rows = [];
17
+ for (const line of text.split('\n')) {
18
+ if (!line) continue;
19
+ try { const row = JSON.parse(line); if (!object(row)) throw new Error(); rows.push(row); }
20
+ catch { result.unreadableLines++; }
21
+ }
22
+ // A torn final write is not a valid durable line, even when its JSON happens to parse.
23
+ if (text && !text.endsWith('\n')) result.unreadableLines++;
24
+ return rows;
25
+ } catch (error) { if (!absentOK || error.code !== 'ENOENT') result.unreadableLines++; return []; }
26
+ };
27
+ const boots = new Map();
28
+ for (const row of lines(join(dir, 'boots.jsonl'), false)) {
29
+ if (typeof row.bootId !== 'string' || (row.startedAt === undefined && row.stoppedAt === undefined && row.spawned !== false) ||
30
+ (row.startedAt !== undefined && !count(row.startedAt)) || (row.stoppedAt !== undefined && (!count(row.stoppedAt) || !object(row.months))) ||
31
+ (row.spawned === false && !count(row.failedAt))) {
32
+ result.unreadableLines++; continue;
33
+ }
34
+ const boot = boots.get(row.bootId) ?? {};
35
+ if (row.startedAt !== undefined) { if (boot.startedAt !== undefined && boot.startedAt !== row.startedAt) result.unreadableLines++; boot.startedAt = row.startedAt; }
36
+ if (row.stoppedAt !== undefined) { if (boot.stoppedAt !== undefined) result.unreadableLines++; boot.stoppedAt = row.stoppedAt; boot.months = row.months; }
37
+ if (row.spawned === false) { boot.stoppedAt = row.failedAt; boot.months = {}; }
38
+ boots.set(row.bootId, boot);
39
+ }
40
+ const starts = [...boots.values()].map(b => b.startedAt).filter(count).sort((a, b) => a - b);
41
+ if (!starts.length) return result;
42
+ result.coverageSince = starts[0]; result.liveBootId = live.bootId; result.state = 'available';
43
+ let complete = startMs >= starts[0] && count(boots.get(live.bootId)?.startedAt);
44
+ const sequences = new Map(), seen = new Map(), sequenceFacts = new Map();
45
+ const months = [];
46
+ const cursor = new Date(startMs); cursor.setUTCDate(1); cursor.setUTCHours(0, 0, 0, 0);
47
+ while (cursor.getTime() <= endMs) {
48
+ const month = cursor.toISOString().slice(0, 7); months.push(month);
49
+ for (const row of lines(join(dir, `engine-started-${month}.jsonl`), true)) {
50
+ if (row.v !== 1 || !['started', 'ended'].includes(row.phase) || typeof row.bootId !== 'string' || !count(row.seq) || row.seq < 1 ||
51
+ !count(row.at) || row.at > 8.64e15 || !count(row.startedAt) || row.startedAt > row.at || new Date(row.at).toISOString().slice(0, 7) !== month ||
52
+ typeof row.chargeId !== 'string' || typeof row.agentId !== 'string' || row.kind !== 'workshop-review' ||
53
+ typeof row.provider !== 'string' || typeof row.model !== 'string' || !boots.has(row.bootId)) { result.unreadableLines++; continue; }
54
+ const key = `${row.bootId}\0${month}`, seq = sequences.get(key) ?? new Set(); seq.add(row.seq); sequences.set(key, seq);
55
+ const phase = `${row.chargeId}\0${row.phase}`;
56
+ const sequenceKey = `${key}\0${row.seq}`;
57
+ if (sequenceFacts.has(sequenceKey) && sequenceFacts.get(sequenceKey) !== phase) result.unreadableLines++;
58
+ sequenceFacts.set(sequenceKey, phase);
59
+ const safe = { v: 1, phase: row.phase, bootId: row.bootId, seq: row.seq, at: row.at, startedAt: row.startedAt,
60
+ chargeId: row.chargeId, agentId: row.agentId, kind: row.kind, provider: row.provider, model: row.model };
61
+ if (typeof row.authProfileId === 'string') safe.authProfileId = row.authProfileId;
62
+ if (['nothing', 'proposed', 'applied', 'failed'].includes(row.outcome)) safe.outcome = row.outcome;
63
+ else if (row.phase === 'ended') { result.unreadableLines++; continue; }
64
+ if (object(row.usage)) {
65
+ safe.usage = {};
66
+ for (const field of ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoningTokens', 'total'])
67
+ if (typeof row.usage[field] === 'number' && Number.isFinite(row.usage[field]) && row.usage[field] >= 0) safe.usage[field] = row.usage[field];
68
+ }
69
+ if (object(row.origin) && typeof row.origin.sessionKey === 'string') safe.origin = { sessionKey: row.origin.sessionKey,
70
+ ...(typeof row.origin.runId === 'string' ? { runId: row.origin.runId } : {}) };
71
+ if (seen.has(phase) && JSON.stringify(seen.get(phase)) !== JSON.stringify(safe)) result.unreadableLines++;
72
+ seen.set(phase, safe);
73
+ }
74
+ cursor.setUTCMonth(cursor.getUTCMonth() + 1);
75
+ }
76
+ for (const fact of seen.values()) {
77
+ const start = fact.phase === 'ended' ? seen.get(`${fact.chargeId}\0started`) : undefined;
78
+ if (start && ['agentId', 'bootId', 'kind', 'provider', 'model', 'authProfileId', 'startedAt'].some(k => start[k] !== fact[k]))
79
+ result.unreadableLines++;
80
+ }
81
+ for (const [id, boot] of boots) {
82
+ if (!count(boot.startedAt) || (boot.stoppedAt !== undefined && boot.stoppedAt < boot.startedAt)) { complete = false; continue; }
83
+ if (boot.startedAt > endMs || (boot.stoppedAt !== undefined && boot.stoppedAt < startMs)) continue;
84
+ const counters = id === live.bootId ? live.months : boot.months;
85
+ if (!object(counters) || (id !== live.bootId && boot.stoppedAt === undefined)) { complete = false; continue; }
86
+ for (const month of months) {
87
+ const counter = counters[month];
88
+ const seq = sequences.get(`${id}\0${month}`) ?? new Set();
89
+ if (counter === undefined) { if (seq.size) complete = false; continue; }
90
+ if (!object(counter) || !count(counter.lastSeq) || !count(counter.failed) || counter.failed !== 0 || seq.size !== counter.lastSeq ||
91
+ [...seq].some(n => n < 1 || n > counter.lastSeq)) complete = false;
92
+ }
93
+ }
94
+ for (const fact of Object.values(live.inFlight)) if (!object(fact) || !count(fact.startedAt) || fact.startedAt <= endMs) complete = false;
95
+ result.facts = [...seen.values()].filter(f => f.agentId === agentId);
96
+ result.live = { bootId: live.bootId, months: Object.fromEntries(months.filter(m => object(live.months[m])).map(m =>
97
+ [m, { lastSeq: live.months[m].lastSeq, failed: live.months[m].failed }])),
98
+ inFlight: Object.fromEntries(Object.entries(live.inFlight).filter(([, f]) => object(f) && count(f.startedAt)).map(([id, f]) => [id, { startedAt: f.startedAt }])) };
99
+ result.complete = complete && result.unreadableLines === 0;
100
+ return result;
101
+ }
102
+ export function registerUsage(api) {
103
+ const dir = process.env.BYOKIT_ENGINE_USAGE_LEDGER, bootId = process.env.BYOKIT_ENGINE_BOOT;
104
+ if (dir && bootId) {
105
+ const live = globalThis[USAGE_SYMBOL] ??= { bootId, months: {}, inFlight: {} };
106
+ let clean = false;
107
+ api.on('gateway_stop', () => { clean = true; });
108
+ // Snapshot at process exit: no async review can append after these counters. SIGKILL cannot forge a clean stop.
109
+ process.once('exit', code => {
110
+ if (!clean || code !== 0 || Object.keys(live.inFlight).length) return;
111
+ let fd;
112
+ try {
113
+ fd = openSync(join(dir, 'boots.jsonl'), 'a', 0o600);
114
+ const line = Buffer.from(JSON.stringify({ bootId, stoppedAt: Date.now(), months: live.months }) + '\n');
115
+ if (writeSync(fd, line) !== line.length) return;
116
+ fsyncSync(fd);
117
+ } catch { /* No stop proof: permanently incomplete, never fail a review or shutdown. */ }
118
+ finally { if (fd !== undefined) { try { closeSync(fd); } catch { /* no further writes at exit */ } } }
119
+ });
120
+ }
121
+ api.registerGatewayMethod('byokit.usage.engineStarted', ({ params, respond }) => {
122
+ try { respond(true, readEngineStarted(process.env.BYOKIT_ENGINE_USAGE_LEDGER, globalThis[USAGE_SYMBOL], params)); }
123
+ catch { respond(false, undefined, { code: 'INVALID_REQUEST', message: 'Invalid usage window' }); }
124
+ }, { scope: 'operator.read' });
125
+ }