@byokit/openclaw 0.1.0

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 +47 -0
  2. package/LICENSE +202 -0
  3. package/README.md +174 -0
  4. package/dist/approvals.d.ts +29 -0
  5. package/dist/approvals.js +200 -0
  6. package/dist/bridge.d.ts +78 -0
  7. package/dist/bridge.js +345 -0
  8. package/dist/classify.d.ts +4 -0
  9. package/dist/classify.js +14 -0
  10. package/dist/config.d.ts +13 -0
  11. package/dist/config.js +74 -0
  12. package/dist/constants.d.ts +3 -0
  13. package/dist/constants.js +13 -0
  14. package/dist/device.d.ts +63 -0
  15. package/dist/device.js +192 -0
  16. package/dist/engine.d.ts +43 -0
  17. package/dist/engine.js +212 -0
  18. package/dist/generated/events.d.ts +61 -0
  19. package/dist/generated/events.js +1 -0
  20. package/dist/generated/methods.d.ts +2368 -0
  21. package/dist/generated/methods.js +1 -0
  22. package/dist/generated/params.d.ts +1717 -0
  23. package/dist/generated/params.js +5 -0
  24. package/dist/generated/report.json +1356 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +5 -0
  27. package/dist/kit.d.ts +108 -0
  28. package/dist/kit.js +655 -0
  29. package/dist/link.d.ts +24 -0
  30. package/dist/link.js +273 -0
  31. package/dist/members.d.ts +11 -0
  32. package/dist/members.js +23 -0
  33. package/dist/migrate.d.ts +22 -0
  34. package/dist/migrate.js +118 -0
  35. package/dist/notices.d.ts +9 -0
  36. package/dist/notices.js +75 -0
  37. package/dist/routes.d.ts +9 -0
  38. package/dist/routes.js +15 -0
  39. package/dist/routes.json +398 -0
  40. package/dist/runs.d.ts +14 -0
  41. package/dist/runs.js +110 -0
  42. package/dist/signin.d.ts +24 -0
  43. package/dist/signin.js +251 -0
  44. package/dist/testing/contract.d.ts +12 -0
  45. package/dist/testing/contract.js +250 -0
  46. package/dist/testing/fake-gateway.d.ts +19 -0
  47. package/dist/testing/fake-gateway.js +378 -0
  48. package/dist/testing/index.d.ts +3 -0
  49. package/dist/testing/index.js +4 -0
  50. package/dist/testing/model-stub.d.ts +29 -0
  51. package/dist/testing/model-stub.js +182 -0
  52. package/dist/transport.d.ts +22 -0
  53. package/dist/transport.js +94 -0
  54. package/dist/types.d.ts +125 -0
  55. package/dist/types.js +2 -0
  56. package/dist/words.d.ts +12 -0
  57. package/dist/words.js +26 -0
  58. package/dist/words.json +20 -0
  59. package/engine/package-lock.json +4680 -0
  60. package/engine/package.json +1 -0
  61. package/package.json +68 -0
  62. package/plugin/index.js +183 -0
  63. package/plugin/package.json +1 -0
  64. package/policy/policy.mjs +197 -0
