@byokit/openclaw 0.18.0 → 0.19.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,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.19.0 (2026-10-19)
6
+
7
+ - FEAT: connect Composio through the pinned engine's native MCP OAuth flow, with browser URL handoff and native sign-out.
8
+
9
+ ## 0.18.3 (2026-10-19)
10
+
11
+ - A signed-in Claude Code login is an account in the kit's account list, and its Claude CLI route's models are listed under it.
12
+
13
+ ## 0.18.2 (2026-10-19)
14
+
15
+ - A signed-in ChatGPT login lists the Codex CLI route's models in the account model list, marked available.
16
+
17
+ ## 0.18.1 (2026-10-18)
18
+
19
+ - Dependency update: pins @byokit/accounts 0.28.1.
20
+ - FIX: (from @byokit/accounts 0.28.1) Installed Codex readiness retries a transient account check once and distinguishes timeouts, startup, pipe, exit, protocol and unreadable-reply failures from a confirmed signed-out account, with safe typed probe metadata and no provider messages or stderr.
21
+
5
22
  ## 0.18.0 (2026-10-18)
6
23
 
7
24
  - Reattach a paired device to a live-kit run by its existing idempotency key: sequenced replay then live events, a bounded journal with typed truncation, and grant-member isolation. Device disconnect does not interrupt the run; engine-restart reconstruction is not included.
