Compare commits

..

8 Commits

Author SHA1 Message Date
Peter Steinberger
185ffca274 fix: filter telegram native command names (#1558) (thanks @Glucksberg) 2026-01-24 06:23:39 +00:00
Peter Steinberger
c2940adc80 fix: register plugin commands natively (#1558) (thanks @Glucksberg) 2026-01-24 06:23:39 +00:00
Peter Steinberger
29043209c9 fix: refresh reserved plugin commands (#1558) (thanks @Glucksberg) 2026-01-24 06:23:39 +00:00
Peter Steinberger
bfc0fb742e fix: harden plugin commands (#1558) (thanks @Glucksberg) 2026-01-24 06:23:39 +00:00
Glucksberg
051c92651f fix: address code review findings for plugin commands
- Add registry lock during command execution to prevent race conditions
- Add input sanitization for command arguments (defense in depth)
- Validate handler is a function during registration
- Remove redundant case-insensitive regex flag
- Add success logging for command execution
- Simplify handler return type (always returns result now)
- Remove dead code branch in commands-plugin.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:23:39 +00:00
Glucksberg
b16576f298 fix: clear plugin commands on reload to prevent duplicates
Add clearPluginCommands() call in loadClawdbotPlugins() to ensure
previously registered commands are cleaned up before reloading plugins.
This prevents command conflicts during hot-reload scenarios.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:23:39 +00:00
Glucksberg
691adccf32 fix: address code review findings for plugin command API
Blockers fixed:
- Fix documentation: requireAuth defaults to true (not false)
- Add command name validation (must start with letter, alphanumeric only)
- Add reserved commands list to prevent shadowing built-in commands
- Emit diagnostic errors for invalid/duplicate command registration

Other improvements:
- Return user-friendly message for unauthorized commands (instead of silence)
- Sanitize error messages to avoid leaking internal details
- Document acceptsArgs behavior when arguments are provided
- Add notes about reserved commands and validation rules to docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:23:39 +00:00
Glucksberg
420af3285f feat: add plugin command API for LLM-free auto-reply commands
This adds a new `api.registerCommand()` method to the plugin API, allowing
plugins to register slash commands that execute without invoking the AI agent.

Features:
- Plugin commands are processed before built-in commands and the agent
- Commands can optionally require authorization
- Commands can accept arguments
- Async handlers are supported

Use case: plugins can implement toggle commands (like /tts_on, /tts_off)
that respond immediately without consuming LLM API calls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:23:39 +00:00
15014 changed files with 366036 additions and 2378536 deletions

BIN
.agent/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,366 @@
---
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
---
# Clawdbot Upstream Sync Workflow
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
## Quick Reference
```bash
# Check divergence status
git fetch upstream && git rev-list --left-right --count main...upstream/main
# Full sync (rebase preferred)
git fetch upstream && git rebase upstream/main && pnpm install && pnpm build && ./scripts/restart-mac.sh
# Check for Swift 6.2 issues after sync
grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift"
```
---
## Step 1: Assess Divergence
```bash
git fetch upstream
git log --oneline --left-right main...upstream/main | head -20
```
This shows:
- `<` = your local commits (ahead)
- `>` = upstream commits you're missing (behind)
**Decision point:**
- Few local commits, many upstream → **Rebase** (cleaner history)
- Many local commits or shared branch → **Merge** (preserves history)
---
## Step 2A: Rebase Strategy (Preferred)
Replays your commits on top of upstream. Results in linear history.
```bash
# Ensure working tree is clean
git status
# Rebase onto upstream
git rebase upstream/main
```
### Handling Rebase Conflicts
```bash
# When conflicts occur:
# 1. Fix conflicts in the listed files
# 2. Stage resolved files
git add <resolved-files>
# 3. Continue rebase
git rebase --continue
# If a commit is no longer needed (already in upstream):
git rebase --skip
# To abort and return to original state:
git rebase --abort
```
### Common Conflict Patterns
| File | Resolution |
|------|------------|
| `package.json` | Take upstream deps, keep local scripts if needed |
| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` |
| `*.patch` files | Usually take upstream version |
| Source files | Merge logic carefully, prefer upstream structure |
---
## Step 2B: Merge Strategy (Alternative)
Preserves all history with a merge commit.
```bash
git merge upstream/main --no-edit
```
Resolve conflicts same as rebase, then:
```bash
git add <resolved-files>
git commit
```
---
## Step 3: Rebuild Everything
After sync completes:
```bash
# Install dependencies (regenerates lock if needed)
pnpm install
# Build TypeScript
pnpm build
# Build UI assets
pnpm ui:build
# Run diagnostics
pnpm clawdbot doctor
```
---
## Step 4: Rebuild macOS App
```bash
# Full rebuild, sign, and launch
./scripts/restart-mac.sh
# Or just package without restart
pnpm mac:package
```
### Install to /Applications
```bash
# Kill running app
pkill -x "Clawdbot" || true
# Move old version
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
# Install new build
cp -R dist/Clawdbot.app /Applications/
# Launch
open /Applications/Clawdbot.app
```
---
## Step 4A: Verify macOS App & Agent
After rebuilding the macOS app, always verify it works correctly:
```bash
# Check gateway health
pnpm clawdbot health
# Verify no zombie processes
ps aux | grep -E "(clawdbot|gateway)" | grep -v grep
# Test agent functionality by sending a verification message
pnpm clawdbot agent --message "Verification: macOS app rebuild successful - agent is responding." --session-id YOUR_TELEGRAM_SESSION_ID
# Confirm the message was received on Telegram
# (Check your Telegram chat with the bot)
```
**Important:** Always wait for the Telegram verification message before proceeding. If the agent doesn't respond, troubleshoot the gateway or model configuration before pushing.
---
## Step 5: Handle Swift/macOS Build Issues (Common After Upstream Sync)
Upstream updates may introduce Swift 6.2 / macOS 26 SDK incompatibilities. Use analyze-mode for systematic debugging:
### Analyze-Mode Investigation
```bash
# Gather context with parallel agents
morph-mcp_warpgrep_codebase_search search_string="Find deprecated FileManager.default and Thread.isMainThread usages in Swift files" repo_path="/Volumes/Main SSD/Developer/clawdis"
morph-mcp_warpgrep_codebase_search search_string="Locate Peekaboo submodule and macOS app Swift files with concurrency issues" repo_path="/Volumes/Main SSD/Developer/clawdis"
```
### Common Swift 6.2 Fixes
**FileManager.default Deprecation:**
```bash
# Search for deprecated usage
grep -r "FileManager\.default" src/ apps/ --include="*.swift"
# Replace with proper initialization
# OLD: FileManager.default
# NEW: FileManager()
```
**Thread.isMainThread Deprecation:**
```bash
# Search for deprecated usage
grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift"
# Replace with modern concurrency check
# OLD: Thread.isMainThread
# NEW: await MainActor.run { ... } or DispatchQueue.main.sync { ... }
```
### Peekaboo Submodule Fixes
```bash
# Check Peekaboo for concurrency issues
cd src/canvas-host/a2ui
grep -r "Thread\.isMainThread\|FileManager\.default" . --include="*.swift"
# Fix and rebuild submodule
cd /Volumes/Main SSD/Developer/clawdis
pnpm canvas:a2ui:bundle
```
### macOS App Concurrency Fixes
```bash
# Check macOS app for issues
grep -r "Thread\.isMainThread\|FileManager\.default" apps/macos/ --include="*.swift"
# Clean and rebuild after fixes
cd apps/macos && rm -rf .build .swiftpm
./scripts/restart-mac.sh
```
### Model Configuration Updates
If upstream introduced new model configurations:
```bash
# Check for OpenRouter API key requirements
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
# Update clawdbot.json with fallback chains
# Add model fallback configurations as needed
```
---
## Step 6: Verify & Push
```bash
# Verify everything works
pnpm clawdbot health
pnpm test
# Push (force required after rebase)
git push origin main --force-with-lease
# Or regular push after merge
git push origin main
```
---
## Troubleshooting
### Build Fails After Sync
```bash
# Clean and rebuild
rm -rf node_modules dist
pnpm install
pnpm build
```
### Type Errors (Bun/Node Incompatibility)
Common issue: `fetch.preconnect` type mismatch. Fix by using `FetchLike` type instead of `typeof fetch`.
### macOS App Crashes on Launch
Usually resource bundle mismatch. Full rebuild required:
```bash
cd apps/macos && rm -rf .build .swiftpm
./scripts/restart-mac.sh
```
### Patch Failures
```bash
# Check patch status
pnpm install 2>&1 | grep -i patch
# If patches fail, they may need updating for new dep versions
# Check patches/ directory against package.json patchedDependencies
```
### Swift 6.2 / macOS 26 SDK Build Failures
**Symptoms:** Build fails with deprecation warnings about `FileManager.default` or `Thread.isMainThread`
**Search-Mode Investigation:**
```bash
# Exhaustive search for deprecated APIs
morph-mcp_warpgrep_codebase_search search_string="Find all Swift files using deprecated FileManager.default or Thread.isMainThread" repo_path="/Volumes/Main SSD/Developer/clawdis"
```
**Quick Fix Commands:**
```bash
# Find all affected files
find . -name "*.swift" -exec grep -l "FileManager\.default\|Thread\.isMainThread" {} \;
# Replace FileManager.default with FileManager()
find . -name "*.swift" -exec sed -i '' 's/FileManager\.default/FileManager()/g' {} \;
# For Thread.isMainThread, need manual review of each usage
grep -rn "Thread\.isMainThread" --include="*.swift" .
```
**Rebuild After Fixes:**
```bash
# Clean all build artifacts
rm -rf apps/macos/.build apps/macos/.swiftpm
rm -rf src/canvas-host/a2ui/.build
# Rebuild Peekaboo bundle
pnpm canvas:a2ui:bundle
# Full macOS rebuild
./scripts/restart-mac.sh
```
---
## Automation Script
Save as `scripts/sync-upstream.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> Fetching upstream..."
git fetch upstream
echo "==> Current divergence:"
git rev-list --left-right --count main...upstream/main
echo "==> Rebasing onto upstream/main..."
git rebase upstream/main
echo "==> Installing dependencies..."
pnpm install
echo "==> Building..."
pnpm build
pnpm ui:build
echo "==> Running doctor..."
pnpm clawdbot doctor
echo "==> Rebuilding macOS app..."
./scripts/restart-mac.sh
echo "==> Verifying gateway health..."
pnpm clawdbot health
echo "==> Checking for Swift 6.2 compatibility issues..."
if grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift" --quiet; then
echo "⚠️ Found potential Swift 6.2 deprecated API usage"
echo " Run manual fixes or use analyze-mode investigation"
else
echo "✅ No obvious Swift deprecation issues found"
fi
echo "==> Testing agent functionality..."
# Note: Update YOUR_TELEGRAM_SESSION_ID with actual session ID
pnpm clawdbot agent --message "Verification: Upstream sync and macOS rebuild completed successfully." --session-id YOUR_TELEGRAM_SESSION_ID || echo "Warning: Agent test failed - check Telegram for verification message"
echo "==> Done! Check Telegram for verification message, then run 'git push --force-with-lease' when ready."
```

View File

@@ -1 +0,0 @@
Maintainer skills now live in [`openclaw/maintainers`](https://github.com/openclaw/maintainers/).

View File

@@ -1,87 +0,0 @@
---
name: openclaw-ghsa-maintainer
description: Maintainer workflow for OpenClaw GitHub Security Advisories (GHSA). Use when Codex needs to inspect, patch, validate, or publish a repo advisory, verify private-fork state, prepare advisory Markdown or JSON payloads safely, handle GHSA API-specific publish constraints, or confirm advisory publish success.
---
# OpenClaw GHSA Maintainer
Use this skill for repo security advisory workflow only. Keep general release work in `openclaw-release-maintainer`.
## Respect advisory guardrails
- Before reviewing or publishing a repo advisory, read `SECURITY.md`.
- Ask permission before any publish action.
- Treat this skill as GHSA-only. Do not use it for stable or beta release work.
## Fetch and inspect advisory state
Fetch the current advisory and the latest published npm version:
```bash
gh api /repos/openclaw/openclaw/security-advisories/<GHSA>
npm view openclaw version --userconfig "$(mktemp)"
```
Use the fetch output to confirm the advisory state, linked private fork, and vulnerability payload shape before patching.
## Verify private fork PRs are closed
Before publishing, verify that the advisory's private fork has no open PRs:
```bash
fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)
gh pr list -R "$fork" --state open
```
The PR list must be empty before publish.
## Prepare advisory Markdown and JSON safely
- Write advisory Markdown via heredoc to a temp file. Do not use escaped `\n` strings.
- Build PATCH payload JSON with `jq`, not hand-escaped shell JSON.
Example pattern:
```bash
cat > /tmp/ghsa.desc.md <<'EOF'
<markdown description>
EOF
jq -n --rawfile desc /tmp/ghsa.desc.md \
'{summary,severity,description:$desc,vulnerabilities:[...]}' \
> /tmp/ghsa.patch.json
```
## Apply PATCH calls in the correct sequence
- Do not set `severity` and `cvss_vector_string` in the same PATCH call.
- Use separate calls when the advisory requires both fields.
- Publish by PATCHing the advisory and setting `"state":"published"`. There is no separate `/publish` endpoint.
Example shape:
```bash
gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> \
--input /tmp/ghsa.patch.json
```
## Publish and verify success
After publish, re-fetch the advisory and confirm:
- `state=published`
- `published_at` is set
- the description does not contain literal escaped `\\n`
Verification pattern:
```bash
gh api /repos/openclaw/openclaw/security-advisories/<GHSA>
jq -r .description < /tmp/ghsa.refetch.json | rg '\\\\n'
```
## Common GHSA footguns
- Publishing fails with HTTP 422 if required fields are missing or the private fork still has open PRs.
- A payload that looks correct in shell can still be wrong if Markdown was assembled with escaped newline strings.
- Advisory PATCH sequencing matters; separate field updates when GHSA API constraints require it.

View File

@@ -1,150 +0,0 @@
---
name: openclaw-parallels-smoke
description: End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.
---
# OpenClaw Parallels Smoke
Use this skill for Parallels guest workflows and smoke interpretation. Do not load it for normal repo work.
## Global rules
- Use the snapshot most closely matching the requested fresh baseline.
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc` unless the stable version being checked does not support it yet.
- Stable `2026.3.12` pre-upgrade diagnostics may require a plain `gateway status --deep` fallback.
- Treat `precheck=latest-ref-fail` on that stable pre-upgrade lane as baseline, not automatically a regression.
- Pass `--json` for machine-readable summaries.
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
- Hard-cap every top-level Parallels lane with host `timeout --foreground` (or `gtimeout --foreground` if that is the available binary) so a stalled install, snapshot switch, or `prlctl exec` transport cannot consume the rest of the testing window. Defaults:
- macOS: `75m`
- Linux: `75m`
- Windows: `90m`
- aggregate npm-update wrapper: `150m`
If a lane hits the cap, stop there, inspect the newest `/tmp/openclaw-parallels-*` run directory and phase log, then fix or rerun the smallest affected lane. Do not keep waiting on a capped lane.
- Actual OpenClaw npm install/update phases are a stricter budget than whole lanes: install phases should finish within 7 minutes, and update phases should finish within 5 minutes. If a phase named `install-main`, `install-latest`, `install-baseline`, or `install-baseline-package` exceeds 420s, or a phase named `update-dev` / same-guest `openclaw update` exceeds 300s, treat it as a failure/harness bug and start diagnosis from that phase log. Do not wait for a longer lane cap.
- For a full OS matrix, prefer running independent guest-family lanes in parallel when host capacity allows:
- `timeout --foreground 75m pnpm test:parallels:macos -- --json`
- `timeout --foreground 90m pnpm test:parallels:windows -- --json`
- `timeout --foreground 75m pnpm test:parallels:linux -- --json`
Keep each lane in its own shell/session and track the run directory for each one.
- Do not run multiple smoke lanes against the same guest family at once. Tahoe lanes share the host HTTP port, and Windows/Linux lanes can collide on snapshot restore/start state if two jobs touch the same VM concurrently.
- Do not run the aggregate `pnpm test:parallels:npm-update` wrapper in parallel with individual macOS/Windows/Linux smoke lanes; it touches the same guest families and snapshots.
- Do not start Parallels lanes while any host command may rebuild, clean, or restage `dist` (`pnpm build`, `pnpm ui:build`, `pnpm release:check`, `pnpm test:install:smoke`, npm pack/install smoke, or Docker lanes that run package/build prep). Run the build/package gates first, let them finish, then start the VM matrix. Concurrent `dist` mutation can make host `npm pack` fail with missing files and wastes a full VM cycle.
- While running or optimizing the matrix, record wall-clock duration per lane and the slowest phase from `/tmp/openclaw-parallels-*` logs. Use that timing before changing smoke order, timeouts, or helper behavior.
- If `main` is moving under active multi-agent work, prefer a detached worktree pinned to one commit for long Parallels suites. The smoke scripts now verify the packed tgz commit instead of live `git rev-parse HEAD`, but a pinned worktree still avoids noisy rebuild/version drift during reruns.
- For `openclaw update --channel dev` lanes, remember the guest clones GitHub `main`, not your local worktree. If a local fix exists but the rerun still fails inside the cloned dev checkout, do not treat that as disproof of the fix until the branch has been pushed.
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
- If the workflow installs OpenClaw from a repo checkout instead of the site installer/npm release, finish by installing a real guest CLI shim and verifying it in a fresh guest shell. `pnpm openclaw ...` inside the repo is not enough for handoff parity.
- On macOS guests, prefer a user-global install plus a stable PATH-visible shim:
- install with `NPM_CONFIG_PREFIX="$HOME/.npm-global" npm install -g .`
- make sure `~/.local/bin/openclaw` exists or `~/.npm-global/bin` is on PATH
- verify from a brand-new guest shell with `which openclaw` and `openclaw --version`
## npm install then update
- Preferred entrypoint: `pnpm test:parallels:npm-update`
- Required coverage: every release/update regression run must include both lanes:
- fresh snapshot -> install requested package/baseline -> smoke
- same guest baseline -> run the guest's installed `openclaw update ...` command -> smoke again
- The update lane must exercise OpenClaw's internal updater. Do not count a direct `npm install -g <tgz-or-spec>` or harness-side package swap as update-flow coverage; those are install smokes only.
- For published targets, install the old baseline package first (for example `openclaw@2026.4.9`), then run the installed guest CLI with the intended channel/tag (for example `openclaw update --channel beta --yes --json`) and verify `openclaw --version`, `openclaw update status --json`, gateway RPC, and an agent turn after the command.
- For unpublished targets, pack the candidate on the host, serve the `.tgz` over the harness HTTP server, and point the guest updater at that served package. Prefer `openclaw update --tag http://<host-ip>:<port>/openclaw-<version>.tgz --yes --json`; when channel persistence also matters, pass `--channel <stable|beta>` and set `OPENCLAW_UPDATE_PACKAGE_SPEC` to the same served URL in the guest update environment. The command under test must still be `openclaw update`, not direct npm.
- For unpublished local-fix validation, remember the old baseline updater code still controls the first hop. A fix that lives only in the new updater code cannot change that already-running old process; the served candidate must either keep package/plugin metadata compatible with the baseline host or the baseline itself must include the updater fix.
- For beta/stable verification, resolve the tag immediately before the run (`npm view openclaw@beta version dist.tarball` or `npm view openclaw@latest ...`). Tags can move while a long VM matrix is already running; restart the matrix when the intended prerelease appears after an earlier registry 404/tag-lag check.
- Source Peter's profile in the host shell (`set -a; source "$HOME/.profile"; set +a`) before OpenAI/Anthropic lanes. Do not print profile contents or env dumps; pass provider secrets through the guest exec environment.
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
- The npm-update aggregate's macOS update leg writes the guest update script as root, then runs it as the desktop user. If `prlctl exec "$MACOS_VM" --current-user ...` cannot authenticate, retry through plain root `prlctl exec` plus `sudo -u <desktop-user> /usr/bin/env HOME=/Users/<desktop-user> USER=<desktop-user> LOGNAME=<desktop-user> PATH=/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/usr/bin:/bin:/usr/sbin:/sbin ...`. That is a Parallels transport fallback; still verify `openclaw --version`, gateway RPC, and an agent turn after the update.
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
- In those Windows same-guest update checks, do not treat one nonzero `openclaw gateway restart` as definitive failure. Current login-item restarts can report failure before the background service becomes observable again; follow with a longer RPC-ready wait and use `gateway start` only as a recovery step if readiness still never returns.
- After that Windows restart, do not trust one `gateway status --deep --require-rpc` call after a fixed sleep. Retry the RPC-ready probe for roughly 30 seconds and log each attempt; current guests can keep port `18789` bound while the fresh RPC endpoint is still coming up.
- For Windows same-guest update checks, prefer the done-file/log-drain PowerShell runner pattern over one long-lived `prlctl exec ... powershell -EncodedCommand ...` transport. The guest can finish successfully while the outer `prlctl exec` still hangs.
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials.
- The npm-update wrapper now prints per-lane progress from the nested log files. If a lane still looks stuck, inspect the nested logs in `runDir` first (`macos-fresh.log`, `windows-fresh.log`, `linux-fresh.log`, `macos-update.log`, `windows-update.log`, `linux-update.log`) instead of assuming the outer wrapper hung.
- If the wrapper fails a lane, read the auto-dumped tail first, then the full nested lane log under `/tmp/openclaw-parallels-npm-update.*`.
- Current known macOS update-lane transport signature when the fallback is missing or bypassed: `Unable to authenticate the user. Make sure that the specified credentials are correct and try again.` Treat that as Parallels current-user authentication before blaming npm or OpenClaw.
## CLI invocation footgun
- The Parallels smoke shell scripts should tolerate a literal bare `--` arg so `pnpm test:parallels:* -- --json` and similar forwarded invocations work without needing to call `bash scripts/e2e/...` directly.
## macOS flow
- Preferred entrypoint: `pnpm test:parallels:macos`
- Default upgrade coverage on macOS should now include: fresh snapshot -> site installer pinned to the latest stable tag -> `openclaw update --channel dev` on the guest. Treat this as part of the default Tahoe regression plan, not an optional side quest.
- `parallels-macos-smoke.sh --mode upgrade` should run that release-to-dev lane by default. Keep the older host-tgz upgrade path only when the caller explicitly passes `--target-package-spec`.
- Because the default upgrade lane no longer needs a host tgz, skip `npm pack` + host HTTP server startup for `--mode upgrade` unless `--target-package-spec` is set. Keep the pack/server path for `fresh` and `both`.
- If that release-to-dev lane fails with `reason=preflight-no-good-commit` and repeated `sh: pnpm: command not found` tails from `preflight build`, treat it as an updater regression first. The fix belongs in the git/dev updater bootstrap path, not in Parallels retry logic.
- Until the public stable train includes that updater bootstrap fix, the macOS release-to-dev lane may seed a temporary guest-local `pnpm` shim immediately before `openclaw update --channel dev`. Keep that workaround scoped to the smoke harness and remove it once the latest stable no longer needs it.
- In Tahoe `prlctl exec --current-user` runs, prefer explicit `node .../openclaw.mjs ...` invocations for the release->dev handoff itself and for post-update verification. The shebanged global `openclaw` wrapper can fail with `env: node: No such file or directory`, and self-updating through the wrapper is a weaker lane than invoking the entrypoint under a fixed `node`.
- Default to the snapshot closest to `macOS 26.3.1 latest`.
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
- For Tahoe `fresh.gateway-status`, prefer non-TTY `prlctl exec --current-user ... openclaw gateway status ...` plus a few short retries. `prlctl enter` can spam TTY control bytes and hang the phase log even when the CLI itself is healthy.
- If a Tahoe lane times out in `fresh.first-agent-turn` and the phase log stops right after `__OPENCLAW_RC__:0` from `models set`, suspect the `prlctl enter` / `expect` wrapper before blaming auth or the model lane. That pattern means the first guest command finished but the transport never released for the next `guest_current_user_cli` call.
- If a packaged install regresses with `500` on `/`, `/healthz`, or `__openclaw/control-ui-config.json` after `fresh.install-main` or `upgrade.install-main`, suspect bundled plugin runtime deps resolving from the package root `node_modules` rather than `dist/extensions/*/node_modules`. Repro quickly with a real `npm pack`/global install lane before blaming dashboard auth or Safari.
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
- Multi-word `openclaw agent --message ...` checks should go through a guest shell wrapper (`guest_current_user_sh` / `guest_current_user_cli` or `/bin/sh -lc ...`), not raw `prlctl exec ... node openclaw.mjs ...`, or the message can be split into extra argv tokens and Commander reports `too many arguments for 'agent'`.
- The same wrapper rule applies when bypassing `--current-user`: write a tiny `/tmp/*.sh` on the guest and execute `/bin/bash /tmp/*.sh` through the sudo desktop-user environment. Do not pass `openclaw agent --message '...'` directly as one raw `prlctl exec` command.
- When ref-mode onboarding stores `OPENAI_API_KEY` as an env secret ref, the post-onboard agent verification should also export `OPENAI_API_KEY` for the guest command. The gateway can still reject with pairing-required and fall back to embedded execution, and that fallback needs the env-backed credential available in the shell.
- On the fresh Tahoe snapshot, `brew` exists but `node` may be missing from PATH in noninteractive exec. Use `/opt/homebrew/bin/node` when needed.
- Fresh host-served tgz installs should install as guest root with `HOME=/var/root`, then run onboarding as the desktop user via `prlctl exec --current-user`.
- Root-installed tgz smoke can log plugin blocks for world-writable `extensions/*`; do not treat that as an onboarding or gateway failure unless plugin loading is the task.
## Windows flow
- Preferred entrypoint: `pnpm test:parallels:windows`
- Use the snapshot closest to `pre-openclaw-native-e2e-2026-03-12`.
- Default upgrade coverage on Windows should now include: fresh snapshot -> site installer pinned to the requested stable tag -> `openclaw update --channel dev` on the guest. Keep the older host-tgz upgrade path only when the caller explicitly passes `--target-package-spec`.
- Optional exact npm-tag baseline on Windows: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --target-package-spec openclaw@<tag> --json`. That lane installs the published npm tarball as baseline, then runs `openclaw update --channel dev`.
- Optional forward-fix Windows validation: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --upgrade-from-packed-main --json`. That lane installs the packed current-main npm tgz as baseline, then runs `openclaw update --channel dev`.
- Always use `prlctl exec --current-user`; plain `prlctl exec` lands in `NT AUTHORITY\\SYSTEM`.
- Prefer explicit `npm.cmd` and `openclaw.cmd`.
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
- Current Windows Node installs expose `corepack` as a `.cmd` shim. If a release-to-dev lane sees `corepack` on PATH but `openclaw update --channel dev` still behaves as if corepack is missing, treat that as an exec-shim regression first.
- If an exact published-tag Windows lane fails during preflight with `npm run build` and `'pnpm' is not recognized`, remember that the guest is still executing the old published updater. Validate the fix with `--upgrade-from-packed-main`, then wait for the next tagged npm release before expecting the historical tag lane to pass.
- Multi-word `openclaw agent --message ...` checks should call `& $openclaw ...` inside PowerShell, not `Start-Process ... -ArgumentList` against `openclaw.cmd`, or Commander can see split argv and throw `too many arguments for 'agent'`.
- Windows installer/tgz phases now retry once after guest-ready recheck; keep new Windows smoke steps idempotent so a transport-flake retry is safe.
- If a Windows retry sees the VM become `suspended` or `stopped`, resume/start it before the next `prlctl exec`; otherwise the second attempt just repeats the same `rc=255`.
- Windows global `npm install -g` phases can stay quiet for a minute or more even when healthy; inspect the phase log before calling it hung, and only treat it as a regression once the retry wrapper or timeout trips.
- When those Windows global installs stay quiet, the useful progress often lives in the guest npm debug log, not the helper phase log. The smoke script now streams incremental `npm-cache/_logs/*-debug-0.log` deltas into the phase log during long baseline/package installs; read those lines before assuming the lane is stalled.
- The Windows baseline-package helpers now auto-dump the latest guest `npm-cache/_logs/*-debug-0.log` tail on timeout or nonzero completion. Read that tail in the phase log before opening a second guest shell.
- The same incremental npm-debug streaming also applies to `--upgrade-from-packed-main` / packaged-install baseline phases. A phase log that still says only `install.start`, `install.download-tgz`, `install.install-tgz` can still be healthy if the streamed npm-debug section shows registry fetches or bundled-plugin postinstall work.
- Fresh Windows tgz install phases should also use the background PowerShell runner plus done-file/log-drain pattern; do not rely on one long-lived `prlctl exec ... powershell ... npm install -g` transport for package installs.
- Windows release-to-dev helpers should log `where pnpm` before and after the update and require `where pnpm` to succeed post-update. That proves the updater installed or enabled `pnpm` itself instead of depending on a smoke-only bootstrap.
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
- Fresh Windows daemon-health reachability should use `openclaw gateway probe --json` with a longer timeout and treat `ok: true` as success; full `gateway status --require-rpc` checks are too eager during initial startup on current main.
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.
- The standalone Windows upgrade smoke lane should stop the managed gateway after `upgrade.install-main` and before `upgrade.onboard-ref`. Restarting before onboard can leave the old process alive on the pre-onboard token while onboard rewrites `~/.openclaw/openclaw.json`, which then fails `gateway-health` with `unauthorized: gateway token mismatch`.
- If standalone Windows upgrade fails with a gateway token mismatch but `pnpm test:parallels:npm-update` passes, trust the mismatch as a standalone ref-onboard ordering bug first; the npm-update helper does not re-run ref-mode onboard on the same guest.
- Keep onboarding and status output ASCII-clean in logs; fancy punctuation becomes mojibake in current capture paths.
- If you hit an older run with `rc=255` plus an empty `fresh.install-main.log` or `upgrade.install-main.log`, treat it as a likely `prlctl exec` transport drop after guest start-up, not immediate proof of an npm/package failure.
## Linux flow
- Preferred entrypoint: `pnpm test:parallels:linux`
- Use the snapshot closest to fresh `Ubuntu 24.04.3 ARM64`.
- If that exact VM is missing on the host, any Ubuntu guest with major version `>= 24` is acceptable; prefer the closest versioned Ubuntu guest with a fresh poweroff snapshot. On Peter's host today, that is `Ubuntu 25.10`.
- Use plain `prlctl exec`; `--current-user` is not the right transport on this snapshot.
- Fresh snapshots may be missing `curl`, and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates`.
- Fresh `main` tgz smoke still needs the latest-release installer first because the snapshot has no Node or npm before bootstrap.
- This snapshot does not have a usable `systemd --user` session; managed daemon install is unsupported.
- The Linux smoke now falls back to a manual `setsid openclaw gateway run --bind loopback --port 18789 --force` launch with `HOME=/root` and the provider secret exported, then verifies `gateway status --deep --require-rpc` when available.
- The Linux manual gateway launch should wait for `gateway status --deep --require-rpc` inside the `gateway-start` phase; otherwise the first status probe can race the background bind and fail a healthy lane.
- If Linux gateway bring-up fails, inspect `/tmp/openclaw-parallels-linux-gateway.log` in the guest phase logs first; the common failure mode is a missing provider secret in the launched gateway environment.
## Discord roundtrip
- Discord roundtrip is optional and should be enabled with:
- `--discord-token-env`
- `--discord-guild-id`
- `--discord-channel-id`
- Keep the Discord token only in a host env var.
- Use installed `openclaw message send/read`, not `node openclaw.mjs message ...`.
- Set `channels.discord.guilds` as one JSON object, not dotted config paths with snowflakes.
- Avoid long `prlctl enter` or expect-driven Discord config scripts; prefer `prlctl exec --current-user /bin/sh -lc ...` with short commands.
- For a narrower macOS-only Discord proof run, the existing `parallels-discord-roundtrip` skill is the deep-dive companion.

View File

@@ -1,75 +0,0 @@
---
name: openclaw-pr-maintainer
description: Maintainer workflow for reviewing, triaging, preparing, closing, or landing OpenClaw pull requests and related issues. Use when Codex needs to validate bug-fix claims, search for related issues or PRs, apply or recommend close/reason labels, prepare GitHub comments safely, check review-thread follow-up, or perform maintainer-style PR decision making before merge or closure.
---
# OpenClaw PR Maintainer
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Apply close and triage labels correctly
- If an issue or PR matches an auto-close reason, apply the label and let `.github/workflows/auto-response.yml` handle the comment/close/lock flow.
- Do not manually close plus manually comment for these reasons.
- `r:*` labels can be used on both issues and PRs.
- Current reasons:
- `r: skill`
- `r: support`
- `r: no-ci-pr`
- `r: too-many-prs`
- `r: testflight`
- `r: third-party-extension`
- `r: moltbook`
- `r: spam`
- `invalid`
- `dirty` for PRs only
## Enforce the bug-fix evidence bar
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
- Before landing, require:
1. symptom evidence such as a repro, logs, or a failing test
2. a verified root cause in code with file/line
3. a fix that touches the implicated code path
4. a regression test when feasible, or explicit manual verification plus a reason no test was added
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
## Handle GitHub text safely
- For issue comments and PR comments, use literal multiline strings or `-F - <<'EOF'` for real newlines. Never embed `\n`.
- Do not use `gh issue/pr comment -b "..."` when the body contains backticks or shell characters. Prefer a single-quoted heredoc.
- Do not wrap issue or PR refs like `#24643` in backticks when you want auto-linking.
- PR landing comments should include clickable full commit links for landed and source SHAs when present.
## Search broadly before deciding
- Prefer targeted keyword search before proposing new work or closing something as duplicate.
- Use `--repo openclaw/openclaw` with `--match title,body` first.
- Add `--match comments` when triaging follow-up discussion.
- Do not stop at the first 500 results when the task requires a full search.
Examples:
```bash
gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"
gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"
gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
--json number,title,state,url,updatedAt -- "auto update" \
--jq '.[] | "\(.number) | \(.state) | \(.title) | \(.url)"'
```
## Follow PR review and landing hygiene
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- When landing or merging any PR, follow the global `/landpr` process.
- Use `scripts/committer "<msg>" <file...>` for scoped commits instead of manual `git add` and `git commit`.
- Keep commit messages concise and action-oriented.
- Group related changes; avoid bundling unrelated refactors.
- Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues.
## Extra safety
- If a close or reopen action would affect more than 5 PRs, ask for explicit confirmation with the exact count and target query first.
- `sync` means: if the tree is dirty, commit all changes with a sensible Conventional Commit message, then `git pull --rebase`, then `git push`. Stop if rebase conflicts cannot be resolved safely.

View File

@@ -1,148 +0,0 @@
---
name: openclaw-qa-testing
description: Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
---
# OpenClaw QA Testing
Use this skill for `qa-lab` / `qa-channel` work. Repo-local QA only.
## Read first
- `docs/concepts/qa-e2e-automation.md`
- `docs/help/testing.md`
- `docs/channels/qa-channel.md`
- `qa/README.md`
- `qa/scenarios/index.md`
- `extensions/qa-lab/src/suite.ts`
- `extensions/qa-lab/src/character-eval.ts`
## Model policy
- Live OpenAI lane: `openai/gpt-5.4`
- Fast mode: on
- Do not use:
- `openai/gpt-5.4-pro`
- `openai/gpt-5.4-mini`
- Only change model policy if the user explicitly asks.
## Default workflow
1. Read the scenario pack and current suite implementation.
2. Decide lane:
- mock/dev: `mock-openai`
- real validation: `live-frontier`
3. For live OpenAI, use:
```bash
OPENCLAW_LIVE_OPENAI_KEY="${OPENAI_API_KEY}" \
pnpm openclaw qa suite \
--provider-mode live-frontier \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--output-dir .artifacts/qa-e2e/run-all-live-frontier-<tag>
```
4. Watch outputs:
- summary: `.artifacts/qa-e2e/run-all-live-frontier-<tag>/qa-suite-summary.json`
- report: `.artifacts/qa-e2e/run-all-live-frontier-<tag>/qa-suite-report.md`
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## Character evals
Use `qa character-eval` for style/persona/vibe checks across multiple live models.
```bash
pnpm openclaw qa character-eval \
--model openai/gpt-5.4,thinking=xhigh \
--model openai/gpt-5.2,thinking=xhigh \
--model openai/gpt-5,thinking=xhigh \
--model anthropic/claude-opus-4-6,thinking=high \
--model anthropic/claude-sonnet-4-6,thinking=high \
--model zai/glm-5.1,thinking=high \
--model moonshot/kimi-k2.5,thinking=high \
--model google/gemini-3.1-pro-preview,thinking=high \
--judge-model openai/gpt-5.4,thinking=xhigh,fast \
--judge-model anthropic/claude-opus-4-6,thinking=high \
--concurrency 16 \
--judge-concurrency 16 \
--output-dir .artifacts/qa-e2e/character-eval-<tag>
```
- Runs local QA gateway child processes, not Docker.
- Preferred model spec syntax is `provider/model,thinking=<level>[,fast|,no-fast|,fast=<bool>]` for both `--model` and `--judge-model`.
- Do not add new examples with separate `--model-thinking`; keep that flag as legacy compatibility only.
- Defaults to candidate models `openai/gpt-5.4`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-6`, `anthropic/claude-sonnet-4-6`, `zai/glm-5.1`, `moonshot/kimi-k2.5`, and `google/gemini-3.1-pro-preview` when no `--model` is passed.
- Candidate thinking defaults to `high`, with `xhigh` for OpenAI models that support it. Prefer inline `--model provider/model,thinking=<level>`; `--thinking <level>` and `--model-thinking <provider/model=level>` remain compatibility shims.
- OpenAI candidate refs default to fast mode so priority processing is used where supported. Use inline `,fast`, `,no-fast`, or `,fast=false` for one model; use `--fast` only to force fast mode for every candidate.
- Judges default to `openai/gpt-5.4,thinking=xhigh,fast` and `anthropic/claude-opus-4-6,thinking=high`.
- Report includes judge ranking, run stats, durations, and full transcripts; do not include raw judge replies. Duration is benchmark context, not a grading signal.
- Candidate and judge concurrency default to 16. Use `--concurrency <n>` and `--judge-concurrency <n>` to override when local gateways or provider limits need a gentler lane.
- Scenario source should stay markdown-driven under `qa/scenarios/`.
- For isolated character/persona evals, write the persona into `SOUL.md` and blank `IDENTITY.md` in the scenario flow. Use `SOUL.md + IDENTITY.md` only when intentionally testing how the normal OpenClaw identity combines with the character.
- Keep prompts natural and task-shaped. The candidate model should receive character setup through `SOUL.md`, then normal user turns such as chat, workspace help, and small file tasks; do not ask "how would you react?" or tell the model it is in an eval.
- Prefer at least one real task, such as creating or editing a tiny workspace artifact, so the transcript captures character under normal tool use instead of pure roleplay.
## Codex CLI model lane
Use model refs shaped like `codex-cli/<codex-model>` whenever QA should exercise Codex as a model backend.
Examples:
```bash
pnpm openclaw qa suite \
--provider-mode live-frontier \
--model codex-cli/<codex-model> \
--alt-model codex-cli/<codex-model> \
--scenario <scenario-id> \
--output-dir .artifacts/qa-e2e/codex-<tag>
```
```bash
pnpm openclaw qa manual \
--model codex-cli/<codex-model> \
--message "Reply exactly: CODEX_OK"
```
- Treat the concrete Codex model name as user/config input; do not hardcode it in source, docs examples, or scenarios.
- Live QA preserves `CODEX_HOME` so Codex CLI auth/config works while keeping `HOME` and `OPENCLAW_HOME` sandboxed.
- Mock QA should scrub `CODEX_HOME`.
- If Codex returns fallback/auth text every turn, first check `CODEX_HOME`, `~/.profile`, and gateway child logs before changing scenario assertions.
- For model comparison, include `codex-cli/<codex-model>` as another candidate in `qa character-eval`; the report should label it as an opaque model name.
## Repo facts
- Seed scenarios live in `qa/`.
- Main live runner: `extensions/qa-lab/src/suite.ts`
- QA lab server: `extensions/qa-lab/src/lab-server.ts`
- Child gateway harness: `extensions/qa-lab/src/gateway-child.ts`
- Synthetic channel: `extensions/qa-channel/`
## What “done” looks like
- Full suite green for the requested lane.
- User gets:
- watch URL if applicable
- pass/fail counts
- artifact paths
- concise note on what was fixed
## Common failure patterns
- Live timeout too short:
- widen live waits in `extensions/qa-lab/src/suite.ts`
- Discovery cannot find repo files:
- point prompts at `repo/...` inside seeded workspace
- Subagent proof too brittle:
- prefer stable final reply evidence over transient child-session listing
- Harness “rebuild” delay:
- dirty tree can trigger a pre-run build; expect that before ports appear
## When adding scenarios
- Add or update scenario markdown under `qa/scenarios/`
- Keep kickoff expectations in `qa/scenarios/index.md` aligned
- Add executable coverage in `extensions/qa-lab/src/suite.ts`
- Prefer end-to-end assertions over mock-only checks
- Save outputs under `.artifacts/qa-e2e/`

View File

@@ -1,4 +0,0 @@
interface:
display_name: "QA Test OpenClaw"
short_description: "Run and debug qa-lab and qa-channel scenarios"
default_prompt: "Use $openclaw-qa-testing to run or extend the OpenClaw QA suite with qa-lab and qa-channel, using regular openai/gpt-5.4 in fast mode for live OpenAI runs."

View File

@@ -1,303 +0,0 @@
---
name: openclaw-release-maintainer
description: Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
---
# OpenClaw Release Maintainer
Use this skill for release and publish-time workflow. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
## Respect release guardrails
- Do not change version numbers without explicit operator approval.
- Ask permission before any npm publish or release step.
- This skill should be sufficient to drive the normal release flow end-to-end.
- Use the private maintainer release docs for credentials, recovery steps, and mac signing/notary specifics, and use `docs/reference/RELEASING.md` for public policy.
- Core `openclaw` publish is manual `workflow_dispatch`; creating or pushing a tag does not publish by itself.
## Keep release channel naming aligned
- `stable`: tagged releases only, published to npm `beta` by default; operators may target npm `latest` explicitly or promote later
- `beta`: prerelease tags like `vYYYY.M.D-beta.N`, with npm dist-tag `beta`
- Prefer `-beta.N`; do not mint new `-1` or `-2` beta suffixes
- `dev`: moving head on `main`
- When using a beta Git tag, publish npm with the matching beta version suffix so the plain version is not consumed or blocked
## Handle versions and release files consistently
- Version locations include:
- `package.json`
- `apps/android/app/build.gradle.kts`
- `apps/ios/Sources/Info.plist`
- `apps/ios/Tests/Info.plist`
- `apps/macos/Sources/OpenClaw/Resources/Info.plist`
- `docs/install/updating.md`
- Peekaboo Xcode project and plist version fields
- Before creating a release tag, make every version location above match the version encoded by that tag.
- For fallback correction tags like `vYYYY.M.D-N`, the repo version locations still stay at `YYYY.M.D`.
- “Bump version everywhere” means all version locations above except `appcast.xml`.
- Release signing and notary credentials live outside the repo in the private maintainer docs.
- Every OpenClaw release ships the npm package and macOS app together.
- The production Sparkle feed lives at `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`, and the canonical published file is `appcast.xml` on `main` in the `openclaw` repo.
- That shared production Sparkle feed is stable-only. Beta mac releases may
upload assets to the GitHub prerelease, but they must not replace the shared
`appcast.xml` unless a separate beta feed exists.
- For fallback correction tags like `vYYYY.M.D-N`, the repo version still stays
at `YYYY.M.D`, but the mac release must use a strictly higher numeric
`APP_BUILD` / Sparkle build than the original release so existing installs
see it as newer.
## Build changelog-backed release notes
- Changelog entries should be user-facing, not internal release-process notes.
- When cutting a mac release with a beta GitHub prerelease:
- tag `vYYYY.M.D-beta.N` from the release commit
- create a prerelease titled `openclaw YYYY.M.D-beta.N`
- use release notes from the matching `CHANGELOG.md` version section
- attach at least the zip and dSYM zip, plus dmg if available
- Keep the top version entries in `CHANGELOG.md` sorted by impact:
- `### Changes` first
- `### Fixes` deduped with user-facing fixes first
## Run publish-time validation
Before tagging or publishing, run:
```bash
pnpm build
pnpm ui:build
pnpm release:check
pnpm test:install:smoke
```
For a non-root smoke path:
```bash
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
```
After npm publish, run:
```bash
node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
```
- This verifies the published registry install path in a fresh temp prefix.
- For stable correction releases like `YYYY.M.D-N`, it also verifies the
upgrade path from `YYYY.M.D` to `YYYY.M.D-N` so a correction publish cannot
silently leave existing global installs on the old base stable payload.
- Treat install smoke as a pack-budget gate too. `pnpm test:install:smoke`
now fails the candidate update tarball when npm reports an oversized
`unpackedSize`, so release-time e2e cannot miss pack bloat that would risk
low-memory install/startup failures.
- Keep direct npm global coverage enabled in install smoke. It exercises plain
`npm install -g <candidate>` fresh installs and npm-driven update installs,
because many users install with npm even when docs prefer pnpm.
- Use `pnpm test:live:media video` for bounded video-provider smoke when video
generation is in release scope. The default video smoke skips `fal`, runs one
text-to-video attempt per provider with a one-second lobster prompt, and caps
each provider operation with `OPENCLAW_LIVE_VIDEO_GENERATION_TIMEOUT_MS`
(`180000` by default).
- Run `pnpm test:live:media video --video-providers fal` only when FAL-specific
proof is required. Its queue latency can dominate release time.
- Set `OPENCLAW_LIVE_VIDEO_GENERATION_FULL_MODES=1` only when intentionally
validating the slower image-to-video and video-to-video transform lanes.
## Check all relevant release builds
- Always validate the OpenClaw npm release path before creating the tag.
- Default release checks:
- `pnpm check`
- `pnpm build`
- `pnpm ui:build`
- `pnpm release:check`
- `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke`
- Check all release-related build surfaces touched by the release, not only the npm package.
- For beta-style full e2e batteries, hard-cap top-level long lanes instead of letting them run indefinitely. Use host `timeout --foreground`/`gtimeout --foreground` caps such as:
- `45m` for `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke`
- `90m` for `pnpm test:docker:all`
- Parallels caps from the `openclaw-parallels-smoke` skill
If a lane hits its cap, stop and inspect/fix the affected lane before continuing; do not continue to wait on the same process.
- Actual npm install/update phases are capped at 5 minutes. If `npm install -g`, installer package install, or `openclaw update` takes longer than 300s in release e2e, stop treating the run as healthy progress and debug the installer/updater or harness.
- Serialize host build/package mutations ahead of VM lanes. Finish `pnpm build`, `pnpm ui:build`, `pnpm release:check`, install smoke, and any Docker/package-prep lanes before starting Parallels `npm pack` lanes; otherwise `dist` can disappear during VM pack prep and produce false failures.
- Include mac release readiness in preflight by running the public validation
workflow in `openclaw/openclaw` and the real mac preflight in
`openclaw/releases-private` for every release.
- Treat the `appcast.xml` update on `main` as part of mac release readiness, not an optional follow-up.
- The workflows remain tag-based. The agent is responsible for making sure
preflight runs complete successfully before any publish run starts.
- Any fix after preflight means a new commit. Delete and recreate the tag and
matching GitHub release from the fixed commit, then rerun preflight from
scratch before publishing.
- For stable mac releases, generate the signed `appcast.xml` before uploading
public release assets so the updater feed cannot lag the published binaries.
- Serialize stable appcast-producing runs across tags so two releases do not
generate replacement `appcast.xml` files from the same stale seed.
- For stable releases, confirm the latest beta already passed the broader release workflows before cutting stable.
- If any required build, packaging step, or release workflow is red, do not say the release is ready.
## Use the right auth flow
- OpenClaw publish uses GitHub trusted publishing.
- Stable npm promotion from `beta` to `latest` is an explicit mode on
`.github/workflows/openclaw-npm-release.yml`, but it still needs a valid
`NPM_TOKEN` because `npm dist-tag` management is separate from trusted
publishing.
- Direct stable publishes can also run the same workflow with
`sync_stable_dist_tags=true` to point both `latest` and `beta` at the
already-published stable version. This also needs the `npm-release`
environment approval and `NPM_TOKEN`.
- The publish run must be started manually with `workflow_dispatch`.
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
public release assets.
- Real npm publish requires a prior successful npm preflight run id so the
publish job promotes the prepared tarball instead of rebuilding it.
- Real private mac publish requires a prior successful private mac preflight
run id so the publish job promotes the prepared artifacts instead of
rebuilding or renotarizing them again.
- The private mac workflow also accepts `smoke_test_only=true` for branch-safe
workflow smoke tests that use ad-hoc signing, skip notarization, skip shared
appcast generation, and do not prove release readiness.
- `preflight_only=true` on the npm workflow is also the right way to validate an
existing tag after publish; it should keep running the build checks even when
the npm version is already published.
- Validation-only runs may be dispatched from a branch when you are testing a
workflow change before merge.
- `.github/workflows/macos-release.yml` in `openclaw/openclaw` is now a
public validation-only handoff. It validates the tag/release state and points
operators to the private repo. It still rebuilds the JS outputs needed for
release validation, but it does not sign, notarize, or publish macOS
artifacts.
- `openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
is the required private mac validation lane for `swift test`; keep it green
before any real mac publish run starts.
- Real mac preflight and real mac publish both use
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`.
- The private mac validation lane runs on GitHub's standard macOS runner.
- The private mac preflight path runs on GitHub's xlarge macOS runner and uses
a SwiftPM cache because the build/sign/notarize/package path is CPU-heavy.
- Private mac preflight uploads notarized build artifacts as workflow artifacts
instead of uploading public GitHub release assets.
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
workflow artifacts and intentionally skip stable `appcast.xml` generation.
- npm preflight, public mac validation, private mac validation, and private mac
preflight must all pass before any real publish run starts.
- Real publish runs must be dispatched from `main`; branch-dispatched publish
attempts should fail before the protected environment is reached.
- The release workflows stay tag-based; rely on the documented release sequence
rather than workflow-level SHA pinning.
- The `npm-release` environment must be approved by `@openclaw/openclaw-release-managers` before publish continues.
- Mac publish uses
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for
private mac preflight artifact preparation and real publish artifact
promotion.
- Real private mac publish uploads the packaged `.zip`, `.dmg`, and
`.dSYM.zip` assets to the existing GitHub release in `openclaw/openclaw`
automatically when `OPENCLAW_PUBLIC_REPO_RELEASE_TOKEN` is present in the
private repo `mac-release` environment.
- For stable releases, the agent must also download the signed
`macos-appcast-<tag>` artifact from the successful private mac workflow and
then update `appcast.xml` on `main`.
- For beta mac releases, do not update the shared production `appcast.xml`
unless a separate beta Sparkle feed exists.
- The private repo targets a dedicated `mac-release` environment. If the GitHub
plan does not yet support required reviewers there, do not assume the
environment alone is the approval boundary; rely on private repo access and
CODEOWNERS until those settings can be enabled.
- Do not use `NPM_TOKEN` or the plugin OTP flow for the OpenClaw package
publish path; package publishing uses trusted publishing.
- Use `NPM_TOKEN` only for explicit npm dist-tag management modes, because npm
does not support trusted publishing for `npm dist-tag add`.
- `@openclaw/*` plugin publishes use a separate maintainer-only flow.
- Only publish plugins that already exist on npm; bundled disk-tree-only plugins stay unpublished.
## Fallback local mac publish
- Keep the original local macOS publish workflow available as a fallback in case
CI/CD mac publishing is unavailable or broken.
- Preserve the existing maintainer workflow Peter uses: run it on a real Mac
with local signing, notary, and Sparkle credentials already configured.
- Follow the private maintainer macOS runbook for the local steps:
`scripts/package-mac-dist.sh` to build, sign, notarize, and package the app;
manual GitHub release asset upload; then `scripts/make_appcast.sh` plus the
`appcast.xml` commit to `main`.
- `scripts/package-mac-dist.sh` now fails closed for release builds if the
bundled app comes out with a debug bundle id, an empty Sparkle feed URL, or a
`CFBundleVersion` below the canonical Sparkle build floor for that short
version. For correction tags, set a higher explicit `APP_BUILD`.
- `scripts/make_appcast.sh` first uses `generate_appcast` from `PATH`, then
falls back to the SwiftPM Sparkle tool output under `apps/macos/.build`.
- For stable tags, the local fallback may update the shared production
`appcast.xml`.
- For beta tags, the local fallback still publishes the mac assets but must not
update the shared production `appcast.xml` unless a separate beta feed exists.
- Treat the local workflow as fallback only. Prefer the CI/CD publish workflow
when it is working.
- After any stable mac publish, verify all of the following before you call the
release finished:
- the GitHub release has `.zip`, `.dmg`, and `.dSYM.zip` assets
- `appcast.xml` on `main` points at the new stable zip
- the packaged app reports the expected short version and a numeric
`CFBundleVersion` at or above the canonical Sparkle build floor
## Run the release sequence
1. Confirm the operator explicitly wants to cut a release.
2. Choose the exact target version and git tag.
3. Make every repo version location match that tag before creating it.
4. Update `CHANGELOG.md` and assemble the matching GitHub release notes.
5. Run the full preflight for all relevant release builds, including mac readiness.
6. Confirm the target npm version is not already published.
7. Create and push the git tag.
8. Create or refresh the matching GitHub release.
9. Start `.github/workflows/openclaw-npm-release.yml` with `preflight_only=true`
and choose the intended `npm_dist_tag` (`beta` default; `latest` only for
an intentional direct stable publish). Wait for it to pass. Save that run id
because the real publish requires it to reuse the prepared npm tarball.
10. Start `.github/workflows/macos-release.yml` in `openclaw/openclaw` and wait
for the public validation-only run to pass.
11. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml`
with the same tag and wait for the private mac validation lane to pass.
12. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
with `preflight_only=true` and wait for it to pass. Save that run id because
the real publish requires it to reuse the notarized mac artifacts.
13. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and matching GitHub release, recreate them from the fixed
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes.
14. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
the real publish, choose `npm_dist_tag` (`beta` default, `latest` only when
you intentionally want direct stable publish), keep it the same as the
preflight run, and pass the successful npm `preflight_run_id`.
15. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
16. If the stable release was published to `beta`, start
`.github/workflows/openclaw-npm-release.yml` again after beta validation
passes with the same stable tag, `promote_beta_to_latest=true`,
`preflight_only=false`, empty `preflight_run_id`, and `npm_dist_tag=beta`,
then verify `latest` now points at that version.
17. If the stable release was published directly to `latest` and `beta` should
follow it, start `.github/workflows/openclaw-npm-release.yml` again with
the same stable tag, `sync_stable_dist_tags=true`,
`promote_beta_to_latest=false`, `preflight_only=false`, empty
`preflight_run_id`, and `npm_dist_tag=latest`, then verify both `latest`
and `beta` point at that version.
18. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
for the real publish with the successful private mac `preflight_run_id` and
wait for success.
19. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.
20. For stable releases, download `macos-appcast-<tag>` from the successful
private mac run, update `appcast.xml` on `main`, and verify the feed.
21. For beta releases, publish the mac assets but expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
22. After publish, verify npm and the attached release artifacts.
## GHSA advisory work
- Use `openclaw-ghsa-maintainer` for GHSA advisory inspection, patch/publish flow, private-fork validation, and GHSA API-specific publish checks.

View File

@@ -1,220 +0,0 @@
---
name: openclaw-secret-scanning-maintainer
description: Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
---
# OpenClaw Secret Scanning Maintainer
**Maintainer-only.** This skill requires repo admin / maintainer permissions to edit or delete other users' comments and resolve secret scanning alerts.
Use this skill when processing alerts from `https://github.com/openclaw/openclaw/security/secret-scanning`.
**Language rule:** All notification comments and replacement comments MUST be written in English.
## Script
All mechanical operations (API calls, temp file management, security enforcements) are handled by:
```
$REPO_ROOT/.agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs
```
The script enforces:
- `hide_secret=true` on all alert fetches (no plaintext secrets in stdout)
- `mktemp` with random UUIDs for all temp files
- `-F body=@file` for all body uploads (no inline shell quoting)
- Notification templates branched by location type
- Never prints `.secret` or `.body` to stdout
## Overall Flow
Supports single or multiple alerts. For multiple alerts, process in ascending order.
For each alert:
1. **Identify**`fetch-alert` + `fetch-content` to get metadata and body
2. **Decide** — Agent reads the body file, identifies all secrets, produces redacted version
3. **Redact**`redact-body` for issue/PR body; skip for comments (delete directly)
4. **Purge**`delete-comment` + `recreate-comment` for comments; cannot purge body history
5. **Notify**`notify` posts the right template per location type
6. **Resolve**`resolve` closes the alert
7. **Summary**`summary` prints formatted results
## Step 1: Identify
```bash
# List all open alerts
node secret-scanning.mjs list-open
# Fetch specific alert metadata + locations
node secret-scanning.mjs fetch-alert <NUMBER>
# Fetch content for each location (saves body to temp file)
node secret-scanning.mjs fetch-content '<location-json>'
```
The `fetch-content` output includes:
- `body_file`: path to temp file with full body content
- `author`: who posted it
- `issue_number` / `pr_number`: where it is
- `edit_history_count`: number of existing edits
- `type`: location type for routing
- For `discussion_comment`, it also includes `comment_node_id`, `discussion_node_id`, and `reply_to_node_id` when the original comment was a reply.
### Location type routing
| type | Flow |
| ----------------------------- | --------------------------------------------- |
| `issue_comment` | Comment: delete+recreate |
| `pull_request_comment` | Comment: delete+recreate |
| `pull_request_review_comment` | Comment: delete+recreate |
| `discussion_comment` | Discussion comment: delete+recreate (GraphQL) |
| `issue_body` | Body: redact in place |
| `pull_request_body` | Body: redact in place |
| `commit` | Notify only |
| _other_ | Skip and report |
## Step 2: Decide (Agent)
The agent reads the body file from `fetch-content` output and:
1. Identifies ALL secrets in the content (there may be more than the alert flagged)
2. Replaces each secret with `[REDACTED <secret_type>]`**no partial values, no prefix/suffix**
3. Saves the redacted content to a new temp file
This is the only step that requires semantic understanding. Everything else is mechanical.
## Step 3: Redact
### For comments (issue_comment / PR comments)
**Do NOT redact.** Skip directly to Step 4 (delete + recreate). PATCHing before DELETE creates an unnecessary edit history revision.
### For issue_body / pull_request_body
```bash
node secret-scanning.mjs redact-body <issue|pr> <NUMBER> <redacted-body-file>
```
## Step 4: Purge Edit History
### Comments — Delete and Recreate
For issue/PR comments:
```bash
# Delete original (all edit history gone)
node secret-scanning.mjs delete-comment <COMMENT_ID>
# Recreate with redacted content
node secret-scanning.mjs recreate-comment <ISSUE_NUMBER> <body-file>
```
For discussion comments (uses GraphQL):
```bash
# Delete original
node secret-scanning.mjs delete-discussion-comment <COMMENT_NODE_ID>
# Recreate with redacted content
node secret-scanning.mjs recreate-discussion-comment <DISCUSSION_NODE_ID> <body-file> [REPLY_TO_NODE_ID]
```
The `fetch-content` output for `discussion_comment` includes `comment_node_id` and `discussion_node_id` for these commands. When the original discussion comment was a reply, it also includes `reply_to_node_id`; pass that optional third argument so the redacted replacement stays in the original thread.
The recreated comment should follow this format:
```
> **Note from maintainer (@<LOGIN>):** The original comment by @<AUTHOR> has been removed due to secret leakage. Below is the redacted version of the original content.
---
<redacted original content>
```
### issue_body / pull_request_body — Cannot Purge
Editing creates an edit history revision with the pre-edit plaintext. This cannot be cleared via API.
**Output to maintainer terminal only (never in public comments):**
```
⚠️ Issue/PR body edit history still contains plaintext secrets.
Contact GitHub Support to purge: https://support.github.com/contact
Request purge of issue/PR #{NUMBER} userContentEdits.
```
> **CRITICAL:** Do NOT mention edit history or the "edited" button in any public comment or resolution_comment.
### Commits
Cannot clean. Notify author to delete branch or force-push (for unmerged PRs).
## Step 5: Notify
```bash
node secret-scanning.mjs notify <TARGET> <AUTHOR> <LOCATION_TYPE> <SECRET_TYPES> [REPLY_TO_NODE_ID]
```
- For non-discussion types, `<TARGET>` is the issue/PR number.
- For `discussion_comment`, `<TARGET>` is the `discussion_node_id` returned by `fetch-content`.
- For reply-style `discussion_comment` locations, pass the optional `reply_to_node_id` from `fetch-content` so the notification stays in the same thread.
Secret types are comma-separated: `"Discord Bot Token,Feishu App Secret"`
The script picks the right template:
- **comment types**: "your comment … removed and replaced"
- **body types**: "your issue/PR description … redacted in place"
- **commit**: "code you committed"
## Step 6: Resolve
```bash
node secret-scanning.mjs resolve <ALERT_NUMBER>
# or with custom resolution:
node secret-scanning.mjs resolve <ALERT_NUMBER> revoked "Custom comment"
```
Resolution is `revoked` by default. As maintainers we cannot control whether users rotate — our responsibility is to redact + notify. The `revoked` means "this secret should be considered leaked", not "I confirmed it was revoked".
## Step 7: Summary
After processing, create a JSON results file and pass it to the summary command:
```bash
node secret-scanning.mjs summary /tmp/results.json
```
The script outputs a block delimited by `---BEGIN SUMMARY---` and `---END SUMMARY---`. **You MUST output the content between these markers verbatim to the user. Do NOT rephrase, reformat, abbreviate, or create your own summary.** The script already includes full URLs for every alert and location.
The JSON format:
```json
[
{
"number": 72,
"secret_type": "Discord Bot Token",
"location_label": "Issue #63101 comment",
"location_url": "https://github.com/openclaw/openclaw/issues/63101#issuecomment-xxx",
"actions": "Deleted+Recreated+Notified",
"history_cleared": true
}
]
```
For unsupported types, add `"skipped": true, "unsupported_type": "<type>"`.
## Safety Rules
- **Agent reads content, identifies secrets, produces redaction.** Script handles all API calls.
- **Never include any portion of a secret** in public comments, redaction markers, or terminal output.
- **Never include alert URLs or numbers** in public comments.
- **For comments, skip PATCH — go directly to DELETE + recreate.**
- **Never mention edit history, "edited" button, or commit SHAs** in any public content.
- **Ask for confirmation** before deleting any comment.
- **One alert at a time** unless user requests batch.
- **All public comments in English.**
- **Skip unsupported location types** and report in summary.

View File

@@ -1,797 +0,0 @@
#!/usr/bin/env node
// Secret scanning alert handler for OpenClaw maintainers.
// Usage: node secret-scanning.mjs <command> [options]
import { execFileSync, spawnSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const REPO = "openclaw/openclaw";
const REPO_URL = `https://github.com/${REPO}`;
// ─── Helpers ────────────────────────────────────────────────────────────────
function fail(message) {
console.error(`error: ${message}`);
process.exit(1);
}
function tmpFile(purpose) {
const filePath = path.join(os.tmpdir(), `secretscan-${purpose}-${crypto.randomUUID()}`);
// 预创建文件,限制权限为 owner-only
fs.writeFileSync(filePath, "", { mode: 0o600 });
return filePath;
}
function gh(args, { json = true, allowFailure = false } = {}) {
const proc = spawnSync("gh", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
if (proc.status !== 0 && !allowFailure) {
fail(`gh ${args.slice(0, 3).join(" ")} failed:\n${(proc.stderr || proc.stdout || "").trim()}`);
}
if (proc.status !== 0) {
return {
gh_failed: true,
status: proc.status,
stdout: proc.stdout,
stderr: proc.stderr,
};
}
if (!json) return proc.stdout;
try {
return JSON.parse(proc.stdout);
} catch {
return proc.stdout;
}
}
function ghGraphQL(query, options = {}) {
return gh(["api", "graphql", "-f", `query=${query}`], options);
}
function failOnGraphQLFailure(result, message) {
if (result?.gh_failed) {
const details = (
result.stderr ||
result.stdout ||
`gh exited with status ${result.status}`
).trim();
fail(`${message}: ${details}`);
}
if (Array.isArray(result?.errors) && result.errors.length > 0) {
fail(`${message}: ${JSON.stringify(result.errors)}`);
}
}
function escapeGraphQLString(value) {
return String(value)
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\r/g, "\\r")
.replace(/\n/g, "\\n");
}
function formatGraphQLAfterClause(cursor) {
return cursor ? `, after: "${escapeGraphQLString(cursor)}"` : "";
}
function findDiscussionCommentNode(nodes, discussionCommentDbId) {
return nodes.find((node) => String(node.databaseId) === String(discussionCommentDbId)) || null;
}
function fetchDiscussionReplyPage(commentNodeId, cursor) {
const afterClause = formatGraphQLAfterClause(cursor);
return ghGraphQL(`{
node(id: "${escapeGraphQLString(commentNodeId)}") {
... on DiscussionComment {
replies(first: 100${afterClause}) {
pageInfo { hasNextPage endCursor }
nodes {
id
databaseId
author { login }
body
url
replyTo { id }
userContentEdits(first: 50) {
totalCount
}
}
}
}
}
}}`);
}
function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
const [owner, name] = REPO.split("/");
let discussionId = null;
let cursor = null;
let hasNextPage = true;
while (hasNextPage) {
const afterClause = formatGraphQLAfterClause(cursor);
const gql = ghGraphQL(
`{
repository(owner: "${owner}", name: "${name}") {
discussion(number: ${discussionNumber}) {
id
comments(first: 50${afterClause}) {
pageInfo { hasNextPage endCursor }
nodes {
id
databaseId
author { login }
body
url
replyTo { id }
userContentEdits(first: 50) {
totalCount
}
replies(first: 100) {
pageInfo { hasNextPage endCursor }
nodes {
id
databaseId
author { login }
body
url
replyTo { id }
userContentEdits(first: 50) {
totalCount
}
}
}
}
}
}
}
}`,
{ allowFailure: true },
);
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
const discussion = gql?.data?.repository?.discussion;
if (!discussion)
fail(
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
);
discussionId = discussion.id;
for (const topLevelComment of discussion.comments.nodes) {
if (String(topLevelComment.databaseId) === String(discussionCommentDbId)) {
return { discussionId, comment: topLevelComment };
}
let reply = findDiscussionCommentNode(topLevelComment.replies.nodes, discussionCommentDbId);
let replyCursor = topLevelComment.replies.pageInfo.endCursor;
let hasMoreReplies = topLevelComment.replies.pageInfo.hasNextPage;
while (!reply && hasMoreReplies) {
const replyPage = fetchDiscussionReplyPage(topLevelComment.id, replyCursor);
failOnGraphQLFailure(
replyPage,
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
);
const replies = replyPage?.data?.node?.replies;
if (!replies)
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
hasMoreReplies = replies.pageInfo.hasNextPage;
replyCursor = replies.pageInfo.endCursor;
}
if (reply) return { discussionId, comment: reply };
}
hasNextPage = discussion.comments.pageInfo.hasNextPage;
cursor = discussion.comments.pageInfo.endCursor;
}
return { discussionId, comment: null };
}
function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
const replyToClause = replyToNodeId ? `, replyToId: "${escapeGraphQLString(replyToNodeId)}"` : "";
const result = ghGraphQL(
`mutation { addDiscussionComment(input: { discussionId: "${escapeGraphQLString(discussionNodeId)}"${replyToClause}, body: "${escapeGraphQLString(body)}" }) { comment { id url } } }`,
);
if (result?.errors) {
fail(`Failed to create discussion comment: ${JSON.stringify(result.errors)}`);
}
return result?.data?.addDiscussionComment?.comment;
}
// ─── Commands ───────────────────────────────────────────────────────────────
/**
* fetch-alert <number>
* Fetch alert metadata + locations. Never exposes .secret.
*/
function cmdFetchAlert(alertNumber) {
if (!alertNumber) fail("Usage: fetch-alert <number>");
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
const locations = gh([
"api",
`repos/${REPO}/secret-scanning/alerts/${alertNumber}/locations`,
"--paginate",
"--slurp",
]);
// --paginate + --slurp 确保多页结果合并为一个 JSON 数组
const flatLocations = Array.isArray(locations?.[0])
? locations.flat()
: Array.isArray(locations)
? locations
: [];
const result = {
number: alert.number,
state: alert.state,
secret_type: alert.secret_type,
secret_type_display_name: alert.secret_type_display_name,
validity: alert.validity,
html_url: alert.html_url,
locations: flatLocations.map((loc) => ({
type: loc.type,
details: loc.details,
})),
};
console.log(JSON.stringify(result, null, 2));
}
/**
* fetch-content <location-json>
* Fetch the content and metadata for a specific location.
* Saves full body to a temp file. Prints metadata + file path to stdout.
*/
function cmdFetchContent(locationJson) {
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
const location = JSON.parse(locationJson);
const type = location.type;
const details = location.details;
if (type === "discussion_comment") {
const commentUrl = details.discussion_comment_url;
if (!commentUrl) fail("No discussion_comment_url in location details");
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
const discussionNumber = urlMatch[1];
const discussionCommentDbId = urlMatch[2];
const { discussionId, comment } = fetchDiscussionComment(
discussionNumber,
discussionCommentDbId,
);
if (!comment)
fail(
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
);
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
console.log(
JSON.stringify(
{
type,
comment_node_id: comment.id,
discussion_node_id: discussionId,
reply_to_node_id: comment.replyTo?.id ?? null,
discussion_number: Number(discussionNumber),
discussion_comment_db_id: Number(discussionCommentDbId),
author: comment.author?.login,
html_url: comment.url || commentUrl,
edit_history_count: comment.userContentEdits?.totalCount ?? 0,
body_file: bodyFile,
},
null,
2,
),
);
} else if (
type === "issue_comment" ||
type === "pull_request_comment" ||
type === "pull_request_review_comment"
) {
// Extract comment ID from URL
const commentUrl =
details.issue_comment_url ||
details.pull_request_comment_url ||
details.pull_request_review_comment_url;
if (!commentUrl) fail(`No comment URL in location details`);
const comment = gh(["api", commentUrl]);
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
// Fetch edit history
const nodeId = comment.node_id;
const typeName =
type === "pull_request_review_comment" ? "PullRequestReviewComment" : "IssueComment";
const gql = ghGraphQL(`{
node(id: "${nodeId}") {
... on ${typeName} {
userContentEdits(first: 50) {
totalCount
}
}
}
}`);
const editCount = gql?.data?.node?.userContentEdits?.totalCount ?? 0;
// Extract issue number from html_url
const htmlUrl = comment.html_url || details.html_url || "";
const issueMatch = htmlUrl.match(/\/(issues|pull)\/(\d+)/);
const issueNumber = issueMatch ? issueMatch[2] : null;
console.log(
JSON.stringify(
{
type,
comment_id: comment.id,
node_id: nodeId,
author: comment.user?.login,
issue_number: issueNumber,
html_url: htmlUrl,
edit_history_count: editCount,
body_file: bodyFile,
},
null,
2,
),
);
} else if (type === "issue_body") {
const issueUrl = details.issue_body_url || details.issue_url;
if (!issueUrl) fail("No issue URL in location details");
const issue = gh(["api", issueUrl]);
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, issue.body || "");
const nodeId = issue.node_id;
const number = issue.number;
const gql = ghGraphQL(`{
node(id: "${nodeId}") {
... on Issue {
userContentEdits(first: 50) {
totalCount
}
}
}
}`);
const editCount = gql?.data?.node?.userContentEdits?.totalCount ?? 0;
console.log(
JSON.stringify(
{
type,
issue_number: number,
node_id: nodeId,
author: issue.user?.login,
html_url: issue.html_url,
edit_history_count: editCount,
body_file: bodyFile,
},
null,
2,
),
);
} else if (type === "pull_request_body") {
const prUrl = details.pull_request_body_url || details.pull_request_url;
if (!prUrl) fail("No PR URL in location details");
const pr = gh(["api", prUrl]);
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, pr.body || "");
const nodeId = pr.node_id;
const number = pr.number;
const gql = ghGraphQL(`{
node(id: "${nodeId}") {
... on PullRequest {
userContentEdits(first: 50) {
totalCount
}
}
}
}`);
const editCount = gql?.data?.node?.userContentEdits?.totalCount ?? 0;
console.log(
JSON.stringify(
{
type,
pr_number: number,
node_id: nodeId,
author: pr.user?.login,
merged: pr.merged,
state: pr.state,
html_url: pr.html_url,
edit_history_count: editCount,
body_file: bodyFile,
},
null,
2,
),
);
} else if (type === "commit") {
console.log(
JSON.stringify(
{
type,
commit_sha: details.commit_sha,
path: details.path,
start_line: details.start_line,
end_line: details.end_line,
html_url: details.html_url || details.commit_url || details.blob_url || null,
// No body file for commits
body_file: null,
},
null,
2,
),
);
} else {
console.log(
JSON.stringify(
{
type,
unsupported: true,
details,
},
null,
2,
),
);
}
}
/**
* redact-body <issue|pr> <number> <redacted-body-file>
* PATCH the issue or PR body with redacted content from a file.
*/
function cmdRedactBody(kind, number, bodyFile) {
if (!kind || !number || !bodyFile) {
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
}
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const endpoint =
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
gh(["api", endpoint, "-X", "PATCH", "-F", `body=@${bodyFile}`]);
console.log(JSON.stringify({ ok: true, kind, number: Number(number) }));
}
/**
* delete-comment <comment-id>
* Delete a comment (and all its edit history).
*/
function cmdDeleteComment(commentId) {
if (!commentId) fail("Usage: delete-comment <comment-id>");
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
}
/**
* delete-discussion-comment <node-id>
* Delete a discussion comment via GraphQL (and all its edit history).
*/
function cmdDeleteDiscussionComment(nodeId) {
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
const result = ghGraphQL(
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
);
if (result?.errors) {
fail(`Failed to delete discussion comment: ${JSON.stringify(result.errors)}`);
}
console.log(JSON.stringify({ ok: true, deleted_node_id: nodeId }));
}
/**
* recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]
* Create a new discussion comment via GraphQL.
*/
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
if (!discussionNodeId || !bodyFile)
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const body = fs.readFileSync(bodyFile, "utf8");
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
console.log(
JSON.stringify({
ok: true,
node_id: newComment?.id,
html_url: newComment?.url,
}),
);
}
/**
* recreate-comment <issue-number> <body-file>
* Create a new comment from a file.
*/
function cmdRecreateComment(issueNumber, bodyFile) {
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const result = gh([
"api",
`repos/${REPO}/issues/${issueNumber}/comments`,
"-X",
"POST",
"-F",
`body=@${bodyFile}`,
]);
console.log(
JSON.stringify({
ok: true,
comment_id: result.id,
html_url: result.html_url,
}),
);
}
/**
* notify <target> <author> <location-type> <secret-types> [reply-to-node-id]
* Post a notification comment with the correct template for the location type.
* target = issue/PR number for non-discussion types, discussion node ID for discussion_comment.
*/
function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
if (!target || !author || !locationType || !secretTypes) {
fail(
"Usage: notify <target> <author> <location-type> <secret-types-comma-sep> [reply-to-node-id]",
);
}
const types = secretTypes.split(",").map((s) => s.trim());
const typeList = types.map((t, i) => `${i + 1}. **${t}**`).join("\n");
let locationDesc;
let actionDesc;
if (
locationType === "issue_comment" ||
locationType === "pull_request_comment" ||
locationType === "pull_request_review_comment" ||
locationType === "discussion_comment"
) {
locationDesc = "your comment";
actionDesc = "The affected comment has been removed and replaced with a redacted version.";
} else if (locationType === "issue_body") {
locationDesc = "your issue description";
actionDesc = "The affected content has been redacted in place.";
} else if (locationType === "pull_request_body") {
locationDesc = "your pull request description";
actionDesc = "The affected content has been redacted in place.";
} else if (locationType === "commit") {
locationDesc = "code you committed";
actionDesc = "";
} else {
locationDesc = "your content";
actionDesc = "";
}
const body = [
`@${author} :warning: **Security Notice: Secret Leakage Detected**`,
"",
`GitHub Secret Scanning detected the following exposed secret types in ${locationDesc}:`,
"",
typeList,
"",
actionDesc,
"",
"**Please rotate these credentials immediately.**",
"",
"These secrets were publicly exposed and should be considered compromised.",
]
.filter((line) => line !== undefined)
.join("\n");
// Discussion comments must be notified via GraphQL
if (locationType === "discussion_comment") {
const newComment = createDiscussionComment(target, body, replyToNodeId);
console.log(
JSON.stringify({
ok: true,
node_id: newComment?.id,
html_url: newComment?.url,
}),
);
return;
}
// Issue/PR comments via REST
const bodyFile = tmpFile("notify.md");
fs.writeFileSync(bodyFile, body);
const result = gh([
"api",
`repos/${REPO}/issues/${target}/comments`,
"-X",
"POST",
"-F",
`body=@${bodyFile}`,
]);
console.log(
JSON.stringify({
ok: true,
comment_id: result.id,
html_url: result.html_url,
}),
);
}
/**
* resolve <alert-number> [resolution] [comment]
* Close a secret scanning alert.
*/
function cmdResolve(alertNumber, resolution, comment) {
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
const res = resolution || "revoked";
const resComment = comment || "Content redacted and author notified to rotate credentials.";
const result = gh([
"api",
`repos/${REPO}/secret-scanning/alerts/${alertNumber}`,
"-X",
"PATCH",
"-f",
`state=resolved`,
"-f",
`resolution=${res}`,
"-f",
`resolution_comment=${resComment}`,
]);
console.log(
JSON.stringify({
ok: true,
number: result.number,
state: result.state,
resolution: result.resolution,
resolved_at: result.resolved_at,
}),
);
}
/**
* list-open
* List all open secret scanning alerts.
*/
function cmdListOpen() {
const alerts = gh([
"api",
`repos/${REPO}/secret-scanning/alerts?hide_secret=true&state=open`,
"--paginate",
"--slurp",
]);
// --slurp 将分页结果合并为 [[page1], [page2], ...] 需要 flat
const flat = Array.isArray(alerts?.[0]) ? alerts.flat() : Array.isArray(alerts) ? alerts : [];
const rows = flat.map((a) => ({
number: a.number,
secret_type_display_name: a.secret_type_display_name,
html_url: a.html_url,
first_location_html_url: a.first_location_detected?.html_url || null,
}));
console.log(JSON.stringify(rows, null, 2));
}
/**
* summary <json-file>
* Print a formatted summary table from a JSON results file.
*/
function cmdSummary(jsonFile) {
if (!jsonFile) fail("Usage: summary <json-file>");
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
const lines = [];
lines.push("---BEGIN SUMMARY---");
lines.push("");
lines.push("## Secret Scanning Results");
lines.push("");
lines.push("| Alert | Type | Location | Actions | Edit History |");
lines.push("|-------|------|----------|---------|--------------|");
const needsPurge = [];
for (const r of results) {
const alertLink = `#${r.number} ${REPO_URL}/security/secret-scanning/${r.number}`;
const locationLink = r.location_url
? `${r.location_label} ${r.location_url}`
: r.location_label;
const history = r.history_cleared ? "Cleared" : "⚠️ History remains";
lines.push(`| ${alertLink} | ${r.secret_type} | ${locationLink} | ${r.actions} | ${history} |`);
if (!r.history_cleared && r.location_url) {
needsPurge.push(r);
}
}
if (needsPurge.length > 0) {
lines.push("");
lines.push("Issues requiring GitHub Support to purge edit history:");
for (const r of needsPurge) {
lines.push(`- ${r.location_label} ${r.location_url}${r.secret_type}`);
}
lines.push(
`Contact: https://support.github.com/contact — request purge of userContentEdits for the above issues.`,
);
}
const skipped = results.filter((r) => r.skipped);
if (skipped.length > 0) {
lines.push("");
lines.push(
"⚠️ The following alerts were skipped because their location type is not supported:",
);
for (const r of skipped) {
lines.push(
`- Alert #${r.number}: unsupported type "${r.unsupported_type}" — ${REPO_URL}/security/secret-scanning/${r.number}`,
);
}
lines.push("Please update the skill to define handling for these types.");
}
lines.push("");
lines.push("---END SUMMARY---");
console.log(lines.join("\n"));
}
// ─── Dispatch ───────────────────────────────────────────────────────────────
const [command, ...args] = process.argv.slice(2);
const commands = {
"fetch-alert": () => cmdFetchAlert(args[0]),
"fetch-content": () => cmdFetchContent(args[0]),
"redact-body": () => cmdRedactBody(args[0], args[1], args[2]),
"delete-comment": () => cmdDeleteComment(args[0]),
"delete-discussion-comment": () => cmdDeleteDiscussionComment(args[0]),
"recreate-comment": () => cmdRecreateComment(args[0], args[1]),
"recreate-discussion-comment": () => cmdRecreateDiscussionComment(args[0], args[1], args[2]),
notify: () => cmdNotify(args[0], args[1], args[2], args[3], args[4]),
resolve: () => cmdResolve(args[0], args[1], args[2]),
"list-open": () => cmdListOpen(),
summary: () => cmdSummary(args[0]),
};
if (!command || !commands[command]) {
console.error(
[
"Usage: node secret-scanning.mjs <command> [args]",
"",
"Commands:",
" fetch-alert <number> Fetch alert metadata + locations",
" fetch-content '<location-json>' Fetch content for a location",
" redact-body <issue|pr> <n> <file> PATCH body with redacted file",
" delete-comment <comment-id> Delete a comment",
" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
" recreate-comment <issue-n> <file> Create replacement comment",
" recreate-discussion-comment <disc-node-id> <file> [reply-to-node-id] Create discussion comment (GraphQL)",
" notify <target> <author> <type> <types> [reply-to-node-id] Post notification",
" resolve <n> [resolution] [comment] Close alert",
" list-open List open alerts",
" summary <json-file> Print formatted summary",
].join("\n"),
);
process.exit(1);
}
commands[command]();

View File

@@ -1,75 +0,0 @@
---
name: openclaw-test-heap-leaks
description: Investigate `pnpm test` memory growth, Vitest worker OOMs, and suspicious RSS increases in OpenClaw using the `scripts/test-parallel.mjs` heap snapshot tooling. Use when Codex needs to reproduce test-lane memory growth, collect repeated `.heapsnapshot` files, compare snapshots from the same worker PID, triage likely transformed-module retention versus likely runtime leaks, and fix or reduce the impact by patching cleanup logic or isolating hotspot tests.
---
# OpenClaw Test Heap Leaks
Use this skill for test-memory investigations. Do not guess from RSS alone when heap snapshots are available. Treat snapshot-name deltas as triage evidence, not proof, until retainers or dominators support the call.
## Workflow
1. Reproduce the failing shape first.
- Match the real entrypoint if possible. For Linux CI-style unit failures, start with:
- `pnpm canvas:a2ui:bundle && OPENCLAW_TEST_MEMORY_TRACE=1 OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap OPENCLAW_TEST_WORKERS=2 OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 pnpm test`
- Keep `OPENCLAW_TEST_MEMORY_TRACE=1` enabled so the wrapper prints per-file RSS summaries alongside the snapshots.
- If the report is about a specific shard or worker budget, preserve that shape.
- Before you analyze snapshots, identify the real lane names from `[test-parallel] start ...` lines or `pnpm test --plan`. Do not assume a single `unit-fast` lane; local plans often split into `unit-fast-batch-*`.
2. Wait for repeated snapshots before concluding anything.
- Take at least two intervals from the same lane.
- Compare snapshots from the same PID inside the real lane directory such as `.tmp/heapsnap/unit-fast-batch-2/`.
- Use `.agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs` to compare either two files directly or the earliest/latest pair per PID in one lane directory.
- If the helper suggests transformed-module retention, confirm the top entries in DevTools retainers/dominators before calling it solved.
3. Classify the growth before choosing a fix.
- If growth is dominated by Vite/Vitest transformed source strings, `Module`, `system / Context`, bytecode, descriptor arrays, or property maps, treat it as likely retained module graph growth in long-lived workers.
- If growth is dominated by app objects, caches, buffers, server handles, timers, mock state, sqlite state, or similar runtime objects, treat it as a likely cleanup or lifecycle leak.
- If the names are ambiguous, stop short of a confident label and inspect retainers/dominators in DevTools for the top deltas.
4. Fix the right layer.
- For likely retained transformed-module growth in shared workers:
- Prefer timing and hotspot-driven scheduling fixes first. Check whether the file is already represented in `test/fixtures/test-timings.unit.json` and whether `scripts/test-update-memory-hotspots.mjs` should refresh the measured hotspot manifest before hand-editing behavior overrides.
- Move hotspot files out of the real shared lane by updating `test/fixtures/test-parallel.behavior.json` only when timing-driven peeling is insufficient.
- Prefer `singletonIsolated` for files that are safe alone but inflate shared worker heaps.
- If the file should already have been peeled out by timings but is absent from `test/fixtures/test-timings.unit.json`, call that out explicitly. Missing timings are a scheduling blind spot.
- For real leaks:
- Patch the implicated test or runtime cleanup path.
- Look for missing `afterEach`/`afterAll`, module-reset gaps, retained global state, unreleased DB handles, or listeners/timers that survive the file.
5. Verify with the most direct proof.
- Re-run the targeted lane or file with heap snapshots enabled if the suite still finishes in reasonable time.
- If snapshot overhead pushes tests over Vitest timeouts, fall back to the same lane without snapshots and confirm the RSS trend or OOM is reduced.
- For wrapper-only changes, at minimum verify the expected lanes start and the snapshot files are written.
## Heuristics
- Do not call everything a leak. In this repo, large `unit-fast` or `unit-fast-batch-*` growth can be a worker-lifetime problem rather than an application object leak.
- `scripts/test-parallel.mjs` and `scripts/test-parallel-memory.mjs` are the primary control points for wrapper diagnostics.
- The lane names printed by `[test-parallel] start ...` and `[test-parallel][mem] summary ...` tell you where to focus.
- When one or two files account for most of the delta and they are missing from timings, reducing impact by isolating them is usually the first pragmatic fix.
- When the same retained object families grow across multiple intervals in the same worker PID, trust the snapshots over intuition, then confirm ambiguous calls with retainer evidence.
## Snapshot Comparison
- Direct comparison:
- `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs before.heapsnapshot after.heapsnapshot`
- Auto-select earliest/latest snapshots per PID within one lane:
- `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs --lane-dir .tmp/heapsnap/unit-fast-batch-2`
- Useful flags:
- `--top 40`
- `--min-kb 32`
- `--pid 16133`
Read the top positive deltas first. Large positive growth in module-transform artifacts suggests lane isolation; large positive growth in runtime objects suggests a real leak. If the names alone do not settle it, open the same snapshot pair in DevTools and inspect retainers/dominators for the top rows before declaring root cause.
## Output Expectations
When using this skill, report:
- The exact reproduce command.
- Which lane and PID were compared.
- The dominant retained object families from the snapshot delta.
- Whether the issue is a likely real leak or likely shared-worker retained module growth, plus whether retainers/dominators confirmed it.
- The concrete fix or impact-reduction patch.
- What you verified, and what snapshot overhead prevented you from verifying.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Test Heap Leaks"
short_description: "Investigate test OOMs with heap snapshots"
default_prompt: "Use $openclaw-test-heap-leaks to investigate test memory growth with heap snapshots and reduce its impact."

View File

@@ -1,553 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
function printUsage() {
console.error(
"Usage: node heapsnapshot-delta.mjs <before.heapsnapshot> <after.heapsnapshot> [--top N] [--min-kb N]",
);
console.error(
" or: node heapsnapshot-delta.mjs --lane-dir <dir> [--pid PID] [--top N] [--min-kb N]",
);
}
function fail(message) {
console.error(message);
process.exit(1);
}
function parseArgs(argv) {
const options = {
top: 30,
minKb: 64,
laneDir: null,
pid: null,
files: [],
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--top") {
options.top = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
if (arg === "--min-kb") {
options.minKb = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
if (arg === "--lane-dir") {
options.laneDir = argv[index + 1] ?? null;
index += 1;
continue;
}
if (arg === "--pid") {
options.pid = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
options.files.push(arg);
}
if (!Number.isFinite(options.top) || options.top <= 0) {
fail("--top must be a positive integer");
}
if (!Number.isFinite(options.minKb) || options.minKb < 0) {
fail("--min-kb must be a non-negative integer");
}
if (options.pid !== null && (!Number.isInteger(options.pid) || options.pid <= 0)) {
fail("--pid must be a positive integer");
}
return options;
}
class JsonStreamScanner {
constructor(filePath) {
this.stream = fs.createReadStream(filePath, {
encoding: "utf8",
highWaterMark: 1024 * 1024,
});
this.iterator = this.stream[Symbol.asyncIterator]();
this.buffer = "";
this.offset = 0;
this.done = false;
}
compactBuffer() {
if (this.offset > 65536) {
this.buffer = this.buffer.slice(this.offset);
this.offset = 0;
}
}
async ensureAvailable(count = 1) {
while (!this.done && this.buffer.length - this.offset < count) {
const next = await this.iterator.next();
if (next.done) {
this.done = true;
break;
}
this.buffer += next.value;
}
}
async peek() {
await this.ensureAvailable(1);
return this.buffer[this.offset] ?? null;
}
async next() {
await this.ensureAvailable(1);
if (this.offset >= this.buffer.length) {
return null;
}
const char = this.buffer[this.offset];
this.offset += 1;
this.compactBuffer();
return char;
}
async skipWhitespace() {
while (true) {
const char = await this.peek();
if (char === null || !/\s/u.test(char)) {
return;
}
await this.next();
}
}
async expectChar(expected) {
const char = await this.next();
if (char !== expected) {
fail(`Expected ${expected} but found ${char ?? "<eof>"}`);
}
}
async find(sequence) {
let matched = 0;
while (true) {
const char = await this.next();
if (char === null) {
fail(`Could not find ${sequence}`);
}
if (char === sequence[matched]) {
matched += 1;
if (matched === sequence.length) {
return;
}
continue;
}
matched = char === sequence[0] ? 1 : 0;
if (matched === sequence.length) {
return;
}
}
}
async readBalancedObject() {
const start = await this.next();
if (start !== "{") {
fail(`Expected { but found ${start ?? "<eof>"}`);
}
let text = "{";
let depth = 1;
let inString = false;
let escaped = false;
while (depth > 0) {
const char = await this.next();
if (char === null) {
fail("Unexpected EOF while reading JSON object");
}
text += char;
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
} else if (char === "{") {
depth += 1;
} else if (char === "}") {
depth -= 1;
}
}
return text;
}
async parseNumberArray(onValue) {
await this.skipWhitespace();
await this.expectChar("[");
await this.skipWhitespace();
if ((await this.peek()) === "]") {
await this.next();
return;
}
let token = "";
let index = 0;
const flush = () => {
if (token.length === 0) {
fail("Unexpected empty number token");
}
const value = Number.parseInt(token, 10);
if (!Number.isFinite(value)) {
fail(`Invalid numeric token: ${token}`);
}
onValue(value, index);
index += 1;
token = "";
};
while (true) {
const char = await this.next();
if (char === null) {
fail("Unexpected EOF while reading number array");
}
if (char === "]") {
flush();
return;
}
if (char === ",") {
flush();
continue;
}
if (/\s/u.test(char)) {
continue;
}
token += char;
}
}
async readJsonString() {
await this.expectChar('"');
let value = "";
while (true) {
const char = await this.next();
if (char === null) {
fail("Unexpected EOF while reading JSON string");
}
if (char === '"') {
return value;
}
if (char !== "\\") {
value += char;
continue;
}
const escaped = await this.next();
if (escaped === null) {
fail("Unexpected EOF while reading JSON string escape");
}
if (escaped === "u") {
let hex = "";
for (let index = 0; index < 4; index += 1) {
const hexChar = await this.next();
if (hexChar === null) {
fail("Unexpected EOF while reading JSON unicode escape");
}
hex += hexChar;
}
value += String.fromCharCode(Number.parseInt(hex, 16));
continue;
}
value +=
escaped === "b"
? "\b"
: escaped === "f"
? "\f"
: escaped === "n"
? "\n"
: escaped === "r"
? "\r"
: escaped === "t"
? "\t"
: escaped;
}
}
async parseStringArray(onValue) {
await this.skipWhitespace();
await this.expectChar("[");
await this.skipWhitespace();
if ((await this.peek()) === "]") {
await this.next();
return;
}
let index = 0;
while (true) {
const value = await this.readJsonString();
onValue(value, index);
index += 1;
await this.skipWhitespace();
const separator = await this.next();
if (separator === "]") {
return;
}
if (separator !== ",") {
fail(`Expected , or ] but found ${separator ?? "<eof>"}`);
}
await this.skipWhitespace();
}
}
}
function parseHeapFilename(filePath) {
const base = path.basename(filePath);
const match = base.match(
/^Heap\.(?<stamp>\d{8}\.\d{6})\.(?<pid>\d+)\.0\.(?<seq>\d+)\.heapsnapshot$/u,
);
if (!match?.groups) {
return null;
}
return {
filePath,
pid: Number.parseInt(match.groups.pid, 10),
stamp: match.groups.stamp,
sequence: Number.parseInt(match.groups.seq, 10),
};
}
function resolvePair(options) {
if (options.laneDir) {
const entries = fs
.readdirSync(options.laneDir)
.map((name) => parseHeapFilename(path.join(options.laneDir, name)))
.filter((entry) => entry !== null)
.filter((entry) => options.pid === null || entry.pid === options.pid)
.toSorted((left, right) => {
if (left.pid !== right.pid) {
return left.pid - right.pid;
}
if (left.stamp !== right.stamp) {
return left.stamp.localeCompare(right.stamp);
}
return left.sequence - right.sequence;
});
if (entries.length === 0) {
fail(`No matching heap snapshots found in ${options.laneDir}`);
}
const groups = new Map();
for (const entry of entries) {
const group = groups.get(entry.pid) ?? [];
group.push(entry);
groups.set(entry.pid, group);
}
const candidates = Array.from(groups.values())
.map((group) => ({
pid: group[0].pid,
before: group[0],
after: group.at(-1),
count: group.length,
}))
.filter((entry) => entry.count >= 2);
if (candidates.length === 0) {
fail(`Need at least two snapshots for one PID in ${options.laneDir}`);
}
const chosen =
options.pid !== null
? (candidates.find((entry) => entry.pid === options.pid) ?? null)
: candidates.toSorted((left, right) => right.count - left.count || left.pid - right.pid)[0];
if (!chosen) {
fail(`No PID with at least two snapshots matched in ${options.laneDir}`);
}
return {
before: chosen.before.filePath,
after: chosen.after.filePath,
pid: chosen.pid,
snapshotCount: chosen.count,
};
}
if (options.files.length !== 2) {
printUsage();
process.exit(1);
}
return {
before: options.files[0],
after: options.files[1],
pid: null,
snapshotCount: 2,
};
}
async function parseSnapshotMeta(scanner) {
await scanner.find('"snapshot":');
await scanner.skipWhitespace();
const metaObjectText = await scanner.readBalancedObject();
const parsed = JSON.parse(metaObjectText);
return parsed?.meta ?? null;
}
async function buildSummary(filePath) {
const scanner = new JsonStreamScanner(filePath);
const meta = await parseSnapshotMeta(scanner);
if (!meta) {
fail(`Invalid heap snapshot: ${filePath}`);
}
const nodeFieldCount = meta.node_fields.length;
const typeNames = meta.node_types[0];
const typeIndex = meta.node_fields.indexOf("type");
const nameIndex = meta.node_fields.indexOf("name");
const selfSizeIndex = meta.node_fields.indexOf("self_size");
if (typeIndex === -1 || nameIndex === -1 || selfSizeIndex === -1) {
fail(`Unsupported heap snapshot schema: ${filePath}`);
}
const summaryByIndex = new Map();
let nodeCount = 0;
let currentTypeId = 0;
let currentNameId = 0;
let currentSelfSize = 0;
await scanner.find('"nodes":');
await scanner.parseNumberArray((value, index) => {
const fieldIndex = index % nodeFieldCount;
if (fieldIndex === typeIndex) {
currentTypeId = value;
return;
}
if (fieldIndex === nameIndex) {
currentNameId = value;
return;
}
if (fieldIndex === selfSizeIndex) {
currentSelfSize = value;
}
if (fieldIndex !== nodeFieldCount - 1) {
return;
}
const key = `${currentTypeId}\t${currentNameId}`;
const current = summaryByIndex.get(key) ?? {
typeId: currentTypeId,
nameId: currentNameId,
selfSize: 0,
count: 0,
};
current.selfSize += currentSelfSize;
current.count += 1;
summaryByIndex.set(key, current);
nodeCount += 1;
});
const requiredNameIds = new Set(
Array.from(summaryByIndex.values(), (entry) => entry.nameId).filter((value) => value >= 0),
);
const nameStrings = new Map();
await scanner.find('"strings":');
await scanner.parseStringArray((value, index) => {
if (requiredNameIds.has(index)) {
nameStrings.set(index, value);
}
});
const summary = new Map();
for (const entry of summaryByIndex.values()) {
const key = `${typeNames[entry.typeId] ?? "unknown"}\t${nameStrings.get(entry.nameId) ?? ""}`;
summary.set(key, {
type: typeNames[entry.typeId] ?? "unknown",
name: nameStrings.get(entry.nameId) ?? "",
selfSize: entry.selfSize,
count: entry.count,
});
}
return {
nodeCount,
summary,
};
}
function formatBytes(bytes) {
if (Math.abs(bytes) >= 1024 ** 2) {
return `${(bytes / 1024 ** 2).toFixed(2)} MiB`;
}
if (Math.abs(bytes) >= 1024) {
return `${(bytes / 1024).toFixed(1)} KiB`;
}
return `${bytes} B`;
}
function formatDelta(bytes) {
return `${bytes >= 0 ? "+" : "-"}${formatBytes(Math.abs(bytes))}`;
}
function truncate(text, maxLength) {
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}`;
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const pair = resolvePair(options);
const before = await buildSummary(pair.before);
const after = await buildSummary(pair.after);
const minBytes = options.minKb * 1024;
const rows = [];
for (const [key, next] of after.summary) {
const previous = before.summary.get(key) ?? { selfSize: 0, count: 0 };
const sizeDelta = next.selfSize - previous.selfSize;
const countDelta = next.count - previous.count;
if (sizeDelta < minBytes) {
continue;
}
rows.push({
type: next.type,
name: next.name,
sizeDelta,
countDelta,
afterSize: next.selfSize,
afterCount: next.count,
});
}
rows.sort(
(left, right) => right.sizeDelta - left.sizeDelta || right.countDelta - left.countDelta,
);
console.log(`before: ${pair.before}`);
console.log(`after: ${pair.after}`);
if (pair.pid !== null) {
console.log(`pid: ${pair.pid} (${pair.snapshotCount} snapshots found)`);
}
console.log(
`nodes: ${before.nodeCount} -> ${after.nodeCount} (${after.nodeCount - before.nodeCount >= 0 ? "+" : ""}${after.nodeCount - before.nodeCount})`,
);
console.log(`filter: top=${options.top} min=${options.minKb} KiB`);
console.log("");
if (rows.length === 0) {
console.log("No entries exceeded the minimum delta.");
return;
}
for (const row of rows.slice(0, options.top)) {
console.log(
[
formatDelta(row.sizeDelta).padStart(11),
`count ${row.countDelta >= 0 ? "+" : ""}${row.countDelta}`.padStart(10),
row.type.padEnd(16),
truncate(row.name || "(empty)", 96),
].join(" "),
);
}
}
await main();

View File

@@ -1,62 +0,0 @@
---
name: parallels-discord-roundtrip
description: Run the macOS Parallels smoke harness with Discord end-to-end roundtrip verification, including guest send, host verification, host reply, and guest readback.
---
# Parallels Discord Roundtrip
Use when macOS Parallels smoke must prove Discord two-way delivery end to end.
## Goal
Cover:
- install on fresh macOS snapshot
- onboard + gateway health
- guest `message send` to Discord
- host sees that message on Discord
- host posts a new Discord message
- guest `message read` sees that new message
## Inputs
- host env var with Discord bot token
- Discord guild ID
- Discord channel ID
- `OPENAI_API_KEY`
## Preferred run
```bash
export OPENCLAW_PARALLELS_DISCORD_TOKEN="$(
ssh peters-mac-studio-1 'jq -r ".channels.discord.token" ~/.openclaw/openclaw.json' | tr -d '\n'
)"
pnpm test:parallels:macos \
--discord-token-env OPENCLAW_PARALLELS_DISCORD_TOKEN \
--discord-guild-id 1456350064065904867 \
--discord-channel-id 1456744319972282449 \
--json
```
## Notes
- Snapshot target: closest to `macOS 26.3.1 fresh`.
- Snapshot resolver now prefers matching `*-poweroff*` clones when the base hint also matches. That lets the harness reuse disk-only recovery snapshots without passing a longer hint.
- If Windows/Linux snapshot restore logs show `PET_QUESTION_SNAPSHOT_STATE_INCOMPATIBLE_CPU`, drop the suspended state once, create a `*-poweroff*` replacement snapshot, and rerun. The smoke scripts now auto-start restored power-off snapshots.
- Harness configures Discord inside the guest; no checked-in token/config.
- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way.
- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated like array indexes.
- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase.
- Full 3-OS sweeps: the shared build lock is safe in parallel, but snapshot restore is still a Parallels bottleneck. Prefer serialized Windows/Linux restore-heavy reruns if the host is already under load.
- Harness cleanup deletes the temporary Discord smoke messages at exit.
- Per-phase logs: `/tmp/openclaw-parallels-smoke.*`
- Machine summary: pass `--json`
- If roundtrip flakes, inspect `fresh.discord-roundtrip.log` and `discord-last-readback.json` in the run dir first.
## Pass criteria
- fresh lane or upgrade lane requested passes
- summary reports `discord=pass` for that lane
- guest outbound nonce appears in channel history
- host inbound nonce appears in `openclaw message read` output

View File

@@ -1,111 +0,0 @@
---
name: security-triage
description: Triage GitHub security advisories for OpenClaw with high-confidence close/keep decisions, exact tag and commit verification, trust-model checks, optional hardening notes, and a final reply ready to post and copy to clipboard.
---
# Security Triage
Use when reviewing OpenClaw security advisories, drafts, or GHSA reports.
Goal: high-confidence maintainers' triage without over-closing real issues or shipping unnecessary regressions.
## Close Bar
Close only if one of these is true:
- duplicate of an existing advisory or fixed issue
- invalid against shipped behavior
- out of scope under `SECURITY.md`
- fixed before any affected release/tag
Do not close only because `main` is fixed. If latest shipped tag or npm release is affected, keep it open until released or published with the right status.
## Required Reads
Before answering:
1. Read `SECURITY.md`.
2. Read the GHSA body with `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`.
3. Inspect the exact implicated code paths.
4. Verify shipped state:
- `git tag --sort=-creatordate | head`
- `npm view openclaw version --userconfig "$(mktemp)"`
- `git tag --contains <fix-commit>`
- if needed: `git show <tag>:path/to/file`
5. Search for canonical overlap:
- existing published GHSAs
- older fixed bugs
- same trust-model class already covered in `SECURITY.md`
## Review Method
For each advisory, decide:
- `close`
- `keep open`
- `keep open but narrow`
Check in this order:
1. Trust model
- Is the prerequisite already inside trusted host/local/plugin/operator state?
- Does `SECURITY.md` explicitly call this class out as out of scope or hardening-only?
2. Shipped behavior
- Is the bug present in the latest shipped tag or npm release?
- Was it fixed before release?
3. Exploit path
- Does the report show a real boundary bypass, not just prompt injection, local same-user control, or helper-level semantics?
- If data only moves between trusted workspace-memory files called out in `SECURITY.md`, do not treat "injection markers" alone as a security bug.
- In that case, frame sanitization as optional hardening only if it preserves expected memory workflows.
4. Functional tradeoff
- If a hardening change would reduce intended user functionality, call that out before proposing it.
- Prefer fixes that preserve user workflows over deny-by-default regressions unless the boundary demands it.
## Response Format
When preparing a maintainer-ready close reply:
1. Print the GHSA URL first.
2. Then draft a detailed response the maintainer can post.
3. Include:
- exact reason for close
- exact code refs
- exact shipped tag / release facts
- exact fix commit or canonical duplicate GHSA when applicable
- optional hardening note only if worthwhile and functionality-preserving
Keep tone firm, specific, non-defensive.
## Clipboard Step
After drafting the final post body, copy it:
```bash
pbcopy <<'EOF'
<final response>
EOF
```
Tell the user that the clipboard now contains the proposed response.
## Useful Commands
```bash
gh api /repos/openclaw/openclaw/security-advisories/<GHSA>
gh api /repos/openclaw/openclaw/security-advisories --paginate
git tag --sort=-creatordate | head -n 20
npm view openclaw version --userconfig "$(mktemp)"
git tag --contains <commit>
git show <tag>:<path>
gh search issues --repo openclaw/openclaw --match title,body,comments -- "<terms>"
gh search prs --repo openclaw/openclaw --match title,body,comments -- "<terms>"
```
## Decision Notes
- “fixed on main, unreleased” is usually not a close.
- “needs attacker-controlled trusted local state first” is usually out of scope.
- “same-host same-user process can already read/write local state” is usually out of scope.
- “trusted workspace memory promotes/reindexes trusted workspace memory” is usually out of scope unless it crosses a documented boundary.
- “helper function behaves differently than documented config semantics” is usually invalid.
- If only the severity is wrong but the bug is real, keep it open and narrow the impact in the reply.

0
.codex
View File

View File

@@ -24,22 +24,3 @@ pattern = "talk\.apiKey"
pattern = === "string"
# specific optional-chaining password check that didn't match the line above.
pattern = typeof remote\?\.password === "string"
# Docker apt signing key fingerprint constant; not a secret.
pattern = OPENCLAW_DOCKER_GPG_FINGERPRINT=
# Credential matrix metadata field in docs JSON; not a secret value.
pattern = "secretShape": "(secret_input|sibling_ref)"
# Docs line describing API key rotation knobs; not a credential.
pattern = API key rotation \(provider-specific\): set `\*_API_KEYS`
# Docs line describing remote password precedence; not a credential.
pattern = passw[o]rd: `OPENCLAW_GATEWAY_PASSW[O]RD` -> `gateway\.auth\.passw[o]rd` -> `gateway\.remote\.passw[o]rd`
pattern = passw[o]rd: `OPENCLAW_GATEWAY_PASSW[O]RD` -> `gateway\.remote\.passw[o]rd` -> `gateway\.auth\.passw[o]rd`
# Test fixture starts a multiline fake private key; detector should ignore the header line.
pattern = const key = `-----BEGIN PRIVATE KEY-----
# Docs examples: literal placeholder API key snippets and shell heredoc helper.
pattern = export CUSTOM_API_K[E]Y="your-key"
pattern = grep -q 'N[O]DE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache' ~/.bashrc \|\| cat >> ~/.bashrc <<'EOF'
pattern = env: \{ MISTRAL_API_K[E]Y: "sk-\.\.\." \},
pattern = "ap[i]Key": "xxxxx",
pattern = ap[i]Key: "A[I]za\.\.\.",
# Sparkle appcast signatures are release metadata, not credentials.
pattern = sparkle:edSignature="[A-Za-z0-9+/=]+"

View File

@@ -1,11 +1,5 @@
.git
.worktrees
# Sensitive files scripts/docker/setup.sh writes .env with OPENCLAW_GATEWAY_TOKEN
# into the project root; keep it out of the build context.
.env
.env.*
.bun-cache
.bun
.tmp
@@ -33,8 +27,6 @@ node_modules
**/.next
coverage
**/coverage
docs/.generated
**/.generated
*.log
tmp
**/tmp
@@ -54,19 +46,3 @@ Swabble/
Core/
Users/
vendor/
# Needed for building the Canvas A2UI bundle during Docker image builds.
# Keep the rest of apps/ and vendor/ excluded to avoid a large build context.
!apps/shared/
!apps/shared/OpenClawKit/
!apps/shared/OpenClawKit/Sources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json
!apps/shared/OpenClawKit/Tools/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/**
!vendor/a2ui/
!vendor/a2ui/renderers/
!vendor/a2ui/renderers/lit/
!vendor/a2ui/renderers/lit/**

View File

@@ -1,83 +1,5 @@
# OpenClaw .env example
#
# Quick start:
# 1) Copy this file to `.env` (for local runs from this repo), OR to `~/.openclaw/.env` (for launchd/systemd daemons).
# 2) Fill only the values you use.
# 3) Keep real secrets out of git.
#
# Env-source precedence for environment variables (highest -> lowest):
# process env, ./.env, ~/.openclaw/.env, then openclaw.json `env` block.
# Existing non-empty process env vars are not overridden by dotenv/config env loading.
# Note: direct config keys (for example `gateway.auth.token` or channel tokens in openclaw.json)
# are resolved separately from env loading and often take precedence over env fallbacks.
# -----------------------------------------------------------------------------
# Gateway auth + paths
# -----------------------------------------------------------------------------
# Required if the gateway binds beyond loopback. Leave blank to have OpenClaw
# auto-generate a token on first start, or provide your own using
# `openssl rand -hex 32`. The gateway will refuse to start if this is set to
# the documented example placeholder, so never copy-paste an example value
# from docs or tutorials into this file verbatim.
OPENCLAW_GATEWAY_TOKEN=
# Optional alternative auth mode (use token OR password).
# OPENCLAW_GATEWAY_PASSWORD=
# Optional path overrides (defaults shown for reference).
# OPENCLAW_STATE_DIR=~/.openclaw
# OPENCLAW_CONFIG_PATH=~/.openclaw/openclaw.json
# OPENCLAW_HOME=~
# Optional: import missing keys from your login shell profile.
# OPENCLAW_LOAD_SHELL_ENV=1
# OPENCLAW_SHELL_ENV_TIMEOUT_MS=15000
# -----------------------------------------------------------------------------
# Model provider API keys (set at least one)
# -----------------------------------------------------------------------------
# OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY=sk-ant-...
# GEMINI_API_KEY=...
# OPENROUTER_API_KEY=sk-or-...
# OPENCLAW_LIVE_OPENAI_KEY=sk-...
# OPENCLAW_LIVE_ANTHROPIC_KEY=sk-ant-...
# OPENCLAW_LIVE_GEMINI_KEY=...
# OPENAI_API_KEY_1=...
# ANTHROPIC_API_KEY_1=...
# GEMINI_API_KEY_1=...
# GOOGLE_API_KEY=...
# OPENAI_API_KEYS=sk-1,sk-2
# ANTHROPIC_API_KEYS=sk-ant-1,sk-ant-2
# GEMINI_API_KEYS=key-1,key-2
# Optional additional providers
# ZAI_API_KEY=...
# AI_GATEWAY_API_KEY=...
# MINIMAX_API_KEY=...
# SYNTHETIC_API_KEY=...
# -----------------------------------------------------------------------------
# Channels (only set what you enable)
# -----------------------------------------------------------------------------
# TELEGRAM_BOT_TOKEN=123456:ABCDEF...
# DISCORD_BOT_TOKEN=...
# SLACK_BOT_TOKEN=xoxb-...
# SLACK_APP_TOKEN=xapp-...
# Optional channel env fallbacks
# MATTERMOST_BOT_TOKEN=...
# MATTERMOST_URL=https://chat.example.com
# ZALO_BOT_TOKEN=...
# OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:...
# -----------------------------------------------------------------------------
# Tools + voice/media (optional)
# -----------------------------------------------------------------------------
# BRAVE_API_KEY=...
# PERPLEXITY_API_KEY=pplx-...
# FIRECRAWL_API_KEY=...
# ELEVENLABS_API_KEY=...
# XI_API_KEY=... # alias for ElevenLabs
# DEEPGRAM_API_KEY=...
# Copy to .env and fill with your Twilio credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
# Must be a WhatsApp-enabled Twilio number, prefixed with whatsapp:
TWILIO_WHATSAPP_FROM=whatsapp:+17343367101

2
.gitattributes vendored
View File

@@ -1,3 +1 @@
* text=auto eol=lf
CLAUDE.md -text
src/gateway/server-methods/CLAUDE.md -text

54
.github/CODEOWNERS vendored
View File

@@ -1,54 +0,0 @@
# Protect the ownership rules themselves.
/.github/CODEOWNERS @steipete
# WARNING: GitHub CODEOWNERS uses last-match-wins semantics.
# If you add overlapping rules below the secops block, include @openclaw/secops
# on those entries too or you can silently remove required secops review.
# Security-sensitive code, config, and docs require secops review.
/SECURITY.md @openclaw/secops
/.github/dependabot.yml @openclaw/secops
/.github/codeql/ @openclaw/secops
/.github/workflows/codeql.yml @openclaw/secops
/src/security/ @openclaw/secops
/src/secrets/ @openclaw/secops
/src/config/*secret*.ts @openclaw/secops
/src/config/**/*secret*.ts @openclaw/secops
/src/gateway/*auth*.ts @openclaw/secops
/src/gateway/**/*auth*.ts @openclaw/secops
/src/gateway/*secret*.ts @openclaw/secops
/src/gateway/**/*secret*.ts @openclaw/secops
/src/gateway/security-path*.ts @openclaw/secops
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/secops
/src/gateway/protocol/**/*secret*.ts @openclaw/secops
/src/gateway/server-methods/secrets*.ts @openclaw/secops
/src/agents/*auth*.ts @openclaw/secops
/src/agents/**/*auth*.ts @openclaw/secops
/src/agents/auth-profiles*.ts @openclaw/secops
/src/agents/auth-health*.ts @openclaw/secops
/src/agents/auth-profiles/ @openclaw/secops
/src/agents/sandbox.ts @openclaw/secops
/src/agents/sandbox-*.ts @openclaw/secops
/src/agents/sandbox/ @openclaw/secops
/src/infra/secret-file*.ts @openclaw/secops
/src/cron/stagger.ts @openclaw/secops
/src/cron/service/jobs.ts @openclaw/secops
/docs/security/ @openclaw/secops
/docs/gateway/authentication.md @openclaw/secops
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/secops
/docs/gateway/sandboxing.md @openclaw/secops
/docs/gateway/secrets-plan-contract.md @openclaw/secops
/docs/gateway/secrets.md @openclaw/secops
/docs/gateway/security/ @openclaw/secops
/docs/cli/approvals.md @openclaw/secops
/docs/cli/sandbox.md @openclaw/secops
/docs/cli/security.md @openclaw/secops
/docs/cli/secrets.md @openclaw/secops
/docs/reference/secretref-credential-surface.md @openclaw/secops
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/secops
# Release workflow and its supporting release-path checks.
/.github/workflows/openclaw-npm-release.yml @openclaw/openclaw-release-managers
/docs/reference/RELEASING.md @openclaw/openclaw-release-managers
/scripts/openclaw-npm-publish.sh @openclaw/openclaw-release-managers
/scripts/openclaw-npm-release-check.ts @openclaw/openclaw-release-managers
/scripts/release-check.ts @openclaw/openclaw-release-managers

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Report a problem or unexpected behavior in Clawdbot.
title: "[Bug]: "
labels: bug
---
## Summary
What went wrong?
## Steps to reproduce
1.
2.
3.
## Expected behavior
What did you expect to happen?
## Actual behavior
What actually happened?
## Environment
- Clawdbot version:
- OS:
- Install method (pnpm/npx/docker/etc):
## Logs or screenshots
Paste relevant logs or add screenshots (redact secrets).

View File

@@ -1,148 +0,0 @@
name: Bug report
description: Report defects, including regressions, crashes, and behavior bugs.
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for filing this report. Keep every answer concise, reproducible, and grounded in observed evidence.
Do not speculate or infer beyond the evidence. If a narrative section cannot be answered from the available evidence, respond with exactly `NOT_ENOUGH_INFO`.
If this is a plugin beta-release blocker, rename the issue title to `Beta blocker: <plugin-name> - <summary>` and apply the `beta-blocker` label after filing.
- type: dropdown
id: bug_type
attributes:
label: Bug type
description: Choose the category that best matches this report.
options:
- Regression (worked before, now fails)
- Crash (process/app exits or hangs)
- Behavior bug (incorrect output/state without crash)
validations:
required: true
- type: dropdown
id: beta_blocker
attributes:
label: Beta release blocker
description: >
Choose `Yes` only if this blocks plugin compatibility during the current beta release window.
Selecting `Yes` does not apply the label automatically. You must also rename the issue title
to `Beta blocker: <plugin-name> - <summary>` for the automation to apply the `beta-blocker` label.
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea
id: summary
attributes:
label: Summary
description: One-sentence statement of what is broken, based only on observed evidence. If the evidence is insufficient, respond with exactly `NOT_ENOUGH_INFO`.
placeholder: After upgrading from 2026.2.10 to 2026.2.17, Telegram thread replies stopped posting; reproduced twice and confirmed by gateway logs.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: Provide the shortest deterministic repro path supported by direct observation. If the repro path cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
placeholder: |
1. Start OpenClaw 2026.2.17 with the attached config.
2. Send a Telegram thread reply in the affected chat.
3. Observe no reply and confirm the attached `reply target not found` log line.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: State the expected result using a concrete reference such as prior observed behavior, attached docs, or a known-good version. If no grounded reference exists, respond with exactly `NOT_ENOUGH_INFO`.
placeholder: In 2026.2.10, the agent posted replies in the same Telegram thread under the same workflow.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: Describe only the observed result, including user-visible errors and cited evidence. If the observed result cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
placeholder: No reply is posted in the thread; the attached gateway log shows `reply target not found` at 14:23:08 UTC.
validations:
required: true
- type: input
id: version
attributes:
label: OpenClaw version
description: Exact version/build tested.
placeholder: <version such as 2026.2.17>
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: OS and version where this occurs.
placeholder: macOS 15.4 / Ubuntu 24.04 / Windows 11
validations:
required: true
- type: input
id: install_method
attributes:
label: Install method
description: How OpenClaw was installed or launched.
placeholder: npm global / pnpm dev / docker / mac app
- type: input
id: model
attributes:
label: Model
description: Effective model under test.
placeholder: minimax/text-01 / openrouter/anthropic/claude-opus-4.1 / anthropic/claude-sonnet-4.5
validations:
required: true
- type: input
id: provider_chain
attributes:
label: Provider / routing chain
description: Effective request path through gateways, proxies, providers, or model routers.
placeholder: openclaw -> cloudflare-ai-gateway -> minimax
validations:
required: true
- type: textarea
id: provider_setup_details
attributes:
label: Additional provider/model setup details
description: Optional. Include redacted routing details, per-agent overrides, auth-profile interactions, env/config context, or anything else needed to explain the effective provider/model setup. Do not include API keys, tokens, or passwords.
placeholder: |
Default route is openclaw -> cloudflare-ai-gateway -> minimax.
Previous setup was openclaw -> cloudflare-ai-gateway -> openrouter -> minimax.
Relevant config lives in ~/.openclaw/openclaw.json under models.providers.minimax and models.providers.cloudflare-ai-gateway.
- type: textarea
id: logs
attributes:
label: Logs, screenshots, and evidence
description: Include the redacted logs, screenshots, recordings, docs, or version comparisons that support the grounded answers above.
render: shell
- type: textarea
id: impact
attributes:
label: Impact and severity
description: |
Explain who is affected, how severe it is, how often it happens, and the practical consequence using only observed evidence.
If any part cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
Include:
- Affected users/systems/channels
- Severity (annoying, blocks workflow, data risk, etc.)
- Frequency (always/intermittent/edge case)
- Consequence (missed messages, failed onboarding, extra cost, etc.)
placeholder: |
Affected: Telegram group users on 2026.2.17
Severity: High (blocks thread replies)
Frequency: 4/4 observed attempts
Consequence: Agents do not respond in the affected threads
- type: textarea
id: additional_information
attributes:
label: Additional information
description: Add any remaining grounded context that helps triage but does not fit above. If this is a regression, include the last known good and first known bad versions when observed. If there is not enough evidence, respond with exactly `NOT_ENOUGH_INFO`.
placeholder: Last known good version 2026.2.10, first known bad version 2026.2.17, temporary workaround is sending a top-level message instead of a thread reply.

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Onboarding
url: https://discord.gg/clawd
about: "New to OpenClaw? Join Discord for setup guidance in #help."
about: New to Clawdbot? Join Discord for setup guidance from Krill in #help.
- name: Support
url: https://discord.gg/clawd
about: "Get help from the OpenClaw community on Discord in #help."
about: Get help from Krill and the community on Discord in #help.

View File

@@ -0,0 +1,18 @@
---
name: Feature request
about: Suggest an idea or improvement for Clawdbot.
title: "[Feature]: "
labels: enhancement
---
## Summary
Describe the problem you are trying to solve or the opportunity you see.
## Proposed solution
What would you like Clawdbot to do?
## Alternatives considered
Any other approaches you have considered?
## Additional context
Links, screenshots, or related issues.

View File

@@ -1,70 +0,0 @@
name: Feature request
description: Propose a new capability or product improvement.
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Help us evaluate this request with concrete use cases and tradeoffs.
- type: textarea
id: summary
attributes:
label: Summary
description: One-line statement of the requested capability.
placeholder: Add per-channel default response prefix.
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem to solve
description: What user pain this solves and why current behavior is insufficient.
placeholder: Agents cannot distinguish persona context in mixed channels, causing misrouted follow-ups.
validations:
required: true
- type: textarea
id: proposed_solution
attributes:
label: Proposed solution
description: Desired behavior/API/UX with as much specificity as possible.
placeholder: Support channels.<channel>.responsePrefix with default fallback and account-level override.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Other approaches considered and why they are weaker.
placeholder: Manual prefixing in prompts is inconsistent and hard to enforce.
- type: textarea
id: impact
attributes:
label: Impact
description: |
Explain who is affected, severity/urgency, how often this pain occurs, and practical consequences.
Include:
- Affected users/systems/channels
- Severity (annoying, blocks workflow, etc.)
- Frequency (always/intermittent/edge case)
- Consequence (delays, errors, extra manual work, etc.)
placeholder: |
Affected: Multi-team shared channels
Severity: Medium
Frequency: Daily
Consequence: +20 minutes/day/operator and delayed alerts
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Evidence/examples
description: Prior art, links, screenshots, logs, or metrics.
placeholder: Comparable behavior in X, sample config, and screenshot of current limitation.
- type: textarea
id: additional_information
attributes:
label: Additional information
description: Extra context, constraints, or references not covered above.
placeholder: Must remain backward-compatible with existing config keys.

View File

@@ -1,26 +0,0 @@
# actionlint configuration
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
self-hosted-runner:
labels:
# Blacksmith CI runners
- blacksmith-8vcpu-ubuntu-2404
- blacksmith-8vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404
- blacksmith-32vcpu-ubuntu-2404
- blacksmith-16vcpu-windows-2025
- blacksmith-32vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404-arm
- blacksmith-6vcpu-macos-latest
- blacksmith-12vcpu-macos-latest
# Ignore patterns for known issues
paths:
.github/workflows/**/*.yml:
ignore:
# Ignore shellcheck warnings (we run shellcheck separately)
- "shellcheck reported issue.+"
# Ignore intentional if: false for disabled jobs
- 'constant expression "false" in condition'
# actionlint's built-in runner label allowlist lags Blacksmith additions.
- 'label "blacksmith-16vcpu-[^"]+" is unknown\.'

View File

@@ -1,53 +0,0 @@
name: Detect docs-only changes
description: >
Outputs docs_only=true when all changed files are under docs/ or are
markdown (.md/.mdx). Fail-safe: if detection fails, outputs false (run
everything). Uses git diff — no API calls, no extra permissions needed.
outputs:
docs_only:
description: "'true' if all changes are docs/markdown, 'false' otherwise"
value: ${{ steps.check.outputs.docs_only }}
docs_changed:
description: "'true' if any changed file is under docs/ or is markdown"
value: ${{ steps.check.outputs.docs_changed }}
runs:
using: composite
steps:
- name: Detect docs-only changes
id: check
shell: bash
run: |
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
else
# Use the exact base SHA from the event payload — stable regardless
# of base branch movement (avoids origin/<ref> drift).
BASE="${{ github.event.pull_request.base.sha }}"
fi
# Fail-safe: if we can't diff, assume non-docs (run everything)
CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")
if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then
echo "docs_only=false" >> "$GITHUB_OUTPUT"
echo "docs_changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check if any changed file is a doc
DOCS=$(echo "$CHANGED" | grep -E '^docs/|\.md$|\.mdx$' || true)
if [ -n "$DOCS" ]; then
echo "docs_changed=true" >> "$GITHUB_OUTPUT"
else
echo "docs_changed=false" >> "$GITHUB_OUTPUT"
fi
# Check if all changed files are docs or markdown
NON_DOCS=$(echo "$CHANGED" | grep -vE '^docs/|\.md$|\.mdx$' || true)
if [ -z "$NON_DOCS" ]; then
echo "docs_only=true" >> "$GITHUB_OUTPUT"
echo "Docs-only change detected — skipping heavy jobs"
else
echo "docs_only=false" >> "$GITHUB_OUTPUT"
fi

View File

@@ -1,61 +0,0 @@
name: Ensure base commit
description: Ensure a shallow checkout has enough history to diff against a base SHA.
inputs:
base-sha:
description: Base commit SHA to diff against.
required: true
fetch-ref:
description: Branch or ref to deepen/fetch from origin when base-sha is missing.
required: true
runs:
using: composite
steps:
- name: Ensure base commit is available
shell: bash
env:
BASE_SHA: ${{ inputs.base-sha }}
FETCH_REF: ${{ inputs.fetch-ref }}
run: |
set -euo pipefail
if [ -z "$BASE_SHA" ] || [[ "$BASE_SHA" =~ ^0+$ ]]; then
echo "No concrete base SHA available; skipping targeted fetch."
exit 0
fi
if ! [[ "$BASE_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
echo "::error title=ensure-base-commit invalid base sha::Refusing invalid base SHA: $BASE_SHA"
exit 2
fi
if ! git check-ref-format --branch "$FETCH_REF" >/dev/null 2>&1; then
echo "::error title=ensure-base-commit invalid fetch ref::Refusing invalid fetch ref: $FETCH_REF"
exit 2
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
echo "Base commit already present: $BASE_SHA"
exit 0
fi
for deepen_by in 25 100 300; do
echo "Base commit missing; deepening $FETCH_REF by $deepen_by."
if ! git fetch --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to deepen $FETCH_REF by $deepen_by while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
echo "Resolved base commit after deepening: $BASE_SHA"
exit 0
fi
done
echo "Base commit still missing; fetching full history for $FETCH_REF."
if ! git fetch --no-tags origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to fetch full history for $FETCH_REF while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
echo "Resolved base commit after full ref fetch: $BASE_SHA"
exit 0
fi
echo "Base commit still unavailable after fetch attempts: $BASE_SHA"

View File

@@ -1,99 +0,0 @@
name: Setup Node environment
description: >
Install Node 24 by default, pnpm, optionally Bun, and optionally run pnpm
install. Requires actions/checkout to run first.
inputs:
node-version:
description: Node.js version to install.
required: false
default: "24.x"
cache-key-suffix:
description: Suffix appended to the pnpm store cache key.
required: false
default: "node24"
pnpm-version:
description: pnpm version for corepack.
required: false
default: "10.32.1"
install-bun:
description: Whether to install Bun alongside Node.
required: false
default: "true"
use-sticky-disk:
description: Request Blacksmith sticky-disk pnpm caching on trusted runs; pull_request runs fall back to actions/cache.
required: false
default: "false"
install-deps:
description: Whether to run pnpm install after environment setup.
required: false
default: "true"
frozen-lockfile:
description: Whether to use --frozen-lockfile for install.
required: false
default: "true"
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
check-latest: false
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: ${{ inputs.pnpm-version }}
cache-key-suffix: ${{ inputs.cache-key-suffix }}
use-sticky-disk: ${{ inputs.use-sticky-disk }}
- name: Setup Bun
if: inputs.install-bun == 'true'
uses: oven-sh/setup-bun@v2.2.0
with:
bun-version: "1.3.9"
- name: Runtime versions
shell: bash
run: |
node -v
npm -v
pnpm -v
if command -v bun &>/dev/null; then bun -v; fi
- name: Capture node path
if: inputs.install-deps == 'true'
shell: bash
run: echo "NODE_BIN=$(dirname "$(node -p "process.execPath")")" >> "$GITHUB_ENV"
- name: Install dependencies
if: inputs.install-deps == 'true'
shell: bash
env:
CI: "true"
FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }}
run: |
set -euo pipefail
export PATH="$NODE_BIN:$PATH"
which node
node -v
pnpm -v
case "$FROZEN_LOCKFILE" in
true) LOCKFILE_FLAG="--frozen-lockfile" ;;
false) LOCKFILE_FLAG="" ;;
*)
echo "::error::Invalid frozen-lockfile input: '$FROZEN_LOCKFILE' (expected true or false)"
exit 2
;;
esac
install_args=(
install
--ignore-scripts=false
--config.engine-strict=false
--config.enable-pre-post-scripts=true
)
if [ -n "$LOCKFILE_FLAG" ]; then
install_args+=("$LOCKFILE_FLAG")
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"

View File

@@ -1,76 +0,0 @@
name: Setup pnpm + store cache
description: Prepare pnpm via corepack and restore pnpm store cache.
inputs:
pnpm-version:
description: pnpm version to activate via corepack.
required: false
default: "10.32.1"
cache-key-suffix:
description: Suffix appended to the cache key.
required: false
default: "node24"
use-sticky-disk:
description: Use Blacksmith sticky disks instead of actions/cache for pnpm store on trusted runs; pull_request runs fall back to actions/cache.
required: false
default: "false"
use-restore-keys:
description: Whether to use restore-keys fallback for actions/cache.
required: false
default: "true"
use-actions-cache:
description: Whether to restore/save pnpm store with actions/cache, including pull_request fallback when sticky disks are disabled.
required: false
default: "true"
runs:
using: composite
steps:
- name: Setup pnpm (corepack retry)
shell: bash
env:
PNPM_VERSION: ${{ inputs.pnpm-version }}
run: |
set -euo pipefail
if [[ ! "$PNPM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid pnpm-version input: '$PNPM_VERSION'"
exit 2
fi
corepack enable
for attempt in 1 2 3; do
if corepack prepare "pnpm@$PNPM_VERSION" --activate; then
pnpm -v
exit 0
fi
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
sleep $((attempt * 10))
done
exit 1
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Mount pnpm store sticky disk
# Keep persistent sticky-disk state off untrusted PR runs.
if: inputs.use-sticky-disk == 'true' && github.event_name != 'pull_request'
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-pnpm-store-${{ runner.os }}-${{ github.ref_name }}-${{ inputs.cache-key-suffix }}-${{ hashFiles('pnpm-lock.yaml') }}
path: ${{ steps.pnpm-store.outputs.path }}
- name: Restore pnpm store cache (exact key only)
# PRs that request sticky disks still need a safe cache restore path.
if: inputs.use-actions-cache == 'true' && (inputs.use-sticky-disk != 'true' || github.event_name == 'pull_request') && inputs.use-restore-keys != 'true'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ inputs.cache-key-suffix }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Restore pnpm store cache (with fallback keys)
if: inputs.use-actions-cache == 'true' && (inputs.use-sticky-disk != 'true' || github.event_name == 'pull_request') && inputs.use-restore-keys == 'true'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ inputs.cache-key-suffix }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-${{ inputs.cache-key-suffix }}-

View File

@@ -1,18 +0,0 @@
name: openclaw-codeql-javascript-typescript
paths:
- src
- extensions
- ui/src
- skills
paths-ignore:
- apps
- dist
- docs
- "**/node_modules"
- "**/coverage"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"

127
.github/dependabot.yml vendored
View File

@@ -1,127 +0,0 @@
# Dependabot configuration
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
registries:
npm-npmjs:
type: npm-registry
url: https://registry.npmjs.org
token: ${{secrets.NPM_NPMJS_TOKEN}}
replaces-base: true
updates:
# npm dependencies (root)
- package-ecosystem: npm
directory: /
schedule:
interval: daily
cooldown:
default-days: 2
groups:
production:
dependency-type: production
update-types:
- minor
- patch
development:
dependency-type: development
update-types:
- minor
- patch
open-pull-requests-limit: 10
registries:
- npm-npmjs
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
cooldown:
default-days: 2
groups:
actions:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - macOS app
- package-ecosystem: swift
directory: /apps/macos
schedule:
interval: daily
cooldown:
default-days: 2
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - shared MoltbotKit
- package-ecosystem: swift
directory: /apps/shared/MoltbotKit
schedule:
interval: daily
cooldown:
default-days: 2
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - Swabble
- package-ecosystem: swift
directory: /Swabble
schedule:
interval: daily
cooldown:
default-days: 2
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Gradle - Android app
- package-ecosystem: gradle
directory: /apps/android
schedule:
interval: daily
cooldown:
default-days: 2
groups:
android-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Docker base images
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
cooldown:
default-days: 2
groups:
docker-images:
patterns:
- "*"
open-pull-requests-limit: 5

View File

@@ -1,64 +0,0 @@
# OpenClaw Codebase Patterns
**Always reuse existing code - no redundancy!**
## Tech Stack
- **Runtime**: Node 22+ (Bun also supported for dev/scripts)
- **Language**: TypeScript (ESM, strict mode)
- **Package Manager**: pnpm (keep `pnpm-lock.yaml` in sync)
- **Lint/Format**: Oxlint, Oxfmt (`pnpm check`)
- **Tests**: Vitest with V8 coverage
- **CLI Framework**: Commander + clack/prompts
- **Build**: tsdown (outputs to `dist/`)
## Anti-Redundancy Rules
- Avoid files that just re-export from another file. Import directly from the original source.
- If a function already exists, import it - do NOT create a duplicate in another file.
- Before creating any formatter, utility, or helper, search for existing implementations first.
## Source of Truth Locations
### Formatting Utilities (`src/infra/`)
- **Time formatting**: `src\infra\format-time`
**NEVER create local `formatAge`, `formatDuration`, `formatElapsedTime` functions - import from centralized modules.**
### Terminal Output (`src/terminal/`)
- Tables: `src/terminal/table.ts` (`renderTable`)
- Themes/colors: `src/terminal/theme.ts` (`theme.success`, `theme.muted`, etc.)
- Progress: `src/cli/progress.ts` (spinners, progress bars)
### CLI Patterns
- CLI option wiring: `src/cli/`
- Commands: `src/commands/`
- Dependency injection via `createDefaultDeps`
## Import Conventions
- Use `.js` extension for cross-package imports (ESM)
- Direct imports only - no re-export wrapper files
- Types: `import type { X }` for type-only imports
## Code Quality
- TypeScript (ESM), strict typing, avoid `any`
- Keep files under ~700 LOC - extract helpers when larger
- Colocated tests: `*.test.ts` next to source files
- Run `pnpm check` before commits (lint + format)
- Run `pnpm tsgo` for type checking
## Stack & Commands
- **Package manager**: pnpm (`pnpm install`)
- **Dev**: `pnpm openclaw ...` or `pnpm dev`
- **Type-check**: `pnpm tsgo`
- **Lint/format**: `pnpm check`
- **Tests**: `pnpm test`
- **Build**: `pnpm build`
If you are coding together with a human, do NOT use scripts/committer, but git directly and run the above commands manually to ensure quality.

371
.github/labeler.yml vendored
View File

@@ -1,371 +0,0 @@
"channel: bluebubbles":
- changed-files:
- any-glob-to-any-file:
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"channel: discord":
- changed-files:
- any-glob-to-any-file:
- "extensions/discord/**"
- "docs/channels/discord.md"
"channel: irc":
- changed-files:
- any-glob-to-any-file:
- "extensions/irc/**"
- "docs/channels/irc.md"
"channel: feishu":
- changed-files:
- any-glob-to-any-file:
- "src/feishu/**"
- "extensions/feishu/**"
- "docs/channels/feishu.md"
"channel: googlechat":
- changed-files:
- any-glob-to-any-file:
- "extensions/googlechat/**"
- "docs/channels/googlechat.md"
"channel: imessage":
- changed-files:
- any-glob-to-any-file:
- "extensions/imessage/**"
- "docs/channels/imessage.md"
"channel: line":
- changed-files:
- any-glob-to-any-file:
- "extensions/line/**"
- "docs/channels/line.md"
"channel: matrix":
- changed-files:
- any-glob-to-any-file:
- "extensions/matrix/**"
- "docs/channels/matrix.md"
"channel: mattermost":
- changed-files:
- any-glob-to-any-file:
- "extensions/mattermost/**"
- "docs/channels/mattermost.md"
"channel: msteams":
- changed-files:
- any-glob-to-any-file:
- "extensions/msteams/**"
- "docs/channels/msteams.md"
"channel: nextcloud-talk":
- changed-files:
- any-glob-to-any-file:
- "extensions/nextcloud-talk/**"
- "docs/channels/nextcloud-talk.md"
"channel: nostr":
- changed-files:
- any-glob-to-any-file:
- "extensions/nostr/**"
- "docs/channels/nostr.md"
"channel: qqbot":
- changed-files:
- any-glob-to-any-file:
- "extensions/qqbot/**"
- "docs/channels/qqbot.md"
"channel: qa-channel":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-channel/**"
- "docs/channels/qa-channel.md"
"extensions: qa-lab":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-lab/**"
- "docs/concepts/qa-e2e-automation.md"
- "docs/channels/qa-channel.md"
"channel: signal":
- changed-files:
- any-glob-to-any-file:
- "extensions/signal/**"
- "docs/channels/signal.md"
"channel: slack":
- changed-files:
- any-glob-to-any-file:
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: telegram":
- changed-files:
- any-glob-to-any-file:
- "extensions/telegram/**"
- "docs/channels/telegram.md"
"channel: tlon":
- changed-files:
- any-glob-to-any-file:
- "extensions/tlon/**"
- "docs/channels/tlon.md"
"channel: twitch":
- changed-files:
- any-glob-to-any-file:
- "extensions/twitch/**"
- "docs/channels/twitch.md"
"channel: voice-call":
- changed-files:
- any-glob-to-any-file:
- "extensions/voice-call/**"
"channel: whatsapp-web":
- changed-files:
- any-glob-to-any-file:
- "extensions/whatsapp/**"
- "docs/channels/whatsapp.md"
"channel: zalo":
- changed-files:
- any-glob-to-any-file:
- "extensions/zalo/**"
- "docs/channels/zalo.md"
"channel: zalouser":
- changed-files:
- any-glob-to-any-file:
- "extensions/zalouser/**"
- "docs/channels/zalouser.md"
"app: android":
- changed-files:
- any-glob-to-any-file:
- "apps/android/**"
- "docs/platforms/android.md"
"app: ios":
- changed-files:
- any-glob-to-any-file:
- "apps/ios/**"
- "docs/platforms/ios.md"
"app: macos":
- changed-files:
- any-glob-to-any-file:
- "apps/macos/**"
- "docs/platforms/macos.md"
- "docs/platforms/mac/**"
"app: web-ui":
- changed-files:
- any-glob-to-any-file:
- "ui/**"
- "src/gateway/control-ui.ts"
- "src/gateway/control-ui-shared.ts"
- "src/gateway/protocol/**"
- "src/gateway/server-methods/chat.ts"
- "src/infra/control-ui-assets.ts"
"gateway":
- changed-files:
- any-glob-to-any-file:
- "src/gateway/**"
- "src/daemon/**"
- "docs/gateway/**"
"docs":
- changed-files:
- any-glob-to-any-file:
- "docs/**"
- "docs.acp.md"
"cli":
- changed-files:
- any-glob-to-any-file:
- "src/cli/**"
"commands":
- changed-files:
- any-glob-to-any-file:
- "src/commands/**"
"scripts":
- changed-files:
- any-glob-to-any-file:
- "scripts/**"
"docker":
- changed-files:
- any-glob-to-any-file:
- "Dockerfile"
- "Dockerfile.*"
- "docker-compose.yml"
- "docker-setup.sh"
- "setup-podman.sh"
- ".dockerignore"
- "scripts/docker/setup.sh"
- "scripts/podman/setup.sh"
- "scripts/**/*docker*"
- "scripts/**/Dockerfile*"
- "scripts/sandbox-*.sh"
- "src/agents/sandbox*.ts"
- "src/commands/sandbox*.ts"
- "src/cli/sandbox-cli.ts"
- "src/docker-setup.test.ts"
- "src/config/**/*sandbox*"
- "docs/cli/sandbox.md"
- "docs/gateway/sandbox*.md"
- "docs/install/docker.md"
- "docs/multi-agent-sandbox-tools.md"
"agents":
- changed-files:
- any-glob-to-any-file:
- "src/agents/**"
"security":
- changed-files:
- any-glob-to-any-file:
- "docs/cli/security.md"
- "docs/gateway/security.md"
"extensions: copilot-proxy":
- changed-files:
- any-glob-to-any-file:
- "extensions/copilot-proxy/**"
"extensions: diagnostics-otel":
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-otel/**"
"extensions: llm-task":
- changed-files:
- any-glob-to-any-file:
- "extensions/llm-task/**"
"extensions: lobster":
- changed-files:
- any-glob-to-any-file:
- "extensions/lobster/**"
"extensions: memory-core":
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-core/**"
"extensions: memory-lancedb":
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-lancedb/**"
"extensions: memory-wiki":
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-wiki/**"
"extensions: open-prose":
- changed-files:
- any-glob-to-any-file:
- "extensions/open-prose/**"
"extensions: webhooks":
- changed-files:
- any-glob-to-any-file:
- "extensions/webhooks/**"
"extensions: device-pair":
- changed-files:
- any-glob-to-any-file:
- "extensions/device-pair/**"
"extensions: duckduckgo":
- changed-files:
- any-glob-to-any-file:
- "extensions/duckduckgo/**"
"extensions: acpx":
- changed-files:
- any-glob-to-any-file:
- "extensions/acpx/**"
"extensions: arcee":
- changed-files:
- any-glob-to-any-file:
- "extensions/arcee/**"
"extensions: byteplus":
- changed-files:
- any-glob-to-any-file:
- "extensions/byteplus/**"
"extensions: deepseek":
- changed-files:
- any-glob-to-any-file:
- "extensions/deepseek/**"
"extensions: stepfun":
- changed-files:
- any-glob-to-any-file:
- "extensions/stepfun/**"
"extensions: anthropic":
- changed-files:
- any-glob-to-any-file:
- "extensions/anthropic/**"
"extensions: cloudflare-ai-gateway":
- changed-files:
- any-glob-to-any-file:
- "extensions/cloudflare-ai-gateway/**"
"extensions: minimax-portal-auth":
- changed-files:
- any-glob-to-any-file:
- "extensions/minimax-portal-auth/**"
"extensions: huggingface":
- changed-files:
- any-glob-to-any-file:
- "extensions/huggingface/**"
"extensions: kilocode":
- changed-files:
- any-glob-to-any-file:
- "extensions/kilocode/**"
"extensions: lmstudio":
- changed-files:
- any-glob-to-any-file:
- "extensions/lmstudio/**"
"extensions: openai":
- changed-files:
- any-glob-to-any-file:
- "extensions/openai/**"
"extensions: codex":
- changed-files:
- any-glob-to-any-file:
- "extensions/codex/**"
"extensions: kimi-coding":
- changed-files:
- any-glob-to-any-file:
- "extensions/kimi-coding/**"
"extensions: minimax":
- changed-files:
- any-glob-to-any-file:
- "extensions/minimax/**"
"extensions: modelstudio":
- changed-files:
- any-glob-to-any-file:
- "extensions/modelstudio/**"
"extensions: moonshot":
- changed-files:
- any-glob-to-any-file:
- "extensions/moonshot/**"
"extensions: nvidia":
- changed-files:
- any-glob-to-any-file:
- "extensions/nvidia/**"
"extensions: phone-control":
- changed-files:
- any-glob-to-any-file:
- "extensions/phone-control/**"
"extensions: qianfan":
- changed-files:
- any-glob-to-any-file:
- "extensions/qianfan/**"
"extensions: synthetic":
- changed-files:
- any-glob-to-any-file:
- "extensions/synthetic/**"
"extensions: tavily":
- changed-files:
- any-glob-to-any-file:
- "extensions/tavily/**"
"extensions: talk-voice":
- changed-files:
- any-glob-to-any-file:
- "extensions/talk-voice/**"
"extensions: together":
- changed-files:
- any-glob-to-any-file:
- "extensions/together/**"
"extensions: venice":
- changed-files:
- any-glob-to-any-file:
- "extensions/venice/**"
"extensions: vercel-ai-gateway":
- changed-files:
- any-glob-to-any-file:
- "extensions/vercel-ai-gateway/**"
"extensions: volcengine":
- changed-files:
- any-glob-to-any-file:
- "extensions/volcengine/**"
"extensions: xiaomi":
- changed-files:
- any-glob-to-any-file:
- "extensions/xiaomi/**"
"extensions: fal":
- changed-files:
- any-glob-to-any-file:
- "extensions/fal/**"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,147 +0,0 @@
## Summary
Describe the problem and fix in 25 bullets:
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
- Problem:
- Why it matters:
- What changed:
- What did NOT change (scope boundary):
## Change Type (select all)
- [ ] Bug fix
- [ ] Feature
- [ ] Refactor required for the fix
- [ ] Docs
- [ ] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [ ] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [ ] Integrations
- [ ] API / contracts
- [ ] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
- Closes #
- Related #
- [ ] This PR fixes a bug or regression
## Root Cause (if applicable)
For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write `N/A`. If the cause is unclear, write `Unknown`.
- Root cause:
- Missing detection / guardrail:
- Contributing context (if known):
## Regression Test Plan (if applicable)
For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write `N/A`.
- Coverage level that should have caught this:
- [ ] Unit test
- [ ] Seam / integration test
- [ ] End-to-end test
- [ ] Existing coverage already sufficient
- Target test or file:
- Scenario the test should lock in:
- Why this is the smallest reliable guardrail:
- Existing test that already covers this (if any):
- If no new test is added, why not:
## User-visible / Behavior Changes
List user-visible changes (including defaults/config).
If none, write `None`.
## Diagram (if applicable)
For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write `N/A`.
```text
Before:
[user action] -> [old state]
After:
[user action] -> [new state] -> [result]
```
## Security Impact (required)
- New permissions/capabilities? (`Yes/No`)
- Secrets/tokens handling changed? (`Yes/No`)
- New/changed network calls? (`Yes/No`)
- Command/tool execution surface changed? (`Yes/No`)
- Data access scope changed? (`Yes/No`)
- If any `Yes`, explain risk + mitigation:
## Repro + Verification
### Environment
- OS:
- Runtime/container:
- Model/provider:
- Integration/channel (if any):
- Relevant config (redacted):
### Steps
1.
2.
3.
### Expected
-
### Actual
-
## Evidence
Attach at least one:
- [ ] Failing test/log before + passing after
- [ ] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
## Human Verification (required)
What you personally verified (not just CI), and how:
- Verified scenarios:
- Edge cases checked:
- What you did **not** verify:
## Review Conversations
- [ ] I replied to or resolved every bot review conversation I addressed in this PR.
- [ ] I left unresolved only the conversations that still need reviewer or maintainer judgment.
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
## Compatibility / Migration
- Backward compatible? (`Yes/No`)
- Config/env changes? (`Yes/No`)
- Migration needed? (`Yes/No`)
- If yes, exact upgrade steps:
## Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write `None`.
- Risk:
- Mitigation:

View File

@@ -1,534 +0,0 @@
name: Auto response
on:
issues:
types: [opened, edited, labeled]
issue_comment:
types: [created]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned label automation; no untrusted checkout or code execution
types: [labeled]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
permissions: {}
jobs:
auto-response:
permissions:
issues: write
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v2
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Handle labeled items
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
// Labels prefixed with "r:" are auto-response triggers.
const activePrLimit = 10;
const rules = [
{
label: "r: skill",
close: true,
message:
"Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. Were keeping the core lean on skills, so Im closing this out.",
},
{
label: "r: support",
close: true,
message:
"Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
},
{
label: "r: no-ci-pr",
close: true,
message:
"Please don't make PRs for test failures on main.\n\n" +
"The team is aware of those and will handle them directly on the codebase, not only fixing the tests but also investigating what the root cause is. Having to sift through test-fix-PRs (including some that have been out of date for weeks...) on top of that doesn't help. There are already way too many PRs for humans to manage; please don't make the flood worse.\n\n" +
"Thank you.",
},
{
label: "r: too-many-prs",
close: true,
message:
`Closing this PR because the author has more than ${activePrLimit} active PRs in this repo. ` +
"Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.",
},
{
label: "r: testflight",
close: true,
commentTriggers: ["testflight"],
message: "Not available, build from source.",
},
{
label: "r: third-party-extension",
close: true,
message:
"Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community",
},
{
label: "r: moltbook",
close: true,
lock: true,
lockReason: "off-topic",
commentTriggers: ["moltbook"],
message:
"OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
},
];
const maintainerTeam = "maintainer";
const pingWarningMessage =
"Please dont spam-ping multiple maintainers at once. Be patient, or join our community Discord for help: https://discord.gg/clawd";
const mentionRegex = /@([A-Za-z0-9-]+)/g;
const maintainerCache = new Map();
const normalizeLogin = (login) => login.toLowerCase();
const bugSubtypeLabelSpecs = {
regression: {
color: "D93F0B",
description: "Behavior that previously worked and now fails",
},
"bug:crash": {
color: "B60205",
description: "Process/app exits unexpectedly or hangs",
},
"bug:behavior": {
color: "D73A4A",
description: "Incorrect behavior without a crash",
},
};
const bugTypeToLabel = {
"Regression (worked before, now fails)": "regression",
"Crash (process/app exits or hangs)": "bug:crash",
"Behavior bug (incorrect output/state without crash)": "bug:behavior",
};
const bugSubtypeLabels = Object.keys(bugSubtypeLabelSpecs);
const extractIssueFormValue = (body, field) => {
if (!body) {
return "";
}
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(
`(?:^|\\n)###\\s+${escapedField}\\s*\\n([\\s\\S]*?)(?=\\n###\\s+|$)`,
"i",
);
const match = body.match(regex);
if (!match) {
return "";
}
for (const line of match[1].split("\n")) {
const trimmed = line.trim();
if (trimmed) {
return trimmed;
}
}
return "";
};
const ensureLabelExists = async (name, color, description) => {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color,
description,
});
}
};
const syncBugSubtypeLabel = async (issue, labelSet) => {
if (!labelSet.has("bug")) {
return;
}
const selectedBugType = extractIssueFormValue(issue.body ?? "", "Bug type");
const targetLabel = bugTypeToLabel[selectedBugType];
if (!targetLabel) {
return;
}
const targetSpec = bugSubtypeLabelSpecs[targetLabel];
await ensureLabelExists(targetLabel, targetSpec.color, targetSpec.description);
for (const subtypeLabel of bugSubtypeLabels) {
if (subtypeLabel === targetLabel) {
continue;
}
if (!labelSet.has(subtypeLabel)) {
continue;
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: subtypeLabel,
});
labelSet.delete(subtypeLabel);
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
if (!labelSet.has(targetLabel)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [targetLabel],
});
labelSet.add(targetLabel);
}
};
const isMaintainer = async (login) => {
if (!login) {
return false;
}
const normalized = normalizeLogin(login);
if (maintainerCache.has(normalized)) {
return maintainerCache.get(normalized);
}
let isMember = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: maintainerTeam,
username: normalized,
});
isMember = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
maintainerCache.set(normalized, isMember);
return isMember;
};
const countMaintainerMentions = async (body, authorLogin) => {
if (!body) {
return 0;
}
const normalizedAuthor = authorLogin ? normalizeLogin(authorLogin) : "";
if (normalizedAuthor && (await isMaintainer(normalizedAuthor))) {
return 0;
}
const haystack = body.toLowerCase();
const teamMention = `@${context.repo.owner.toLowerCase()}/${maintainerTeam}`;
if (haystack.includes(teamMention)) {
return 3;
}
const mentions = new Set();
for (const match of body.matchAll(mentionRegex)) {
mentions.add(normalizeLogin(match[1]));
}
if (normalizedAuthor) {
mentions.delete(normalizedAuthor);
}
let count = 0;
for (const login of mentions) {
if (await isMaintainer(login)) {
count += 1;
}
}
return count;
};
const triggerLabel = "trigger-response";
const activePrLimitLabel = "r: too-many-prs";
const activePrLimitOverrideLabel = "r: too-many-prs-override";
const target = context.payload.issue ?? context.payload.pull_request;
if (!target) {
return;
}
const labelSet = new Set(
(target.labels ?? [])
.map((label) => (typeof label === "string" ? label : label?.name))
.filter((name) => typeof name === "string"),
);
const issue = context.payload.issue;
const pullRequest = context.payload.pull_request;
const comment = context.payload.comment;
if (comment) {
const authorLogin = comment.user?.login ?? "";
if (comment.user?.type === "Bot" || authorLogin.endsWith("[bot]")) {
return;
}
const commentBody = comment.body ?? "";
const responses = [];
const mentionCount = await countMaintainerMentions(commentBody, authorLogin);
if (mentionCount >= 3) {
responses.push(pingWarningMessage);
}
const commentHaystack = commentBody.toLowerCase();
const commentRule = rules.find((item) =>
(item.commentTriggers ?? []).some((trigger) =>
commentHaystack.includes(trigger),
),
);
if (commentRule) {
responses.push(commentRule.message);
}
if (responses.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: target.number,
body: responses.join("\n\n"),
});
}
return;
}
if (issue) {
const action = context.payload.action;
if (action === "opened" || action === "edited") {
const issueText = `${issue.title ?? ""}\n${issue.body ?? ""}`.trim();
const authorLogin = issue.user?.login ?? "";
const mentionCount = await countMaintainerMentions(
issueText,
authorLogin,
);
if (mentionCount >= 3) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: pingWarningMessage,
});
}
await syncBugSubtypeLabel(issue, labelSet);
}
}
const hasTriggerLabel = labelSet.has(triggerLabel);
if (hasTriggerLabel) {
labelSet.delete(triggerLabel);
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: target.number,
name: triggerLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
const isLabelEvent = context.payload.action === "labeled";
if (!hasTriggerLabel && !isLabelEvent) {
return;
}
if (issue) {
const title = issue.title ?? "";
const body = issue.body ?? "";
const haystack = `${title}\n${body}`.toLowerCase();
const hasMoltbookLabel = labelSet.has("r: moltbook");
const hasTestflightLabel = labelSet.has("r: testflight");
const hasSecurityLabel = labelSet.has("security");
if (title.toLowerCase().includes("security") && !hasSecurityLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["security"],
});
labelSet.add("security");
}
if (title.toLowerCase().includes("testflight") && !hasTestflightLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: testflight"],
});
labelSet.add("r: testflight");
}
if (haystack.includes("moltbook") && !hasMoltbookLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: moltbook"],
});
labelSet.add("r: moltbook");
}
}
const invalidLabel = "invalid";
const spamLabel = "r: spam";
const dirtyLabel = "dirty";
const badBarnacleLabel = "bad-barnacle";
const noisyPrMessage =
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
if (pullRequest) {
if (labelSet.has(badBarnacleLabel)) {
core.info(`Skipping PR auto-response checks for #${pullRequest.number} because ${badBarnacleLabel} is present.`);
return;
}
if (labelSet.has(dirtyLabel)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body: noisyPrMessage,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
const labelCount = labelSet.size;
if (labelCount > 20) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body: noisyPrMessage,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
if (labelSet.has(spamLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
lock_reason: "spam",
});
return;
}
if (labelSet.has(invalidLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
}
if (issue && labelSet.has(spamLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
lock_reason: "spam",
});
return;
}
if (issue && labelSet.has(invalidLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
});
return;
}
if (pullRequest && labelSet.has(activePrLimitOverrideLabel)) {
labelSet.delete(activePrLimitLabel);
}
const rule = rules.find((item) => labelSet.has(item.label));
if (!rule) {
return;
}
const issueNumber = target.number;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: rule.message,
});
if (rule.close) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: "closed",
});
}
if (rule.lock) {
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
lock_reason: rule.lockReason ?? "resolved",
});
}

1948
.github/workflows/ci.yml vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,139 +0,0 @@
name: CodeQL
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
concurrency:
group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ matrix.runs_on }}
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: true
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ./.github/codeql/codeql-javascript-typescript.yml
- language: actions
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: python
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: true
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: java-kotlin
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: true
needs_swift_tools: false
needs_manual_build: true
needs_autobuild: false
config_file: ""
- language: swift
runs_on: macos-latest
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: true
needs_manual_build: true
needs_autobuild: false
config_file: ""
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Setup Node environment
if: matrix.needs_node
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Setup Python
if: matrix.needs_python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Setup Java
if: matrix.needs_java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- name: Setup Swift build tools
if: matrix.needs_swift_tools
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
brew install xcodegen swiftlint swiftformat
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
config-file: ${{ matrix.config_file || '' }}
- name: Autobuild
if: matrix.needs_autobuild
uses: github/codeql-action/autobuild@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
- name: Build Android for CodeQL
if: matrix.language == 'java-kotlin'
working-directory: apps/android
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Build Swift for CodeQL
if: matrix.language == 'swift'
run: |
set -euo pipefail
swift build --package-path apps/macos --configuration release
cd apps/ios
xcodegen generate
xcodebuild build \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "generic/platform=iOS Simulator" \
CODE_SIGNING_ALLOWED=NO
- name: Analyze
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -1,172 +0,0 @@
name: Control UI Locale Refresh
on:
push:
branches:
- main
paths:
- ui/src/i18n/locales/en.ts
- ui/src/i18n/locales/*.ts
- ui/src/i18n/.i18n/*
- ui/src/i18n/lib/types.ts
- ui/src/i18n/lib/registry.ts
- scripts/control-ui-i18n.ts
- .github/workflows/control-ui-locale-refresh.yml
release:
types:
- published
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: control-ui-locale-refresh
cancel-in-progress: false
jobs:
plan:
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'push' || github.actor != 'github-actions[bot]')
runs-on: ubuntu-latest
outputs:
has_locales: ${{ steps.plan.outputs.has_locales }}
locales_json: ${{ steps.plan.outputs.locales_json }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
submodules: false
- name: Plan locale matrix
id: plan
env:
BEFORE_SHA: ${{ github.event.before }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","tr","uk","id","pl"]'
if [ "$EVENT_NAME" != "push" ]; then
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
exit 0
fi
before_ref="$BEFORE_SHA"
if [ -z "$before_ref" ] || [ "$before_ref" = "0000000000000000000000000000000000000000" ]; then
before_ref="$(git rev-parse HEAD^)"
fi
changed_files="$(git diff --name-only "$before_ref" HEAD)"
echo "changed files:"
printf '%s\n' "$changed_files"
if printf '%s\n' "$changed_files" | grep -Eq '^(ui/src/i18n/locales/en\.ts|ui/src/i18n/lib/types\.ts|ui/src/i18n/lib/registry\.ts|scripts/control-ui-i18n\.ts|\.github/workflows/control-ui-locale-refresh\.yml)$'; then
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
exit 0
fi
locales_json="$(printf '%s\n' "$changed_files" | node <<'EOF'
const fs = require("node:fs");
const changed = fs.readFileSync(0, "utf8").split(/\r?\n/).filter(Boolean);
const locales = new Set();
for (const file of changed) {
let match = file.match(/^ui\/src\/i18n\/locales\/(.+)\.ts$/);
if (match && match[1] !== "en") {
locales.add(match[1]);
continue;
}
match = file.match(/^ui\/src\/i18n\/\.i18n\/(.+)\.(?:meta\.json|tm\.jsonl)$/);
if (match) {
locales.add(match[1]);
}
}
process.stdout.write(JSON.stringify([...locales]));
EOF
)"
if [ "$locales_json" = "[]" ]; then
echo "has_locales=false" >> "$GITHUB_OUTPUT"
echo "locales_json=[]" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$locales_json" >> "$GITHUB_OUTPUT"
refresh:
needs: plan
if: github.repository == 'openclaw/openclaw' && needs.plan.outputs.has_locales == 'true'
strategy:
fail-fast: false
max-parallel: 4
matrix:
locale: ${{ fromJson(needs.plan.outputs.locales_json) }}
runs-on: ubuntu-latest
name: Refresh ${{ matrix.locale }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: true
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure translation provider secrets exist
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -euo pipefail
if [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "Missing OPENCLAW_DOCS_I18N_OPENAI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY secret."
exit 1
fi
- name: Refresh control UI locale files
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_MODEL: gpt-5.4
OPENCLAW_CONTROL_UI_I18N_THINKING: low
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${{ matrix.locale }}" --write
- name: Commit and push locale updates
env:
LOCALE: ${{ matrix.locale }}
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
if git diff --quiet -- ui/src/i18n; then
echo "No control UI locale changes for ${LOCALE}."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A ui/src/i18n
git commit --no-verify -m "chore(ui): refresh ${LOCALE} control ui locale"
for attempt in 1 2 3 4 5; do
git fetch origin "${TARGET_BRANCH}"
git rebase --autostash "origin/${TARGET_BRANCH}"
if git push origin HEAD:"${TARGET_BRANCH}"; then
exit 0
fi
echo "Push attempt ${attempt} for ${LOCALE} failed; retrying."
sleep $((attempt * 2))
done
echo "Failed to push ${LOCALE} locale update after retries."
exit 1

View File

@@ -1,389 +0,0 @@
name: Docker Release
on:
push:
tags:
- "v*"
paths-ignore:
- "docs/**"
- "**/*.md"
- "**/*.mdx"
- ".agents/**"
- "skills/**"
workflow_dispatch:
inputs:
tag:
description: Existing release tag to backfill (for example v2026.3.22)
required: true
type: string
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('docker-release-manual-{0}', inputs.tag) || format('docker-release-push-{0}', github.run_id) }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
validate_manual_backfill:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
echo "Invalid release tag: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout selected tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
approve_manual_backfill:
if: github.event_name == 'workflow_dispatch'
needs: validate_manual_backfill
# WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
runs-on: ubuntu-24.04
environment: docker-release
steps:
- name: Approve Docker backfill
env:
RELEASE_TAG: ${{ inputs.tag }}
run: echo "Approved Docker backfill for $RELEASE_TAG"
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
# Build amd64 images (default + slim share the build stage cache)
build-amd64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04
permissions:
packages: write
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image tags (amd64)
id: tags
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main-amd64")
slim_tags+=("${IMAGE}:main-slim-amd64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}-amd64")
slim_tags+=("${IMAGE}:${version}-slim-amd64")
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No amd64 tags resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Resolve OCI labels (amd64)
id: labels
shell: bash
env:
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
version="${source_sha}"
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
version="main"
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
fi
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
echo "value<<EOF"
echo "org.opencontainers.image.revision=${source_sha}"
echo "org.opencontainers.image.version=${version}"
echo "org.opencontainers.image.created=${created}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push amd64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
# Build arm64 images (default + slim share the build stage cache)
build-arm64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04-arm
permissions:
packages: write
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image tags (arm64)
id: tags
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main-arm64")
slim_tags+=("${IMAGE}:main-slim-arm64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}-arm64")
slim_tags+=("${IMAGE}:${version}-slim-arm64")
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No arm64 tags resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Resolve OCI labels (arm64)
id: labels
shell: bash
env:
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
version="${source_sha}"
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
version="main"
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
fi
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
echo "value<<EOF"
echo "org.opencontainers.image.revision=${source_sha}"
echo "org.opencontainers.image.version=${version}"
echo "org.opencontainers.image.created=${created}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push arm64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
# Create multi-platform manifests
create-manifest:
needs: [approve_manual_backfill, build-amd64, build-arm64]
if: ${{ always() && needs.build-amd64.result == 'success' && needs.build-arm64.result == 'success' && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve manifest tags
id: tags
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main")
slim_tags+=("${IMAGE}:main-slim")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}")
slim_tags+=("${IMAGE}:${version}-slim")
# Manual backfills should only republish the requested version tags.
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
tags+=("${IMAGE}:latest")
slim_tags+=("${IMAGE}:slim")
fi
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No manifest tags resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create and push default manifest
shell: bash
run: |
set -euo pipefail
mapfile -t tags <<< "${{ steps.tags.outputs.value }}"
args=()
for tag in "${tags[@]}"; do
[ -z "$tag" ] && continue
args+=("-t" "$tag")
done
docker buildx imagetools create "${args[@]}" \
${{ needs.build-amd64.outputs.digest }} \
${{ needs.build-arm64.outputs.digest }}
- name: Create and push slim manifest
shell: bash
run: |
set -euo pipefail
mapfile -t tags <<< "${{ steps.tags.outputs.slim }}"
args=()
for tag in "${tags[@]}"; do
[ -z "$tag" ] && continue
args+=("-t" "$tag")
done
docker buildx imagetools create "${args[@]}" \
${{ needs.build-amd64.outputs.slim-digest }} \
${{ needs.build-arm64.outputs.slim-digest }}

View File

@@ -1,70 +0,0 @@
name: Docs Sync Publish Repo
on:
push:
branches:
- main
paths:
- docs/**
- scripts/docs-sync-publish.mjs
- .github/workflows/docs-sync-publish.yml
workflow_dispatch:
permissions:
contents: read
jobs:
sync-publish-repo:
runs-on: ubuntu-latest
steps:
- name: Checkout source repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22.18.0"
- name: Clone publish repo
env:
OPENCLAW_DOCS_SYNC_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
run: |
set -euo pipefail
git clone \
"https://x-access-token:${OPENCLAW_DOCS_SYNC_TOKEN}@github.com/openclaw/docs.git" \
publish
- name: Sync docs into publish repo
run: |
node scripts/docs-sync-publish.mjs \
--target "$GITHUB_WORKSPACE/publish" \
--source-repo "$GITHUB_REPOSITORY" \
--source-sha "$GITHUB_SHA"
- name: Commit publish repo sync
working-directory: publish
run: |
set -euo pipefail
if git diff --quiet -- docs .openclaw-sync; then
echo "No publish-repo changes."
exit 0
fi
git config user.name "openclaw-docs-sync[bot]"
git config user.email "openclaw-docs-sync[bot]@users.noreply.github.com"
git add docs .openclaw-sync
git commit -m "chore(sync): mirror docs from $GITHUB_REPOSITORY@$GITHUB_SHA"
for attempt in 1 2 3 4 5; do
git fetch origin main
git rebase origin/main
if git push origin HEAD:main; then
exit 0
fi
echo "Push attempt ${attempt} failed; retrying."
sleep $((attempt * 2))
done
echo "Failed to push publish-repo sync after retries."
exit 1

View File

@@ -1,42 +0,0 @@
name: Docs Trigger Locale Translate On Release
on:
release:
types:
- published
permissions:
contents: read
jobs:
dispatch-translate:
runs-on: ubuntu-latest
steps:
- name: Trigger locale translates in publish repo
env:
GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
for event_type in \
translate-zh-cn-release \
translate-ja-jp-release \
translate-es-release \
translate-pt-br-release \
translate-ko-release \
translate-de-release \
translate-fr-release \
translate-ar-release \
translate-it-release \
translate-tr-release \
translate-uk-release \
translate-id-release \
translate-pl-release
do
gh api repos/openclaw/docs/dispatches \
--method POST \
-f event_type="${event_type}" \
-f client_payload[release_tag]="${RELEASE_TAG}" \
-f client_payload[source_repository]="${GITHUB_REPOSITORY}" \
-f client_payload[source_sha]="${GITHUB_SHA}"
done

View File

@@ -4,216 +4,31 @@ on:
push:
branches: [main]
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
preflight:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
run_install_smoke: ${{ steps.manifest.outputs.run_install_smoke }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Ensure preflight base commit
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
- name: Detect docs-only changes
id: docs_scope
uses: ./.github/actions/detect-docs-changes
- name: Detect changed smoke scope
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
else
BASE="${{ github.event.pull_request.base.sha }}"
fi
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Setup Node environment
if: steps.docs_scope.outputs.docs_only != 'true'
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "false"
use-sticky-disk: "false"
- name: Build install-smoke CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_RUN_CHANGED_SMOKE: ${{ steps.changed_scope.outputs.run_changed_smoke || 'false' }}
run: |
docs_only="${OPENCLAW_CI_DOCS_ONLY:-false}"
run_changed_smoke="${OPENCLAW_CI_RUN_CHANGED_SMOKE:-false}"
run_install_smoke=false
if [ "$docs_only" != "true" ] && [ "$run_changed_smoke" = "true" ]; then
run_install_smoke=true
fi
{
echo "docs_only=$docs_only"
echo "run_install_smoke=$run_install_smoke"
} >> "$GITHUB_OUTPUT"
install-smoke:
needs: [preflight]
if: needs.preflight.outputs.run_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
runs-on: ubuntu-latest
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
# Blacksmith can fall back to the local docker driver, which rejects gha
# cache export/import. Keep smoke builds driver-agnostic.
- name: Build root Dockerfile smoke image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
tags: openclaw-dockerfile-smoke:local
load: true
push: false
provenance: false
version: 10
- name: Enable Corepack
run: corepack enable
- name: Run root Dockerfile CLI smoke
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
# This smoke validates that the build-arg path preinstalls the matrix
# runtime deps declared by the plugin and that matrix discovery stays
# healthy in the final runtime image.
- name: Build extension Dockerfile smoke image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: openclaw-ext-smoke:local
load: true
push: false
provenance: false
- name: Smoke test Dockerfile with matrix extension build arg
run: |
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
const Module = require(\"node:module\");
const matrixPackage = require(\"/app/extensions/matrix/package.json\");
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {});
if (runtimeDeps.length === 0) {
throw new Error(
\"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\",
);
}
for (const dep of runtimeDeps) {
requireFromMatrix.resolve(dep);
}
const { spawnSync } = require(\"node:child_process\");
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
if (run.status !== 0) {
process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\");
process.exit(run.status ?? 1);
}
const parsed = JSON.parse(run.stdout);
const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\");
if (!matrix) {
throw new Error(\"matrix plugin missing from bundled plugin list\");
}
const matrixDiag = (parsed.diagnostics || []).filter(
(diag) =>
typeof diag.source === \"string\" &&
diag.source.includes(\"/extensions/matrix\") &&
typeof diag.message === \"string\" &&
diag.message.includes(\"extension entry escapes package directory\"),
);
if (matrixDiag.length > 0) {
throw new Error(
\"unexpected matrix diagnostics: \" +
matrixDiag.map((diag) => diag.message).join(\"; \"),
);
}
"
'
- name: Build installer smoke image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-smoke/Dockerfile
tags: openclaw-install-smoke:local
load: true
push: false
provenance: false
- name: Build installer non-root image
if: github.event_name != 'pull_request'
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-nonroot/Dockerfile
tags: openclaw-install-nonroot:local
load: true
push: false
provenance: false
- name: Setup Node environment for local pack smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "true"
use-sticky-disk: "false"
- name: Install pnpm deps (minimal)
run: pnpm install --ignore-scripts --frozen-lockfile
- name: Run installer docker tests
env:
OPENCLAW_INSTALL_URL: https://openclaw.ai/install.sh
OPENCLAW_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh
OPENCLAW_NO_ONBOARD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: ${{ github.event_name == 'pull_request' && '0' || '1' }}
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh
CLAWDBOT_INSTALL_URL: https://clawd.bot/install.sh
CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh
CLAWDBOT_NO_ONBOARD: "1"
CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1"
CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
CLAWDBOT_INSTALL_SMOKE_PREVIOUS: "2026.1.11-4"
run: pnpm test:install:smoke

View File

@@ -1,877 +0,0 @@
name: Labeler
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned triage workflow; no untrusted checkout or PR code execution
types: [opened, synchronize, reopened, edited]
issues:
types: [opened, edited]
workflow_dispatch:
inputs:
max_prs:
description: "Maximum number of open PRs to process (0 = all)"
required: false
default: "200"
per_page:
description: "PRs per page (1-100)"
required: false
default: "50"
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
permissions: {}
jobs:
label:
permissions:
contents: read
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v2
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- uses: actions/labeler@v6
with:
configuration-path: .github/labeler.yml
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
sync-labels: true
- name: Apply PR size label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
return;
}
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const labelColor = "b76e79";
for (const label of sizeLabels) {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
color: labelColor,
});
}
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequest.number,
per_page: 100,
});
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
const totalChangedLines = files.reduce((total, file) => {
const path = file.filename ?? "";
if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) {
return total;
}
return total + (file.additions ?? 0) + (file.deletions ?? 0);
}, 0);
let targetSizeLabel = "size: XL";
if (totalChangedLines < 50) {
targetSizeLabel = "size: XS";
} else if (totalChangedLines < 200) {
targetSizeLabel = "size: S";
} else if (totalChangedLines < 500) {
targetSizeLabel = "size: M";
} else if (totalChangedLines < 1000) {
targetSizeLabel = "size: L";
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
for (const label of currentLabels) {
const name = label.name ?? "";
if (!sizeLabels.includes(name)) {
continue;
}
if (name === targetSizeLabel) {
continue;
}
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name,
});
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [targetSizeLabel],
});
- name: Apply maintainer or trusted-contributor label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const login = context.payload.pull_request?.user?.login;
if (!login) {
return;
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
// const trustedLabel = "trusted-contributor";
// const experiencedLabel = "experienced-contributor";
// const trustedThreshold = 4;
// const experiencedThreshold = 10;
let isMaintainer = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: "maintainer",
username: login,
});
isMaintainer = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
if (isMaintainer) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: context.payload.pull_request.number,
labels: ["maintainer"],
});
return;
}
// trusted-contributor and experienced-contributor labels disabled.
// const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`;
// let mergedCount = 0;
// try {
// const merged = await github.rest.search.issuesAndPullRequests({
// q: mergedQuery,
// per_page: 1,
// });
// mergedCount = merged?.data?.total_count ?? 0;
// } catch (error) {
// if (error?.status !== 422) {
// throw error;
// }
// core.warning(`Skipping merged search for ${login}; treating as 0.`);
// }
//
// if (mergedCount >= experiencedThreshold) {
// await github.rest.issues.addLabels({
// ...context.repo,
// issue_number: context.payload.pull_request.number,
// labels: [experiencedLabel],
// });
// return;
// }
//
// if (mergedCount >= trustedThreshold) {
// await github.rest.issues.addLabels({
// ...context.repo,
// issue_number: context.payload.pull_request.number,
// labels: [trustedLabel],
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
return;
}
const labelName = "beta-blocker";
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
return;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const hasLabel = currentLabels.some((label) => label.name === labelName);
if (matchesBetaBlocker && !hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [labelName],
});
return;
}
if (!matchesBetaBlocker && hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: labelName,
});
}
- name: Apply too-many-prs label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
return;
}
const activePrLimitLabel = "r: too-many-prs";
const activePrLimitOverrideLabel = "r: too-many-prs-override";
const activePrLimit = 10;
const labelColor = "B60205";
const labelDescription = `Author has more than ${activePrLimit} active PRs in this repo`;
const authorLogin = pullRequest.user?.login;
if (!authorLogin) {
return;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const labelNames = new Set(
currentLabels
.map((label) => (typeof label === "string" ? label : label?.name))
.filter((name) => typeof name === "string"),
);
if (labelNames.has(activePrLimitOverrideLabel)) {
if (labelNames.has(activePrLimitLabel)) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: activePrLimitLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
return;
}
const ensureLabelExists = async () => {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: activePrLimitLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: activePrLimitLabel,
color: labelColor,
description: labelDescription,
});
}
};
const isPrivilegedAuthor = async () => {
if (pullRequest.author_association === "OWNER") {
return true;
}
let isMaintainer = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: "maintainer",
username: authorLogin,
});
isMaintainer = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
if (isMaintainer) {
return true;
}
try {
const permission = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: authorLogin,
});
const roleName = (permission?.data?.role_name ?? "").toLowerCase();
return roleName === "admin" || roleName === "maintain";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
return false;
};
if (await isPrivilegedAuthor()) {
if (labelNames.has(activePrLimitLabel)) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: activePrLimitLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
return;
}
let openPrCount = 0;
try {
const result = await github.rest.search.issuesAndPullRequests({
q: `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open author:${authorLogin}`,
per_page: 1,
});
openPrCount = result?.data?.total_count ?? 0;
} catch (error) {
if (error?.status !== 422) {
throw error;
}
core.warning(`Skipping open PR count for ${authorLogin}; treating as 0.`);
}
if (openPrCount > activePrLimit) {
await ensureLabelExists();
if (!labelNames.has(activePrLimitLabel)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [activePrLimitLabel],
});
}
return;
}
if (labelNames.has(activePrLimitLabel)) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: activePrLimitLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
backfill-pr-labels:
if: github.event_name == 'workflow_dispatch'
permissions:
contents: read
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v2
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Backfill PR labels
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const repoFull = `${owner}/${repo}`;
const inputs = context.payload.inputs ?? {};
const maxPrsInput = inputs.max_prs ?? "200";
const perPageInput = inputs.per_page ?? "50";
const parsedMaxPrs = Number.parseInt(maxPrsInput, 10);
const parsedPerPage = Number.parseInt(perPageInput, 10);
const maxPrs = Number.isFinite(parsedMaxPrs) ? parsedMaxPrs : 200;
const perPage = Number.isFinite(parsedPerPage) ? Math.min(100, Math.max(1, parsedPerPage)) : 50;
const processAll = maxPrs <= 0;
const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs);
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const betaBlockerLabel = "beta-blocker";
const labelColor = "b76e79";
// const trustedLabel = "trusted-contributor";
// const experiencedLabel = "experienced-contributor";
// const trustedThreshold = 4;
// const experiencedThreshold = 10;
const contributorCache = new Map();
async function ensureSizeLabels() {
for (const label of sizeLabels) {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: label,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner,
repo,
name: label,
color: labelColor,
});
}
}
}
async function hasBetaBlockerLabel() {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: betaBlockerLabel,
});
return true;
} catch (error) {
if (error?.status !== 404) {
throw error;
}
return false;
}
}
async function resolveContributorLabel(login) {
if (contributorCache.has(login)) {
return contributorCache.get(login);
}
let isMaintainer = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: owner,
team_slug: "maintainer",
username: login,
});
isMaintainer = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
if (isMaintainer) {
contributorCache.set(login, "maintainer");
return "maintainer";
}
// trusted-contributor and experienced-contributor labels disabled.
// const mergedQuery = `repo:${repoFull} is:pr is:merged author:${login}`;
// let mergedCount = 0;
// try {
// const merged = await github.rest.search.issuesAndPullRequests({
// q: mergedQuery,
// per_page: 1,
// });
// mergedCount = merged?.data?.total_count ?? 0;
// } catch (error) {
// if (error?.status !== 422) {
// throw error;
// }
// core.warning(`Skipping merged search for ${login}; treating as 0.`);
// }
const label = null;
// if (mergedCount >= experiencedThreshold) {
// label = experiencedLabel;
// } else if (mergedCount >= trustedThreshold) {
// label = trustedLabel;
// }
contributorCache.set(login, label);
return label;
}
async function applySizeLabel(pullRequest, currentLabels, labelNames) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pullRequest.number,
per_page: 100,
});
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
const totalChangedLines = files.reduce((total, file) => {
const path = file.filename ?? "";
if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) {
return total;
}
return total + (file.additions ?? 0) + (file.deletions ?? 0);
}, 0);
let targetSizeLabel = "size: XL";
if (totalChangedLines < 50) {
targetSizeLabel = "size: XS";
} else if (totalChangedLines < 200) {
targetSizeLabel = "size: S";
} else if (totalChangedLines < 500) {
targetSizeLabel = "size: M";
} else if (totalChangedLines < 1000) {
targetSizeLabel = "size: L";
}
for (const label of currentLabels) {
const name = label.name ?? "";
if (!sizeLabels.includes(name)) {
continue;
}
if (name === targetSizeLabel) {
continue;
}
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pullRequest.number,
name,
});
labelNames.delete(name);
}
if (!labelNames.has(targetSizeLabel)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pullRequest.number,
labels: [targetSizeLabel],
});
labelNames.add(targetSizeLabel);
}
}
async function applyContributorLabel(pullRequest, labelNames) {
const login = pullRequest.user?.login;
if (!login) {
return;
}
const label = await resolveContributorLabel(login);
if (!label) {
return;
}
if (labelNames.has(label)) {
return;
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pullRequest.number,
labels: [label],
});
labelNames.add(label);
}
async function applyBetaBlockerTitleLabel(pullRequest, labelNames) {
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
if (matchesBetaBlocker) {
if (!labelNames.has(betaBlockerLabel)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pullRequest.number,
labels: [betaBlockerLabel],
});
labelNames.add(betaBlockerLabel);
}
return;
}
if (!labelNames.has(betaBlockerLabel)) {
return;
}
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pullRequest.number,
name: betaBlockerLabel,
});
labelNames.delete(betaBlockerLabel);
}
await ensureSizeLabels();
const betaBlockerLabelExists = await hasBetaBlockerLabel();
let page = 1;
let processed = 0;
while (processed < maxCount) {
const remaining = maxCount - processed;
const pageSize = processAll ? perPage : Math.min(perPage, remaining);
const { data: pullRequests } = await github.rest.pulls.list({
owner,
repo,
state: "open",
per_page: pageSize,
page,
});
if (pullRequests.length === 0) {
break;
}
for (const pullRequest of pullRequests) {
if (!processAll && processed >= maxCount) {
break;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
issue_number: pullRequest.number,
per_page: 100,
});
const labelNames = new Set(
currentLabels.map((label) => label.name).filter((name) => typeof name === "string"),
);
await applySizeLabel(pullRequest, currentLabels, labelNames);
await applyContributorLabel(pullRequest, labelNames);
if (betaBlockerLabelExists) {
await applyBetaBlockerTitleLabel(pullRequest, labelNames);
}
processed += 1;
}
if (pullRequests.length < pageSize) {
break;
}
page += 1;
}
core.info(`Processed ${processed} pull requests.`);
label-issues:
permissions:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v2
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Apply maintainer or trusted-contributor label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const login = context.payload.issue?.user?.login;
if (!login) {
return;
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
// const trustedLabel = "trusted-contributor";
// const experiencedLabel = "experienced-contributor";
// const trustedThreshold = 4;
// const experiencedThreshold = 10;
let isMaintainer = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: "maintainer",
username: login,
});
isMaintainer = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
if (isMaintainer) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: context.payload.issue.number,
labels: ["maintainer"],
});
return;
}
// trusted-contributor and experienced-contributor labels disabled.
// const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`;
// let mergedCount = 0;
// try {
// const merged = await github.rest.search.issuesAndPullRequests({
// q: mergedQuery,
// per_page: 1,
// });
// mergedCount = merged?.data?.total_count ?? 0;
// } catch (error) {
// if (error?.status !== 422) {
// throw error;
// }
// core.warning(`Skipping merged search for ${login}; treating as 0.`);
// }
//
// if (mergedCount >= experiencedThreshold) {
// await github.rest.issues.addLabels({
// ...context.repo,
// issue_number: context.payload.issue.number,
// labels: [experiencedLabel],
// });
// return;
// }
//
// if (mergedCount >= trustedThreshold) {
// await github.rest.issues.addLabels({
// ...context.repo,
// issue_number: context.payload.issue.number,
// labels: [trustedLabel],
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
const issue = context.payload.issue;
if (!issue || issue.pull_request) {
return;
}
const labelName = "beta-blocker";
const matchesBetaBlocker = /^beta blocker:/i.test(issue.title ?? "");
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
return;
}
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
});
const hasLabel = currentLabels.some((label) => label.name === labelName);
if (matchesBetaBlocker && !hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [labelName],
});
return;
}
if (!matchesBetaBlocker && hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: labelName,
});
}

View File

@@ -1,93 +0,0 @@
name: macOS Release
on:
workflow_dispatch:
inputs:
tag:
description: Existing release tag to validate for macOS release handoff (for example v2026.3.22 or v2026.3.22-beta.1)
required: true
type: string
preflight_only:
description: Retained for operator compatibility; this public workflow is validation-only
required: true
default: true
type: boolean
concurrency:
group: macos-release-${{ inputs.tag }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
validate_macos_release_request:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout selected tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure matching GitHub release exists
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
- name: Build
run: pnpm build
- name: Build Control UI
run: pnpm ui:build
- name: Validate release tag and package metadata
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
pnpm release:openclaw:npm:check
- name: Summarize next step
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
{
echo "## Public macOS validation only"
echo
echo "This workflow validates the public release handoff and still builds JS artifacts needed for release checks."
echo "It does not sign, notarize, or upload macOS assets."
echo
echo "Next step:"
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml\` with tag \`${RELEASE_TAG}\` and wait for the private mac validation lane to pass."
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\` and \`preflight_only=true\` for the full private mac preflight."
echo "- For the real publish path, run the same private mac publish workflow from \`main\` with the successful private preflight \`preflight_run_id\` so it promotes the prepared artifacts instead of rebuilding them."
echo "- For stable releases, also download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,472 +0,0 @@
name: OpenClaw Cross-OS Release Checks (Reusable)
on:
workflow_dispatch:
inputs:
ref:
description: Public OpenClaw ref to validate (tag, branch, or full commit SHA)
required: true
default: main
type: string
workflow_ref:
description: Optional openclaw/openclaw ref that provides the reusable workflow harness
required: false
default: ""
type: string
provider:
description: Provider lane to use for onboarding and the end-to-end turn
required: true
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which release-check lanes to run
required: true
default: both
type: choice
options:
- fresh
- upgrade
- both
previous_version:
description: Optional baseline version for installer/dev-update and packaged upgrade
required: false
default: ""
type: string
ubuntu_runner:
description: Optional Linux runner label override
required: false
default: ""
type: string
windows_runner:
description: Optional Windows runner label override
required: false
default: ""
type: string
macos_runner:
description: Optional macOS runner label override
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
description: Public OpenClaw ref to validate (tag, branch, or full commit SHA)
required: true
type: string
workflow_ref:
description: Optional openclaw/openclaw ref that provides the reusable workflow harness
required: false
default: ""
type: string
provider:
description: Provider lane to use for onboarding and the end-to-end turn
required: true
type: string
mode:
description: Which release-check lanes to run
required: true
type: string
previous_version:
description: Optional baseline version for the upgrade lane (defaults to npm latest)
required: false
default: ""
type: string
ubuntu_runner:
description: Optional Linux runner label override
required: false
default: ""
type: string
windows_runner:
description: Optional Windows runner label override
required: false
default: ""
type: string
macos_runner:
description: Optional macOS runner label override
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
ANTHROPIC_API_KEY:
required: false
MINIMAX_API_KEY:
required: false
OPENCLAW_DISCORD_SMOKE_BOT_TOKEN:
required: false
OPENCLAW_DISCORD_SMOKE_GUILD_ID:
required: false
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID:
required: false
permissions: read-all
concurrency:
group: openclaw-cross-os-release-checks-${{ inputs.ref }}-${{ inputs.provider }}-${{ inputs.mode }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
OPENCLAW_REPOSITORY: openclaw/openclaw
TSX_VERSION: "4.21.0"
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }}
baseline_spec: ${{ steps.baseline.outputs.value }}
candidate_file_name: ${{ steps.candidate_metadata.outputs.file_name }}
candidate_version: ${{ steps.candidate_metadata.outputs.version }}
matrix: ${{ steps.matrix.outputs.value }}
source_sha: ${{ steps.candidate_metadata.outputs.source_sha }}
workflow_ref: ${{ steps.workflow_ref.outputs.value }}
steps:
- name: Validate provider secret availability
env:
PROVIDER: ${{ inputs.provider }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
run: |
set -euo pipefail
case "${PROVIDER}" in
openai)
[[ -n "${OPENAI_API_KEY}" ]] || { echo "Missing OPENAI_API_KEY secret." >&2; exit 1; }
;;
anthropic)
[[ -n "${ANTHROPIC_API_KEY}" ]] || { echo "Missing ANTHROPIC_API_KEY secret." >&2; exit 1; }
;;
minimax)
[[ -n "${MINIMAX_API_KEY}" ]] || { echo "Missing MINIMAX_API_KEY secret." >&2; exit 1; }
;;
*)
echo "Unsupported provider: ${PROVIDER}" >&2
exit 1
;;
esac
- name: Resolve workflow ref
id: workflow_ref
env:
INPUT_WORKFLOW_REF: ${{ inputs.workflow_ref }}
CALLER_REPOSITORY: ${{ github.repository }}
CURRENT_SHA: ${{ github.sha }}
WORKFLOW_CONTEXT_REF: ${{ github.workflow_ref }}
WORKFLOW_REPOSITORY: ${{ env.OPENCLAW_REPOSITORY }}
run: |
set -euo pipefail
resolve_unique_remote_ref() {
local remote_url="$1"
shift
local -a refs=("$@")
local -a matches=()
local ref=""
for ref in "${refs[@]}"; do
[[ -n "${ref}" ]] || continue
mapfile -t matches < <(
git ls-remote "${remote_url}" "${ref}" | awk '{print $1}' | awk '!seen[$0]++'
)
if [[ "${#matches[@]}" -eq 0 ]]; then
continue
fi
if [[ "${#matches[@]}" -ne 1 ]]; then
return 2
fi
printf '%s\n' "${matches[0]}"
return 0
done
return 1
}
if [[ -n "${INPUT_WORKFLOW_REF}" ]]; then
TARGET_REF="${INPUT_WORKFLOW_REF}"
elif [[ "${CALLER_REPOSITORY}" == "${WORKFLOW_REPOSITORY}" ]]; then
TARGET_REF="${CURRENT_SHA}"
elif [[ "${WORKFLOW_CONTEXT_REF}" == "${WORKFLOW_REPOSITORY}/"* ]] && [[ "${WORKFLOW_CONTEXT_REF}" == *"@"* ]]; then
TARGET_REF="${WORKFLOW_CONTEXT_REF##*@}"
else
echo "Failed to infer workflow ref from github.workflow_ref=${WORKFLOW_CONTEXT_REF}" >&2
exit 1
fi
if [[ "${TARGET_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "value=${TARGET_REF}" >> "$GITHUB_OUTPUT"
exit 0
fi
REMOTE_URL="https://github.com/${WORKFLOW_REPOSITORY}.git"
if [[ "${TARGET_REF}" == refs/* ]]; then
if [[ "${TARGET_REF}" == refs/tags/* ]]; then
mapfile -t MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}^{}" "${TARGET_REF}" || true
)
else
mapfile -t MATCHES < <(resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}" || true)
fi
else
mapfile -t BRANCH_MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "refs/heads/${TARGET_REF}" || true
)
mapfile -t TAG_MATCHES < <(
resolve_unique_remote_ref "${REMOTE_URL}" "refs/tags/${TARGET_REF}^{}" "refs/tags/${TARGET_REF}" || true
)
MATCH_COUNT=$(( ${#BRANCH_MATCHES[@]} + ${#TAG_MATCHES[@]} ))
if [[ "${MATCH_COUNT}" -eq 1 ]]; then
if [[ "${#BRANCH_MATCHES[@]}" -eq 1 ]]; then
MATCHES=("${BRANCH_MATCHES[0]}")
else
MATCHES=("${TAG_MATCHES[0]}")
fi
elif [[ "${MATCH_COUNT}" -eq 0 ]]; then
MATCHES=()
else
echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2
exit 1
fi
fi
case "${#MATCHES[@]}" in
1)
echo "value=${MATCHES[0]}" >> "$GITHUB_OUTPUT"
;;
0)
echo "Failed to resolve workflow ref: ${TARGET_REF}" >&2
exit 1
;;
*)
echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2
exit 1
;;
esac
- name: Checkout workflow repo
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ steps.workflow_ref.outputs.value }}
path: workflow
fetch-depth: 1
persist-credentials: false
- name: Checkout public source ref
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
persist-credentials: false
submodules: recursive
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Build candidate artifact once
env:
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare
run: |
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--prepare-only \
--source-dir source \
--output-dir "${OUTPUT_DIR}"
- name: Resolve baseline package spec
if: ${{ inputs.mode != 'fresh' }}
id: baseline
env:
INPUT_PREVIOUS_VERSION: ${{ inputs.previous_version }}
run: |
set -euo pipefail
if [[ -n "${INPUT_PREVIOUS_VERSION}" ]]; then
echo "value=openclaw@${INPUT_PREVIOUS_VERSION}" >> "$GITHUB_OUTPUT"
exit 0
fi
BASELINE_VERSION="$(npm view openclaw@latest version)"
echo "value=openclaw@${BASELINE_VERSION}" >> "$GITHUB_OUTPUT"
- name: Pack baseline artifact
if: ${{ inputs.mode != 'fresh' }}
env:
BASELINE_SPEC: ${{ steps.baseline.outputs.value }}
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline
run: |
mkdir -p "${OUTPUT_DIR}"
npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json"
- name: Capture candidate metadata
id: candidate_metadata
env:
CANDIDATE_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/candidate.json
run: |
node <<'NODE' >>"$GITHUB_OUTPUT"
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(process.env.CANDIDATE_JSON, "utf8"));
process.stdout.write(`file_name=${payload.candidateFileName}\n`);
process.stdout.write(`version=${payload.candidateVersion}\n`);
process.stdout.write(`source_sha=${payload.sourceSha}\n`);
NODE
- name: Capture baseline metadata
if: ${{ inputs.mode != 'fresh' }}
id: baseline_metadata
env:
BASELINE_PACK_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/pack.json
run: |
node <<'NODE' >>"$GITHUB_OUTPUT"
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(process.env.BASELINE_PACK_JSON, "utf8"));
const entry = Array.isArray(payload) ? payload.at(-1) : null;
if (!entry?.filename) {
throw new Error("Baseline npm pack did not produce a filename.");
}
process.stdout.write(`file_name=${entry.filename}\n`);
NODE
- name: Upload candidate artifact
uses: actions/upload-artifact@v7
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package/${{ steps.candidate_metadata.outputs.file_name }}
if-no-files-found: error
- name: Upload baseline artifact
if: ${{ inputs.mode != 'fresh' }}
uses: actions/upload-artifact@v7
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/${{ steps.baseline_metadata.outputs.file_name }}
if-no-files-found: error
- name: Resolve runner matrix
id: matrix
env:
INPUT_REF: ${{ inputs.ref }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_UBUNTU_RUNNER: ${{ inputs.ubuntu_runner }}
INPUT_WINDOWS_RUNNER: ${{ inputs.windows_runner }}
INPUT_MACOS_RUNNER: ${{ inputs.macos_runner }}
VAR_UBUNTU_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER }}
VAR_WINDOWS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER }}
VAR_MACOS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER }}
run: |
MATRIX_JSON="$(pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--resolve-matrix \
--ref "${INPUT_REF}" \
--mode "${INPUT_MODE}" \
--ubuntu-runner "${INPUT_UBUNTU_RUNNER}" \
--windows-runner "${INPUT_WINDOWS_RUNNER}" \
--macos-runner "${INPUT_MACOS_RUNNER}")"
echo "value=${MATRIX_JSON}" >> "$GITHUB_OUTPUT"
cross_os_release_checks:
name: "${{ matrix.display_name }} / ${{ matrix.suite_label }}"
needs: prepare
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- name: Checkout workflow repo
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ needs.prepare.outputs.workflow_ref }}
path: workflow
fetch-depth: 1
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download candidate artifact
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
- name: Download baseline artifact
if: ${{ matrix.suite == 'packaged-upgrade' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Run cross-OS release checks
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCLAW_DISCORD_SMOKE_BOT_TOKEN: ${{ secrets.OPENCLAW_DISCORD_SMOKE_BOT_TOKEN }}
OPENCLAW_DISCORD_SMOKE_GUILD_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_GUILD_ID }}
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
OPENCLAW_RELEASE_CHECK_OS: ${{ matrix.os_id }}
OPENCLAW_RELEASE_CHECK_RUNNER: ${{ matrix.runner }}
run: |
DISCORD_ARGS=()
if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then
DISCORD_ARGS+=(--run-discord-roundtrip true)
fi
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--candidate-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}" \
--candidate-version "${{ needs.prepare.outputs.candidate_version }}" \
--source-sha "${{ needs.prepare.outputs.source_sha }}" \
--baseline-spec "${{ needs.prepare.outputs.baseline_spec }}" \
--previous-version "${{ inputs.previous_version }}" \
--baseline-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}" \
--provider "${{ inputs.provider }}" \
--mode "${{ matrix.lane }}" \
--suite "${{ matrix.suite }}" \
--ref "${{ inputs.ref }}" \
"${DISCORD_ARGS[@]}" \
--output-dir "$RUNNER_TEMP/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}"
- name: Summarize release checks
if: always()
shell: bash
env:
SUMMARY_PATH: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}/summary.md
run: |
if [[ -f "${SUMMARY_PATH}" ]]; then
cat "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
else
echo "No summary generated." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload release-check artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: openclaw-cross-os-release-checks-${{ matrix.artifact_name }}-${{ matrix.suite }}-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}
if-no-files-found: error

View File

@@ -1,572 +0,0 @@
name: OpenClaw Live And E2E Checks (Reusable)
on:
workflow_dispatch:
inputs:
ref:
description: Ref, tag, or SHA to validate
required: true
default: main
type: string
include_repo_e2e:
description: Whether to run pnpm test:e2e plus repo-specific extra E2E lanes
required: false
default: true
type: boolean
include_release_path_suites:
description: Whether to run the Docker release-path suites
required: false
default: true
type: boolean
include_openwebui:
description: Whether to run the Open WebUI Docker smoke
required: false
default: true
type: boolean
include_live_suites:
description: Whether to run live-provider coverage
required: false
default: true
type: boolean
workflow_call:
inputs:
ref:
description: Ref, tag, or SHA to validate
required: true
type: string
include_repo_e2e:
description: Whether to run pnpm test:e2e
required: false
default: false
type: boolean
include_release_path_suites:
description: Whether to run the Docker release-path suites
required: false
default: false
type: boolean
include_openwebui:
description: Whether to run the Open WebUI Docker smoke
required: false
default: true
type: boolean
include_live_suites:
description: Whether to run live-provider coverage
required: false
default: true
type: boolean
secrets:
OPENAI_API_KEY:
required: false
OPENAI_BASE_URL:
required: false
ANTHROPIC_API_KEY:
required: false
ANTHROPIC_API_KEY_OLD:
required: false
ANTHROPIC_API_TOKEN:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
required: false
DASHSCOPE_API_KEY:
required: false
GROQ_API_KEY:
required: false
KIMI_API_KEY:
required: false
MODELSTUDIO_API_KEY:
required: false
MOONSHOT_API_KEY:
required: false
MISTRAL_API_KEY:
required: false
MINIMAX_API_KEY:
required: false
OPENCODE_API_KEY:
required: false
OPENCODE_ZEN_API_KEY:
required: false
OPENCLAW_LIVE_BROWSER_CDP_URL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_MODEL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_VALUE:
required: false
GEMINI_API_KEY:
required: false
GOOGLE_API_KEY:
required: false
OPENROUTER_API_KEY:
required: false
QWEN_API_KEY:
required: false
FAL_KEY:
required: false
RUNWAY_API_KEY:
required: false
DEEPGRAM_API_KEY:
required: false
TOGETHER_API_KEY:
required: false
VYDRA_API_KEY:
required: false
XAI_API_KEY:
required: false
ZAI_API_KEY:
required: false
Z_AI_API_KEY:
required: false
BYTEPLUS_ACCESS_KEY_ID:
required: false
BYTEPLUS_SECRET_ACCESS_KEY:
required: false
CLAUDE_CODE_OAUTH_TOKEN:
required: false
OPENCLAW_CODEX_AUTH_JSON:
required: false
OPENCLAW_CODEX_CONFIG_TOML:
required: false
OPENCLAW_CLAUDE_JSON:
required: false
OPENCLAW_CLAUDE_CREDENTIALS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON:
required: false
OPENCLAW_GEMINI_SETTINGS_JSON:
required: false
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
validate_release_live_cache:
if: inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_LIVE_CACHE_TEST: "1"
OPENCLAW_LIVE_TEST: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Validate live cache credentials
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing OPENAI_API_KEY secret for live-cache validation." >&2
exit 1
fi
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "Missing ANTHROPIC_API_KEY secret for live-cache validation." >&2
exit 1
fi
- name: Verify live prompt cache floors
run: pnpm test:live:cache
validate_repo_e2e:
if: inputs.include_repo_e2e
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Build dist for repo E2E
run: pnpm build
- name: Run repo E2E suite
run: pnpm test:e2e
validate_special_e2e:
if: inputs.include_repo_e2e || inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: openshell-e2e
label: OpenShell repo E2E
command: pnpm test:e2e:openshell
timeout_minutes: 120
requires_repo_e2e: true
requires_live_suites: false
- suite_id: openai-ws-stream-live-e2e
label: OpenAI WebSocket live E2E
command: pnpm test:e2e -- src/agents/openai-ws-stream.e2e.test.ts
timeout_minutes: 90
requires_repo_e2e: false
requires_live_suites: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_E2E_WORKERS: "1"
OPENCLAW_VITEST_MAX_WORKERS: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Build dist for special E2E
if: |
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
(inputs.include_live_suites && matrix.requires_live_suites)
run: pnpm build
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
openai-ws-stream-live-e2e)
echo "OPENAI_LIVE_TEST=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_TEST=1" >> "$GITHUB_ENV"
;;
esac
- name: Validate suite credentials
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
openai-ws-stream-live-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for the OpenAI WebSocket live E2E suite." >&2
exit 1
}
;;
esac
- name: Run ${{ matrix.label }}
if: |
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
(inputs.include_live_suites && matrix.requires_live_suites)
run: ${{ matrix.command }}
validate_docker_e2e:
if: inputs.include_release_path_suites || inputs.include_openwebui
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: docker-onboard
label: Onboarding Docker E2E
command: pnpm test:docker:onboard
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-gateway-network
label: Gateway Network Docker E2E
command: pnpm test:docker:gateway-network
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-mcp-channels
label: MCP Channels Docker E2E
command: pnpm test:docker:mcp-channels
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-plugins
label: Plugins Docker E2E
command: pnpm test:docker:plugins
timeout_minutes: 75
release_path: true
openwebui_only: false
- suite_id: docker-doctor-switch
label: Doctor Install Switch Docker E2E
command: pnpm test:docker:doctor-switch
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-qr
label: QR Import Docker E2E
command: pnpm test:docker:qr
timeout_minutes: 60
release_path: true
openwebui_only: false
- suite_id: docker-install-e2e
label: Installer Docker E2E
command: pnpm test:install:e2e
timeout_minutes: 120
release_path: true
openwebui_only: false
- suite_id: docker-openwebui
label: Open WebUI Docker E2E
command: pnpm test:docker:openwebui
timeout_minutes: 75
release_path: false
openwebui_only: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
echo "OPENCLAW_E2E_MODELS=both" >> "$GITHUB_ENV"
;;
esac
- name: Validate suite credentials
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for installer Docker E2E." >&2
exit 1
}
if [[ -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for installer Docker E2E." >&2
exit 1
fi
;;
docker-openwebui)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for the Open WebUI Docker smoke." >&2
exit 1
}
;;
esac
- name: Run ${{ matrix.label }}
if: |
(inputs.include_release_path_suites && matrix.release_path) ||
(inputs.include_openwebui && matrix.openwebui_only)
run: ${{ matrix.command }}
validate_live_provider_suites:
if: inputs.include_live_suites
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: live-all
label: pnpm test:live
command: pnpm test:live
timeout_minutes: 180
profile_env_only: false
- suite_id: live-models-docker
label: Docker live models
command: pnpm test:docker:live-models
timeout_minutes: 120
profile_env_only: false
- suite_id: live-gateway-docker
label: Docker live gateway
command: pnpm test:docker:live-gateway
timeout_minutes: 120
profile_env_only: false
- suite_id: live-cli-backend-docker
label: Docker live CLI backend
command: pnpm test:docker:live-cli-backend
timeout_minutes: 120
profile_env_only: false
- suite_id: live-acp-bind-docker
label: Docker live ACP bind
command: pnpm test:docker:live-acp-bind
timeout_minutes: 120
profile_env_only: false
- suite_id: live-codex-harness-docker
label: Docker live Codex harness
command: pnpm test:docker:live-codex-harness
timeout_minutes: 120
profile_env_only: false
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
OPENCLAW_LIVE_VIDEO_GENERATION_SKIP_PROVIDERS: ""
OPENCLAW_LIVE_VYDRA_VIDEO: "1"
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
shell: bash
run: |
set -euo pipefail
if [[ "${{ matrix.profile_env_only }}" == "true" ]]; then
echo "OPENCLAW_DOCKER_PROFILE_ENV_ONLY=1" >> "$GITHUB_ENV"
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
;;
live-acp-bind-docker)
echo "OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini" >> "$GITHUB_ENV"
;;
esac
- name: Run ${{ matrix.label }}
run: ${{ matrix.command }}

View File

@@ -1,406 +0,0 @@
name: OpenClaw NPM Release
on:
workflow_dispatch:
inputs:
tag:
description: Release tag to publish, or a full 40-character main commit SHA for validation-only preflight (for example v2026.3.22 or 0123456789abcdef0123456789abcdef01234567)
required: true
type: string
preflight_only:
description: Run validation/build only and skip the gated publish job
required: true
default: false
type: boolean
preflight_run_id:
description: Existing successful preflight workflow run id to promote without rebuilding
required: false
type: string
npm_dist_tag:
description: npm dist-tag to publish to for stable releases
required: true
default: beta
type: choice
options:
- beta
- latest
concurrency:
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', inputs.tag, inputs.npm_dist_tag) || github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
# PLEASE DON'T ADD LONG-RUNNING OR FLAKY CHECKS TO THE npm RELEASE PATH.
# KEEP THIS WORKFLOW SHORT AND DETERMINISTIC OR IT CAN GET STUCK AND JEOPARDIZE THE RELEASE.
# RELEASE-TIME LIVE OR END-TO-END VALIDATION BELONGS IN openclaw-release-checks.yml.
# SECURITY NOTE: TOKEN-BASED npm dist-tag mutation moved to
# openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml
# so this public workflow can stay focused on OIDC publish only.
preflight_openclaw_npm:
if: ${{ inputs.preflight_only }}
runs-on: blacksmith-32vcpu-ubuntu-2404
permissions:
contents: read
steps:
- name: Validate release ref input format
env:
RELEASE_REF: ${{ inputs.tag }}
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Invalid release ref format: ${RELEASE_REF}"
exit 1
fi
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]] && [[ "${PREFLIGHT_ONLY}" != "true" ]]; then
echo "Full commit SHA input is only supported for validation-only preflight runs."
exit 1
fi
if [[ "${RELEASE_REF}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1
fi
- name: Forbid preflight artifact promotion on validation-only runs
if: ${{ inputs.preflight_only && inputs.preflight_run_id != '' }}
run: |
echo "preflight_run_id is only valid for real publish runs."
exit 1
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
- name: Ensure version is not already published
env:
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
run: |
set -euo pipefail
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
if [[ "${PREFLIGHT_ONLY}" == "true" ]]; then
echo "openclaw@${PACKAGE_VERSION} is already published on npm; continuing because preflight_only=true."
exit 0
fi
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
exit 1
fi
echo "Publishing openclaw@${PACKAGE_VERSION}"
- name: Check
env:
OPENCLAW_LOCAL_CHECK: "0"
run: pnpm check
- name: Build
run: pnpm build
- name: Build Control UI
run: pnpm ui:build
- name: Validate release metadata
if: ${{ inputs.preflight_run_id == '' }}
env:
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1"
RELEASE_REF: ${{ inputs.tag }}
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
RELEASE_MAIN_REF: origin/main
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_MAIN_REF
# Fetch the full main ref so merge-base ancestry checks keep working
# for older tagged commits that are still contained in main.
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
MAIN_SHA="$(git rev-parse origin/main)"
if [[ "${RELEASE_SHA}" != "${MAIN_SHA}" ]]; then
echo "Validation-only SHA mode only supports the current origin/main HEAD." >&2
exit 1
fi
RELEASE_TAG="v$(node -p "require('./package.json').version")"
export RELEASE_TAG
echo "Validation-only SHA mode: using synthetic release tag ${RELEASE_TAG} for package metadata checks."
else
RELEASE_TAG="${RELEASE_REF}"
export RELEASE_TAG
fi
pnpm release:openclaw:npm:check
# KEEP THIS LANE LIMITED TO FAST, REPEATABLE RELEASE READINESS CHECKS.
# IF A CHECK CAN TAKE A LONG TIME, NEEDS LIVE CREDENTIALS, OR IS KNOWN TO BE FLAKY,
# IT BELONGS IN openclaw-release-checks.yml INSTEAD OF BLOCKING npm PUBLISH.
- name: Verify release contents
run: pnpm release:check
- name: Pack prepared npm tarball
id: packed_tarball
env:
OPENCLAW_PREPACK_PREPARED: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
npm pack --json 2>&1 | tee "$PACK_OUTPUT"
PACK_PATH="$(node - "$PACK_OUTPUT" <<'NODE'
const fs = require("node:fs");
const input = fs.readFileSync(process.argv[2], "utf8");
function arrayEndFrom(start) {
let depth = 0;
let inString = false;
let escape = false;
for (let i = start; i < input.length; i += 1) {
const char = input[i];
if (inString) {
if (escape) {
escape = false;
} else if (char === "\\") {
escape = true;
} else if (char === "\"") {
inString = false;
}
continue;
}
if (char === "\"") {
inString = true;
} else if (char === "[") {
depth += 1;
} else if (char === "]") {
depth -= 1;
if (depth === 0) {
return i + 1;
}
}
}
return -1;
}
for (let start = input.indexOf("["); start !== -1; start = input.indexOf("[", start + 1)) {
const end = arrayEndFrom(start);
if (end === -1) {
continue;
}
try {
const parsed = JSON.parse(input.slice(start, end));
const first = Array.isArray(parsed) ? parsed[0] : null;
if (first && typeof first.filename === "string" && first.filename) {
process.stdout.write(first.filename);
process.exit(0);
}
} catch {
// Keep scanning; npm lifecycle output can legally precede the JSON.
}
}
console.error("Could not find npm pack --json output with a filename.");
process.exit(1);
NODE
)"
if [[ -z "$PACK_PATH" || ! -f "$PACK_PATH" ]]; then
echo "npm pack did not produce a tarball file." >&2
exit 1
fi
RELEASE_SHA="$(git rev-parse HEAD)"
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
cp "$PACK_PATH" "$ARTIFACT_DIR/"
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"
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
- name: Upload prepared npm publish bundle
uses: actions/upload-artifact@v7
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: ${{ steps.packed_tarball.outputs.dir }}
if-no-files-found: error
validate_publish_request:
if: ${{ !inputs.preflight_only }}
runs-on: blacksmith-32vcpu-ubuntu-2404
permissions:
contents: read
steps:
- name: Require main workflow ref for publish
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "Real publish runs must be dispatched from main. Use preflight_only=true for branch validation."
exit 1
fi
- name: Require preflight artifact promotion on real publish
env:
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
run: |
set -euo pipefail
if [[ -z "${PREFLIGHT_RUN_ID}" ]]; then
echo "Real publish requires preflight_run_id from a successful npm preflight run." >&2
exit 1
fi
publish_openclaw_npm:
# KEEP THE REAL RELEASE/PUBLISH PATH ON A GITHUB-HOSTED RUNNER.
# npm trusted publishing + provenance requires this to stay on ubuntu-latest.
needs: [validate_publish_request]
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
environment: npm-release
permissions:
actions: read
contents: read
id-token: write
steps:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1
fi
- name: Checkout
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure version is not already published
run: |
set -euo pipefail
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
exit 1
fi
echo "Publishing openclaw@${PACKAGE_VERSION}"
- name: Verify preflight run metadata
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
run: |
set -euo pipefail
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["headBranch", "main"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`);'
- name: Download prepared npm tarball
uses: actions/download-artifact@v8
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: preflight-tarball
repository: ${{ github.repository }}
run-id: ${{ inputs.preflight_run_id }}
github-token: ${{ github.token }}
- name: Validate release tag and package metadata
if: ${{ inputs.preflight_run_id == '' }}
env:
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
# Fetch the full main ref so merge-base ancestry checks keep working
# for older tagged commits that are still contained in main.
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
pnpm release:openclaw:npm:check
- name: Verify prepared tarball provenance
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
TAG_FILE="preflight-tarball/release-tag.txt"
SHA_FILE="preflight-tarball/release-sha.txt"
NPM_DIST_TAG_FILE="preflight-tarball/release-npm-dist-tag.txt"
if [[ ! -f "$TAG_FILE" || ! -f "$SHA_FILE" || ! -f "$NPM_DIST_TAG_FILE" ]]; then
echo "Prepared preflight metadata is missing." >&2
ls -la preflight-tarball >&2 || true
exit 1
fi
ARTIFACT_RELEASE_TAG="$(tr -d '\r\n' < "$TAG_FILE")"
ARTIFACT_RELEASE_SHA="$(tr -d '\r\n' < "$SHA_FILE")"
ARTIFACT_RELEASE_NPM_DIST_TAG="$(tr -d '\r\n' < "$NPM_DIST_TAG_FILE")"
if [[ "$ARTIFACT_RELEASE_TAG" != "$RELEASE_TAG" ]]; then
echo "Prepared preflight tag mismatch: expected $RELEASE_TAG, got $ARTIFACT_RELEASE_TAG" >&2
exit 1
fi
if [[ "$ARTIFACT_RELEASE_SHA" != "$EXPECTED_RELEASE_SHA" ]]; then
echo "Prepared preflight SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $ARTIFACT_RELEASE_SHA" >&2
exit 1
fi
if [[ "$ARTIFACT_RELEASE_NPM_DIST_TAG" != "$RELEASE_NPM_DIST_TAG" ]]; then
echo "Prepared preflight npm dist-tag mismatch: expected $RELEASE_NPM_DIST_TAG, got $ARTIFACT_RELEASE_NPM_DIST_TAG" >&2
exit 1
fi
- name: Resolve publish tarball
id: publish_tarball
run: |
set -euo pipefail
TARBALL_PATH="$(find preflight-tarball -type f -name '*.tgz' -print | sort | tail -n 1)"
if [[ -z "$TARBALL_PATH" ]]; then
echo "Prepared preflight tarball not found." >&2
ls -la preflight-tarball >&2 || true
exit 1
fi
echo "path=$TARBALL_PATH" >> "$GITHUB_OUTPUT"
- name: Publish
env:
OPENCLAW_PREPACK_PREPARED: "1"
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
publish_target="${{ steps.publish_tarball.outputs.path }}"
if [[ -n "${publish_target}" ]]; then
publish_target="./${publish_target}"
fi
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"

View File

@@ -1,146 +0,0 @@
name: OpenClaw Release Checks
on:
workflow_dispatch:
inputs:
ref:
description: Existing release tag or current full 40-character main commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567)
required: true
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which cross-OS release lanes to run
required: false
default: both
type: choice
options:
- fresh
- upgrade
- both
concurrency:
group: openclaw-release-checks-${{ inputs.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
resolve_target:
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
contents: read
outputs:
ref: ${{ steps.inputs.outputs.ref }}
sha: ${{ steps.ref.outputs.sha }}
provider: ${{ steps.inputs.outputs.provider }}
mode: ${{ steps.inputs.outputs.mode }}
steps:
- name: Require main workflow ref for release checks
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "Release checks must be dispatched from main so the workflow logic and secrets stay canonical." >&2
exit 1
fi
- name: Validate ref input
env:
RELEASE_REF: ${{ inputs.ref }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Expected an existing release tag or current full 40-character main commit SHA, got: ${RELEASE_REF}" >&2
exit 1
fi
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Resolve checked-out SHA
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate selected ref is on main
env:
RELEASE_REF: ${{ inputs.ref }}
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
MAIN_SHA="$(git rev-parse origin/main)"
if [[ "$(git rev-parse HEAD)" != "${MAIN_SHA}" ]]; then
echo "Commit SHA mode only supports the current origin/main HEAD. Use a release tag for older commits." >&2
exit 1
fi
else
git merge-base --is-ancestor HEAD origin/main
fi
- name: Capture selected inputs
id: inputs
env:
RELEASE_REF_INPUT: ${{ inputs.ref }}
RELEASE_PROVIDER_INPUT: ${{ inputs.provider }}
RELEASE_MODE_INPUT: ${{ inputs.mode }}
run: |
set -euo pipefail
{
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
env:
RELEASE_REF: ${{ inputs.ref }}
RELEASE_SHA: ${{ steps.ref.outputs.sha }}
RELEASE_PROVIDER: ${{ inputs.provider }}
RELEASE_MODE: ${{ inputs.mode }}
run: |
{
echo "## Release checks"
echo
echo "- Requested ref: \`${RELEASE_REF}\`"
echo "- Validated SHA: \`${RELEASE_SHA}\`"
echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`"
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- This run will execute cross-OS release validation plus the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
} >> "$GITHUB_STEP_SUMMARY"
cross_os_release_checks:
needs: [resolve_target]
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
provider: ${{ needs.resolve_target.outputs.provider }}
mode: ${{ needs.resolve_target.outputs.mode }}
secrets: inherit
live_and_e2e_release_checks:
needs: [resolve_target]
permissions:
contents: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
include_repo_e2e: true
include_release_path_suites: true
include_openwebui: true
include_live_suites: true
secrets: inherit

View File

@@ -1,29 +0,0 @@
name: OpenClaw Scheduled Live And E2E Checks
on:
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: openclaw-scheduled-live-checks-${{ github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
live_and_openwebui_checks:
permissions:
contents: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ github.sha }}
include_repo_e2e: true
include_release_path_suites: false
include_openwebui: true
include_live_suites: true
secrets: inherit

View File

@@ -1,102 +0,0 @@
name: Parity gate
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- "extensions/qa-lab/**"
- "extensions/qa-channel/**"
- "extensions/openai/**"
- "qa/scenarios/**"
- "src/agents/**"
- "src/context-engine/**"
- "src/gateway/**"
- "src/media/**"
- ".github/workflows/parity-gate.yml"
permissions:
contents: read
concurrency:
group: parity-gate-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
parity-gate:
name: Run the GPT-5.4 / Opus 4.6 parity gate against the qa-lab mock
if: ${{ github.event.pull_request.draft != true }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 20
env:
# Fence the gate off from any real provider credentials. The qa-lab
# mock server + auth staging (PR N) should be enough to produce a
# meaningful verdict without touching a real API. If any of these
# leak into the job env, fail hard instead of silently running
# against a live provider and burning real budget.
#
# The parity pack has 11 isolated scenario workers. Letting qa suite
# fan out to its default "all scenarios at once" mode on smaller CI
# VMs makes the short strict-agentic scenarios flaky, especially the
# approval-turn followthrough gate that expects a fast post-approval
# read within a 30s agent.wait timeout.
QA_PARITY_CONCURRENCY: "2"
OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
OPENCLAW_LIVE_OPENAI_KEY: ""
OPENCLAW_LIVE_ANTHROPIC_KEY: ""
OPENCLAW_LIVE_GEMINI_KEY: ""
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ""
steps:
- name: Checkout PR
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22.18.0"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run GPT-5.4 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
- name: Run Opus 4.6 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-6 \
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/opus46
- name: Generate parity report
run: |
pnpm openclaw qa parity-report \
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-label openai/gpt-5.4 \
--baseline-label anthropic/claude-opus-4-6 \
--output-dir .artifacts/qa-e2e/parity
- name: Upload parity artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: parity-gate-${{ github.event.pull_request.number || github.sha }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn

View File

@@ -1,276 +0,0 @@
name: Plugin ClawHub Release
on:
workflow_dispatch:
inputs:
publish_scope:
description: Publish the selected plugins or all ClawHub-publishable plugins from the workflow ref
required: true
default: selected
type: choice
options:
- selected
- all-publishable
plugins:
description: Comma-separated plugin package names to publish when publish_scope=selected
required: false
type: string
concurrency:
group: plugin-clawhub-release-${{ github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "4af2bd50a71465683dbf8aa269af764b9d39bdf5"
jobs:
preview_plugins_clawhub:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
ref_sha: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Resolve checked-out ref
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on main
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor HEAD origin/main
- name: Validate publishable plugin metadata
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
run: |
set -euo pipefail
if [[ -n "${PUBLISH_SCOPE}" ]]; then
release_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
release_args+=(--plugins "${RELEASE_PLUGINS}")
fi
pnpm release:plugins:clawhub:check -- "${release_args[@]}"
elif [[ -n "${BASE_REF}" ]]; then
pnpm release:plugins:clawhub:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
else
pnpm release:plugins:clawhub:check
fi
- name: Resolve plugin release plan
id: plan
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
mkdir -p .local
if [[ -n "${PUBLISH_SCOPE}" ]]; then
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
plan_args+=(--plugins "${RELEASE_PLUGINS}")
fi
node --import tsx scripts/plugin-clawhub-release-plan.ts "${plan_args[@]}" > .local/plugin-clawhub-release-plan.json
elif [[ -n "${BASE_REF}" ]]; then
node --import tsx scripts/plugin-clawhub-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-clawhub-release-plan.json
else
node --import tsx scripts/plugin-clawhub-release-plan.ts > .local/plugin-clawhub-release-plan.json
fi
cat .local/plugin-clawhub-release-plan.json
candidate_count="$(jq -r '.candidates | length' .local/plugin-clawhub-release-plan.json)"
skipped_published_count="$(jq -r '.skippedPublished | length' .local/plugin-clawhub-release-plan.json)"
has_candidates="false"
if [[ "${candidate_count}" != "0" ]]; then
has_candidates="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "skipped_published_count=${skipped_published_count}"
echo "has_candidates=${has_candidates}"
echo "matrix=${matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-clawhub-release-plan.json
- name: Fail manual publish when target versions already exist
if: github.event_name == 'workflow_dispatch' && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
run: |
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
exit 1
preview_plugin_pack:
needs: preview_plugins_clawhub
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
install-deps: "false"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 1
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: bun install --frozen-lockfile
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Preview publish command
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, preview_plugin_pack]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
install-deps: "false"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 1
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: bun install --frozen-lockfile
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Ensure version is not already published
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
if [[ "${status}" =~ ^2 ]]; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
exit 1
fi
if [[ "${status}" != "404" ]]; then
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
exit 1
fi
- name: Publish
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"

View File

@@ -1,217 +0,0 @@
name: Plugin NPM Release
on:
push:
branches:
- main
paths:
- ".github/workflows/plugin-npm-release.yml"
- "extensions/**"
- "package.json"
- "scripts/lib/plugin-npm-release.ts"
- "scripts/plugin-npm-publish.sh"
- "scripts/plugin-npm-release-check.ts"
- "scripts/plugin-npm-release-plan.ts"
workflow_dispatch:
inputs:
publish_scope:
description: Publish the selected plugins or all publishable plugins from the ref
required: true
default: selected
type: choice
options:
- selected
- all-publishable
ref:
description: Commit SHA on main to publish from (copy from the preview run)
required: true
type: string
plugins:
description: Comma-separated plugin package names to publish when publish_scope=selected
required: false
type: string
concurrency:
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
jobs:
preview_plugins_npm:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
ref_sha: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Resolve checked-out ref
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on main
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor HEAD origin/main
- name: Validate publishable plugin metadata
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
run: |
set -euo pipefail
if [[ -n "${PUBLISH_SCOPE}" ]]; then
release_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
release_args+=(--plugins "${RELEASE_PLUGINS}")
fi
pnpm release:plugins:npm:check -- "${release_args[@]}"
elif [[ -n "${BASE_REF}" ]]; then
pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
else
pnpm release:plugins:npm:check
fi
- name: Resolve plugin release plan
id: plan
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
run: |
set -euo pipefail
mkdir -p .local
if [[ -n "${PUBLISH_SCOPE}" ]]; then
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
plan_args+=(--plugins "${RELEASE_PLUGINS}")
fi
node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json
elif [[ -n "${BASE_REF}" ]]; then
node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json
else
node --import tsx scripts/plugin-npm-release-plan.ts > .local/plugin-npm-release-plan.json
fi
cat .local/plugin-npm-release-plan.json
candidate_count="$(jq -r '.candidates | length' .local/plugin-npm-release-plan.json)"
has_candidates="false"
if [[ "${candidate_count}" != "0" ]]; then
has_candidates="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "has_candidates=${has_candidates}"
echo "matrix=${matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-npm-release-plan.json
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json
preview_plugin_pack:
needs: preview_plugins_npm
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
install-deps: "false"
- name: Preview publish command
run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"
- name: Preview npm pack contents
working-directory: ${{ matrix.plugin.packageDir }}
run: npm pack --dry-run --json --ignore-scripts
publish_plugins_npm:
needs: [preview_plugins_npm, preview_plugin_pack]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: npm-release
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
install-deps: "false"
- name: Ensure version is not already published
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
run: |
set -euo pipefail
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
exit 1
fi
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"

View File

@@ -1,67 +0,0 @@
name: Sandbox Common Smoke
on:
push:
branches: [main]
paths:
- Dockerfile.sandbox
- Dockerfile.sandbox-common
- scripts/sandbox-common-setup.sh
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
paths:
- Dockerfile.sandbox
- Dockerfile.sandbox-common
- scripts/sandbox-common-setup.sh
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
sandbox-common-smoke:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: false
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build minimal sandbox base (USER sandbox)
shell: bash
run: |
set -euo pipefail
docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF'
FROM debian:bookworm-slim
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox
WORKDIR /home/sandbox
EOF
- name: Build sandbox-common image (root for installs, sandbox at runtime)
shell: bash
run: |
set -euo pipefail
BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \
TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \
PACKAGES="ca-certificates" \
INSTALL_PNPM=0 \
INSTALL_BUN=0 \
INSTALL_BREW=0 \
FINAL_USER=sandbox \
scripts/sandbox-common-setup.sh
u="$(docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
test "$u" = "sandbox"

View File

@@ -1,217 +0,0 @@
name: Stale
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions: {}
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v2
id: app-token-fallback
continue-on-error: true
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests (primary)
id: stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
exempt-all-assignees: true
remove-stale-when-updated: true
stale-issue-message: |
This issue has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
stale-pr-message: |
This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Check stale state cache
id: stale-state
if: always()
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token-fallback.outputs.token || steps.app-token.outputs.token }}
script: |
const cacheKey = "_state";
const { owner, repo } = context.repo;
try {
const { data } = await github.rest.actions.getActionsCacheList({
owner,
repo,
key: cacheKey,
});
const caches = data.actions_caches ?? [];
const hasState = caches.some(cache => cache.key === cacheKey);
core.setOutput("has_state", hasState ? "true" : "false");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.warning(`Failed to check stale state cache: ${message}`);
core.setOutput("has_state", "false");
}
- name: Mark stale issues and pull requests (fallback)
if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
exempt-all-assignees: true
remove-stale-when-updated: true
stale-issue-message: |
This issue has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
stale-pr-message: |
This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:
permissions:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Lock closed issues after 48h of no comments
uses: actions/github-script@v8
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const lockAfterHours = 48;
const lockAfterMs = lockAfterHours * 60 * 60 * 1000;
const perPage = 100;
const cutoffMs = Date.now() - lockAfterMs;
const { owner, repo } = context.repo;
let locked = 0;
let inspected = 0;
let page = 1;
while (true) {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "closed",
sort: "updated",
direction: "desc",
per_page: perPage,
page,
});
if (issues.length === 0) {
break;
}
for (const issue of issues) {
if (issue.pull_request) {
continue;
}
if (issue.locked) {
continue;
}
if (!issue.closed_at) {
continue;
}
inspected += 1;
const closedAtMs = Date.parse(issue.closed_at);
if (!Number.isFinite(closedAtMs)) {
continue;
}
if (closedAtMs > cutoffMs) {
continue;
}
let lastCommentMs = 0;
if (issue.comments > 0) {
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issue.number,
per_page: 1,
page: 1,
sort: "created",
direction: "desc",
});
if (comments.length > 0) {
lastCommentMs = Date.parse(comments[0].created_at);
}
}
const lastActivityMs = Math.max(closedAtMs, lastCommentMs || 0);
if (lastActivityMs > cutoffMs) {
continue;
}
await github.rest.issues.lock({
owner,
repo,
issue_number: issue.number,
lock_reason: "resolved",
});
locked += 1;
}
page += 1;
}
core.info(`Inspected ${inspected} closed issues; locked ${locked}.`);

View File

@@ -3,26 +3,13 @@ name: Workflow Sanity
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
no-tabs:
if: github.event_name != 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fail on tabs in workflow files
run: |
@@ -48,54 +35,3 @@ jobs:
print(f"- {path}")
sys.exit(1)
PY
actionlint:
if: github.event_name != 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install actionlint
shell: bash
run: |
set -euo pipefail
ACTIONLINT_VERSION="1.7.11"
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
base_url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}"
# GitHub release downloads occasionally return transient 5xx responses.
# Retry all curl errors here so workflow-sanity does not fail closed on
# a one-off release edge outage.
curl --retry 5 --retry-delay 2 --retry-all-errors -sSfL -o "${archive}" "${base_url}/${archive}"
curl --retry 5 --retry-delay 2 --retry-all-errors -sSfL -o checksums.txt "${base_url}/actionlint_${ACTIONLINT_VERSION}_checksums.txt"
grep " ${archive}\$" checksums.txt | sha256sum -c -
tar -xzf "${archive}" actionlint
sudo install -m 0755 actionlint /usr/local/bin/actionlint
- name: Lint workflows
run: actionlint
- name: Disallow direct inputs interpolation in composite run blocks
run: python3 scripts/check-composite-action-input-interpolation.py
- name: Disallow tracked merge conflict markers
run: node scripts/check-no-conflict-markers.mjs
generated-doc-baselines:
if: github.event_name == 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Check config docs drift statefile
run: pnpm config:docs:check
- name: Check plugin SDK API baseline drift
run: pnpm plugin-sdk:api:check

93
.gitignore vendored
View File

@@ -1,18 +1,13 @@
node_modules
**/node_modules/
.env
docker-compose.override.yml
docker-compose.extra.yml
dist
dist-runtime/
*.bun-build
pnpm-lock.yaml
bun.lock
bun.lockb
coverage
__openclaw_vitest__/
__pycache__/
*.pyc
.tsbuildinfo
.pnpm-store
.worktrees/
.DS_Store
@@ -20,39 +15,22 @@ __pycache__/
ui/src/ui/__screenshots__/
ui/playwright-report/
ui/test-results/
packages/dashboard-next/.next/
packages/dashboard-next/out/
# Mise configuration files
mise.toml
# Android build artifacts
apps/android/.gradle/
apps/android/app/build/
apps/android/.cxx/
apps/android/.kotlin/
apps/android/benchmark/results/
# Bun build artifacts
*.bun-build
apps/macos/.build/
apps/shared/MoltbotKit/.build/
apps/shared/OpenClawKit/.build/
apps/shared/OpenClawKit/Package.resolved
apps/shared/ClawdbotKit/.build/
**/ModuleCache/
bin/
bin/clawdbot-mac
bin/docs-list
apps/macos/.build-local/
apps/macos/.swiftpm/
apps/shared/MoltbotKit/.swiftpm/
apps/shared/OpenClawKit/.swiftpm/
apps/shared/ClawdbotKit/.swiftpm/
Core/
apps/ios/*.xcodeproj/
apps/ios/*.xcworkspace/
apps/ios/.swiftpm/
apps/ios/.derivedData/
apps/ios/.local-signing.xcconfig
vendor/
apps/ios/Clawdbot.xcodeproj/
apps/ios/Clawdbot.xcodeproj/**
@@ -62,8 +40,6 @@ apps/ios/*.xcfilelist
# Vendor build artifacts
vendor/a2ui/renderers/lit/dist/
src/canvas-host/a2ui/*.bundle.js
src/canvas-host/a2ui/*.map
.bundle.hash
# fastlane (iOS)
@@ -74,6 +50,7 @@ apps/ios/fastlane/screenshots/
apps/ios/fastlane/test_output/
apps/ios/fastlane/logs/
apps/ios/fastlane/.env
apps/ios/fastlane/report.xml
# fastlane build artifacts (local)
apps/ios/*.ipa
@@ -81,72 +58,14 @@ apps/ios/*.dSYM.zip
# provisioning profiles (local)
apps/ios/*.mobileprovision
.env
# Local untracked files
.local/
docs/.local/
docs/internal/
tmp/
.vscode/
IDENTITY.md
USER.md
.tgz
.idea
# local tooling
.serena/
# Agent credentials and memory (NEVER COMMIT)
/memory/
.agent/*.json
!.agent/workflows/
/local/
package-lock.json
.claude/
.agent/
skills-lock.json
# Local iOS signing overrides
apps/ios/LocalSigning.xcconfig
# Xcode build directories (xcodebuild output)
apps/ios/build/
apps/shared/OpenClawKit/build/
Swabble/build/
# Generated protocol schema (produced via pnpm protocol:gen)
dist/protocol.schema.json
.ant-colony/
# Eclipse
**/.project
**/.classpath
**/.settings/
**/.gradle/
# Synthing
**/.stfolder/
.dev-state
docs/superpowers/plans/2026-03-10-collapsed-side-nav.md
docs/superpowers/specs/2026-03-10-collapsed-side-nav-design.md
.gitignore
test/config-form.analyze.telegram.test.ts
ui/src/ui/theme-variants.browser.test.ts
ui/src/ui/__screenshots__
ui/src/ui/views/__screenshots__
ui/.vitest-attachments
docs/superpowers
# Generated docs baseline artifacts (locally generated, only hashes tracked)
docs/.generated/*.json
docs/.generated/*.jsonl
# Deprecated changelog fragment workflow
changelog/fragments/
# Local scratch workspace
.tmp/
.artifacts/
test/fixtures/openclaw-vitest-unit-report.json
analysis/
.artifacts/qa-e2e/
extensions/qa-lab/web/dist/

View File

@@ -1,16 +0,0 @@
{
"gitignore": true,
"noSymlinks": true,
"ignore": [
"**/node_modules/**",
"**/dist/**",
"dist/**",
"**/.git/**",
"**/coverage/**",
"**/build/**",
"**/.build/**",
"**/.artifacts/**",
"docs/zh-CN/**",
"**/CHANGELOG.md"
]
}

View File

@@ -1,13 +0,0 @@
# Canonical contributor identity mappings for cherry-picked commits.
bmendonca3 <208517100+bmendonca3@users.noreply.github.com> <brianmendonca@Brians-MacBook-Air.local>
hcl <7755017+hclsys@users.noreply.github.com> <chenglunhu@gmail.com>
Glucksberg <80581902+Glucksberg@users.noreply.github.com> <markuscontasul@gmail.com>
JackyWay <53031570+JackyWay@users.noreply.github.com> <jackybbc@gmail.com>
Marcus Castro <7562095+mcaxtr@users.noreply.github.com> <mcaxtr@gmail.com>
Marc Gratch <2238658+mgratch@users.noreply.github.com> <me@marcgratch.com>
Peter Machona <7957943+chilu18@users.noreply.github.com> <chilu.machona@icloud.com>
Ben Marvell <92585+easternbloc@users.noreply.github.com> <ben@marvell.consulting>
zerone0x <39543393+zerone0x@users.noreply.github.com> <hi@trine.dev>
Marco Di Dionisio <3519682+marcodd23@users.noreply.github.com> <m.didionisio23@gmail.com>
mujiannan <46643837+mujiannan@users.noreply.github.com> <shennan@mujiannan.com>
Santhanakrishnan <239082898+bitfoundry-ai@users.noreply.github.com> <noreply@anthropic.com>

View File

@@ -1,55 +0,0 @@
{
"globs": ["docs/**/*.md", "docs/**/*.mdx", "README.md"],
"ignores": ["docs/zh-CN/**", "docs/.i18n/**", "docs/reference/templates/**", "**/.local/**"],
"config": {
"default": true,
"MD013": false,
"MD025": false,
"MD029": false,
"MD033": {
"allowed_elements": [
"Note",
"Info",
"Tip",
"Warning",
"Card",
"CardGroup",
"Columns",
"Steps",
"Step",
"Tabs",
"Tab",
"Accordion",
"AccordionGroup",
"CodeGroup",
"Frame",
"Callout",
"ParamField",
"ResponseField",
"RequestExample",
"ResponseExample",
"img",
"a",
"br",
"table",
"tr",
"td",
"details",
"summary",
"p",
"strong",
"picture",
"source",
"Tooltip",
"Check",
],
},
"MD036": false,
"MD040": false,
"MD041": false,
"MD046": false,
},
}

View File

@@ -1,3 +0,0 @@
**/node_modules/
**/.runtime-deps-*/
docs/.generated/

5
.npmrc
View File

@@ -1,4 +1 @@
# pnpm build-script allowlist lives in package.json -> pnpm.onlyBuiltDependencies.
# TS 7 native-preview fails to resolve packages reliably from pnpm's isolated linker.
# Keep the workspace on a hoisted layout so pnpm check/build stay stable.
node-linker=hoisted
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty,@lydell/node-pty,@matrix-org/matrix-sdk-crypto-nodejs

View File

@@ -1,27 +1,5 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"sortImports": {
"newlinesBetween": false,
},
"sortPackageJson": {
"sortScripts": true,
},
"tabWidth": 2,
"useTabs": false,
"ignorePatterns": [
"apps/",
"assets/",
"CLAUDE.md",
"docker-compose.yml",
"dist/",
"docs/_layouts/",
"node_modules/",
"patches/",
"pnpm-lock.yaml/",
"src/gateway/server-methods/CLAUDE.md",
"src/auto-reply/reply/export-html/",
"src/canvas-host/a2ui/a2ui.bundle.js",
"Swabble/",
"vendor/",
],
"indentWidth": 2,
"printWidth": 100
}

View File

@@ -1,67 +1,12 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["unicorn", "typescript", "oxc"],
"categories": {
"correctness": "error",
"perf": "error",
"suspicious": "error"
},
"rules": {
"curly": "error",
"eslint-plugin-unicorn/prefer-array-find": "error",
"eslint/no-await-in-loop": "off",
"eslint/no-new": "error",
"eslint/no-shadow": "off",
"eslint/no-unmodified-loop-condition": "error",
"eslint-plugin-unicorn/prefer-set-size": "error",
"oxc/no-accumulating-spread": "error",
"oxc/no-async-endpoint-handlers": "off",
"oxc/no-map-spread": "off",
"typescript/consistent-return": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extraneous-class": "error",
"typescript/no-unnecessary-type-conversion": "error",
"typescript/no-unsafe-type-assertion": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/prefer-set-size": "error",
"unicorn/require-post-message-target-origin": "error"
},
"ignorePatterns": [
"assets/",
"dist/",
"dist-runtime/",
"docs/_layouts/",
"node_modules/",
"patches/",
"pnpm-lock.yaml",
"skills/",
"src/auto-reply/reply/export-html/template.js",
"src/canvas-host/a2ui/a2ui.bundle.js",
"Swabble/",
"vendor/",
"**/.cache/**",
"**/build/**",
"**/coverage/**",
"**/dist/**",
"**/dist-runtime/**",
"**/node_modules/**"
"plugins": [
"unicorn",
"typescript",
"oxc"
],
"overrides": [
{
"files": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.e2e.test.ts",
"**/*.live.test.ts",
"**/*test-harness.ts",
"**/*test-helpers.ts",
"**/*test-support.ts"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/unbound-method": "off",
"eslint/no-unsafe-optional-chaining": "off"
}
}
]
"categories": {
"correctness": "error"
},
"ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js"]
}

View File

@@ -1,117 +0,0 @@
/**
* Diff Extension
*
* /diff command shows modified/deleted/new files from git status and opens
* the selected file in VS Code's diff view.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { showPagedSelectList } from "./ui/paged-select";
interface FileInfo {
status: string;
statusLabel: string;
file: string;
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("diff", {
description: "Show git changes and open in VS Code diff view",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("No UI available", "error");
return;
}
// Get changed files from git status
const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
if (result.code !== 0) {
ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
return;
}
if (!result.stdout || !result.stdout.trim()) {
ctx.ui.notify("No changes in working tree", "info");
return;
}
// Parse git status output
// Format: XY filename (where XY is two-letter status, then space, then filename)
const lines = result.stdout.split("\n");
const files: FileInfo[] = [];
for (const line of lines) {
if (line.length < 4) {
continue;
} // Need at least "XY f"
const status = line.slice(0, 2);
const file = line.slice(2).trimStart();
// Translate status codes to short labels
let statusLabel: string;
if (status.includes("M")) {
statusLabel = "M";
} else if (status.includes("A")) {
statusLabel = "A";
} else if (status.includes("D")) {
statusLabel = "D";
} else if (status.includes("?")) {
statusLabel = "?";
} else if (status.includes("R")) {
statusLabel = "R";
} else if (status.includes("C")) {
statusLabel = "C";
} else {
statusLabel = status.trim() || "~";
}
files.push({ status: statusLabel, statusLabel, file });
}
if (files.length === 0) {
ctx.ui.notify("No changes found", "info");
return;
}
const openSelected = async (fileInfo: FileInfo): Promise<void> => {
try {
// Open in VS Code diff view.
// For untracked files, git difftool won't work, so fall back to just opening the file.
if (fileInfo.status === "?") {
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
return;
}
const diffResult = await pi.exec(
"git",
["difftool", "-y", "--tool=vscode", fileInfo.file],
{
cwd: ctx.cwd,
},
);
if (diffResult.code !== 0) {
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
}
};
const items = files.map((file) => ({
value: file,
label: `${file.status} ${file.file}`,
}));
await showPagedSelectList({
ctx,
title: " Select file to diff",
items,
onSelect: (item) => {
void openSelected(item.value as FileInfo);
},
});
},
});
}

View File

@@ -1,134 +0,0 @@
/**
* Files Extension
*
* /files command lists all files the model has read/written/edited in the active session branch,
* coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { showPagedSelectList } from "./ui/paged-select";
interface FileEntry {
path: string;
operations: Set<"read" | "write" | "edit">;
lastTimestamp: number;
}
type FileToolName = "read" | "write" | "edit";
export default function (pi: ExtensionAPI) {
pi.registerCommand("files", {
description: "Show files read/written/edited in this session",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("No UI available", "error");
return;
}
// Get the current branch (path from leaf to root)
const branch = ctx.sessionManager.getBranch();
// First pass: collect tool calls (id -> {path, name}) from assistant messages
const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
for (const entry of branch) {
if (entry.type !== "message") {
continue;
}
const msg = entry.message;
if (msg.role === "assistant" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "toolCall") {
const name = block.name;
if (name === "read" || name === "write" || name === "edit") {
const path = block.arguments?.path;
if (path && typeof path === "string") {
toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
}
}
}
}
}
}
// Second pass: match tool results to get the actual execution timestamp
const fileMap = new Map<string, FileEntry>();
for (const entry of branch) {
if (entry.type !== "message") {
continue;
}
const msg = entry.message;
if (msg.role === "toolResult") {
const toolCall = toolCalls.get(msg.toolCallId);
if (!toolCall) {
continue;
}
const { path, name } = toolCall;
const timestamp = msg.timestamp;
const existing = fileMap.get(path);
if (existing) {
existing.operations.add(name);
if (timestamp > existing.lastTimestamp) {
existing.lastTimestamp = timestamp;
}
} else {
fileMap.set(path, {
path,
operations: new Set([name]),
lastTimestamp: timestamp,
});
}
}
}
if (fileMap.size === 0) {
ctx.ui.notify("No files read/written/edited in this session", "info");
return;
}
// Sort by most recent first
const files = Array.from(fileMap.values()).toSorted(
(a, b) => b.lastTimestamp - a.lastTimestamp,
);
const openSelected = async (file: FileEntry): Promise<void> => {
try {
await pi.exec("code", ["-g", file.path], { cwd: ctx.cwd });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
}
};
const items = files.map((file) => {
const ops: string[] = [];
if (file.operations.has("read")) {
ops.push("R");
}
if (file.operations.has("write")) {
ops.push("W");
}
if (file.operations.has("edit")) {
ops.push("E");
}
return {
value: file,
label: `${ops.join("")} ${file.path}`,
};
});
await showPagedSelectList({
ctx,
title: " Select file to open",
items,
onSelect: (item) => {
void openSelected(item.value as FileEntry);
},
});
},
});
}

View File

@@ -1,190 +0,0 @@
import {
DynamicBorder,
type ExtensionAPI,
type ExtensionContext,
} from "@mariozechner/pi-coding-agent";
import { Container, Text } from "@mariozechner/pi-tui";
const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
type PromptMatch = {
kind: "pr" | "issue";
url: string;
};
type GhMetadata = {
title?: string;
author?: {
login?: string;
name?: string | null;
};
};
function extractPromptMatch(prompt: string): PromptMatch | undefined {
const prMatch = prompt.match(PR_PROMPT_PATTERN);
if (prMatch?.[1]) {
return { kind: "pr", url: prMatch[1].trim() };
}
const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN);
if (issueMatch?.[1]) {
return { kind: "issue", url: issueMatch[1].trim() };
}
return undefined;
}
async function fetchGhMetadata(
pi: ExtensionAPI,
kind: PromptMatch["kind"],
url: string,
): Promise<GhMetadata | undefined> {
const args =
kind === "pr"
? ["pr", "view", url, "--json", "title,author"]
: ["issue", "view", url, "--json", "title,author"];
try {
const result = await pi.exec("gh", args);
if (result.code !== 0 || !result.stdout) {
return undefined;
}
return JSON.parse(result.stdout) as GhMetadata;
} catch {
return undefined;
}
}
function formatAuthor(author?: GhMetadata["author"]): string | undefined {
if (!author) {
return undefined;
}
const name = author.name?.trim();
const login = author.login?.trim();
if (name && login) {
return `${name} (@${login})`;
}
if (login) {
return `@${login}`;
}
if (name) {
return name;
}
return undefined;
}
export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
const setWidget = (
ctx: ExtensionContext,
match: PromptMatch,
title?: string,
authorText?: string,
) => {
ctx.ui.setWidget("prompt-url", (_tui, thm) => {
const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url);
const authorLine = authorText ? thm.fg("muted", authorText) : undefined;
const urlLine = thm.fg("dim", match.url);
const lines = [titleText];
if (authorLine) {
lines.push(authorLine);
}
lines.push(urlLine);
const container = new Container();
container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s)));
container.addChild(new Text(lines.join("\n"), 1, 0));
return container;
});
};
const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => {
const label = match.kind === "pr" ? "PR" : "Issue";
const trimmedTitle = title?.trim();
const fallbackName = `${label}: ${match.url}`;
const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName;
const currentName = pi.getSessionName()?.trim();
if (!currentName) {
pi.setSessionName(desiredName);
return;
}
if (currentName === match.url || currentName === fallbackName) {
pi.setSessionName(desiredName);
}
};
const renderPromptMatch = (ctx: ExtensionContext, match: PromptMatch) => {
setWidget(ctx, match);
applySessionName(ctx, match);
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
const title = meta?.title?.trim();
const authorText = formatAuthor(meta?.author);
setWidget(ctx, match, title, authorText);
applySessionName(ctx, match, title);
});
};
pi.on("before_agent_start", async (event, ctx) => {
if (!ctx.hasUI) {
return;
}
const match = extractPromptMatch(event.prompt);
if (!match) {
return;
}
renderPromptMatch(ctx, match);
});
pi.on("session_switch", async (_event, ctx) => {
rebuildFromSession(ctx);
});
const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
if (!content) {
return "";
}
if (typeof content === "string") {
return content;
}
return (
content
.filter((block): block is { type: "text"; text: string } => block.type === "text")
.map((block) => block.text)
.join("\n") ?? ""
);
};
const rebuildFromSession = (ctx: ExtensionContext) => {
if (!ctx.hasUI) {
return;
}
const entries = ctx.sessionManager.getEntries();
const lastMatch = [...entries].toReversed().find((entry) => {
if (entry.type !== "message" || entry.message.role !== "user") {
return false;
}
const text = getUserText(entry.message.content);
return !!extractPromptMatch(text);
});
const content =
lastMatch?.type === "message" && lastMatch.message.role === "user"
? lastMatch.message.content
: undefined;
const text = getUserText(content);
const match = text ? extractPromptMatch(text) : undefined;
if (!match) {
ctx.ui.setWidget("prompt-url", undefined);
return;
}
renderPromptMatch(ctx, match);
};
pi.on("session_start", async (_event, ctx) => {
rebuildFromSession(ctx);
});
}

View File

@@ -1,26 +0,0 @@
/**
* Redraws Extension
*
* Exposes /tui to show TUI redraw stats.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
export default function (pi: ExtensionAPI) {
pi.registerCommand("tui", {
description: "Show TUI stats",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
return;
}
let redraws = 0;
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
redraws = tui.fullRedraws;
done(undefined);
return new Text("", 0, 0);
});
ctx.ui.notify(`TUI full redraws: ${redraws}`, "info");
},
});
}

View File

@@ -1,82 +0,0 @@
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import {
Container,
Key,
matchesKey,
type SelectItem,
SelectList,
Text,
} from "@mariozechner/pi-tui";
type CustomUiContext = {
ui: {
custom: <T>(
render: (
tui: { requestRender: () => void },
theme: {
fg: (tone: string, text: string) => string;
bold: (text: string) => string;
},
kb: unknown,
done: () => void,
) => {
render: (width: number) => string;
invalidate: () => void;
handleInput: (data: string) => void;
},
) => Promise<T>;
};
};
export async function showPagedSelectList(params: {
ctx: CustomUiContext;
title: string;
items: SelectItem[];
onSelect: (item: SelectItem) => void;
}): Promise<void> {
await params.ctx.ui.custom<void>((tui, theme, _kb, done) => {
const container = new Container();
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
container.addChild(new Text(theme.fg("accent", theme.bold(params.title)), 0, 0));
const visibleRows = Math.min(params.items.length, 15);
let currentIndex = 0;
const selectList = new SelectList(params.items, visibleRows, {
selectedPrefix: (text) => theme.fg("accent", text),
selectedText: (text) => text,
description: (text) => theme.fg("muted", text),
scrollInfo: (text) => theme.fg("dim", text),
noMatch: (text) => theme.fg("warning", text),
});
selectList.onSelect = (item) => params.onSelect(item);
selectList.onCancel = () => done();
selectList.onSelectionChange = (item) => {
currentIndex = params.items.indexOf(item);
};
container.addChild(selectList);
container.addChild(
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
);
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
return {
render: (width) => container.render(width),
invalidate: () => container.invalidate(),
handleInput: (data) => {
if (matchesKey(data, Key.left)) {
currentIndex = Math.max(0, currentIndex - visibleRows);
selectList.setSelectedIndex(currentIndex);
} else if (matchesKey(data, Key.right)) {
currentIndex = Math.min(params.items.length - 1, currentIndex + visibleRows);
selectList.setSelectedIndex(currentIndex);
} else {
selectList.handleInput(data);
}
tui.requestRender();
},
};
});
}

2
.pi/git/.gitignore vendored
View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -1,58 +0,0 @@
---
description: Audit changelog entries before release
---
Audit changelog entries for all commits since the last release.
## Process
1. **Find the last release tag:**
```bash
git tag --sort=-version:refname | head -1
```
2. **List all commits since that tag:**
```bash
git log <tag>..HEAD --oneline
```
3. **Read each package's [Unreleased] section:**
- packages/ai/CHANGELOG.md
- packages/tui/CHANGELOG.md
- packages/coding-agent/CHANGELOG.md
4. **For each commit, check:**
- Skip: changelog updates, doc-only changes, release housekeeping
- Determine which package(s) the commit affects (use `git show <hash> --stat`)
- Verify a changelog entry exists in the affected package(s)
- For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))`
5. **Cross-package duplication rule:**
Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them.
6. **Add New Features section after changelog fixes:**
- Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`.
- Propose the top new features to the user for confirmation before writing them.
- Link to relevant docs and sections whenever possible.
7. **Report:**
- List commits with missing entries
- List entries that need cross-package duplication
- Add any missing entries directly
## Changelog Format Reference
Sections (in order):
- `### Breaking Changes` - API changes requiring migration
- `### Added` - New features
- `### Changed` - Changes to existing functionality
- `### Fixed` - Bug fixes
- `### Removed` - Removed features
Attribution:
- Internal: `Fixed foo ([#123](https://github.com/badlogic/pi-mono/issues/123))`
- External: `Added bar ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@user](https://github.com/user))`

View File

@@ -1,22 +0,0 @@
---
description: Analyze GitHub issues (bugs or feature requests)
---
Analyze GitHub issue(s): $ARGUMENTS
For each issue:
1. Read the issue in full, including all comments and linked issues/PRs.
2. **For bugs**:
- Ignore any root cause analysis in the issue (likely wrong)
- Read all related code files in full (no truncation)
- Trace the code path and identify the actual root cause
- Propose a fix
3. **For feature requests**:
- Read all related code files in full (no truncation)
- Propose the most concise implementation approach
- List affected files and changes needed
Do NOT implement unless explicitly asked. Analyze and propose only.

View File

@@ -1,73 +0,0 @@
---
description: Land a PR (merge with proper workflow)
---
Input
- PR: $1 <number|url>
- If missing: use the most recent PR mentioned in the conversation.
- If ambiguous: ask.
Do (end-to-end)
Goal: PR must end in GitHub state = MERGED (never CLOSED). Prefer `gh pr merge --squash`; use `--rebase` only when preserving commit history is required.
1. Assign PR to self:
- `gh pr edit <PR> --add-assignee @me`
2. Repo clean: `git status`.
3. Identify PR meta (author + head branch):
```sh
gh pr view <PR> --json number,title,author,headRefName,baseRefName,headRepository --jq '{number,title,author:.author.login,head:.headRefName,base:.baseRefName,headRepo:.headRepository.nameWithOwner}'
contrib=$(gh pr view <PR> --json author --jq .author.login)
head=$(gh pr view <PR> --json headRefName --jq .headRefName)
head_repo_url=$(gh pr view <PR> --json headRepository --jq .headRepository.url)
```
4. Fast-forward base:
- `git checkout main`
- `git pull --ff-only`
5. Create temp base branch from main:
- `git checkout -b temp/landpr-<ts-or-pr>`
6. Check out PR branch locally:
- `gh pr checkout <PR>`
7. Rebase PR branch onto temp base:
- `git rebase temp/landpr-<ts-or-pr>`
- Fix conflicts; keep history tidy.
8. Fix + tests + changelog:
- Implement fixes + add/adjust tests
- Update `CHANGELOG.md` and mention `#<PR>` + `@$contrib`
9. Decide merge strategy:
- Squash (preferred): use when we want a single clean commit
- Rebase: use only when we explicitly want to preserve commit history
- If unclear, ask
10. Full gate (BEFORE commit):
- `pnpm lint && pnpm build && pnpm test`
11. Commit via committer (final merge commit only includes PR # + thanks):
- For the final merge-ready commit: `committer "fix: <summary> (#<PR>) (thanks @$contrib)" CHANGELOG.md <changed files>`
- If you need intermediate fix commits before the final merge commit, keep those messages concise and **omit** PR number/thanks.
- `land_sha=$(git rev-parse HEAD)`
12. Push updated PR branch (rebase => usually needs force):
```sh
git remote add prhead "$head_repo_url.git" 2>/dev/null || git remote set-url prhead "$head_repo_url.git"
git push --force-with-lease prhead HEAD:$head
```
13. Merge PR (must show MERGED on GitHub):
- Squash (preferred): `gh pr merge <PR> --squash`
- Rebase (history-preserving fallback): `gh pr merge <PR> --rebase`
- Never `gh pr close` (closing is wrong)
14. Sync main:
- `git checkout main`
- `git pull --ff-only`
15. Comment on PR with what we did + SHAs + thanks:
```sh
merge_sha=$(gh pr view <PR> --json mergeCommit --jq '.mergeCommit.oid')
gh pr comment <PR> --body "Landed via temp rebase onto main.\n\n- Gate: pnpm lint && pnpm build && pnpm test\n- Land commit: $land_sha\n- Merge commit: $merge_sha\n\nThanks @$contrib!"
```
16. Verify PR state == MERGED:
- `gh pr view <PR> --json state --jq .state`
17. Delete temp branch:
- `git branch -D temp/landpr-<ts-or-pr>`

View File

@@ -1,134 +0,0 @@
---
description: Review a PR thoroughly without merging
---
Input
- PR: $1 <number|url>
- If missing: use the most recent PR mentioned in the conversation.
- If ambiguous: ask.
Do (review-only)
Goal: produce a thorough review and a clear recommendation (READY FOR /landpr vs NEEDS WORK vs INVALID CLAIM). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
0. Truthfulness + reality gate (required for bug-fix claims)
- Do not trust the issue text or PR summary by default; verify in code and evidence.
- If the PR claims to fix a bug linked to an issue, confirm the bug exists now (repro steps, logs, failing test, or clear code-path proof).
- Prove root cause with exact location (`path/file.ts:line` + explanation of why behavior is wrong).
- Verify fix targets the same code path as the root cause.
- Require a regression test when feasible (fails before fix, passes after fix). If not feasible, require explicit justification + manual verification evidence.
- Hallucination/BS red flags (treat as BLOCKER until disproven):
- claimed behavior not present in repo,
- issue/PR says "fixes #..." but changed files do not touch implicated path,
- only docs/comments changed for a runtime bug claim,
- vague AI-generated rationale without concrete evidence.
1. Identify PR meta + context
```sh
gh pr view <PR> --json number,title,state,isDraft,author,baseRefName,headRefName,headRepository,url,body,labels,assignees,reviewRequests,files,additions,deletions --jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.files|length}'
```
2. Read the PR description carefully
- Summarize the stated goal, scope, and any "why now?" rationale.
- Call out any missing context: motivation, alternatives considered, rollout/compat notes, risk.
3. Read the diff thoroughly (prefer full diff)
```sh
gh pr diff <PR>
# If you need more surrounding context for files:
gh pr checkout <PR> # optional; still review-only
git show --stat
```
4. Validate the change is needed / valuable
- What user/customer/dev pain does this solve?
- Is this change the smallest reasonable fix?
- Are we introducing complexity for marginal benefit?
- Are we changing behavior/contract in a way that needs docs or a release note?
5. Evaluate implementation quality + optimality
- Correctness: edge cases, error handling, null/undefined, concurrency, ordering.
- Design: is the abstraction/architecture appropriate or over/under-engineered?
- Performance: hot paths, allocations, queries, network, N+1s, caching.
- Security/privacy: authz/authn, input validation, secrets, logging PII.
- Backwards compatibility: public APIs, config, migrations.
- Style consistency: formatting, naming, patterns used elsewhere.
6. Tests & verification
- Identify what's covered by tests (unit/integration/e2e).
- Are there regression tests for the bug fixed / scenario added?
- Missing tests? Call out exact cases that should be added.
- If tests are present, do they actually assert the important behavior (not just snapshots / happy path)?
7. Follow-up refactors / cleanup suggestions
- Any code that should be simplified before merge?
- Any TODOs that should be tickets vs addressed now?
- Any deprecations, docs, types, or lint rules we should adjust?
8. Key questions to answer explicitly
- Is the core claim substantiated by evidence, or is it likely invalid/hallucinated?
- Can we fix everything ourselves in a follow-up, or does the contributor need to update this PR?
- Any blocking concerns (must-fix before merge)?
- Is this PR ready to land, or does it need work?
9. Output (structured)
Produce a review with these sections:
A) TL;DR recommendation
- One of: READY FOR /landpr | NEEDS WORK | INVALID CLAIM (issue/bug not substantiated) | NEEDS DISCUSSION
- 13 sentence rationale.
B) Claim verification matrix (required)
- Fill this table:
| Field | Evidence |
| ----------------------------------------------- | -------- |
| Claimed problem | ... |
| Evidence observed (repro/log/test/code) | ... |
| Root cause location (`path:line`) | ... |
| Why this fix addresses that root cause | ... |
| Regression coverage (test name or manual proof) | ... |
- If any row is missing/weak, default to `NEEDS WORK` or `INVALID CLAIM`.
C) What changed
- Brief bullet summary of the diff/behavioral changes.
D) What's good
- Bullets: correctness, simplicity, tests, docs, ergonomics, etc.
E) Concerns / questions (actionable)
- Numbered list.
- Mark each item as:
- BLOCKER (must fix before merge)
- IMPORTANT (should fix before merge)
- NIT (optional)
- For each: point to the file/area and propose a concrete fix or alternative.
- If evidence for the core bug claim is missing, add a `BLOCKER` explicitly.
F) Tests
- What exists.
- What's missing (specific scenarios).
- State clearly whether there is a regression test for the claimed bug.
G) Follow-ups (optional)
- Non-blocking refactors/tickets to open later.
H) Suggested PR comment (optional)
- Offer: "Want me to draft a PR comment to the author?"
- If yes, provide a ready-to-paste comment summarizing the above, with clear asks.
Rules / Guardrails
- Review only: do not merge (`gh pr merge`), do not push branches, do not edit code.
- If you need clarification, ask questions rather than guessing.

View File

@@ -1,157 +0,0 @@
# Pre-commit hooks for openclaw
# Install: prek install
# Run manually: prek run --all-files
#
# See https://pre-commit.com for more information
repos:
# Basic file hygiene
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: end-of-file-fixer
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
- id: detect-private-key
exclude: '(^|/)(\.secrets\.baseline$|\.detect-secrets\.cfg$|\.pre-commit-config\.yaml$|apps/ios/fastlane/Fastfile$|.*\.test\.ts$)'
# Secret detection (same as CI)
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args:
- --baseline
- .secrets.baseline
- --exclude-files
- '(^|/)pnpm-lock\.yaml$'
- --exclude-lines
- 'key_content\.include\?\("BEGIN PRIVATE KEY"\)'
- --exclude-lines
- 'case \.apiKeyEnv: "API key \(env var\)"'
- --exclude-lines
- 'case apikey = "apiKey"'
- --exclude-lines
- '"gateway\.remote\.password"'
- --exclude-lines
- '"gateway\.auth\.password"'
- --exclude-lines
- '"talk\.apiKey"'
- --exclude-lines
- '=== "string"'
- --exclude-lines
- 'typeof remote\?\.password === "string"'
- --exclude-lines
- "OPENCLAW_DOCKER_GPG_FINGERPRINT="
- --exclude-lines
- '"secretShape": "(secret_input|sibling_ref)"'
- --exclude-lines
- 'API key rotation \(provider-specific\): set `\*_API_KEYS`'
- --exclude-lines
- 'password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway\.auth\.password` -> `gateway\.remote\.password`'
- --exclude-lines
- 'password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway\.remote\.password` -> `gateway\.auth\.password`'
- --exclude-files
- '^src/gateway/client\.watchdog\.test\.ts$'
- --exclude-lines
- 'export CUSTOM_API_K[E]Y="your-key"'
- --exclude-lines
- 'grep -q ''N[O]DE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache'' ~/.bashrc \|\| cat >> ~/.bashrc <<''EOF'''
- --exclude-lines
- 'env: \{ MISTRAL_API_K[E]Y: "sk-\.\.\." \},'
- --exclude-lines
- '"ap[i]Key": "xxxxx"(,)?'
- --exclude-lines
- 'ap[i]Key: "A[I]za\.\.\.",'
- --exclude-lines
- '"ap[i]Key": "(resolved|normalized|legacy)-key"(,)?'
- --exclude-lines
- 'sparkle:edSignature="[A-Za-z0-9+/=]+"'
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
args: [--severity=error] # Only fail on errors, not warnings/info
# Exclude vendor and scripts with embedded code or known issues
exclude: "^(vendor/|scripts/e2e/)"
# GitHub Actions linting
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
# GitHub Actions security audit
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
hooks:
- id: zizmor
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
exclude: "^(vendor/|Swabble/)"
# Python checks for skills scripts
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
hooks:
- id: ruff
files: "^skills/.*\\.py$"
args: [--config, pyproject.toml]
- repo: local
hooks:
- id: skills-python-tests
name: skills python tests
entry: pytest -q skills
language: python
additional_dependencies: [pytest>=8, <9]
pass_filenames: false
files: "^skills/.*\\.py$"
# Project checks (same commands as CI)
- repo: local
hooks:
# node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
- id: pnpm-audit-prod
name: pnpm-audit-prod
entry: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
language: system
pass_filenames: false
# oxlint --type-aware src test
- id: oxlint
name: oxlint
entry: scripts/pre-commit/run-node-tool.sh oxlint --type-aware src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# oxfmt --check src test
- id: oxfmt
name: oxfmt
entry: scripts/pre-commit/run-node-tool.sh oxfmt --check src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# swiftlint (same as CI)
- id: swiftlint
name: swiftlint
entry: swiftlint --config .swiftlint.yml
language: system
pass_filenames: false
types: [swift]
# swiftformat --lint (same as CI)
- id: swiftformat
name: swiftformat
entry: swiftformat --lint apps/macos/Sources --config .swiftformat
language: system
pass_filenames: false
types: [swift]

View File

@@ -1 +1 @@
docs/.generated/
src/canvas-host/a2ui/a2ui.bundle.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
# ShellCheck configuration
# https://www.shellcheck.net/wiki/
# Disable common false positives and style suggestions
# SC2034: Variable appears unused (often exported or used indirectly)
disable=SC2034
# SC2155: Declare and assign separately (common idiom, rarely causes issues)
disable=SC2155
# SC2295: Expansions inside ${..} need quoting (info-level, rarely causes issues)
disable=SC2295
# SC1012: \r is literal (tr -d '\r' works as intended on most systems)
disable=SC1012
# SC2026: Word outside quotes (info-level, often intentional)
disable=SC2026
# SC2016: Expressions don't expand in single quotes (often intentional in sed/awk)
disable=SC2016
# SC2129: Consider using { cmd1; cmd2; } >> file (style preference)
disable=SC2129

View File

@@ -23,7 +23,7 @@
# Whitespace
--trimwhitespace always
--emptybraces no-space
--nospaceoperators ...,..<
--nospaceoperators ...,..<
--ranges no-space
--someAny true
--voidtype void
@@ -48,4 +48,4 @@
--allman false
# Exclusions
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,Peekaboo,Swabble,apps/android,apps/ios,apps/shared,apps/macos/Sources/OpenClawProtocol,apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,Peekaboo,Swabble,apps/android,apps/ios,apps/shared,apps/macos/Sources/ClawdisProtocol,apps/macos/Sources/ClawdbotProtocol

View File

@@ -18,9 +18,7 @@ excluded:
- coverage
- "*.playground"
# Generated (protocol-gen-swift.ts)
- apps/macos/Sources/OpenClawProtocol/GatewayModels.swift
# Generated (generate-host-env-security-policy-swift.mjs)
- apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
- apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
analyzer_rules:
- unused_declaration

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["oxc.oxc-vscode"]
}

21
.vscode/settings.json vendored
View File

@@ -1,21 +0,0 @@
{
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"typescript.preferences.importModuleSpecifierEnding": "js",
"typescript.reportStyleChecksAsWarnings": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.tsdk": "node_modules/typescript/lib"
}

327
AGENTS.md
View File

@@ -1,308 +1,132 @@
# Repository Guidelines
- Repo: https://github.com/openclaw/openclaw
- In chat replies, file references must be repo-root relative only (example: `extensions/telegram/src/index.ts:80`); never absolute paths or `~/...`.
- Do not edit files covered by security-focused `CODEOWNERS` rules unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted surfaces, not drive-by cleanup.
- Repo: https://github.com/clawdbot/clawdbot
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
## Project Structure & Module Organization
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, infra in `src/infra`, media pipeline in `src/media`, web provider helpers in `src/web` and `src/plugins/web-*provider*.ts`).
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, web provider in `src/provider-web.ts`, infra in `src/infra`, media pipeline in `src/media`).
- Tests: colocated `*.test.ts`.
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
- Nomenclature: use "plugin" / "plugins" in docs, UI, changelogs, and contributor guidance. The bundled workspace plugin tree remains the internal package layout to avoid repo-wide churn from a rename.
- Bundled plugin naming: for repo-owned workspace plugins, keep the canonical plugin id aligned across `openclaw.plugin.json:id`, the default workspace folder name, and package names anchored to the same id (`@openclaw/<id>` or approved suffix forms like `-provider`, `-plugin`, `-speech`, `-sandbox`, `-media-understanding`). Keep `openclaw.install.npmSpec` equal to the package name and `openclaw.channel.id` equal to the plugin id when present. Exceptions must be explicit and covered by the repo invariant test.
- Plugins: live in the bundled workspace plugin tree (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `openclaw` in `devDependencies` or `peerDependencies` instead (runtime resolves `openclaw/plugin-sdk` via jiti alias).
- Import boundaries: extension production code should treat `openclaw/plugin-sdk/*` plus local `api.ts` / `runtime-api.ts` barrels as the public surface. Do not import core `src/**`, `src/plugin-sdk-internal/**`, or another extension's `src/**` directly.
- Installers served from `https://openclaw.ai/*`: live in the sibling repo `../openclaw.ai` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
- Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `clawdbot` in `devDependencies` or `peerDependencies` instead (runtime resolves `clawdbot/plugin-sdk` via jiti alias).
- Installers served from `https://clawd.bot/*`: live in the sibling repo `../clawd.bot` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs).
- Core channel docs: `docs/channels/`
- Core channel code: `src/channels`, `src/routing`, `src/web`
- Bundled plugin channels: `extensions/<channel>/` (for example Discord, Telegram, Slack, Matrix, Zalo, ZaloUser, Voice Call)
- When adding channels/plugins/apps/docs, update `.github/labeler.yml` and create matching GitHub labels (use existing channel/plugin label colors).
- Core channel code: `src/telegram`, `src/discord`, `src/slack`, `src/signal`, `src/imessage`, `src/web` (WhatsApp web), `src/channels`, `src/routing`
- Extensions (channel plugins): `extensions/*` (e.g. `extensions/msteams`, `extensions/matrix`, `extensions/zalo`, `extensions/zalouser`, `extensions/voice-call`)
## Architecture Boundaries
- Start here for the repo map:
- bundled workspace plugin tree = bundled plugins and the closest example surface for third-party plugins
- `src/plugin-sdk/*` = the public plugin contract that extensions are allowed to import
- `src/channels/*` = core channel implementation details behind the plugin/channel boundary
- `src/plugins/*` = plugin discovery, manifest validation, loader, registry, and contract enforcement
- `src/gateway/protocol/*` = typed Gateway control-plane and node wire protocol
- Progressive disclosure lives in local boundary guides:
- repo root `AGENTS.md`
- bundled-plugin-tree `extensions/AGENTS.md`
- `src/plugin-sdk/AGENTS.md`
- `src/channels/AGENTS.md`
- `src/plugins/AGENTS.md`
- `src/gateway/protocol/AGENTS.md`
- Workflow hygiene:
- Do not grep or existence-check every `docs/*.md`, `AGENTS.md`, or guide path mentioned in this file before starting work.
- Read only the guides and docs that are directly relevant to the files or boundary you are touching.
- Only do full broken-link or missing-guide sweeps when the task is explicitly about docs or repo-instruction maintenance.
- Plugin and extension boundary:
- Public docs: `docs/plugins/building-plugins.md`, `docs/plugins/architecture.md`, `docs/plugins/sdk-overview.md`, `docs/plugins/sdk-entrypoints.md`, `docs/plugins/sdk-runtime.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-channel-plugins.md`, `docs/plugins/sdk-provider-plugins.md`
- Definition files: `src/plugin-sdk/plugin-entry.ts`, `src/plugin-sdk/core.ts`, `src/plugin-sdk/provider-entry.ts`, `src/plugin-sdk/channel-contract.ts`, `scripts/lib/plugin-sdk-entrypoints.json`, `package.json`
- Invariant: core must stay extension-agnostic. Adding a bundled or third-party extension should not require unrelated core edits just to teach core that the extension exists.
- Rule: extensions must cross into core only through `openclaw/plugin-sdk/*`, manifest metadata, and documented runtime helpers. Do not import `src/**` from extension production code.
- Rule: core code and tests must not deep-import bundled plugin internals such as a plugin's `src/**` files or `onboard.js`. If core needs a bundled plugin helper, expose it through that plugin's `api.ts` and, when it is a real cross-package contract, through `src/plugin-sdk/<id>.ts`.
- Rule: do not add hardcoded bundled extension/provider/channel/capability id lists, maps, or named special cases in core when a manifest, capability, registry, or plugin-owned contract can express the same behavior.
- Rule: extension-owned compatibility behavior belongs to the owning extension. Core may orchestrate generic doctor/config flows, but extension-specific legacy repairs, detection rules, onboarding, auth detection, and provider defaults should live in plugin-owned contracts.
- Rule: for legacy config specifically, prefer doctor-owned repair paths over startup/load-time core migrations. Do not add new plugin-specific legacy migration logic to shared core/runtime surfaces when `openclaw doctor --fix` can own it.
- Rule: when a test is asserting extension-specific behavior, keep that coverage in the owning extension when feasible. Core tests should assert generic contracts and registry/capability behavior, not extension internals.
- Refactor trigger: if you encounter core code or tests that name a specific extension/provider/channel for extension-owned behavior, refactor toward a generic registry/capability/plugin-owned seam instead of adding another special case.
- Compatibility: new plugin seams are allowed, but they must be added as documented, backwards-compatible, versioned contracts. We have third-party plugins in the wild and do not break them casually.
- Channel boundary:
- Public docs: `docs/plugins/sdk-channel-plugins.md`, `docs/plugins/architecture.md`
- Definition files: `src/channels/plugins/types.plugin.ts`, `src/channels/plugins/types.core.ts`, `src/channels/plugins/types.adapters.ts`, `src/plugin-sdk/core.ts`, `src/plugin-sdk/channel-contract.ts`
- Rule: `src/channels/**` is core implementation. If plugin authors need a new seam, add it to the Plugin SDK instead of telling them to import channel internals.
- Provider/model boundary:
- Public docs: `docs/plugins/sdk-provider-plugins.md`, `docs/concepts/model-providers.md`, `docs/plugins/architecture.md`
- Definition files: `src/plugins/types.ts`, `src/plugin-sdk/provider-entry.ts`, `src/plugin-sdk/provider-auth.ts`, `src/plugin-sdk/provider-catalog-shared.ts`, `src/plugin-sdk/provider-model-shared.ts`
- Rule: core owns the generic inference loop; provider plugins own provider-specific behavior through registration and typed hooks. Do not solve provider needs by reaching into unrelated core internals.
- Rule: avoid ad hoc reads of `plugins.entries.<id>.config` from unrelated core code. If core needs plugin-owned auth/config behavior, add or use a generic seam (`resolveSyntheticAuth`, public SDK/helper facades, manifest metadata, plugin auto-enable hooks) and honor plugin disablement plus SecretRef semantics.
- Rule: vendor-owned tools and settings belong in the owning plugin. Do not add provider-specific tool config, secret collection, or runtime enablement to core `tools.*` surfaces unless the tool is intentionally core-owned.
- Gateway protocol boundary:
- Public docs: `docs/gateway/protocol.md`, `docs/gateway/bridge-protocol.md`, `docs/concepts/architecture.md`
- Definition files: `src/gateway/protocol/schema.ts`, `src/gateway/protocol/schema/*.ts`, `src/gateway/protocol/index.ts`
- Rule: protocol changes are contract changes. Prefer additive evolution; incompatible changes require explicit versioning, docs, and client/codegen follow-through.
- Config contract boundary:
- Canonical public config lives in exported config types, zod/schema surfaces, schema help/labels, generated config metadata, config baselines, and any user-facing gateway/config payloads. Keep those surfaces aligned.
- When a legacy config key is retired from the public contract, remove it from every public config surface above. Keep backward compatibility only through raw-config migration/doctor seams unless explicit product policy says otherwise.
- Do not reintroduce removed legacy aliases into public types/schema/help/baselines “for convenience”. If old configs still need to load, handle that in `legacy.migrations.*`, config ingest, or `openclaw doctor --fix`.
- `hooks.internal.entries` is the canonical public hook config model. `hooks.internal.handlers` is compatibility-only input and must not be re-exposed in public schema/help/baseline surfaces.
- Bundled plugin contract boundary:
- Public docs: `docs/plugins/architecture.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-overview.md`
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-surface-loader.ts`, `src/plugins/public-surface-runtime.ts`, `src/plugins/provider-public-artifacts.ts`, `src/plugins/web-provider-public-artifacts.ts`
- Rule: keep manifest metadata, runtime registration, public SDK exports, and contract tests aligned. Do not create a hidden path around the declared plugin interfaces.
- Extension test boundary:
- Keep extension-owned onboarding/config/provider coverage under the owning bundled plugin package when feasible.
- If core tests need bundled plugin behavior, consume it through public `src/plugin-sdk/<id>.ts` facades or the plugin's `api.ts`, not private extension modules.
- Shared helpers under `test/helpers/**` are part of that same boundary. Do not hardcode repo-relative `extensions/**` imports there, and do not keep plugin-local deep mocks in shared helpers just because multiple tests use them.
- When core tests or shared helpers need bundled plugin public surfaces, use `src/test-utils/bundled-plugin-public-surface.ts` for `api.ts`, `runtime-api.ts`, `contract-api.ts`, `test-api.ts`, plugin entrypoint `index.js`, and resolved module ids for dynamic import or mocking.
- If a core test is asserting extension-specific behavior instead of a generic contract, move it to the owning extension package.
- Scoped guides still matter:
- `extensions/AGENTS.md` expands extension/plugin boundary rules.
- `src/channels/AGENTS.md` expands core channel boundary and hot-path rules.
- `src/plugin-sdk/AGENTS.md` expands public SDK contract rules.
- `src/plugins/AGENTS.md` expands plugin loading, registry, and manifest rules.
- `src/gateway/protocol/AGENTS.md` expands typed Gateway protocol rules.
- `src/gateway/AGENTS.md` expands Gateway server hot-path and plugin artifact rules.
- `src/agents/AGENTS.md` expands agent test/import performance rules.
- `test/helpers/AGENTS.md` and `test/helpers/channels/AGENTS.md` expand shared test helper boundary rules.
- Plugin architecture direction:
- Keep a manifest-first control plane: discovery, validation, enablement, setup hints, and activation planning should stay metadata-driven by default.
- Keep runtime execution separate: actual provider/channel/tool execution should resolve through narrow targeted loaders, not broad registry materialization.
- Host loads plugins; plugins do not load host internals. Prefer a small versioned host/kernel seam plus documented SDK entrypoints over ambient reachability.
- Treat broad runtime registries and mutable global plugin state as transitional compatibility surfaces, not the target architecture.
- If a setup or config flow truly needs plugin runtime, make that explicit instead of silently importing runtime code on the cold path.
## Scoped Workflow Guides
- `docs/AGENTS.md` owns Mintlify docs, docs links, and docs i18n rules.
- `ui/AGENTS.md` owns Control UI i18n and generated locale rules.
- `scripts/AGENTS.md` owns script-runner, local-check lock, and test/lint wrapper rules.
## Docs Linking (Mintlify)
- Docs are hosted on Mintlify (docs.clawd.bot).
- Internal doc links in `docs/**/*.md`: root-relative, no `.md`/`.mdx` (example: `[Config](/configuration)`).
- Section cross-references: use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`).
- When Peter asks for links, reply with full `https://docs.clawd.bot/...` URLs (not root-relative).
- When you touch docs, end the reply with the `https://docs.clawd.bot/...` URLs you referenced.
- README (GitHub): keep absolute docs URLs (`https://docs.clawd.bot/...`) so links work on GitHub.
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
## exe.dev VM ops (general)
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
- SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops.
- Update: `sudo npm i -g openclaw@latest` (global install needs root on `/usr/lib/node_modules`).
- Config: use `openclaw config set ...`; ensure `gateway.mode=local` is set.
- Update: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`).
- Config: use `clawdbot config set ...`; ensure `gateway.mode=local` is set.
- Discord: store raw token only (no `DISCORD_BOT_TOKEN=` prefix).
- Restart: stop old gateway and run:
`pkill -9 -f openclaw-gateway || true; nohup openclaw gateway run --bind loopback --port 18789 --force > /tmp/openclaw-gateway.log 2>&1 &`
- Verify: `openclaw channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/openclaw-gateway.log`.
`pkill -9 -f clawdbot-gateway || true; nohup clawdbot gateway run --bind loopback --port 18789 --force > /tmp/clawdbot-gateway.log 2>&1 &`
- Verify: `clawdbot channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/clawdbot-gateway.log`.
## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install`
- If deps are missing (for example `node_modules` missing, `vitest not found`, or `command not found`), run the repos package-manager install command (prefer lockfile/README-defined PM), then rerun the exact requested command once. Apply this to test/build/lint/typecheck/dev commands; if retry still fails, report the command and first actionable error.
- Pre-commit hooks are installed by the package `prepare` script (`git config core.hooksPath git-hooks`). The hook formats/lints staged source files and runs `pnpm check` unless the staged change is docs-only or `FAST_COMMIT=1` is set.
- `FAST_COMMIT=1` skips the repo-wide `pnpm check` inside the pre-commit hook only. The hook still runs targeted formatting/linting for staged files and restages formatter changes. Use it when you intentionally want a faster commit path and are running equivalent targeted verification manually. It does not change CI and does not change what `pnpm check` itself does.
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`.
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
- Node remains supported for running built output (`dist/*`) and production installs.
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
- Type-check/build: `pnpm build`
- TypeScript checks: `pnpm tsgo`
- Lint/format: `pnpm check`
- Local agent/dev shells default to host-aware `OPENCLAW_LOCAL_CHECK=1` behavior for `pnpm tsgo` and `pnpm lint`; set `OPENCLAW_LOCAL_CHECK_MODE=throttled` to force the lower-memory profile, `OPENCLAW_LOCAL_CHECK_MODE=full` to keep lock-only behavior, or `OPENCLAW_LOCAL_CHECK=0` in CI/shared runs.
- Format check: `pnpm format:check` (oxfmt --check)
- Format fix: `pnpm format` or `pnpm format:fix` (oxfmt --write)
- Terminology:
- "gate" means a verification command or command set that must be green for the decision you are making.
- A local dev gate is the fast default loop, usually `pnpm check` plus any scoped test you actually need.
- A landing gate is the broader bar before pushing `main`, usually `pnpm check`, `pnpm test`, and `pnpm build` when the touched surface can affect build output, packaging, lazy-loading/module boundaries, or published surfaces.
- A CI gate is whatever the relevant workflow enforces for that lane (for example `check`, `check-additional`, `build-smoke`, or release validation).
- Local dev gate: prefer `pnpm check` for the normal edit loop. It keeps the repo-architecture policy guards out of the default local loop.
- CI architecture gate: `check-additional` enforces architecture and boundary policy guards that are intentionally kept out of the default local loop.
- Formatting gate: the pre-commit hook runs targeted formatting on staged source files before `pnpm check`. If you want a repo-wide formatting-only preflight locally, run `pnpm format:check` explicitly.
- If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hooks repo-wide `pnpm check`; targeted formatting/linting still runs, so use that only when you are deliberately covering the touched surface some other way.
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`.
- Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
- Generated baseline drift detection uses SHA-256 hash files under `docs/.generated/` (`.sha256` files tracked in git; full JSON baselines are gitignored, generated locally for inspection).
- Config schema drift uses `pnpm config:docs:gen` / `pnpm config:docs:check`.
- Plugin SDK API drift uses `pnpm plugin-sdk:api:gen` / `pnpm plugin-sdk:api:check`.
- If you change config schema/help or the public Plugin SDK surface, run the matching gen command and commit the updated `.sha256` hash file. Keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
- When `pnpm tsgo` fails, triage by coherent surface instead of by raw error count: rerun the gate, group failures by package/module/type contract, open the source-of-truth type or export file first, fix the root mismatch, then rerun `pnpm tsgo` before widening into downstream consumers. Check `origin/main` before doing broad cleanup because some apparent type debt is already fixed upstream.
- For narrowly scoped changes, prefer narrowly scoped tests that directly validate the touched behavior. If no meaningful scoped test exists, say so explicitly and use the next most direct validation available.
- Verification modes for work on `main`:
- Default mode: `main` is relatively stable. Count pre-commit hook coverage when it already verified the current tree, avoid rerunning the exact same checks just for ceremony, and prefer keeping CI/main green before landing.
- Fast-commit mode: `main` is moving fast and you intentionally optimize for shorter commit loops. Prefer explicit local verification close to the final landing point, and it is acceptable to use `--no-verify` for intermediate or catch-up commits after equivalent checks have already run locally.
- Preferred landing bar for pushes to `main`: in Default mode, favor `pnpm check` and `pnpm test` near the final rebase/push point when feasible. In fast-commit mode, verify the touched surface locally near landing without insisting every intermediate commit replay the full hook.
- Scoped tests prove the change itself. `pnpm test` remains the default `main` landing bar; scoped tests do not replace full-suite gates by default.
- Hard gate: if the change can affect build output, packaging, lazy-loading/module boundaries, or published surfaces, `pnpm build` MUST be run and MUST pass before pushing `main`.
- Default rule: do not land changes with failing format, lint, type, build, or required test checks when those failures are caused by the change or plausibly related to the touched surface. Fast-commit mode changes how verification is sequenced; it does not lower the requirement to validate and clean up the touched surface before final landing.
- For narrowly scoped changes, if unrelated failures already exist on latest `origin/main`, state that clearly, report the scoped tests you ran, and ask before broadening scope into unrelated fixes or landing despite those failures.
- Do not use scoped tests as permission to ignore plausibly related failures.
## Prompt Cache Stability
- Treat prompt-cache stability as correctness/perf-critical, not cosmetic.
- Any code that assembles model or tool payloads from maps, sets, registries, plugin lists, MCP catalogs, filesystem reads, or network results must make ordering deterministic before building the request.
- Do not rewrite older transcript/history bytes on every turn unless you intentionally want to invalidate the cached prefix. Legacy cleanup, pruning, normalization, and migration logic should preserve recent prompt bytes when possible.
- If truncation or compaction is required, prefer mutating newest or tail content first so the cached prefix stays byte-identical for as long as possible.
- For cache-sensitive changes, require a regression test that proves turn-to-turn prefix stability or deterministic request assembly; helper-local tests alone are not enough.
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Oxlint and Oxfmt.
- Never add `@ts-nocheck` and do not add inline lint suppressions by default. Fix root causes first; only keep a suppression when the code is intentionally correct, the rule cannot express that safely, and the comment explains why.
- Do not disable `no-explicit-any`; prefer real types, `unknown`, or a narrow adapter/helper instead. Update Oxlint/Oxfmt config only when required.
- Prefer `zod` or existing schema helpers at external boundaries such as config, webhook payloads, CLI/JSON output, persisted JSON, and third-party API responses.
- Prefer discriminated unions when parameter shape changes runtime behavior.
- Prefer `Result<T, E>`-style outcomes and closed error-code unions for recoverable runtime decisions.
- Keep human-readable strings for logs, CLI output, and UI; do not use freeform strings as the source of truth for internal branching.
- Avoid `?? 0`, empty-string, empty-object, or magic-string sentinels when they can change runtime meaning silently.
- If introducing a new optional field or nullable semantic in core logic, prefer an explicit union or dedicated type when the value changes behavior.
- New runtime control-flow code should not branch on `error: string` or `reason: string` when a closed code union would be reasonable.
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
- Circular dependencies: keep both `pnpm check:import-cycles` and `pnpm check:madge-import-cycles` green; do not reintroduce runtime import cycles or madge-detected import loops.
- Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/<extension>` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/<extension>` path as the external contract only.
- Extension package boundary guardrail: inside a bundled plugin package, do not use relative imports/exports that resolve outside that same package root. If shared code belongs in the plugin SDK, import `openclaw/plugin-sdk/<subpath>` instead of reaching into `src/plugin-sdk/**` or other repo paths via `../`.
- Extension API surface rule: `openclaw/plugin-sdk/<subpath>` is the only public cross-package contract for extension-facing SDK code. If an extension needs a new seam, add a public subpath first; do not reach into `src/plugin-sdk/**` by relative path.
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits.
- Add brief code comments for tricky or non-obvious logic.
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
- Naming: use **OpenClaw** for product/app/docs headings; use `openclaw` for CLI command, package/binary, paths, and config keys.
- Written English: use American spelling and grammar in code, comments, docs, and UI strings (e.g. "color" not "colour", "behavior" not "behaviour", "analyze" not "analyse").
- Naming: use **Clawdbot** for product/app/docs headings; use `clawdbot` for CLI command, package/binary, paths, and config keys.
## Release / Advisory Workflows
- Use `$openclaw-release-maintainer` at `.agents/skills/openclaw-release-maintainer/SKILL.md` for release naming, version coordination, release auth, and changelog-backed release-note workflows.
- Use `$openclaw-ghsa-maintainer` at `.agents/skills/openclaw-ghsa-maintainer/SKILL.md` for GHSA advisory inspection, patch/publish flow, private-fork checks, and GHSA API validation.
- Release and publish remain explicit-approval actions even when using the skill.
## Release Channels (Naming)
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app).
- dev: moving head on `main` (no tag; git checkout main).
## Testing Guidelines
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
- When tests need example Anthropic/OpenAI model constants, prefer `sonnet-4.6` and `gpt-5.4`; update older Anthropic/GPT examples when you touch those tests.
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
- Write tests to clean up timers, env, globals, mocks, sockets, temp dirs, and module state so `--isolate=false` stays green.
- Test performance guardrail: do not put `vi.resetModules()` plus `await import(...)` in `beforeEach`/per-test loops for heavy modules unless module state truly requires it. Prefer static imports or one-time `beforeAll` imports, then reset mocks/runtime state directly.
- Test performance guardrail: if a test file uses stable `vi.mock(...)` hoists or other static module mocks, do not pair them with `vi.resetModules()` and a fresh `await import(...)` in every `beforeEach`. Import the heavy module once in `beforeAll`, then reset/prime mocks in `beforeEach` so Browser/Matrix-style hotspot tests do not pay the module graph cost per case.
- Test performance guardrail: inside an extension package, prefer a thin local seam (`./api.ts`, `./runtime-api.ts`, or a narrower local `*.runtime-api.ts`) over direct `openclaw/plugin-sdk/*` imports for internal production code. Keep local seams curated and lightweight; only reach for direct `plugin-sdk/*` imports when you are crossing a real package boundary or when no suitable local seam exists yet.
- Test performance guardrail: keep expensive runtime fallback work such as snapshotting, migration, installs, or bootstrap behind dedicated `*.runtime.ts` boundaries so tests can mock the seam instead of accidentally invoking real work.
- Test performance guardrail: for import-only/runtime-wrapper tests, keep the wrapper lazy. Do not eagerly load heavy verification/bootstrap/runtime modules at module top level if the exported function can import them on demand.
- Test performance guardrail: prefer explicit mock factories over `importOriginal()` for broad modules. Reserve `importOriginal()` for narrow modules where partial-real behavior is genuinely needed.
- Test performance guardrail: do not partial-mock broad `openclaw/plugin-sdk/*` barrels in hot tests. Add a plugin-local `*.runtime.ts` seam and mock that seam instead.
- Test performance guardrail: when production code already accepts `deps`, callbacks, or runtime injection, use that seam in tests before adding module-level mocks.
- Test performance guardrail: prefer narrow public SDK subpaths such as `models-provider-runtime`, `skill-commands-runtime`, and `reply-dispatch-runtime` over older broad helper barrels when both expose the needed helper.
- Test performance guardrail: treat import-dominated test time as a boundary bug. Refactor the import surface before adding more cases to the slow file.
- Test performance guardrail: when replacing a slow integration test with helper-level coverage, extract the exact production composition into a named helper and test that helper. Do not trade coverage shape for speed without preserving the behavior proof somewhere cheaper.
- Test performance guardrail: for plugin-owned static descriptors used by core tests or cold paths, prefer lightweight public artifacts with full-runtime fallback over loading broad bundled plugin barrels.
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
- For targeted/local debugging, use the native root-project entrypoint: `pnpm test <path-or-filter> [vitest args...]` (for example `pnpm test src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses the repo's default config/profile/pool routing.
- Do not set test workers above 16; tried already.
- Vitest now defaults to native root-project `threads`, with hard `forks` exceptions for `gateway`, `agents`, and `commands`. Keep new pool changes explicit and justified; use `OPENCLAW_VITEST_POOL=forks` for full local fork debugging.
- If local Vitest runs cause memory pressure, the default worker budget now derives from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live tests (real keys): `OPENCLAW_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- `pnpm test:live` defaults quiet now. Keep `[live]` progress; suppress profile/gateway chatter. Full logs: `OPENCLAW_LIVE_TEST_QUIET=0 pnpm test:live`.
- Full kit + whats covered: `docs/help/testing.md`.
- Changelog: user-facing changes only; no internal/meta notes (version alignment, appcast reminders, release process).
- Changelog placement: in the active version block, append new entries to the end of the target section (`### Changes` or `### Fixes`); do not insert new entries at the top of a section.
- Changelog attribution: use at most one contributor mention per line; prefer `Thanks @author` and do not also add `by @author` on the same entry.
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Clawdbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Full kit + whats covered: `docs/testing.md`.
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available.
## Commit & Pull Request Guidelines
- Use `$openclaw-pr-maintainer` at `.agents/skills/openclaw-pr-maintainer/SKILL.md` for maintainer PR triage, review, close, search, and landing workflows.
- This includes auto-close labels, bug-fix evidence gates, GitHub comment/search footguns, and maintainer PR decision flow.
- For the repo's end-to-end maintainer PR workflow, use `$openclaw-pr-maintainer` at `.agents/skills/openclaw-pr-maintainer/SKILL.md`.
- `/landpr` lives in the global Codex prompts (`~/.codex/prompts/landpr.md`); when landing or merging any PR, always follow that `/landpr` process.
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
- Group related changes; avoid bundling unrelated refactors.
- PR submission template (canonical): `.github/pull_request_template.md`
- Issue submission templates (canonical): `.github/ISSUE_TEMPLATE/`
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless its truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
- When working on an issue: reference the issue in the changelog entry.
- When merging a PR: leave a PR comment that explains exactly what we did and include the SHA hashes.
- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list.
- After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README.
## Git Notes
## Shorthand Commands
- `sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`.
- If `git branch -d/-D <branch>` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/<branch>`.
- Agents MUST NOT create or push merge commits on `main`. If `main` has advanced, rebase local commits onto the latest `origin/main` before pushing.
- Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query.
### PR Workflow (Review vs Land)
- **Review mode (PR link only):** read `gh pr view/diff`; **do not** switch branches; **do not** change code.
- **Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm lint && pnpm build && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing). Important: contributor needs to be in git graph after this!
## Security & Configuration Tips
- Channel/provider state lives under `~/.openclaw/credentials/`; rerun `openclaw channels login` if logged out. Model auth profiles live under `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`; legacy OAuth import still reads `~/.openclaw/credentials/oauth.json`.
- Pi sessions live under `~/.openclaw/agents/<agentId>/sessions/` by default; `session.store` can override the session store path.
- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
- Environment variables: see `~/.profile`.
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook, `docs/reference/RELEASING.md` for the public release policy, and `$openclaw-release-maintainer` for the maintainership workflow.
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
## Local Runtime / Platform Notes
## Troubleshooting
- Rebrand/migration issues or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
## Agent-Specific Notes
- Vocabulary: "makeup" = "mac app".
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
- Use `$openclaw-parallels-smoke` at `.agents/skills/openclaw-parallels-smoke/SKILL.md` for Parallels smoke, rerun, upgrade, debug, and result-interpretation workflows across macOS, Windows, and Linux guests.
- For the macOS Discord roundtrip deep dive, use the narrower `.agents/skills/parallels-discord-roundtrip/SKILL.md` companion skill.
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
- If you need local-only `.agents` ignores, use `.git/info/exclude` instead of repo `.gitignore`.
- When adding a new `AGENTS.md` anywhere in the repo, also add a `CLAUDE.md` symlink pointing to it (example: `ln -s AGENTS.md CLAUDE.md`).
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
- Never update the Carbon dependency.
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); dont hand-roll spinners/bars.
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
- Gateway may run as an app-managed launchd job. Restart the gateway via the app or `openclaw gateway restart`; inspect with `openclaw gateway status --deep` or, for the default profile, `launchctl print gui/$UID/ai.openclaw.gateway`. Use `scripts/restart-mac.sh` when you need to rebuild/relaunch the local macOS app itself. The app LaunchAgent uses `ai.openclaw.mac`. **When debugging on macOS, start/stop the gateway via the app or gateway CLI, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the OpenClaw subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; dont introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/version.json` (source for generated iOS config and Fastlane metadata), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), and `docs/install/updating.md` (pinned npm version).
- "Bump version everywhere" means all version locations above, then run `pnpm ios:version:sync` for iOS generated outputs. Only touch appcast metadata when cutting a new macOS Sparkle release.
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/Clawdbot/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION).
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- Mobile pairing: `ws://` (cleartext) is allowed for private LAN addresses (RFC 1918, link-local, mDNS `.local`) and loopback. Private LAN hosts typically lack PKI-backed identity, so requiring TLS there adds complexity without meaningful security gain. `wss://` is required for Tailscale and public endpoints.
- Security report scope: reports that treat cleartext `ws://` mobile pairing over private LAN as a vulnerability are out of scope unless they demonstrate a trust-boundary bypass beyond passive network observation on the same LAN.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).
- Lobster palette: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
- When asked to open a “session” file, open the Pi session logs under `~/.openclaw/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
- Voice wake forwarding tips:
- Command template should stay `openclaw-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Dont add extra quotes.
- launchd PATH is minimal; ensure the apps launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`openclaw` binaries resolve when invoked via `openclaw-mac`.
## Collaboration / Safety Notes
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
- Carbon version edits are owner-only: do not change `@buape/carbon` version pins unless you are Shadow (@thewilloftheshadow) as verified by gh.
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
- Release signing/notary keys are managed outside the repo; follow internal release docs.
- Notary auth env vars (`APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_API_KEY_P8`) are expected in your environment (per internal release docs).
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
- **Multi-agent safety:** when the user says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When the user says "commit", scope to your changes only. When the user says "commit all", commit everything in grouped chunks.
- **Multi-agent safety:** prefer grouped `commit` / `pull --rebase` / `push` cycles for related work instead of many tiny syncs.
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless explicitly requested.
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless explicitly requested.
- **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session.
@@ -311,12 +135,25 @@
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
- Only ask when changes are semantic (logic/data/behavior).
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
- Code style: add brief comments for tricky logic; keep files under ~700 LOC when feasible (split/refactor as needed).
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
- Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel.
- For manual `openclaw message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
- Voice wake forwarding tips:
- Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Dont add extra quotes.
- launchd PATH is minimal; ensure the apps launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- For manual `clawdbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
- Release guardrails: do not change version numbers without operators explicit consent; always ask permission before running any npm publish/release step.
- Beta release guardrail: when using a beta Git tag (for example `vYYYY.M.D-beta.N`), publish npm with a matching beta version suffix (for example `YYYY.M.D-beta.N`) rather than a plain version on `--tag beta`; otherwise the plain version name gets consumed/blocked.
## NPM + 1Password (publish/verify)
- Use the 1password skill; all `op` commands must run inside a fresh tmux session.
- Sign in: `eval "$(op signin --account my.1password.com)"` (app unlocked + integration on).
- OTP: `op read 'op://Private/Npmjs/one-time password?attribute=otp'`.
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
- Kill the tmux session after publish.

File diff suppressed because it is too large Load Diff

View File

@@ -1,229 +1,42 @@
# Contributing to OpenClaw
# Contributing to Clawdbot
Welcome to the lobster tank! 🦞
## Quick Links
- **GitHub:** https://github.com/openclaw/openclaw
- **Vision:** [`VISION.md`](VISION.md)
- **GitHub:** https://github.com/clawdbot/clawdbot
- **Discord:** https://discord.gg/qkhbAGHRBT
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@openclaw](https://x.com/openclaw)
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@clawdbot](https://x.com/clawdbot)
## Maintainers
- **Peter Steinberger** - Benevolent Dictator
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
- **Shadow** - Discord subsystem, Discord admin, Clawhub, all community moderation
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shadowed](https://x.com/4shadowed)
- **Vignesh** - Memory (QMD), formal modeling, TUI, IRC, and Lobster
- GitHub: [@vignesh07](https://github.com/vignesh07) · X: [@\_vgnsh](https://x.com/_vgnsh)
- **Shadow** - Discord + Slack subsystem
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed)
- **Jos** - Telegram, API, Nix mode
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
- **Ayaan Zaidi** - Telegram subsystem, Android app
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@obviyus](https://x.com/obviyus)
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
- **Mariano Belinky** - iOS app, Security
- GitHub: [@mbelinky](https://github.com/mbelinky) · X: [@belimad](https://x.com/belimad)
- **Nimrod Gutman** - iOS app, macOS app and crustacean features
- GitHub: [@ngutman](https://github.com/ngutman) · X: [@theguti](https://x.com/theguti)
- **Vincent Koc** - Agents, Telemetry, Hooks, Security
- GitHub: [@vincentkoc](https://github.com/vincentkoc) · X: [@vincent_koc](https://x.com/vincent_koc)
- **Val Alexander** - UI/UX, Docs, and Agent DevX
- GitHub: [@BunsDev](https://github.com/BunsDev) · X: [@BunsDev](https://x.com/BunsDev)
- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening
- GitHub: [@sebslight](https://github.com/sebslight) · X: [@sebslig](https://x.com/sebslig)
- **Christoph Nakazawa** - JS Infra
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
- **Gustavo Madeira Santana** - Multi-agents, CLI, Performance, Plugins, Matrix
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
- **Onur Solmaz** - Agents, dev workflows, ACP integrations, MS Teams
- GitHub: [@onutc](https://github.com/onutc), [@osolmaz](https://github.com/osolmaz) · X: [@onusoz](https://x.com/onusoz)
- **Josh Avant** - Core, CLI, Gateway, Security, Agents
- GitHub: [@joshavant](https://github.com/joshavant) · X: [@joshavant](https://x.com/joshavant)
- **Jonathan Taylor** - ACP subsystem, Gateway features/bugs, Gog/Mog/Sog CLI's, SEDMAT
- GitHub [@visionik](https://github.com/visionik) · X: [@visionik](https://x.com/visionik)
- **Josh Lehman** - Compaction, Context Engine
- GitHub [@jalehman](https://github.com/jalehman) · X: [@jlehman\_](https://x.com/jlehman_)
- **Radek Sienkiewicz** - Docs, Control UI
- GitHub [@velvet-shark](https://github.com/velvet-shark) · X: [@velvet_shark](https://twitter.com/velvet_shark)
- **Muhammed Mukhthar** - Mattermost, CLI
- GitHub [@mukhtharcm](https://github.com/mukhtharcm) · X: [@mukhtharcm](https://x.com/mukhtharcm)
- **Altay** - Agents, CLI, error handling
- GitHub [@altaywtf](https://github.com/altaywtf) · X: [@altaywtf](https://x.com/altaywtf)
- **Robin Waslander** - Security, PR triage, bug fixes
- GitHub: [@hydro13](https://github.com/hydro13) · X: [@Robin_waslander](https://x.com/Robin_waslander)
- **Tengji (George) Zhang** - Chinese model APIs, cloud, pi
- GitHub: [@odysseus0](https://github.com/odysseus0) · X: [@odysseus0z](https://x.com/odysseus0z)
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Dingtalk, Feishu
- GitHub: [@sliverp](https://github.com/sliverp) · X: [@sliver01234](https://x.com/sliver01234)
- **Mason Huang** - Stability, Security, Speed
- GitHub: [@hxy91819](https://github.com/hxy91819) · X: [@chenjingtalk](https://x.com/chenjingtalk)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!
2. **New features / architecture** → Start a [GitHub Issue](https://github.com/openclaw/openclaw/issues/new/choose) or ask in Discord first. Most features are not accepted and should be third party plugins instead using our plugin SDK.
3. **Refactor-only PRs** → Don't open a PR. We are not accepting refactor-only changes unless a maintainer explicitly asks for them as part of a concrete fix.
4. **Test/CI-only PRs for known `main` failures** → Don't open a PR. The Maintainer team is already tracking those failures, and PRs that only tweak tests or CI to chase them will be closed unless they are required to validate a new fix.
5. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
## PR Limits
We cap at **10 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
For coordinated change sets that genuinely need more than 10 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/clawdbot/clawdbot/discussions) or ask in Discord first
3. **Questions** → Discord #setup-help
## Before You PR
- Test locally with your OpenClaw instance
- Run tests: `pnpm build && pnpm check && pnpm test`
- For iterative local commits, `scripts/committer --fast "message" <files...>` passes `FAST_COMMIT=1` through to the pre-commit hook so it skips the repo-wide `pnpm check`. Only use it when you've already run equivalent targeted validation for the touched surface.
- For extension/plugin changes, run the fast local lane first:
- `pnpm test:extension <extension-name>`
- `pnpm test:extension --list` to see valid extension ids
- If you changed shared plugin or channel surfaces, run `pnpm test:contracts`
- For targeted shared-surface work, use `pnpm test:contracts:channels` or `pnpm test:contracts:plugins`
- These commands also cover the shared seam/smoke files that the default unit lane skips
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
- If you touched bundled-plugin boundaries in shared code, run the matching inventories:
- `node scripts/check-src-extension-import-boundary.mjs --json` for `src/**`
- `node scripts/check-sdk-package-extension-import-boundary.mjs --json` for `src/plugin-sdk/**` and `packages/**`
- `node scripts/check-test-helper-extension-import-boundary.mjs --json` for `test/helpers/**`
- Shared test helpers must use `src/test-utils/bundled-plugin-public-surface.ts` instead of repo-relative `extensions/**` imports. Keep plugin-local deep mocks inside the owning bundled plugin package.
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
- Do not submit refactor-only PRs unless a maintainer explicitly requested that refactor for an active fix or deliverable.
- Do not submit test or CI-config fixes for failures already red on `main` CI. If a failure is already visible in the [main branch CI runs](https://github.com/openclaw/openclaw/actions), it's a known issue the Maintainer team is tracking, and a PR that only addresses those failures will be closed automatically. If you spot a _new_ regression not yet shown in main CI, report it as an issue first.
- Do not submit test-only PRs that just try to make known `main` CI failures pass. Test changes are acceptable when they are required to validate a new fix or cover new behavior in the same PR.
- Ensure CI checks pass
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
- Test locally with your Clawdbot instance
- Run linter: `npm run lint`
- Keep PRs focused (one thing per PR)
- Describe what & why
- Reply to or resolve bot review conversations you addressed before asking for review again
- **Include screenshots** — one showing the problem/before, one showing the fix/after (for UI or visual changes)
- Use American English spelling and grammar in code, comments, docs, and UI strings
- Do not edit files covered by `CODEOWNERS` security ownership unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted review surfaces, not opportunistic cleanup targets.
## Review Conversations Are Author-Owned
If a review bot leaves review conversations on your PR, you are expected to handle the follow-through:
- Resolve the conversation yourself once the code or explanation fully addresses the bot's concern
- Reply and leave it open only when you need maintainer or reviewer judgment
- Do not leave "fixed" bot review conversations for maintainers to clean up for you
- If Codex leaves comments, address every relevant one or resolve it with a short explanation when it is not applicable to your change
- If GitHub Codex review does not trigger for some reason, run `codex review --base origin/main` locally anyway and treat that output as required review work
This applies to both human-authored and AI-assisted PRs.
## Control UI Decorators
The Control UI uses Lit with **legacy** decorators (current Rollup parsing does not support
`accessor` fields required for standard decorators). When adding reactive fields, keep the
legacy style:
```ts
@state() foo = "bar";
@property({ type: Number }) count = 0;
```
The root `tsconfig.json` is configured for legacy decorators (`experimentalDecorators: true`)
with `useDefineForClassFields: false`. Avoid flipping these unless you are also updating the UI
build tooling to support standard decorators.
## AI/Vibe-Coded PRs Welcome! 🤖
Built with Codex, Claude, or other AI tools? **Awesome - just mark it!**
Please include in your PR:
- [ ] Mark as AI-assisted in the PR title or description
- [ ] Note the degree of testing (untested / lightly tested / fully tested)
- [ ] Include prompts or session logs if possible (super helpful!)
- [ ] Confirm you understand what the code does
- [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review
- [ ] Resolve or reply to bot review conversations after you address them
AI PRs are first-class citizens here. We just want transparency so reviewers know what to look for. If you are using an LLM coding agent, instruct it to resolve bot review conversations it has addressed instead of leaving them for maintainers.
## Current Focus & Roadmap 🗺
We are currently prioritizing:
- **Stability**: Fixing edge cases in channel connections (WhatsApp/Telegram).
- **UX**: Improving the onboarding wizard and error messages.
- **Skills**: For skill contributions, head to [ClawHub](https://clawhub.ai/) — the community hub for OpenClaw skills.
- **Performance**: Optimizing token usage and compaction logic.
Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for
["good first issue"](https://github.com/openclaw/openclaw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
labels. If none are open, pick a small docs or bug issue and leave a quick comment saying
you'd like to work on it.
## Maintainers
We're selectively expanding the maintainer team.
If you're an experienced contributor who wants to help shape OpenClaw's direction — whether through code, docs, or community — we'd like to hear from you.
Being a maintainer is a responsibility, not an honorary title. We expect active, consistent involvement — triaging issues, reviewing PRs, and helping move the project forward.
Still interested? Email contributing@openclaw.ai with:
- Links to your PRs on OpenClaw (if you don't have any, start there first)
- Links to open source projects you maintain or actively contribute to
- Your GitHub, Discord, and X/Twitter handles
- A brief intro: background, experience, and areas of interest
- Languages you speak and where you're based
- How much time you can realistically commit
We welcome people across all skill sets — engineering, documentation, community management, and more.
We review every human-only-written application carefully and add maintainers slowly and deliberately.
Please allow a few weeks for a response.
## Report a Vulnerability
We take security reports seriously. Report vulnerabilities directly to the repository where the issue lives:
- **Core CLI and gateway** — [openclaw/openclaw](https://github.com/openclaw/openclaw)
- **macOS desktop app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/macos)
- **iOS app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/ios)
- **Android app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/android)
- **ClawHub** — [openclaw/clawhub](https://github.com/openclaw/clawhub)
- **Trust and threat model** — [openclaw/trust](https://github.com/openclaw/trust)
For issues that don't fit a specific repo, or if you're unsure, email **security@openclaw.ai** and we'll route it.
### Required in Reports
1. **Title**
2. **Severity Assessment**
3. **Impact**
4. **Affected Component**
5. **Technical Reproduction**
6. **Demonstrated Impact**
7. **Environment**
8. **Remediation Advice**
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
AI PRs are first-class citizens here. We just want transparency so reviewers know what to look for.

View File

@@ -1,282 +1,35 @@
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm
# Opt-in extension dependencies at build time (space-separated directory names).
# Example: docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel matrix" .
#
# Multi-stage build produces a minimal runtime image without build tools,
# source code, or Bun. Works with Docker, Buildx, and Podman.
# The ext-deps stage extracts only the package.json files we need from the
# bundled plugin workspace tree, so the main build layer is not invalidated by
# unrelated plugin source changes.
#
# Two runtime variants:
# Default (bookworm): docker build .
# Slim (bookworm-slim): docker build --build-arg OPENCLAW_VARIANT=slim .
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_VARIANT=default
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_DOCKER_APT_UPGRADE=1
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_DIGEST="sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
# Base images are pinned to SHA256 digests for reproducible builds.
# Trade-off: digests must be updated manually when upstream tags move.
# To update, run: docker buildx imagetools inspect node:24-bookworm (or podman)
# and replace the digest below with the current multi-arch manifest list entry.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
COPY ${OPENCLAW_BUNDLED_PLUGIN_DIR} /tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}
# Copy package.json for opted-in extensions so pnpm resolves their deps.
RUN mkdir -p /out && \
for ext in $OPENCLAW_EXTENSIONS; do \
if [ -f "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" ]; then \
mkdir -p "/out/$ext" && \
cp "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" "/out/$ext/package.json"; \
fi; \
done
# ── Stage 2: Build ──────────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# Install Bun (required for build scripts). Retry the whole bootstrap flow to
# tolerate transient 5xx failures from bun.sh/GitHub during CI image builds.
RUN set -eux; \
for attempt in 1 2 3 4 5; do \
if curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL https://bun.sh/install | bash; then \
break; \
fi; \
if [ "$attempt" -eq 5 ]; then \
exit 1; \
fi; \
sleep $((attempt * 2)); \
done
# Install Bun (required for build scripts)
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
RUN corepack enable
WORKDIR /app
ARG CLAWDBOT_DOCKER_APT_PACKAGES=""
RUN if [ -n "$CLAWDBOT_DOCKER_APT_PACKAGES" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $CLAWDBOT_DOCKER_APT_PACKAGES && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts ./scripts
COPY --from=ext-deps /out/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
# Reduce OOM risk on low-memory hosts during dependency installation.
# Docker builds on small VMs may otherwise fail with "Killed" (exit 137).
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Fail fast here if the Matrix native binding did not materialize after install.
RUN echo "==> Verifying critical native addons..." && \
find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q . || \
(echo "ERROR: matrix-sdk-crypto native addon missing (pnpm install may have silently failed on this arch)" >&2 && exit 1)
RUN pnpm install --frozen-lockfile
COPY . .
# Normalize extension paths now so runtime COPY preserves safe modes
# without adding a second full extensions layer.
RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do \
if [ -d "$dir" ]; then \
find "$dir" -type d -exec chmod 755 {} +; \
find "$dir" -type f -exec chmod 644 {} +; \
fi; \
done
# A2UI bundle may fail under QEMU cross-compilation (e.g. building amd64
# on Apple Silicon). CI builds natively per-arch so this is a no-op there.
# Stub it so local cross-arch builds still succeed.
RUN pnpm canvas:a2ui:bundle || \
(echo "A2UI bundle: creating stub (non-fatal)" && \
mkdir -p src/canvas-host/a2ui && \
echo "/* A2UI bundle unavailable in this build */" > src/canvas-host/a2ui/a2ui.bundle.js && \
echo "stub" > src/canvas-host/a2ui/.bundle.hash && \
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
RUN pnpm build:docker
RUN pnpm build
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1
ENV CLAWDBOT_PREFER_PNPM=1
RUN pnpm ui:install
RUN pnpm ui:build
RUN pnpm qa:lab:build
# Prune dev dependencies and strip build-only metadata before copying
# runtime assets into the final image.
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# Keep the install layer frozen, but allow prune to run against the full copied
# workspace tree subset used during `pnpm install`. The build stage only copied
# the root, `ui`, and opted-in plugin manifests into the install layer, so
# prune must not rediscover unrelated workspaces from the later full source
# copy.
RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
for ext in $OPENCLAW_EXTENSIONS; do \
printf ' - %s/%s\n' "$OPENCLAW_BUNDLED_PLUGIN_DIR" "$ext" >> /tmp/pnpm-workspace.runtime.yaml; \
done && \
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default
ARG OPENCLAW_NODE_BOOKWORM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_DIGEST}"
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm-slim" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST}"
# ── Stage 3: Runtime ────────────────────────────────────────────
FROM base-${OPENCLAW_VARIANT}
ARG OPENCLAW_VARIANT
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_DOCKER_APT_UPGRADE
# OCI base-image metadata for downstream image consumers.
# If you change these annotations, also update:
# - docs/install/docker.md ("Base image metadata" section)
# - https://docs.openclaw.ai/install/docker
LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
org.opencontainers.image.url="https://openclaw.ai" \
org.opencontainers.image.documentation="https://docs.openclaw.ai/install/docker" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="OpenClaw" \
org.opencontainers.image.description="OpenClaw gateway and CLI runtime container image"
WORKDIR /app
# Install system utilities present in bookworm but missing in bookworm-slim.
# On the full bookworm image these are already installed (apt-get is a no-op).
# Smoke workflows can opt out of distro upgrades to cut repeated CI time while
# keeping the default runtime image behavior unchanged.
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update && \
if [ "${OPENCLAW_DOCKER_APT_UPGRADE}" != "0" ]; then \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends; \
fi && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
procps hostname curl git lsof openssl
RUN chown node:node /app
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
COPY --from=runtime-assets --chown=node:node /app/package.json .
COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
COPY --from=runtime-assets --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
COPY --from=runtime-assets --chown=node:node /app/qa ./qa
# Keep pnpm available in the runtime image for container-local workflows.
# Use a shared Corepack home so the non-root `node` user does not need a
# first-run network fetch when invoking pnpm.
ENV COREPACK_HOME=/usr/local/share/corepack
RUN install -d -m 0755 "$COREPACK_HOME" && \
corepack enable && \
for attempt in 1 2 3 4 5; do \
if corepack prepare "$(node -p "require('./package.json').packageManager")" --activate; then \
break; \
fi; \
if [ "$attempt" -eq 5 ]; then \
exit 1; \
fi; \
sleep $((attempt * 2)); \
done && \
chmod -R a+rX "$COREPACK_HOME"
# Install additional system packages needed by your skills or extensions.
# Example: docker build --build-arg OPENCLAW_DOCKER_APT_PACKAGES="python3 wget" .
ARG OPENCLAW_DOCKER_APT_PACKAGES=""
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES; \
fi
# Optionally install Chromium and Xvfb for browser automation.
# Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ...
# Adds ~300MB but eliminates the 60-90s Playwright install on every container start.
# Must run after node_modules COPY so playwright-core is available.
ARG OPENCLAW_INSTALL_BROWSER=""
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
mkdir -p /home/node/.cache/ms-playwright && \
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
chown -R node:node /home/node/.cache/ms-playwright; \
fi
# Optionally install Docker CLI for sandbox container management.
# Build with: docker build --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1 ...
# Adds ~50MB. Only the CLI is installed — no Docker daemon.
# Required for agents.defaults.sandbox to function in Docker deployments.
ARG OPENCLAW_INSTALL_DOCKER_CLI=""
ARG OPENCLAW_DOCKER_GPG_FINGERPRINT="9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl gnupg && \
install -m 0755 -d /etc/apt/keyrings && \
# Verify Docker apt signing key fingerprint before trusting it as a root key.
# Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys.
curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
exit 1; \
fi && \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg.asc && \
rm -f /tmp/docker.gpg.asc && \
chmod a+r /etc/apt/keyrings/docker.gpg && \
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable\n' \
"$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
docker-ce-cli docker-compose-plugin; \
fi
# Expose the CLI binary without requiring npm global writes as non-root.
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
ENV NODE_ENV=production
# Security hardening: Run as non-root user
# The node:24-bookworm image includes a 'node' user (uid 1000)
# This reduces the attack surface by preventing container escape via root privileges
USER node
# Start gateway server with default config.
# Binds to loopback (127.0.0.1) by default for security.
#
# IMPORTANT: With Docker bridge networking (-p 18789:18789), loopback bind
# makes the gateway unreachable from the host. Either:
# - Use --network host, OR
# - Override --bind to "lan" (0.0.0.0) and set auth credentials
#
# Built-in probe endpoints for container health checks:
# - GET /healthz (liveness) and GET /readyz (readiness)
# - aliases: /health and /ready
# For external access from host/ingress, override bind to "lan" and set auth.
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
CMD ["node", "dist/index.js"]

View File

@@ -1,13 +1,8 @@
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
@@ -15,10 +10,7 @@ RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/
git \
jq \
python3 \
ripgrep
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox
WORKDIR /home/sandbox
ripgrep \
&& rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]

View File

@@ -1,20 +1,14 @@
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
chromium \
curl \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
git \
jq \
@@ -23,14 +17,12 @@ RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/
socat \
websockify \
x11vnc \
xvfb
xvfb \
&& rm -rf /var/lib/apt/lists/*
COPY --chmod=755 scripts/sandbox-browser-entrypoint.sh /usr/local/bin/openclaw-sandbox-browser
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox
WORKDIR /home/sandbox
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdbot-sandbox-browser
RUN chmod +x /usr/local/bin/clawdbot-sandbox-browser
EXPOSE 9222 5900 6080
CMD ["openclaw-sandbox-browser"]
CMD ["clawdbot-sandbox-browser"]

View File

@@ -1,48 +0,0 @@
# syntax=docker/dockerfile:1.7
ARG BASE_IMAGE=openclaw-sandbox:bookworm-slim
FROM ${BASE_IMAGE}
USER root
ENV DEBIAN_FRONTEND=noninteractive
ARG PACKAGES="curl wget jq coreutils grep nodejs npm python3 git ca-certificates golang-go rustc cargo unzip pkg-config libasound2-dev build-essential file"
ARG INSTALL_PNPM=1
ARG INSTALL_BUN=1
ARG BUN_INSTALL_DIR=/opt/bun
ARG INSTALL_BREW=1
ARG BREW_INSTALL_DIR=/home/linuxbrew/.linuxbrew
ARG FINAL_USER=sandbox
ENV BUN_INSTALL=${BUN_INSTALL_DIR}
ENV HOMEBREW_PREFIX=${BREW_INSTALL_DIR}
ENV HOMEBREW_CELLAR=${BREW_INSTALL_DIR}/Cellar
ENV HOMEBREW_REPOSITORY=${BREW_INSTALL_DIR}/Homebrew
ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin:${PATH}
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends ${PACKAGES}
RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm; fi
RUN if [ "${INSTALL_BUN}" = "1" ]; then \
curl -fsSL https://bun.sh/install | bash; \
ln -sf "${BUN_INSTALL_DIR}/bin/bun" /usr/local/bin/bun; \
fi
RUN if [ "${INSTALL_BREW}" = "1" ]; then \
if ! id -u linuxbrew >/dev/null 2>&1; then useradd -m -s /bin/bash linuxbrew; fi; \
mkdir -p "${BREW_INSTALL_DIR}"; \
chown -R linuxbrew:linuxbrew "$(dirname "${BREW_INSTALL_DIR}")"; \
su - linuxbrew -c "NONINTERACTIVE=1 CI=1 /bin/bash -c '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'"; \
if [ ! -e "${BREW_INSTALL_DIR}/Library" ]; then ln -s "${BREW_INSTALL_DIR}/Homebrew/Library" "${BREW_INSTALL_DIR}/Library"; fi; \
if [ ! -x "${BREW_INSTALL_DIR}/bin/brew" ]; then echo \"brew install failed\"; exit 1; fi; \
ln -sf "${BREW_INSTALL_DIR}/bin/brew" /usr/local/bin/brew; \
fi
# Default is sandbox, but allow BASE_IMAGE overrides to select another final user.
USER ${FINAL_USER}

View File

@@ -1,52 +0,0 @@
# OpenClaw Incident Response Plan
## 1. Detection and triage
We monitor security signals from:
- GitHub Security Advisories (GHSA) and private vulnerability reports.
- Public GitHub issues/discussions when reports are not sensitive.
- Automated signals (for example Dependabot, CodeQL, npm advisories, and secret scanning).
Initial triage:
1. Confirm affected component, version, and trust boundary impact.
2. Classify as security issue vs hardening/no-action using the repository `SECURITY.md` scope and out-of-scope rules.
3. An incident owner responds accordingly.
## 2. Assessment
Severity guide:
- **Critical:** Package/release/repository compromise, active exploitation, or unauthenticated trust-boundary bypass with high-impact control or data exposure.
- **High:** Verified trust-boundary bypass requiring limited preconditions (for example authenticated but unauthorized high-impact action), or exposure of OpenClaw-owned sensitive credentials.
- **Medium:** Significant security weakness with practical impact but constrained exploitability or substantial prerequisites.
- **Low:** Defense-in-depth findings, narrowly scoped denial-of-service, or hardening/parity gaps without a demonstrated trust-boundary bypass.
## 3. Response
1. Acknowledge receipt to the reporter (private when sensitive).
2. Reproduce on supported releases and latest `main`, then implement and validate a patch with regression coverage.
3. For critical/high incidents, prepare patched release(s) as fast as practical.
4. For medium/low incidents, patch in normal release flow and document mitigation guidance.
## 4. Communication
We communicate through:
- GitHub Security Advisories in the affected repository.
- Release notes/changelog entries for fixed versions.
- Direct reporter follow-up on status and resolution.
Disclosure policy:
- Critical/high incidents should receive coordinated disclosure, with CVE issuance when appropriate.
- Low-risk hardening findings may be documented in release notes or advisories without CVE, depending on impact and user exposure.
## 5. Recovery and follow-up
After shipping the fix:
1. Verify remediations in CI and release artifacts.
2. Run a short post-incident review (timeline, root cause, detection gap, prevention plan).
3. Add follow-up hardening/tests/docs tasks and track them to completion.

View File

@@ -1,4 +0,0 @@
.PHONY: build
build:
pnpm build

BIN
README-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

878
README.md
View File

@@ -1,10 +1,7 @@
# 🦞 OpenClaw — Personal AI Assistant
# 🦞 Clawdbot — Personal AI Assistant
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.svg" alt="OpenClaw" width="500">
</picture>
<img src="https://raw.githubusercontent.com/clawdbot/clawdbot/main/docs/whatsapp-clawd.jpg" alt="Clawdbot" width="400">
</p>
<p align="center">
@@ -12,221 +9,67 @@
</p>
<p align="center">
<a href="https://github.com/openclaw/openclaw/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/openclaw/openclaw/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/openclaw/openclaw/releases"><img src="https://img.shields.io/github/v/release/openclaw/openclaw?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://github.com/clawdbot/clawdbot/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/clawdbot/clawdbot/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/clawdbot/clawdbot/releases"><img src="https://img.shields.io/github/v/release/clawdbot/clawdbot?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://deepwiki.com/clawdbot/clawdbot"><img src="https://img.shields.io/badge/DeepWiki-clawdbot-111111?style=for-the-badge" alt="DeepWiki"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p>
**OpenClaw** is a _personal AI assistant_ you run on your own devices.
It answers you on the channels you already use. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
**Clawdbot** is a *personal AI assistant* you run on your own devices.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat.
[Website](https://clawdbot.com) · [Docs](https://docs.clawd.bot) · [Getting Started](https://docs.clawd.bot/start/getting-started) · [Updating](https://docs.clawd.bot/install/updating) · [Showcase](https://docs.clawd.bot/start/showcase) · [FAQ](https://docs.clawd.bot/start/faq) · [Wizard](https://docs.clawd.bot/start/wizard) · [Nix](https://github.com/clawdbot/nix-clawdbot) · [Docker](https://docs.clawd.bot/install/docker) · [Discord](https://discord.gg/clawd)
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
Preferred setup: run `openclaw onboard` in your terminal.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
## Sponsors
<table>
<tr>
<td align="center" width="16.66%">
<a href="https://openai.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/openai-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/openai.svg" alt="OpenAI" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://github.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github.svg" alt="GitHub" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://www.nvidia.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/nvidia.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/nvidia-dark.svg" alt="NVIDIA" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://vercel.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/vercel-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/vercel.svg" alt="Vercel" height="24">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://blacksmith.sh/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/blacksmith-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/blacksmith.svg" alt="Blacksmith" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://www.convex.dev/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/convex-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/convex.svg" alt="Convex" height="24">
</picture>
</a>
</td>
</tr>
</table>
New install? Start here: [Getting started](https://docs.clawd.bot/start/getting-started)
**Subscriptions (OAuth):**
- **[Anthropic](https://www.anthropic.com/)** (Claude Pro/Max)
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for longcontext strength and better promptinjection resistance. See [Onboarding](https://docs.clawd.bot/start/onboarding).
## Models (selection + auth)
- Models config + CLI: [Models](https://docs.clawd.bot/concepts/models)
- Auth profile rotation (OAuth vs API keys) + fallbacks: [Model failover](https://docs.clawd.bot/concepts/model-failover)
## Install (recommended)
Runtime: **Node 24 (recommended) or Node 22.16+**.
Runtime: **Node ≥22**.
```bash
npm install -g openclaw@latest
# or: pnpm add -g openclaw@latest
npm install -g clawdbot@latest
# or: pnpm add -g clawdbot@latest
openclaw onboard --install-daemon
clawdbot onboard --install-daemon
```
OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so it stays running.
The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running.
## Quick start (TL;DR)
Runtime: **Node 24 (recommended) or Node 22.16+**.
Runtime: **Node ≥22**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.clawd.bot/start/getting-started)
```bash
openclaw onboard --install-daemon
clawdbot onboard --install-daemon
openclaw gateway --port 18789 --verbose
clawdbot gateway --port 18789 --verbose
# Send a message
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
clawdbot message send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/QQ/WebChat)
openclaw agent --message "Ship checklist" --thinking high
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Signal/iMessage/BlueBubbles/Microsoft Teams/Matrix/Zalo/Zalo Personal/WebChat)
clawdbot agent --message "Ship checklist" --thinking high
```
Upgrading? [Updating guide](https://docs.openclaw.ai/install/updating) (and run `openclaw doctor`).
Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models). Auth profile rotation + fallbacks: [Model failover](https://docs.openclaw.ai/concepts/model-failover).
## Security defaults (DM access)
OpenClaw connects to real messaging surfaces. Treat inbound DMs as **untrusted input**.
Full security guide: [Security](https://docs.openclaw.ai/gateway/security)
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dmPolicy="pairing"` / `channels.slack.dmPolicy="pairing"`; legacy: `channels.discord.dm.policy`, `channels.slack.dm.policy`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `openclaw pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.allowFrom` / `channels.slack.allowFrom`; legacy: `channels.discord.dm.allowFrom`, `channels.slack.dm.allowFrom`).
Run `openclaw doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
## Security model (important)
- Default: tools run on the host for the `main` session, so the agent has full access when it is just you.
- Group/channel safety: set `agents.defaults.sandbox.mode: "non-main"` to run non-`main` sessions inside per-session Docker sandboxes.
- Typical sandbox default: allow `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; deny `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
- Before exposing anything remotely, read [Security](https://docs.openclaw.ai/gateway/security), [Docker sandboxing](https://docs.openclaw.ai/install/docker), and [Configuration](https://docs.openclaw.ai/gateway/configuration).
## Operator quick refs
- Chat commands: `/status`, `/new`, `/reset`, `/compact`, `/think <level>`, `/verbose on|off`, `/trace on|off`, `/usage off|tokens|full`, `/restart`, `/activation mention|always`
- Session tools: `sessions_list`, `sessions_history`, `sessions_send`
- Skills registry: [ClawHub](https://clawhub.com)
- Architecture overview: [Architecture](https://docs.openclaw.ai/concepts/architecture)
## Docs by goal
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Docker sandboxing](https://docs.openclaw.ai/install/docker)
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)
- Internals: [Architecture](https://docs.openclaw.ai/concepts/architecture), [Agent](https://docs.openclaw.ai/concepts/agent), [Session model](https://docs.openclaw.ai/concepts/session), [Gateway protocol](https://docs.openclaw.ai/reference/rpc)
- Troubleshooting: [Channel troubleshooting](https://docs.openclaw.ai/channels/troubleshooting), [Logging](https://docs.openclaw.ai/logging), [Docs home](https://docs.openclaw.ai)
## Apps (optional)
The Gateway alone delivers a great experience. All apps are optional and add extra features.
If you plan to build/run companion apps, follow the platform runbooks below.
### macOS (OpenClaw.app) (optional)
- Menu bar control for the Gateway and health.
- Voice Wake + push-to-talk overlay.
- WebChat + debug tools.
- Remote gateway control over SSH.
Note: signed builds required for macOS permissions to stick across rebuilds (see [macOS Permissions](https://docs.openclaw.ai/platforms/mac/permissions)).
### iOS node (optional)
- Pairs as a node over the Gateway WebSocket (device pairing).
- Voice trigger forwarding + Canvas surface.
- Controlled via `openclaw nodes …`.
Runbook: [iOS connect](https://docs.openclaw.ai/platforms/ios).
### Android node (optional)
- Pairs as a WS node via device pairing (`openclaw devices ...`).
- Exposes Connect/Chat/Voice tabs plus Canvas, Camera, Screen capture, and Android device command families.
- Runbook: [Android connect](https://docs.openclaw.ai/platforms/android).
## From source (development)
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm openclaw onboard --install-daemon
# Dev loop (auto-reload on source/config changes)
pnpm gateway:watch
```
Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `openclaw` binary.
Upgrading? [Updating guide](https://docs.clawd.bot/install/updating) (and run `clawdbot doctor`).
## Development channels
@@ -234,301 +77,430 @@ Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` pro
- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing).
- **dev**: moving head of `main`, npm dist-tag `dev` (when published).
Switch channels (git + npm): `openclaw update --channel stable|beta|dev`.
Details: [Development channels](https://docs.openclaw.ai/install/development-channels).
Switch channels (git + npm): `clawdbot update --channel stable|beta|dev`.
Details: [Development channels](https://docs.clawd.bot/install/development-channels).
## From source (development)
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm clawdbot onboard --install-daemon
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
```
Note: `pnpm clawdbot ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `clawdbot` binary.
## Security defaults (DM access)
Clawdbot connects to real messaging surfaces. Treat inbound DMs as **untrusted input**.
Full security guide: [Security](https://docs.clawd.bot/gateway/security)
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `clawdbot pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
Run `clawdbot doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.clawd.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.clawd.bot/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.clawd.bot/nodes).
- **[Onboarding](https://docs.clawd.bot/start/wizard) + [skills](https://docs.clawd.bot/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=clawdbot/clawdbot&type=date&legend=top-left)](https://www.star-history.com/#clawdbot/clawdbot&type=date&legend=top-left)
## Everything we built so far
### Core platform
- [Gateway WS control plane](https://docs.clawd.bot/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.clawd.bot/web), and [Canvas host](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui).
- [CLI surface](https://docs.clawd.bot/tools/agent-send): gateway, agent, send, [wizard](https://docs.clawd.bot/start/wizard), and [doctor](https://docs.clawd.bot/gateway/doctor).
- [Pi agent runtime](https://docs.clawd.bot/concepts/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.clawd.bot/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawd.bot/concepts/groups).
- [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio).
### Channels
- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [BlueBubbles](https://docs.clawd.bot/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (extension), [Matrix](https://docs.clawd.bot/channels/matrix) (extension), [Zalo](https://docs.clawd.bot/channels/zalo) (extension), [Zalo Personal](https://docs.clawd.bot/channels/zalouser) (extension), [WebChat](https://docs.clawd.bot/web/webchat).
- [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.clawd.bot/channels).
### Apps + nodes
- [macOS app](https://docs.clawd.bot/platforms/macos): menu bar control plane, [Voice Wake](https://docs.clawd.bot/nodes/voicewake)/PTT, [Talk Mode](https://docs.clawd.bot/nodes/talk) overlay, [WebChat](https://docs.clawd.bot/web/webchat), debug tools, [remote gateway](https://docs.clawd.bot/gateway/remote) control.
- [iOS node](https://docs.clawd.bot/platforms/ios): [Canvas](https://docs.clawd.bot/platforms/mac/canvas), [Voice Wake](https://docs.clawd.bot/nodes/voicewake), [Talk Mode](https://docs.clawd.bot/nodes/talk), camera, screen recording, Bonjour pairing.
- [Android node](https://docs.clawd.bot/platforms/android): [Canvas](https://docs.clawd.bot/platforms/mac/canvas), [Talk Mode](https://docs.clawd.bot/nodes/talk), camera, screen recording, optional SMS.
- [macOS node mode](https://docs.clawd.bot/nodes): system.run/notify + canvas/camera exposure.
### Tools + automation
- [Browser control](https://docs.clawd.bot/tools/browser): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://docs.clawd.bot/platforms/mac/canvas): [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://docs.clawd.bot/nodes): camera snap/clip, screen record, [location.get](https://docs.clawd.bot/nodes/location-command), notifications.
- [Cron + wakeups](https://docs.clawd.bot/automation/cron-jobs); [webhooks](https://docs.clawd.bot/automation/webhook); [Gmail Pub/Sub](https://docs.clawd.bot/automation/gmail-pubsub).
- [Skills platform](https://docs.clawd.bot/tools/skills): bundled, managed, and workspace skills with install gating + UI.
### Runtime + safety
- [Channel routing](https://docs.clawd.bot/concepts/channel-routing), [retry policy](https://docs.clawd.bot/concepts/retry), and [streaming/chunking](https://docs.clawd.bot/concepts/streaming).
- [Presence](https://docs.clawd.bot/concepts/presence), [typing indicators](https://docs.clawd.bot/concepts/typing-indicators), and [usage tracking](https://docs.clawd.bot/concepts/usage-tracking).
- [Models](https://docs.clawd.bot/concepts/models), [model failover](https://docs.clawd.bot/concepts/model-failover), and [session pruning](https://docs.clawd.bot/concepts/session-pruning).
- [Security](https://docs.clawd.bot/gateway/security) and [troubleshooting](https://docs.clawd.bot/channels/troubleshooting).
### Ops + packaging
- [Control UI](https://docs.clawd.bot/web) + [WebChat](https://docs.clawd.bot/web/webchat) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://docs.clawd.bot/gateway/tailscale) or [SSH tunnels](https://docs.clawd.bot/gateway/remote) with token/password auth.
- [Nix mode](https://docs.clawd.bot/install/nix) for declarative config; [Docker](https://docs.clawd.bot/install/docker)-based installs.
- [Doctor](https://docs.clawd.bot/gateway/doctor) migrations, [logging](https://docs.clawd.bot/logging).
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / BlueBubbles / Microsoft Teams / Matrix / Zalo / Zalo Personal / WebChat
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ ws://127.0.0.1:18789 │
└──────────────┬────────────────┘
├─ Pi agent (RPC)
├─ CLI (clawdbot …)
├─ WebChat UI
├─ macOS app
└─ iOS / Android nodes
```
## Key subsystems
- **[Gateway WebSocket network](https://docs.clawd.bot/concepts/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.clawd.bot/gateway)).
- **[Tailscale exposure](https://docs.clawd.bot/gateway/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.clawd.bot/gateway/remote)).
- **[Browser control](https://docs.clawd.bot/tools/browser)** — clawdmanaged Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://docs.clawd.bot/platforms/mac/canvas)** — agentdriven visual workspace (A2UI host: [Canvas/A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui)).
- **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — alwayson speech and continuous conversation.
- **[Nodes](https://docs.clawd.bot/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOSonly `system.run`/`system.notify`.
## Tailscale access (Gateway dashboard)
Clawdbot can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`:
- `off`: no Tailscale automation (default).
- `serve`: tailnet-only HTTPS via `tailscale serve` (uses Tailscale identity headers by default).
- `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth).
Notes:
- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (Clawdbot enforces this).
- Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`.
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
- Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown.
Details: [Tailscale guide](https://docs.clawd.bot/gateway/tailscale) · [Web surfaces](https://docs.clawd.bot/web)
## Remote Gateway (Linux is great)
Its perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute devicelocal actions when needed.
- **Gateway host** runs the exec tool and channel connections by default.
- **Device nodes** run devicelocal actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: exec runs where the Gateway lives; device actions run where the device lives.
Details: [Remote access](https://docs.clawd.bot/gateway/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/gateway/security)
## macOS permissions via the Gateway protocol
The macOS app can run in **node mode** and advertises its capabilities + permission map over the Gateway WebSocket (`node.list` / `node.describe`). Clients can then execute local actions via `node.invoke`:
- `system.run` runs a local command and returns stdout/stderr/exit code; set `needsScreenRecording: true` to require screen-recording permission (otherwise youll get `PERMISSION_MISSING`).
- `system.notify` posts a user notification and fails if notifications are denied.
- `canvas.*`, `camera.*`, `screen.record`, and `location.get` are also routed via `node.invoke` and follow TCC permission status.
Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle persession elevated access when enabled + allowlisted.
- Gateway persists the persession toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
Details: [Nodes](https://docs.clawd.bot/nodes) · [macOS app](https://docs.clawd.bot/platforms/macos) · [Gateway protocol](https://docs.clawd.bot/concepts/architecture)
## Agent to Agent (sessions_* tools)
- Use these to coordinate work across sessions without jumping between chat surfaces.
- `sessions_list` — discover active sessions (agents) and their metadata.
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional replyback pingpong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
Details: [Session tools](https://docs.clawd.bot/concepts/session-tool)
## Skills registry (ClawdHub)
ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can search for skills automatically and pull in new ones as needed.
[ClawdHub](https://ClawdHub.com)
## Chat commands
Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands are owner-only):
- `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only)
- `/verbose on|off`
- `/usage off|tokens|full` — per-response usage footer
- `/restart` — restart the gateway (owner-only in groups)
- `/activation mention|always` — group activation toggle (groups only)
## Apps (optional)
The Gateway alone delivers a great experience. All apps are optional and add extra features.
If you plan to build/run companion apps, follow the platform runbooks below.
### macOS (Clawdbot.app) (optional)
- Menu bar control for the Gateway and health.
- Voice Wake + push-to-talk overlay.
- WebChat + debug tools.
- Remote gateway control over SSH.
Note: signed builds required for macOS permissions to stick across rebuilds (see `docs/mac/permissions.md`).
### iOS node (optional)
- Pairs as a node via the Bridge.
- Voice trigger forwarding + Canvas surface.
- Controlled via `clawdbot nodes …`.
Runbook: [iOS connect](https://docs.clawd.bot/platforms/ios).
### Android node (optional)
- Pairs via the same Bridge + pairing flow as iOS.
- Exposes Canvas, Camera, and Screen capture commands.
- Runbook: [Android connect](https://docs.clawd.bot/platforms/android).
## Agent workspace + skills
- Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`).
- Workspace root: `~/clawd` (configurable via `agents.defaults.workspace`).
- Injected prompt files: `AGENTS.md`, `SOUL.md`, `TOOLS.md`.
- Skills: `~/.openclaw/workspace/skills/<skill>/SKILL.md`.
- Skills: `~/clawd/skills/<skill>/SKILL.md`.
## Configuration
Minimal `~/.openclaw/openclaw.json` (model + defaults):
Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
```json5
{
agent: {
model: "<provider>/<model-id>",
},
model: "anthropic/claude-opus-4-5"
}
}
```
[Full configuration reference (all keys + examples).](https://docs.openclaw.ai/gateway/configuration)
[Full configuration reference (all keys + examples).](https://docs.clawd.bot/gateway/configuration)
## Star History
## Security model (important)
[![Star History Chart](https://api.star-history.com/svg?repos=openclaw/openclaw&type=date&legend=top-left)](https://www.star-history.com/#openclaw/openclaw&type=date&legend=top-left)
- **Default:** tools run on the host for the **main** session, so the agent has full access when its just you.
- **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **nonmain sessions** (groups/channels) inside persession Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
## Molty
Details: [Security guide](https://docs.clawd.bot/gateway/security) · [Docker + sandboxing](https://docs.clawd.bot/install/docker) · [Sandbox config](https://docs.clawd.bot/gateway/configuration)
OpenClaw was built for **Molty**, a space lobster AI assistant. 🦞
### [WhatsApp](https://docs.clawd.bot/channels/whatsapp)
- Link the device: `pnpm clawdbot channels login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `channels.whatsapp.allowFrom`.
- If `channels.whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Telegram](https://docs.clawd.bot/channels/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins).
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed.
```json5
{
channels: {
telegram: {
botToken: "123456:ABCDEF"
}
}
}
```
### [Slack](https://docs.clawd.bot/channels/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `channels.slack.botToken` + `channels.slack.appToken`).
### [Discord](https://docs.clawd.bot/channels/discord)
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
```json5
{
channels: {
discord: {
token: "1234abcd"
}
}
}
```
### [Signal](https://docs.clawd.bot/channels/signal)
- Requires `signal-cli` and a `channels.signal` config section.
### [iMessage](https://docs.clawd.bot/channels/imessage)
- macOS only; Messages must be signed in.
- If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Microsoft Teams](https://docs.clawd.bot/channels/msteams)
- Configure a Teams app + Bot Framework, then add a `msteams` config section.
- Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`.
### [WebChat](https://docs.clawd.bot/web/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
{
browser: {
enabled: true,
controlUrl: "http://127.0.0.1:18791",
color: "#FF4500"
}
}
```
## Docs
Use these when youre past the onboarding flow and want the deeper reference.
- [Start with the docs index for navigation and “whats where.”](https://docs.clawd.bot)
- [Read the architecture overview for the gateway + protocol model.](https://docs.clawd.bot/concepts/architecture)
- [Use the full configuration reference when you need every key and example.](https://docs.clawd.bot/gateway/configuration)
- [Run the Gateway by the book with the operational runbook.](https://docs.clawd.bot/gateway)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.clawd.bot/web)
- [Understand remote access over SSH tunnels or tailnets.](https://docs.clawd.bot/gateway/remote)
- [Follow the onboarding wizard flow for a guided setup.](https://docs.clawd.bot/start/wizard)
- [Wire external triggers via the webhook surface.](https://docs.clawd.bot/automation/webhook)
- [Set up Gmail Pub/Sub triggers.](https://docs.clawd.bot/automation/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.clawd.bot/platforms/mac/menu-bar)
- [Platform guides: Windows (WSL2)](https://docs.clawd.bot/platforms/windows), [Linux](https://docs.clawd.bot/platforms/linux), [macOS](https://docs.clawd.bot/platforms/macos), [iOS](https://docs.clawd.bot/platforms/ios), [Android](https://docs.clawd.bot/platforms/android)
- [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/channels/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.clawd.bot/gateway/security)
## Advanced docs (discovery + control)
- [Discovery + transports](https://docs.clawd.bot/gateway/discovery)
- [Bonjour/mDNS](https://docs.clawd.bot/gateway/bonjour)
- [Gateway pairing](https://docs.clawd.bot/gateway/pairing)
- [Remote gateway README](https://docs.clawd.bot/gateway/remote-gateway-readme)
- [Control UI](https://docs.clawd.bot/web/control-ui)
- [Dashboard](https://docs.clawd.bot/web/dashboard)
## Operations & troubleshooting
- [Health checks](https://docs.clawd.bot/gateway/health)
- [Gateway lock](https://docs.clawd.bot/gateway/gateway-lock)
- [Background process](https://docs.clawd.bot/gateway/background-process)
- [Browser troubleshooting (Linux)](https://docs.clawd.bot/tools/browser-linux-troubleshooting)
- [Logging](https://docs.clawd.bot/logging)
## Deep dives
- [Agent loop](https://docs.clawd.bot/concepts/agent-loop)
- [Presence](https://docs.clawd.bot/concepts/presence)
- [TypeBox schemas](https://docs.clawd.bot/concepts/typebox)
- [RPC adapters](https://docs.clawd.bot/reference/rpc)
- [Queue](https://docs.clawd.bot/concepts/queue)
## Workspace & skills
- [Skills config](https://docs.clawd.bot/tools/skills-config)
- [Default AGENTS](https://docs.clawd.bot/reference/AGENTS.default)
- [Templates: AGENTS](https://docs.clawd.bot/reference/templates/AGENTS)
- [Templates: BOOTSTRAP](https://docs.clawd.bot/reference/templates/BOOTSTRAP)
- [Templates: IDENTITY](https://docs.clawd.bot/reference/templates/IDENTITY)
- [Templates: SOUL](https://docs.clawd.bot/reference/templates/SOUL)
- [Templates: TOOLS](https://docs.clawd.bot/reference/templates/TOOLS)
- [Templates: USER](https://docs.clawd.bot/reference/templates/USER)
## Platform internals
- [macOS dev setup](https://docs.clawd.bot/platforms/mac/dev-setup)
- [macOS menu bar](https://docs.clawd.bot/platforms/mac/menu-bar)
- [macOS voice wake](https://docs.clawd.bot/platforms/mac/voicewake)
- [iOS node](https://docs.clawd.bot/platforms/ios)
- [Android node](https://docs.clawd.bot/platforms/android)
- [Windows (WSL2)](https://docs.clawd.bot/platforms/windows)
- [Linux app](https://docs.clawd.bot/platforms/linux)
## Email hooks (Gmail)
- [docs.clawd.bot/gmail-pubsub](https://docs.clawd.bot/automation/gmail-pubsub)
## Clawd
Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞
by Peter Steinberger and the community.
- [openclaw.ai](https://openclaw.ai)
- [clawd.me](https://clawd.me)
- [soul.md](https://soul.md)
- [steipete.me](https://steipete.me)
- [@openclaw](https://x.com/openclaw)
## Community
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
[pi-mono](https://github.com/badlogic/pi-mono).
Special thanks to Adam Doppelt for the lobster.bot domain.
Thanks to all clawtributors:
<!-- clawtributors:start -->
[![steipete](https://avatars.githubusercontent.com/u/58493?v=4&s=48)](https://github.com/steipete) [![vincentkoc](https://avatars.githubusercontent.com/u/25068?v=4&s=48)](https://github.com/vincentkoc) [![Takhoffman](https://avatars.githubusercontent.com/u/781889?v=4&s=48)](https://github.com/Takhoffman) [![obviyus](https://avatars.githubusercontent.com/u/22031114?v=4&s=48)](https://github.com/obviyus) [![gumadeiras](https://avatars.githubusercontent.com/u/5599352?v=4&s=48)](https://github.com/gumadeiras) [![Mariano Belinky](https://avatars.githubusercontent.com/u/132747814?v=4&s=48)](https://github.com/mbelinky) [![vignesh07](https://avatars.githubusercontent.com/u/1436853?v=4&s=48)](https://github.com/vignesh07) [![joshavant](https://avatars.githubusercontent.com/u/830519?v=4&s=48)](https://github.com/joshavant) [![scoootscooob](https://avatars.githubusercontent.com/u/167050519?v=4&s=48)](https://github.com/scoootscooob) [![jacobtomlinson](https://avatars.githubusercontent.com/u/1610850?v=4&s=48)](https://github.com/jacobtomlinson)
[![shakkernerd](https://avatars.githubusercontent.com/u/165377636?v=4&s=48)](https://github.com/shakkernerd) [![sebslight](https://avatars.githubusercontent.com/u/19554889?v=4&s=48)](https://github.com/sebslight) [![tyler6204](https://avatars.githubusercontent.com/u/64381258?v=4&s=48)](https://github.com/tyler6204) [![ngutman](https://avatars.githubusercontent.com/u/1540134?v=4&s=48)](https://github.com/ngutman) [![thewilloftheshadow](https://avatars.githubusercontent.com/u/35580099?v=4&s=48)](https://github.com/thewilloftheshadow) [![Sid-Qin](https://avatars.githubusercontent.com/u/201593046?v=4&s=48)](https://github.com/Sid-Qin) [![mcaxtr](https://avatars.githubusercontent.com/u/7562095?v=4&s=48)](https://github.com/mcaxtr) [![eleqtrizit](https://avatars.githubusercontent.com/u/31522568?v=4&s=48)](https://github.com/eleqtrizit) [![BunsDev](https://avatars.githubusercontent.com/u/68980965?v=4&s=48)](https://github.com/BunsDev) [![cpojer](https://avatars.githubusercontent.com/u/13352?v=4&s=48)](https://github.com/cpojer)
[![Glucksberg](https://avatars.githubusercontent.com/u/80581902?v=4&s=48)](https://github.com/Glucksberg) [![osolmaz](https://avatars.githubusercontent.com/u/2453968?v=4&s=48)](https://github.com/osolmaz) [![bmendonca3](https://avatars.githubusercontent.com/u/208517100?v=4&s=48)](https://github.com/bmendonca3) [![jalehman](https://avatars.githubusercontent.com/u/550978?v=4&s=48)](https://github.com/jalehman) [![huntharo](https://avatars.githubusercontent.com/u/5617868?v=4&s=48)](https://github.com/huntharo) [![neeravmakwana](https://avatars.githubusercontent.com/u/261249544?v=4&s=48)](https://github.com/neeravmakwana) [![openperf](https://avatars.githubusercontent.com/u/80630709?v=4&s=48)](https://github.com/openperf) [![joshp123](https://avatars.githubusercontent.com/u/1497361?v=4&s=48)](https://github.com/joshp123) [![pgondhi987](https://avatars.githubusercontent.com/u/270720687?v=4&s=48)](https://github.com/pgondhi987) [![altaywtf](https://avatars.githubusercontent.com/u/9790196?v=4&s=48)](https://github.com/altaywtf)
[![quotentiroler](https://avatars.githubusercontent.com/u/40643627?v=4&s=48)](https://github.com/quotentiroler) [![liuxiaopai-ai](https://avatars.githubusercontent.com/u/73659136?v=4&s=48)](https://github.com/liuxiaopai-ai) [![rodrigouroz](https://avatars.githubusercontent.com/u/384037?v=4&s=48)](https://github.com/rodrigouroz) [![frankekn](https://avatars.githubusercontent.com/u/4488090?v=4&s=48)](https://github.com/frankekn) [![drobison00](https://avatars.githubusercontent.com/u/5256797?v=4&s=48)](https://github.com/drobison00) [![zerone0x](https://avatars.githubusercontent.com/u/39543393?v=4&s=48)](https://github.com/zerone0x) [![onutc](https://avatars.githubusercontent.com/u/152018508?v=4&s=48)](https://github.com/onutc) [![ademczuk](https://avatars.githubusercontent.com/u/5212682?v=4&s=48)](https://github.com/ademczuk) [![ImLukeF](https://avatars.githubusercontent.com/u/92253590?v=4&s=48)](https://github.com/ImLukeF) [![hydro13](https://avatars.githubusercontent.com/u/6640526?v=4&s=48)](https://github.com/hydro13)
[![hxy91819](https://avatars.githubusercontent.com/u/8814856?v=4&s=48)](https://github.com/hxy91819) [![coygeek](https://avatars.githubusercontent.com/u/65363919?v=4&s=48)](https://github.com/coygeek) [![dutifulbob](https://avatars.githubusercontent.com/u/261991368?v=4&s=48)](https://github.com/dutifulbob) [![sliverp](https://avatars.githubusercontent.com/u/38134380?v=4&s=48)](https://github.com/sliverp) [![Elonito](https://avatars.githubusercontent.com/u/190923101?v=4&s=48)](https://github.com/0xRaini) [![robbyczgw-cla](https://avatars.githubusercontent.com/u/239660374?v=4&s=48)](https://github.com/robbyczgw-cla) [![joelnishanth](https://avatars.githubusercontent.com/u/140015627?v=4&s=48)](https://github.com/joelnishanth) [![echoVic](https://avatars.githubusercontent.com/u/16428813?v=4&s=48)](https://github.com/echoVic) [![sallyom](https://avatars.githubusercontent.com/u/11166065?v=4&s=48)](https://github.com/sallyom) [![yinghaosang](https://avatars.githubusercontent.com/u/261132136?v=4&s=48)](https://github.com/yinghaosang)
[![BradGroux](https://avatars.githubusercontent.com/u/3053586?v=4&s=48)](https://github.com/BradGroux) [![christianklotz](https://avatars.githubusercontent.com/u/69443?v=4&s=48)](https://github.com/christianklotz) [![odysseus0](https://avatars.githubusercontent.com/u/8635094?v=4&s=48)](https://github.com/odysseus0) [![hclsys](https://avatars.githubusercontent.com/u/7755017?v=4&s=48)](https://github.com/hclsys) [![byungsker](https://avatars.githubusercontent.com/u/72309817?v=4&s=48)](https://github.com/byungsker) [![pashpashpash](https://avatars.githubusercontent.com/u/20898225?v=4&s=48)](https://github.com/pashpashpash) [![stakeswky](https://avatars.githubusercontent.com/u/64798754?v=4&s=48)](https://github.com/stakeswky) [![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4&s=48)](https://github.com/apps/github-actions) [![xinhuagu](https://avatars.githubusercontent.com/u/562450?v=4&s=48)](https://github.com/xinhuagu) [![MonkeyLeeT](https://avatars.githubusercontent.com/u/6754057?v=4&s=48)](https://github.com/MonkeyLeeT)
[![100yenadmin](https://avatars.githubusercontent.com/u/239388517?v=4&s=48)](https://github.com/100yenadmin) [![mcinteerj](https://avatars.githubusercontent.com/u/3613653?v=4&s=48)](https://github.com/mcinteerj) [![samzong](https://avatars.githubusercontent.com/u/13782141?v=4&s=48)](https://github.com/samzong) [![chilu18](https://avatars.githubusercontent.com/u/7957943?v=4&s=48)](https://github.com/chilu18) [![darkamenosa](https://avatars.githubusercontent.com/u/6668014?v=4&s=48)](https://github.com/darkamenosa) [![widingmarcus-cyber](https://avatars.githubusercontent.com/u/245375637?v=4&s=48)](https://github.com/widingmarcus-cyber) [![cgdusek](https://avatars.githubusercontent.com/u/38732970?v=4&s=48)](https://github.com/cgdusek) [![Lukavyi](https://avatars.githubusercontent.com/u/1013690?v=4&s=48)](https://github.com/Lukavyi) [![davidrudduck](https://avatars.githubusercontent.com/u/47308254?v=4&s=48)](https://github.com/davidrudduck) [![VACInc](https://avatars.githubusercontent.com/u/3279061?v=4&s=48)](https://github.com/VACInc)
[![MoerAI](https://avatars.githubusercontent.com/u/26067127?v=4&s=48)](https://github.com/MoerAI) [![velvet-shark](https://avatars.githubusercontent.com/u/126378?v=4&s=48)](https://github.com/velvet-shark) [![HenryLoenwind](https://avatars.githubusercontent.com/u/1485873?v=4&s=48)](https://github.com/HenryLoenwind) [![omarshahine](https://avatars.githubusercontent.com/u/10343873?v=4&s=48)](https://github.com/omarshahine) [![bohdanpodvirnyi](https://avatars.githubusercontent.com/u/31819391?v=4&s=48)](https://github.com/bohdanpodvirnyi) [![Verite Igiraneza](https://avatars.githubusercontent.com/u/69280208?v=4&s=48)](https://github.com/VeriteIgiraneza) [![akramcodez](https://avatars.githubusercontent.com/u/179671552?v=4&s=48)](https://github.com/akramcodez) [![Kaneki-x](https://avatars.githubusercontent.com/u/6857108?v=4&s=48)](https://github.com/Kaneki-x) [![aether-ai-agent](https://avatars.githubusercontent.com/u/261339948?v=4&s=48)](https://github.com/aether-ai-agent) [![joaohlisboa](https://avatars.githubusercontent.com/u/8200873?v=4&s=48)](https://github.com/joaohlisboa)
[![MaudeBot](https://avatars.githubusercontent.com/u/255777700?v=4&s=48)](https://github.com/MaudeBot) [![davidguttman](https://avatars.githubusercontent.com/u/431696?v=4&s=48)](https://github.com/davidguttman) [![justinhuangcode](https://avatars.githubusercontent.com/u/252443740?v=4&s=48)](https://github.com/justinhuangcode) [![lml2468](https://avatars.githubusercontent.com/u/39320777?v=4&s=48)](https://github.com/lml2468) [![wirjo](https://avatars.githubusercontent.com/u/165846?v=4&s=48)](https://github.com/wirjo) [![iHildy](https://avatars.githubusercontent.com/u/25069719?v=4&s=48)](https://github.com/iHildy) [![mudrii](https://avatars.githubusercontent.com/u/220262?v=4&s=48)](https://github.com/mudrii) [![advaitpaliwal](https://avatars.githubusercontent.com/u/66044327?v=4&s=48)](https://github.com/advaitpaliwal) [![czekaj](https://avatars.githubusercontent.com/u/1464539?v=4&s=48)](https://github.com/czekaj) [![dlauer](https://avatars.githubusercontent.com/u/757041?v=4&s=48)](https://github.com/dlauer)
[![Solvely-Colin](https://avatars.githubusercontent.com/u/211764741?v=4&s=48)](https://github.com/Solvely-Colin) [![feiskyer](https://avatars.githubusercontent.com/u/676637?v=4&s=48)](https://github.com/feiskyer) [![brandonwise](https://avatars.githubusercontent.com/u/21148772?v=4&s=48)](https://github.com/brandonwise) [![conroywhitney](https://avatars.githubusercontent.com/u/249891?v=4&s=48)](https://github.com/conroywhitney) [![mneves75](https://avatars.githubusercontent.com/u/2423436?v=4&s=48)](https://github.com/mneves75) [![jaydenfyi](https://avatars.githubusercontent.com/u/213395523?v=4&s=48)](https://github.com/jaydenfyi) [![davemorin](https://avatars.githubusercontent.com/u/78139?v=4&s=48)](https://github.com/davemorin) [![joeykrug](https://avatars.githubusercontent.com/u/5925937?v=4&s=48)](https://github.com/joeykrug) [![kevinWangSheng](https://avatars.githubusercontent.com/u/118158941?v=4&s=48)](https://github.com/kevinWangSheng) [![pejmanjohn](https://avatars.githubusercontent.com/u/481729?v=4&s=48)](https://github.com/pejmanjohn)
[![Lanfei](https://avatars.githubusercontent.com/u/2156642?v=4&s=48)](https://github.com/Lanfei) [![liuy](https://avatars.githubusercontent.com/u/1192888?v=4&s=48)](https://github.com/liuy) [![lc0rp](https://avatars.githubusercontent.com/u/2609441?v=4&s=48)](https://github.com/lc0rp) [![teconomix](https://avatars.githubusercontent.com/u/6959299?v=4&s=48)](https://github.com/teconomix) [![omair445](https://avatars.githubusercontent.com/u/32237905?v=4&s=48)](https://github.com/omair445) [![dorukardahan](https://avatars.githubusercontent.com/u/35905596?v=4&s=48)](https://github.com/dorukardahan) [![mmaps](https://avatars.githubusercontent.com/u/3399869?v=4&s=48)](https://github.com/mmaps) [![Tobias Bischoff](https://avatars.githubusercontent.com/u/711564?v=4&s=48)](https://github.com/tobiasbischoff) [![adhitShet](https://avatars.githubusercontent.com/u/131381638?v=4&s=48)](https://github.com/adhitShet) [![pandego](https://avatars.githubusercontent.com/u/7780875?v=4&s=48)](https://github.com/pandego)
[![bradleypriest](https://avatars.githubusercontent.com/u/167215?v=4&s=48)](https://github.com/bradleypriest) [![bjesuiter](https://avatars.githubusercontent.com/u/2365676?v=4&s=48)](https://github.com/bjesuiter) [![grp06](https://avatars.githubusercontent.com/u/1573959?v=4&s=48)](https://github.com/grp06) [![shadril238](https://avatars.githubusercontent.com/u/63901551?v=4&s=48)](https://github.com/shadril238) [![kesku](https://avatars.githubusercontent.com/u/62210496?v=4&s=48)](https://github.com/kesku) [![YuriNachos](https://avatars.githubusercontent.com/u/19365375?v=4&s=48)](https://github.com/YuriNachos) [![vrknetha](https://avatars.githubusercontent.com/u/20596261?v=4&s=48)](https://github.com/vrknetha) [![smartprogrammer93](https://avatars.githubusercontent.com/u/33181301?v=4&s=48)](https://github.com/smartprogrammer93) [![nachx639](https://avatars.githubusercontent.com/u/71144023?v=4&s=48)](https://github.com/Nachx639) [![jnMetaCode](https://avatars.githubusercontent.com/u/12096460?v=4&s=48)](https://github.com/jnMetaCode)
[![Phineas1500](https://avatars.githubusercontent.com/u/41450967?v=4&s=48)](https://github.com/Phineas1500) [![dingn42](https://avatars.githubusercontent.com/u/17723822?v=4&s=48)](https://github.com/dingn42) [![geekhuashan](https://avatars.githubusercontent.com/u/47098938?v=4&s=48)](https://github.com/geekhuashan) [![Nanako0129](https://avatars.githubusercontent.com/u/44753291?v=4&s=48)](https://github.com/Nanako0129) [![AytuncYildizli](https://avatars.githubusercontent.com/u/47717026?v=4&s=48)](https://github.com/AytuncYildizli) [![BruceMacD](https://avatars.githubusercontent.com/u/5853428?v=4&s=48)](https://github.com/BruceMacD) [![jjjojoj](https://avatars.githubusercontent.com/u/88077783?v=4&s=48)](https://github.com/jjjojoj) [![mvanhorn](https://avatars.githubusercontent.com/u/455140?v=4&s=48)](https://github.com/mvanhorn) [![bugkill3r](https://avatars.githubusercontent.com/u/2924124?v=4&s=48)](https://github.com/bugkill3r) [![rahthakor](https://avatars.githubusercontent.com/u/8470553?v=4&s=48)](https://github.com/rahthakor)
[![GodsBoy](https://avatars.githubusercontent.com/u/5792287?v=4&s=48)](https://github.com/GodsBoy) [![SARAMALI15792](https://avatars.githubusercontent.com/u/140950904?v=4&s=48)](https://github.com/SARAMALI15792) [![Radek Paclt](https://avatars.githubusercontent.com/u/50451445?v=4&s=48)](https://github.com/radek-paclt) [![Elarwei001](https://avatars.githubusercontent.com/u/168552401?v=4&s=48)](https://github.com/Elarwei001) [![ingyukoh](https://avatars.githubusercontent.com/u/6015960?v=4&s=48)](https://github.com/ingyukoh) [![SnowSky1](https://avatars.githubusercontent.com/u/126348592?v=4&s=48)](https://github.com/SnowSky1) [![lewiswigmore](https://avatars.githubusercontent.com/u/58551848?v=4&s=48)](https://github.com/lewiswigmore) [![Hiroshi Tanaka](https://avatars.githubusercontent.com/u/145330217?v=4&s=48)](https://github.com/solavrc) [![aldoeliacim](https://avatars.githubusercontent.com/u/17973757?v=4&s=48)](https://github.com/aldoeliacim) [![Jakub Rusz](https://avatars.githubusercontent.com/u/55534579?v=4&s=48)](https://github.com/jrusz)
[![Tony Dehnke](https://avatars.githubusercontent.com/u/36720180?v=4&s=48)](https://github.com/tonydehnke) [![roshanasingh4](https://avatars.githubusercontent.com/u/88576930?v=4&s=48)](https://github.com/roshanasingh4) [![zssggle-rgb](https://avatars.githubusercontent.com/u/226775494?v=4&s=48)](https://github.com/zssggle-rgb) [![adam91holt](https://avatars.githubusercontent.com/u/9592417?v=4&s=48)](https://github.com/adam91holt) [![graysurf](https://avatars.githubusercontent.com/u/10785178?v=4&s=48)](https://github.com/graysurf) [![xadenryan](https://avatars.githubusercontent.com/u/165437834?v=4&s=48)](https://github.com/xadenryan) [![sfo2001](https://avatars.githubusercontent.com/u/103369858?v=4&s=48)](https://github.com/sfo2001) [![Jamieson O'Reilly](https://avatars.githubusercontent.com/u/6668807?v=4&s=48)](https://github.com/orlyjamie) [![hsrvc](https://avatars.githubusercontent.com/u/129702169?v=4&s=48)](https://github.com/hsrvc) [![tomsun28](https://avatars.githubusercontent.com/u/24788200?v=4&s=48)](https://github.com/tomsun28)
[![BillChirico](https://avatars.githubusercontent.com/u/13951316?v=4&s=48)](https://github.com/BillChirico) [![carrotRakko](https://avatars.githubusercontent.com/u/24588751?v=4&s=48)](https://github.com/carrotRakko) [![ranausmanai](https://avatars.githubusercontent.com/u/257128159?v=4&s=48)](https://github.com/ranausmanai) [![arkyu2077](https://avatars.githubusercontent.com/u/42494191?v=4&s=48)](https://github.com/arkyu2077) [![hoyyeva](https://avatars.githubusercontent.com/u/63033505?v=4&s=48)](https://github.com/hoyyeva) [![luoyanglang](https://avatars.githubusercontent.com/u/238804951?v=4&s=48)](https://github.com/luoyanglang) [![sibbl](https://avatars.githubusercontent.com/u/866535?v=4&s=48)](https://github.com/sibbl) [![gregmousseau](https://avatars.githubusercontent.com/u/5036458?v=4&s=48)](https://github.com/gregmousseau) [![sahilsatralkar](https://avatars.githubusercontent.com/u/62758655?v=4&s=48)](https://github.com/sahilsatralkar) [![akoscz](https://avatars.githubusercontent.com/u/1360047?v=4&s=48)](https://github.com/akoscz)
[![rrenamed](https://avatars.githubusercontent.com/u/87486610?v=4&s=48)](https://github.com/rrenamed) [![YuzuruS](https://avatars.githubusercontent.com/u/1485195?v=4&s=48)](https://github.com/YuzuruS) [![Hongwei Ma](https://avatars.githubusercontent.com/u/11957602?v=4&s=48)](https://github.com/Marvae) [![mitchmcalister](https://avatars.githubusercontent.com/u/209334?v=4&s=48)](https://github.com/mitchmcalister) [![juanpablodlc](https://avatars.githubusercontent.com/u/92012363?v=4&s=48)](https://github.com/juanpablodlc) [![shtse8](https://avatars.githubusercontent.com/u/8020099?v=4&s=48)](https://github.com/shtse8) [![thebenignhacker](https://avatars.githubusercontent.com/u/32418586?v=4&s=48)](https://github.com/thebenignhacker) [![nimbleenigma](https://avatars.githubusercontent.com/u/129692390?v=4&s=48)](https://github.com/nimbleenigma) [![Linux2010](https://avatars.githubusercontent.com/u/35169750?v=4&s=48)](https://github.com/Linux2010) [![shichangs](https://avatars.githubusercontent.com/u/46870204?v=4&s=48)](https://github.com/shichangs)
[![efe-arv](https://avatars.githubusercontent.com/u/259833796?v=4&s=48)](https://github.com/efe-arv) [![Hsiao A](https://avatars.githubusercontent.com/u/70124331?v=4&s=48)](https://github.com/hsiaoa) [![nabbilkhan](https://avatars.githubusercontent.com/u/203121263?v=4&s=48)](https://github.com/nabbilkhan) [![ayanesakura](https://avatars.githubusercontent.com/u/40628300?v=4&s=48)](https://github.com/ayanesakura) [![lupuletic](https://avatars.githubusercontent.com/u/105351510?v=4&s=48)](https://github.com/lupuletic) [![polooooo](https://avatars.githubusercontent.com/u/50262693?v=4&s=48)](https://github.com/polooooo) [![xaeon2026](https://avatars.githubusercontent.com/u/264572156?v=4&s=48)](https://github.com/xaeon2026) [![shrey150](https://avatars.githubusercontent.com/u/3813908?v=4&s=48)](https://github.com/shrey150) [![taw0002](https://avatars.githubusercontent.com/u/42811278?v=4&s=48)](https://github.com/taw0002) [![dinakars777](https://avatars.githubusercontent.com/u/250428393?v=4&s=48)](https://github.com/dinakars777)
[![giulio-leone](https://avatars.githubusercontent.com/u/6887247?v=4&s=48)](https://github.com/giulio-leone) [![nyanjou](https://avatars.githubusercontent.com/u/258645604?v=4&s=48)](https://github.com/nyanjou) [![meaningfool](https://avatars.githubusercontent.com/u/2862331?v=4&s=48)](https://github.com/meaningfool) [![kunalk16](https://avatars.githubusercontent.com/u/5303824?v=4&s=48)](https://github.com/kunalk16) [![ide-rea](https://avatars.githubusercontent.com/u/30512600?v=4&s=48)](https://github.com/ide-rea) [![Jonathan Jing](https://avatars.githubusercontent.com/u/17068507?v=4&s=48)](https://github.com/JonathanJing) [![yelog](https://avatars.githubusercontent.com/u/14227866?v=4&s=48)](https://github.com/yelog) [![markmusson](https://avatars.githubusercontent.com/u/4801649?v=4&s=48)](https://github.com/markmusson) [![kiranvk-2011](https://avatars.githubusercontent.com/u/91108465?v=4&s=48)](https://github.com/kiranvk-2011) [![Sathvik Veerapaneni](https://avatars.githubusercontent.com/u/98241593?v=4&s=48)](https://github.com/Sathvik-Chowdary-Veerapaneni)
[![rogerdigital](https://avatars.githubusercontent.com/u/13251150?v=4&s=48)](https://github.com/rogerdigital) [![artwalker](https://avatars.githubusercontent.com/u/44759507?v=4&s=48)](https://github.com/artwalker) [![azade-c](https://avatars.githubusercontent.com/u/252790079?v=4&s=48)](https://github.com/azade-c) [![chinar-amrutkar](https://avatars.githubusercontent.com/u/22189135?v=4&s=48)](https://github.com/chinar-amrutkar) [![maxsumrall](https://avatars.githubusercontent.com/u/628843?v=4&s=48)](https://github.com/maxsumrall) [![Minidoracat](https://avatars.githubusercontent.com/u/11269639?v=4&s=48)](https://github.com/Minidoracat) [![unisone](https://avatars.githubusercontent.com/u/32521398?v=4&s=48)](https://github.com/unisone) [![ly85206559](https://avatars.githubusercontent.com/u/12526624?v=4&s=48)](https://github.com/ly85206559) [![Sam Padilla](https://avatars.githubusercontent.com/u/35386211?v=4&s=48)](https://github.com/theSamPadilla) [![AnonO6](https://avatars.githubusercontent.com/u/124311066?v=4&s=48)](https://github.com/AnonO6)
[![afurm](https://avatars.githubusercontent.com/u/6375192?v=4&s=48)](https://github.com/afurm) [![황재원](https://avatars.githubusercontent.com/u/91544407?v=4&s=48)](https://github.com/jwchmodx) [![Leszek Szpunar](https://avatars.githubusercontent.com/u/13106764?v=4&s=48)](https://github.com/leszekszpunar) [![Mrseenz](https://avatars.githubusercontent.com/u/101962919?v=4&s=48)](https://github.com/Mrseenz) [![Yida-Dev](https://avatars.githubusercontent.com/u/92713555?v=4&s=48)](https://github.com/Yida-Dev) [![kesor](https://avatars.githubusercontent.com/u/7056?v=4&s=48)](https://github.com/kesor) [![mazhe-nerd](https://avatars.githubusercontent.com/u/106217973?v=4&s=48)](https://github.com/mazhe-nerd) [![Harald Buerbaumer](https://avatars.githubusercontent.com/u/44548809?v=4&s=48)](https://github.com/buerbaumer) [![magimetal](https://avatars.githubusercontent.com/u/36491250?v=4&s=48)](https://github.com/magimetal) [![Hiren Patel](https://avatars.githubusercontent.com/u/172098?v=4&s=48)](https://github.com/patelhiren)
[![BinHPdev](https://avatars.githubusercontent.com/u/219093083?v=4&s=48)](https://github.com/BinHPdev) [![RyanLee-Dev](https://avatars.githubusercontent.com/u/33855278?v=4&s=48)](https://github.com/RyanLee-Dev) [![cathrynlavery](https://avatars.githubusercontent.com/u/50469282?v=4&s=48)](https://github.com/cathrynlavery) [![al3mart](https://avatars.githubusercontent.com/u/11448715?v=4&s=48)](https://github.com/al3mart) [![JustYannicc](https://avatars.githubusercontent.com/u/52761674?v=4&s=48)](https://github.com/JustYannicc) [![abhisekbasu1](https://avatars.githubusercontent.com/u/40645221?v=4&s=48)](https://github.com/AbhisekBasu1) [![dbhurley](https://avatars.githubusercontent.com/u/5251425?v=4&s=48)](https://github.com/dbhurley) [![Kris Wu](https://avatars.githubusercontent.com/u/32388289?v=4&s=48)](https://github.com/mpz4life) [![tmimmanuel](https://avatars.githubusercontent.com/u/14046872?v=4&s=48)](https://github.com/tmimmanuel) [![JustasM](https://avatars.githubusercontent.com/u/59362982?v=4&s=48)](https://github.com/JustasMonkev)
[![Simantak Dabhade](https://avatars.githubusercontent.com/u/67303107?v=4&s=48)](https://github.com/simantak-dabhade) [![NicholasSpisak](https://avatars.githubusercontent.com/u/129075147?v=4&s=48)](https://github.com/NicholasSpisak) [![natefikru](https://avatars.githubusercontent.com/u/10344644?v=4&s=48)](https://github.com/natefikru) [![dunamismax](https://avatars.githubusercontent.com/u/65822992?v=4&s=48)](https://github.com/dunamismax) [![Simone Macario](https://avatars.githubusercontent.com/u/2116609?v=4&s=48)](https://github.com/simonemacario) [![ENCHIGO](https://avatars.githubusercontent.com/u/38551565?v=4&s=48)](https://github.com/ENCHIGO) [![xingsy97](https://avatars.githubusercontent.com/u/87063252?v=4&s=48)](https://github.com/xingsy97) [![emonty](https://avatars.githubusercontent.com/u/95156?v=4&s=48)](https://github.com/emonty) [![jadilson12](https://avatars.githubusercontent.com/u/36805474?v=4&s=48)](https://github.com/jadilson12) [![Yi-Cheng Wang](https://avatars.githubusercontent.com/u/80525895?v=4&s=48)](https://github.com/kirisame-wang)
[![Mathias Nagler](https://avatars.githubusercontent.com/u/9951231?v=4&s=48)](https://github.com/mathiasnagler) [![Sean McLellan](https://avatars.githubusercontent.com/u/760674?v=4&s=48)](https://github.com/Oceanswave) [![gumclaw](https://avatars.githubusercontent.com/u/265388744?v=4&s=48)](https://github.com/gumclaw) [![RichardCao](https://avatars.githubusercontent.com/u/4612401?v=4&s=48)](https://github.com/RichardCao) [![MKV21](https://avatars.githubusercontent.com/u/4974411?v=4&s=48)](https://github.com/MKV21) [![petter-b](https://avatars.githubusercontent.com/u/62076402?v=4&s=48)](https://github.com/petter-b) [![CodeForgeNet](https://avatars.githubusercontent.com/u/166907114?v=4&s=48)](https://github.com/CodeForgeNet) [![Johnson Shi](https://avatars.githubusercontent.com/u/13926417?v=4&s=48)](https://github.com/johnsonshi) [![durenzidu](https://avatars.githubusercontent.com/u/38130340?v=4&s=48)](https://github.com/durenzidu) [![dougvk](https://avatars.githubusercontent.com/u/401660?v=4&s=48)](https://github.com/dougvk)
[![Whoaa512](https://avatars.githubusercontent.com/u/1581943?v=4&s=48)](https://github.com/Whoaa512) [![zimeg](https://avatars.githubusercontent.com/u/18134219?v=4&s=48)](https://github.com/zimeg) [![Tseka Luk](https://avatars.githubusercontent.com/u/79151285?v=4&s=48)](https://github.com/TsekaLuk) [![Ryan Haines](https://avatars.githubusercontent.com/u/1855752?v=4&s=48)](https://github.com/Ryan-Haines) [![ufhy](https://avatars.githubusercontent.com/u/41638541?v=4&s=48)](https://github.com/uf-hy) [![Daan van der Plas](https://avatars.githubusercontent.com/u/93204684?v=4&s=48)](https://github.com/Daanvdplas) [![bittoby](https://avatars.githubusercontent.com/u/218712309?v=4&s=48)](https://github.com/bittoby) [![XuHao](https://avatars.githubusercontent.com/u/5087930?v=4&s=48)](https://github.com/xuhao1) [![Lucenx9](https://avatars.githubusercontent.com/u/185146821?v=4&s=48)](https://github.com/Lucenx9) [![HeMuling](https://avatars.githubusercontent.com/u/74801533?v=4&s=48)](https://github.com/HeMuling)
[![AaronLuo00](https://avatars.githubusercontent.com/u/112882500?v=4&s=48)](https://github.com/AaronLuo00) [![YUJIE2002](https://avatars.githubusercontent.com/u/123847463?v=4&s=48)](https://github.com/YUJIE2002) [![DhruvBhatia0](https://avatars.githubusercontent.com/u/69252327?v=4&s=48)](https://github.com/DhruvBhatia0) [![Divanoli Mydeen Pitchai](https://avatars.githubusercontent.com/u/12023205?v=4&s=48)](https://github.com/divanoli) [![Bronko](https://avatars.githubusercontent.com/u/2217509?v=4&s=48)](https://github.com/derbronko) [![rubyrunsstuff](https://avatars.githubusercontent.com/u/246602379?v=4&s=48)](https://github.com/rubyrunsstuff) [![rabsef-bicrym](https://avatars.githubusercontent.com/u/52549148?v=4&s=48)](https://github.com/rabsef-bicrym) [![IVY-AI-gif](https://avatars.githubusercontent.com/u/62232838?v=4&s=48)](https://github.com/IVY-AI-gif) [![pvtclawn](https://avatars.githubusercontent.com/u/258811507?v=4&s=48)](https://github.com/pvtclawn) [![stephenschoettler](https://avatars.githubusercontent.com/u/7587303?v=4&s=48)](https://github.com/stephenschoettler)
[![Dale Babiy](https://avatars.githubusercontent.com/u/42547246?v=4&s=48)](https://github.com/minupla) [![LeftX](https://avatars.githubusercontent.com/u/53989315?v=4&s=48)](https://github.com/xzq-xu) [![David Gelberg](https://avatars.githubusercontent.com/u/57605064?v=4&s=48)](https://github.com/mousberg) [![Engr. Arif Ahmed Joy](https://avatars.githubusercontent.com/u/4543396?v=4&s=48)](https://github.com/arifahmedjoy) [![Masataka Shinohara](https://avatars.githubusercontent.com/u/11906529?v=4&s=48)](https://github.com/harhogefoo) [![2233admin](https://avatars.githubusercontent.com/u/57929895?v=4&s=48)](https://github.com/2233admin) [![ameno-](https://avatars.githubusercontent.com/u/2416135?v=4&s=48)](https://github.com/ameno-) [![battman21](https://avatars.githubusercontent.com/u/2656916?v=4&s=48)](https://github.com/battman21) [![bcherny](https://avatars.githubusercontent.com/u/1761758?v=4&s=48)](https://github.com/bcherny) [![bobashopcashier](https://avatars.githubusercontent.com/u/77253505?v=4&s=48)](https://github.com/bobashopcashier)
[![dguido](https://avatars.githubusercontent.com/u/294844?v=4&s=48)](https://github.com/dguido) [![druide67](https://avatars.githubusercontent.com/u/212749853?v=4&s=48)](https://github.com/druide67) [![guirguispierre](https://avatars.githubusercontent.com/u/22091706?v=4&s=48)](https://github.com/guirguispierre) [![jzakirov](https://avatars.githubusercontent.com/u/15848838?v=4&s=48)](https://github.com/jzakirov) [![loganprit](https://avatars.githubusercontent.com/u/72722788?v=4&s=48)](https://github.com/loganprit) [![martinfrancois](https://avatars.githubusercontent.com/u/14319020?v=4&s=48)](https://github.com/martinfrancois) [![neo1027144-creator](https://avatars.githubusercontent.com/u/267440006?v=4&s=48)](https://github.com/neo1027144-creator) [![RealKai42](https://avatars.githubusercontent.com/u/44634134?v=4&s=48)](https://github.com/RealKai42) [![schumilin](https://avatars.githubusercontent.com/u/2003498?v=4&s=48)](https://github.com/schumilin) [![shuofengzhang](https://avatars.githubusercontent.com/u/24763026?v=4&s=48)](https://github.com/shuofengzhang)
[![solstead](https://avatars.githubusercontent.com/u/168413654?v=4&s=48)](https://github.com/solstead) [![hengm3467](https://avatars.githubusercontent.com/u/100685635?v=4&s=48)](https://github.com/hengm3467) [![chziyue](https://avatars.githubusercontent.com/u/62380760?v=4&s=48)](https://github.com/chziyue) [![James L. Cowan Jr.](https://avatars.githubusercontent.com/u/112015792?v=4&s=48)](https://github.com/jameslcowan) [![scifantastic](https://avatars.githubusercontent.com/u/150712374?v=4&s=48)](https://github.com/scifantastic) [![ryan-crabbe](https://avatars.githubusercontent.com/u/128659760?v=4&s=48)](https://github.com/ryan-crabbe) [![alexfilatov](https://avatars.githubusercontent.com/u/138589?v=4&s=48)](https://github.com/alexfilatov) [![Luckymingxuan](https://avatars.githubusercontent.com/u/159552597?v=4&s=48)](https://github.com/Luckymingxuan) [![HollyChou](https://avatars.githubusercontent.com/u/128659251?v=4&s=48)](https://github.com/Hollychou924) [![badlogic](https://avatars.githubusercontent.com/u/514052?v=4&s=48)](https://github.com/badlogic)
[![Daniel Hnyk](https://avatars.githubusercontent.com/u/2741256?v=4&s=48)](https://github.com/hnykda) [![dan bachelder](https://avatars.githubusercontent.com/u/325706?v=4&s=48)](https://github.com/dbachelder) [![heavenlost](https://avatars.githubusercontent.com/u/70937055?v=4&s=48)](https://github.com/heavenlost) [![shad0wca7](https://avatars.githubusercontent.com/u/9969843?v=4&s=48)](https://github.com/shad0wca7) [![Jared](https://avatars.githubusercontent.com/u/37019497?v=4&s=48)](https://github.com/jared596) [![kiranjd](https://avatars.githubusercontent.com/u/25822851?v=4&s=48)](https://github.com/kiranjd) [![Mars](https://avatars.githubusercontent.com/u/40958792?v=4&s=48)](https://github.com/Mellowambience) [![Kim](https://avatars.githubusercontent.com/u/150593189?v=4&s=48)](https://github.com/KimGLee) [![seheepeak](https://avatars.githubusercontent.com/u/134766597?v=4&s=48)](https://github.com/seheepeak) [![tsavo](https://avatars.githubusercontent.com/u/877990?v=4&s=48)](https://github.com/TSavo)
[![McRolly NWANGWU](https://avatars.githubusercontent.com/u/60803337?v=4&s=48)](https://github.com/mcrolly) [![dashed](https://avatars.githubusercontent.com/u/139499?v=4&s=48)](https://github.com/dashed) [![Shuai-DaiDai](https://avatars.githubusercontent.com/u/134567396?v=4&s=48)](https://github.com/Shuai-DaiDai) [![Subash Natarajan](https://avatars.githubusercontent.com/u/11032439?v=4&s=48)](https://github.com/suboss87) [![emanuelst](https://avatars.githubusercontent.com/u/9994339?v=4&s=48)](https://github.com/emanuelst) [![magendary](https://avatars.githubusercontent.com/u/30611068?v=4&s=48)](https://github.com/magendary) [![LI SHANXIN](https://avatars.githubusercontent.com/u/128674037?v=4&s=48)](https://github.com/PeterShanxin) [![j2h4u](https://avatars.githubusercontent.com/u/39818683?v=4&s=48)](https://github.com/j2h4u) [![bsormagec](https://avatars.githubusercontent.com/u/965219?v=4&s=48)](https://github.com/bsormagec) [![mjamiv](https://avatars.githubusercontent.com/u/142179942?v=4&s=48)](https://github.com/mjamiv)
[![Lalit Singh](https://avatars.githubusercontent.com/u/17166039?v=4&s=48)](https://github.com/aerolalit) [![Jessy LANGE](https://avatars.githubusercontent.com/u/89694096?v=4&s=48)](https://github.com/jessy2027) [![buddyh](https://avatars.githubusercontent.com/u/31752869?v=4&s=48)](https://github.com/buddyh) [![Aaron Zhu](https://avatars.githubusercontent.com/u/139607425?v=4&s=48)](https://github.com/aaron-he-zhu) [![F_ool](https://avatars.githubusercontent.com/u/112874572?v=4&s=48)](https://github.com/hhhhao28) [![Ben Stein](https://avatars.githubusercontent.com/u/31802821?v=4&s=48)](https://github.com/benostein) [![Lyle](https://avatars.githubusercontent.com/u/31182860?v=4&s=48)](https://github.com/LyleLiu666) [![Ping](https://avatars.githubusercontent.com/u/5123601?v=4&s=48)](https://github.com/pingren) [![popomore](https://avatars.githubusercontent.com/u/360661?v=4&s=48)](https://github.com/popomore) [![Dithilli](https://avatars.githubusercontent.com/u/41286037?v=4&s=48)](https://github.com/Dithilli)
[![fal3](https://avatars.githubusercontent.com/u/6484295?v=4&s=48)](https://github.com/fal3) [![mkbehr](https://avatars.githubusercontent.com/u/1285?v=4&s=48)](https://github.com/mkbehr) [![mteam88](https://avatars.githubusercontent.com/u/84196639?v=4&s=48)](https://github.com/mteam88) [![gupsammy](https://avatars.githubusercontent.com/u/20296019?v=4&s=48)](https://github.com/gupsammy) [![Shailesh](https://avatars.githubusercontent.com/u/75851986?v=4&s=48)](https://github.com/gut-puncture) [![Garnet Liu](https://avatars.githubusercontent.com/u/12513503?v=4&s=48)](https://github.com/garnetlyx) [![Thorfinn](https://avatars.githubusercontent.com/u/136994453?v=4&s=48)](https://github.com/miloudbelarebia) [![Protocol-zero-0](https://avatars.githubusercontent.com/u/257158451?v=4&s=48)](https://github.com/Protocol-zero-0) [![Paul van Oorschot](https://avatars.githubusercontent.com/u/20116814?v=4&s=48)](https://github.com/pvoo) [![Patrick Yingxi Pan](https://avatars.githubusercontent.com/u/5210631?v=4&s=48)](https://github.com/patrick-yingxi-pan)
[![Ptah.ai](https://avatars.githubusercontent.com/u/11701?v=4&s=48)](https://github.com/ptahdunbar) [![정우용](https://avatars.githubusercontent.com/u/71975659?v=4&s=48)](https://github.com/keepitmello) [![artuskg](https://avatars.githubusercontent.com/u/11966157?v=4&s=48)](https://github.com/artuskg) [![Anandesh-Sharma](https://avatars.githubusercontent.com/u/30695364?v=4&s=48)](https://github.com/Anandesh-Sharma) [![zidongdesign](https://avatars.githubusercontent.com/u/81469543?v=4&s=48)](https://github.com/zidongdesign) [![innocent-children](https://avatars.githubusercontent.com/u/55626758?v=4&s=48)](https://github.com/Innocent-children) [![El-Fitz](https://avatars.githubusercontent.com/u/8971906?v=4&s=48)](https://github.com/El-Fitz) [![arthurbr11](https://avatars.githubusercontent.com/u/99079981?v=4&s=48)](https://github.com/arthurbr11) [![jackheuberger](https://avatars.githubusercontent.com/u/7830838?v=4&s=48)](https://github.com/jackheuberger) [![Sergiusz](https://avatars.githubusercontent.com/u/6172067?v=4&s=48)](https://github.com/serkonyc)
[![Xu Gu](https://avatars.githubusercontent.com/u/53551744?v=4&s=48)](https://github.com/guxu11) [![hyojin](https://avatars.githubusercontent.com/u/3413183?v=4&s=48)](https://github.com/hyojin) [![jeann2013](https://avatars.githubusercontent.com/u/3299025?v=4&s=48)](https://github.com/jeann2013) [![jogelin](https://avatars.githubusercontent.com/u/954509?v=4&s=48)](https://github.com/jogelin) [![rmorse](https://avatars.githubusercontent.com/u/853547?v=4&s=48)](https://github.com/rmorse) [![scz2011](https://avatars.githubusercontent.com/u/9337506?v=4&s=48)](https://github.com/scz2011) [![Andyliu](https://avatars.githubusercontent.com/u/2377291?v=4&s=48)](https://github.com/andyliu) [![benithors](https://avatars.githubusercontent.com/u/20652882?v=4&s=48)](https://github.com/benithors) [![xiwuqi](https://avatars.githubusercontent.com/u/64734786?v=4&s=48)](https://github.com/xiwuqi) [![Alvin](https://avatars.githubusercontent.com/u/48358093?v=4&s=48)](https://github.com/TigerInYourDream)
[![AARON AGENT](https://avatars.githubusercontent.com/u/78432083?v=4&s=48)](https://github.com/aaronagent) [![Derek YU](https://avatars.githubusercontent.com/u/154693526?v=4&s=48)](https://github.com/TonyDerek-dot) [![Marvin](https://avatars.githubusercontent.com/u/43185740?v=4&s=48)](https://github.com/Zitzak) [![Andrew Jeon](https://avatars.githubusercontent.com/u/46941315?v=4&s=48)](https://github.com/ruypang) [![stain lu](https://avatars.githubusercontent.com/u/109842185?v=4&s=48)](https://github.com/stainlu) [![OpenCils](https://avatars.githubusercontent.com/u/114985039?v=4&s=48)](https://github.com/OpenCils) [![Stefan Galescu](https://avatars.githubusercontent.com/u/52995748?v=4&s=48)](https://github.com/stefangalescu) [![SP](https://avatars.githubusercontent.com/u/8068616?v=4&s=48)](https://github.com/sp-hk2ldn) [![Michael Flanagan](https://avatars.githubusercontent.com/u/39276573?v=4&s=48)](https://github.com/MikeORed) [![Gracie Gould](https://avatars.githubusercontent.com/u/66045258?v=4&s=48)](https://github.com/graciegould)
[![cash-echo-bot](https://avatars.githubusercontent.com/u/252747386?v=4&s=48)](https://github.com/cash-echo-bot) [![visionik](https://avatars.githubusercontent.com/u/52174?v=4&s=48)](https://github.com/visionik) [![WalterSumbon](https://avatars.githubusercontent.com/u/45062253?v=4&s=48)](https://github.com/WalterSumbon) [![huangcj](https://avatars.githubusercontent.com/u/43933609?v=4&s=48)](https://github.com/SubtleSpark) [![krizpoon](https://avatars.githubusercontent.com/u/1977532?v=4&s=48)](https://github.com/krizpoon) [![rodbland2021](https://avatars.githubusercontent.com/u/86267410?v=4&s=48)](https://github.com/rodbland2021) [![Thomas M](https://avatars.githubusercontent.com/u/44269971?v=4&s=48)](https://github.com/thomasxm) [![sar618](https://avatars.githubusercontent.com/u/214745104?v=4&s=48)](https://github.com/sar618) [![fagemx](https://avatars.githubusercontent.com/u/117356295?v=4&s=48)](https://github.com/fagemx) [![daymade](https://avatars.githubusercontent.com/u/4291901?v=4&s=48)](https://github.com/daymade)
[![Tyson Cung](https://avatars.githubusercontent.com/u/45380903?v=4&s=48)](https://github.com/tysoncung) [![Igor Markelov](https://avatars.githubusercontent.com/u/1489583?v=4&s=48)](https://github.com/pycckuu) [![Eng. Juan Combetto](https://avatars.githubusercontent.com/u/322761?v=4&s=48)](https://github.com/omniwired) [![connorshea](https://avatars.githubusercontent.com/u/2977353?v=4&s=48)](https://github.com/connorshea) [![bonald](https://avatars.githubusercontent.com/u/12394874?v=4&s=48)](https://github.com/bonald) [![Keenan](https://avatars.githubusercontent.com/u/85285887?v=4&s=48)](https://github.com/BeeSting50) [![nachoiacovino](https://avatars.githubusercontent.com/u/50103937?v=4&s=48)](https://github.com/nachoiacovino) [![zhumengzhu](https://avatars.githubusercontent.com/u/4508623?v=4&s=48)](https://github.com/zhumengzhu) [![Amine Harch el korane](https://avatars.githubusercontent.com/u/95189778?v=4&s=48)](https://github.com/Vitalcheffe) [![zhoulc777](https://avatars.githubusercontent.com/u/65058500?v=4&s=48)](https://github.com/zhoulongchao77)
[![Alex Navarro](https://avatars.githubusercontent.com/u/78754189?v=4&s=48)](https://github.com/navarrotech) [![Tanwa Arpornthip](https://avatars.githubusercontent.com/u/72845369?v=4&s=48)](https://github.com/CommanderCrowCode) [![TIHU](https://avatars.githubusercontent.com/u/44923937?v=4&s=48)](https://github.com/paceyw) [![Aftabbs](https://avatars.githubusercontent.com/u/112916888?v=4&s=48)](https://github.com/Aftabbs) [![Alex-Alaniz](https://avatars.githubusercontent.com/u/88956822?v=4&s=48)](https://github.com/Alex-Alaniz) [![jarvis-medmatic](https://avatars.githubusercontent.com/u/252428873?v=4&s=48)](https://github.com/jarvis-medmatic) [![Tom Ron](https://avatars.githubusercontent.com/u/126325152?v=4&s=48)](https://github.com/tomron87) [![day253](https://avatars.githubusercontent.com/u/9634619?v=4&s=48)](https://github.com/day253) [![Jaaneek](https://avatars.githubusercontent.com/u/25470423?v=4&s=48)](https://github.com/Jaaneek) [![Justin Song](https://avatars.githubusercontent.com/u/32268203?v=4&s=48)](https://github.com/AnCoSONG)
[![ziomancer](https://avatars.githubusercontent.com/u/262232137?v=4&s=48)](https://github.com/ziomancer) [![shayan919293](https://avatars.githubusercontent.com/u/60409704?v=4&s=48)](https://github.com/shayan919293) [![Edward](https://avatars.githubusercontent.com/u/53964601?v=4&s=48)](https://github.com/edwluo) [![Roger Chien](https://avatars.githubusercontent.com/u/20276663?v=4&s=48)](https://github.com/rjchien728) [![Michael Lee](https://avatars.githubusercontent.com/u/5957298?v=4&s=48)](https://github.com/TinyTb) [![Tomáš Dinh](https://avatars.githubusercontent.com/u/82420070?v=4&s=48)](https://github.com/No898) [![Ian Derrington](https://avatars.githubusercontent.com/u/76016868?v=4&s=48)](https://github.com/ianderrington) [![Lucky](https://avatars.githubusercontent.com/u/14868134?v=4&s=48)](https://github.com/L-U-C-K-Y) [![peschee](https://avatars.githubusercontent.com/u/63866?v=4&s=48)](https://github.com/peschee) [![Harry Cui Kepler](https://avatars.githubusercontent.com/u/166882517?v=4&s=48)](https://github.com/Kepler2024)
[![julianengel](https://avatars.githubusercontent.com/u/10634231?v=4&s=48)](https://github.com/julianengel) [![markfietje](https://avatars.githubusercontent.com/u/4325889?v=4&s=48)](https://github.com/markfietje) [![Dakshay Mehta](https://avatars.githubusercontent.com/u/50276213?v=4&s=48)](https://github.com/dakshaymehta) [![TheRipper](https://avatars.githubusercontent.com/u/144421782?v=4&s=48)](https://github.com/DavidNitZ) [![Dominic](https://avatars.githubusercontent.com/u/43616264?v=4&s=48)](https://github.com/dominicnunez) [![danielwanwx](https://avatars.githubusercontent.com/u/144515713?v=4&s=48)](https://github.com/danielwanwx) [![Seungwoo hong](https://avatars.githubusercontent.com/u/1100974?v=4&s=48)](https://github.com/hongsw) [![Youyou972](https://avatars.githubusercontent.com/u/50808411?v=4&s=48)](https://github.com/Youyou972) [![boris721](https://avatars.githubusercontent.com/u/257853888?v=4&s=48)](https://github.com/boris721) [![damoahdominic](https://avatars.githubusercontent.com/u/4623434?v=4&s=48)](https://github.com/damoahdominic)
[![dan-dr](https://avatars.githubusercontent.com/u/6669808?v=4&s=48)](https://github.com/dan-dr) [![doodlewind](https://avatars.githubusercontent.com/u/7312949?v=4&s=48)](https://github.com/doodlewind) [![kkarimi](https://avatars.githubusercontent.com/u/875218?v=4&s=48)](https://github.com/kkarimi) [![brokemac79](https://avatars.githubusercontent.com/u/255583030?v=4&s=48)](https://github.com/brokemac79) [![ozbillwang](https://avatars.githubusercontent.com/u/8954908?v=4&s=48)](https://github.com/ozbillwang) [![Ravish Gupta](https://avatars.githubusercontent.com/u/1249023?v=4&s=48)](https://github.com/ravyg) [![Jason Hargrove](https://avatars.githubusercontent.com/u/285708?v=4&s=48)](https://github.com/jasonhargrove) [![BrianWang1990](https://avatars.githubusercontent.com/u/20699847?v=4&s=48)](https://github.com/BrianWang1990) [![Joshua McKiddy](https://avatars.githubusercontent.com/u/43189238?v=4&s=48)](https://github.com/hackersifu) [![Fologan](https://avatars.githubusercontent.com/u/164580328?v=4&s=48)](https://github.com/Fologan)
[![Anonymous Amit](https://avatars.githubusercontent.com/u/134582556?v=4&s=48)](https://github.com/AnonAmit) [![v1p0r](https://avatars.githubusercontent.com/u/25909990?v=4&s=48)](https://github.com/v1p0r) [![Ajay Elika](https://avatars.githubusercontent.com/u/73169130?v=4&s=48)](https://github.com/ajay99511) [![Iranb](https://avatars.githubusercontent.com/u/49674669?v=4&s=48)](https://github.com/Iranb) [![Yonatan](https://avatars.githubusercontent.com/u/10474956?v=4&s=48)](https://github.com/yhyatt) [![codexGW](https://avatars.githubusercontent.com/u/9350182?v=4&s=48)](https://github.com/codexGW) [![Shaun Tsai](https://avatars.githubusercontent.com/u/13811075?v=4&s=48)](https://github.com/ShaunTsai) [![TideFinder](https://avatars.githubusercontent.com/u/68721273?v=4&s=48)](https://github.com/papago2355) [![Chase Dorsey](https://avatars.githubusercontent.com/u/12650570?v=4&s=48)](https://github.com/cdorsey) [![tda](https://avatars.githubusercontent.com/u/95275462?v=4&s=48)](https://github.com/tda1017)
[![0xJonHoldsCrypto](https://avatars.githubusercontent.com/u/81202085?v=4&s=48)](https://github.com/0xJonHoldsCrypto) [![akyourowngames](https://avatars.githubusercontent.com/u/123736861?v=4&s=48)](https://github.com/akyourowngames) [![clawdinator[bot]](https://avatars.githubusercontent.com/in/2607181?v=4&s=48)](https://github.com/apps/clawdinator) [![koala73](https://avatars.githubusercontent.com/u/996596?v=4&s=48)](https://github.com/koala73) [![sircrumpet](https://avatars.githubusercontent.com/u/4436535?v=4&s=48)](https://github.com/sircrumpet) [![thesomewhatyou](https://avatars.githubusercontent.com/u/162917831?v=4&s=48)](https://github.com/thesomewhatyou) [![zats](https://avatars.githubusercontent.com/u/2688806?v=4&s=48)](https://github.com/zats) [![Accunza](https://avatars.githubusercontent.com/u/12242811?v=4&s=48)](https://github.com/duqaXxX) [![Joly0](https://avatars.githubusercontent.com/u/13993216?v=4&s=48)](https://github.com/Joly0) [![Hanna](https://avatars.githubusercontent.com/u/4538260?v=4&s=48)](https://github.com/hannasdev)
[![Jeremiah Lowin](https://avatars.githubusercontent.com/u/153965?v=4&s=48)](https://github.com/jlowin) [![peetzweg/](https://avatars.githubusercontent.com/u/839848?v=4&s=48)](https://github.com/peetzweg) [![Skyler Miao](https://avatars.githubusercontent.com/u/153898832?v=4&s=48)](https://github.com/adao-max) [![tumf](https://avatars.githubusercontent.com/u/69994?v=4&s=48)](https://github.com/tumf) [![Hiago Silva](https://avatars.githubusercontent.com/u/97215740?v=4&s=48)](https://github.com/Huntterxx) [![Nate](https://avatars.githubusercontent.com/u/12980165?v=4&s=48)](https://github.com/nk1tz) [![lidamao633](https://avatars.githubusercontent.com/u/94925404?v=4&s=48)](https://github.com/lidamao633) [![Cklee](https://avatars.githubusercontent.com/u/99405438?v=4&s=48)](https://github.com/liebertar) [![CornBrother0x](https://avatars.githubusercontent.com/u/101160087?v=4&s=48)](https://github.com/CornBrother0x) [![DukeDeSouth](https://avatars.githubusercontent.com/u/51200688?v=4&s=48)](https://github.com/DukeDeSouth)
[![Sahan](https://avatars.githubusercontent.com/u/57447079?v=4&s=48)](https://github.com/sahancava) [![CashWilliams](https://avatars.githubusercontent.com/u/613573?v=4&s=48)](https://github.com/CashWilliams) [![Felix Lu](https://avatars.githubusercontent.com/u/58391009?v=4&s=48)](https://github.com/lumpinif) [![AdeboyeDN](https://avatars.githubusercontent.com/u/65312338?v=4&s=48)](https://github.com/AdeboyeDN) [![Rohan Santhosh Kumar](https://avatars.githubusercontent.com/u/181558744?v=4&s=48)](https://github.com/Rohan5commit) [![Srinivas Pavan](https://avatars.githubusercontent.com/u/34889400?v=4&s=48)](https://github.com/srinivaspavan9) [![h0tp](https://avatars.githubusercontent.com/u/141889580?v=4&s=48)](https://github.com/h0tp-ftw) [![Neo](https://avatars.githubusercontent.com/u/54811660?v=4&s=48)](https://github.com/neooriginal) [![Tianworld](https://avatars.githubusercontent.com/u/40754565?v=4&s=48)](https://github.com/Tianworld) [![neverland](https://avatars.githubusercontent.com/u/10937319?v=4&s=48)](https://github.com/Bermudarat)
[![asklee-klawd](https://avatars.githubusercontent.com/u/105007315?v=4&s=48)](https://github.com/asklee-klawd) [![Yuting Lin](https://avatars.githubusercontent.com/u/32728916?v=4&s=48)](https://github.com/yuting0624) [![constansino](https://avatars.githubusercontent.com/u/65108260?v=4&s=48)](https://github.com/constansino) [![ghsmc](https://avatars.githubusercontent.com/u/68118719?v=4&s=48)](https://github.com/ghsmc) [![ibrahimq21](https://avatars.githubusercontent.com/u/8392472?v=4&s=48)](https://github.com/ibrahimq21) [![irtiq7](https://avatars.githubusercontent.com/u/3823029?v=4&s=48)](https://github.com/irtiq7) [![kelvinCB](https://avatars.githubusercontent.com/u/50544379?v=4&s=48)](https://github.com/kelvinCB) [![mitsuhiko](https://avatars.githubusercontent.com/u/7396?v=4&s=48)](https://github.com/mitsuhiko) [![nohat](https://avatars.githubusercontent.com/u/838027?v=4&s=48)](https://github.com/nohat) [![santiagomed](https://avatars.githubusercontent.com/u/30184543?v=4&s=48)](https://github.com/santiagomed)
[![suminhthanh](https://avatars.githubusercontent.com/u/2907636?v=4&s=48)](https://github.com/suminhthanh) [![svkozak](https://avatars.githubusercontent.com/u/31941359?v=4&s=48)](https://github.com/svkozak) [![张哲芳](https://avatars.githubusercontent.com/u/34058239?v=4&s=48)](https://github.com/zhangzhefang-github) [![Ho Lim](https://avatars.githubusercontent.com/u/166576253?v=4&s=48)](https://github.com/HOYALIM) [![Toven](https://avatars.githubusercontent.com/u/69218856?v=4&s=48)](https://github.com/ping-Toven) [![R. Desmond](https://avatars.githubusercontent.com/u/134018026?v=4&s=48)](https://github.com/0-CYBERDYNE-SYSTEMS-0) [![游乐场](https://avatars.githubusercontent.com/u/79438767?v=4&s=48)](https://github.com/ylc0919) [![Reed](https://avatars.githubusercontent.com/u/129141816?v=4&s=48)](https://github.com/reed1898) [![Aditya Chaudhary](https://avatars.githubusercontent.com/u/55331140?v=4&s=48)](https://github.com/ItsAditya-xyz) [![Sam](https://avatars.githubusercontent.com/u/14844597?v=4&s=48)](https://github.com/samrusani)
[![Andy](https://avatars.githubusercontent.com/u/91510251?v=4&s=48)](https://github.com/andyk-ms) [![Rajat Joshi](https://avatars.githubusercontent.com/u/78920780?v=4&s=48)](https://github.com/18-RAJAT) [![cyb1278588254](https://avatars.githubusercontent.com/u/48212932?v=4&s=48)](https://github.com/cyb1278588254) [![Zoher Ghadyali](https://avatars.githubusercontent.com/u/34316555?v=4&s=48)](https://github.com/zoherghadyali) [![Manik Vahsith](https://avatars.githubusercontent.com/u/49544491?v=4&s=48)](https://github.com/manikv12) [![tarouca](https://avatars.githubusercontent.com/u/36767065?v=4&s=48)](https://github.com/manueltarouca) [![MrBrain](https://avatars.githubusercontent.com/u/176294248?v=4&s=48)](https://github.com/GaosCode) [![Daniel Zou](https://avatars.githubusercontent.com/u/12799392?v=4&s=48)](https://github.com/pahdo) [![Lilo](https://avatars.githubusercontent.com/u/1622461?v=4&s=48)](https://github.com/detecti1) [![Jason](https://avatars.githubusercontent.com/u/101583541?v=4&s=48)](https://github.com/JasonOA888)
[![SUMUKH](https://avatars.githubusercontent.com/u/130692934?v=4&s=48)](https://github.com/sumukhj1219) [![Bakhtier Sizhaev](https://avatars.githubusercontent.com/u/108124494?v=4&s=48)](https://github.com/bakhtiersizhaev) [![Ganghyun Kim](https://avatars.githubusercontent.com/u/58307870?v=4&s=48)](https://github.com/kyleok) [![AkashKobal](https://avatars.githubusercontent.com/u/98216083?v=4&s=48)](https://github.com/AkashKobal) [![Brian](https://avatars.githubusercontent.com/u/95547369?v=4&s=48)](https://github.com/zhuisDEV) [![wu-tian807](https://avatars.githubusercontent.com/u/61640083?v=4&s=48)](https://github.com/wu-tian807) [![Vasanth Rao Naik Sabavat](https://avatars.githubusercontent.com/u/50385532?v=4&s=48)](https://github.com/vsabavat) [![Kinfey](https://avatars.githubusercontent.com/u/93169410?v=4&s=48)](https://github.com/kinfey) [![Artemii](https://avatars.githubusercontent.com/u/35071559?v=4&s=48)](https://github.com/crimeacs) [![VibhorGautam](https://avatars.githubusercontent.com/u/55019395?v=4&s=48)](https://github.com/VibhorGautam)
[![John Rood](https://avatars.githubusercontent.com/u/62669593?v=4&s=48)](https://github.com/John-Rood) [![velamints2](https://avatars.githubusercontent.com/u/93711796?v=4&s=48)](https://github.com/velamints2) [![Benji Peng](https://avatars.githubusercontent.com/u/11394934?v=4&s=48)](https://github.com/benjipeng) [![JINNYEONG KIM](https://avatars.githubusercontent.com/u/41609506?v=4&s=48)](https://github.com/divisonofficer) [![Rahul kumar Pal](https://avatars.githubusercontent.com/u/151990777?v=4&s=48)](https://github.com/Rahulkumar070) [![Rockcent](https://avatars.githubusercontent.com/u/128210877?v=4&s=48)](https://github.com/rockcent) [![Limitless](https://avatars.githubusercontent.com/u/127183162?v=4&s=48)](https://github.com/Limitless2023) [![24601](https://avatars.githubusercontent.com/u/1157207?v=4&s=48)](https://github.com/24601) [![awkoy](https://avatars.githubusercontent.com/u/13995636?v=4&s=48)](https://github.com/awkoy) [![dawondyifraw](https://avatars.githubusercontent.com/u/9797257?v=4&s=48)](https://github.com/dawondyifraw)
[![google-labs-jules[bot]](https://avatars.githubusercontent.com/in/842251?v=4&s=48)](https://github.com/apps/google-labs-jules) [![henrino3](https://avatars.githubusercontent.com/u/4260288?v=4&s=48)](https://github.com/henrino3) [![Kansodata](https://avatars.githubusercontent.com/u/225288021?v=4&s=48)](https://github.com/Kansodata) [![kaonash](https://avatars.githubusercontent.com/u/7535663?v=4&s=48)](https://github.com/kaonash) [![p6l-richard](https://avatars.githubusercontent.com/u/18185649?v=4&s=48)](https://github.com/p6l-richard) [![pi0](https://avatars.githubusercontent.com/u/5158436?v=4&s=48)](https://github.com/pi0) [![skainguyen1412](https://avatars.githubusercontent.com/u/14249881?v=4&s=48)](https://github.com/skainguyen1412) [![Starhappysh](https://avatars.githubusercontent.com/u/221244539?v=4&s=48)](https://github.com/Starhappysh) [![xdanger](https://avatars.githubusercontent.com/u/7087?v=4&s=48)](https://github.com/xdanger) [![Penchan](https://avatars.githubusercontent.com/u/5032148?v=4&s=48)](https://github.com/p3nchan)
[![scald](https://avatars.githubusercontent.com/u/1215913?v=4&s=48)](https://github.com/scald) [![Serhii](https://avatars.githubusercontent.com/u/151471784?v=4&s=48)](https://github.com/kashevk0) [![a](https://avatars.githubusercontent.com/u/33371662?v=4&s=48)](https://github.com/Yuandiaodiaodiao) [![Doğu Abaris](https://avatars.githubusercontent.com/u/135986694?v=4&s=48)](https://github.com/doguabaris) [![ysqander](https://avatars.githubusercontent.com/u/80843820?v=4&s=48)](https://github.com/ysqander) [![andranik-sahakyan](https://avatars.githubusercontent.com/u/8908029?v=4&s=48)](https://github.com/andranik-sahakyan) [![Wangnov](https://avatars.githubusercontent.com/u/48670012?v=4&s=48)](https://github.com/Wangnov) [![Austin](https://avatars.githubusercontent.com/u/112558420?v=4&s=48)](https://github.com/rixau) [![lisitan](https://avatars.githubusercontent.com/u/50470712?v=4&s=48)](https://github.com/lisitan) [![Rishi Vhavle](https://avatars.githubusercontent.com/u/134706404?v=4&s=48)](https://github.com/kaizen403)
[![Frank Harris](https://avatars.githubusercontent.com/u/183158?v=4&s=48)](https://github.com/hirefrank) [![Kenny Lee](https://avatars.githubusercontent.com/u/1432489?v=4&s=48)](https://github.com/kennyklee) [![Alice Losasso](https://avatars.githubusercontent.com/u/104875499?v=4&s=48)](https://github.com/dddabtc) [![edincampara](https://avatars.githubusercontent.com/u/142477787?v=4&s=48)](https://github.com/edincampara) [![Felix Hellström](https://avatars.githubusercontent.com/u/30758862?v=4&s=48)](https://github.com/fellanH) [![Varun Chopra](https://avatars.githubusercontent.com/u/113368492?v=4&s=48)](https://github.com/VarunChopra11) [![wangai-studio](https://avatars.githubusercontent.com/u/256938352?v=4&s=48)](https://github.com/wangai-studio) [![sleontenko](https://avatars.githubusercontent.com/u/7135949?v=4&s=48)](https://github.com/sleontenko) [![Yassine Amjad](https://avatars.githubusercontent.com/u/59234686?v=4&s=48)](https://github.com/yassine20011) [![Anton Eicher](https://avatars.githubusercontent.com/u/54324760?v=4&s=48)](https://github.com/ant1eicher)
[![Drake Thomsen](https://avatars.githubusercontent.com/u/120344051?v=4&s=48)](https://github.com/ThomsenDrake) [![Hinata Kaga (samon)](https://avatars.githubusercontent.com/u/61647657?v=4&s=48)](https://github.com/kakuteki) [![andreabadesso](https://avatars.githubusercontent.com/u/3586068?v=4&s=48)](https://github.com/andreabadesso) [![chenxin-yan](https://avatars.githubusercontent.com/u/71162231?v=4&s=48)](https://github.com/chenxin-yan) [![cordx56](https://avatars.githubusercontent.com/u/23298744?v=4&s=48)](https://github.com/cordx56) [![dvrshil](https://avatars.githubusercontent.com/u/81693876?v=4&s=48)](https://github.com/dvrshil) [![MarvinCui](https://avatars.githubusercontent.com/u/130876763?v=4&s=48)](https://github.com/MarvinCui) [![Yeom-JinHo](https://avatars.githubusercontent.com/u/81306489?v=4&s=48)](https://github.com/Yeom-JinHo) [![Jeremy Mumford](https://avatars.githubusercontent.com/u/36290330?v=4&s=48)](https://github.com/17jmumford) [![Charlie Niño](https://avatars.githubusercontent.com/u/2346724?v=4&s=48)](https://github.com/KnHack)
[![Sharoon Sharif](https://avatars.githubusercontent.com/u/150296639?v=4&s=48)](https://github.com/SharoonSharif) [![Oren](https://avatars.githubusercontent.com/u/168856?v=4&s=48)](https://github.com/orenyomtov) [![MattQ](https://avatars.githubusercontent.com/u/115874885?v=4&s=48)](https://github.com/mattqdev) [![Parker Todd Brooks](https://avatars.githubusercontent.com/u/585456?v=4&s=48)](https://github.com/parkertoddbrooks) [![Yufeng He](https://avatars.githubusercontent.com/u/40085740?v=4&s=48)](https://github.com/he-yufeng) [![Milofax](https://avatars.githubusercontent.com/u/2537423?v=4&s=48)](https://github.com/Milofax) [![Steve (OpenClaw)](https://avatars.githubusercontent.com/u/261149299?v=4&s=48)](https://github.com/stevebot-alive) [![zhoulf1006](https://avatars.githubusercontent.com/u/35586967?v=4&s=48)](https://github.com/zhoulf1006) [![Jonatan](https://avatars.githubusercontent.com/u/19454127?v=4&s=48)](https://github.com/jrrcdev) [![Sebastian B Otaegui](https://avatars.githubusercontent.com/u/91633?v=4&s=48)](https://github.com/feniix)
[![Matthew](https://avatars.githubusercontent.com/u/76985631?v=4&s=48)](https://github.com/ZetiMente) [![ABFS Tech](https://avatars.githubusercontent.com/u/82096803?v=4&s=48)](https://github.com/QuantDeveloperUSA) [![alexstyl](https://avatars.githubusercontent.com/u/1665273?v=4&s=48)](https://github.com/alexstyl) [![Ethan Palm](https://avatars.githubusercontent.com/u/56270045?v=4&s=48)](https://github.com/ethanpalm) [![Qkal](https://avatars.githubusercontent.com/u/77361240?v=4&s=48)](https://github.com/qkal) [![cygaar](https://avatars.githubusercontent.com/u/97691933?v=4&s=48)](https://github.com/cygaar) [![Umut CAN](https://avatars.githubusercontent.com/u/78921017?v=4&s=48)](https://github.com/U-C4N) [![Jakob](https://avatars.githubusercontent.com/u/38699060?v=4&s=48)](https://github.com/jakobdylanc) [![antons](https://avatars.githubusercontent.com/u/129705?v=4&s=48)](https://github.com/antons) [![austinm911](https://avatars.githubusercontent.com/u/31991302?v=4&s=48)](https://github.com/austinm911)
[![mahmoudashraf93](https://avatars.githubusercontent.com/u/9130129?v=4&s=48)](https://github.com/mahmoudashraf93) [![philipp-spiess](https://avatars.githubusercontent.com/u/458591?v=4&s=48)](https://github.com/philipp-spiess) [![pkrmf](https://avatars.githubusercontent.com/u/1714267?v=4&s=48)](https://github.com/pkrmf) [![joshrad-dev](https://avatars.githubusercontent.com/u/62785552?v=4&s=48)](https://github.com/joshrad-dev) [![factnest365-ops](https://avatars.githubusercontent.com/u/236534360?v=4&s=48)](https://github.com/factnest365-ops) [![yingchunbai](https://avatars.githubusercontent.com/u/33477283?v=4&s=48)](https://github.com/yingchunbai) [![AJ (@techfren)](https://avatars.githubusercontent.com/u/8023513?v=4&s=48)](https://github.com/aj47) [![Marchel Fahrezi](https://avatars.githubusercontent.com/u/53804949?v=4&s=48)](https://github.com/Alg0rix) [![futhgar](https://avatars.githubusercontent.com/u/51002668?v=4&s=48)](https://github.com/futhgar) [![Zhang](https://avatars.githubusercontent.com/u/56248212?v=4&s=48)](https://github.com/YonganZhang)
[![Rémi](https://avatars.githubusercontent.com/u/1299873?v=4&s=48)](https://github.com/remusao) [![Dan Ballance](https://avatars.githubusercontent.com/u/13839912?v=4&s=48)](https://github.com/danballance) [![Eric Su](https://avatars.githubusercontent.com/u/60202455?v=4&s=48)](https://github.com/GHesericsu) [![Kimitaka Watanabe](https://avatars.githubusercontent.com/u/167225?v=4&s=48)](https://github.com/kimitaka) [![Justin Ling](https://avatars.githubusercontent.com/u/2521993?v=4&s=48)](https://github.com/itsjling) [![Raymond Berger](https://avatars.githubusercontent.com/u/921217?v=4&s=48)](https://github.com/RayBB) [![lutr0](https://avatars.githubusercontent.com/u/76906369?v=4&s=48)](https://github.com/lutr0) [![claude](https://avatars.githubusercontent.com/u/81847?v=4&s=48)](https://github.com/claude) [![AngryBird](https://avatars.githubusercontent.com/u/48046333?v=4&s=48)](https://github.com/angrybirddd) [![Fabian Williams](https://avatars.githubusercontent.com/u/92543063?v=4&s=48)](https://github.com/fabianwilliams)
[![0x4C33](https://avatars.githubusercontent.com/u/60883781?v=4&s=48)](https://github.com/haoruilee) [![8BlT](https://avatars.githubusercontent.com/u/162764392?v=4&s=48)](https://github.com/8BlT) [![atalovesyou](https://avatars.githubusercontent.com/u/3534502?v=4&s=48)](https://github.com/atalovesyou) [![erikpr1994](https://avatars.githubusercontent.com/u/6299331?v=4&s=48)](https://github.com/erikpr1994) [![jonasjancarik](https://avatars.githubusercontent.com/u/2459191?v=4&s=48)](https://github.com/jonasjancarik) [![longmaba](https://avatars.githubusercontent.com/u/9361500?v=4&s=48)](https://github.com/longmaba) [![mitschabaude-bot](https://avatars.githubusercontent.com/u/247582884?v=4&s=48)](https://github.com/mitschabaude-bot) [![thesash](https://avatars.githubusercontent.com/u/1166151?v=4&s=48)](https://github.com/thesash) [![Max](https://avatars.githubusercontent.com/u/8418866?v=4&s=48)](https://github.com/rdev) [![easternbloc](https://avatars.githubusercontent.com/u/92585?v=4&s=48)](https://github.com/easternbloc)
[![chrisrodz](https://avatars.githubusercontent.com/u/2967620?v=4&s=48)](https://github.com/chrisrodz) [![gabriel-trigo](https://avatars.githubusercontent.com/u/38991125?v=4&s=48)](https://github.com/gabriel-trigo) [![manmal](https://avatars.githubusercontent.com/u/142797?v=4&s=48)](https://github.com/manmal) [![neist](https://avatars.githubusercontent.com/u/1029724?v=4&s=48)](https://github.com/neist) [![wes-davis](https://avatars.githubusercontent.com/u/16506720?v=4&s=48)](https://github.com/wes-davis) [![manuelhettich](https://avatars.githubusercontent.com/u/17690367?v=4&s=48)](https://github.com/ManuelHettich) [![sktbrd](https://avatars.githubusercontent.com/u/116202536?v=4&s=48)](https://github.com/sktbrd) [![larlyssa](https://avatars.githubusercontent.com/u/13128869?v=4&s=48)](https://github.com/larlyssa) [![pcty-nextgen-service-account](https://avatars.githubusercontent.com/u/112553441?v=4&s=48)](https://github.com/pcty-nextgen-service-account) [![Syhids](https://avatars.githubusercontent.com/u/671202?v=4&s=48)](https://github.com/Syhids)
[![tmchow](https://avatars.githubusercontent.com/u/517103?v=4&s=48)](https://github.com/tmchow) [![Marc Gratch](https://avatars.githubusercontent.com/u/2238658?v=4&s=48)](https://github.com/mgratch) [![xtao](https://avatars.githubusercontent.com/u/1050163?v=4&s=48)](https://github.com/xtao) [![JackyWay](https://avatars.githubusercontent.com/u/53031570?v=4&s=48)](https://github.com/JackyWay) [![Josh Phillips](https://avatars.githubusercontent.com/u/3744255?v=4&s=48)](https://github.com/j1philli) [![T5-AndyML](https://avatars.githubusercontent.com/u/22801233?v=4&s=48)](https://github.com/T5-AndyML) [![huohua-dev](https://avatars.githubusercontent.com/u/258873123?v=4&s=48)](https://github.com/huohua-dev) [![imfing](https://avatars.githubusercontent.com/u/5097752?v=4&s=48)](https://github.com/imfing) [![Randy Torres](https://avatars.githubusercontent.com/u/149904821?v=4&s=48)](https://github.com/RandyVentures) [![Marco Di Dionisio](https://avatars.githubusercontent.com/u/3519682?v=4&s=48)](https://github.com/marcodd23)
[![iamadig](https://avatars.githubusercontent.com/u/102129234?v=4&s=48)](https://github.com/Iamadig) [![humanwritten](https://avatars.githubusercontent.com/u/206531610?v=4&s=48)](https://github.com/humanwritten) [![Rob Axelsen](https://avatars.githubusercontent.com/u/13132899?v=4&s=48)](https://github.com/robaxelsen) [![Pratham Dubey](https://avatars.githubusercontent.com/u/134331217?v=4&s=48)](https://github.com/prathamdby) [![0oAstro](https://avatars.githubusercontent.com/u/79555780?v=4&s=48)](https://github.com/0oAstro) [![aaronn](https://avatars.githubusercontent.com/u/1653630?v=4&s=48)](https://github.com/aaronn) [![Arturo](https://avatars.githubusercontent.com/u/34192856?v=4&s=48)](https://github.com/afern247) [![Asleep123](https://avatars.githubusercontent.com/u/122379135?v=4&s=48)](https://github.com/Asleep123) [![dantelex](https://avatars.githubusercontent.com/u/631543?v=4&s=48)](https://github.com/dantelex) [![fcatuhe](https://avatars.githubusercontent.com/u/17382215?v=4&s=48)](https://github.com/fcatuhe)
[![gtsifrikas](https://avatars.githubusercontent.com/u/8904378?v=4&s=48)](https://github.com/gtsifrikas) [![hrdwdmrbl](https://avatars.githubusercontent.com/u/554881?v=4&s=48)](https://github.com/hrdwdmrbl) [![hugobarauna](https://avatars.githubusercontent.com/u/2719?v=4&s=48)](https://github.com/hugobarauna) [![jayhickey](https://avatars.githubusercontent.com/u/1676460?v=4&s=48)](https://github.com/jayhickey) [![jiulingyun](https://avatars.githubusercontent.com/u/126459548?v=4&s=48)](https://github.com/jiulingyun) [![Jonathan D. Rhyne (DJ-D)](https://avatars.githubusercontent.com/u/7828464?v=4&s=48)](https://github.com/jdrhyne) [![jverdi](https://avatars.githubusercontent.com/u/345050?v=4&s=48)](https://github.com/jverdi) [![kitze](https://avatars.githubusercontent.com/u/1160594?v=4&s=48)](https://github.com/kitze) [![loukotal](https://avatars.githubusercontent.com/u/18210858?v=4&s=48)](https://github.com/loukotal) [![minghinmatthewlam](https://avatars.githubusercontent.com/u/14224566?v=4&s=48)](https://github.com/minghinmatthewlam)
[![MSch](https://avatars.githubusercontent.com/u/7475?v=4&s=48)](https://github.com/MSch) [![odrobnik](https://avatars.githubusercontent.com/u/333270?v=4&s=48)](https://github.com/odrobnik) [![oswalpalash](https://avatars.githubusercontent.com/u/6431196?v=4&s=48)](https://github.com/oswalpalash) [![ratulsarna](https://avatars.githubusercontent.com/u/105903728?v=4&s=48)](https://github.com/ratulsarna) [![reeltimeapps](https://avatars.githubusercontent.com/u/637338?v=4&s=48)](https://github.com/reeltimeapps) [![snopoke](https://avatars.githubusercontent.com/u/249606?v=4&s=48)](https://github.com/snopoke) [![sreekaransrinath](https://avatars.githubusercontent.com/u/50989977?v=4&s=48)](https://github.com/sreekaransrinath) [![timkrase](https://avatars.githubusercontent.com/u/38947626?v=4&s=48)](https://github.com/timkrase)
<!-- clawtributors:end -->
<!-- clawtributors:hidden:start
default-avatar-cache: hidden from the rendered wall because these users still use GitHub's default avatar
13otkmdr
aaronveklabs
adityashaw2
ai-reviewer-qs
alexyyyander
alphonse-arianee
amitbiswal007
bbblending
bbddbb1
bitfoundry-ai
bugkillerking
carlulsoe
charzhou
cheeeee
dalomeve
danielz1z
diaspar4u
dirbalak
djangonavarro220
dobbylorenzbot
drcrinkle
drickon
eddertalmor
eengad
efe-buken
eric-fr4
eronfan
evandance
extrasmall0
ezhikkk
fuller-stack-dev
fwhite13
gambletan
gejifeng
harrington-bot
heimdallstrategy
heyhudson
hougangdev
jamesgroat
jamtujest
jaymishra-source
joe2643
joetomasone
jonathanworks
jonisjongithub
jscaldwell55
julbarth
junjunjunbong
kirillshchetinin
kyohwang
lailoo
latitudeki5223
lawrence3699
liaosvcaf
livingghost
luijoc
lukeboyett
lurebat
mahanandhi
maple778
martingarramon
matthew19990919
moktamd
moltbot886
mujiannan
mukhtharcm
mylszd
natedenh
nicholascyh
nickhood1984
nico-hoff
nikus-pan
nonggialiang
oliviareid-svg
openclaw-bot
pablohrcarvalho
patrick-barletta
pinghuachiu
private-peter
prospectore
rafaelreis-r
rexl2018
rexlunae
rhjoh
ronak-guliani
ryancontent
ryanngit
rybnikov
sandpile
sbking
shivamraut101
shuicici
slats24
slepybear
sline
socialnerd42069
solodmd
sudie-codes
sumleo
superman32432432
ted-developer
tempeste
theonejvo
tosh-hamburg
uli-will-code
w-sss
whiskyboy
wittam-01
xieyongliang
yassinebkr
yuna78
yuweuii
yxjsxy
zijiess
clawtributors:hidden:end -->
<p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a>
<a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a>
<a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a>
<a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
<a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a>
<a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
<a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a>
<a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a>
<a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a>
<a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a>
<a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
<a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a>
<a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a>
<a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a>
<a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
<a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a>
<a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a>
<a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a>
<a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a>
<a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
<a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
</p>

View File

@@ -1,325 +1,15 @@
# Security Policy
If you believe you've found a security issue in OpenClaw, please report it privately.
If you believe youve found a security issue in Clawdbot, please report it privately.
## Reporting
Report vulnerabilities directly to the repository where the issue lives:
- **Core CLI and gateway** — [openclaw/openclaw](https://github.com/openclaw/openclaw)
- **macOS desktop app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/macos)
- **iOS app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/ios)
- **Android app** — [openclaw/openclaw](https://github.com/openclaw/openclaw) (apps/android)
- **ClawHub** — [openclaw/clawhub](https://github.com/openclaw/clawhub)
- **Trust and threat model** — [openclaw/trust](https://github.com/openclaw/trust)
For issues that don't fit a specific repo, or if you're unsure, email **[security@openclaw.ai](mailto:security@openclaw.ai)** and we'll route it.
For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
### Required in Reports
1. **Title**
2. **Severity Assessment**
3. **Impact**
4. **Affected Component**
5. **Technical Reproduction**
6. **Demonstrated Impact**
7. **Environment**
8. **Remediation Advice**
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
### Report Acceptance Gate (Triage Fast Path)
For fastest triage, include all of the following:
- Exact vulnerable path (`file`, function, and line range) on a current revision.
- Tested version details (OpenClaw version and/or commit SHA).
- Reproducible PoC against latest `main` or latest released version.
- If the claim targets a released version, evidence from the shipped tag and published artifact/package for that exact version (not only `main`).
- Demonstrated impact tied to OpenClaw's documented trust boundaries.
- For exposed-secret reports: proof the credential is OpenClaw-owned (or grants access to OpenClaw-operated infrastructure/services).
- Explicit statement that the report does not rely on adversarial operators sharing one gateway host/config.
- Scope check explaining why the report is **not** covered by the Out of Scope section below.
- For command-risk/parity reports (for example obfuscation detection differences), a concrete boundary-bypass path is required (auth/approval/allowlist/sandbox). Parity-only findings are treated as hardening, not vulnerabilities.
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Common False-Positive Patterns
These are frequently reported but are typically closed with no code change:
- Prompt-injection-only chains without a boundary bypass (prompt injection is out of scope).
- Operator-intended local features (for example TUI local `!` shell) presented as remote injection.
- Reports that treat explicit operator-control surfaces (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution primitives) as vulnerabilities without demonstrating an auth/policy/sandbox boundary bypass. These capabilities are intentional when enabled and are trusted-operator features, not standalone security bugs.
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
- Reports that only show a malicious plugin executing privileged actions after a trusted operator installs/enables it.
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
- Reports that only show quoted/replied/thread/forwarded supplemental context from non-allowlisted senders being visible to the model, without demonstrating an auth, policy, approval, or sandbox boundary bypass.
- Reports that treat the Gateway HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) as if they implemented scoped operator auth (`operator.write` vs `operator.admin`). These endpoints authenticate the shared Gateway bearer secret/password and are documented full operator-access surfaces, not per-user/per-scope boundaries.
- Reports that assume `x-openclaw-scopes` can reduce or redefine shared-secret bearer auth on the OpenAI-compatible HTTP endpoints. For shared-secret auth (`gateway.auth.mode="token"` or `"password"`), those endpoints ignore narrower bearer-declared scopes and restore the full default operator scope set plus owner semantics.
- Reports that treat `POST /tools/invoke` under shared-secret bearer auth (`gateway.auth.mode="token"` or `"password"`) as a narrower per-request/per-scope authorization surface. That endpoint is designed as the same trusted-operator HTTP boundary: shared-secret bearer auth is full operator access there, narrower `x-openclaw-scopes` values do not reduce that path, and owner-only tool policy follows the shared-secret operator contract.
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- Reports that only show an ACP tool can indirectly execute, mutate, orchestrate sessions, or reach another tool/runtime without demonstrating bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. ACP silent approval is intentionally limited to narrow readonly classes; parity-only indirect-command findings are hardening, not vulnerabilities.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
- Reports that depend on pre-existing symlinked skill/workspace filesystem state (for example symlink chains involving `skills/*/SKILL.md`) without showing an untrusted path that can create/control that state.
- Missing HSTS findings on default local/loopback deployments.
- Reports against test-only harnesses, QA Lab, QE Lab, E2E fixtures, benchmark rigs, or maintainer-only debugging tools when the vulnerable code is not shipped as a supported production surface.
- Slack webhook signature findings when HTTP mode already uses signing-secret verification.
- Discord inbound webhook signature findings for paths not used by this repo's Discord integration.
- Claims that Microsoft Teams `fileConsent/invoke` `uploadInfo.uploadUrl` is attacker-controlled without demonstrating one of: auth boundary bypass, a real authenticated Teams/Bot Framework event carrying attacker-chosen URL, or compromise of the Microsoft/Bot trust path.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
- Reports that restate an already-fixed issue against later released versions without showing the vulnerable path still exists in the shipped tag or published artifact for that later version.
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security & Trust
**Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development.
## Bug Bounties
OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly.
The best way to help the project right now is by sending PRs.
## Maintainers: GHSA Updates via CLI
When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (or newer). Without it, some fields (notably CVSS) may not persist even if the request returns 200.
## Operator Trust Model (Important)
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
- Authenticated Gateway callers are treated as trusted operators for that gateway instance.
- Direct localhost/loopback Control UI and Gateway WebSocket sessions authenticated with the shared gateway secret (`token` / `password`) are in that same trusted-operator bucket. Local auto-paired device sessions on that path are expected to retain full localhost operator capability; they do not create a separate `operator.write` vs `operator.admin` security boundary.
- The HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) and direct tool endpoint (`POST /tools/invoke`) are in that same trusted-operator bucket. Passing Gateway bearer auth there is equivalent to operator access for that gateway; they do not implement a narrower `operator.write` vs `operator.admin` trust split.
- Concretely, on the OpenAI-compatible HTTP surface:
- shared-secret bearer auth (`token` / `password`) authenticates possession of the gateway operator secret
- those requests receive the full default operator scope set (`operator.admin`, `operator.read`, `operator.write`, `operator.approvals`, `operator.pairing`)
- chat-turn endpoints (`/v1/chat/completions`, `/v1/responses`) also treat those shared-secret callers as owner senders for owner-only tool policy
- `POST /tools/invoke` follows that same shared-secret rule and also treats those callers as owner senders for owner-only tool policy
- narrower `x-openclaw-scopes` headers are ignored for that shared-secret path
- only identity-bearing HTTP modes (for example trusted proxy auth or `gateway.auth.mode="none"` on private ingress) honor declared per-request operator scopes
- Session identifiers (`sessionKey`, session IDs, labels) are routing controls, not per-user authorization boundaries.
- If one operator can view data from another operator on the same gateway, that is expected in this trust model.
- OpenClaw can technically run multiple gateway instances on one machine, but recommended operations are clean separation by trust boundary.
- Recommended mode: one user per machine/host (or VPS), one gateway for that user, and one or more agents inside that gateway.
- If multiple users need OpenClaw, use one VPS (or host/OS user boundary) per user.
- For advanced setups, multiple gateways on one machine are possible, but only with strict isolation and are not the recommended default.
- Exec behavior is host-first by default: `agents.defaults.sandbox.mode` defaults to `off`.
- `tools.exec.host` defaults to `auto`: sandbox when sandbox runtime is active for the session, otherwise gateway.
- Implicit exec calls (no explicit host in the tool call) follow the same behavior.
- This is expected in OpenClaw's one-user trusted-operator model. If you need isolation, enable sandbox mode (`non-main`/`all`) and keep strict tool policy.
## Trusted Plugin Concept (Core)
Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Installing or enabling a plugin grants it the same trust level as local code running on that gateway host.
- Plugin behavior such as reading env/files or running host commands is expected inside this trust boundary.
- Security reports must show a boundary bypass (for example unauthenticated plugin load, allowlist/policy bypass, or sandbox/path-safety bypass), not only malicious behavior from a trusted-installed plugin.
## Out of Scope
- Public Internet Exposure
- Using OpenClaw in ways that the docs recommend not to
- Test-only code and maintainer harnesses, including QA Lab, QE Lab, E2E fixtures, benchmark rigs, smoke-test containers, and local debugging proxies, unless the report demonstrates that the same vulnerable behavior is reachable from shipped OpenClaw production code or a published package artifact intended for users.
- Deployments where mutually untrusted/adversarial operators share one gateway host and config (for example, reports expecting per-operator isolation for `sessions.list`, `sessions.preview`, `chat.history`, or similar control-plane reads)
- Prompt-injection-only attacks (without a policy/auth/sandbox boundary bypass)
- Reports that require write access to trusted local state (`~/.openclaw`, workspace files like `MEMORY.md` / `memory/*.md`)
- Reports where exploitability depends on attacker-controlled pre-existing symlink/hardlink filesystem state in trusted local paths (for example extraction/install target trees) unless a separate untrusted boundary bypass is shown that creates that state.
- Reports whose only claim is sandbox/workspace read expansion through trusted local skill/workspace symlink state (for example `skills/*/SKILL.md` symlink chains) unless a separate untrusted boundary bypass is shown that creates/controls that state.
- Reports whose only claim is post-approval executable identity drift on a trusted host via same-path file replacement/rewrite unless a separate untrusted boundary bypass is shown for that host write primitive.
- Reports where the only demonstrated impact is an already-authorized sender intentionally invoking a local-action command (for example `/export-session` writing to an absolute host path) without bypassing auth, sandbox, or another documented boundary
- Reports whose only claim is use of an explicit trusted-operator control surface (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution) without demonstrating an auth, policy, allowlist, approval, or sandbox bypass.
- Reports where the only claim is that a trusted-installed/enabled plugin can execute with gateway/host privileges (documented trust model behavior).
- Any report whose only claim is that an operator-enabled `dangerous*`/`dangerously*` config option weakens defaults (these are explicit break-glass tradeoffs by design)
- Reports that depend on trusted operator-supplied configuration values to trigger availability impact (for example custom regex patterns). These may still be fixed as defense-in-depth hardening, but are not security-boundary bypasses.
- Reports whose only claim is heuristic/parity drift in command-risk detection (for example obfuscation-pattern checks) across exec surfaces, without a demonstrated trust-boundary bypass. These are hardening-only findings and are not vulnerabilities; triage may close them as `invalid`/`no-action` or track them separately as low/informational hardening.
- Reports whose only claim is that an ACP-exposed tool can indirectly execute commands, mutate host state, or reach another privileged tool/runtime without demonstrating a bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. These are hardening-only findings, not vulnerabilities.
- Reports whose only claim is that exec approvals do not semantically model every interpreter/runtime loader form, subcommand, flag combination, package script, or transitive module/config import. Exec approvals bind exact request context and best-effort direct local file operands; they are not a complete semantic model of everything a runtime may load.
- Exposed secrets that are third-party/user-controlled credentials (not OpenClaw-owned and not granting access to OpenClaw-operated infrastructure/services) without demonstrated OpenClaw impact
- Reports whose only claim is host-side exec when sandbox runtime is disabled/unavailable (documented default behavior in the trusted-operator model), without a boundary bypass.
- Reports whose only claim is that a platform-provided upload destination URL is untrusted (for example Microsoft Teams `fileConsent/invoke` `uploadInfo.uploadUrl`) without proving attacker control in an authenticated production flow.
## Deployment Assumptions
OpenClaw security guidance assumes:
- The host where OpenClaw runs is within a trusted OS/admin boundary.
- Anyone who can modify `~/.openclaw` state/config (including `openclaw.json`) is effectively a trusted operator.
- A single Gateway shared by mutually untrusted people is **not a recommended setup**. Use separate gateways (or at minimum separate OS users/hosts) per trust boundary.
- Authenticated Gateway callers are treated as trusted operators. Session identifiers (for example `sessionKey`) are routing controls, not per-user authorization boundaries.
- Multiple gateway instances can run on one machine, but the recommended model is clean per-user isolation (prefer one host/VPS per user).
## One-User Trust Model (Personal Assistant)
OpenClaw's security model is "personal assistant" (one trusted operator, potentially many agents), not "shared multi-tenant bus."
- If multiple people can message the same tool-enabled agent (for example a shared Slack workspace), they can all steer that agent within its granted permissions.
- Non-owner sender status only affects owner-only tools/commands. If a non-owner can still access a non-owner-only tool on that same agent (for example `canvas`), that is within the granted tool boundary unless the report demonstrates an auth, policy, allowlist, approval, or sandbox bypass.
- Session or memory scoping reduces context bleed, but does **not** create per-user host authorization boundaries.
- For mixed-trust or adversarial users, isolate by OS user/host/gateway and use separate credentials per boundary.
- A company-shared agent can be a valid setup when users are in the same trust boundary and the agent is strictly business-only.
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
## Context Visibility and Allowlists
OpenClaw distinguishes:
- **Trigger authorization**: who can trigger the agent (`dmPolicy`, `groupPolicy`, allowlists, mention gates)
- **Context visibility**: what supplemental context is provided to the model (reply body, quoted text, thread history, forwarded metadata)
In current releases, allowlists primarily gate triggering and owner-style command access. They do not guarantee universal supplemental-context redaction across every channel/surface.
Current channel behavior is not fully uniform:
- some channels already filter parts of supplemental context by sender allowlist
- other channels still pass supplemental context as received
Reports that only show supplemental-context visibility differences are typically hardening/consistency findings unless they also demonstrate a documented boundary bypass (auth, policy, approvals, sandbox, or equivalent).
Hardening roadmap may add explicit visibility modes (for example `all`, `allowlist`, `allowlist_quote`) so operators can opt into stricter context filtering with predictable tradeoffs.
## Agent and Model Assumptions
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.
- Security boundaries come from host/config trust, auth, tool policy, sandboxing, and exec approvals.
- Prompt injection by itself is not a vulnerability report unless it crosses one of those boundaries.
- Hook/webhook-driven payloads should be treated as untrusted content; keep unsafe bypass flags disabled unless doing tightly scoped debugging (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`).
- Weak model tiers are generally easier to prompt-inject. For tool-enabled or hook-driven agents, prefer strong modern model tiers and strict tool policy (for example `tools.profile: "messaging"` or stricter), plus sandboxing where possible.
## Gateway and Node trust concept
OpenClaw separates routing from execution, but both remain inside the same operator trust boundary:
- **Gateway** is the control plane. If a caller passes Gateway auth, they are treated as a trusted operator for that Gateway.
- **Node** is an execution extension of the Gateway. Pairing a node grants operator-level remote capability on that node.
- **Exec approvals** (allowlist/ask UI) are operator guardrails to reduce accidental command execution, not a multi-tenant authorization boundary.
- Exec approvals bind exact command/cwd/env context and, when OpenClaw can identify one concrete local script/file operand, that file snapshot too. This is best-effort integrity hardening, not a complete semantic model of every interpreter/runtime loader path.
- Differences in command-risk warning heuristics between exec surfaces (`gateway`, `node`, `sandbox`) do not, by themselves, constitute a security-boundary bypass.
- For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary.
## Workspace Memory Trust Boundary
`MEMORY.md` and `memory/*.md` are plain workspace files and are treated as trusted local operator state.
- If someone can edit workspace memory files, they already crossed the trusted operator boundary.
- Memory search indexing/recall over those files is expected behavior, not a sandbox/security boundary.
- Example report pattern considered out of scope: "attacker writes malicious content into `memory/*.md`, then `memory_search` returns it."
- If you need isolation between mutually untrusted users, split by OS user or host and run separate gateways.
## Plugin Trust Boundary
Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
- Plugins can execute with the same OS privileges as the OpenClaw process.
- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary.
- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids.
## Temp Folder Boundary (Media/Sandbox)
OpenClaw uses a dedicated temp root for local media handoff and sandbox-adjacent temp artifacts:
- Preferred temp root: `/tmp/openclaw` (when available and safe on the host).
- Fallback temp root: `os.tmpdir()/openclaw` (or `openclaw-<uid>` on multi-user hosts).
Security boundary notes:
- Sandbox media validation allows absolute temp paths only under the OpenClaw-managed temp root.
- Arbitrary host tmp paths are not treated as trusted media roots.
- Plugin/extension code should use OpenClaw temp helpers (`resolvePreferredOpenClawTmpDir`, `buildRandomTempFilePath`, `withTempDownloadPath`) rather than raw `os.tmpdir()` defaults when handling media files.
- Enforcement reference points:
- temp root resolver: `src/infra/tmp-openclaw-dir.ts`
- SDK temp helpers: `src/plugin-sdk/temp-path.ts`
- messaging/channel tmp guardrail: `scripts/check-no-random-messaging-tmp.mjs`
- Email: `steipete@gmail.com`
- What to include: reproduction steps, impact assessment, and (if possible) a minimal PoC.
## Operational Guidance
For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see:
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
- `https://docs.openclaw.ai/gateway/security`
- `https://docs.clawd.bot/gateway/security`
### Tool filesystem hardening
- `tools.exec.applyPatch.workspaceOnly: true` (recommended): keeps `apply_patch` writes/deletes within the configured workspace directory.
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths and native prompt image auto-load paths to the workspace directory.
- Avoid setting `tools.exec.applyPatch.workspaceOnly: false` unless you fully trust who can trigger tool execution.
### Sub-agent delegation hardening
- Keep `sessions_spawn` denied unless you explicitly need delegated runs.
- Keep `agents.list[].subagents.allowAgents` narrow, and only include agents with sandbox settings you trust.
- When delegation must stay sandboxed, call `sessions_spawn` with `sandbox: "require"` (default is `inherit`).
- `sandbox: "require"` rejects the spawn unless the target child runtime is sandboxed.
- This prevents a less-restricted session from delegating work into an unsandboxed child by mistake.
### Web Interface Safety
OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for **local use only**.
- Recommended: keep the Gateway **loopback-only** (`127.0.0.1` / `::1`).
- Config: `gateway.bind="loopback"` (default).
- CLI: `openclaw gateway run --bind loopback`.
- `gateway.controlUi.dangerouslyDisableDeviceAuth` is intended for localhost-only break-glass use.
- OpenClaw keeps deployment flexibility by design and does not hard-forbid non-local setups.
- Non-local and other risky configurations are surfaced by `openclaw security audit` as dangerous findings.
- This operator-selected tradeoff is by design and not, by itself, a security vulnerability.
- Canvas host note: network-visible canvas is **intentional** for trusted node scenarios (LAN/tailnet).
- Expected setup: non-loopback bind + Gateway auth (token/password/trusted-proxy) + firewall/tailnet controls.
- Expected routes: `/__openclaw__/canvas/`, `/__openclaw__/a2ui/`.
- This deployment model alone is not a security vulnerability.
- Do **not** expose it to the public internet (no direct bind to `0.0.0.0`, no public reverse proxy). It is not hardened for public exposure.
- If you need remote access, prefer an SSH tunnel or Tailscale serve/funnel (so the Gateway still binds to loopback), plus strong Gateway auth.
- The Gateway HTTP surface includes the canvas host (`/__openclaw__/canvas/`, `/__openclaw__/a2ui/`). Treat canvas content as sensitive/untrusted and avoid exposing it beyond loopback unless you understand the risk.
## Runtime Requirements
### Node.js Version
OpenClaw requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches:
- CVE-2025-59466: async_hooks DoS vulnerability
- CVE-2026-21636: Permission model bypass vulnerability
Verify your Node.js version:
```bash
node --version # Should be v22.12.0 or later
```
### Docker Security
When running OpenClaw in Docker:
1. The official image runs as a non-root user (`node`) for reduced attack surface
2. Use `--read-only` flag when possible for additional filesystem protection
3. Limit container capabilities with `--cap-drop=ALL`
Example secure Docker run:
```bash
docker run --read-only --cap-drop=ALL \
-v openclaw-data:/app/data \
openclaw/openclaw:latest
```
## Security Scanning
This project uses `detect-secrets` for automated secret detection in CI/CD.
See `.detect-secrets.cfg` for configuration and `.secrets.baseline` for the baseline.
Run locally:
```bash
pip install detect-secrets==1.5.0
detect-secrets scan --baseline .secrets.baseline
```

View File

@@ -1,5 +1,5 @@
{
"originHash" : "24a723309d7a0039d3df3051106f77ac1ed7068a02508e3a6804e41d757e6c72",
"originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3",
"pins" : [
{
"identity" : "commander",
@@ -10,24 +10,6 @@
"version" : "0.2.1"
}
},
{
"identity" : "elevenlabskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
"version" : "0.1.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
@@ -45,24 +27,6 @@
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
"version" : "0.99.0"
}
},
{
"identity" : "swiftui-math",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swiftui-math",
"state" : {
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
"version" : "0.1.0"
}
},
{
"identity" : "textual",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/textual",
"state" : {
"revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38",
"version" : "0.3.1"
}
}
],
"version" : 3

View File

@@ -101,19 +101,25 @@ public enum WakeWordGate {
}
public static func commandText(
transcript _: String,
transcript: String,
segments: [WakeWordSegment],
triggerEndTime: TimeInterval)
-> String {
let threshold = triggerEndTime + 0.001
var commandWords: [String] = []
commandWords.reserveCapacity(segments.count)
for segment in segments where segment.start >= threshold {
let normalized = normalizeToken(segment.text)
if normalized.isEmpty { continue }
commandWords.append(segment.text)
if normalizeToken(segment.text).isEmpty { continue }
if let range = segment.range {
let slice = transcript[range.lowerBound...]
return String(slice).trimmingCharacters(in: Self.whitespaceAndPunctuation)
}
break
}
return commandWords.joined(separator: " ").trimmingCharacters(in: Self.whitespaceAndPunctuation)
let text = segments
.filter { $0.start >= threshold && !normalizeToken($0.text).isEmpty }
.map(\.text)
.joined(separator: " ")
return text.trimmingCharacters(in: Self.whitespaceAndPunctuation)
}
public static func matchesTextOnly(text: String, triggers: [String]) -> Bool {

Some files were not shown because too many files have changed in this diff Show More