mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user