package/README.md CHANGED
@@ -390,6 +390,8 @@ apps can offer them when the app or person opts in, labelled API key (billed per
390
390
  `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 kit re-checks the installed Claude login at each setup detection and, once it is signed in, restarts the engine onto it. A plan key (OpenCode Go: `kit.addAccount(member, { authChoice: 'opencode-go' }, on)`) asks for `Plan key`; `paste` it and the engine live-tests it before `done`. A paired device starts the same sign-in with either `via` and pastes the key over the sealed link, which keeps it on the host. A run reaches the pinned engine's static Go models (default `opencode-go/deepseek-v4-pro`); the kit keeps OpenCode Go's `x-opencode-session` header on every request, the setup probe included. A live-only Go model such as Muse needs no app config: the kit registers the plan's live catalog with each model's API, so `run({ model: 'opencode-go/muse-spark-1.3-contributor' })` runs on the account (or with `auth: 'apiKey'` on a Go key); an app that sets `config.models.providers['opencode-go'].models` keeps its own list. A choice the pinned Gateway cannot set up has readiness `no_upstream_flow` and is never offered. Pass the accounts the app offers in `KitOptions.offered` (the provider, auth choice or alias each route carries, e.g. `'openrouter'`); the kit allow-lists the plugin of every route those name, so an opted-in sign-in is not blocked. An app can still name a plugin directly in `config.plugins.allow`, and pass the route's `choice` explicitly to `kit.signIn()`.
391
391
  Offered provider plugins are allowed by default alongside the app's plugin ids. A route whose provider plugin ships outside the engine (Z.AI's GLM Coding Plan, for one) has readiness `needs_plugin` until the app calls `await kit.installPlugin(route.needs.plugin)`: it installs that plugin into the kit's own engine at exactly the engine's version and pinned npm integrity, keeps it allowed across restarts, and rejects `PluginInstallError` (`code` `version` or `integrity`) with nothing installed on a mismatch. Discovery never installs.
392
392
 
393
+ Composio uses its hosted MCP endpoint with the pinned OpenClaw engine's native OAuth client; BYOKit does not maintain a Composio app/tool list or install a separate plugin. `beginComposioSignIn()` configures `https://connect.composio.dev/mcp` for Streamable HTTP OAuth and returns `{ url }`; open it in the browser, then call `completeComposioSignIn()` to wait for OpenClaw's callback and token exchange. OpenClaw stores the OAuth credentials in its engine-owned SQLite state; when the app supplies `authSeal`, the kit seals that state at rest with other engine data. `signOutComposio()` invokes native MCP logout. Composio usage is billed under the person's own Composio plan.
394
+
393
395
  ## API at a glance
394
396
 
395
397
  Entries:
@@ -2,6 +2,7 @@ import { createLocks } from './locks.ts';
2
2
  import { type RouteView } from './routes.ts';
3
3
  import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
4
4
  export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
5
+ export declare const modelsOfAccount: (row: Pick<Account, 'provider' | 'state'>, answer: unknown) => ModelInfo[];
5
6
  type IndexEntry = {
6
7
  agent: string;
7
8
  name: string;
package/dist/accounts.js CHANGED
@@ -16,7 +16,7 @@ import { KEY_PREFIX, MEMBER_ID, NEW_MEMBER_ID, keyAgentId, untilListed, untilUnl
16
16
  import { routes, routeFor } from './routes.js';
17
17
  import { words } from './words.js';
18
18
  const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
19
- // `models.list { agentId, view: 'default' }` rows of the account's provider, mapped to `provider/model`. The pin
19
+ // `models.list { agentId, view: 'all' }` rows of the account's provider, mapped to `provider/model`. The pin
20
20
  // reports no per-plan availability, so `why: 'plan'` only comes from a run ending `plan`, never from here.
21
21
  export const modelsOf = (provider, answer) => {
22
22
  const rows = record(answer) && Array.isArray(answer.models) ? answer.models : [];
@@ -48,6 +48,16 @@ export const modelsOf = (provider, answer) => {
48
48
  }
49
49
  return out;
50
50
  };
51
+ // The account's models: its provider's rows, plus the Codex CLI route's rows when the account is ready. The pin reports
52
+ // those as missing-auth (no auth reaches the Codex route from the ChatGPT login), so the kit, which holds the login, is the
53
+ // signal for them.
54
+ export const modelsOfAccount = (row, answer) => {
55
+ const own = modelsOf(row.provider, answer);
56
+ if (row.provider !== 'openai' || row.state !== 'ready')
57
+ return own;
58
+ const codex = modelsOf('openai-codex', answer).map(({ why: _why, ...m }) => ({ ...m, available: true }));
59
+ return [...own, ...codex];
60
+ };
51
61
  const ACCOUNT_AGENT = /^[a-z][a-z0-9-]{0,31}--[0-9a-f]{6}$/;
52
62
  const usable = (status) => status === 'ok' || status === 'expiring' || status === 'static';
53
63
  const IDENTITY = 'IDENTITY.md';
@@ -512,7 +522,14 @@ export function createAccounts(ctx) {
512
522
  }
513
523
  return { rows, dirty };
514
524
  };
515
- const gather = (agentId) => Promise.all([rosterOf(), profilesOf(agentId)]);
525
+ // A detected Claude Code login has no profile in the kit: it stays in Claude Code. The engine's detection says whether
526
+ // it is there, and a detected one is the member's signed-in claude-cli profile for this read.
527
+ const claudeDetected = (agentId) => ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 })
528
+ .then((r) => r.candidates?.some((c) => c.kind === 'claude-cli' && c.credentials === true) === true, () => false);
529
+ const gather = async (agentId) => {
530
+ const [roster, profiles, claude] = await Promise.all([rosterOf(), profilesOf(agentId), claudeDetected(agentId)]);
531
+ return [roster, claude && !profiles.some((p) => p.provider === 'claude-cli') ? [...profiles, { provider: 'claude-cli', status: 'ok' }] : profiles];
532
+ };
516
533
  const buildRows = async (member, agentId) => {
517
534
  const file = read(member);
518
535
  const [roster, profiles] = await gather(agentId);
@@ -638,7 +655,7 @@ export function createAccounts(ctx) {
638
655
  return readModels(row, entry);
639
656
  }
640
657
  // The shared hold covers the read, so no kit read of the agent is in flight while it is removed.
641
- const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOf(row.provider, await ctx.request('models.list', { agentId: entry.agent, view: 'default' })));
658
+ const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' })));
642
659
  async function room(member, id, demand) {
643
660
  check(member);
644
661
  await ctx.ensure(member);
@@ -812,8 +829,8 @@ export function createAccounts(ctx) {
812
829
  return failed('start_failed', words('move.taken', { name: row.name }));
813
830
  // The session's model when the target lists it, else the target's first available; the read takes no shared
814
831
  // hold (this move holds the target exclusively). With no answer the fork keeps the parent's own selection.
815
- const models = await ctx.request('models.list', { agentId: entry.agent, view: 'default' })
816
- .then((answer) => modelsOf(row.provider, answer), () => []);
832
+ const models = await ctx.request('models.list', { agentId: entry.agent, view: 'all' })
833
+ .then((answer) => modelsOfAccount(row, answer), () => []);
817
834
  const current = typeof session.model !== 'string' ? undefined
818
835
  : (typeof session.modelProvider === 'string' && !session.model.includes('/') ? `${session.modelProvider}/${session.model}` : session.model).toLowerCase();
819
836
  const model = models.find((m) => m.id.toLowerCase() === current)?.id ?? models.find((m) => m.available)?.id;
package/dist/bridge.js CHANGED
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
3
3
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { createServer } from 'node:net';
5
5
  import { rm } from 'node:fs/promises';
6
+ import { provider } from '@byokit/accounts';
6
7
  import { words } from './words.js';
7
8
  /** Newline-framed JSON, one request per connection; anything larger is not a gate request. */
8
9
  const FRAME_CAP = 1_000_000;
@@ -38,6 +39,8 @@ export function writePlugin(dir, o) {
38
39
  name: 'BYOKit bridge',
39
40
  activation: { onStartup: true },
40
41
  cliBackends: ['openai-codex'],
42
+ // The Codex CLI route lists the ChatGPT catalogue's models; the engine lists a plugin route's rows only from here.
43
+ modelCatalog: { providers: { 'openai-codex': { models: [provider('chatgpt').models.strong, provider('chatgpt').models.fast].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
41
44
  contracts: { tools: o.tools.map((t) => t.name), ...(o.browser ? { agentToolResultMiddleware: ['openclaw', 'codex'] } : {}) },
42
45
  configSchema: { type: 'object', additionalProperties: false },
43
46
  };
package/dist/engine.d.ts CHANGED
@@ -62,6 +62,14 @@ export declare class Engine {
62
62
  output: string;
63
63
  }>;
64
64
  private get path();
65
+ private runOwned;
66
+ private composioMcpLogin?;
67
+ private composioMcpContext;
68
+ beginComposioMcpLogin(): Promise<{
69
+ url: string;
70
+ }>;
71
+ completeComposioMcpLogin(): Promise<void>;
72
+ signOutComposio(): Promise<void>;
65
73
  private get claudeFolder();
66
74
  doctorContext(): {
67
75
  entry: string;
package/dist/engine.js CHANGED
@@ -341,6 +341,115 @@ export class Engine {
341
341
  });
342
342
  }
343
343
  get path() { return ['/usr/bin', '/bin', ...this.o.enginePath ?? []].join(':'); }
344
+ async runOwned(binary, args, env, timeoutMs, input) {
345
+ return new Promise((resolve, reject) => {
346
+ const child = spawn(binary, args, { cwd: this.root, env, stdio: ['pipe', 'pipe', 'pipe'] });
347
+ let output = '', timedOut = false;
348
+ this.started.add(child);
349
+ const keep = (chunk) => { output = (output + chunk.toString()).slice(-65_536); };
350
+ child.stdout.on('data', keep);
351
+ child.stderr.on('data', keep);
352
+ const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
353
+ child.once('error', error => { clearTimeout(timer); this.started.forget(child); reject(error); });
354
+ child.once('close', code => { clearTimeout(timer); this.started.forget(child); resolve({ code: timedOut ? null : code, output }); });
355
+ child.stdin.end(input);
356
+ });
357
+ }
358
+ composioMcpLogin;
359
+ composioMcpContext() {
360
+ const { entry, env: inherited } = this.doctorContext();
361
+ const home = join(this.root, 'home');
362
+ return { entry, env: { ...inherited, HOME: home, XDG_CONFIG_HOME: join(home, '.config'),
363
+ XDG_DATA_HOME: join(home, '.local/share'), XDG_STATE_HOME: join(home, '.local/state') } };
364
+ }
365
+ beginComposioMcpLogin() {
366
+ if (this.composioMcpLogin)
367
+ throw new Error('Composio MCP sign-in is already pending.');
368
+ const { entry, env } = this.composioMcpContext();
369
+ const child = spawn(process.execPath, [entry, 'mcp', 'login', 'composio'], {
370
+ cwd: join(this.root, 'home'), env, stdio: ['ignore', 'pipe', 'pipe'],
371
+ });
372
+ this.started.add(child);
373
+ let output = '', urlFound = false;
374
+ let resolveUrl, rejectUrl;
375
+ let resolveDone;
376
+ const url = new Promise((resolve, reject) => { resolveUrl = resolve; rejectUrl = reject; });
377
+ const done = new Promise(resolve => { resolveDone = resolve; });
378
+ const timer = setTimeout(() => { child.kill('SIGKILL'); rejectUrl(new Error('OpenClaw MCP login did not return an authorization URL.')); }, 30_000);
379
+ const capture = (chunk) => {
380
+ output = (output + chunk.toString()).slice(-65_536);
381
+ const raw = output.match(/https:\/\/[^\s"'<>]+/)?.[0];
382
+ if (!raw || urlFound)
383
+ return;
384
+ try {
385
+ const parsed = new URL(raw);
386
+ if (parsed.protocol !== 'https:' || !['connect.composio.dev', 'login.composio.dev'].includes(parsed.hostname)
387
+ || parsed.username || parsed.password)
388
+ throw new Error('OpenClaw returned an unsafe Composio OAuth URL.');
389
+ urlFound = true;
390
+ clearTimeout(timer);
391
+ resolveUrl(parsed.href);
392
+ }
393
+ catch (error) {
394
+ clearTimeout(timer);
395
+ rejectUrl(error);
396
+ child.kill('SIGKILL');
397
+ }
398
+ };
399
+ child.stdout.on('data', capture);
400
+ child.stderr.on('data', capture);
401
+ child.once('error', error => {
402
+ clearTimeout(timer);
403
+ rejectUrl(error);
404
+ this.started.forget(child);
405
+ if (this.composioMcpLogin?.done === done && !urlFound)
406
+ this.composioMcpLogin = undefined;
407
+ resolveDone({ code: null, output });
408
+ });
409
+ child.once('close', code => {
410
+ clearTimeout(timer);
411
+ this.started.forget(child);
412
+ if (!urlFound) {
413
+ rejectUrl(new Error(`OpenClaw MCP login exited before authorization URL (exit ${code}).`));
414
+ if (this.composioMcpLogin?.done === done)
415
+ this.composioMcpLogin = undefined;
416
+ }
417
+ resolveDone({ code, output });
418
+ });
419
+ this.composioMcpLogin = { url, done, child };
420
+ return url.then(value => ({ url: value }));
421
+ }
422
+ async completeComposioMcpLogin() {
423
+ const pending = this.composioMcpLogin;
424
+ if (!pending)
425
+ throw new Error('No Composio MCP sign-in is pending.');
426
+ try {
427
+ await pending.url;
428
+ let timer;
429
+ const timedOut = new Promise(resolve => { timer = setTimeout(() => resolve(null), 10 * 60_000); timer.unref(); });
430
+ const result = await Promise.race([pending.done, timedOut]);
431
+ clearTimeout(timer);
432
+ if (!result) {
433
+ pending.child.kill('SIGKILL');
434
+ await pending.done;
435
+ throw new Error('Composio MCP sign-in timed out.');
436
+ }
437
+ if (result.code !== 0)
438
+ throw new Error(`Composio MCP sign-in did not complete (OpenClaw exit ${result.code}).`);
439
+ }
440
+ finally {
441
+ if (this.composioMcpLogin?.done === pending.done)
442
+ this.composioMcpLogin = undefined;
443
+ }
444
+ }
445
+ async signOutComposio() {
446
+ if (this.composioMcpLogin)
447
+ throw new Error('Cannot sign out while Composio MCP sign-in is pending.');
448
+ const { entry, env } = this.composioMcpContext();
449
+ const result = await this.runOwned(process.execPath, [entry, 'mcp', 'logout', 'composio'], env, 30_000);
450
+ if (result.code !== 0)
451
+ throw new Error(`Composio MCP sign-out failed (OpenClaw exit ${result.code}).`);
452
+ }
344
453
  // The engine's own Claude folder: the fallback login writes it and, only with claudeFallback, the gateway reads it.
345
454
  get claudeFolder() { return join(this.root, 'home', '.claude'); }
346
455
  doctorContext() {
@@ -350,7 +459,8 @@ export class Engine {
350
459
  OPENCLAW_STATE_DIR: join(this.root, 'state'), OPENCLAW_CONFIG_PATH: join(this.root, 'openclaw.json'),
351
460
  // Upstream eagerly imports/refreshes Codex OAuth. Only the CLI may see the person's login.
352
461
  CODEX_HOME: join(home, '.codex'),
353
- XDG_CACHE_HOME: join(home, '.cache'), ...this.claudeFallback ? { CLAUDE_CONFIG_DIR: this.claudeFolder } : {},
462
+ XDG_CACHE_HOME: join(home, '.cache'),
463
+ ...this.claudeFallback ? { CLAUDE_CONFIG_DIR: this.claudeFolder } : {},
354
464
  TMPDIR: join(this.root, 'tmp'), npm_config_cache: join(this.root, 'npm-cache'), npm_config_userconfig: join(this.root, 'npmrc'),
355
465
  OPENCLAW_NO_RESPAWN: '1', OPENCLAW_SKIP_CHANNELS: '1', OPENCLAW_DISABLE_BONJOUR: '1',
356
466
  OPENCLAW_EXEC_SHELL_SNAPSHOT: '0', OPENCLAW_LOAD_SHELL_ENV: '0', OPENCLAW_GATEWAY_TOKEN: this.token,
@@ -679,6 +789,7 @@ export class Engine {
679
789
  await this.starting?.catch(() => { });
680
790
  const child = this.child;
681
791
  await this.started.terminate(delay);
792
+ this.composioMcpLogin = undefined;
682
793
  if (child && child.exitCode === null && child.signalCode === null)
683
794
  throw new Error('engine did not stop; credential store still in use');
684
795
  this.child = undefined;
package/dist/kit.d.ts CHANGED
@@ -131,6 +131,14 @@ export declare class OpenClawKit {
131
131
  }>;
132
132
  private signInCtx;
133
133
  routes(): RouteView[];
134
+ /** Start Composio's hosted MCP OAuth flow through the pinned engine; return the URL for the app to open. */
135
+ beginComposioSignIn(): Promise<{
136
+ url: string;
137
+ }>;
138
+ /** Wait for OpenClaw's native MCP OAuth callback and token exchange to finish. */
139
+ completeComposioSignIn(): Promise<void>;
140
+ /** Clear the engine-owned native MCP OAuth credential for Composio. */
141
+ signOutComposio(): Promise<void>;
134
142
  /** B7 (5.7): install one external provider plugin a `needs_plugin` route names into this kit's own engine, at its
135
143
  * pin; resolves once the restarted gateway lists it enabled at that version. Discovery never installs. Serialized:
136
144
  * concurrent installs would race the allow-list update and double-spawn the installer into one engine tree. */
package/dist/kit.js CHANGED
@@ -1089,6 +1089,17 @@ export class OpenClawKit {
1089
1089
  routes() {
1090
1090
  return routeTable({ plugins: installedPlugins(this.engine.root) });
1091
1091
  }
1092
+ /** Start Composio's hosted MCP OAuth flow through the pinned engine; return the URL for the app to open. */
1093
+ async beginComposioSignIn() {
1094
+ await this.patchConfig({ mcp: { servers: { composio: {
1095
+ url: 'https://connect.composio.dev/mcp', transport: 'streamable-http', auth: 'oauth', enabled: true,
1096
+ } } } });
1097
+ return this.engine.beginComposioMcpLogin();
1098
+ }
1099
+ /** Wait for OpenClaw's native MCP OAuth callback and token exchange to finish. */
1100
+ completeComposioSignIn() { return this.engine.completeComposioMcpLogin(); }
1101
+ /** Clear the engine-owned native MCP OAuth credential for Composio. */
1102
+ signOutComposio() { return this.engine.signOutComposio(); }
1092
1103
  /** B7 (5.7): install one external provider plugin a `needs_plugin` route names into this kit's own engine, at its
1093
1104
  * pin; resolves once the restarted gateway lists it enabled at that version. Discovery never installs. Serialized:
1094
1105
  * concurrent installs would race the allow-list update and double-spawn the installer into one engine tree. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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",
@@ -60,7 +60,7 @@
60
60
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
61
61
  },
62
62
  "dependencies": {
63
- "@byokit/accounts": "0.28.0",
63
+ "@byokit/accounts": "0.28.1",
64
64
  "@byokit/pair": "0.10.4",
65
65
  "@byokit/relay": "0.8.2",
66
66
  "@byokit/discover": "0.8.4",