diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f3ca1b4202b..f3c09d2bcf08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- **Buzz plugin packaging:** keep the live QA runner on the shipped QA runner SDK surface and remove the obsolete package shrinkwrap so standalone npm and ClawHub package builds use current host exports and dependency resolutions. Thanks @shakkernerd. - **Control UI sharing connection isolation:** discard stale visibility and membership mutation results after switching gateways or accounts so previous-connection refreshes and errors cannot update the replacement connection. Fixes #116800. Thanks @shakkernerd. - **Control UI session refreshes:** preserve explicitly queued list filters and background hydration across later Gateway event invalidation, while keeping append pagination followed by a canonical refresh. Fixes #116697. Thanks @shakkernerd. - **Gateway device clock skew:** sign device proofs with the Gateway-issued challenge timestamp across TypeScript, Control UI, browser extension, Android, Apple, Linux, and watchOS clients so incorrect local clocks no longer block authentication, while retaining no-challenge compatibility for pre-challenge Control UI servers and older watch-node HTTP endpoints and keeping nonce binding and freshness checks enforced. Fixes #103455. diff --git a/extensions/buzz/npm-shrinkwrap.json b/extensions/buzz/npm-shrinkwrap.json deleted file mode 100644 index 9149cb446a20..000000000000 --- a/extensions/buzz/npm-shrinkwrap.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "name": "@openclaw/buzz", - "version": "2026.7.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@openclaw/buzz", - "version": "2026.7.2", - "dependencies": { - "nostr-tools": "2.23.12", - "zod": "4.4.3" - }, - "peerDependencies": { - "openclaw": ">=2026.7.2" - }, - "peerDependenciesMeta": { - "openclaw": { - "optional": true - } - } - }, - "node_modules/@noble/ciphers": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", - "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", - "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", - "license": "MIT", - "dependencies": { - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", - "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nostr-tools": { - "version": "2.23.12", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.12.tgz", - "integrity": "sha512-dLE9r0b4pCmrOKLUPD0KhUj9IXeh6RwiYoonEWeBh2AaDmlczUjJqJC8pQfggXzKUZ7+uZvhcrKkfwuZk0/e1g==", - "license": "Unlicense", - "dependencies": { - "@noble/ciphers": "2.1.1", - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0", - "@scure/bip32": "2.0.1", - "@scure/bip39": "2.0.1", - "nostr-wasm": "0.1.0" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/nostr-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "license": "MIT" - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/extensions/buzz/src/qa/cli.test.ts b/extensions/buzz/src/qa/cli.test.ts index 430ceabb03b0..4281c79f6fb2 100644 --- a/extensions/buzz/src/qa/cli.test.ts +++ b/extensions/buzz/src/qa/cli.test.ts @@ -1,13 +1,13 @@ import { Command } from "commander"; -import type { LiveTransportQaSuiteCommandOptions } from "openclaw/plugin-sdk/qa-runtime"; +import type { LiveTransportQaSuiteCommandOptions } from "openclaw/plugin-sdk/qa-runner-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; const runLiveTransportQaSuiteCommand = vi.hoisted(() => vi.fn<(params: LiveTransportQaSuiteCommandOptions) => Promise>(async () => {}), ); -vi.mock("openclaw/plugin-sdk/qa-runtime", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("openclaw/plugin-sdk/qa-runner-runtime", async (importOriginal) => ({ + ...(await importOriginal()), runLiveTransportQaSuiteCommand, })); diff --git a/extensions/buzz/src/qa/cli.ts b/extensions/buzz/src/qa/cli.ts index 61f0fff4cb78..8045b9ea2c52 100644 --- a/extensions/buzz/src/qa/cli.ts +++ b/extensions/buzz/src/qa/cli.ts @@ -4,7 +4,7 @@ import { runLiveTransportQaSuiteCommand, type LiveTransportQaCliRegistration, type LiveTransportQaCommandOptions, -} from "openclaw/plugin-sdk/qa-runtime"; +} from "openclaw/plugin-sdk/qa-runner-runtime"; const DEFAULT_BUZZ_QA_SCENARIOS = ["channel-canary", "channel-mention-gating"] as const; diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 77fc1b86c532..18356f9be096 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -202,6 +202,193 @@ export type QaRunnerCliRegistration = { register(qa: Command): void; }; +/** Normalized options passed from live-transport QA CLIs into lane runners. */ +export type LiveTransportQaCommandOptions = { + repoRoot?: string; + outputDir?: string; + providerMode?: string; + primaryModel?: string; + alternateModel?: string; + fastMode?: boolean; + allowFailures?: boolean; + failFast?: boolean; + profile?: string; + scenarioIds?: string[]; + listScenarios?: boolean; + sutAccountId?: string; + credentialFile?: string; + credentialSource?: string; + credentialRole?: string; +}; + +export type LiveTransportQaSuiteCommandOptions = { + channelId: string; + credentialMode?: "env-only" | "shared-lease"; + defaultProviderMode: string; + envCredentialReason?: string; + laneLabel?: string; + options: LiveTransportQaCommandOptions; + selectScenarioIds: (params: { + profile?: string; + primaryModel: string; + providerMode: string; + scenarioIds?: readonly string[]; + }) => string[]; +}; + +type LiveTransportQaCommanderOptions = { + repoRoot?: string; + outputDir?: string; + providerMode?: string; + model?: string; + altModel?: string; + scenario?: string[]; + listScenarios?: boolean; + fast?: boolean; + allowFailures?: boolean; + failFast?: boolean; + profile?: string; + sutAccount?: string; + credentialFile?: string; + credentialSource?: string; + credentialRole?: string; +}; + +/** Commander registration hook for one live-transport QA subcommand. */ +export type LiveTransportQaCliRegistration = QaRunnerCliRegistration; + +/** Help text customizations for live credential source and role flags. */ +export type LiveTransportQaCredentialCliOptions = { + sourceDescription?: string; + roleDescription?: string; +}; + +/** Declarative command metadata and runner used to install a live-transport QA CLI. */ +export type LiveTransportQaCliRegistrationOptions = { + commandName: string; + credentialFileHelp?: string; + credentialOptions?: LiveTransportQaCredentialCliOptions; + defaultProviderMode: string; + description: string; + providerModeHelp: string; + listScenariosHelp?: string; + outputDirHelp: string; + profileHelp?: string; + failFastHelp?: string; + allowFailuresHelp?: string; + scenarioHelp: string; + sutAccountHelp: string; + adapterFactory?: QaRunnerCliRegistration["adapterFactory"]; + run: (opts: LiveTransportQaCommandOptions) => Promise; +}; + +/** Memoize a lazy CLI runtime import so repeated command paths share one loaded module. */ +export function createLazyCliRuntimeLoader(load: () => Promise) { + let promise: Promise | null = null; + return async () => { + promise ??= load(); + return await promise; + }; +} + +function collectLiveTransportQaStringOption(value: string, previous: string[]) { + const trimmed = value.trim(); + return trimmed ? [...previous, trimmed] : previous; +} + +function mapLiveTransportQaCommanderOptions( + opts: LiveTransportQaCommanderOptions, +): LiveTransportQaCommandOptions { + return { + repoRoot: opts.repoRoot, + outputDir: opts.outputDir, + providerMode: opts.providerMode, + primaryModel: opts.model, + alternateModel: opts.altModel, + fastMode: opts.fast, + allowFailures: opts.allowFailures, + failFast: opts.failFast, + profile: opts.profile, + scenarioIds: opts.scenario, + listScenarios: opts.listScenarios, + sutAccountId: opts.sutAccount, + credentialFile: opts.credentialFile, + credentialSource: opts.credentialSource, + credentialRole: opts.credentialRole, + }; +} + +function registerLiveTransportQaCli( + params: LiveTransportQaCliRegistrationOptions & { + qa: Command; + run: (opts: LiveTransportQaCommandOptions) => Promise; + }, +) { + const command = params.qa + .command(params.commandName) + .description(params.description) + .option("--repo-root ", "Repository root to target when running from a neutral cwd") + .option("--output-dir ", params.outputDirHelp) + .option("--provider-mode ", params.providerModeHelp, params.defaultProviderMode) + .option("--model ", "Primary provider/model ref") + .option("--alt-model ", "Alternate provider/model ref") + .option("--scenario ", params.scenarioHelp, collectLiveTransportQaStringOption, []) + .option("--fast", "Enable provider fast mode where supported", false); + + if (params.allowFailuresHelp) { + command.option("--allow-failures", params.allowFailuresHelp, false); + } + + command.option("--sut-account ", params.sutAccountHelp, "sut"); + + if (params.credentialFileHelp) { + command.option("--credential-file ", params.credentialFileHelp); + } + + if (params.listScenariosHelp) { + command.option("--list-scenarios", params.listScenariosHelp, false); + } + + if (params.profileHelp) { + command.option("--profile ", params.profileHelp); + } + + if (params.failFastHelp) { + command.option("--fail-fast", params.failFastHelp, false); + } + + if (params.credentialOptions) { + command.option( + "--credential-source ", + params.credentialOptions.sourceDescription ?? + "Credential source for live lanes: env or convex (default: env)", + ); + if (params.credentialOptions.roleDescription) { + command.option("--credential-role ", params.credentialOptions.roleDescription); + } + } + + command.action(async (opts: LiveTransportQaCommanderOptions) => { + await params.run(mapLiveTransportQaCommanderOptions(opts)); + }); +} + +/** Build a Commander registration object for one live-transport QA command. */ +export function createLiveTransportQaCliRegistration( + params: LiveTransportQaCliRegistrationOptions, +): LiveTransportQaCliRegistration { + return { + commandName: params.commandName, + adapterFactory: params.adapterFactory, + register(qa: Command) { + registerLiveTransportQaCli({ + ...params, + qa, + }); + }, + }; +} + type QaRunnerRuntimeSurface = { qaRunnerCliRegistrations?: readonly QaRunnerCliRegistration[]; }; @@ -215,6 +402,7 @@ type QaRuntimeSurface = { }, ) => string; startQaLiveLaneGateway: (...args: unknown[]) => Promise; + runLiveTransportQaSuiteCommand: (params: LiveTransportQaSuiteCommandOptions) => Promise; }; /** Resolved QA runner CLI contribution declared by plugin manifest metadata. */ @@ -278,6 +466,11 @@ export function isQaRuntimeAvailable(): boolean { } } +/** Run a plugin-owned transport adapter through QA Lab's shared suite host. */ +export async function runLiveTransportQaSuiteCommand(params: LiveTransportQaSuiteCommandOptions) { + return await loadQaRuntimeModule().runLiveTransportQaSuiteCommand(params); +} + function listDeclaredQaRunnerPlugins( env: NodeJS.ProcessEnv | undefined = resolvePrivateQaBundledPluginsEnv(), ): Array< diff --git a/src/plugin-sdk/qa-runtime.ts b/src/plugin-sdk/qa-runtime.ts index 1e3fe34732ca..53a300d03f47 100644 --- a/src/plugin-sdk/qa-runtime.ts +++ b/src/plugin-sdk/qa-runtime.ts @@ -4,16 +4,26 @@ import { createServer } from "node:net"; import path from "node:path"; // QA runtime helpers register and execute plugin QA scenarios from local files. import { toErrorObject } from "@openclaw/normalization-core/error-coercion"; -import type { Command } from "commander"; import { formatErrorMessage } from "./error-runtime.js"; import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; import { resolvePrivateQaBundledPluginsEnv } from "./private-qa-bundled-env.js"; import { runExec } from "./process-runtime.js"; -import type { QaRunnerCliRegistration } from "./qa-runner-runtime.js"; import { fetchWithSsrFGuard } from "./ssrf-runtime.js"; import { normalizeStringEntries } from "./string-coerce-runtime.js"; export { writeGatewayRestartIntentSync } from "../infra/restart-intent.js"; +export { + createLazyCliRuntimeLoader, + createLiveTransportQaCliRegistration, + runLiveTransportQaSuiteCommand, +} from "./qa-runner-runtime.js"; +export type { + LiveTransportQaCliRegistration, + LiveTransportQaCliRegistrationOptions, + LiveTransportQaCommandOptions, + LiveTransportQaCredentialCliOptions, + LiveTransportQaSuiteCommandOptions, +} from "./qa-runner-runtime.js"; type QaRuntimeSurface = { defaultQaRuntimeModelForMode: ( @@ -24,7 +34,6 @@ type QaRuntimeSurface = { }, ) => string; startQaLiveLaneGateway: (...args: unknown[]) => Promise; - runLiveTransportQaSuiteCommand: (params: LiveTransportQaSuiteCommandOptions) => Promise; }; function isMissingQaRuntimeError(error: unknown) { @@ -58,198 +67,6 @@ export function isQaRuntimeAvailable(): boolean { } } -/** Normalized options passed from live-transport QA CLIs into lane runners. */ -export type LiveTransportQaCommandOptions = { - repoRoot?: string; - outputDir?: string; - providerMode?: string; - primaryModel?: string; - alternateModel?: string; - fastMode?: boolean; - allowFailures?: boolean; - failFast?: boolean; - profile?: string; - scenarioIds?: string[]; - listScenarios?: boolean; - sutAccountId?: string; - credentialFile?: string; - credentialSource?: string; - credentialRole?: string; -}; - -export type LiveTransportQaSuiteCommandOptions = { - channelId: string; - credentialMode?: "env-only" | "shared-lease"; - defaultProviderMode: string; - envCredentialReason?: string; - laneLabel?: string; - options: LiveTransportQaCommandOptions; - selectScenarioIds: (params: { - profile?: string; - primaryModel: string; - providerMode: string; - scenarioIds?: readonly string[]; - }) => string[]; -}; - -/** Run a plugin-owned transport adapter through QA Lab's shared suite host. */ -export async function runLiveTransportQaSuiteCommand(params: LiveTransportQaSuiteCommandOptions) { - return await loadQaRuntimeModule().runLiveTransportQaSuiteCommand(params); -} - -type LiveTransportQaCommanderOptions = { - repoRoot?: string; - outputDir?: string; - providerMode?: string; - model?: string; - altModel?: string; - scenario?: string[]; - listScenarios?: boolean; - fast?: boolean; - allowFailures?: boolean; - failFast?: boolean; - profile?: string; - sutAccount?: string; - credentialFile?: string; - credentialSource?: string; - credentialRole?: string; -}; - -/** Commander registration hook for one live-transport QA subcommand. */ -export type LiveTransportQaCliRegistration = QaRunnerCliRegistration; - -/** Help text customizations for live credential source and role flags. */ -export type LiveTransportQaCredentialCliOptions = { - sourceDescription?: string; - roleDescription?: string; -}; - -/** Declarative command metadata and runner used to install a live-transport QA CLI. */ -export type LiveTransportQaCliRegistrationOptions = { - commandName: string; - credentialFileHelp?: string; - credentialOptions?: LiveTransportQaCredentialCliOptions; - defaultProviderMode: string; - description: string; - providerModeHelp: string; - listScenariosHelp?: string; - outputDirHelp: string; - profileHelp?: string; - failFastHelp?: string; - allowFailuresHelp?: string; - scenarioHelp: string; - sutAccountHelp: string; - adapterFactory?: QaRunnerCliRegistration["adapterFactory"]; - run: (opts: LiveTransportQaCommandOptions) => Promise; -}; - -/** Memoize a lazy CLI runtime import so repeated command paths share one loaded module. */ -export function createLazyCliRuntimeLoader(load: () => Promise) { - let promise: Promise | null = null; - return async () => { - promise ??= load(); - return await promise; - }; -} - -function collectLiveTransportQaStringOption(value: string, previous: string[]) { - const trimmed = value.trim(); - return trimmed ? [...previous, trimmed] : previous; -} - -function mapLiveTransportQaCommanderOptions( - opts: LiveTransportQaCommanderOptions, -): LiveTransportQaCommandOptions { - return { - repoRoot: opts.repoRoot, - outputDir: opts.outputDir, - providerMode: opts.providerMode, - primaryModel: opts.model, - alternateModel: opts.altModel, - fastMode: opts.fast, - allowFailures: opts.allowFailures, - failFast: opts.failFast, - profile: opts.profile, - scenarioIds: opts.scenario, - listScenarios: opts.listScenarios, - sutAccountId: opts.sutAccount, - credentialFile: opts.credentialFile, - credentialSource: opts.credentialSource, - credentialRole: opts.credentialRole, - }; -} - -function registerLiveTransportQaCli( - params: LiveTransportQaCliRegistrationOptions & { - qa: Command; - run: (opts: LiveTransportQaCommandOptions) => Promise; - }, -) { - const command = params.qa - .command(params.commandName) - .description(params.description) - .option("--repo-root ", "Repository root to target when running from a neutral cwd") - .option("--output-dir ", params.outputDirHelp) - .option("--provider-mode ", params.providerModeHelp, params.defaultProviderMode) - .option("--model ", "Primary provider/model ref") - .option("--alt-model ", "Alternate provider/model ref") - .option("--scenario ", params.scenarioHelp, collectLiveTransportQaStringOption, []) - .option("--fast", "Enable provider fast mode where supported", false); - - if (params.allowFailuresHelp) { - command.option("--allow-failures", params.allowFailuresHelp, false); - } - - command.option("--sut-account ", params.sutAccountHelp, "sut"); - - if (params.credentialFileHelp) { - command.option("--credential-file ", params.credentialFileHelp); - } - - if (params.listScenariosHelp) { - command.option("--list-scenarios", params.listScenariosHelp, false); - } - - if (params.profileHelp) { - command.option("--profile ", params.profileHelp); - } - - if (params.failFastHelp) { - command.option("--fail-fast", params.failFastHelp, false); - } - - if (params.credentialOptions) { - command.option( - "--credential-source ", - params.credentialOptions.sourceDescription ?? - "Credential source for live lanes: env or convex (default: env)", - ); - if (params.credentialOptions.roleDescription) { - command.option("--credential-role ", params.credentialOptions.roleDescription); - } - } - - command.action(async (opts: LiveTransportQaCommanderOptions) => { - await params.run(mapLiveTransportQaCommanderOptions(opts)); - }); -} - -/** Build a Commander registration object for one live-transport QA command. */ -export function createLiveTransportQaCliRegistration( - params: LiveTransportQaCliRegistrationOptions, -): LiveTransportQaCliRegistration { - return { - commandName: params.commandName, - adapterFactory: params.adapterFactory, - register(qa: Command) { - registerLiveTransportQaCli({ - ...params, - qa, - }); - }, - }; -} - /** Docker command runner abstraction used by QA Docker helpers and tests. */ export type QaDockerRunCommand = ( command: string,