@byokit/openclaw 0.3.6 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.0 (2026-10-01)
6
+
7
+ - Dependency update: pins @byokit/relay 0.5.1.
8
+
9
+ - FIX: Wait for approval callbacks in the bridge parking test instead of assuming the gate finishes within 50 ms.
10
+ - FIX: Successful runs preserve complete generated text in the final callback and result instead of replacing it with a capped terminal snapshot; silent and empty replies remain empty.
11
+
12
+ - Add app-supplied output schemas to host and device runs, with inferred result data, local final-answer validation and typed output failures. Subscription routes and explicit API key (billed per use) opt-in are unchanged.
13
+
14
+ ## 0.4.0 (2026-10-01)
15
+
16
+ - Dependency update: pins @byokit/ui-core 0.6.0.
17
+ - Dependency update: pins @byokit/reach 0.6.0.
18
+
19
+ - Key readiness waits through the engine's retryable restart refusal after activation; missing keys still fail without another account.
20
+ - API key (billed per use): `addKey` verifies and stores an explicitly entered key in a separate member-owned agent, with copying disabled and no automatic subscription fallback. `run({ auth: 'apiKey' })` selects it explicitly with separate history; key-entry labels, errors and route revision dates are included.
21
+ - Offer subscription sign-ins, including pasted tokens and native Claude Code, by default and allow their provider plugins. API key (billed per use) routes remain app opt-in; proxies, compatibility aliases and local routes stay off.
22
+
5
23
  ## 0.3.6 (2026-10-01)
6
24
 
7
25
  - FIX: Live orphan gateways recover after the engine rewrites its process title. Recovery verifies the recorded launch pid and process start time with the executable and isolated store paths; ambiguous ownership preserves the saved sign-in state.
package/README.md CHANGED
@@ -69,6 +69,23 @@ provider or model. If the member isn't signed in to it, the run ends `{ ok: fals
69
69
  engine is never called. Leave `model` out to keep the engine's own choice. A specific sign-in (`@profile`) can't be
70
70
  picked per run: the pinned engine may still switch to another sign-in for the same provider.
71
71
 
72
+
73
+ For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'openai-api-key', apiKey })` returns
74
+ `ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
75
+ `keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
76
+ label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
77
+ app state. The kit never returns engine key-check errors or the key. `ui-core/kits` supplies `keyStep`/`keyView`
78
+ for entry, checking, success and failure states, using the kit's `key.*` words.
79
+
80
+ To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
81
+ `byokit-key-<member>` agent and workspace, with `copyToAgents: false` and only that key in its local auth order.
82
+ Its selected model is used; omit `model` or pass that same model. Key runs have separate history. To steer or stop
83
+ one, pass `{ auth: 'apiKey' }` to `steer` or `abort` too (the device helpers accept the same option).
84
+ Ordinary runs never enter the key agent, even when the subscription is resting. This agent boundary is necessary
85
+ because the engine's profile pins can rotate. The `byokit-key-` member prefix is reserved. Adding another key
86
+ replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
87
+ an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
88
+
72
89
  A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
73
90
  (`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
74
91
  it. Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
