openclaw-brokerkit 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # OpenClaw BrokerKit
2
2
 
3
- `openclaw-brokerkit` adds a provider-neutral Approvals tab and `/brokerkit`
4
- commands to OpenClaw. It connects to one or more BrokerKit Operator V1 sources.
5
- The brokers remain authoritative and retain all provider credentials and
6
- executable plans.
3
+ `openclaw-brokerkit` is an OpenClaw plugin that adds a provider-neutral
4
+ Approvals tab and `/brokerkit` commands to OpenClaw. It connects to one or
5
+ more BrokerKit Operator V1 sources; the brokers remain authoritative and
6
+ retain all provider credentials and executable plans.
7
7
 
8
- The minimum supported host is `openclaw@2026.7.1-beta.5`, the first published
8
+ The minimum supported host is `openclaw@2026.7.1`, the first stable
9
9
  SDK used by this package that includes public tab descriptors.
10
10
 
11
11
  ## Install
12
12
 
13
- After the package is published, install an exact release from
13
+ Install an exact release from
14
14
  [npm](https://www.npmjs.com/package/openclaw-brokerkit) through OpenClaw:
15
15
 
16
16
  ```sh
@@ -24,15 +24,16 @@ pnpm --filter openclaw-brokerkit build
24
24
  openclaw plugins install --link ./plugins/openclaw
25
25
  ```
26
26
 
27
- Choose one explicit trust mode:
27
+ ## Choose a trust mode
28
28
 
29
- - `direct` trusts the OpenClaw process with operator SecretRefs and enables the
30
- tab, background reconciliation, `/brokerkit` commands, and channel delivery.
31
- - `delegated-web` packages only the tab UI and delegates authenticated browser
32
- requests to a same-origin trusted backend. It gives OpenClaw no broker
33
- credential and registers no command or background source service.
29
+ - `direct` trusts the OpenClaw process with operator SecretRefs and enables
30
+ the tab, background reconciliation, `/brokerkit` commands, and channel
31
+ delivery.
32
+ - `delegated-web` packages only the tab UI and delegates authenticated
33
+ browser requests to a same-origin trusted backend. It gives OpenClaw no
34
+ broker credential and registers no command or background source service.
34
35
 
35
- ## Configuration
36
+ ## Direct mode configuration
36
37
 
37
38
  ```json5
38
39
  {
@@ -46,7 +47,7 @@ Choose one explicit trust mode:
46
47
  {
47
48
  id: "hf-primary",
48
49
  label: "Hugging Face",
49
- endpoint: "http://127.0.0.1:18080",
50
+ endpoint: "https://hf-broker.example.com",
50
51
  operatorCredential: {
51
52
  source: "env",
52
53
  provider: "default",
@@ -61,12 +62,34 @@ Choose one explicit trust mode:
61
62
  }
62
63
  ```
63
64
 
64
- Literal operator credentials are rejected. Non-loopback sources require HTTPS.
65
- Each `endpoint` is the broker's Operator V1 listener, not its agent listener.
66
- The plugin stores only cursors, opaque handles, generic channel destinations,
67
- and delivery bookkeeping under the OpenClaw service state directory.
65
+ Literal operator credentials are rejected; `operatorCredential.source` is
66
+ `env`, `file`, or `exec`. Non-loopback endpoints require HTTPS. Each
67
+ `endpoint` is the broker's Operator V1 listener, not its agent listener. The
68
+ plugin stores only cursors, opaque handles, generic channel destinations, and
69
+ delivery bookkeeping under the OpenClaw service state directory.
68
70
 
69
- For a deployment that treats OpenClaw as untrusted, configure delegated web:
71
+ ## Commands
72
+
73
+ ```text
74
+ /brokerkit subscribe
75
+ /brokerkit unsubscribe
76
+ /brokerkit pending
77
+ /brokerkit show <handle>
78
+ /brokerkit approve <handle>
79
+ /brokerkit deny <handle>
80
+ /brokerkit revoke <handle>
81
+ ```
82
+
83
+ Commands are registered only in direct mode. They require an authorized
84
+ sender and the `operator.approvals` scope. Requesters cancel their own
85
+ pending requests through the authenticated broker client API, not through
86
+ this operator surface. Subscriptions use OpenClaw's generic outbound adapter,
87
+ so this package contains no Telegram, Discord, Slack, or other
88
+ channel-specific implementation.
89
+
90
+ ## Delegated web mode
91
+
92
+ For a deployment that treats OpenClaw as untrusted:
70
93
 
71
94
  ```json5
72
95
  {
@@ -87,111 +110,123 @@ For a deployment that treats OpenClaw as untrusted, configure delegated web:
87
110
  ```
88
111
 
89
112
  The trusted backend implements `brokerkit.io/delegated-web/v1`, authenticates
90
- the human operator, and issues an in-memory decision token lasting no more than
91
- five minutes. The base path must be a normalized same-origin absolute path.
92
- The OpenClaw gateway must set `gateway.controlUi.embedSandbox` to `scripts` so
93
- the approval tab can run while retaining its opaque sandbox origin. Do not use
94
- `trusted`; delegated authentication is designed for the stricter scripts-only
95
- sandbox.
113
+ the human operator, and issues an in-memory decision token lasting no more
114
+ than five minutes. The base path must be a normalized same-origin absolute
115
+ path. The OpenClaw gateway must set `gateway.controlUi.embedSandbox` to
116
+ `scripts` so the approval tab can run while retaining its opaque sandbox
117
+ origin; do not use `trusted`.
96
118
 
97
119
  When OpenClaw is outside the credential trust boundary, the trusted backend
98
120
  must serve the packaged `dist/ui` files at the registered UI path itself. A
99
- framed popover may receive a server-enforced `read` or `decide` session. A
100
- `read` session shows requests without action controls. A `decide` session lets
101
- the operator act directly in the frame. Hosts that use framed decisions accept
102
- that the surrounding application controls the iframe's placement and can
103
- attempt UI redressing; use the navigation-only launcher below when that threat
104
- must remain out of scope.
121
+ framed popover may receive a server-enforced `read` or `decide` session: a
122
+ `read` session shows requests without action controls, and a `decide` session
123
+ lets the operator act directly in the frame. Hosts that use framed decisions
124
+ accept that the surrounding application controls the iframe's placement and
125
+ can attempt UI redressing; use the navigation-only launcher below when that
126
+ threat must remain out of scope.
105
127
 
106
- The host injects the short-lived delegated session into the sandboxed response:
128
+ ### Session injection
129
+
130
+ The host injects the short-lived delegated session into the sandboxed
131
+ response:
107
132
 
108
133
  ```html
109
134
  <meta name="brokerkit-delegated-session" content="BASE64URL_SESSION_JSON" />
110
135
  ```
111
136
 
112
- With `access: "read"`, each actionable request renders a **Review securely**
113
- button instead of decision controls. The button posts this navigation-only
114
- message:
115
-
116
- ```text
117
- { type: "brokerkit.delegated-web.open", version: 1, nonce }
118
- ```
119
-
120
- The host verifies the exact opaque-origin frame source and navigates the whole
121
- browser tab to the trusted UI URL. The top-level response is unframeable and
122
- contains a server-enforced decision session, so approval, denial, cancellation,
123
- and revocation require a second explicit action in the trusted document.
124
-
125
- A host that does not provide a framed inbox can inject this marker so the UI
126
- renders only the secure-navigation launcher:
127
-
128
- ```html
129
- <meta name="brokerkit-delegated-top-level" />
130
- ```
137
+ Token-bearing HTML responses must enforce `sandbox allow-scripts` in their
138
+ CSP, and immutable UI assets must be loadable by the opaque document (serve
139
+ them without a same-origin CORP restriction or set
140
+ `Cross-Origin-Resource-Policy: cross-origin`).
131
141
 
132
142
  The content is a base64url-encoded `brokerkit.io/delegated-web/v1` session
133
- object. The UI removes the element as soon as it reads it. Version 1 uses this
134
- closed payload:
143
+ object; the UI removes the element as soon as it reads it. Version 1 uses
144
+ this closed payload:
135
145
 
136
146
  ```json
137
147
  {
138
148
  "api_version": "brokerkit.io/delegated-web/v1",
139
- "token": "opaque-short-lived-bearer-token",
140
- "expires_at": "2026-07-12T13:30:00Z",
149
+ "token": "opaque-short-lived-browser-session",
150
+ "expires_at": "<no more than five minutes from issue time, RFC3339>",
141
151
  "access": "read",
142
152
  "renewal_transport": "direct"
143
153
  }
144
154
  ```
145
155
 
146
156
  `access` is `read` or `decide`; the host enforces it on every endpoint.
147
- `renewal_transport` is `direct` or `parent`. Direct renewal posts to
148
- `<basePath>/session` with the current token as a bearer credential and omits
149
- cookies. Parent renewal uses the nonce-bound bridge below. The host preserves
150
- the token's access when it returns a replacement session.
157
+ `renewal_transport` is `direct` (the UI posts the current session to
158
+ `<basePath>/session`) or `parent` (the nonce-bound bridge below returns the
159
+ replacement session in memory). The host preserves the token's access when it
160
+ returns a replacement session.
151
161
 
152
- When the parent application is trusted, a sandboxed tab that cannot call the
153
- delegated session endpoint directly may instead use this host-neutral bridge:
162
+ ### Browser session transport
154
163
 
155
- ```text
156
- request: { type: "brokerkit.delegated-web.session.request", version: 1, nonce }
157
- response: { type: "brokerkit.delegated-web.session.response", nonce, session }
164
+ Every direct and delegated-web browser API call carries its raw session in
165
+ one fixed HTTP field:
166
+
167
+ ```http
168
+ GET /trusted-host/api/brokerkit/snapshot HTTP/1.1
169
+ Origin: null
170
+ BrokerKit-Session: eyJ2ZXJzaW9uIjoxLCJhdWRpZW5jZSI6Ii4uLiJ9.signature
171
+ Accept: application/json
158
172
  ```
159
173
 
160
- The trusted parent must answer only requests from the embedded BrokerKit frame
161
- and must bind each response to the supplied 128-bit nonce. `session` is the
162
- same `brokerkit.io/delegated-web/v1` object returned by `POST
163
- <basePath>/session`. The bridge is a BrokerKit interface; it contains no
164
- host-product namespace or host-specific payload.
174
+ `BrokerKit-Session` has no authentication-scheme prefix. A host must reject
175
+ empty, repeated, combined, malformed, or oversized values; tokens are 32 to
176
+ 4096 bytes of visible ASCII excluding comma and whitespace. The same field
177
+ protects `POST /session`, `GET /snapshot`, `GET /events`, request detail,
178
+ approval, denial, and revocation; direct-mode capabilities use it on
179
+ `/plugins/brokerkit/api/v1` as well.
180
+
181
+ Standard `Authorization` is reserved for the hosting edge and for
182
+ server-to-server Operator V1 clients. Browser clients never use it, and never
183
+ put a delegated session in cookies, query parameters, local storage, or
184
+ session storage. Tokens must not be logged, traced, reflected in errors,
185
+ copied into an Operator V1 request, or persisted by the host or UI.
186
+
187
+ Because the packaged UI has an opaque origin under the required scripts-only
188
+ sandbox, a delegated host must accept only the expected `Origin: null`,
189
+ return `Access-Control-Allow-Origin: null`, allow `BrokerKit-Session` and
190
+ `Content-Type` in preflight responses, allow only its fixed route methods,
191
+ and set `Vary: Origin` plus `Cache-Control: no-store`. It must not return
192
+ `Access-Control-Allow-Credentials: true`; delegated API requests deliberately
193
+ omit cookies.
194
+
195
+ ### Secure-navigation launcher
165
196
 
166
- ## Commands
197
+ With `access: "read"`, each actionable request renders a **Review securely**
198
+ button instead of decision controls. The button posts this navigation-only
199
+ message:
167
200
 
168
201
  ```text
169
- /brokerkit subscribe
170
- /brokerkit unsubscribe
171
- /brokerkit pending
172
- /brokerkit show <handle>
173
- /brokerkit approve <handle>
174
- /brokerkit deny <handle>
175
- /brokerkit cancel <handle>
176
- /brokerkit revoke <handle>
202
+ { type: "brokerkit.delegated-web.open", version: 1, nonce }
177
203
  ```
178
204
 
179
- Commands are registered only in direct mode. They require an authorized sender
180
- and the `operator.approvals` scope.
181
- Subscriptions use OpenClaw's generic outbound adapter, so this package contains
182
- no Telegram, Discord, Slack, or other channel-specific implementation.
205
+ The host verifies the exact opaque-origin frame source and navigates the
206
+ whole browser tab to the trusted UI URL. The top-level response is
207
+ unframeable and contains a server-enforced decision session, so approval,
208
+ denial, and revocation require a second explicit action in the trusted
209
+ document.
183
210
 
184
- ## Verify
211
+ A host that does not provide a framed inbox can inject this marker so the UI
212
+ renders only the secure-navigation launcher:
185
213
 
186
- ```sh
187
- pnpm --filter openclaw-brokerkit check
188
- pnpm --filter openclaw-brokerkit test
189
- pnpm --filter openclaw-brokerkit test:browser
190
- pnpm --filter openclaw-brokerkit build
191
- pnpm --filter openclaw-brokerkit test:package
214
+ ```html
215
+ <meta name="brokerkit-delegated-top-level" />
216
+ ```
217
+
218
+ ### Parent session bridge
219
+
220
+ When the parent application is trusted, a sandboxed tab that cannot call the
221
+ delegated session endpoint directly may instead use this host-neutral bridge:
222
+
223
+ ```text
224
+ request: { type: "brokerkit.delegated-web.session.request", version: 1, nonce }
225
+ response: { type: "brokerkit.delegated-web.session.response", nonce, session }
192
226
  ```
193
227
 
194
- The browser suite runs direct and delegated hosting in Chromium, Firefox, and
195
- WebKit at desktop and mobile sizes. The package test installs the tarball in a
196
- fresh directory beside the exact minimum OpenClaw release and imports the
197
- public plugin entry.
228
+ The trusted parent must answer only requests from the embedded BrokerKit
229
+ frame and must bind each response to the supplied 128-bit nonce. `session` is
230
+ the same `brokerkit.io/delegated-web/v1` object returned by
231
+ `POST <basePath>/session`. The bridge is a BrokerKit interface; it contains
232
+ no host-product namespace or host-specific payload.
@@ -0,0 +1,7 @@
1
+ export const BROWSER_SESSION_HEADER = "BrokerKit-Session";
2
+ export const MAX_BROWSER_SESSION_BYTES = 4096;
3
+ export function validBrowserSession(value) {
4
+ return (value.length >= 32 &&
5
+ value.length <= MAX_BROWSER_SESSION_BYTES &&
6
+ /^[\x21-\x2B\x2D-\x7E]+$/u.test(value));
7
+ }
@@ -36,8 +36,8 @@ export async function handleCommand(runtime, ctx) {
36
36
  return { text: "Invalid BrokerKit command." };
37
37
  const lines = runtime
38
38
  .snapshot()
39
- .requests.filter((request) => request.status === "pending")
40
- .map((request) => `${request.handle} · ${request.sourceLabel} · ${request.presentation.title}`);
39
+ .requests.filter((request) => request.request.status === "pending")
40
+ .map((request) => `${request.handle} · ${request.source_label} · ${request.request.presentation.title}`);
41
41
  return {
42
42
  text: lines.length ? lines.join("\n") : "No pending BrokerKit requests.",
43
43
  };
@@ -49,13 +49,13 @@ export async function handleCommand(runtime, ctx) {
49
49
  .requests.find((item) => item.handle === handle);
50
50
  if (command === "show")
51
51
  return { text: request ? formatRequest(request) : "Request not found." };
52
- if (["approve", "deny", "cancel", "revoke"].includes(command)) {
52
+ if (["approve", "deny", "revoke"].includes(command)) {
53
53
  if (!request)
54
54
  return { text: "Request not found." };
55
55
  try {
56
- const updated = await runtime.decide(handle, command, request.revision, ctx.senderId);
56
+ const updated = await runtime.decide(handle, command, request.request.revision, ctx.senderId);
57
57
  return {
58
- text: `${capitalize(command)} committed for ${updated.presentation.title}.`,
58
+ text: `${capitalize(command)} committed for ${updated.request.presentation.title}.`,
59
59
  };
60
60
  }
61
61
  catch (error) {
@@ -88,11 +88,10 @@ function capitalize(value) {
88
88
  return value.charAt(0).toUpperCase() + value.slice(1);
89
89
  }
90
90
  function formatRequest(request) {
91
- const facts = request.presentation.facts?.map((fact) => `${fact.label}: ${fact.value}`) ??
92
- [];
91
+ const facts = request.request.presentation.facts?.map((fact) => `${fact.label}: ${fact.value}`) ?? [];
93
92
  return [
94
- `${request.sourceLabel}: ${request.presentation.title}`,
95
- request.presentation.summary ?? "",
93
+ `${request.source_label}: ${request.request.presentation.title}`,
94
+ request.request.presentation.summary ?? "",
96
95
  ...facts,
97
96
  `Handle: ${request.handle}`,
98
97
  ]
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+ export {};
@@ -1,5 +1,4 @@
1
- // Generated by scripts/generate-operator-v1.mjs. Do not edit.
2
- export const OPERATOR_V1_SCHEMA_SHA256 = "a4510c3d0e1ddf32dfb8e8bd684677ade3f3d6541912d42720af8c43ef11c2eb";
1
+ export const OPERATOR_V1_SCHEMA_SHA256 = "62065b7c9d09989c66379c6a81dd23f9ae468c7d1a859b69c88a1dee079aa6a6";
3
2
  export const operatorV1 = {
4
3
  apiVersion: "brokerkit.io/operator/v1",
5
4
  statuses: [
@@ -11,7 +10,7 @@ export const operatorV1 = {
11
10
  "consumed",
12
11
  "revoked",
13
12
  ],
14
- actions: ["approve", "deny", "cancel", "revoke"],
13
+ actions: ["approve", "deny", "revoke"],
15
14
  risks: ["unknown", "low", "medium", "high", "critical"],
16
15
  eventKinds: [
17
16
  "request.created",
@@ -19,21 +18,26 @@ export const operatorV1 = {
19
18
  "request.denied",
20
19
  "request.canceled",
21
20
  "request.expired",
22
- "request.consumed",
23
- "request.revoked",
24
- "request.updated",
21
+ "grant.revoked",
22
+ "grant.reserved",
23
+ "grant.consumed",
24
+ "grant.released",
25
+ "execution.succeeded",
26
+ "execution.failed",
27
+ "execution.ambiguous",
25
28
  ],
26
29
  errorCodes: [
27
30
  "invalid_request",
28
31
  "unauthorized",
29
32
  "forbidden",
30
33
  "not_found",
34
+ "method_not_allowed",
31
35
  "revision_conflict",
32
- "invalid_transition",
33
- "constraint_exceeded",
34
36
  "idempotency_conflict",
37
+ "constraint_exceeded",
38
+ "invalid_transition",
39
+ "invalid_decision_token",
35
40
  "cursor_expired",
36
- "rate_limited",
37
41
  "temporarily_unavailable",
38
42
  "internal_error",
39
43
  ],
@@ -52,6 +56,6 @@ export const operatorV1 = {
52
56
  page: 100,
53
57
  idempotencyKey: 200,
54
58
  errorMessage: 500,
55
- correlationId: 200,
59
+ correlationId: 128,
56
60
  },
57
61
  };