@@ -0,0 +1,94 @@
1
+ import { createHash, createPrivateKey, createPublicKey, sign } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { GatewayClient } from '@openclaw/gateway-client';
4
+ import { OPERATOR_SCOPES, PROTOCOL_VERSION } from "./constants.js";
5
+ /**
6
+ * Client caps for the operator connection. `approvals` makes this connection an approval client, so the
7
+ * engine delivers exec/plugin.approval.requested to it (B7); without it the broadcast set is empty.
8
+ */
9
+ export const GATEWAY_CAPS = ['tool-events', 'approvals'];
10
+ /**
11
+ * Read the device key pair from `device.json`. The kit writes `{ privateKey, publicKey }` (PEM strings);
12
+ * existing Crewhouse state carries `{ deviceId, publicKeyPem, privateKeyPem }` instead. Both shapes are
13
+ * accepted and the file is only ever read, never rewritten, so an existing device identity keeps its keys.
14
+ */
15
+ export function loadDeviceKeys(identityPath) {
16
+ const raw = JSON.parse(readFileSync(identityPath, 'utf8'));
17
+ const privateKeyPem = typeof raw.privateKey === 'string' ? raw.privateKey
18
+ : typeof raw.privateKeyPem === 'string' ? raw.privateKeyPem
19
+ : undefined;
20
+ const publicKeyPem = typeof raw.publicKey === 'string' ? raw.publicKey
21
+ : typeof raw.publicKeyPem === 'string' ? raw.publicKeyPem
22
+ : undefined;
23
+ if (privateKeyPem === undefined || publicKeyPem === undefined) {
24
+ throw new Error(`The device identity file at ${identityPath} is not usable: it holds no device key pair.`);
25
+ }
26
+ // A paired file names its own deviceId (O11 fan-out): it wins so the paired identity survives re-derivation.
27
+ const deviceId = typeof raw.deviceId === 'string' && raw.deviceId ? raw.deviceId : undefined;
28
+ return { privateKeyPem, publicKeyPem, ...(deviceId ? { deviceId } : {}) };
29
+ }
30
+ export function gatewayTransport(ctx) {
31
+ const pem = loadDeviceKeys(ctx.identityPath);
32
+ const publicKey = createPublicKey(pem.publicKeyPem).export({ format: 'der', type: 'spki' }).subarray(-32);
33
+ const events = new Set();
34
+ const closes = new Set();
35
+ let ready;
36
+ let pending;
37
+ let stopped = false;
38
+ // The last transient connect failure, kept for the terminal error. The client retries boot-time
39
+ // ECONNREFUSED itself with backoff (O11: the first kit.start() lands while the gateway still binds),
40
+ // so connect errors never settle the handshake; only a paused reconnect (terminal: auth, protocol)
41
+ // or the kit's own deadline does.
42
+ let lastConnectError;
43
+ const client = new GatewayClient({
44
+ url: `ws://127.0.0.1:${ctx.port}`, token: ctx.token, role: 'operator', scopes: [...OPERATOR_SCOPES],
45
+ clientName: 'cli', caps: [...GATEWAY_CAPS], minProtocol: PROTOCOL_VERSION, maxProtocol: PROTOCOL_VERSION,
46
+ deviceIdentity: { deviceId: pem.deviceId ?? createHash('sha256').update(publicKey).digest('hex'),
47
+ privateKeyPem: pem.privateKeyPem, publicKeyPem: pem.publicKeyPem },
48
+ hostDeps: {
49
+ signDevicePayload: (key, payload) => sign(null, Buffer.from(payload), createPrivateKey(key)).toString('base64url'),
50
+ publicKeyRawBase64UrlFromPem: (key) => createPublicKey(key).export({ format: 'der', type: 'spki' }).subarray(-32).toString('base64url'),
51
+ loadDeviceAuthToken: () => null, storeDeviceAuthToken: () => { }, clearDeviceAuthToken: () => { },
52
+ },
53
+ onHelloOk: (h) => {
54
+ if (stopped)
55
+ return;
56
+ ready = { protocol: h.protocol, server: { version: h.server.version }, methods: h.features.methods, events: h.features.events };
57
+ pending?.resolve(ready);
58
+ pending = undefined;
59
+ },
60
+ onEvent: (e) => { for (const fn of events)
61
+ fn({ event: e.event, payload: e.payload }); },
62
+ onConnectError: (e) => { lastConnectError = e; },
63
+ onReconnectPaused: (info) => {
64
+ const terminal = new Error(`gateway connect paused: ${info.code} ${info.reason ?? ''}${lastConnectError ? ` (last: ${lastConnectError.message})` : ''}`);
65
+ pending?.reject(terminal);
66
+ pending = undefined;
67
+ },
68
+ onClose: (code, reason) => {
69
+ ready = undefined;
70
+ // A pre-hello close while the client still retries is transient (backoff continues inside the
71
+ // client); only a stopped transport settles the handshake here. Terminal give-ups arrive paused.
72
+ if (pending && stopped) {
73
+ pending.reject(new Error(`gateway closed: ${code} ${reason}`));
74
+ pending = undefined;
75
+ }
76
+ else if (!pending && !stopped)
77
+ for (const fn of closes)
78
+ fn(reason || String(code));
79
+ },
80
+ });
81
+ return {
82
+ start: () => {
83
+ if (ready)
84
+ return Promise.resolve(ready);
85
+ if (pending)
86
+ return Promise.reject(new Error('gateway handshake already pending'));
87
+ return new Promise((resolve, reject) => { pending = { resolve, reject }; stopped = false; client.start(); });
88
+ },
89
+ request: (method, params, o) => client.request(method, params, o),
90
+ onEvent: (fn) => { events.add(fn); return () => { events.delete(fn); }; },
91
+ onClose: (fn) => { closes.add(fn); return () => { closes.delete(fn); }; },
92
+ stop: async () => { stopped = true; pending?.reject(new Error('gateway stopped')); pending = undefined; ready = undefined; await client.stopAndWait(); },
93
+ };
94
+ }
@@ -0,0 +1,125 @@
1
+ export type Member = string;
2
+ export interface ToolSpec {
3
+ name: string;
4
+ description: string;
5
+ parameters: object;
6
+ }
7
+ export interface RunRef {
8
+ sessionKey: string;
9
+ member: Member;
10
+ meta?: unknown;
11
+ }
12
+ export type GateResult = {
13
+ allow: true;
14
+ } | {
15
+ allow: false;
16
+ reason: string;
17
+ } | {
18
+ ask: {
19
+ summary: string;
20
+ };
21
+ };
22
+ export interface ToolHost {
23
+ gate(run: RunRef, tool: string, input: Record<string, unknown>, info: {
24
+ builtin: boolean;
25
+ }): Promise<GateResult>;
26
+ call(run: RunRef, tool: string, input: Record<string, unknown>, signal: AbortSignal): Promise<string>;
27
+ }
28
+ export interface RunSpec extends RunRef {
29
+ message: string;
30
+ system?: string;
31
+ images?: {
32
+ data: string;
33
+ mimeType: string;
34
+ }[];
35
+ thinking?: 'off' | 'low' | 'medium' | 'high';
36
+ model?: string;
37
+ register?: boolean;
38
+ }
39
+ export type RunEvent = {
40
+ type: 'text';
41
+ text: string;
42
+ } | {
43
+ type: 'tool';
44
+ name: string;
45
+ phase: 'start' | 'end';
46
+ };
47
+ export type RunEnd = {
48
+ ok: true;
49
+ text: string;
50
+ } | {
51
+ ok: false;
52
+ aborted: true;
53
+ } | {
54
+ ok: false;
55
+ kind: 'signed-out' | 'resting' | 'plan' | 'network' | 'other';
56
+ until?: number;
57
+ message: string;
58
+ };
59
+ export type SignInView = {
60
+ state: 'waiting' | 'done' | 'failed';
61
+ via: 'browser' | 'code';
62
+ url?: string;
63
+ code?: string;
64
+ error?: string;
65
+ why?: 'busy' | 'declined' | 'expired' | 'failed';
66
+ };
67
+ export type Approval = {
68
+ id: string;
69
+ source: 'gate' | 'exec' | 'plugin' | 'question';
70
+ member: Member;
71
+ sessionKey?: string;
72
+ tool?: string;
73
+ summary: string;
74
+ input?: unknown;
75
+ at: number;
76
+ expires: number;
77
+ };
78
+ export type Decision = {
79
+ allow: boolean;
80
+ reason?: string;
81
+ answer?: unknown;
82
+ };
83
+ export type KitState = {
84
+ phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update';
85
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version';
86
+ retryAt?: number;
87
+ };
88
+ export type Hello = {
89
+ protocol: number;
90
+ server: {
91
+ version: string;
92
+ };
93
+ methods: string[];
94
+ events: string[];
95
+ };
96
+ export type Route = {
97
+ choice: string;
98
+ provider: string;
99
+ plugin: string;
100
+ billing: 'subscription' | 'api' | 'local';
101
+ via: 'browser' | 'code';
102
+ prerequisite: string | null;
103
+ offer: boolean;
104
+ reason: string;
105
+ source: string;
106
+ };
107
+ export interface GatewayTransport {
108
+ start(): Promise<Hello>;
109
+ request(method: string, params?: unknown, o?: {
110
+ timeoutMs?: number;
111
+ signal?: AbortSignal;
112
+ }): Promise<unknown>;
113
+ onEvent(fn: (e: {
114
+ event: string;
115
+ payload?: unknown;
116
+ }) => void): () => void;
117
+ onClose(fn: (why: string) => void): () => void;
118
+ stop(): Promise<void>;
119
+ }
120
+ export type CallOptions = {
121
+ timeoutMs?: number;
122
+ signal?: AbortSignal;
123
+ };
124
+ export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
125
+ export type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Public types for the OpenClaw runtime kit (docs/runtime-kits.md 5.2). Types only; behavior lives in the modules.
2
+ export {};
@@ -0,0 +1,12 @@
1
+ import wordsJson from './words.json' with { type: 'json' };
2
+ import type { KitState, SignInView } from './types.ts';
3
+ /** ui-core's AccountView shape, declared locally so this module stays dependency-free (5.14). */
4
+ export type AccountView = {
5
+ ready?: boolean;
6
+ work?: string | boolean;
7
+ signIn?: SignInView | null;
8
+ };
9
+ export type WordKey = Extract<keyof typeof wordsJson, string>;
10
+ export declare function words(key: WordKey, vars?: Record<string, string>): string;
11
+ export declare function stateWords(s: KitState): string;
12
+ export declare function toAccountView(view: SignInView | null, ready: boolean): AccountView;
package/dist/words.js ADDED
@@ -0,0 +1,26 @@
1
+ // Every sentence a person can see, in one file other languages can read too (5.14). Plain words only (4.3).
2
+ import wordsJson from './words.json' with { type: 'json' };
3
+ export function words(key, vars) {
4
+ let sentence = wordsJson[key];
5
+ if (vars)
6
+ for (const [name, value] of Object.entries(vars))
7
+ sentence = sentence.replaceAll(`{${name}}`, value);
8
+ return sentence;
9
+ }
10
+ const PHASE_WORDS = {
11
+ installing: 'engine.installing',
12
+ starting: 'engine.starting',
13
+ repairing: 'engine.repairing',
14
+ ready: 'engine.ready',
15
+ restarting: 'engine.restarting',
16
+ failed: 'engine.failed',
17
+ 'needs-update': 'engine.needsUpdate',
18
+ };
19
+ export function stateWords(s) {
20
+ // 'stopped' is never on screen (4.3 covers what a person can see), so 5.14 has no row for it.
21
+ return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
22
+ }
23
+ export function toAccountView(view, ready) {
24
+ // `why` maps 1:1; ui-core's phaseOf reads busy/declined/expired itself (5.14).
25
+ return { ready, signIn: view };
26
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "engine.installing": "Getting things ready on this computer. The first time takes a few minutes.",
3
+ "engine.starting": "Starting up…",
4
+ "engine.repairing": "Fixing a small problem with the setup. This takes a moment.",
5
+ "engine.ready": "Ready.",
6
+ "engine.restarting": "Something stopped. Starting it again by itself.",
7
+ "engine.failed": "This computer couldn't start the helper. Restart the app to try again.",
8
+ "engine.needsUpdate": "This app needs an update to keep working.",
9
+ "member.signedOut": "Sign in with {name} to start.",
10
+ "member.resting": "{name} needs a break until {time}.",
11
+ "member.plan": "Your {name} plan doesn't include this.",
12
+ "member.network": "Can't reach {name} right now. This keeps trying by itself.",
13
+ "signin.returned": "Thanks. Finishing the sign-in — you can go back to the app now.",
14
+ "signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
15
+ "signin.expired": "The sign-in took too long. Start it again.",
16
+ "approval.ask": "{helper} wants to {summary}. Allow it?",
17
+ "approval.expired": "Nobody answered in time, so this wasn't allowed.",
18
+ "approval.notice": "Something is waiting for your yes.",
19
+ "link.notAllowed": "This device can't do that. Ask the person at the computer."
20
+ }