build(protocol): make @openclaw/gateway-protocol publishable to npm (#111664)

* build(protocol): make @openclaw/gateway-protocol publishable to npm

* chore(mxc): align package version with release train

* fix(release): support gateway-only core packages
This commit is contained in:
Peter Steinberger
2026-07-19 23:18:24 -07:00
committed by GitHub
parent 8e40acbbb7
commit fef846a3f0
16 changed files with 720 additions and 75 deletions
+160 -40
View File
@@ -201,28 +201,44 @@ jobs:
if: steps.dist_build_cache.outputs.cache-hit != 'true'
run: pnpm build
- name: Pack AI runtime package
id: ai_runtime_tarballs
- name: Pack publishable core packages
id: core_package_tarballs
env:
CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol
run: |
set -euo pipefail
if ! node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
echo "Frozen target does not depend on @openclaw/ai; packing OpenClaw without a separate AI runtime tarball."
exit 0
fi
if [[ ! -f packages/ai/package.json ]]; then
echo "OpenClaw depends on @openclaw/ai but packages/ai/package.json is missing." >&2
exit 1
fi
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
package_version="$(node -p "require('./packages/ai/package.json').version")"
if [[ "$package_version" != "$PACKAGE_VERSION" ]]; then
echo "packages/ai version ${package_version} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-ai-runtime-packages"
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-core-packages"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
pnpm --dir packages/ai pack --pack-destination "$ARTIFACT_DIR"
for package_dir in $CORE_PACKAGE_DIRS; do
if [[ "$package_dir" == "packages/ai" ]] && ! node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
echo "Frozen target does not depend on @openclaw/ai; packing OpenClaw without a separate AI runtime tarball."
continue
fi
if [[ ! -f "$package_dir/package.json" ]]; then
if [[ "$package_dir" == "packages/ai" ]]; then
echo "Publishable core package manifest is missing: $package_dir/package.json" >&2
exit 1
fi
echo "Frozen target does not contain $package_dir; skipping its core package tarball."
continue
fi
if [[ "$package_dir" != "packages/ai" ]] && ! node -e 'const pkg = require(process.argv[1]); process.exit(pkg.openclaw?.release?.publishToNpm === true ? 0 : 1)' "./$package_dir/package.json"; then
echo "Frozen target does not publish $package_dir; skipping its core package tarball."
continue
fi
package_version="$(node -p "require('./$package_dir/package.json').version")"
if [[ "$package_version" != "$PACKAGE_VERSION" ]]; then
echo "$package_dir version ${package_version} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
pnpm --dir "$package_dir" pack --pack-destination "$ARTIFACT_DIR"
done
if ! find "$ARTIFACT_DIR" -maxdepth 1 -type f -name '*.tgz' -print -quit | grep -q .; then
echo "dir=" >> "$GITHUB_OUTPUT"
exit 0
fi
(cd "$ARTIFACT_DIR" && sha256sum ./*.tgz > SHA256SUMS)
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
@@ -293,7 +309,7 @@ jobs:
# IT BELONGS IN openclaw-release-checks.yml INSTEAD OF BLOCKING npm PUBLISH.
- name: Verify release contents
env:
OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR: ${{ steps.ai_runtime_tarballs.outputs.dir }}
OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR: ${{ steps.core_package_tarballs.outputs.dir }}
run: pnpm release:check
- name: Exercise all extended-stable plugin npm packages
@@ -365,18 +381,35 @@ jobs:
RELEASE_REF: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
AI_RUNTIME_TARBALL_DIR: ${{ steps.ai_runtime_tarballs.outputs.dir }}
CORE_PACKAGE_TARBALL_DIR: ${{ steps.core_package_tarballs.outputs.dir }}
run: |
set -euo pipefail
AI_TARBALL_PATH=""
if [[ -n "$AI_RUNTIME_TARBALL_DIR" ]]; then
mapfile -t AI_TARBALLS < <(find "$AI_RUNTIME_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | sort)
if [[ "${#AI_TARBALLS[@]}" -ne 1 ]]; then
echo "Expected exactly one prepared @openclaw/ai tarball, found ${#AI_TARBALLS[@]}." >&2
GATEWAY_PROTOCOL_TARBALL_PATH=""
if [[ -n "$CORE_PACKAGE_TARBALL_DIR" ]]; then
mapfile -t AI_TARBALLS < <(find "$CORE_PACKAGE_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | sort)
if node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
if [[ "${#AI_TARBALLS[@]}" -ne 1 ]]; then
echo "Expected exactly one prepared @openclaw/ai tarball, found ${#AI_TARBALLS[@]}." >&2
exit 1
fi
AI_TARBALL_PATH="${AI_TARBALLS[0]}"
node --import tsx scripts/prepare-openclaw-npm-shrinkwrap.ts "$AI_TARBALL_PATH"
elif [[ "${#AI_TARBALLS[@]}" -ne 0 ]]; then
echo "Frozen target without an @openclaw/ai dependency contains unexpected AI runtime artifacts." >&2
exit 1
fi
mapfile -t GATEWAY_PROTOCOL_TARBALLS < <(find "$CORE_PACKAGE_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-gateway-protocol-*.tgz' -print | sort)
if [[ -f packages/gateway-protocol/package.json ]] && node -e 'const pkg = require("./packages/gateway-protocol/package.json"); process.exit(pkg.openclaw?.release?.publishToNpm === true ? 0 : 1)'; then
if [[ "${#GATEWAY_PROTOCOL_TARBALLS[@]}" -ne 1 ]]; then
echo "Expected exactly one prepared @openclaw/gateway-protocol tarball, found ${#GATEWAY_PROTOCOL_TARBALLS[@]}." >&2
exit 1
fi
GATEWAY_PROTOCOL_TARBALL_PATH="${GATEWAY_PROTOCOL_TARBALLS[0]}"
elif [[ "${#GATEWAY_PROTOCOL_TARBALLS[@]}" -ne 0 ]]; then
echo "Frozen target without a publishable @openclaw/gateway-protocol package contains unexpected artifacts." >&2
exit 1
fi
AI_TARBALL_PATH="${AI_TARBALLS[0]}"
node --import tsx scripts/prepare-openclaw-npm-shrinkwrap.ts "$AI_TARBALL_PATH"
fi
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
pnpm pack --json 2>&1 | tee "$PACK_OUTPUT"
@@ -472,6 +505,9 @@ jobs:
AI_TARBALL_NAME=""
AI_TARBALL_SHA256=""
AI_PACKAGE_VERSION=""
GATEWAY_PROTOCOL_TARBALL_NAME=""
GATEWAY_PROTOCOL_TARBALL_SHA256=""
GATEWAY_PROTOCOL_PACKAGE_VERSION=""
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
@@ -487,14 +523,28 @@ jobs:
echo "Prepared @openclaw/ai version ${AI_PACKAGE_VERSION} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
cp "$AI_TARBALL_PATH" "$ARTIFACT_DIR/"
cp "$AI_RUNTIME_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/ai-runtime-SHA256SUMS"
fi
if [[ -n "$GATEWAY_PROTOCOL_TARBALL_PATH" ]]; then
GATEWAY_PROTOCOL_TARBALL_NAME="$(basename "$GATEWAY_PROTOCOL_TARBALL_PATH")"
GATEWAY_PROTOCOL_TARBALL_SHA256="$(sha256sum "$GATEWAY_PROTOCOL_TARBALL_PATH" | awk '{print $1}')"
GATEWAY_PROTOCOL_PACKAGE_VERSION="$(
tar -xOf "$GATEWAY_PROTOCOL_TARBALL_PATH" package/package.json |
node -e 'let raw = ""; process.stdin.on("data", (chunk) => (raw += chunk)); process.stdin.on("end", () => process.stdout.write(JSON.parse(raw).version));'
)"
if [[ "$GATEWAY_PROTOCOL_PACKAGE_VERSION" != "$PACKAGE_VERSION" ]]; then
echo "Prepared @openclaw/gateway-protocol version ${GATEWAY_PROTOCOL_PACKAGE_VERSION} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
fi
if [[ -n "$CORE_PACKAGE_TARBALL_DIR" ]]; then
cp "$CORE_PACKAGE_TARBALL_DIR"/*.tgz "$ARTIFACT_DIR/"
cp "$CORE_PACKAGE_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/core-packages-SHA256SUMS"
fi
cp -R "$DEPENDENCY_EVIDENCE_DIR" "$ARTIFACT_DIR/dependency-evidence"
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
printf '%s\n' "$RELEASE_NPM_DIST_TAG" > "$ARTIFACT_DIR/release-npm-dist-tag.txt"
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" AI_PACKAGE_VERSION="$AI_PACKAGE_VERSION" AI_TARBALL_NAME="$AI_TARBALL_NAME" AI_TARBALL_SHA256="$AI_TARBALL_SHA256" node <<'NODE'
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" AI_PACKAGE_VERSION="$AI_PACKAGE_VERSION" AI_TARBALL_NAME="$AI_TARBALL_NAME" AI_TARBALL_SHA256="$AI_TARBALL_SHA256" GATEWAY_PROTOCOL_PACKAGE_VERSION="$GATEWAY_PROTOCOL_PACKAGE_VERSION" GATEWAY_PROTOCOL_TARBALL_NAME="$GATEWAY_PROTOCOL_TARBALL_NAME" GATEWAY_PROTOCOL_TARBALL_SHA256="$GATEWAY_PROTOCOL_TARBALL_SHA256" node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const manifest = {
@@ -506,6 +556,28 @@ jobs:
packageVersion: process.env.PACKAGE_VERSION,
tarballName: process.env.TARBALL_NAME,
tarballSha256: process.env.TARBALL_SHA256,
corePackageTarballs: [
...(process.env.AI_TARBALL_NAME
? [
{
packageName: "@openclaw/ai",
packageVersion: process.env.AI_PACKAGE_VERSION,
tarballName: process.env.AI_TARBALL_NAME,
tarballSha256: process.env.AI_TARBALL_SHA256,
},
]
: []),
...(process.env.GATEWAY_PROTOCOL_TARBALL_NAME
? [
{
packageName: "@openclaw/gateway-protocol",
packageVersion: process.env.GATEWAY_PROTOCOL_PACKAGE_VERSION,
tarballName: process.env.GATEWAY_PROTOCOL_TARBALL_NAME,
tarballSha256: process.env.GATEWAY_PROTOCOL_TARBALL_SHA256,
},
]
: []),
],
dependencyTarballs: process.env.AI_TARBALL_NAME
? [
{
@@ -959,16 +1031,68 @@ jobs:
echo "Prepared preflight tarball digest mismatch." >&2
exit 1
fi
if node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
if [[ ! -f preflight-tarball/ai-runtime-SHA256SUMS ]]; then
echo "Prepared AI runtime dependency checksums are missing." >&2
if ! jq -e '.corePackageTarballs | type == "array"' "$MANIFEST_FILE" >/dev/null; then
echo "Prepared core package metadata is missing." >&2
exit 1
fi
CORE_PACKAGE_TARBALL_COUNT="$(jq -r '.corePackageTarballs | length' "$MANIFEST_FILE")"
if [[ "$CORE_PACKAGE_TARBALL_COUNT" -gt 0 ]]; then
if [[ ! -f preflight-tarball/core-packages-SHA256SUMS ]]; then
echo "Prepared core package checksums are missing." >&2
exit 1
fi
(cd preflight-tarball && sha256sum -c ai-runtime-SHA256SUMS)
elif [[ -f preflight-tarball/ai-runtime-SHA256SUMS ]] || find preflight-tarball -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print -quit | grep -q .; then
(cd preflight-tarball && sha256sum -c core-packages-SHA256SUMS)
fi
EXPECTED_PACKAGE_VERSION="$(node -p "require('./package.json').version")"
SEEN_AI_TARBALL="false"
SEEN_GATEWAY_PROTOCOL_TARBALL="false"
while IFS=$'\t' read -r package_name package_version tarball_name tarball_sha256; do
if [[ -z "$tarball_name" || "$tarball_name" != "$(basename -- "$tarball_name")" || "$tarball_name" != *.tgz ]]; then
echo "Prepared core package tarball name is unsafe: $tarball_name" >&2
exit 1
fi
case "$package_name" in
"@openclaw/ai")
[[ "$SEEN_AI_TARBALL" == "false" ]] || { echo "Prepared core package manifest contains duplicate @openclaw/ai metadata." >&2; exit 1; }
SEEN_AI_TARBALL="true"
;;
"@openclaw/gateway-protocol")
[[ "$SEEN_GATEWAY_PROTOCOL_TARBALL" == "false" ]] || { echo "Prepared core package manifest contains duplicate @openclaw/gateway-protocol metadata." >&2; exit 1; }
SEEN_GATEWAY_PROTOCOL_TARBALL="true"
;;
*)
echo "Prepared core package manifest contains unsupported package: $package_name" >&2
exit 1
;;
esac
if [[ "$package_version" != "$EXPECTED_PACKAGE_VERSION" ]]; then
echo "Prepared ${package_name} version ${package_version} does not match openclaw ${EXPECTED_PACKAGE_VERSION}." >&2
exit 1
fi
core_tarball_path="preflight-tarball/$tarball_name"
if [[ ! -f "$core_tarball_path" ]] || [[ "$(sha256sum "$core_tarball_path" | awk '{print $1}')" != "$tarball_sha256" ]]; then
echo "Prepared ${package_name} tarball is missing or has a digest mismatch." >&2
exit 1
fi
done < <(jq -r '.corePackageTarballs[] | [.packageName, .packageVersion, .tarballName, .tarballSha256] | @tsv' "$MANIFEST_FILE")
if node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
if [[ "$SEEN_AI_TARBALL" != "true" ]] || ! jq -e '.dependencyTarballs[] | select(.packageName == "@openclaw/ai")' "$MANIFEST_FILE" >/dev/null; then
echo "Prepared AI runtime dependency tarball is missing from the manifest." >&2
exit 1
fi
elif jq -e '.dependencyTarballs[] | select(.packageName == "@openclaw/ai")' "$MANIFEST_FILE" >/dev/null || find preflight-tarball -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print -quit | grep -q .; then
echo "Frozen target without an @openclaw/ai dependency contains unexpected AI runtime artifacts." >&2
exit 1
fi
if [[ -f packages/gateway-protocol/package.json ]] && node -e 'const pkg = require("./packages/gateway-protocol/package.json"); process.exit(pkg.openclaw?.release?.publishToNpm === true ? 0 : 1)'; then
if [[ "$SEEN_GATEWAY_PROTOCOL_TARBALL" != "true" ]]; then
echo "Prepared @openclaw/gateway-protocol tarball is missing from the manifest." >&2
exit 1
fi
elif jq -e '.corePackageTarballs[] | select(.packageName == "@openclaw/gateway-protocol")' "$MANIFEST_FILE" >/dev/null || find preflight-tarball -maxdepth 1 -type f -name 'openclaw-gateway-protocol-*.tgz' -print -quit | grep -q .; then
echo "Frozen target without a publishable @openclaw/gateway-protocol package contains unexpected artifacts." >&2
exit 1
fi
echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
@@ -1049,13 +1173,9 @@ jobs:
fi
bash scripts/openclaw-npm-publish.sh --publish "./${tarball_path}"
}
ai_tarball="$(find preflight-tarball -type f -name 'openclaw-ai-*.tgz' -print | head -n 1)"
if [[ -n "$ai_tarball" ]]; then
publish_if_missing "@openclaw/ai" "$ai_tarball"
elif node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
echo "Prepared tarball for @openclaw/ai was not found." >&2
exit 1
fi
while IFS=$'\t' read -r package_name tarball_name; do
publish_if_missing "$package_name" "preflight-tarball/$tarball_name"
done < <(jq -r '.corePackageTarballs[] | [.packageName, .tarballName] | @tsv' preflight-tarball/preflight-manifest.json)
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
- name: Verify extended-stable registry readback
+1
View File
@@ -247,6 +247,7 @@ apps/swabble/build/
# Generated protocol schema (produced via pnpm protocol:gen)
dist/protocol.schema.json
packages/gateway-protocol/protocol.schema.json
.ant-colony/
# Eclipse
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@openclaw/mxc-sandbox",
"version": "2026.6.11",
"version": "2026.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/mxc-sandbox",
"version": "2026.6.11",
"version": "2026.7.2",
"dependencies": {
"@microsoft/mxc-sdk": "0.7.0",
"zod": "4.4.3"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@openclaw/mxc-sandbox",
"version": "2026.6.11",
"version": "2026.7.2",
"description": "OpenClaw MXC sandbox execution plugin for MXC-capable hosts",
"repository": {
"type": "git",
@@ -24,10 +24,10 @@
"minHostVersion": ">=2026.6.11"
},
"compat": {
"pluginApi": ">=2026.6.11"
"pluginApi": ">=2026.7.2"
},
"build": {
"openclawVersion": "2026.6.11",
"openclawVersion": "2026.7.2",
"bundledDist": false,
"staticAssets": [
{
+143
View File
@@ -0,0 +1,143 @@
# Changelog — @openclaw/gateway-protocol
Wire-protocol and schema contracts for the OpenClaw Gateway (WebSocket JSON-RPC-style
frames, handshake, and method/event payload schemas). Protocol version is negotiated
per connection via `minProtocol`/`maxProtocol`. This log covers the wire protocol
version and the additive schema surface. Dates are authoring dates (2026).
## Unreleased
- Rename structured-question item `id` to `questionId` and flatten keyed answer arrays.
- Slim worker and session-catalog payloads to the active wire contract.
- Remove dead protocol surfaces and add since-vintage metadata to retained schemas and methods.
## Protocol v4 (current)
Introduced 2026-05-07 (commit `330ba1f`). The stable, current wire version.
Wire contract:
- Frame envelopes: `req` / `res` / `event`, discriminated on `type`.
- Handshake: client sends `ConnectParams` (advertises `minProtocol`/`maxProtocol`,
client identity, `caps`, `commands`, `permissions`, `role`/`scopes`, optional signed
`device` identity, and an `auth` bag: token / bootstrapToken / deviceToken / password /
approvalRuntimeToken / agentRuntimeIdentityToken).
- Server replies `hello-ok` with the negotiated `protocol`, server identity, the live
`features` map (`methods`, `events`, `capabilities`), initial `snapshot`, minted `auth`
device tokens, and connection `policy` (maxPayload / maxBufferedBytes / tickIntervalMs).
- Server events: `tick` heartbeat and `shutdown` notice; event frames may carry `seq`
and `stateVersion` for ordered state sync.
Changed vs v3:
- `hello-ok` handshake replaced the single `canvasHostUrl` string with a
`pluginSurfaceUrls` map (canvas generalized into arbitrary plugin surfaces). This
field change is the breaking bump behind v4.
- Later additively extended (no bump) with `controlUiTabs` (plugin-declared Control UI
tabs) and multi-`deviceTokens` in the handshake auth block.
Compatibility window (`version.ts`):
- `MIN_CLIENT_PROTOCOL_VERSION = 4` — general clients must speak v4.
- `MIN_NODE_PROTOCOL_VERSION = 3` and `MIN_PROBE_PROTOCOL_VERSION = 3` — authenticated
nodes and lightweight probes are accepted at N-1 (v3) to stay manageable during
rolling upgrades. Added 2026-07-06 (#101109).
- A transient v5 (2026-05-16, `07f05e9`) renamed `inboundTurnKind` -> `inboundEventKind`;
it was reverted the next day (2026-05-17, `ad155fb`, "restore v4 message action
protocol"). v5 never shipped as a stable ceiling; v4 remains current.
## Protocol v3
Baseline wire version. Present since repo genesis (2026-04-21) as an inline literal in
`protocol-schemas.ts`; extracted into `version.ts` unchanged on 2026-05-04 (`2949171`).
The first externally-relevant version — there is no 2->3 bump in tracked history.
Established the still-current shape: `req`/`res`/`event` frame envelopes, the
`ConnectParams` -> `hello-ok` handshake with protocol negotiation, `snapshot` state sync,
and the founding method/event families: sessions, agent chat, cron, devices, nodes,
channels, config, commands, logs-chat, exec-approvals, plugin-approvals, secrets, push,
wizard. (v3 `hello-ok` carried `canvasHostUrl`; v4 replaced it — see above.)
## Schema surface history
Additive method/event/schema families added over time. Pure refactors, test-only, and
docs commits are omitted. The package was extracted from `src/gateway/protocol` to
`packages/gateway-protocol` on 2026-05-29 (#87797); paths before that date lived under
the old tree.
### 2026-04 (v3 baseline era)
- Ship baseline families: frames/handshake, sessions, agent chat, cron, devices, nodes,
channels, config, commands, logs-chat, exec-approvals, plugin-approvals, secrets, push,
wizard, snapshot, primitives, agents-models-skills.
- Add WhatsApp `replyToMode` quoting (#62305).
- Add browser realtime Talk and transports — origin of the talk/voice families.
- Add Control UI PWA web push support (#44590).
- Add plugins and artifacts schema modules.
- Add OpenClaw SDK package and authenticated iOS background presence beacon (#73330).
### 2026-05
- Add environments discovery RPCs (#74867) and task-ledger RPCs (#74847) — tasks family.
- Add unified Talk gateway sessions, realtime active-run control, and typed `sessionKey`
on the wake protocol.
- Add SDK `tools.invoke` RPC; extend cron with agentId filtering (#77602), run
diagnostics (#75928), and direct job lookup.
- Add Skill Workshop gateway methods: proposal files, revision requests, persisted origin.
- Add core session goals (#87469).
- Add heartbeat flag on agent event broadcast (#80610), warm-MCP effective inventory, and
plugin approval action metadata.
- Harden auth/device identity: bind approval access to requester metadata (#81380);
require approval for setup-code device pairing (#81292); scope Talk session to resolver.
- Introduce and revert transient protocol v5 (`07f05e9` / `ad155fb`).
### 2026-06
Enhancement-only month (no new schema modules):
- Extend cron with command jobs, compact list responses (#93395), and an on-exit
schedule kind that fires when a watched command exits.
- Forward-port fast-Talks auto mode (#85104); add session workspace rail (#92856).
### 2026-07 (largest expansion)
- Add terminal family: `terminal.*` RPC methods/events, detach/reattach with output
replay, `terminal.list`/`terminal.text`, and file uploads into terminals (#107364).
- Add managed git worktrees: lifecycle create/provision/snapshot/restore/GC (#100535),
new-session-in-worktree (#100788), session worktree targeting and branch listing
(#103432); add read-only `agents.workspace` browsing RPCs (#100738).
- Add audit family: metadata-only message audit events (#103903), native-search audit
correlation (#98704), and audit-activity schema.
- Add `tts.speak` returning synthesized audio inline (#100770).
- Add cooperative host suspension / gateway-suspend prepare/status/resume RPCs (#103618).
- Add durable approvals: persisted operator approvals (#103579), typed cross-surface
approval actions (#103679), approval-id, and the durable-approvals stack (#104837).
- Add cloud-workers stack: durable environments + lifecycle RPCs (#104401), worker bundle
+ SSH bootstrap + admission handshake (#104532), authenticated worker protocol with
minted credentials (#104688), durable transcript commit (#104809), live-event streaming
(#105275), inference proxy (#105719), and session placement/dispatch (#106332).
- Add session catalog: sessions-catalog + sessions-create with external-session
pagination unification (#104717).
- Add fs family: `sessions.files.set` hash-CAS writes (#104757) and gateway/node folder
browsing (#105114).
- Add node-hosted plugins — dynamic tools, MCP servers, skills (#90431) — plus node
invoke/presence protocol schemas.
- Add migrations family: log-migration protocol schemas and Codex/Claude memory import
(#106406).
- Add durable device rename for human-friendly device names (#94517).
- Add follow-up task suggestions (#102422) and task-suggestions schema.
- Add cron event triggers via polled condition-watcher scripts (#101195) and native
mobile Automations parity (#106355).
- Add system-agent conversational onboarding (#99935); rename `crestodian.*` methods to
`openclaw.chat` / `openclaw.setup.*` (2026-07-14, `a6a0716`).
- Add typed structured questions / `ask_user` with live option cards (#109922, #110242)
and the questions schema module.
- Add ui-command / screen-tool Control UI layout control and capability-gated
`show_widget` inline web chat widgets (#101840).
- Add direct watch/watchOS node connect to Gateway (#102893); widen node/probe protocol
acceptance to N-1 (#101109).
## Notes for external versioning
- Post-v4 changes are additive except: the transient v5 rename (reverted, net-zero) and
the 2026-07-14 `crestodian.*` -> `openclaw.*` method rename. The renamed feature was
introduced only 9 days earlier (2026-07-05, #99935), never left the v4 window, and
predates public publish, so no released method name changed under v4.
- `schema/types.ts` was removed 2026-07-11 (#103679); it re-exported compile-time type
aliases only and has no wire impact.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 OpenClaw Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+158
View File
@@ -0,0 +1,158 @@
# `@openclaw/gateway-protocol`
Typed schemas, inferred TypeScript types, and runtime validators for the OpenClaw
Gateway WebSocket protocol.
The current wire protocol is version 4. General clients must use v4; authenticated
node clients and lightweight probes may use the N-1 window during rolling upgrades.
See the [Gateway protocol specification](https://docs.openclaw.ai/gateway/protocol)
for transport, authentication, roles, scopes, and complete frame examples.
## Versioning
Package versions follow the OpenClaw calendar release train:
`YYYY.M.PATCH`, with the same prerelease suffix when applicable. A package version
therefore identifies the OpenClaw source release that produced the schemas; it is
not the wire protocol number.
The wire protocol integer is versioned separately. Its current value is exported
as `PROTOCOL_VERSION` from `@openclaw/gateway-protocol/version`. Gateway protocol
changes are additive first. An incompatible wire change requires an explicit
protocol-version decision and coordinated client follow-through. See
[`CHANGELOG.md`](./CHANGELOG.md) for the wire and schema history.
## Install
```bash
npm install @openclaw/gateway-protocol
```
## Entry points
- `@openclaw/gateway-protocol` exports runtime validators, selected schemas, error
formatting, and their TypeScript types. This is the main TypeBox-backed entry.
- `@openclaw/gateway-protocol/schema` exports the TypeBox schema graph, including
the `ProtocolSchemas` registry used by generators.
- `@openclaw/gateway-protocol/frame-guards` exports dependency-free structural
guards for gateway event and response envelopes.
- `@openclaw/gateway-protocol/client-info` exports client ID, mode, and capability
registries plus normalization helpers.
- `@openclaw/gateway-protocol/connect-error-details` exports structured connect
error readers and recovery metadata.
- `@openclaw/gateway-protocol/gateway-error-details` exports helpers for reading
structured details from general gateway errors.
- `@openclaw/gateway-protocol/startup-unavailable` exports startup retry constants
and helpers.
- `@openclaw/gateway-protocol/version` exports the current and minimum accepted
protocol versions.
The `frame-guards`, `client-info`, `connect-error-details`, `gateway-error-details`,
`startup-unavailable`, and `version` entry points are TypeBox-free. Prefer them when
a browser bundle only needs envelope dispatch, handshake constants, or reconnect
policy. This also avoids runtime compilation in CSP-sensitive consumers. The root
and `schema` entry points provide the full validation surface and depend on TypeBox.
## Validate an inbound frame
The compiled validators are callable type guards. Their `errors` property contains
the most recent validation errors.
```ts
import { formatValidationErrors, validateRequestFrame } from "@openclaw/gateway-protocol";
const frame: unknown = JSON.parse(inboundText);
if (!validateRequestFrame(frame)) {
throw new Error(formatValidationErrors(validateRequestFrame.errors));
}
console.log(frame.id, frame.method);
```
`validateRequestFrame` validates the request envelope. Dispatch code must also use
the validator for the selected method's `params`; the root entry point exports those
validators as `validate*Params` functions.
## Guard an event without TypeBox
Use the lightweight guards when code only needs safe frame discrimination. They
check dispatch-critical envelope fields and intentionally allow additive payload
fields.
```ts
import { isGatewayEventFrame } from "@openclaw/gateway-protocol/frame-guards";
const frame: unknown = JSON.parse(inboundText);
if (isGatewayEventFrame(frame)) {
console.log(frame.event, frame.seq);
}
```
## Build handshake version and capability fields
Protocol levels and client capabilities live in TypeBox-free entry points.
```ts
import { GATEWAY_CLIENT_CAPS } from "@openclaw/gateway-protocol/client-info";
import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";
const handshake = {
minProtocol: MIN_CLIENT_PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
};
```
Nodes and probes use `MIN_NODE_PROTOCOL_VERSION` and
`MIN_PROBE_PROTOCOL_VERSION`, respectively. A capability advertises client support;
it does not grant authorization.
## Contract notes
### Session identifiers
Several identifier names coexist because they identify different things:
- `key` is the established logical session selector used by most `sessions.*`
CRUD, send, subscription, patch, reset, delete, compaction, and usage methods.
A key can be canonicalized or resolved within an agent's session store.
- `sessionKey` names the same logical routing identity where the contract needs to
make that meaning explicit. `chat.*`, session file and diff APIs, transcript
branch/rewind/fork APIs, agent events, and channel delivery payloads use this
spelling.
- `sessionId` is the opaque stored transcript or runtime instance ID. Session
results may return it beside a key. Talk, terminal, worker, and selected channel
protocols also use `sessionId` for their own concrete session instances; do not
substitute a logical session key there.
Follow each method schema rather than converting fields based on their spelling.
`sessions.resolve` is the explicit bridge when a caller has a key, raw session ID,
label, or parent/agent scope.
### Intentionally open fields
The schema graph is strict by default, but roughly 60 fields intentionally use
`Type.Unknown()` passthroughs. The main clusters are transport-owned channel
payloads, logs-chat message and attachment passthrough, worker and node tool
arguments/results, and the dynamic `config.schema` response. Frame `params`,
`payload`, and error `details` are also open at the envelope layer because the
selected method, event, or error code owns their concrete shape.
Do not treat these fields as validated domain objects. Narrow them at their owner
boundary before reading nested values.
### Machine-readable schema
`protocol.schema.json` ships in the npm tarball as the generated machine-readable
contract. It contains the frame union, named schema definitions, and core method
metadata. It is generated during `prepack` and is not committed to the repository.
### Method discovery
The `hello-ok.features.methods` list is conservative discovery, not a complete
enumeration of every callable method. It reflects the methods the connected
Gateway intentionally advertises. Core-internal, role-specific, plugin-provided,
or otherwise non-advertised methods can have valid schemas without appearing in
that list. Clients should use discovery to enable optional UI, not to reject an
otherwise documented method contract.
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@openclaw/gateway-protocol",
"version": "2026.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/gateway-protocol",
"version": "2026.7.2",
"license": "MIT",
"dependencies": {
"typebox": "1.3.3"
},
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/typebox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.3.tgz",
"integrity": "sha512-URXGUE31PJDQC+PtRMJeLdF4kmmOdFoVPikPCtV2oOIhUpNpppEdIz7W8bH8cFYPYHdDpaRvqwdegMTmHliudg==",
"license": "MIT"
}
}
}
+38 -4
View File
@@ -1,9 +1,31 @@
{
"name": "@openclaw/gateway-protocol",
"version": "0.0.0-private",
"private": true,
"version": "2026.7.2",
"description": "Typed schemas and runtime validators for the OpenClaw Gateway WebSocket protocol",
"keywords": [
"gateway",
"openclaw",
"protocol",
"typebox",
"websocket"
],
"homepage": "https://github.com/openclaw/openclaw#readme",
"bugs": {
"url": "https://github.com/openclaw/openclaw/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/openclaw/openclaw.git",
"directory": "packages/gateway-protocol"
},
"files": [
"dist"
"dist",
"npm-shrinkwrap.json",
"LICENSE",
"README.md",
"CHANGELOG.md",
"protocol.schema.json"
],
"sideEffects": false,
"type": "module",
@@ -52,9 +74,21 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/client-info.ts src/connect-error-details.ts src/frame-guards.ts src/gateway-error-details.ts src/schema.ts src/startup-unavailable.ts src/version.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "tsdown src/index.ts src/client-info.ts src/connect-error-details.ts src/frame-guards.ts src/gateway-error-details.ts src/schema.ts src/startup-unavailable.ts src/version.ts --no-config --platform node --format esm --dts --out-dir dist --clean",
"prepack": "pnpm run build && node --import tsx ../../scripts/protocol-gen.ts --out ./protocol.schema.json"
},
"dependencies": {
"typebox": "1.3.3"
},
"engines": {
"node": ">=22.19.0"
},
"publishConfig": {
"access": "public"
},
"openclaw": {
"release": {
"publishToNpm": true
}
}
}
+26 -5
View File
@@ -7,8 +7,31 @@ import { listCoreGatewayMethodMetadata } from "../src/gateway/methods/core-descr
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const defaultOutputPath = path.join(repoRoot, "dist", "protocol.schema.json");
async function writeJsonSchema() {
function resolveOutputPath(args: string[]): string {
let outputPath = defaultOutputPath;
let hasOutputPath = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg !== "--out") {
throw new Error(`Unknown argument: ${arg}`);
}
if (hasOutputPath) {
throw new Error("--out may only be specified once.");
}
const value = args[index + 1]?.trim();
if (!value || value === "--out") {
throw new Error("--out requires a path.");
}
outputPath = path.resolve(value);
hasOutputPath = true;
index += 1;
}
return outputPath;
}
async function writeJsonSchema(jsonSchemaPath: string) {
const definitions: Record<string, unknown> = {};
for (const [name, schema] of Object.entries(ProtocolSchemas)) {
definitions[name] = schema;
@@ -39,16 +62,14 @@ async function writeJsonSchema() {
definitions,
};
const distDir = path.join(repoRoot, "dist");
await fs.mkdir(distDir, { recursive: true });
const jsonSchemaPath = path.join(distDir, "protocol.schema.json");
await fs.mkdir(path.dirname(jsonSchemaPath), { recursive: true });
await fs.writeFile(jsonSchemaPath, JSON.stringify(rootSchema, null, 2));
console.log(`wrote ${jsonSchemaPath}`);
return { jsonSchemaPath, schemaString: JSON.stringify(rootSchema) };
}
async function main() {
await writeJsonSchema();
await writeJsonSchema(resolveOutputPath(process.argv.slice(2)));
}
main().catch((err: unknown) => {
+69 -10
View File
@@ -456,6 +456,7 @@ export function resolvePackedTarballPath(packDestination: string, results: PackR
export function resolveReleaseCheckLocalPackageTarballs(
tarballDir: string | undefined = process.env[RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV],
requiresAi = rootPackageRequiresLocalAiTarball(),
): string[] {
if (!tarballDir) {
return [];
@@ -470,14 +471,48 @@ export function resolveReleaseCheckLocalPackageTarballs(
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
.map((entry) => resolve(resolvedDir, entry.name))
.toSorted((left, right) => left.localeCompare(right));
if (tarballs.length !== 1) {
const aiTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/ai",
);
const gatewayProtocolTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/gateway-protocol",
);
const recognizedTarballs = aiTarballs.length + gatewayProtocolTarballs.length;
if (recognizedTarballs !== tarballs.length) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} contains ${tarballs.length} tarballs; expected exactly one.`,
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} contains an unsupported package tarball.`,
);
}
const expectedAiTarballs = requiresAi ? 1 : 0;
const aiTarballRequirement = requiresAi
? "exactly one @openclaw/ai tarball"
: "no @openclaw/ai tarballs";
if (aiTarballs.length !== expectedAiTarballs || gatewayProtocolTarballs.length > 1) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must contain ${aiTarballRequirement} and at most one @openclaw/gateway-protocol tarball; found ${aiTarballs.length} and ${gatewayProtocolTarballs.length}.`,
);
}
return tarballs;
}
function rootPackageRequiresLocalAiTarball(): boolean {
const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as {
dependencies?: Record<string, unknown>;
};
return typeof packageJson.dependencies?.["@openclaw/ai"] === "string";
}
function localPackageNameForTarball(tarballPath: string): string | undefined {
const filename = basename(tarballPath);
if (/^openclaw-ai(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/ai";
}
if (/^openclaw-gateway-protocol(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/gateway-protocol";
}
return undefined;
}
export function prepareReleaseCheckLocalPackageTarballs(params: {
tmpRoot: string;
tarballDir?: string;
@@ -505,17 +540,38 @@ export function writePackedTarballInstallManifest(
prefixDir: string,
tarballPath: string,
localPackageTarballs: string[],
requiresAi = rootPackageRequiresLocalAiTarball(),
): void {
const aiTarball = localPackageTarballs[0];
if (localPackageTarballs.length !== 1 || !aiTarball) {
const localPackages = localPackageTarballs.map((localPackageTarballPath) => ({
packageName: localPackageNameForTarball(localPackageTarballPath),
tarballPath: localPackageTarballPath,
}));
const aiTarballs = localPackages.filter(({ packageName }) => packageName === "@openclaw/ai");
const expectedAiTarballs = requiresAi ? 1 : 0;
const aiTarballRequirement = requiresAi
? "exactly one @openclaw/ai tarball"
: "no @openclaw/ai tarballs";
if (aiTarballs.length !== expectedAiTarballs) {
throw new Error(
`release-check: packed install requires exactly one @openclaw/ai tarball; found ${localPackageTarballs.length}.`,
`release-check: packed install requires ${aiTarballRequirement}; found ${aiTarballs.length}.`,
);
}
const dependencies: Record<string, string> = {
"@openclaw/ai": pathToFileURL(aiTarball).href,
openclaw: pathToFileURL(tarballPath).href,
};
const unsupportedTarball = localPackages.find(({ packageName }) => !packageName);
if (unsupportedTarball) {
throw new Error(
`release-check: packed install received an unsupported package tarball: ${basename(unsupportedTarball.tarballPath)}.`,
);
}
const dependencies = Object.fromEntries(
localPackages.map(({ packageName, tarballPath: localPackageTarballPath }) => [
packageName as string,
pathToFileURL(localPackageTarballPath).href,
]),
);
if (Object.keys(dependencies).length !== localPackages.length) {
throw new Error("release-check: packed install received duplicate local package tarballs.");
}
dependencies.openclaw = pathToFileURL(tarballPath).href;
mkdirSync(prefixDir, { recursive: true });
writeFileSync(
join(prefixDir, "package.json"),
@@ -765,10 +821,13 @@ function runPackedPluginSdkTypescriptSmoke(
localPackageTarballs: string[],
): void {
const consumerDir = join(tmpRoot, "plugin-sdk-type-consumer");
const aiTarball = localPackageTarballs.find(
(localPackageTarball) => localPackageNameForTarball(localPackageTarball) === "@openclaw/ai",
);
createPackedPluginSdkTypescriptSmokeProject({
consumerDir,
packageSpec: `file:${tarballPath}`,
aiPackageSpec: localPackageTarballs[0] ? `file:${localPackageTarballs[0]}` : undefined,
aiPackageSpec: aiTarball ? `file:${aiTarball}` : undefined,
});
execNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund"], {
cwd: consumerDir,
+1 -1
View File
@@ -25,7 +25,7 @@ type SyncPluginVersionsOptions = {
};
const OPENCLAW_VERSION_RANGE_RE = /^>=\d{4}\.\d{1,2}\.\d{1,2}(?:[-.][^"\s]+)?$/u;
const VERSION_ALIGNED_PACKAGE_DIRS = ["packages/ai"] as const;
const VERSION_ALIGNED_PACKAGE_DIRS = ["packages/ai", "packages/gateway-protocol"] as const;
function syncOpenClawDependencyRange(
deps: Record<string, string> | undefined,
@@ -488,6 +488,15 @@ describe("generate-npm-shrinkwrap", () => {
).toEqual(["extensions/acpx"]);
});
it("targets the changed publishable gateway protocol shrinkwrap", () => {
expect(
shrinkwrapPackageDirsForChangedPaths([
"packages/gateway-protocol/package.json",
"packages/gateway-protocol/npm-shrinkwrap.json",
]).map(repoRelativePath),
).toEqual(["packages/gateway-protocol"]);
});
it("targets changed tracked shrinkwraps for private packages", () => {
expect(
shrinkwrapPackageDirsForChangedPaths(["extensions/vault/package.json"]).map(repoRelativePath),
@@ -500,6 +509,7 @@ describe("generate-npm-shrinkwrap", () => {
);
expect(packageDirs).toContain("");
expect(packageDirs).toContain("packages/gateway-protocol");
expect(packageDirs).toContain("extensions/acpx");
expect(packageDirs).toContain("extensions/vault");
});
@@ -138,7 +138,7 @@ describe("minimal npm extended-stable workflow", () => {
const plugins = step(preflight, "Exercise all extended-stable plugin npm packages");
expect(step(preflight, "Verify release contents").env).toMatchObject({
OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR:
"${{ steps.ai_runtime_tarballs.outputs.dir }}",
"${{ steps.core_package_tarballs.outputs.dir }}",
});
expect(plugins.if).toBe("${{ inputs.npm_dist_tag == 'extended-stable' }}");
expect(plugins.env).toMatchObject({
@@ -3674,12 +3674,18 @@ describe("package artifact reuse", () => {
"Workflow-dispatched real publish requires release_publish_run_id",
);
expect(npmWorkflow).toContain("tarballSha256");
expect(npmWorkflow).toContain("corePackageTarballs");
expect(npmWorkflow).toContain("dependencyTarballs");
expect(npmWorkflow).toContain("dependencyTarballs: process.env.AI_TARBALL_NAME");
expect(npmWorkflow).toContain('packageName: "@openclaw/ai"');
expect(npmWorkflow).toContain('packageName: "@openclaw/gateway-protocol"');
expect(npmWorkflow).toContain("CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol");
expect(npmWorkflow).toContain("AI_TARBALL_SHA256");
expect(npmWorkflow).toContain("GATEWAY_PROTOCOL_TARBALL_SHA256");
expect(npmWorkflow).toContain("does not match openclaw");
expect(npmWorkflow).toContain("Frozen target does not depend on @openclaw/ai");
expect(npmWorkflow).toContain("dependencyTarballs: process.env.AI_TARBALL_NAME");
expect(npmWorkflow).toContain("core-packages-SHA256SUMS");
expect(npmWorkflow).toContain(".corePackageTarballs[] | [.packageName, .tarballName] | @tsv");
expect(npmWorkflow).toContain('verify_args=("$TARBALL_PATH" "$PACKAGE_VERSION")');
expect(npmWorkflow).toContain("Frozen target without an @openclaw/ai dependency");
const npmTelegramWorkflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");
+55 -8
View File
@@ -31,13 +31,15 @@ describe("release-check", () => {
]);
});
it("resolves exactly one prepacked local dependency tarball", () => {
it("resolves prepacked publishable core package tarballs", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
writeFileSync(join(root, "openclaw-ai-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "openclaw-gateway-protocol-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "SHA256SUMS"), "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root)).toEqual([
join(root, "openclaw-ai-2026.6.33.tgz"),
join(root, "openclaw-gateway-protocol-2026.6.33.tgz"),
]);
expect(resolveReleaseCheckLocalPackageTarballs(undefined)).toEqual([]);
} finally {
@@ -45,10 +47,24 @@ describe("release-check", () => {
}
});
it("writes an explicit local project for unpublished core and AI tarballs", () => {
it("accepts a gateway-only core package directory when the root does not require AI", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
const gatewayTarball = join(root, "openclaw-gateway-protocol-2026.7.2.tgz");
writeFileSync(gatewayTarball, "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root, false)).toEqual([gatewayTarball]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("writes an explicit local project for unpublished core package tarballs", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-"));
try {
writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", ["/tmp/openclaw-ai.tgz"]);
writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", [
"/tmp/openclaw-ai.tgz",
"/tmp/openclaw-gateway-protocol.tgz",
]);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
private?: boolean;
@@ -56,6 +72,28 @@ describe("release-check", () => {
expect(manifest.private).toBe(true);
expect(manifest.dependencies).toEqual({
"@openclaw/ai": "file:///tmp/openclaw-ai.tgz",
"@openclaw/gateway-protocol": "file:///tmp/openclaw-gateway-protocol.tgz",
openclaw: "file:///tmp/openclaw.tgz",
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("writes a gateway-only local project when the root does not require AI", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-"));
try {
writePackedTarballInstallManifest(
root,
"/tmp/openclaw.tgz",
["/tmp/openclaw-gateway-protocol.tgz"],
false,
);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
expect(manifest.dependencies).toEqual({
"@openclaw/gateway-protocol": "file:///tmp/openclaw-gateway-protocol.tgz",
openclaw: "file:///tmp/openclaw.tgz",
});
} finally {
@@ -80,13 +118,18 @@ describe("release-check", () => {
}
});
it("prefers the prepared AI tarball over packing the workspace", () => {
it("prefers prepared core package tarballs over packing the AI workspace", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-ai-pack-test-"));
try {
const preparedDir = join(root, "prepared");
mkdirSync(preparedDir);
const preparedTarball = join(preparedDir, "openclaw-ai-2026.7.1-beta.3.tgz");
const gatewayProtocolTarball = join(
preparedDir,
"openclaw-gateway-protocol-2026.7.1-beta.3.tgz",
);
writeFileSync(preparedTarball, "fixture");
writeFileSync(gatewayProtocolTarball, "fixture");
const tarballs = prepareReleaseCheckLocalPackageTarballs({
tmpRoot: root,
tarballDir: preparedDir,
@@ -94,7 +137,7 @@ describe("release-check", () => {
throw new Error("workspace pack should not run");
},
});
expect(tarballs).toEqual([preparedTarball]);
expect(tarballs).toEqual([preparedTarball, gatewayProtocolTarball]);
} finally {
rmSync(root, { recursive: true, force: true });
}
@@ -117,7 +160,7 @@ describe("release-check", () => {
}
});
it("rejects missing, empty, or ambiguous local dependency tarball directories", () => {
it("rejects missing, incomplete, or ambiguous local package tarball directories", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
expect(() => resolveReleaseCheckLocalPackageTarballs(join(root, "missing"))).toThrow(
@@ -125,10 +168,14 @@ describe("release-check", () => {
);
const empty = join(root, "empty");
mkdirSync(empty);
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow("contains 0 tarballs");
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow(
"must contain exactly one @openclaw/ai tarball",
);
writeFileSync(join(empty, "one.tgz"), "fixture");
writeFileSync(join(empty, "two.tgz"), "fixture");
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow("contains 2 tarballs");
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow(
"contains an unsupported package tarball",
);
} finally {
rmSync(root, { recursive: true, force: true });
}