@@ -77,6 +94,46 @@ ends ok carries `usage` (the engine's token total for the run, and `costUsd` whe
77
94
  `openclawDevice(link).run(message, o)` takes the same options over the link, and `state()` adds the kit and engine
78
95
  versions and the providers the device's member is signed in to.
79
96
 
97
+ For a validated JSON answer, pass a literal `schema`. The result's `data` is inferred from that schema:
98
+
99
+ ```ts
100
+ import type { OpenClawKit } from '@byokit/openclaw';
101
+
102
+ async function summarize(kit: OpenClawKit) {
103
+ const end = await kit.run({
104
+ member: 'umer', sessionKey: 'agent:umer:report', message: 'Summarize the work.',
105
+ schema: {
106
+ type: 'object',
107
+ properties: { summary: { type: 'string' }, ready: { type: 'boolean' } },
108
+ required: ['summary', 'ready'],
109
+ additionalProperties: false,
110
+ },
111
+ });
112
+ if (end.ok && end.data) {
113
+ const summary: string = end.data.summary;
114
+ const ready: boolean = end.data.ready;
115
+ // Show summary and ready in the app.
116
+ }
117
+ return end;
118
+ }
119
+ ```
120
+
121
+ `openclawDevice(link).run(message, { schema })` carries the same schema and typed result over the link.
122
+ The pinned engine has no general schema parameter: the kit adds a JSON-only instruction to the run's
123
+ system prompt, then parses and validates the final answer locally. Partial text events are unvalidated.
124
+ Malformed JSON or a schema mismatch returns `{ ok: false, kind: 'output', message }`; the message never
125
+ includes the answer. The kit sends one agent request, with no kit retries or sign-in changes. Subscription
126
+ routes stay subscriptions; an API key (billed per use) still requires the app's explicit opt-in.
127
+
128
+ The supported JSON Schema subset covers types (including nullable arrays of types), objects with
129
+ `properties`, `required` and `additionalProperties`, arrays with one `items` schema, `enum`, `const`,
130
+ `anyOf`, `oneOf`, `allOf`, `not`, numeric bounds, string/array/object length bounds and `uniqueItems`.
131
+ Annotations `title`, `description`, `default`, `examples` and the draft-07 `$schema` are accepted;
132
+ defaults are never inserted and values are never coerced. Unsupported keywords (including `$ref`,
133
+ `format` and `pattern`), malformed schemas, schemas over 64 KiB or over 32 levels deep throw before
134
+ any Gateway request. Use `as const` for schemas defined separately; dynamic schemas return unknown
135
+ `data`. App-specific business checks belong in the app after validation.
136
+
80
137
  The same kit runs offline against the fake Gateway from `@byokit/openclaw/testing` (no engine, no network, no
81
138
  account), and lists the sign-in routes it offers:
82
139
 
@@ -120,10 +177,15 @@ route: github-copilot subscription code
120
177
  route: github-copilot-enterprise subscription code
121
178
  route: minimax-global-oauth subscription code
122
179
  route: minimax-cn-oauth subscription code
180
+ route: anthropic-cli subscription browser
181
+ route: setup-token subscription browser
123
182
  ```
124
183
 
125
- Every offered route is a subscription sign-in. Each `Route` carries `billing` (`subscription`, `api` or `local`), so
126
- an app labels any other route it shows; API-billed routes are not offered by default.
184
+ Subscription sign-ins are offered by default. API-billed routes remain in `kit.routes()` with `offer: false`;
185
+ apps can offer them when the app or person opts in, labelled API key (billed per use). Each `Route` carries `billing` (`subscription`, `api` or
186
+ `local`) so the app can label what is billed. Proxy routes, compatibility aliases, local runtimes stay off. Native Claude Code sign-in is offered by default. The pinned Gateway guides only some routes; other choices need manual paste or key entry. For an opted-in route, allow its `plugin` id
187
+ in the app configuration and pass its `choice` explicitly to `kit.signIn()`.
188
+ Offered provider plugins are allowed by default alongside the app's plugin ids.
127
189
 
128
190
  ## API at a glance
129
191
 
package/dist/device.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { DeviceLink } from '@byokit/link';
2
2
  import { type AccountView } from './words.ts';
3
- import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, Route, RunEnd, RunEvent, SignInView } from './types.ts';
3
+ import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, Route, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView } from './types.ts';
4
4
  export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
5
- export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView } from './types.ts';
5
+ export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
6
6
  /** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
7
7
  export type OpenClawLinkEvent = {
8
8
  [E in GatewayEventName]: {
@@ -19,10 +19,16 @@ export type SessionRow = {
19
19
  sessionKey: string;
20
20
  [k: string]: unknown;
21
21
  };
22
+ type EndFrame<T = unknown> = {
23
+ type: 'end';
24
+ end: RunEnd<T>;
25
+ };
22
26
  /** `oc.run`'s options beside the message: the member's own session, account, and the kit's run options. */
23
- export type DeviceRunOptions = {
27
+ export type DeviceRunOptions<S extends OutputSchema | undefined = OutputSchema | undefined> = {
28
+ schema?: S;
24
29
  sessionKey?: string;
25
30
  model?: string;
31
+ auth?: 'apiKey';
26
32
  system?: string;
27
33
  images?: {
28
34
  data: string;
@@ -60,12 +66,13 @@ export declare function openclawDevice(link: DeviceLink): {
60
66
  };
61
67
  signOut(p: string): Promise<void>;
62
68
  sessions(): Promise<SessionRow[]>;
63
- run(message: string, o?: DeviceRunOptions): AsyncIterable<RunEvent | {
64
- type: 'end';
65
- end: RunEnd;
66
- }>;
67
- steer(k: string, t: string): Promise<void>;
68
- abort(k: string): Promise<void>;
69
+ run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<RunEvent | EndFrame<SchemaOutput<S>>>;
70
+ steer(k: string, t: string, o?: {
71
+ auth?: 'apiKey';
72
+ }): Promise<void>;
73
+ abort(k: string, o?: {
74
+ auth?: 'apiKey';
75
+ }): Promise<void>;
69
76
  approvals(): Promise<Approval[]>;
70
77
  decide(id: string, d: Decision): Promise<void>;
71
78
  events(): AsyncIterable<OpenClawLinkEvent>;
package/dist/device.js CHANGED
@@ -139,8 +139,9 @@ export function openclawDevice(link) {
139
139
  run: (message, o) => {
140
140
  const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: o.sessionKey } : {}),
141
141
  ...(o?.model !== undefined ? { model: o.model } : {}), ...(o?.system !== undefined ? { system: o.system } : {}),
142
+ ...(o?.auth !== undefined ? { auth: o.auth } : {}),
142
143
  ...(o?.images !== undefined ? { images: o.images } : {}), ...(o?.thinking !== undefined ? { thinking: o.thinking } : {}),
143
- ...(o?.tools !== undefined ? { tools: o.tools } : {}) }));
144
+ ...(o?.tools !== undefined ? { tools: o.tools } : {}), ...(o?.schema !== undefined ? { schema: o.schema } : {}) }));
144
145
  let finished = false;
145
146
  return {
146
147
  [Symbol.asyncIterator]() {
@@ -168,11 +169,11 @@ export function openclawDevice(link) {
168
169
  },
169
170
  };
170
171
  },
171
- steer: async (k, t) => {
172
- await link.request('oc.steer', { sessionKey: k, text: t });
172
+ steer: async (k, t, o) => {
173
+ await link.request('oc.steer', { sessionKey: k, text: t, ...(o?.auth ? { auth: o.auth } : {}) });
173
174
  },
174
- abort: async (k) => {
175
- await link.request('oc.abort', { sessionKey: k });
175
+ abort: async (k, o) => {
176
+ await link.request('oc.abort', { sessionKey: k, ...(o?.auth ? { auth: o.auth } : {}) });
176
177
  },
177
178
  approvals: () => link.request('oc.approvals'),
178
179
  decide: async (id, d) => {
package/dist/engine.js CHANGED
@@ -148,7 +148,7 @@ export class Engine {
148
148
  const pluginDir = join(this.root, 'plugin');
149
149
  mkdirSync(pluginDir, { recursive: true, mode: 0o700 });
150
150
  // The shipped plugin follows the kit on every prepare too: a state dir from an older kit must not keep its gate.
151
- for (const f of ['package.json', 'index.js']) {
151
+ for (const f of ['package.json', 'index.js', 'keys.js']) {
152
152
  const source = join(kitDir, 'plugin', f);
153
153
  putChanged(join(pluginDir, f), readFileSync(source, 'utf8'));
154
154
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
2
- export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
2
+ export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
3
3
  export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
4
+ export type { AddKeyResult } from './keys.ts';
4
5
  export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
5
6
  export { EngineAlreadyRunningError } from './engine-status.ts';
package/dist/keys.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { GatewayTransport, Member } from './types.ts';
2
+ export type AddKeyResult = 'ok' | 'invalid' | 'not_included';
3
+ export declare function createKeys(ctx: {
4
+ root: string;
5
+ request: GatewayTransport['request'];
6
+ ensure(member: Member): Promise<{
7
+ agentId: string;
8
+ }>;
9
+ restarting?(): boolean;
10
+ }): {
11
+ exclusive: <T>(member: Member, work: () => Promise<T>) => Promise<T>;
12
+ add(member: Member, o: {
13
+ authChoice: string;
14
+ apiKey: string;
15
+ }): Promise<AddKeyResult>;
16
+ ready(member: Member): Promise<{
17
+ agentId: string;
18
+ model: string;
19
+ } | undefined>;
20
+ };
package/dist/keys.js ADDED
@@ -0,0 +1,73 @@
1
+ import { join } from 'node:path';
2
+ import { keyAgentId, MEMBER_ID, KEY_PREFIX } from "./members.js";
3
+ import { routes } from "./routes.js";
4
+ export function createKeys(ctx) {
5
+ // Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
6
+ const tails = new Map();
7
+ const exclusive = async (member, work) => {
8
+ const previous = tails.get(member) ?? Promise.resolve();
9
+ const next = previous.catch(() => { }).then(work);
10
+ tails.set(member, next);
11
+ try {
12
+ return await next;
13
+ }
14
+ finally {
15
+ if (tails.get(member) === next)
16
+ tails.delete(member);
17
+ }
18
+ };
19
+ const check = (member) => MEMBER_ID.test(member) && !member.startsWith(KEY_PREFIX);
20
+ const control = (member, action, provider) => ctx.request('byokit.keys', { member, action, ...(provider ? { provider } : {}) });
21
+ return {
22
+ exclusive,
23
+ async add(member, o) {
24
+ if (!check(member) || !o.apiKey.trim())
25
+ return 'invalid';
26
+ const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry);
27
+ if (!route)
28
+ return 'not_included';
29
+ return exclusive(member, async () => {
30
+ try {
31
+ await ctx.ensure(member);
32
+ const agentId = keyAgentId(member);
33
+ const list = await ctx.request('agents.list');
34
+ if (!list.agents.some((a) => a.id === agentId))
35
+ await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
36
+ if (!(await control(member, 'prepare', route.provider)).ok)
37
+ return 'invalid';
38
+ // The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
39
+ const params = {
40
+ kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey,
41
+ };
42
+ const result = (await ctx.request('openclaw.setup.activate', params, { timeoutMs: 180_000 }));
43
+ if (!result.ok)
44
+ return result.status === 'unavailable' ? 'not_included' : 'invalid';
45
+ return (await control(member, 'seal')).ok ? 'ok' : 'invalid';
46
+ }
47
+ catch {
48
+ return 'invalid';
49
+ }
50
+ });
51
+ },
52
+ async ready(member) {
53
+ if (!check(member))
54
+ return undefined;
55
+ const deadline = Date.now() + 60_000;
56
+ for (;;) {
57
+ try {
58
+ const ready = await control(member, 'ready');
59
+ return ready.ok && ready.model ? { agentId: keyAgentId(member), model: ready.model } : undefined;
60
+ }
61
+ catch (error) {
62
+ // Config writes may drain/restart the gateway after activation. This read is safe to retry;
63
+ // an absent profile or a terminal refusal still fails closed immediately.
64
+ const refusal = error;
65
+ const transient = ctx.restarting?.() || (refusal?.code === 'UNAVAILABLE' && refusal.retryable === true);
66
+ if (!transient || Date.now() >= deadline)
67
+ return undefined;
68
+ await new Promise((resolve) => setTimeout(resolve, 250));
69
+ }
70
+ }
71
+ },
72
+ };
73
+ }
package/dist/kit.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { SealingAdapter } from '@byokit/secrets';
2
- import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
2
+ import { type AddKeyResult } from './keys.ts';
3
+ import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
3
4
  export type KitOptions = {
4
5
  stateDir: string;
5
6
  authSeal?: SealingAdapter;
@@ -47,6 +48,7 @@ export declare class OpenClawKit {
47
48
  private greeting?;
48
49
  private members?;
49
50
  private readonly bridge;
51
+ private readonly keys;
50
52
  private readonly approvalsCtl;
51
53
  private listeners;
52
54
  private off;
@@ -81,14 +83,24 @@ export declare class OpenClawKit {
81
83
  done: Promise<SignInView>;
82
84
  };
83
85
  signOut(member: Member, provider: string): Promise<void>;
86
+ /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
87
+ addKey(agentId: Member, o: {
88
+ authChoice: string;
89
+ apiKey: string;
90
+ }): Promise<AddKeyResult>;
84
91
  migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
85
92
  confirmRetainedLogin(member: Member, source: RetainedLogin): Promise<boolean>;
86
93
  private runs;
87
94
  /** The app tool names (`KitOptions.tools`): the only names `RunSpec.tools` may carry. */
88
95
  toolNames(): string[];
89
- run(spec: RunSpec, on?: (e: RunEvent) => void): Promise<RunEnd>;
90
- steer(sessionKey: string, text: string): Promise<void>;
91
- abort(sessionKey: string): Promise<void>;
96
+ run<const S extends OutputSchema | undefined = undefined>(spec: RunSpec<S>, on?: (e: RunEvent) => void): Promise<RunEnd<SchemaOutput<S>>>;
97
+ private runKey;
98
+ steer(sessionKey: string, text: string, o?: {
99
+ auth?: 'apiKey';
100
+ }): Promise<void>;
101
+ abort(sessionKey: string, o?: {
102
+ auth?: 'apiKey';
103
+ }): Promise<void>;
92
104
  approvals(member?: Member): Approval[];
93
105
  onApproval(fn: (a: Approval, change: 'added' | 'resolved') => void): () => void;
94
106
  decide(id: string, d: Decision): Promise<void>;
package/dist/kit.js CHANGED
@@ -8,8 +8,11 @@ import { Bridge } from "./bridge.js";
8
8
  import { Engine } from "./engine.js";
9
9
  import { gatewayTransport } from "./transport.js";
10
10
  import { createMembers } from "./members.js";
11
+ import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
11
12
  import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
12
13
  import { createRuns } from "./runs.js";
14
+ import { outputSchema } from "./output.js";
15
+ import { createKeys } from "./keys.js";
13
16
  import { routes as routeTable } from "./routes.js";
14
17
  import { providers as engineProviders, signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
15
18
  import { reconcileConfig, memoryLimited as configMemoryLimited } from "./config.js";
@@ -420,6 +423,7 @@ export class OpenClawKit {
420
423
  // One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
421
424
  // Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
422
425
  bridge;
426
+ keys;
423
427
  approvalsCtl;
424
428
  listeners = new Set();
425
429
  off = [];
@@ -439,6 +443,10 @@ export class OpenClawKit {
439
443
  this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: o.tools ?? [],
440
444
  gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
441
445
  onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
446
+ this.keys = createKeys({ root: this.engine.root,
447
+ restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
448
+ request: (method, params, options) => this.request()(method, params, options),
449
+ ensure: (member) => this.ensureMember(member) });
442
450
  const slot = {};
443
451
  this.approvalsCtl = new Approvals({
444
452
  request: (method, params, co) => this.request()(method, params, co),
@@ -599,6 +607,10 @@ export class OpenClawKit {
599
607
  signOut(member, provider) {
600
608
  return engineSignOut(this.signInCtx(), member, provider);
601
609
  }
610
+ /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
611
+ addKey(agentId, o) {
612
+ return this.keys.add(agentId, o);
613
+ }
602
614
  migrateRetainedLogin(member, source) {
603
615
  return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
604
616
  seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
@@ -627,13 +639,43 @@ export class OpenClawKit {
627
639
  return (this.o.tools ?? []).map((t) => t.name);
628
640
  }
629
641
  run(spec, on) {
630
- return this.runs().run(spec, on);
642
+ if (spec.auth !== undefined && spec.auth !== 'apiKey')
643
+ return Promise.reject(new Error('Choose a supported account option.'));
644
+ // Snapshot before account readiness awaits; invalid schemas never reach the Gateway.
645
+ let output;
646
+ try {
647
+ output = spec.schema === undefined ? undefined : outputSchema(spec.schema);
648
+ }
649
+ catch (error) {
650
+ return Promise.reject(error);
651
+ }
652
+ if (spec.auth === 'apiKey')
653
+ return this.keys.exclusive(spec.member, async () => {
654
+ if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
655
+ throw new Error('This conversation belongs to another member.');
656
+ const selected = await this.keys.ready(spec.member);
657
+ if (!selected)
658
+ return { ok: false, kind: 'signed-out', message: 'Add an API key to use this option.' };
659
+ if (spec.model !== undefined && spec.model !== selected.model)
660
+ return { ok: false, kind: 'plan', message: 'Choose the model saved with this key.' };
661
+ // RunRef.member stays the person for app gates; the engine receives the isolated agent and history.
662
+ return this.runs().run({ ...spec, model: selected.model }, on, selected.agentId, output);
663
+ });
664
+ return this.runs().run(spec, on, undefined, output);
665
+ }
666
+ runKey(sessionKey, o) {
667
+ if (o?.auth !== 'apiKey')
668
+ return sessionKey;
669
+ const [, member, tail] = /^agent:([^:]+):(.*)$/.exec(sessionKey) ?? [];
670
+ if (!member || !MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
671
+ throw new Error('Choose your own conversation.');
672
+ return `agent:${keyAgentId(member)}:${tail}`;
631
673
  }
632
- steer(sessionKey, text) {
633
- return this.runs().steer(sessionKey, text);
674
+ steer(sessionKey, text, o) {
675
+ return this.runs().steer(this.runKey(sessionKey, o), text);
634
676
  }
635
- abort(sessionKey) {
636
- return this.runs().abort(sessionKey);
677
+ abort(sessionKey, o) {
678
+ return this.runs().abort(this.runKey(sessionKey, o));
637
679
  }
638
680
  // approvals (5.9)
639
681
  approvals(member) {
package/dist/link.js CHANGED
@@ -10,6 +10,7 @@ import { ENGINE_VERSION } from "./constants.js";
10
10
  import { b64urlDecode, sealNotice } from "./notices.js";
11
11
  import { routeFor } from "./routes.js";
12
12
  import { authStatus } from "./auth-status.js";
13
+ import { outputSchema } from "./output.js";
13
14
  import { signedInProviders } from "./runs.js";
14
15
  import { stateWords, words } from "./words.js";
15
16
  const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events']);
@@ -25,18 +26,23 @@ function memberSessions(result, member) {
25
26
  }
26
27
  /** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
27
28
  function runOptions(args) {
28
- const { message, model, system, images, thinking, tools } = args;
29
+ const { message, model, auth, system, images, thinking, tools, schema } = args;
29
30
  const ok = typeof message === 'string'
30
31
  && (model === undefined || typeof model === 'string')
32
+ && (auth === undefined || auth === 'apiKey')
31
33
  && (system === undefined || typeof system === 'string')
32
34
  && (images === undefined || (Array.isArray(images) && images.every((i) => isRecord(i) && typeof i.data === 'string' && typeof i.mimeType === 'string')))
33
35
  && (thinking === undefined || (typeof thinking === 'string' && THINKING.has(thinking)))
34
36
  && (tools === undefined || (Array.isArray(tools) && tools.every((t) => typeof t === 'string')));
35
37
  if (!ok)
36
- throw new Error('oc.run needs { message, sessionKey?, model?, system?, images?, thinking?, tools? }');
38
+ throw new Error('oc.run needs { message, sessionKey?, model?, auth?, system?, images?, thinking?, tools?, schema? }');
39
+ if (schema !== undefined)
40
+ outputSchema(schema);
37
41
  return {
38
42
  message: message,
43
+ ...(schema === undefined ? {} : { schema: schema }),
39
44
  ...(model === undefined ? {} : { model: model }),
45
+ ...(auth === undefined ? {} : { auth: 'apiKey' }),
40
46
  ...(system === undefined ? {} : { system: system }),
41
47
  ...(images === undefined ? {} : { images: images
42
48
  .map((i) => ({ data: i.data, mimeType: i.mimeType })) }),
@@ -180,19 +186,19 @@ export function openclawLink(kit, o) {
180
186
  return memberSessions(result, member);
181
187
  }
182
188
  case 'oc.steer': {
183
- if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string')
189
+ if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
184
190
  throw new Error('oc.steer needs { sessionKey, text }');
185
191
  if (!memberKey(member, args.sessionKey))
186
192
  throw refused();
187
- await kit.steer(args.sessionKey, args.text);
193
+ await kit.steer(args.sessionKey, args.text, args.auth === 'apiKey' ? { auth: 'apiKey' } : undefined);
188
194
  return null;
189
195
  }
190
196
  case 'oc.abort': {
191
- if (typeof args.sessionKey !== 'string')
197
+ if (typeof args.sessionKey !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
192
198
  throw new Error('oc.abort needs { sessionKey }');
193
199
  if (!memberKey(member, args.sessionKey))
194
200
  throw refused();
195
- await kit.abort(args.sessionKey);
201
+ await kit.abort(args.sessionKey, args.auth === 'apiKey' ? { auth: 'apiKey' } : undefined);
196
202
  return null;
197
203
  }
198
204
  case 'oc.approvals':
package/dist/members.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { GatewayTransport, Member } from './types.ts';
2
2
  export declare const MEMBER_ID: RegExp;
3
+ export declare const KEY_PREFIX = "byokit-key-";
4
+ export declare const keyAgentId: (member: Member) => string;
3
5
  export declare function createMembers(ctx: {
4
6
  request: GatewayTransport['request'];
5
7
  root: string;
package/dist/members.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { join } from 'node:path';
2
2
  export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
3
+ export const KEY_PREFIX = 'byokit-key-';
4
+ export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
3
5
  export function createMembers(ctx) {
4
6
  const cache = new Map();
5
7
  return { ensure(member) {
6
- if (!MEMBER_ID.test(member))
8
+ if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
7
9
  return Promise.reject(new Error(`invalid member id: ${member}`));
8
10
  const existing = cache.get(member);
9
11
  if (existing)
@@ -0,0 +1,68 @@
1
+ export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
2
+ readonly [key: string]: JsonValue;
3
+ };
4
+ type JsonType = 'null' | 'boolean' | 'number' | 'integer' | 'string' | 'array' | 'object';
5
+ type SchemaNode = boolean | OutputSchema;
6
+ export type OutputSchema = {
7
+ readonly type?: JsonType | readonly JsonType[];
8
+ readonly properties?: Readonly<Record<string, SchemaNode>>;
9
+ readonly required?: readonly string[];
10
+ readonly additionalProperties?: SchemaNode;
11
+ readonly items?: SchemaNode;
12
+ readonly enum?: readonly JsonValue[];
13
+ readonly const?: JsonValue;
14
+ readonly anyOf?: readonly SchemaNode[];
15
+ readonly oneOf?: readonly SchemaNode[];
16
+ readonly allOf?: readonly SchemaNode[];
17
+ readonly not?: SchemaNode;
18
+ readonly minimum?: number;
19
+ readonly maximum?: number;
20
+ readonly exclusiveMinimum?: number;
21
+ readonly exclusiveMaximum?: number;
22
+ readonly minLength?: number;
23
+ readonly maxLength?: number;
24
+ readonly minItems?: number;
25
+ readonly maxItems?: number;
26
+ readonly uniqueItems?: boolean;
27
+ readonly minProperties?: number;
28
+ readonly maxProperties?: number;
29
+ readonly title?: string;
30
+ readonly description?: string;
31
+ readonly default?: JsonValue;
32
+ readonly examples?: readonly JsonValue[];
33
+ readonly $schema?: 'http://json-schema.org/draft-07/schema#';
34
+ };
35
+ type KeysInEveryList<R extends readonly string[]> = (R extends unknown ? (key: number extends R['length'] ? never : R[number]) => void : never) extends (key: infer K) => void ? Extract<K, string> : never;
36
+ type RequiredKeys<S> = S extends {
37
+ readonly required: infer R extends readonly string[];
38
+ } ? KeysInEveryList<R> : never;
39
+ type ObjectOutput<S> = S extends {
40
+ readonly properties: infer P extends Record<string, SchemaNode>;
41
+ } ? {
42
+ -readonly [K in keyof P as K extends RequiredKeys<S> ? K : never]: SchemaOutput<P[K]>;
43
+ } & {
44
+ -readonly [K in keyof P as K extends RequiredKeys<S> ? never : K]?: SchemaOutput<P[K]>;
45
+ } : Record<string, unknown>;
46
+ type OutputType<T, S> = T extends 'null' ? null : T extends 'boolean' ? boolean : T extends 'number' | 'integer' ? number : T extends 'string' ? string : T extends 'array' ? (S extends {
47
+ readonly items: infer I;
48
+ } ? SchemaOutput<I> : unknown)[] : T extends 'object' ? ObjectOutput<S> : unknown;
49
+ /** Literal schemas infer their output; broad/dynamic schemas and unsupported inference shapes remain unknown. */
50
+ export type SchemaOutput<S> = S extends {
51
+ readonly const: infer V;
52
+ } ? V : S extends {
53
+ readonly enum: readonly (infer V)[];
54
+ } ? V : S extends {
55
+ readonly type: infer T;
56
+ } ? OutputType<T extends readonly unknown[] ? T[number] : T, S> : S extends {
57
+ readonly anyOf: readonly (infer B)[];
58
+ } | {
59
+ readonly oneOf: readonly (infer B)[];
60
+ } ? SchemaOutput<B> : unknown;
61
+ /** Snapshot a JSON-only schema before any await; no mutation can change the prompt or its validation. */
62
+ export declare function outputSchema(value: unknown): {
63
+ prompt: string;
64
+ parse(text: string): {
65
+ data: unknown;
66
+ } | undefined;
67
+ };
68
+ export {};