@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
|
@@ -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.
|
|
3
|
+
"version": "0.6.1",
|
|
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.
|
|
64
|
+
"@byokit/relay": "0.5.2",
|
|
57
65
|
"@byokit/reach": "0.6.0",
|
|
58
|
-
"@byokit/seal": "0.
|
|
66
|
+
"@byokit/seal": "0.3.0",
|
|
59
67
|
"@byokit/secrets": "0.5.0",
|
|
60
|
-
"@openclaw/gateway-client": "2026.
|
|
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,
|
|
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
|
-
...
|
|
251
|
+
...params,
|
|
179
252
|
[RUN_PARAM]: key,
|
|
180
253
|
...(typeof decision.permit === 'string' ? { [PERMIT_PARAM]: decision.permit } : {}),
|
|
181
254
|
},
|
package/plugin/usage.js
ADDED
|
@@ -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
|
+
}
|