mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
Merge branch 'main' into meow/diagnostic-liveness-stability
This commit is contained in:
@@ -127,6 +127,7 @@ Read the JSON summary. Useful fields:
|
||||
- `provider`: should be `blacksmith-testbox`
|
||||
- `leaseId`: `tbx_...`
|
||||
- `syncDelegated`: should be `true`
|
||||
- `commandPhases`: populated when the command prints `CRABBOX_PHASE:<name>`
|
||||
- `commandMs` / `totalMs`
|
||||
- `exitCode`
|
||||
|
||||
@@ -138,6 +139,62 @@ unclear:
|
||||
blacksmith testbox list
|
||||
```
|
||||
|
||||
## Observability Flags
|
||||
|
||||
Use these on debugging runs before inventing ad hoc logging:
|
||||
|
||||
- `--preflight`: prints run context, workspace mode, SSH target, remote user/cwd,
|
||||
sudo/apt, Node, pnpm, Docker, and bubblewrap. On `blacksmith-testbox`, this
|
||||
prints a delegated-unsupported note because the workflow owns setup.
|
||||
- `CRABBOX_ENV_ALLOW=NAME,...`: forwards only listed local env vars for direct
|
||||
providers and prints `set len=N secret=true` style summaries. On
|
||||
`blacksmith-testbox`, env forwarding is unsupported; put secrets in the
|
||||
Testbox workflow instead.
|
||||
- `--env-from-profile <file>` plus `--allow-env NAME`: loads simple
|
||||
`export NAME=value` / `NAME=value` lines from a local profile without
|
||||
executing it, then forwards only allowlisted names. `--allow-env` is
|
||||
repeatable and comma-separated. Profile values override ambient allowlisted
|
||||
env values for that run.
|
||||
- `--script <file>` / `--script-stdin`: upload a local script into
|
||||
`.crabbox/scripts/` and execute it on the remote box. Shebang scripts execute
|
||||
directly; scripts without a shebang run through `bash`. Arguments after `--`
|
||||
become script args.
|
||||
- `--fresh-pr owner/repo#123|URL|number`: skip dirty local sync and create a
|
||||
fresh remote checkout of the GitHub PR. Bare numbers use the current repo's
|
||||
GitHub origin. Add `--apply-local-patch` only when the current local
|
||||
`git diff --binary HEAD` should be applied on top of that PR checkout.
|
||||
- `--capture-stdout <path>` / `--capture-stderr <path>`: write remote streams to
|
||||
local files and keep binary/noisy output out of retained logs. Parent
|
||||
directories must already exist. These are direct-provider only.
|
||||
- `--capture-on-fail`: on non-zero direct-provider exits, downloads
|
||||
`.crabbox/captures/*.tar.gz` with `test-results`, `playwright-report`,
|
||||
`coverage`, JUnit XML, and nearby logs. Treat as secret-bearing until reviewed.
|
||||
- `--timing-json`: final machine-readable timing. Add
|
||||
`echo CRABBOX_PHASE:install`, `CRABBOX_PHASE:test`, etc. in long shell
|
||||
commands; direct providers and Blacksmith Testbox both report them as
|
||||
`commandPhases`.
|
||||
|
||||
Live-provider debug template for direct AWS/Hetzner leases:
|
||||
|
||||
```sh
|
||||
mkdir -p .crabbox/logs
|
||||
pnpm crabbox:run -- --provider aws \
|
||||
--preflight \
|
||||
--env-from-profile ~/.profile \
|
||||
--allow-env OPENAI_API_KEY,OPENAI_BASE_URL \
|
||||
--timing-json \
|
||||
--capture-stdout .crabbox/logs/live-provider.stdout.log \
|
||||
--capture-stderr .crabbox/logs/live-provider.stderr.log \
|
||||
--capture-on-fail \
|
||||
--shell -- \
|
||||
"echo CRABBOX_PHASE:install; pnpm install --frozen-lockfile; echo CRABBOX_PHASE:test; pnpm test:live"
|
||||
```
|
||||
|
||||
Do not pass `--capture-*`, `--download`, `--checksum`, `--force-sync-large`, or
|
||||
`--sync-only` to delegated providers. Also do not pass `--script*` or
|
||||
`--fresh-pr` there. Crabbox rejects these because the provider owns sync or
|
||||
command transport.
|
||||
|
||||
## Efficient Bug E2E Verification
|
||||
|
||||
Use the smallest Crabbox lane that proves the reported user path, not just the
|
||||
@@ -179,6 +236,11 @@ Efficient flow:
|
||||
Keep it efficient:
|
||||
|
||||
- Reuse existing E2E scripts and helper assertions before writing ad hoc shell.
|
||||
- Use `--script <file>` or `--script-stdin` for multi-line E2E commands instead
|
||||
of quote-heavy `--shell` strings on direct SSH providers.
|
||||
- Use `--fresh-pr <pr>` when validating an upstream PR in isolation from the
|
||||
local dirty tree. Add `--apply-local-patch` only when testing a local fixup on
|
||||
top of that PR.
|
||||
- Use one-shot Crabbox for a single proof; use a reusable Testbox only when
|
||||
several commands must share built images, installed packages, or live state.
|
||||
- Prefer `OPENCLAW_CURRENT_PACKAGE_TGZ` with Docker/package lanes when testing a
|
||||
@@ -285,7 +347,9 @@ Common Crabbox-only failures:
|
||||
- Slug/claim confusion: use the raw `tbx_...` id, or run one-shot without
|
||||
`--id`.
|
||||
- Sync/timing bug: add `--debug --timing-json`; capture the final JSON and the
|
||||
printed Actions URL.
|
||||
printed Actions URL. Large sync warnings now include top source directories
|
||||
by file count and a hint to update `.crabboxignore` / `sync.exclude`; inspect
|
||||
those before reaching for `--force-sync-large`.
|
||||
- Cleanup uncertainty: run `blacksmith testbox list` and stop only boxes you
|
||||
created.
|
||||
- Testbox queued/capacity pressure: do not convert a broad changed gate or full
|
||||
@@ -294,18 +358,19 @@ Common Crabbox-only failures:
|
||||
report the capacity blocker.
|
||||
|
||||
If Crabbox cannot dispatch, sync, attach, or stop but Blacksmith itself works,
|
||||
use direct Blacksmith from the repo root:
|
||||
first try the same command through the repo wrapper with `--debug` and
|
||||
`--timing-json`:
|
||||
|
||||
```sh
|
||||
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
|
||||
blacksmith testbox run --id <tbx_id> "env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
|
||||
blacksmith testbox stop --id <tbx_id>
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --debug --timing-json -- \
|
||||
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed
|
||||
```
|
||||
|
||||
Direct full suite:
|
||||
Full suite:
|
||||
|
||||
```sh
|
||||
blacksmith testbox run --id <tbx_id> "env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test"
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --debug --timing-json -- \
|
||||
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test
|
||||
```
|
||||
|
||||
Auth fallback, only when `blacksmith` says auth is missing:
|
||||
@@ -340,16 +405,15 @@ The hydration workflow owns checkout, Node/pnpm setup, dependency install,
|
||||
secrets, ready marker, and keepalive. Crabbox owns dispatch, sync, SSH command
|
||||
execution, timing, logs/results, and cleanup.
|
||||
|
||||
Minimal direct Blacksmith fallback, from repo root:
|
||||
Minimal Blacksmith-backed Crabbox run, from repo root:
|
||||
|
||||
```sh
|
||||
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
|
||||
blacksmith testbox run --id <tbx_id> "env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test:changed"
|
||||
blacksmith testbox stop --id <tbx_id>
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json -- \
|
||||
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test:changed
|
||||
```
|
||||
|
||||
Use direct Blacksmith only when Crabbox is the broken layer and Blacksmith
|
||||
itself still works. Prefer direct `blacksmith testbox list` for cleanup
|
||||
Use direct Blacksmith only when Crabbox is the broken layer and you are
|
||||
isolating a Crabbox bug. Prefer direct `blacksmith testbox list` for cleanup
|
||||
diagnostics, not as a reusable work queue.
|
||||
|
||||
Important Blacksmith footguns:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: openclaw-debugging
|
||||
description: Debug OpenClaw model, provider, tool-surface, code-mode, streaming, and live/Crabbox behavior by choosing the right logs, probes, and proof path before changing code.
|
||||
---
|
||||
|
||||
# OpenClaw Debugging
|
||||
|
||||
Use this skill when OpenClaw behavior differs between local tests, live models,
|
||||
providers, code mode, Tool Search, Crabbox, or CI, and the next move should be a
|
||||
debug signal rather than a guess.
|
||||
|
||||
## Read First
|
||||
|
||||
- `docs/logging.md` for log files, `openclaw logs`, and targeted debug flags.
|
||||
- `docs/reference/test.md` for local test commands.
|
||||
- `docs/reference/code-mode.md` for code-mode exec/wait and tool catalog rules.
|
||||
- Use `$openclaw-testing` for choosing test lanes.
|
||||
- Use `$crabbox` for broad, Docker, package, Linux, live-key, or CI-parity proof.
|
||||
|
||||
## Default Loop
|
||||
|
||||
1. State the suspected boundary: config, tool construction, provider payload,
|
||||
fetch, stream/SSE, transcript replay, worker/runtime, package/dist, or CI.
|
||||
2. Add or enable the narrowest signal that proves that boundary.
|
||||
3. Reproduce with the same provider/model/config. Do not randomly switch models
|
||||
unless the model itself is the variable being tested.
|
||||
4. Compare configured state with actual run activation.
|
||||
5. Patch the root cause.
|
||||
6. Rerun the exact failing probe, then broaden only if the contract requires it.
|
||||
|
||||
## Model Transport Logs
|
||||
|
||||
Use targeted env flags instead of global debug when the model request shape or
|
||||
stream timing matters:
|
||||
|
||||
```bash
|
||||
OPENCLAW_DEBUG_MODEL_TRANSPORT=1 openclaw gateway
|
||||
OPENCLAW_DEBUG_MODEL_PAYLOAD=tools OPENCLAW_DEBUG_SSE=events openclaw gateway
|
||||
OPENCLAW_DEBUG_MODEL_PAYLOAD=full-redacted OPENCLAW_DEBUG_SSE=peek openclaw gateway
|
||||
```
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `OPENCLAW_DEBUG_MODEL_TRANSPORT=1`: request start, fetch response, SDK
|
||||
headers, first SSE event, stream done, and transport errors at `info`.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=summary`: bounded payload summary.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=tools`: all model-facing tool names.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=full-redacted`: capped, redacted JSON payload.
|
||||
Use only while debugging; prompts/message text may still appear.
|
||||
- `OPENCLAW_DEBUG_SSE=events`: first-event and stream-completion timing.
|
||||
- `OPENCLAW_DEBUG_SSE=peek`: first five redacted SSE events.
|
||||
- `OPENCLAW_DEBUG_CODE_MODE=1`: code-mode tool-surface diagnostics.
|
||||
|
||||
Watch logs with:
|
||||
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
## Common Boundaries
|
||||
|
||||
- **Config vs activation:** config can be enabled while the run disables tools,
|
||||
is raw, has an empty allowlist, or lacks model tool support. Check the actual
|
||||
visible tools before enforcing provider payload invariants.
|
||||
- **Tool surface:** inspect final model-visible tool names, not only the tool
|
||||
registry or config. Code mode means exactly `exec` and `wait` only after it
|
||||
actually activates.
|
||||
- **Provider payload:** log fields, model id, service tier, reasoning, input
|
||||
size, metadata keys, prompt-cache key presence, and tool names before SDK
|
||||
call.
|
||||
- **Fetch vs SSE:** fetch response proves HTTP headers arrived; first SSE event
|
||||
proves provider body progress. A gap here is a stream/body/provider issue, not
|
||||
tool execution.
|
||||
- **Worker/dist:** run `pnpm build` when touching workers, dynamic imports,
|
||||
package exports, lazy runtime boundaries, or published paths.
|
||||
- **Live keys:** check local `~/.profile` for key presence/length before saying
|
||||
live proof is blocked. Never print secrets.
|
||||
|
||||
## Code Pointers
|
||||
|
||||
- Model payload + Responses stream:
|
||||
`src/agents/openai-transport-stream.ts`
|
||||
- Guarded fetch/timing:
|
||||
`src/agents/provider-transport-fetch.ts`
|
||||
- OpenAI/Codex provider wrappers:
|
||||
`src/agents/pi-embedded-runner/openai-stream-wrappers.ts`
|
||||
- Tool construction, Tool Search, code-mode activation:
|
||||
`src/agents/pi-embedded-runner/run/attempt.ts`
|
||||
- Code-mode runtime and worker:
|
||||
`src/agents/code-mode.ts`
|
||||
`src/agents/code-mode.worker.ts`
|
||||
- Tool Search catalog:
|
||||
`src/agents/tool-search.ts`
|
||||
|
||||
## Proof Choice
|
||||
|
||||
- Single helper/payload bug: local targeted Vitest.
|
||||
- Docs/logging-only: `pnpm check:docs` and `git diff --check`.
|
||||
- Worker/dist/lazy import/package surface: targeted tests plus `pnpm build`.
|
||||
- Live provider/model behavior: same provider/model with debug flags and a real
|
||||
key if available.
|
||||
- Docker/package/Linux/CI-parity: `$crabbox`.
|
||||
- CI failure: exact SHA, relevant job only, logs only after failure/completion.
|
||||
|
||||
## Output Habit
|
||||
|
||||
Report:
|
||||
|
||||
- boundary tested
|
||||
- exact command/env shape, redacted
|
||||
- observed signal, such as tool names or first SSE event timing
|
||||
- fix location
|
||||
- narrow proof and any remaining risk
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "OpenClaw Debugging"
|
||||
short_description: "Debug model, tool, stream, and live behavior"
|
||||
default_prompt: "Use $openclaw-debugging to identify the right OpenClaw debug boundary, turn on targeted logs, and choose the narrowest local or Crabbox proof."
|
||||
@@ -92,11 +92,11 @@ barrels, package-boundary tests, or extension suites.
|
||||
- runtime capture should be quiet and config-tolerant.
|
||||
- command output should include wall time, exit code, and peak RSS when
|
||||
available.
|
||||
4. For broad or package-heavy plugin proof, use Blacksmith Testbox by default on
|
||||
maintainer machines. Warm once and reuse the same box:
|
||||
- `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`
|
||||
- `blacksmith testbox run --id <ID> "OPENCLAW_TESTBOX=1 pnpm test:extensions:batch <ids>"`
|
||||
- stop the box when done.
|
||||
4. For broad or package-heavy plugin proof, use Crabbox-backed Blacksmith
|
||||
Testbox by default on maintainer machines:
|
||||
- `pnpm crabbox:run -- --provider blacksmith-testbox --timing-json -- OPENCLAW_TESTBOX=1 pnpm test:extensions:batch <ids>`
|
||||
- add `--keep`/`--id <id-or-slug>` only when several commands must share one
|
||||
warmed box; stop it with `pnpm crabbox:stop -- <id-or-slug>`.
|
||||
5. If plugin performance is package-artifact sensitive, switch to
|
||||
`openclaw-pre-release-plugin-testing` and Package Acceptance rather than
|
||||
trusting source-only timing.
|
||||
|
||||
@@ -36,14 +36,11 @@ Prove the touched surface first. Do not reflexively run the whole suite.
|
||||
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
|
||||
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
|
||||
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
|
||||
- For Blacksmith Testbox proof, reuse only an id warmed and claimed in this
|
||||
operator session. `blacksmith testbox list` is diagnostics only; a listed id
|
||||
can have a local key and still carry stale rsync state from another lane.
|
||||
After warmup, run `pnpm testbox:claim --id <id>`, then prefer
|
||||
`pnpm testbox:run --id <id> -- "<command>"` for OpenClaw gates so stale
|
||||
org-visible ids fail fast before syncing. Claims older than 12 hours are
|
||||
stale unless `OPENCLAW_TESTBOX_CLAIM_TTL_MINUTES` is explicitly set for long
|
||||
work.
|
||||
- For Blacksmith Testbox proof, use Crabbox first. `pnpm crabbox:run -- --provider
|
||||
blacksmith-testbox --timing-json -- <command...>` warms, claims, syncs, runs,
|
||||
reports, and cleans up one-shot boxes. Reuse only an id/slug created in this
|
||||
operator session; `blacksmith testbox list` is diagnostics only, not a shared
|
||||
work queue.
|
||||
|
||||
## Local Test Shortcuts
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Mantis Telegram Desktop Proof Agent
|
||||
|
||||
You are Mantis running native Telegram Desktop visual proof for an OpenClaw PR.
|
||||
|
||||
Goal: inspect the pull request, decide the best Telegram-visible behavior to
|
||||
prove, run before/after native Telegram Desktop sessions, iterate until the GIFs
|
||||
are visually good, and leave a Mantis evidence manifest for the workflow to
|
||||
publish.
|
||||
|
||||
Hard limits:
|
||||
|
||||
- Do not post GitHub comments or reviews. The workflow publishes the manifest.
|
||||
- Do not commit, push, label, merge, or edit PR metadata.
|
||||
- Do not print secrets, credential payloads, Telegram profile data, TDLib data,
|
||||
or raw session archives.
|
||||
- Do not use fixed `/status` proof unless it genuinely proves the PR.
|
||||
- Do not finish with tiny, cropped-wrong, off-bottom, or sidebar-heavy GIFs.
|
||||
- Do not invent a generic proof. The proof must match the PR behavior.
|
||||
|
||||
Inputs are provided as environment variables:
|
||||
|
||||
- `MANTIS_PR_NUMBER`
|
||||
- `BASELINE_REF`
|
||||
- `BASELINE_SHA`
|
||||
- `CANDIDATE_REF`
|
||||
- `CANDIDATE_SHA`
|
||||
- `MANTIS_OUTPUT_DIR`
|
||||
- `MANTIS_INSTRUCTIONS`
|
||||
- `CRABBOX_PROVIDER`
|
||||
- `OPENCLAW_TELEGRAM_USER_PROOF_CMD`
|
||||
- optional `CRABBOX_LEASE_ID`
|
||||
|
||||
Required workflow:
|
||||
|
||||
1. Read `.agents/skills/telegram-crabbox-e2e-proof/SKILL.md`.
|
||||
2. Inspect the PR with `gh pr view "$MANTIS_PR_NUMBER"` and
|
||||
`gh pr diff "$MANTIS_PR_NUMBER"` when `MANTIS_PR_NUMBER` is set. If the run
|
||||
came from workflow dispatch without a PR number, inspect
|
||||
`BASELINE_SHA..CANDIDATE_SHA`.
|
||||
3. Decide what Telegram message, mock model response, command, callback, button,
|
||||
media, or sequence best proves the PR. Use `MANTIS_INSTRUCTIONS` as extra
|
||||
maintainer guidance, not as a replacement for reading the PR.
|
||||
4. Create detached worktrees under
|
||||
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/baseline` and
|
||||
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/candidate`, then
|
||||
install and build each worktree with the repo's normal `pnpm` commands.
|
||||
5. In each worktree, run the real-user Telegram Crabbox proof flow from the
|
||||
skill with `$OPENCLAW_TELEGRAM_USER_PROOF_CMD`; do not run
|
||||
`pnpm qa:telegram-user:crabbox` directly. The proof command comes from the
|
||||
trusted workflow checkout while the current directory controls which
|
||||
baseline or candidate OpenClaw build is tested. Use
|
||||
`$OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT`, the workflow-provided `crabbox`
|
||||
binary, and the workflow-provided local `ffmpeg`/`ffprobe`; do not generate,
|
||||
install, or patch replacement proof tooling during the run. Use the same
|
||||
proof idea for baseline and candidate. You may iterate and rerun if the
|
||||
visual result is not convincing.
|
||||
6. Open Telegram Desktop directly to the newest relevant message with the
|
||||
runner `view` command before finishing each recording. Keep the chat scrolled
|
||||
to the bottom so new proof messages appear in-frame.
|
||||
7. Finish each session with `--preview-crop telegram-window`.
|
||||
8. Build `${MANTIS_OUTPUT_DIR}/mantis-evidence.json` with:
|
||||
|
||||
```bash
|
||||
node scripts/mantis/build-telegram-desktop-proof-evidence.mjs \
|
||||
--output-dir "$MANTIS_OUTPUT_DIR" \
|
||||
--baseline-repo-root <baseline-worktree> \
|
||||
--baseline-output-dir <baseline-session-output-dir> \
|
||||
--baseline-ref "$BASELINE_REF" \
|
||||
--baseline-sha "$BASELINE_SHA" \
|
||||
--candidate-repo-root <candidate-worktree> \
|
||||
--candidate-output-dir <candidate-session-output-dir> \
|
||||
--candidate-ref "$CANDIDATE_REF" \
|
||||
--candidate-sha "$CANDIDATE_SHA" \
|
||||
--scenario-label telegram-desktop-proof
|
||||
```
|
||||
|
||||
Visual acceptance:
|
||||
|
||||
- The GIFs show native Telegram Desktop, not transcript HTML.
|
||||
- Telegram is in single-chat proof view with no left chat list or right info
|
||||
pane.
|
||||
- The proof behavior is visible without reading logs.
|
||||
- Main and PR GIFs are comparable side by side.
|
||||
- The final relevant message or button is visible near the bottom.
|
||||
- If one run fails because the PR genuinely changes behavior, still finish the
|
||||
session and produce the manifest if useful visual artifacts exist.
|
||||
|
||||
Expected final state:
|
||||
|
||||
- `${MANTIS_OUTPUT_DIR}/mantis-evidence.json` exists.
|
||||
- The manifest contains paired `motionPreview` artifacts labeled `Main` and
|
||||
`This PR`.
|
||||
- The worktree can be dirty only under `.artifacts/`.
|
||||
@@ -183,6 +183,7 @@ jobs:
|
||||
ITEM_NUMBER: ${{ github.event.issue.number }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
SOURCE_ACTION: ${{ github.event.action }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -213,13 +214,39 @@ jobs:
|
||||
else
|
||||
echo "::notice::Skipping ClawSweeper comment acknowledgement because no target token is configured."
|
||||
fi
|
||||
status_comment_id=""
|
||||
if [ -n "$TARGET_TOKEN" ]; then
|
||||
case "$AUTHOR_ASSOCIATION" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
status_body="$(printf '%s\n' \
|
||||
"<!-- clawsweeper-command-ack:$COMMENT_ID -->" \
|
||||
"🦞👀" \
|
||||
"ClawSweeper picked this up." \
|
||||
"" \
|
||||
"Command router queued. I will update this comment with the next step.")"
|
||||
status_payload="$(jq -nc --arg body "$status_body" '{body:$body}')"
|
||||
status_err="$(mktemp)"
|
||||
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh api \
|
||||
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \
|
||||
--method POST \
|
||||
--input - <<< "$status_payload" 2>"$status_err")"; then
|
||||
status_comment_id="$(jq -r '.id // empty' <<< "$status_response")"
|
||||
else
|
||||
cat "$status_err" >&2
|
||||
echo "::warning::Could not create ClawSweeper queued status comment; dispatching command router without one."
|
||||
fi
|
||||
rm -f "$status_err"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
payload="$(jq -nc \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--argjson item_number "$ITEM_NUMBER" \
|
||||
--argjson comment_id "$COMMENT_ID" \
|
||||
--arg status_comment_id "$status_comment_id" \
|
||||
--arg source_event "issue_comment" \
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
'{event_type:"clawsweeper_comment",client_payload:{target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action}}')"
|
||||
'{event_type:"clawsweeper_comment",client_payload:({target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action,max_comments:"1"} + (if $status_comment_id != "" then {status_comment_id:($status_comment_id|tonumber)} else {} end))}')"
|
||||
if GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
- discord-thread-reply-filepath-attachment
|
||||
- slack-desktop-smoke
|
||||
- telegram-live
|
||||
- telegram-desktop-proof
|
||||
baseline_ref:
|
||||
description: Optional baseline ref for before/after scenarios
|
||||
required: false
|
||||
@@ -103,6 +104,23 @@ jobs:
|
||||
fi
|
||||
gh "${args[@]}"
|
||||
;;
|
||||
telegram-desktop-proof)
|
||||
baseline_ref="$BASELINE_REF"
|
||||
if [[ -z "$baseline_ref" || "$baseline_ref" == "0bf06e953fdda290799fc9fb9244a8f67fdae593" ]]; then
|
||||
baseline_ref="main"
|
||||
fi
|
||||
args=(
|
||||
workflow run mantis-telegram-desktop-proof.yml
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
--ref main
|
||||
-f "baseline_ref=${baseline_ref}"
|
||||
-f "candidate_ref=${CANDIDATE_REF}"
|
||||
)
|
||||
if [[ -n "${PR_NUMBER:-}" ]]; then
|
||||
args+=(-f "pr_number=${PR_NUMBER}")
|
||||
fi
|
||||
gh "${args[@]}"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported Mantis scenario: ${SCENARIO_ID}" >&2
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
name: Mantis Telegram Desktop Proof
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
baseline_ref:
|
||||
description: Ref, tag, or SHA to capture as the before GIF
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
candidate_ref:
|
||||
description: Ref, tag, or SHA to capture as the after GIF
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
pr_number:
|
||||
description: Optional PR number to receive the QA evidence comment
|
||||
required: false
|
||||
type: string
|
||||
instructions:
|
||||
description: Optional freeform proof instructions for the agent
|
||||
required: false
|
||||
type: string
|
||||
crabbox_provider:
|
||||
description: Crabbox provider for the native Telegram Desktop capture
|
||||
required: false
|
||||
default: aws
|
||||
type: choice
|
||||
options:
|
||||
- aws
|
||||
- hetzner
|
||||
crabbox_lease_id:
|
||||
description: Optional existing Crabbox desktop lease id or slug to reuse
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: mantis-telegram-desktop-proof-${{ github.event.issue.number || inputs.pr_number || inputs.candidate_ref || github.run_id }}-${{ github.run_attempt }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "11.0.8"
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1"
|
||||
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
|
||||
CRABBOX_REF: main
|
||||
MANTIS_OUTPUT_DIR: .artifacts/qa-e2e/mantis/telegram-desktop-proof
|
||||
|
||||
jobs:
|
||||
authorize_actor:
|
||||
name: Authorize workflow actor
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, '@openclaw-mantis') ||
|
||||
contains(github.event.comment.body, '/openclaw-mantis')
|
||||
)
|
||||
)
|
||||
}}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Require maintainer-level repository access
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allowed = new Set(["admin", "maintain", "write"]);
|
||||
const { owner, repo } = context.repo;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: context.actor,
|
||||
});
|
||||
const permission = data.permission;
|
||||
core.info(`Actor ${context.actor} permission: ${permission}`);
|
||||
if (!allowed.has(permission)) {
|
||||
core.setFailed(
|
||||
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
|
||||
);
|
||||
}
|
||||
|
||||
resolve_request:
|
||||
name: Resolve Mantis request
|
||||
needs: authorize_actor
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
baseline_ref: ${{ steps.resolve.outputs.baseline_ref }}
|
||||
candidate_ref: ${{ steps.resolve.outputs.candidate_ref }}
|
||||
crabbox_provider: ${{ steps.resolve.outputs.crabbox_provider }}
|
||||
instructions: ${{ steps.resolve.outputs.instructions }}
|
||||
lease_id: ${{ steps.resolve.outputs.lease_id }}
|
||||
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
||||
request_source: ${{ steps.resolve.outputs.request_source }}
|
||||
should_run: ${{ steps.resolve.outputs.should_run }}
|
||||
steps:
|
||||
- name: Resolve refs and target PR
|
||||
id: resolve
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const eventName = context.eventName;
|
||||
|
||||
function setOutput(name, value) {
|
||||
core.setOutput(name, value ?? "");
|
||||
core.info(`${name}=${value ?? ""}`);
|
||||
}
|
||||
|
||||
if (eventName === "workflow_dispatch") {
|
||||
const inputs = context.payload.inputs ?? {};
|
||||
setOutput("should_run", "true");
|
||||
setOutput("baseline_ref", inputs.baseline_ref || "main");
|
||||
setOutput("candidate_ref", inputs.candidate_ref || "main");
|
||||
setOutput("pr_number", inputs.pr_number || "");
|
||||
setOutput("instructions", inputs.instructions || "");
|
||||
setOutput("crabbox_provider", inputs.crabbox_provider || "aws");
|
||||
setOutput("lease_id", inputs.crabbox_lease_id || "");
|
||||
setOutput("request_source", "workflow_dispatch");
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventName !== "issue_comment") {
|
||||
core.setFailed(`Unsupported event: ${eventName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
const body = context.payload.comment?.body ?? "";
|
||||
if (!issue?.pull_request) {
|
||||
core.setFailed("Mantis issue_comment trigger requires a pull request comment.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = body.toLowerCase();
|
||||
const requested =
|
||||
(normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) &&
|
||||
normalized.includes("telegram") &&
|
||||
(normalized.includes("desktop") || normalized.includes("native")) &&
|
||||
normalized.includes("proof");
|
||||
if (!requested) {
|
||||
core.notice("Comment mentioned Mantis but did not request Telegram desktop proof.");
|
||||
setOutput("should_run", "false");
|
||||
setOutput("baseline_ref", "");
|
||||
setOutput("candidate_ref", "");
|
||||
setOutput("pr_number", "");
|
||||
setOutput("instructions", "");
|
||||
setOutput("crabbox_provider", "");
|
||||
setOutput("lease_id", "");
|
||||
setOutput("request_source", "unsupported_issue_comment");
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number,
|
||||
});
|
||||
let mergedBaseline = "";
|
||||
let mergedCandidate = "";
|
||||
if (pr.merged) {
|
||||
const { data: commits } = await github.rest.pulls.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
mergedCandidate = pr.merge_commit_sha || commits.at(-1)?.sha || "";
|
||||
mergedBaseline = mergedCandidate && commits.length > 0 ? `${mergedCandidate}~${commits.length}` : "";
|
||||
}
|
||||
const baselineMatch = body.match(/(?:baseline|base)[\s:=]+([^\s`]+)/i);
|
||||
const candidateMatch = body.match(/(?:candidate|head)[\s:=]+([^\s`]+)/i);
|
||||
const providerMatch = body.match(/(?:provider|crabbox_provider)[\s:=]+([^\s`]+)/i);
|
||||
const leaseMatch = body.match(/(?:lease|lease_id|crabbox_lease_id)[\s:=]+([^\s`]+)/i);
|
||||
const provider = providerMatch?.[1] || "aws";
|
||||
if (!["aws", "hetzner"].includes(provider)) {
|
||||
core.setFailed(`Unsupported Crabbox provider for Mantis Telegram desktop proof: ${provider}`);
|
||||
return;
|
||||
}
|
||||
const rawCandidate = candidateMatch?.[1];
|
||||
const candidate =
|
||||
rawCandidate && !["head", "pr", "pr-head"].includes(rawCandidate.toLowerCase())
|
||||
? rawCandidate
|
||||
: mergedCandidate || pr.head.sha;
|
||||
|
||||
setOutput("should_run", "true");
|
||||
setOutput("baseline_ref", baselineMatch?.[1] || mergedBaseline || "main");
|
||||
setOutput("candidate_ref", candidate);
|
||||
setOutput("pr_number", String(issue.number));
|
||||
setOutput("instructions", body);
|
||||
setOutput("crabbox_provider", provider);
|
||||
setOutput("lease_id", leaseMatch?.[1] || "");
|
||||
setOutput("request_source", "issue_comment");
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: "eyes",
|
||||
}).catch((error) => core.warning(`Could not add eyes reaction: ${error.message}`));
|
||||
|
||||
validate_refs:
|
||||
name: Validate selected refs
|
||||
needs: resolve_request
|
||||
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
baseline_revision: ${{ steps.validate.outputs.baseline_revision }}
|
||||
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
|
||||
steps:
|
||||
- name: Checkout harness ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate refs are trusted
|
||||
id: validate
|
||||
env:
|
||||
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
|
||||
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
if [[ -n "${PR_NUMBER:-}" ]]; then
|
||||
git fetch --no-tags origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr/${PR_NUMBER}" || true
|
||||
fi
|
||||
|
||||
validate_ref() {
|
||||
local label="$1"
|
||||
local input_ref="$2"
|
||||
local revision=""
|
||||
local reason=""
|
||||
|
||||
if ! revision="$(git rev-parse --verify "${input_ref}^{commit}" 2>/dev/null)"; then
|
||||
echo "${label} ref '${input_ref}' is not available in the workflow checkout." >&2
|
||||
exit 1
|
||||
fi
|
||||
if git merge-base --is-ancestor "$revision" refs/remotes/origin/main; then
|
||||
reason="main-ancestor"
|
||||
elif git tag --points-at "$revision" | grep -Eq '^v'; then
|
||||
reason="release-tag"
|
||||
else
|
||||
local pr_head_count
|
||||
pr_head_count="$(
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${revision}/pulls" \
|
||||
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${revision}"'")] | length'
|
||||
)"
|
||||
if [[ "$pr_head_count" != "0" ]]; then
|
||||
reason="open-pr-head"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$reason" ]]; then
|
||||
echo "${label} ref '${input_ref}' resolved to ${revision}, which is not trusted for this secret-bearing Mantis run." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$revision"
|
||||
}
|
||||
|
||||
baseline_revision="$(validate_ref baseline "$BASELINE_REF")"
|
||||
candidate_revision="$(validate_ref candidate "$CANDIDATE_REF")"
|
||||
echo "baseline_revision=${baseline_revision}" >> "$GITHUB_OUTPUT"
|
||||
echo "candidate_revision=${candidate_revision}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "baseline: \`${BASELINE_REF}\`"
|
||||
echo "baseline SHA: \`${baseline_revision}\`"
|
||||
echo "candidate: \`${CANDIDATE_REF}\`"
|
||||
echo "candidate SHA: \`${candidate_revision}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
run_telegram_desktop_proof:
|
||||
name: Run agentic native Telegram proof
|
||||
needs: [resolve_request, validate_refs]
|
||||
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 360
|
||||
environment: qa-live-shared
|
||||
outputs:
|
||||
comparison_status: ${{ steps.inspect.outputs.comparison_status }}
|
||||
output_dir: ${{ steps.inspect.outputs.output_dir }}
|
||||
steps:
|
||||
- name: Checkout harness ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
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"
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
- name: Install Crabbox CLI
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
install_dir="${RUNNER_TEMP}/crabbox"
|
||||
mkdir -p "$install_dir/src"
|
||||
git init "$install_dir/src"
|
||||
git -C "$install_dir/src" remote add origin https://github.com/openclaw/crabbox.git
|
||||
git -C "$install_dir/src" fetch --depth 1 origin "$CRABBOX_REF"
|
||||
git -C "$install_dir/src" checkout --detach FETCH_HEAD
|
||||
go build -C "$install_dir/src" -o "$install_dir/crabbox" ./cmd/crabbox
|
||||
sudo install -m 0755 "$install_dir/crabbox" /usr/local/bin/crabbox
|
||||
crabbox --version
|
||||
crabbox media preview --help >/dev/null
|
||||
|
||||
- name: Install local proof tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f scripts/e2e/telegram-user-driver.py
|
||||
cat >"${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec node --import tsx "${GITHUB_WORKSPACE}/scripts/e2e/telegram-user-crabbox-proof.ts" "$@"
|
||||
EOF
|
||||
chmod 0755 "${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof"
|
||||
sudo install -m 0755 "${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof" /usr/local/bin/openclaw-telegram-user-crabbox-proof
|
||||
/usr/local/bin/openclaw-telegram-user-crabbox-proof --help >/dev/null
|
||||
media_tools="${RUNNER_TEMP}/mantis-media-tools"
|
||||
install -d "$media_tools"
|
||||
curl --fail --location --retry 3 --retry-delay 2 \
|
||||
--connect-timeout 15 --max-time 180 \
|
||||
https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz \
|
||||
--output "$media_tools/ffmpeg.tar.xz"
|
||||
tar -xJf "$media_tools/ffmpeg.tar.xz" -C "$media_tools"
|
||||
bin_dir="$(find "$media_tools" -type d -path '*/bin' | head -n 1)"
|
||||
sudo install -m 0755 "$bin_dir/ffmpeg" /usr/local/bin/ffmpeg
|
||||
sudo install -m 0755 "$bin_dir/ffprobe" /usr/local/bin/ffprobe
|
||||
ffmpeg -version >/dev/null
|
||||
ffprobe -version >/dev/null
|
||||
|
||||
- name: Ensure agent key exists
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "Missing OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Prepare Codex user
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo useradd --create-home --shell /bin/bash codex
|
||||
{
|
||||
printf '%s\n' 'Defaults env_keep += "CODEX_HOME CODEX_INTERNAL_ORIGINATOR_OVERRIDE"'
|
||||
printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"'
|
||||
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER"'
|
||||
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
|
||||
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
|
||||
printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"'
|
||||
} | sudo tee /etc/sudoers.d/mantis-codex-env >/dev/null
|
||||
sudo chmod 0440 /etc/sudoers.d/mantis-codex-env
|
||||
codex_home="/tmp/mantis-codex-home-${GITHUB_RUN_ID}"
|
||||
sudo install -d -m 0770 -o codex -g codex "$codex_home"
|
||||
sudo setfacl -m u:runner:rwx,u:codex:rwx "$codex_home"
|
||||
sudo setfacl -d -m u:runner:rwx,u:codex:rwx "$codex_home"
|
||||
workspace_parent="$(dirname "$GITHUB_WORKSPACE")"
|
||||
while [ "$workspace_parent" != "/" ]; do
|
||||
sudo setfacl -m u:codex:--x "$workspace_parent"
|
||||
[ "$workspace_parent" = "/home/runner" ] && break
|
||||
workspace_parent="$(dirname "$workspace_parent")"
|
||||
done
|
||||
sudo chown -R codex:codex "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Run Codex Mantis Telegram agent
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
|
||||
env:
|
||||
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
|
||||
BASELINE_SHA: ${{ needs.validate_refs.outputs.baseline_revision }}
|
||||
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
||||
CANDIDATE_SHA: ${{ needs.validate_refs.outputs.candidate_revision }}
|
||||
CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }}
|
||||
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
|
||||
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
|
||||
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
|
||||
CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }}
|
||||
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MANTIS_INSTRUCTIONS: ${{ needs.resolve_request.outputs.instructions }}
|
||||
MANTIS_OUTPUT_DIR: ${{ env.MANTIS_OUTPUT_DIR }}
|
||||
MANTIS_PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
|
||||
OPENCLAW_TELEGRAM_USER_CRABBOX_BIN: /usr/local/bin/crabbox
|
||||
OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
|
||||
OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT: ${{ github.workspace }}/scripts/e2e/telegram-user-driver.py
|
||||
OPENCLAW_TELEGRAM_USER_PROOF_CMD: /usr/local/bin/openclaw-telegram-user-crabbox-proof
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/codex/prompts/mantis-telegram-desktop-proof.md
|
||||
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
effort: high
|
||||
sandbox: danger-full-access
|
||||
codex-home: /tmp/mantis-codex-home-${{ github.run_id }}
|
||||
safety-strategy: unprivileged-user
|
||||
codex-user: codex
|
||||
|
||||
- name: Inspect Mantis evidence manifest
|
||||
id: inspect
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output_dir="$MANTIS_OUTPUT_DIR"
|
||||
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
|
||||
manifest="$output_dir/mantis-evidence.json"
|
||||
if [[ ! -f "$manifest" ]]; then
|
||||
echo "Mantis agent did not produce ${manifest}." >&2
|
||||
exit 1
|
||||
fi
|
||||
comparison_status="$(jq -r 'if .comparison.pass then "pass" else "fail" end' "$manifest")"
|
||||
echo "comparison_status=${comparison_status}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Mantis Telegram desktop artifacts
|
||||
id: upload_artifact
|
||||
if: ${{ always() && steps.inspect.outputs.output_dir != '' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mantis-telegram-desktop-proof-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.inspect.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Comment PR with inline QA evidence
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.inspect.outputs.output_dir != '' }}
|
||||
env:
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
root="${{ steps.inspect.outputs.output_dir }}"
|
||||
if [[ ! -f "$root/mantis-evidence.json" ]]; then
|
||||
echo "No Mantis evidence manifest found; skipping PR evidence comment."
|
||||
exit 0
|
||||
fi
|
||||
artifact_url_args=()
|
||||
if [[ -n "${ARTIFACT_URL:-}" ]]; then
|
||||
artifact_url_args=(--artifact-url "$ARTIFACT_URL")
|
||||
fi
|
||||
node scripts/mantis/publish-pr-evidence.mjs \
|
||||
--manifest "$root/mantis-evidence.json" \
|
||||
--target-pr "$TARGET_PR" \
|
||||
--artifact-root "mantis/telegram-desktop/pr-${TARGET_PR}/run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
--marker "<!-- mantis-telegram-desktop-proof -->" \
|
||||
"${artifact_url_args[@]}" \
|
||||
--run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--request-source "$REQUEST_SOURCE"
|
||||
|
||||
- name: Fail when Mantis Telegram desktop proof failed
|
||||
if: ${{ always() && steps.inspect.outputs.output_dir != '' && steps.inspect.outputs.comparison_status != 'pass' }}
|
||||
env:
|
||||
COMPARISON_STATUS: ${{ steps.inspect.outputs.comparison_status }}
|
||||
run: |
|
||||
echo "Mantis Telegram desktop proof failed: comparison=${COMPARISON_STATUS:-unset}." >&2
|
||||
exit 1
|
||||
@@ -44,7 +44,7 @@ concurrency:
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "10.33.0"
|
||||
PNPM_VERSION: "11.0.8"
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1"
|
||||
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
|
||||
CRABBOX_REF: main
|
||||
|
||||
@@ -455,7 +455,7 @@ jobs:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -490,12 +490,12 @@ jobs:
|
||||
- name: Verify live prompt cache floors
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
echo "live-cache attempt ${attempt}/3"
|
||||
if timeout --foreground --kill-after=30s 12m pnpm test:live:cache; then
|
||||
for attempt in 1 2; do
|
||||
echo "live-cache attempt ${attempt}/2"
|
||||
if timeout --foreground --kill-after=30s 8m pnpm test:live:cache; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$attempt" == "3" ]]; then
|
||||
if [[ "$attempt" == "2" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 15))
|
||||
|
||||
@@ -346,6 +346,185 @@ jobs:
|
||||
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
|
||||
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
|
||||
|
||||
plugin-prerelease-inspector:
|
||||
permissions:
|
||||
contents: read
|
||||
name: plugin-prerelease-inspector
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_prerelease_suite == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preflight.outputs.checkout_revision }}
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Run plugin inspector advisory sweep
|
||||
env:
|
||||
OPENCLAW_PLUGIN_INSPECTOR_VERSION: "0.3.10"
|
||||
OPENCLAW_PLUGIN_INSPECTOR_ROOT: .artifacts/plugin-inspector
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$OPENCLAW_PLUGIN_INSPECTOR_ROOT"
|
||||
set +e
|
||||
node --input-type=module <<'EOF'
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const artifactRoot = process.env.OPENCLAW_PLUGIN_INSPECTOR_ROOT;
|
||||
if (!artifactRoot) {
|
||||
throw new Error("OPENCLAW_PLUGIN_INSPECTOR_ROOT is required");
|
||||
}
|
||||
|
||||
const readJson = async (filePath) => JSON.parse(await readFile(filePath, "utf8"));
|
||||
const inferSeams = (pluginManifest, packageJson) => {
|
||||
const contracts = Object.keys(pluginManifest?.contracts ?? {});
|
||||
if (contracts.includes("tools")) {
|
||||
return ["dynamic-tool"];
|
||||
}
|
||||
const openclawPackage = packageJson?.openclaw ?? {};
|
||||
if (openclawPackage.extensions || openclawPackage.runtimeExtensions) {
|
||||
return ["plugin-runtime"];
|
||||
}
|
||||
return ["plugin-metadata"];
|
||||
};
|
||||
|
||||
const extensionRoot = path.resolve("extensions");
|
||||
const fixtures = [];
|
||||
for (const entry of await readdir(extensionRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const relativePath = `extensions/${entry.name}`;
|
||||
const packagePath = path.join(extensionRoot, entry.name, "package.json");
|
||||
const manifestPath = path.join(extensionRoot, entry.name, "openclaw.plugin.json");
|
||||
if (!existsSync(packagePath) || !existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
const packageJson = await readJson(packagePath);
|
||||
const pluginManifest = await readJson(manifestPath);
|
||||
fixtures.push({
|
||||
id: entry.name,
|
||||
name: pluginManifest.name ?? packageJson.name ?? entry.name,
|
||||
path: relativePath,
|
||||
priority: "high",
|
||||
repo: "local",
|
||||
seams: inferSeams(pluginManifest, packageJson),
|
||||
why: "bundled OpenClaw plugin prerelease advisory fixture",
|
||||
});
|
||||
}
|
||||
fixtures.sort((left, right) => left.id.localeCompare(right.id));
|
||||
if (fixtures.length === 0) {
|
||||
throw new Error("No bundled plugin fixtures found under extensions/");
|
||||
}
|
||||
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
const config = `${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
submoduleRoot: ".",
|
||||
openclaw: {
|
||||
defaultCheckoutPath: ".",
|
||||
},
|
||||
fixtures,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
await writeFile("plugin-inspector.config.json", config, "utf8");
|
||||
await writeFile(path.join(artifactRoot, "plugin-inspector.config.json"), config, "utf8");
|
||||
EOF
|
||||
config_status=$?
|
||||
set -e
|
||||
echo "$config_status" > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/config-exit-code.txt"
|
||||
|
||||
if [ "$config_status" -eq 0 ]; then
|
||||
set +e
|
||||
npm exec --yes "@openclaw/plugin-inspector@${OPENCLAW_PLUGIN_INSPECTOR_VERSION}" -- ci \
|
||||
--config plugin-inspector.config.json \
|
||||
--openclaw "$PWD" \
|
||||
--out "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/reports" \
|
||||
--json \
|
||||
> "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stdout.json" \
|
||||
2> "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stderr.log"
|
||||
inspector_status=$?
|
||||
set -e
|
||||
else
|
||||
inspector_status=127
|
||||
echo "Skipped plugin-inspector because config generation failed." \
|
||||
> "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stderr.log"
|
||||
fi
|
||||
echo "$inspector_status" > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/exit-code.txt"
|
||||
|
||||
node --input-type=module <<'EOF'
|
||||
import { existsSync } from "node:fs";
|
||||
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const artifactRoot = process.env.OPENCLAW_PLUGIN_INSPECTOR_ROOT;
|
||||
const summaryPath = path.join(artifactRoot, "reports/plugin-inspector-ci-summary.json");
|
||||
const markdownPath = path.join(artifactRoot, "reports/plugin-inspector-ci-summary.md");
|
||||
const configExitCode = (await readFile(path.join(artifactRoot, "config-exit-code.txt"), "utf8")).trim();
|
||||
const exitCode = (await readFile(path.join(artifactRoot, "exit-code.txt"), "utf8")).trim();
|
||||
const lines = [
|
||||
"## Plugin Inspector Advisory",
|
||||
"",
|
||||
`Inspector: @openclaw/plugin-inspector@${process.env.OPENCLAW_PLUGIN_INSPECTOR_VERSION}`,
|
||||
`Config exit code: ${configExitCode}`,
|
||||
`Exit code: ${exitCode}`,
|
||||
];
|
||||
|
||||
if (existsSync(summaryPath)) {
|
||||
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
|
||||
lines.push(
|
||||
`Status: ${String(summary.status ?? "unknown").toUpperCase()}`,
|
||||
"",
|
||||
"| Metric | Count |",
|
||||
"| --- | ---: |",
|
||||
`| Hard breakages | ${summary.summary?.breakages ?? 0} |`,
|
||||
`| Issues | ${summary.summary?.issues ?? 0} |`,
|
||||
`| P0 issues | ${summary.summary?.p0Issues ?? 0} |`,
|
||||
`| P1 issues | ${summary.summary?.p1Issues ?? 0} |`,
|
||||
`| Compat gaps | ${summary.summary?.compatGaps ?? 0} |`,
|
||||
`| Inspector gaps | ${summary.summary?.inspectorGaps ?? 0} |`,
|
||||
"",
|
||||
"This job is informational; Plugin Prerelease blocking status is unchanged.",
|
||||
);
|
||||
await writeFile(path.join(artifactRoot, "advisory-summary.md"), `${lines.join("\n")}\n`, "utf8");
|
||||
if (existsSync(markdownPath)) {
|
||||
lines.push("", "### Full inspector summary", "");
|
||||
lines.push(await readFile(markdownPath, "utf8"));
|
||||
}
|
||||
} else {
|
||||
lines.push("", "No plugin-inspector CI summary was produced.", "");
|
||||
lines.push("This job is informational; inspect the uploaded stdout/stderr artifacts.");
|
||||
await writeFile(path.join(artifactRoot, "advisory-summary.md"), `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
await appendFile(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`, "utf8");
|
||||
EOF
|
||||
|
||||
- name: Upload plugin inspector advisory artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: plugin-inspector-advisory
|
||||
path: .artifacts/plugin-inspector/**
|
||||
if-no-files-found: warn
|
||||
|
||||
plugin-prerelease-docker-suite:
|
||||
name: plugin-prerelease-docker-suite
|
||||
needs: [preflight]
|
||||
@@ -375,6 +554,7 @@ jobs:
|
||||
- plugin-prerelease-static-shard
|
||||
- plugin-prerelease-node-shard
|
||||
- plugin-prerelease-extension-shard
|
||||
- plugin-prerelease-inspector
|
||||
- plugin-prerelease-docker-suite
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_prerelease_suite == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -389,6 +569,7 @@ jobs:
|
||||
STATIC_RESULT: ${{ needs.plugin-prerelease-static-shard.result }}
|
||||
NODE_RESULT: ${{ needs.plugin-prerelease-node-shard.result }}
|
||||
EXTENSIONS_RESULT: ${{ needs.plugin-prerelease-extension-shard.result }}
|
||||
INSPECTOR_RESULT: ${{ needs.plugin-prerelease-inspector.result }}
|
||||
DOCKER_RESULT: ${{ needs.plugin-prerelease-docker-suite.result }}
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -411,4 +592,5 @@ jobs:
|
||||
check_required "plugin-prerelease-node" "$RUN_NODE" "$NODE_RESULT"
|
||||
check_required "plugin-prerelease-extensions" "$RUN_EXTENSIONS" "$EXTENSIONS_RESULT"
|
||||
check_required "plugin-prerelease-docker" "$RUN_DOCKER" "$DOCKER_RESULT"
|
||||
echo "plugin-prerelease-inspector advisory result: ${INSPECTOR_RESULT}"
|
||||
exit "$failed"
|
||||
|
||||
@@ -118,6 +118,8 @@ USER.md
|
||||
!.agents/skills/gitcrawl/
|
||||
!.agents/skills/gitcrawl/**
|
||||
!.agents/skills/openclaw-docs/**
|
||||
!.agents/skills/openclaw-debugging/
|
||||
!.agents/skills/openclaw-debugging/**
|
||||
!.agents/skills/openclaw-ghsa-maintainer/
|
||||
!.agents/skills/openclaw-ghsa-maintainer/**
|
||||
!.agents/skills/openclaw-parallels-smoke/
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertFinalNewline": true,
|
||||
"jsxSingleQuote": false,
|
||||
"objectWrap": "preserve",
|
||||
"printWidth": 100,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"semi": true,
|
||||
"singleAttributePerLine": false,
|
||||
"singleQuote": false,
|
||||
"sortImports": {
|
||||
"newlinesBetween": false,
|
||||
},
|
||||
@@ -7,6 +22,7 @@
|
||||
"sortScripts": true,
|
||||
},
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"ignorePatterns": [
|
||||
"apps/",
|
||||
|
||||
+65
-2
@@ -36,8 +36,12 @@
|
||||
"eslint/no-new-wrappers": "error",
|
||||
"eslint/no-else-return": "error",
|
||||
"eslint/no-case-declarations": "error",
|
||||
"eslint/default-case-last": "error",
|
||||
"eslint/default-param-last": "error",
|
||||
"eslint/prefer-exponentiation-operator": "error",
|
||||
"eslint/prefer-numeric-literals": "error",
|
||||
"eslint/prefer-rest-params": "error",
|
||||
"eslint/prefer-spread": "error",
|
||||
"eslint/radix": "error",
|
||||
"eslint/unicode-bom": "error",
|
||||
"eslint/yoda": "error",
|
||||
@@ -49,7 +53,12 @@
|
||||
"oxc/no-accumulating-spread": "error",
|
||||
"oxc/no-async-endpoint-handlers": "error",
|
||||
"oxc/no-map-spread": "error",
|
||||
"promise/no-callback-in-promise": "error",
|
||||
"promise/no-multiple-resolved": "error",
|
||||
"promise/no-promise-in-callback": "error",
|
||||
"promise/no-return-in-finally": "error",
|
||||
"promise/no-new-statics": "error",
|
||||
"promise/valid-params": "error",
|
||||
"typescript/adjacent-overload-signatures": "error",
|
||||
"typescript/ban-tslint-comment": "error",
|
||||
"typescript/consistent-return": "error",
|
||||
@@ -66,24 +75,35 @@
|
||||
"typescript/no-unnecessary-type-parameters": "error",
|
||||
"typescript/no-unsafe-type-assertion": "off",
|
||||
"typescript/no-useless-default-assignment": "error",
|
||||
"typescript/no-useless-empty-export": "error",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/switch-exhaustiveness-check": [
|
||||
"error",
|
||||
{ "considerDefaultExhaustiveForUnions": true }
|
||||
],
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/prefer-namespace-keyword": "error",
|
||||
"typescript/prefer-return-this-type": "error",
|
||||
"typescript/prefer-find": "error",
|
||||
"typescript/prefer-function-type": "error",
|
||||
"typescript/prefer-includes": "error",
|
||||
"typescript/prefer-reduce-type-parameter": "error",
|
||||
"typescript/prefer-ts-expect-error": "error",
|
||||
"typescript/require-array-sort-compare": "error",
|
||||
"typescript/restrict-template-expressions": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
"unicorn/consistent-date-clone": "error",
|
||||
"unicorn/consistent-empty-array-spread": "error",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-console-spaces": "error",
|
||||
"unicorn/no-empty-file": "error",
|
||||
"unicorn/no-invalid-fetch-options": "error",
|
||||
"unicorn/no-invalid-remove-event-listener": "error",
|
||||
"unicorn/no-length-as-slice-end": "error",
|
||||
"unicorn/no-instanceof-array": "error",
|
||||
"unicorn/no-negation-in-equality-check": "error",
|
||||
"unicorn/no-new-buffer": "error",
|
||||
"unicorn/no-thenable": "error",
|
||||
"unicorn/no-typeof-undefined": "error",
|
||||
"unicorn/no-unnecessary-array-flat-depth": "error",
|
||||
"unicorn/no-unnecessary-array-splice-count": "error",
|
||||
@@ -102,16 +122,59 @@
|
||||
"unicorn/prefer-prototype-methods": "error",
|
||||
"unicorn/prefer-regexp-test": "error",
|
||||
"unicorn/prefer-set-size": "error",
|
||||
"unicorn/prefer-string-starts-ends-with": "error",
|
||||
"unicorn/prefer-string-slice": "error",
|
||||
"unicorn/require-array-join-separator": "error",
|
||||
"unicorn/require-number-to-fixed-digits-argument": "error",
|
||||
"unicorn/require-post-message-target-origin": "error",
|
||||
"unicorn/throw-new-error": "error",
|
||||
"vitest/no-import-node-test": "error",
|
||||
"vitest/consistent-vitest-vi": "error",
|
||||
"vitest/consistent-each-for": "error",
|
||||
"vitest/expect-expect": "error",
|
||||
"vitest/hoisted-apis-on-top": "error",
|
||||
"vitest/no-alias-methods": "error",
|
||||
"vitest/no-commented-out-tests": "error",
|
||||
"vitest/no-conditional-expect": "error",
|
||||
"vitest/no-conditional-in-test": "error",
|
||||
"vitest/no-conditional-tests": "error",
|
||||
"vitest/no-disabled-tests": "error",
|
||||
"vitest/no-duplicate-hooks": "error",
|
||||
"vitest/no-focused-tests": "error",
|
||||
"vitest/no-identical-title": "error",
|
||||
"vitest/no-import-node-test": "error",
|
||||
"vitest/no-standalone-expect": "error",
|
||||
"vitest/no-test-return-statement": "error",
|
||||
"vitest/prefer-called-once": "error",
|
||||
"vitest/prefer-called-times": "error",
|
||||
"vitest/prefer-expect-type-of": "error"
|
||||
"vitest/prefer-called-with": "error",
|
||||
"vitest/prefer-comparison-matcher": "error",
|
||||
"vitest/prefer-each": "error",
|
||||
"vitest/prefer-equality-matcher": "error",
|
||||
"vitest/prefer-expect-resolves": "error",
|
||||
"vitest/prefer-expect-type-of": "error",
|
||||
"vitest/prefer-hooks-in-order": "error",
|
||||
"vitest/prefer-hooks-on-top": "error",
|
||||
"vitest/prefer-mock-promise-shorthand": "error",
|
||||
"vitest/prefer-mock-return-shorthand": "error",
|
||||
"vitest/prefer-spy-on": "error",
|
||||
"vitest/prefer-strict-boolean-matchers": "error",
|
||||
"vitest/prefer-strict-equal": "error",
|
||||
"vitest/prefer-to-be": "error",
|
||||
"vitest/prefer-to-be-falsy": "error",
|
||||
"vitest/prefer-to-be-object": "error",
|
||||
"vitest/prefer-to-be-truthy": "error",
|
||||
"vitest/prefer-to-contain": "error",
|
||||
"vitest/prefer-to-have-length": "error",
|
||||
"vitest/require-awaited-expect-poll": "error",
|
||||
"vitest/require-hook": "error",
|
||||
"vitest/require-local-test-context-for-concurrent-snapshots": "error",
|
||||
"vitest/require-mock-type-parameters": "error",
|
||||
"vitest/require-to-throw-message": "error",
|
||||
"vitest/valid-describe-callback": "error",
|
||||
"vitest/valid-expect": "error",
|
||||
"vitest/valid-expect-in-promise": "error",
|
||||
"vitest/valid-title": "error",
|
||||
"vitest/warn-todo": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist/",
|
||||
|
||||
@@ -73,6 +73,8 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- PR review answer: bug/behavior, URL(s), affected surface, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
|
||||
- Issue/PR final answer: last line is the full GitHub URL.
|
||||
- Changelog: PR landings/fixes need one unless pure test/internal. Do not mention missing changelog as a review finding; Codex handles it during fix/landing.
|
||||
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
|
||||
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
|
||||
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
|
||||
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
|
||||
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
|
||||
@@ -132,7 +134,7 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Never commit real phone numbers, videos, credentials, live config.
|
||||
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
|
||||
- Env keys: check `~/.profile`; redact output.
|
||||
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm.patchedDependencies` exact versions only.
|
||||
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
|
||||
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
|
||||
- Releases/publish/version bumps need explicit approval. Use `$openclaw-release-maintainer`.
|
||||
- GHSA/advisories: `$openclaw-ghsa-maintainer` / `$security-triage`. Secret scanning: `$openclaw-secret-scanning-maintainer`.
|
||||
|
||||
@@ -6,7 +6,20 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- CI: add a non-blocking `plugin-inspector-advisory` artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate.
|
||||
- Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to `/edit` with `image_urls` array, enforce NB2 edit geometry using `aspect_ratio` and `resolution` params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007.
|
||||
|
||||
- Build: enable additional low-churn oxlint rules for promise, TypeScript, and runtime footgun checks.
|
||||
- Build: enable stricter Vitest lint rules for focused, disabled, conditional, hook, matcher, and expectation hazards.
|
||||
- Build: pin explicit oxfmt defaults in the shared formatter config to keep formatting behavior stable across upgrades.
|
||||
- TypeScript: enable stricter compiler checks for implicit returns, side-effect imports, overrides, and unused production code.
|
||||
- Logging: add targeted model transport, payload, SSE, and code-mode diagnostics with redacted URL handling.
|
||||
- Agents: allow `session.agentToAgent.maxPingPongTurns` up to 20 while keeping the default at 5 for longer agent-to-agent reply chains. Fixes #52382. (#52400) Thanks @thirumaleshp.
|
||||
- Agents: add per-agent `tools.message.crossContext` overrides so sandboxed/public agents can restrict message sends to the current conversation without changing the global bot policy.
|
||||
- Agents: add per-agent `tools.message.actions.allow` overrides so sandboxed/public agents can expose and enforce send-only message tools.
|
||||
- Agents: omit the sandbox workspace marker from compact command progress previews while keeping internal sandbox diagnostics unchanged.
|
||||
- Build: upgrade workspace package management to pnpm 11 and keep Docker, install, update, and release workflows on the pnpm 11 config surface. (#79414) Thanks @altaywtf.
|
||||
- Build: align Telegram QA workflows and git source installs with the pnpm 11 workspace build allowlist surface. (#80588) Thanks @altaywtf.
|
||||
- Models: add provider-level `localService` startup for on-demand local model servers before OpenAI-compatible requests, including one-shot model probes.
|
||||
- Agents: trim default system prompt guidance and send-only message tool schemas to reduce prompt tokens while preserving GPT-5 personality guidance.
|
||||
- Context: add `/context map` to send a treemap image of the current session context contributors. (#79867)
|
||||
@@ -28,28 +41,79 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/skills: add an opt-in private skill archive upload install path gated by `skills.install.allowUploadedArchives`, so trusted Gateway clients can stage and install zip-backed skills only when operators explicitly enable the code-install surface. (#74430) Thanks @samzong.
|
||||
- Codex app-server: enable Codex native code-mode-only for harness threads so deferred OpenClaw dynamic tools run through Codex's own searchable code execution surface instead of a PI-style wrapper.
|
||||
- Dependencies: refresh workspace pins and patch targets, including ACPX `@agentclientprotocol/claude-agent-acp` `0.33.1`, Codex ACP `0.14.0`, Baileys `7.0.0-rc10`, Google GenAI `2.0.1`, OpenAI `6.37.0`, AWS SDK `3.1045.0`, Kysely `0.29.0`, Tlon skill `0.3.6`, Aimock `1.19.5`, and tsdown `0.22.0`.
|
||||
- Dependencies: move embedded Pi packages to the `@earendil-works` namespace, refresh Twitch Twurple packages, and move `@openclaw/fs-safe` from the GitHub release pin to the published npm package.
|
||||
- Build: route Testbox changed-check delegation through Crabbox and remove the OpenClaw-specific Blacksmith Testbox helper scripts.
|
||||
- Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle.
|
||||
- Agents/process: tell agents to inspect background sessions with `process log` before sending interactive input and to use `waitingForInput`/`stdinWritable` hints from `log`/`poll`.
|
||||
- CLI/onboarding: improve setup, onboarding, configure, and channel command wayfinding so terminal flows explain the next useful command instead of relying on terse setup labels.
|
||||
- Agents/Codex: remove the configurable Codex dynamic-tools profile so Codex app-server always owns workspace, edit, patch, exec, process, and plan tools while OpenClaw integration tools remain available.
|
||||
- macOS app: update the Peekaboo bridge dependency to Peekaboo 3.0.0.
|
||||
- Dependencies: refresh workspace pins and move the WhatsApp plugin from `@whiskeysockets/baileys` to `baileys` while keeping the `7.0.0-rc10` runtime.
|
||||
- Plugin SDK: add bundled-plugin session actions, `sendSessionAttachment`, and Cron-backed `scheduleSessionTurn`/tag cleanup under the grouped session namespace. Replaces #75578/#75581/#75588 and part of #73384/#74483. Thanks @100yenadmin.
|
||||
- Plugin SDK/media-understanding: add `extractStructuredWithModel(...)` plus the optional provider-side `extractStructured(...)` seam so trusted plugins can run bounded image-first structured extraction with optional supplemental text context through provider-owned runtimes such as Codex.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc.
|
||||
- Memory: reject symlinked directory components in configured extra memory paths before reading Markdown files. (#80331) Thanks @samzong.
|
||||
- Sessions/transcripts: replace whole-file `readFile` scans with shared streaming helpers (`streamSessionTranscriptLines` and `streamSessionTranscriptLinesReverse`) for idempotency lookup, latest/tail assistant text reads, delivery-mirror dedupe, and compaction fork loading, so long-running sessions no longer materialize the full transcript in memory. Forward scans use `readline` over a bounded `createReadStream`; reverse scans read bounded chunks from the file end and decode complete JSONL lines newest-first without a fixed tail cap. Synthetic 200 MiB transcript: peak RSS delta drops from +252 MiB to +27 MiB while preserving malformed-line tolerance and idempotency-key return semantics. Fixes #54296. Thanks @jack-stormentswe.
|
||||
- WhatsApp: apply hot-reloaded `dmPolicy` and `allowFrom` settings to the active Web listener before processing new inbound DMs. Fixes #80538. Thanks @Ampaskopi129.
|
||||
- Plugins: let `openclaw doctor --fix` repair managed plugin installs whose package entrypoints fail package-directory boundary validation after local state moves. Fixes #80592. Thanks @wei-wei-zhao.
|
||||
- Voice-call: resume voice-originated exec approval follow-ups as internal non-delivery turns instead of rejecting them as `unknown channel: voice`. Fixes #80540. Thanks @patrickmch.
|
||||
- Control UI: preserve the composer draft when Stop is tapped during an active chat run, preventing accidental prompt loss on mobile. Fixes #80586. Thanks @KCALLC.
|
||||
- Infra/retry: keep jittered retry delays at or above server-supplied Retry-After lower bounds when the hint can be honored. Fixes #68541. (#68543) Thanks @Feelw00.
|
||||
- Docs: clarify that `/model provider/model` is an exact session route, while duplicate bare model ids only use configured fallback order on non-session override paths. Refs #80562. Thanks @gaodaabao.
|
||||
- Redact persisted secret-shaped payloads [AI]. (#79006) Thanks @pgondhi987.
|
||||
- Agents: label `.openclaw/sandboxes` exec workdirs as sandbox runs in compact tool summaries instead of showing the full path.
|
||||
- OpenAI Codex: surface browser OAuth and device-code login failures instead of treating failed logins as empty successful auth results. Refs #80363.
|
||||
- CLI agents: carry runtime-only current-turn sender/reply context into CLI model prompts while keeping prompt-build hook input and transcript text clean.
|
||||
- Control UI: keep workspace file presence checks from treating `fs-safe` stat helper failures as missing files, restoring Agents file status for existing Windows workspace files. Fixes #79953. Thanks @lovelefeng-glitch.
|
||||
- Microsoft Foundry: report an explicit error when the Azure subscription prompt returns an id that is not present in the enabled subscription list, instead of continuing from an unsafe subscription assertion. (#62742) Thanks @oliviareid-svg.
|
||||
- fix(matrix): gate name-based allowlist resolution [AI]. (#79007) Thanks @pgondhi987.
|
||||
- Slack: include the bot's own root/parent message in new thread sessions so in-thread replies reach the agent with the parent text the user is responding to, instead of only `reply_to_id` metadata. Fixes #79338. Thanks @sxxtony.
|
||||
- Docker: keep image builds on the source pnpm workspace policy so pnpm 11 can prune production dependencies without a Docker-only workspace rewrite.
|
||||
- Agents/compaction: restore info-level gateway logs for embedded compaction start, completion, and incomplete outcomes. (#71961) Thanks @rubencu.
|
||||
- Telegram: build reply-aware inbound turns through the shared channel context path so agents see the current reply target inline with the current message.
|
||||
- Telegram: recover legacy message cache files that mixed JSON-array and line-delimited entries so restarted gateways preserve reply-window context. (#80567)
|
||||
- Telegram: update the reply-context cache when messages are edited, so streamed bot replies appear in later agent context with their final text instead of the first draft.
|
||||
- Skills/Windows: normalize compacted skill prompt locations to forward slashes after home-prefix compaction so Windows skill paths remain readable by model file tools. (#52200) Thanks @chienchandler.
|
||||
- Control UI/Windows: update `@openclaw/fs-safe` so agent workspace file presence checks fall back correctly on Windows, preventing existing AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md, and MEMORY.md files from showing as missing. Fixes #79953. Thanks @lovelefeng-glitch.
|
||||
- Memory: skip managed dreaming cron reconciliation warnings for ordinary cron and heartbeat hook contexts that cannot manage Gateway cron. (#77027) Thanks @rubencu.
|
||||
- Cron: treat Codex app-server turn acceptance, CLI process spawn, and tool starts as execution milestones, preventing isolated runs from tripping the early startup watchdog after work has begun.
|
||||
- Codex app-server: treat current-turn `<turn_aborted>` raw markers as terminal so interrupted native-tool turns release Discord agent sessions instead of waiting for the outer timeout.
|
||||
- Yuanbao: bump `openclaw-plugin-yuanbao` to 2.13.1 to support `sourceReplyDeliveryMode: "automatic"` for group chat. (#79814) Thanks @loongfay.
|
||||
- Memory: keep `memory_search` result `corpus` labels aligned with the hit source, so session transcript hits surface as `sessions` and memory-file hits stay `memory`. Fixes #72885. (#71898, #72886) Thanks @rubencu.
|
||||
- Codex app-server: default native plugin app tool approvals to automatic so non-destructive read tools run when destructive actions are disabled.
|
||||
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so `google/gemini-3.1-pro-preview` is used for testing instead of `google/gemini-3-pro-preview`.
|
||||
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids in configured proxy/provider-auth model catalogs, so regenerated config keeps testing `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`.
|
||||
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while onboarding provider catalog presets, so setup-emitted proxy configs test `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`.
|
||||
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows during generic config writes, so unrelated config changes keep testing `google/gemini-3.1-pro-preview`.
|
||||
- Models: keep configured fallback chains ahead of configured primary models for override selections with duplicate model ids, preventing fallback jumps to the wrong provider. Fixes #80562.
|
||||
- Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates.
|
||||
- Gateway/agents: keep stale `sessions_send` ACP manager and `web_fetch` runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72.
|
||||
- Gateway/install: preserve service environment value-source metadata in `openclaw gateway install`, so systemd reinstall paths keep env-file-backed secrets out of inline unit metadata. Refs #77406, #77427. Thanks @stainlu and @brokemac79.
|
||||
- Auto-reply/reset: include inbound sender context in bare `/new` and `/reset` model prompts while keeping startup instructions out of transcript prompts, so agents see sender identity on the first reset turn. Fixes #77360. Thanks @srb11e.
|
||||
- Gateway: avoid synchronous restart-sentinel state probes during post-attach startup, preventing slow Windows or redirected state directories from blocking channel turns. Fixes #79264. Thanks @liyi58.
|
||||
- Agents/auth: update successful model auth profile status with one locked store write, reducing post-model reply latency from duplicate `auth-profiles.json` saves. Thanks @mcaxtr.
|
||||
- Agents/image: honor explicit `image` tool model overrides even when `agents.defaults.imageModel` is unset, restoring one-off vision calls for configured multimodal providers. Fixes #79341. Thanks @haumanto.
|
||||
- Doctor/update: leave live systemd gateway units unchanged during noninteractive update-mode service repair, so update-time doctor does not silently overwrite operator-owned unit directives. Refs #80462.
|
||||
- Update: accept optional leading `v` prefixes when verifying exact npm package install targets, so `openclaw update --tag v2026...` does not roll back after installing the matching bare package version. Refs #74069; #80480. Thanks @Kaspre.
|
||||
- Doctor: treat missing plugin ids in `plugins.deny` as stale config warnings instead of fatal validation errors, and remove them during stale plugin cleanup so update repair does not restore last-known-good config for deny-only stale plugin refs. Refs #77802. Thanks @Kaspre.
|
||||
- Codex app-server: preserve prompt-local current-turn context through context-engine prompt projection, so replied-to Telegram messages stay visible to the Codex model input.
|
||||
- Telegram: pass agent-scoped media roots through gateway message actions so workspace-local media from the active agent is not rejected as cross-agent access. Thanks @frankekn.
|
||||
- CLI/gateway: keep `gateway status --deep` plugin-aware so configured plugin manifest warnings, including missing channel config metadata, stay visible during install and update smoke checks.
|
||||
- Doctor/status: clarify gateway token source conflict warnings and suppress them inside the managed Gateway service credential context.
|
||||
- Feishu: accept Schema 2 card callbacks whose operator identity is nested under `operator.user_id`, so card buttons dispatch instead of being dropped as malformed. Fixes #71670. (#71787) Thanks @rubencu.
|
||||
- Feishu: fall back to a top-level group send when normal group quoted replies target a withdrawn or missing message, preventing replies from disappearing silently while preserving native topic safety. Fixes #79349. Thanks @arlen8411.
|
||||
- Doctor: stop flagging the live compatibility agent directory as orphaned when the configured default agent is not `main`. Fixes #74313. (#74438) Thanks @carlos4s.
|
||||
- Auth/Claude CLI: persist fresher managed external CLI OAuth credentials back to `auth-profiles.json`, preventing stale `anthropic:claude-cli` profiles from repeatedly bootstrapping and flooding debug logs. Fixes #80129. Thanks @Caulderein.
|
||||
- Context: render `/context map` only from actual run context and persist Codex app-server run reports without counting deferred tool-search schemas as prompt-loaded tool schemas.
|
||||
- Codex app-server: report Codex-native tool execution to diagnostics so long-running native `bash`, web, file, and MCP tools no longer look like stale embedded runs to the watchdog. (#80217)
|
||||
- Codex app-server: refresh Codex account rate limits after subscription usage-limit failures so Discord and other channel replies can show the next reset time instead of saying Codex returned none. Thanks @pashpashpash.
|
||||
- Agents/auth: let Codex-backed OpenAI agent turns use `auth.order.openai` entries for Codex-compatible OAuth and API-key profiles while keeping existing `openai-codex` profile ordering valid.
|
||||
- Tasks: route group and channel task completions through the requester session so the parent agent can send the visible summary instead of stopping at a generic task-status line. Fixes #77251. (#77365) Thanks @funmerlin.
|
||||
- Telegram: preserve blank lines between manually indented bullet blocks and following numbered sections in rendered replies. Fixes #76998. Thanks @evgyur.
|
||||
- Agents/sandbox: allow read-only sandbox sessions to read the `/agent` workspace mount while keeping write/edit/apply_patch workspace-only guarded, restoring `read /agent/...` for `workspaceAccess: "ro"`. Fixes #39497. Thanks @stainlu and @teosborne.
|
||||
- Slack: pass configured agent identity through draft preview sends so partial streaming replies keep custom username/avatar on the initial Slack message. Fixes #38235. (#38237) Thanks @lacymorrow.
|
||||
- Slack: support `allowBots: "mentions"` for bot-authored messages that mention the receiving bot, matching the documented Discord-style mode without accepting every bot message. Fixes #43587. (#43588) Thanks @raw34.
|
||||
- Slack: refresh private file URLs with `files.info` when inbound DM file events omit or stale attachment URLs, preventing file attachments from being dropped before media hydration. Fixes #50129. (#50200) Thanks @smartchainark.
|
||||
@@ -92,6 +156,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/voice: keep default agent-proxy realtime sessions from auto-speaking filler before the forced OpenClaw consult answer, finish Discord playback on realtime response completion, and queue later exact-speech answers until playback idles to avoid mid-sentence replacement.
|
||||
- Gateway: return deterministic `400 invalid_request_error` responses for malformed encoded session-kill HTTP paths instead of letting route-shaped requests fall through to later Gateway handlers. (#72439) Thanks @rubencu.
|
||||
- Control UI: serve root PWA and favicon assets from `/__openclaw__/` SPA routes so tab icons, install metadata, and the service worker do not 404 after internal navigation. Fixes #80072. Thanks @CodeNovice2017.
|
||||
- Exec/safe bins: compare trusted safe-bin dirs with path-specific case folding on case-insensitive filesystems so Windows and default macOS paths match without weakening case-sensitive mounts. (#42131) Thanks @hkochar.
|
||||
- OpenAI/realtime voice: honor disabled input-audio interruption locally so server VAD speech-start events do not clear Discord playback after operators set `interruptResponseOnInputAudio: false`.
|
||||
- Telegram: keep no-response DM turns quiet instead of rewriting them into visible silent-reply chatter. Fixes #78188. (#78228) Thanks @Beandon13.
|
||||
- Telegram: handle managed select button callbacks before the raw callback fallback while preserving delimiter-containing option values such as `env|prod`. (#79816) Thanks @moeedahmed.
|
||||
@@ -102,6 +167,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Models/Anthropic: add `anthropic/claude-haiku-4-5` to Anthropic API-key agent allowlist defaults when an Anthropic default model is configured, so cron model overrides can select the current Haiku alias. Fixes #78000.
|
||||
- Agents/compaction: initialize built-in context engines before CLI transcript compaction resolves the default engine, preventing clean-process `legacy` engine registration failures during CLI session persistence. Fixes #79446. Thanks @TurboTheTurtle.
|
||||
- Agents/Anthropic-compatible: strip replayed thinking blocks for custom Anthropic-compatible models that explicitly declare `supportsReasoningEffort: false`, preventing Kimi-compatible providers from resending unsupported `thinking` content. Fixes #47452.
|
||||
- Kimi: keep Anthropic-compatible thinking streams valid by supplying required thinking budgets and enough output room for hidden reasoning plus final text. (#80481) Thanks @InTheCloudDan.
|
||||
- Browser: wait longer for existing-session Chrome MCP status and non-deep doctor probes so slow first attaches do not falsely report offline while keeping raw CDP status probes short. (#77473) Thanks @rubencu.
|
||||
- Gateway/logging: install console capture before foreground Gateway fast-path parsing and suppress known libsignal session dumps even in verbose mode, preventing raw terminal logs from printing WhatsApp session key material. (#76306) Thanks @rubencu.
|
||||
- Exec approvals: keep `exec.approval.list` on the lightweight policy-summary path so listing pending approvals no longer loads the rich tree-sitter command explainer. (#76943) Thanks @rubencu.
|
||||
@@ -136,8 +202,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside provider auth config patches so setup-emitted provider catalogs test `google/gemini-3.1-pro-preview`.
|
||||
- GitHub Copilot: mint short-lived Copilot API tokens with the same `vscode-chat` integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the `copilot-language-server` scope. Fixes #79946, #80074. Thanks @TurboTheTurtle.
|
||||
- Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.
|
||||
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) — both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369. Thanks @Kaspre.
|
||||
- Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.
|
||||
- Discord/voice: skip likely incomplete realtime forced-consult transcript fragments and non-actionable closings so stale partial speech does not queue delayed answers over the next turn.
|
||||
- Discord/voice: keep realtime forced consults from clearing active exact-speech playback, so back-to-back voice answers queue instead of cutting each other off.
|
||||
- Discord/voice: synthesize realtime playback timestamps from emitted Discord PCM so OpenAI realtime barge-in truncation no longer sees `audioEndMs=0` and skips legitimate interruptions.
|
||||
- Plugin SDK: keep activated linked plugin runtime facades loadable when bundled plugin fallback is disabled. Thanks @shakkernerd.
|
||||
- Feishu: auto-thread `message(action="send")` replies inside the topic when the active session is group_topic or group_topic_sender, and propagate `replyInThread` through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.
|
||||
@@ -171,6 +239,8 @@ Docs: https://docs.openclaw.ai
|
||||
- iMessage: emit a WARN log when an action is blocked because the imsg private API bridge is not attached, so operators see the silent-drop in `~/.openclaw/logs/openclaw.log` instead of having to read per-session trajectory JSONL `tool.result` payloads. Common after a gateway restart un-injects the dylib from Messages.app. (#80035) Thanks @omarshahine.
|
||||
- Codex: cross-fill missing `thread.id` and `thread.sessionId` before schema validation so live Codex app-server responses that omit `sessionId` no longer fail `thread/start` or `thread/resume`. Fixes #80124. (#80137) Thanks @kagura-agent.
|
||||
- Agents/Pi: wait for embedded abort cleanup to settle before releasing the session write lock, preventing follow-up turns from racing previous prompt teardown. (#80239) Thanks @samzong.
|
||||
- WhatsApp: downgrade OpenClaw watchdog-triggered Web reconnects from runtime errors to recovery warnings and clear the recovered reconnect status after the next healthy connection. (#77026) Thanks @rubencu.
|
||||
- ACPX/Windows: hide the MCP proxy target child process window on Windows so ACP-backed agents do not flash or fail because of terminal window handling. Fixes #60672. (#60678) Thanks @KChow-ctrl.
|
||||
|
||||
## 2026.5.9
|
||||
|
||||
@@ -178,6 +248,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Skills: add `skills.load.allowSymlinkTargets` so intentional symlinked skill folders can resolve into trusted sibling repos without disabling root containment.
|
||||
- Agents/tools: add core Tool Search so agents can search and call large OpenClaw, MCP, and client tool catalogs through one compact PI bridge.
|
||||
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) — both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369.
|
||||
- Chat commands: add `/think default` and `/fast default` to clear session overrides and inherit configured/provider defaults. (#79385) Thanks @VACInc.
|
||||
- Dependencies: refresh workspace dependency pins and lockfile, including `@openai/codex` `0.130.0`, `acpx` `0.7.0`, AWS SDK `3.1044.0`, OpenTelemetry `0.217.0`, `typebox` `1.1.38`, `vite` `8.0.11`, `oxfmt` `0.48.0`, and `oxlint` `1.63.0`, and update the Codex harness model snapshot for the new bundled app-server catalog.
|
||||
- Plugins/install: add guarded plugin install overrides so onboarding and repair tests can route specific plugins to registry specs or local `npm pack` artifacts via environment variables.
|
||||
@@ -237,6 +308,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Models/config: allow `compat.thinkingFormat` values `qwen` and `qwen-chat-template` for configured OpenAI-compatible Qwen models, preserving them through catalog normalization and mapping `/think` levels to `enable_thinking` or `chat_template_kwargs.enable_thinking`. Fixes #79677. (#79777) Thanks @indulgeback.
|
||||
- Codex app-server: default implicit local stdio app-server permissions to guardian when Codex system requirements disallow the YOLO approval, reviewer, or sandbox value, including hostname-scoped remote sandbox entries, avoiding turn-start failures on managed hosts that permit only reviewed approval or narrower sandboxes.
|
||||
- Plugins/install: run managed npm-root install, uninstall, prune, and repair commands from the managed root without a redundant `--prefix .`, avoiding npm 10.9.3 Arborist crashes on native Windows WhatsApp plugin installs. Fixes #78514. (#78902) Thanks @melihselamett-stack.
|
||||
- Config/schema/Windows: detect direct execution of the base config schema generator with `pathToFileURL` so Windows paths with backslashes still run the `--check` and `--write` command body. (#52989) Thanks @easyteacher.
|
||||
- Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner.
|
||||
- Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics.
|
||||
- iMessage: expose native private-API message actions through `imsg rpc` for reactions, edits, unsends, replies, rich sends, attachments, and group management when `imsg status --json` reports the required bridge capabilities.
|
||||
@@ -257,6 +329,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Process tool: show input-wait hints from `log` and `poll` for idle interactive background sessions so operators can inspect stuck CLIs and resume them with existing input actions. Fixes #33957. Thanks @bitloi and @vincentkoc.
|
||||
- Shell env/Windows: hide the login-shell environment probe child window so gateway startup and shell-env refreshes do not flash a console on Windows. Fixes #78159. (#78266) Thanks @BradGroux.
|
||||
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
|
||||
- Windows/restart: skip duplicate scheduled-task `/Run` calls when the gateway task is already running, using a locale-stable PowerShell task-state probe before retrying. Fixes #52044. (#52487) Thanks @andyk-ms.
|
||||
- Media/host-read: allow buffer-verified ZIP archives in the host-local media validator so agents can send ZIP attachments via the message tool. Fixes #78057. (#78292) Thanks @Linux2010.
|
||||
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
|
||||
- Contributor PRs: remind external contributors to redact private information like IP addresses, API keys, phone numbers, and non-public endpoints from real behavior proof. Thanks @pashpashpash.
|
||||
@@ -322,6 +395,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc.
|
||||
- Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin.
|
||||
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
|
||||
- Plugin SDK/Gateway: add scoped `plugins.sessionAction` dispatch and plugin-attributed `emitAgentEvent` support so plugins can expose typed session actions and workflow events to trusted clients. (#75578; replaces part of #73384/#74483) Thanks @100yenadmin.
|
||||
- Plugins/SDK: expose host-derived tool target paths to `before_tool_call` and trusted policy hooks so workflow plugins can reason about known file targets without reparsing tool envelopes. (#75605) Thanks @100yenadmin.
|
||||
- Control UI/WebChat: show a persistent compact context usage indicator from fresh session token data before the high-pressure warning state, while keeping the existing compaction prompt threshold. Fixes #46398; refs #45048, #50071, and #73744. Thanks @walterwkchoy, @AxelrodAI, @Brissux, @vincentkoc, and @BunsDev.
|
||||
- Contributor PRs: require external pull requests to include after-fix real behavior proof from a real OpenClaw setup, with terminal screenshots, console output, redacted runtime logs, linked artifacts, and copied live output treated as valid evidence while unit tests, mocks, lint, typechecks, snapshots, and CI remain supplemental only.
|
||||
@@ -382,6 +456,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex app-server: keep native hook relays alive for long-running turns so shell and file approvals stay reachable until the configured run window finishes. (#77533) Thanks @rubencu.
|
||||
- Gateway/macOS: clear ignored SIGUSR1 restart state, skip redundant package-update restarts when the refreshed LaunchAgent already serves the expected version, and give launchd a 10s throttle plus 20s shutdown window so update restarts do not leave old gateways alive or fight supervisor recovery. Fixes #79577; refs #78699 and #60885. Thanks @BunsDev.
|
||||
- Status/Codex: route Codex-harness `openai/*` usage through the OpenAI Codex quota provider and scope CLI status usage to the default agent auth store so `/status` and `openclaw status --usage` show Codex quota windows again. Fixes #79312. Thanks @keshavbotagent.
|
||||
- Matrix: keep joined strict DM rooms discoverable when stale `m.direct` mappings already point at an older strict room, and let `dm.sessionScope: "per-room"` promote safe unmapped strict rooms through the existing unnamed/unaliased room gate. Fixes #79514. Thanks @stainlu.
|
||||
- Gateway/agent: pass the session-key agent id into inline image attachment validation so the first image in a fresh per-agent session uses the agent's vision-capable model override instead of the text-only system default. Fixes #79407. Thanks @pandadev66.
|
||||
- Gateway/maintenance: prune dedupe overflow against a stable excess count and keep active agent retries from starting duplicate runs after cache eviction. (#73841) Thanks @thesomewhatyou.
|
||||
- Control UI/subagents: suppress internal `subagent_announce` handoff prompts from requester transcripts and hide legacy inter-session wrapper rows so completed subagent results no longer surface runtime context in WebChat history. (#79618) Thanks @joshavant.
|
||||
@@ -396,6 +471,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA-lab/parity: bump the live mock-openai parity baseline from `claude-opus-4-6`/`claude-sonnet-4-6` to `claude-opus-4-7`/`claude-sonnet-4-7` and the candidate alt from `gpt-5.4-alt` to `gpt-5.5-alt` in `openclaw-release-checks.yml` and `qa-live-transports-convex.yml`, matching the active Opus 4.7 / GPT-5.5 defaults already used elsewhere on main. Carries forward the surface-bump portion of #74290. Thanks @100yenadmin.
|
||||
- QA-lab/scenarios: raise the `approval-turn-tool-followthrough` per-turn fallback timeouts from 20s/30s to 60s so cold mock-gateway parity runs do not flake on the approval-turn chain. Carries forward the timeout-bump portion of #74290. Thanks @100yenadmin.
|
||||
- Gateway/restart continuation: treat routed post-reboot agent turns as trusted internal continuations while preserving the original Telegram topic route, and retry briefly when the previous run is still shutting down, so owner-only tools remain available for chained restart workflows after reboot.
|
||||
- MS Teams: normalize pre-thread-qualified route session keys before deriving channel-thread lanes so cached route reuse cannot create malformed mixed `:thread:OLD:thread:NEW` sessions. Fixes #66771. (#78850) Thanks @harrisali0101.
|
||||
- Agents/compaction: keep the recent tail after manual `/compact` when Pi returns an empty or no-op compaction summary, preventing blank checkpoints from replacing the live context.
|
||||
- Native commands: handle slash commands before workspace and agent-reply bootstrap so Telegram `/status` and other command-only native replies do not wait behind full agent turn setup.
|
||||
- Telegram/groups: include the recent local chat window and nearby reply-target window as generic inbound context so stale reply ancestry does not overshadow the live group conversation.
|
||||
@@ -484,6 +560,7 @@ Docs: https://docs.openclaw.ai
|
||||
- OpenRouter: keep the default `openrouter/auto` model ref canonical while preventing TUI and Control UI catalog pickers from displaying or submitting `openrouter/openrouter/auto`. Fixes #62655.
|
||||
- Status/Claude CLI: show `oauth (claude-cli)` for working Claude CLI OAuth runtime sessions instead of `unknown` when no local auth profile exists. Fixes #78632. Thanks @gorkem2020.
|
||||
- Memory search: preserve keyword-only hybrid FTS matches when vector scoring is unavailable or below the configured minimum score, so exact lexical hits are not dropped by weighted min-score filtering.
|
||||
- Heartbeat/async exec: remap cron-run session keys to agent-main (or `"global"` under `session.scope=global`) at the bash exec, ACP, gateway node-event, and CLI watchdog enqueue sites, and treat cron-run descendants as ephemeral for retention pruning, so async exec completion events land in the same queue the heartbeat drains instead of being stranded under the ephemeral cron-run key. Refs #52305. Thanks @Kaspre.
|
||||
- Exec approvals/node: let trusted backend node invokes complete no-device Control UI approvals after the original request connection changes, while keeping node, command, cwd, env, and allow-once replay bindings enforced. Fixes #78569. Thanks @naturedogdog.
|
||||
- Agents/subagents: keep background completion delivery on the requester-agent handoff/queue-retry path instead of raw-sending child results directly, and strip child-result wrapper or OpenClaw runtime-context scaffolding from queued outbound retries. Fixes #78531. Thanks @EthanSK.
|
||||
- Sandbox: recreate cached browser bridges when JavaScript-evaluation permission changes, keep failed prune removals tracked for retry, and make cross-device directory moves copy-then-commit without partially emptying the source on failure.
|
||||
@@ -773,6 +850,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Browser/chrome-mcp: read Chrome DevTools MCP screenshot output from the extension-suffixed path, fixing ENOENT on screenshot capture. Fixes #77222. (#74685) Thanks @barbarhan.
|
||||
|
||||
- macOS/launchd: set generated Gateway LaunchAgent plists to `ProcessType=Interactive` so the gateway keeps timely execution during idle periods. Fixes #58061; refs #62294 and closed duplicate #66992. (#62308) Thanks @bryanpearson and @zssggle-rgb.
|
||||
- Plugins/install: honor the beta update channel for onboarding and doctor-managed plugin installs by requesting floating npm and ClawHub specs with `@beta` while keeping persistent install records on the catalog default. Thanks @vincentkoc.
|
||||
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
|
||||
@@ -1071,6 +1150,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/LanceDB: declare `apache-arrow` in the bundled memory plugin package so LanceDB installs include its runtime peer. Fixes #76910. Thanks @afiqfiles-max.
|
||||
- CLI/devices: retry explicit device-pair approval with `operator.admin` after a pairing-scope ownership denial, so existing admin-capable paired-device tokens can recover new Control UI/browser pairing after upgrades instead of requiring manual JSON edits. Fixes #76956. Thanks @neo19482.
|
||||
- CLI/devices: stop local pairing fallback when the active Gateway names a pending request that is absent from the local pairing store, so profile or state-dir mismatches no longer make `openclaw devices list/approve` inspect the wrong store while a real device stays blocked. Thanks @vincentkoc.
|
||||
- Control UI/webchat: fix streaming assistant responses causing the chat viewport to scroll upward by guarding `handleChatScroll` against scroll events triggered by the auto-scroll logic itself; introduces a `chatIsProgrammaticScroll` flag that suppresses near-bottom state updates during programmatic `scrollTo` calls so streaming output stays pinned to the bottom. Thanks @nickmopen.
|
||||
- Google Meet: use the local call-control microphone button instead of disabled remote participant mute buttons, and block realtime speech when the OpenClaw Meet microphone remains muted.
|
||||
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.
|
||||
- Plugins/install: recover the install ledger from the managed npm root when `plugins/installs.json` is empty or partial, so reinstalling Discord and Codex no longer makes the other installed plugin disappear.
|
||||
|
||||
+33
-30
@@ -5,9 +5,8 @@
|
||||
#
|
||||
# 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.
|
||||
# The dependency manifest stages extract only package.json files, so the main
|
||||
# build layer is not invalidated by unrelated source changes.
|
||||
#
|
||||
# Build stages use full bookworm; the runtime image is always bookworm-slim.
|
||||
ARG OPENCLAW_EXTENSIONS=""
|
||||
@@ -26,16 +25,24 @@ ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c
|
||||
# node:24-bookworm-slim (or podman) and replace the digests below with the
|
||||
# current multi-arch manifest list entries.
|
||||
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS workspace-deps
|
||||
ARG OPENCLAW_EXTENSIONS
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
|
||||
# Copy package.json for opted-in extensions so pnpm resolves their deps.
|
||||
RUN --mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR},readonly \
|
||||
mkdir -p /out && \
|
||||
# Copy package.json files for workspace packages used by the install layer.
|
||||
RUN --mount=type=bind,source=packages,target=/tmp/packages,readonly \
|
||||
--mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR},readonly \
|
||||
mkdir -p /out/packages "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}" && \
|
||||
for manifest in /tmp/packages/*/package.json; do \
|
||||
[ -f "$manifest" ] || continue; \
|
||||
pkg_dir="${manifest%/package.json}"; \
|
||||
pkg_name="${pkg_dir##*/}"; \
|
||||
mkdir -p "/out/packages/$pkg_name" && \
|
||||
cp "$manifest" "/out/packages/$pkg_name/package.json"; \
|
||||
done && \
|
||||
for ext in $(printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' '); 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"; \
|
||||
mkdir -p "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext" && \
|
||||
cp "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
@@ -58,12 +65,16 @@ 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/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
|
||||
|
||||
COPY --from=ext-deps /out/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
|
||||
COPY --from=workspace-deps /out/packages/ ./packages/
|
||||
COPY --from=workspace-deps /out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/ ./${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
|
||||
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile \
|
||||
--config.supportedArchitectures.os=linux \
|
||||
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
|
||||
--config.supportedArchitectures.libc=glibc
|
||||
|
||||
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
|
||||
# paths. Matrix's native downloader can hit transient release CDN errors while
|
||||
@@ -95,37 +106,29 @@ RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do
|
||||
# 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 || \
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
|
||||
(echo "A2UI bundle: creating stub (non-fatal)" && \
|
||||
mkdir -p extensions/canvas/src/host/a2ui && \
|
||||
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
|
||||
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
|
||||
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
|
||||
RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm build:docker
|
||||
RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
|
||||
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
|
||||
ENV OPENCLAW_PREFER_PNPM=1
|
||||
RUN pnpm ui:build
|
||||
RUN pnpm qa:lab:build
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
|
||||
RUN pnpm_config_verify_deps_before_run=false 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. Restore the source workspace config after prune so runtime pnpm keeps
|
||||
# patch metadata and package-manager policy.
|
||||
RUN cp pnpm-workspace.yaml /tmp/pnpm-workspace.source.yaml && \
|
||||
printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
|
||||
for ext in $(printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' '); 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 pnpm_config_frozen_lockfile=false pnpm prune --prod && \
|
||||
cp /tmp/pnpm-workspace.source.yaml pnpm-workspace.yaml && \
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
CI=true pnpm prune --prod \
|
||||
--config.offline=true \
|
||||
--config.supportedArchitectures.os=linux \
|
||||
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
|
||||
--config.supportedArchitectures.libc=glibc && \
|
||||
node scripts/postinstall-bundled-plugins.mjs && \
|
||||
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
|
||||
@@ -163,7 +166,7 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates procps hostname curl git lsof openssl python3 tini && \
|
||||
ca-certificates curl git hostname lsof openssl procps python3 tini && \
|
||||
update-ca-certificates
|
||||
|
||||
RUN chown node:node /app
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
const val GATEWAY_PROTOCOL_VERSION = 4
|
||||
const val GATEWAY_MIN_PROTOCOL_VERSION = 3
|
||||
|
||||
@@ -687,7 +687,7 @@ class GatewaySession(
|
||||
}
|
||||
|
||||
return buildJsonObject {
|
||||
put("minProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
|
||||
put("minProtocol", JsonPrimitive(GATEWAY_MIN_PROTOCOL_VERSION))
|
||||
put("maxProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
|
||||
put("client", clientObj)
|
||||
if (options.caps.isNotEmpty()) put("caps", JsonArray(options.caps.map(::JsonPrimitive)))
|
||||
|
||||
@@ -79,6 +79,50 @@ private data class InvokeScenarioResult(
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class GatewaySessionInvokeTest {
|
||||
@Test
|
||||
fun connect_advertisesCompatibleProtocolRange() =
|
||||
runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val connectParams = CompletableDeferred<JsonObject>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
val server =
|
||||
startGatewayServer(json) { webSocket, id, method, frame ->
|
||||
when (method) {
|
||||
"connect" -> {
|
||||
if (!connectParams.isCompleted) {
|
||||
connectParams.complete(frame["params"]!!.jsonObject)
|
||||
}
|
||||
webSocket.send(connectResponseFrame(id))
|
||||
webSocket.close(1000, "done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
connectNodeSession(harness.session, server.port)
|
||||
awaitConnectedOrThrow(connected, lastDisconnect, server)
|
||||
|
||||
val params = withTimeout(TEST_TIMEOUT_MS) { connectParams.await() }
|
||||
assertEquals(
|
||||
GATEWAY_MIN_PROTOCOL_VERSION,
|
||||
params["minProtocol"]?.jsonPrimitive?.content?.toInt(),
|
||||
)
|
||||
assertEquals(
|
||||
GATEWAY_PROTOCOL_VERSION,
|
||||
params["maxProtocol"]?.jsonPrimitive?.content?.toInt(),
|
||||
)
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connect_usesBootstrapTokenWhenSharedAndDeviceTokensAreAbsent() =
|
||||
runBlocking {
|
||||
|
||||
@@ -99,7 +99,7 @@ enum ModelCatalogLoader {
|
||||
]
|
||||
for root in roots {
|
||||
let candidate = root
|
||||
.appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js")
|
||||
.appendingPathComponent("node_modules/@earendil-works/pi-ai/dist/models.generated.js")
|
||||
if FileManager().isReadableFile(atPath: candidate.path) {
|
||||
return candidate.path
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ actor GatewayWizardClient {
|
||||
]
|
||||
|
||||
var params: [String: ProtoAnyCodable] = [
|
||||
"minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
|
||||
"minProtocol": ProtoAnyCodable(GATEWAY_MIN_PROTOCOL_VERSION),
|
||||
"maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
|
||||
"client": ProtoAnyCodable(client),
|
||||
"caps": ProtoAnyCodable([String]()),
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct GatewayChannelConnectTests {
|
||||
private final class ConnectParamsRecorder: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var params: [String: Any]?
|
||||
|
||||
func record(_ message: URLSessionWebSocketTask.Message) {
|
||||
guard let params = GatewayWebSocketTestSupport.connectRequestParams(from: message) else {
|
||||
return
|
||||
}
|
||||
self.lock.lock()
|
||||
self.params = params
|
||||
self.lock.unlock()
|
||||
}
|
||||
|
||||
func snapshot() -> [String: Any]? {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
return self.params
|
||||
}
|
||||
}
|
||||
|
||||
private final class TLSFailureSession: WebSocketSessioning, GatewayTLSFailureProviding, @unchecked Sendable {
|
||||
private var failure: GatewayTLSValidationFailure?
|
||||
|
||||
@@ -87,6 +108,28 @@ struct GatewayChannelConnectTests {
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
}
|
||||
|
||||
@Test func `connect advertises compatible protocol range`() async throws {
|
||||
let recorder = ConnectParamsRecorder()
|
||||
let session = GatewayTestWebSocketSession(
|
||||
taskFactory: {
|
||||
GatewayTestWebSocketTask(
|
||||
sendHook: { _, message, sendIndex in
|
||||
guard sendIndex == 0 else { return }
|
||||
recorder.record(message)
|
||||
})
|
||||
})
|
||||
let channel = try GatewayChannelActor(
|
||||
url: #require(URL(string: "ws://example.invalid")),
|
||||
token: nil,
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
try await channel.connect()
|
||||
|
||||
let params = try #require(recorder.snapshot())
|
||||
#expect(params["minProtocol"] as? Int == GATEWAY_MIN_PROTOCOL_VERSION)
|
||||
#expect(params["maxProtocol"] as? Int == GATEWAY_PROTOCOL_VERSION)
|
||||
}
|
||||
|
||||
@Test func `concurrent connect shares failure`() async throws {
|
||||
let session = self.makeSession(response: .invalid(delayMs: 200))
|
||||
let channel = try GatewayChannelActor(
|
||||
|
||||
@@ -28,6 +28,14 @@ enum GatewayWebSocketTestSupport {
|
||||
return obj["id"] as? String
|
||||
}
|
||||
|
||||
static func connectRequestParams(from message: URLSessionWebSocketTask.Message) -> [String: Any]? {
|
||||
guard let obj = self.requestFrameObject(from: message) else { return nil }
|
||||
guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else {
|
||||
return nil
|
||||
}
|
||||
return obj["params"] as? [String: Any]
|
||||
}
|
||||
|
||||
static func connectOkData(id: String) -> Data {
|
||||
let json = """
|
||||
{
|
||||
@@ -74,6 +82,7 @@ enum GatewayWebSocketTestSupport {
|
||||
"id": "\(id)",
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "INVALID_REQUEST",
|
||||
"message": "\(message)",
|
||||
"details": {
|
||||
"code": "\(detailCode)",
|
||||
|
||||
@@ -130,7 +130,9 @@ private func gatewayErrorDetails(_ error: ErrorShape?) -> [String: ProtoAnyCodab
|
||||
details.merge(nested) { _, nestedValue in nestedValue }
|
||||
}
|
||||
if let error {
|
||||
details["code"] = ProtoAnyCodable(error.code)
|
||||
if details["code"] == nil {
|
||||
details["code"] = ProtoAnyCodable(error.code)
|
||||
}
|
||||
details["message"] = ProtoAnyCodable(error.message)
|
||||
if let retryable = error.retryable {
|
||||
details["retryable"] = ProtoAnyCodable(retryable)
|
||||
@@ -423,7 +425,7 @@ public actor GatewayChannelActor {
|
||||
client["modelIdentifier"] = ProtoAnyCodable(model)
|
||||
}
|
||||
var params: [String: ProtoAnyCodable] = [
|
||||
"minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
|
||||
"minProtocol": ProtoAnyCodable(GATEWAY_MIN_PROTOCOL_VERSION),
|
||||
"maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
|
||||
"client": ProtoAnyCodable(client),
|
||||
"caps": ProtoAnyCodable(options.caps),
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
import Foundation
|
||||
|
||||
public let GATEWAY_PROTOCOL_VERSION = 4
|
||||
public let GATEWAY_MIN_PROTOCOL_VERSION = 3
|
||||
|
||||
private struct GatewayAnyCodingKey: CodingKey, Hashable {
|
||||
let stringValue: String
|
||||
let intValue: Int?
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
self.stringValue = String(intValue)
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
|
||||
public enum ErrorCode: String, Codable, Sendable {
|
||||
case notLinked = "NOT_LINKED"
|
||||
@@ -578,6 +594,9 @@ public struct SendParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
public let replytoid: String?
|
||||
public let threadid: String?
|
||||
public let forcedocument: Bool?
|
||||
public let silent: Bool?
|
||||
public let parsemode: String?
|
||||
public let sessionkey: String?
|
||||
public let idempotencykey: String
|
||||
|
||||
@@ -593,6 +612,9 @@ public struct SendParams: Codable, Sendable {
|
||||
agentid: String?,
|
||||
replytoid: String?,
|
||||
threadid: String?,
|
||||
forcedocument: Bool?,
|
||||
silent: Bool?,
|
||||
parsemode: String?,
|
||||
sessionkey: String?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
@@ -607,6 +629,9 @@ public struct SendParams: Codable, Sendable {
|
||||
self.agentid = agentid
|
||||
self.replytoid = replytoid
|
||||
self.threadid = threadid
|
||||
self.forcedocument = forcedocument
|
||||
self.silent = silent
|
||||
self.parsemode = parsemode
|
||||
self.sessionkey = sessionkey
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
@@ -623,6 +648,9 @@ public struct SendParams: Codable, Sendable {
|
||||
case agentid = "agentId"
|
||||
case replytoid = "replyToId"
|
||||
case threadid = "threadId"
|
||||
case forcedocument = "forceDocument"
|
||||
case silent
|
||||
case parsemode = "parseMode"
|
||||
case sessionkey = "sessionKey"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
@@ -5706,6 +5734,156 @@ public struct PluginControlUiDescriptor: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct PluginsSessionActionFailureResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let error: String
|
||||
public let code: String?
|
||||
public let details: AnyCodable?
|
||||
|
||||
public init(
|
||||
error: String,
|
||||
code: String?,
|
||||
details: AnyCodable?
|
||||
)
|
||||
{
|
||||
self.ok = false
|
||||
self.error = error
|
||||
self.code = code
|
||||
self.details = details
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case error
|
||||
case code
|
||||
case details
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let rawContainer = try decoder.container(keyedBy: GatewayAnyCodingKey.self)
|
||||
let unexpectedKeys = rawContainer.allKeys
|
||||
.map(\.stringValue)
|
||||
.filter { !Set(["ok", "error", "code", "details"]).contains($0) }
|
||||
if !unexpectedKeys.isEmpty {
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(
|
||||
codingPath: rawContainer.codingPath,
|
||||
debugDescription: "Unexpected keys for PluginsSessionActionFailureResult: \(unexpectedKeys.sorted().joined(separator: ", "))"
|
||||
)
|
||||
)
|
||||
}
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let decodedOk = try container.decode(Bool.self, forKey: .ok)
|
||||
guard decodedOk == false else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ok,
|
||||
in: container,
|
||||
debugDescription: "Expected ok to equal false"
|
||||
)
|
||||
}
|
||||
self.ok = false
|
||||
self.error = try container.decode(String.self, forKey: .error)
|
||||
self.code = try container.decodeIfPresent(String.self, forKey: .code)
|
||||
self.details = try container.decodeIfPresent(AnyCodable.self, forKey: .details)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(false, forKey: .ok)
|
||||
try container.encode(error, forKey: .error)
|
||||
try container.encodeIfPresent(code, forKey: .code)
|
||||
try container.encodeIfPresent(details, forKey: .details)
|
||||
}
|
||||
}
|
||||
|
||||
public struct PluginsSessionActionParams: Codable, Sendable {
|
||||
public let pluginid: String
|
||||
public let actionid: String
|
||||
public let sessionkey: String?
|
||||
public let payload: AnyCodable?
|
||||
|
||||
public init(
|
||||
pluginid: String,
|
||||
actionid: String,
|
||||
sessionkey: String?,
|
||||
payload: AnyCodable?)
|
||||
{
|
||||
self.pluginid = pluginid
|
||||
self.actionid = actionid
|
||||
self.sessionkey = sessionkey
|
||||
self.payload = payload
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pluginid = "pluginId"
|
||||
case actionid = "actionId"
|
||||
case sessionkey = "sessionKey"
|
||||
case payload
|
||||
}
|
||||
}
|
||||
|
||||
public struct PluginsSessionActionSuccessResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let result: AnyCodable?
|
||||
public let continueagent: Bool?
|
||||
public let reply: AnyCodable?
|
||||
|
||||
public init(
|
||||
result: AnyCodable?,
|
||||
continueagent: Bool?,
|
||||
reply: AnyCodable?
|
||||
)
|
||||
{
|
||||
self.ok = true
|
||||
self.result = result
|
||||
self.continueagent = continueagent
|
||||
self.reply = reply
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case result
|
||||
case continueagent = "continueAgent"
|
||||
case reply
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let rawContainer = try decoder.container(keyedBy: GatewayAnyCodingKey.self)
|
||||
let unexpectedKeys = rawContainer.allKeys
|
||||
.map(\.stringValue)
|
||||
.filter { !Set(["ok", "result", "continueAgent", "reply"]).contains($0) }
|
||||
if !unexpectedKeys.isEmpty {
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(
|
||||
codingPath: rawContainer.codingPath,
|
||||
debugDescription: "Unexpected keys for PluginsSessionActionSuccessResult: \(unexpectedKeys.sorted().joined(separator: ", "))"
|
||||
)
|
||||
)
|
||||
}
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let decodedOk = try container.decode(Bool.self, forKey: .ok)
|
||||
guard decodedOk == true else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ok,
|
||||
in: container,
|
||||
debugDescription: "Expected ok to equal true"
|
||||
)
|
||||
}
|
||||
self.ok = true
|
||||
self.result = try container.decodeIfPresent(AnyCodable.self, forKey: .result)
|
||||
self.continueagent = try container.decodeIfPresent(Bool.self, forKey: .continueagent)
|
||||
self.reply = try container.decodeIfPresent(AnyCodable.self, forKey: .reply)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(true, forKey: .ok)
|
||||
try container.encodeIfPresent(result, forKey: .result)
|
||||
try container.encodeIfPresent(continueagent, forKey: .continueagent)
|
||||
try container.encodeIfPresent(reply, forKey: .reply)
|
||||
}
|
||||
}
|
||||
|
||||
public struct PluginsUiDescriptorsParams: Codable, Sendable {}
|
||||
|
||||
public struct PluginsUiDescriptorsResult: Codable, Sendable {
|
||||
@@ -6156,6 +6334,37 @@ public struct ShutdownEvent: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PluginsSessionActionResult: Codable, Sendable {
|
||||
case success(PluginsSessionActionSuccessResult)
|
||||
case failure(PluginsSessionActionFailureResult)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case discriminator = "ok"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let discriminator = try container.decode(Bool.self, forKey: .discriminator)
|
||||
switch discriminator {
|
||||
case true: self = try .success(PluginsSessionActionSuccessResult(from: decoder))
|
||||
case false: self = try .failure(PluginsSessionActionFailureResult(from: decoder))
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .discriminator,
|
||||
in: container,
|
||||
debugDescription: "Unknown PluginsSessionActionResult discriminator value"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .success(let value): try value.encode(to: encoder)
|
||||
case .failure(let value): try value.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GatewayFrame: Codable, Sendable {
|
||||
case req(RequestFrame)
|
||||
case res(ResponseFrame)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
c9e88800854b697cb3c9721d0087eb2bc7bcf6ae7239cb51d9849c49ef3d48e3 config-baseline.json
|
||||
67c58457ed2b525975cdb053489f92a5f840c8cf982666393e111fd327dd132e config-baseline.core.json
|
||||
ebb8fa25af8be3a6c42a8bbf505f119819ee49b3c28a317ae04a244f740be381 config-baseline.json
|
||||
647f7a12deed46b4a962848a17ed5666d24fc526b777feab62cf331d84ce957d config-baseline.core.json
|
||||
f90c9d96ccc4c0c703d6c489f86d89fde208cd7f697b396aeee96ff3ee087956 config-baseline.channel.json
|
||||
18f71e9d4a62fe68fbd5bf18d5833a4e380fc705ad641769e1cf05794286344c config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
5219fe6237bdf573740840ef3c29631a8465abb73dd60128fb94589384ae2a96 plugin-sdk-api-baseline.json
|
||||
f63c9723859bb900cf636e5e3fc1349a48563e279ca09e01a7ffafad9efe72f8 plugin-sdk-api-baseline.jsonl
|
||||
19455aee06dd33e2679cfcd8075b10cce806069667097fd7e717aa641c262e51 plugin-sdk-api-baseline.json
|
||||
ea6e0b36ab14977bed8dcf64118e58a8e58a76f41860c32055a73bcd04612826 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -110,8 +110,9 @@ To accept every invite, use `autoJoin: "always"`.
|
||||
|
||||
DM and room allowlists are best populated with stable IDs:
|
||||
|
||||
- DMs (`dm.allowFrom`, `groupAllowFrom`, `groups.<room>.users`): use `@user:server`. Display names only resolve when the homeserver directory returns exactly one match.
|
||||
- Rooms (`groups`, `autoJoinAllowlist`): use `!room:server` or `#alias:server`. Names are resolved best-effort against joined rooms; unresolved entries are ignored at runtime.
|
||||
- DMs (`dm.allowFrom`, `groupAllowFrom`, `groups.<room>.users`): use `@user:server`. Display names are ignored by default because they are mutable; set `dangerouslyAllowNameMatching: true` only when you explicitly need compatibility with display-name entries.
|
||||
- Room allowlist keys (`groups`, legacy `rooms`): use `!room:server` or `#alias:server`. Plain room names are ignored by default; set `dangerouslyAllowNameMatching: true` only when you explicitly need compatibility with joined-room name lookup.
|
||||
- Invite allowlists (`autoJoinAllowlist`): use `!room:server`, `#alias:server`, or `*`. Plain room names are rejected.
|
||||
|
||||
### Account ID normalization
|
||||
|
||||
@@ -823,12 +824,14 @@ keys are not a reliable source for Matrix delivery IDs.
|
||||
Live directory lookup uses the logged-in Matrix account:
|
||||
|
||||
- User lookups query the Matrix user directory on that homeserver.
|
||||
- Room lookups accept explicit room IDs and aliases directly, then fall back to searching joined room names for that account.
|
||||
- Joined-room name lookup is best-effort. If a room name cannot be resolved to an ID or alias, it is ignored by runtime allowlist resolution.
|
||||
- Room lookups accept explicit room IDs and aliases directly. Joined-room name lookup is best-effort and only applies to runtime room allowlists when `dangerouslyAllowNameMatching: true` is set.
|
||||
- If a room name cannot be resolved to an ID or alias, it is ignored by runtime allowlist resolution.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Allowlist-style fields (`groupAllowFrom`, `dm.allowFrom`, `groups.<room>.users`) accept full Matrix user IDs (safest). Exact directory matches are resolved at startup and whenever the allowlist changes while the monitor is running; entries that cannot be resolved are ignored at runtime. Room allowlists prefer room IDs or aliases for the same reason.
|
||||
Allowlist-style user fields (`groupAllowFrom`, `dm.allowFrom`, `groups.<room>.users`) accept full Matrix user IDs (safest). Non-ID user entries are ignored by default. If you set `dangerouslyAllowNameMatching: true`, exact Matrix directory display-name matches are resolved at startup and whenever the allowlist changes while the monitor is running; entries that cannot be resolved are ignored at runtime.
|
||||
|
||||
Room allowlist keys (`groups`, legacy `rooms`) should be room IDs or aliases. Plain room-name keys are ignored by default; `dangerouslyAllowNameMatching: true` restores best-effort lookup against joined room names.
|
||||
|
||||
### Account and connection
|
||||
|
||||
@@ -864,6 +867,7 @@ Allowlist-style fields (`groupAllowFrom`, `dm.allowFrom`, `groups.<room>.users`)
|
||||
- `dm.threadReplies`: DM-only override for reply threading (`"off"`, `"inbound"`, `"always"`).
|
||||
- `allowBots`: accept messages from other configured Matrix bot accounts (`true` or `"mentions"`).
|
||||
- `allowlistOnly`: when `true`, forces all active DM policies (except `"disabled"`) and `"open"` group policies to `"allowlist"`. Does not change `"disabled"` policies.
|
||||
- `dangerouslyAllowNameMatching`: when `true`, allows Matrix display-name directory lookup for user allowlist entries and joined-room name lookup for room allowlist keys. Prefer full `@user:server` IDs and room IDs or aliases.
|
||||
- `autoJoin`: `"always"`, `"allowlist"`, or `"off"`. Default: `"off"`. Applies to every Matrix invite, including DM-style invites.
|
||||
- `autoJoinAllowlist`: rooms/aliases allowed when `autoJoin` is `"allowlist"`. Alias entries are resolved against the homeserver, not against state claimed by the invited room.
|
||||
- `contextVisibility`: supplemental context visibility (`"all"` default, `"allowlist"`, `"allowlist_quote"`).
|
||||
|
||||
@@ -62,7 +62,15 @@ openclaw pairing approve telegram <CODE>
|
||||
</Step>
|
||||
|
||||
<Step title="Add the bot to a group">
|
||||
Add the bot to your group, then set `channels.telegram.groups` and `groupPolicy` to match your access model.
|
||||
Add the bot to your group, then get both IDs that group access needs:
|
||||
|
||||
- your Telegram user ID, used in `allowFrom` / `groupAllowFrom`
|
||||
- the Telegram group chat ID, used as the key under `channels.telegram.groups`
|
||||
|
||||
For first-time setup, get the group chat ID from `openclaw logs --follow`, a forwarded-ID bot, or Bot API `getUpdates`. After the group is allowed, `/whoami@<bot_username>` can confirm the user and group IDs.
|
||||
|
||||
Negative Telegram supergroup IDs that start with `-100` are group chat IDs. Put them under `channels.telegram.groups`, not under `groupAllowFrom`.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -169,6 +177,28 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
Practical pattern for one-owner bots: set your user ID in `channels.telegram.allowFrom`, leave `groupAllowFrom` unset, and allow the target groups under `channels.telegram.groups`.
|
||||
Runtime note: if `channels.telegram` is completely missing, runtime defaults to fail-closed `groupPolicy="allowlist"` unless `channels.defaults.groupPolicy` is explicitly set.
|
||||
|
||||
Owner-only group setup:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["<YOUR_TELEGRAM_USER_ID>"],
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
"<GROUP_CHAT_ID>": {
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Test it from the group with `@<bot_username> ping`. Plain group messages do not trigger the bot while `requireMention: true`.
|
||||
|
||||
Example: allow any member in one specific group:
|
||||
|
||||
```json5
|
||||
@@ -250,6 +280,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- forward a group message to `@userinfobot` / `@getidsbot`
|
||||
- or read `chat.id` from `openclaw logs --follow`
|
||||
- or inspect Bot API `getUpdates`
|
||||
- after the group is allowed, run `/whoami@<bot_username>` if native commands are enabled
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
+19
-13
@@ -388,7 +388,7 @@ The scheduled live/E2E workflow runs the full release-path Docker suite daily.
|
||||
|
||||
## Plugin Prerelease
|
||||
|
||||
`Plugin Prerelease` is more expensive product/package coverage, so it is a separate workflow dispatched by `Full Release Validation` or by an explicit operator. Normal pull requests, `main` pushes, and standalone manual CI dispatches keep that suite off. It balances bundled plugin tests across eight extension workers; those extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The release-only Docker prerelease path batches targeted Docker lanes in small groups to avoid reserving dozens of runners for one-to-three-minute jobs.
|
||||
`Plugin Prerelease` is more expensive product/package coverage, so it is a separate workflow dispatched by `Full Release Validation` or by an explicit operator. Normal pull requests, `main` pushes, and standalone manual CI dispatches keep that suite off. It balances bundled plugin tests across eight extension workers; those extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The release-only Docker prerelease path batches targeted Docker lanes in small groups to avoid reserving dozens of runners for one-to-three-minute jobs. The workflow also uploads an informational `plugin-inspector-advisory` artifact from `@openclaw/plugin-inspector`; inspector findings are triage input and do not change the blocking Plugin Prerelease gate.
|
||||
|
||||
## QA Lab
|
||||
|
||||
@@ -493,13 +493,23 @@ Local changed-test routing lives in `scripts/test-projects.test-support.mjs` and
|
||||
|
||||
## Testbox validation
|
||||
|
||||
Run Testbox from the repo root and prefer a fresh warmed box for broad proof. Before spending a slow gate on a box that was reused, expired, or just reported an unexpectedly large sync, run `pnpm testbox:sanity` inside the box first.
|
||||
Crabbox is the repo-owned remote-box wrapper for maintainer Linux proof. Use it
|
||||
from the repo root when a check is too broad for a local edit loop, when CI
|
||||
parity matters, or when the proof needs secrets, Docker, package lanes,
|
||||
reusable boxes, or remote logs. The normal OpenClaw backend is
|
||||
`blacksmith-testbox`; owned AWS/Hetzner capacity is a fallback for Blacksmith
|
||||
outages, quota issues, or explicit owned-capacity testing.
|
||||
|
||||
The sanity check fails fast when required root files such as `pnpm-lock.yaml` disappeared or when `git status --short` shows at least 200 tracked deletions. That usually means the remote sync state is not a trustworthy copy of the PR; stop that box and warm a fresh one instead of debugging the product test failure. For intentional large-deletion PRs, set `OPENCLAW_TESTBOX_ALLOW_MASS_DELETIONS=1` for that sanity run.
|
||||
Crabbox-backed Blacksmith runs warm, claim, sync, run, report, and clean up
|
||||
one-shot Testboxes. The built-in sync sanity check fails fast when required
|
||||
root files such as `pnpm-lock.yaml` disappear or when `git status --short`
|
||||
shows at least 200 tracked deletions. For intentional large-deletion PRs, set
|
||||
`OPENCLAW_TESTBOX_ALLOW_MASS_DELETIONS=1` for the remote command.
|
||||
|
||||
`pnpm testbox:run` also terminates a local Blacksmith CLI invocation that stays in the sync phase for more than five minutes without post-sync output. Set `OPENCLAW_TESTBOX_SYNC_TIMEOUT_MS=0` to disable that guard, or use a larger millisecond value for unusually large local diffs.
|
||||
|
||||
Crabbox is the repo-owned remote-box wrapper for maintainer Linux proof. Use it when a check is too broad for a local edit loop, when CI parity matters, or when the proof needs secrets, Docker, package lanes, reusable boxes, or remote logs. The normal OpenClaw backend is `blacksmith-testbox`; owned AWS/Hetzner capacity is a fallback for Blacksmith outages, quota issues, or explicit owned-capacity testing.
|
||||
Crabbox also terminates a local Blacksmith CLI invocation that stays in the
|
||||
sync phase for more than five minutes without post-sync output. Set
|
||||
`CRABBOX_BLACKSMITH_SYNC_TIMEOUT_MS=0` to disable that guard, or use a larger
|
||||
millisecond value for unusually large local diffs.
|
||||
|
||||
Before a first run, check the wrapper from the repo root:
|
||||
|
||||
@@ -569,13 +579,9 @@ pnpm crabbox:run -- --provider blacksmith-testbox --id <tbx_id> --no-sync --timi
|
||||
pnpm crabbox:stop -- <tbx_id>
|
||||
```
|
||||
|
||||
If Crabbox is the broken layer but Blacksmith itself works, use direct Blacksmith as a narrow fallback:
|
||||
|
||||
```bash
|
||||
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
|
||||
blacksmith testbox run --id <tbx_id> "env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
|
||||
blacksmith testbox stop --id <tbx_id>
|
||||
```
|
||||
If Crabbox is the broken layer but Blacksmith itself works, use direct
|
||||
Blacksmith only for diagnostics such as `list`, `status`, and cleanup. Fix the
|
||||
Crabbox path before treating a direct Blacksmith run as maintainer proof.
|
||||
|
||||
If `blacksmith testbox list --all` and `blacksmith testbox status` work but new
|
||||
warmups sit `queued` with no IP or Actions run URL after a couple of minutes,
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ Notes:
|
||||
- When WhatsApp is enabled, doctor checks for a degraded Gateway event loop with local `openclaw-tui` clients still running. `doctor --fix` stops only verified local TUI clients so WhatsApp replies are not queued behind stale TUI refresh loops.
|
||||
- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` moves Codex intent onto provider/model-scoped `agentRuntime.id: "codex"` entries, preserves session auth-profile pins such as `openai-codex:...`, removes stale whole-agent/session runtime pins, and keeps repaired OpenAI agent refs on Codex auth routing instead of direct OpenAI API-key auth.
|
||||
- Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions. It also repairs missing downloadable plugins that are referenced by config, such as `plugins.entries`, configured channels, configured provider/search settings, or configured agent runtimes. During package updates, doctor skips package-manager plugin repair until the package swap is complete; rerun `openclaw doctor --fix` afterward if a configured plugin still needs recovery. If the download fails, doctor reports the install error and preserves the configured plugin entry for the next repair attempt.
|
||||
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
|
||||
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.deny`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
|
||||
- Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.<id>` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running.
|
||||
- Set `OPENCLAW_SERVICE_REPAIR_POLICY=external` when another supervisor owns the gateway lifecycle. Doctor still reports gateway/service health and applies non-service repairs, but skips service install/start/restart/bootstrap and legacy service cleanup.
|
||||
- On Linux, doctor ignores inactive extra gateway-like systemd units and does not rewrite command/entrypoint metadata for a running systemd gateway service during repair. Stop the service first or use `openclaw gateway install --force` when you intentionally want to replace the active launcher.
|
||||
|
||||
@@ -24,6 +24,7 @@ Auto-compaction is on by default. It runs when the session nears the context lim
|
||||
|
||||
You will see:
|
||||
|
||||
- `embedded run auto-compaction start` / `complete` in normal Gateway logs.
|
||||
- `🧹 Auto-compaction complete` in verbose mode.
|
||||
- `/status` showing `🧹 Compactions: <count>`.
|
||||
|
||||
|
||||
@@ -250,6 +250,17 @@ number is available. This lane is transcript-visual rather than logged-in
|
||||
Telegram Web proof: the Telegram Bot API gives stable live message evidence, but
|
||||
Telegram Web login state is not required for normal Mantis automation.
|
||||
|
||||
`Mantis Telegram Desktop Proof` is the agentic native Telegram Desktop
|
||||
before/after wrapper. A maintainer can trigger it from a PR comment with
|
||||
`@Mantis telegram desktop proof`, from the Actions UI with freeform
|
||||
instructions, or through the generic `Mantis Scenario` dispatcher. The workflow
|
||||
hands the PR, baseline ref, candidate ref, and maintainer instructions to Codex.
|
||||
The agent reads the PR, decides what Telegram-visible behavior proves the
|
||||
change, runs the real-user Crabbox Telegram Desktop proof lane for baseline and
|
||||
candidate, iterates until the native GIFs are useful, writes paired
|
||||
`motionPreview` artifacts into `mantis-evidence.json`, uploads the bundle, and
|
||||
posts a 2-column PR evidence table when a PR number is available.
|
||||
|
||||
For human-in-the-loop Telegram desktop setup, use the scenario builder:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -123,15 +123,38 @@ OpenClaw **pins the chosen auth profile per session** to keep provider caches wa
|
||||
Manual selection via `/model …@<profileId>` sets a **user override** for that session and is not auto-rotated until a new session starts.
|
||||
|
||||
<Note>
|
||||
Auto-pinned profiles (selected by the session router) are treated as a **preference**: they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts. User-pinned profiles stay locked to that profile; if it fails and model fallbacks are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
Auto-pinned profiles (selected by the session router) are treated as a **preference**: they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts. When the original profile becomes available again, new runs can prefer it again without changing the selected model or runtime. User-pinned profiles stay locked to that profile; if it fails and model fallbacks are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
</Note>
|
||||
|
||||
### Why OAuth can "look lost"
|
||||
### OpenAI Codex subscription plus API-key backup
|
||||
|
||||
If you have both an OAuth profile and an API key profile for the same provider, round-robin can switch between them across messages unless pinned. To force a single profile:
|
||||
For OpenAI agent models, auth and runtime are separate. `openai/gpt-*` stays on
|
||||
the Codex harness while auth can rotate between a Codex subscription profile and
|
||||
an OpenAI API-key backup.
|
||||
|
||||
- Pin with `auth.order[provider] = ["provider:profileId"]`, or
|
||||
- Use a per-session override via `/model …` with a profile override (when supported by your UI/chat surface).
|
||||
Use `auth.order.openai` for the user-facing order:
|
||||
|
||||
```json5
|
||||
{
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:user@example.com", "openai:api-key-backup"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Existing Codex subscription profiles may still use the legacy
|
||||
`openai-codex:*` profile id. The ordered API-key backup can be a normal
|
||||
`openai:*` API-key profile. When the subscription hits a Codex usage limit,
|
||||
OpenClaw records the exact reset time when Codex provides one, tries the next
|
||||
ordered auth profile, and keeps the run inside the Codex harness. Once the reset
|
||||
time passes, the subscription profile is eligible again and the next automatic
|
||||
selection can return to it.
|
||||
|
||||
Use a user-pinned profile only when you want to force one account/key for that
|
||||
session. User-pinned profiles are intentionally strict and do not silently jump
|
||||
to another profile.
|
||||
|
||||
## Cooldowns
|
||||
|
||||
@@ -229,7 +252,7 @@ OpenClaw builds the candidate list from the currently requested `provider/model`
|
||||
- The requested model is always first.
|
||||
- Explicit configured fallbacks are deduplicated but not filtered by the model allowlist. They are treated as explicit operator intent.
|
||||
- If the current run is already on a configured fallback in the same provider family, OpenClaw keeps using the full configured chain.
|
||||
- If the current run is on a different provider than config and that current model is not already part of the configured fallback chain, OpenClaw does not append unrelated configured fallbacks from another provider.
|
||||
- When no explicit fallback override is supplied, configured fallbacks are tried before the configured primary even if the requested model uses a different provider.
|
||||
- When no explicit fallback override is supplied to the fallback runner, the configured primary is appended at the end so the chain can settle back onto the normal default once earlier candidates are exhausted.
|
||||
- When a caller supplies `fallbacksOverride`, the runner uses exactly the requested model plus that override list. An empty list disables model fallback and prevents the configured primary from being appended as a hidden retry target.
|
||||
|
||||
|
||||
@@ -165,11 +165,13 @@ Example allowlist config:
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ Claude login on the host, onboarding/configure can reuse it directly.
|
||||
|
||||
## OAuth exchange (how login works)
|
||||
|
||||
OpenClaw's interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
|
||||
OpenClaw's interactive login flows are implemented in `@earendil-works/pi-ai` and wired into the wizards/commands.
|
||||
|
||||
### Anthropic setup-token
|
||||
|
||||
|
||||
@@ -104,7 +104,8 @@ provenance. The receiving agent should treat them as tool-routed data, not as a
|
||||
direct end-user-authored instruction.
|
||||
|
||||
After the target responds, OpenClaw can run a **reply-back loop** where the
|
||||
agents alternate messages (up to 5 turns). The target agent can reply
|
||||
agents alternate messages (up to `session.agentToAgent.maxPingPongTurns`, range
|
||||
0-20, default 5). The target agent can reply
|
||||
`REPLY_SKIP` to stop early.
|
||||
|
||||
## Status and orchestration helpers
|
||||
|
||||
@@ -94,7 +94,7 @@ Connect (first message):
|
||||
"id": "c1",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 4,
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "openclaw-macos",
|
||||
@@ -266,14 +266,15 @@ The Swift generator emits:
|
||||
|
||||
- `GatewayFrame` enum with `req`, `res`, `event`, and `unknown` cases
|
||||
- Strongly typed payload structs/enums
|
||||
- `ErrorCode` values and `GATEWAY_PROTOCOL_VERSION`
|
||||
- `ErrorCode` values, `GATEWAY_PROTOCOL_VERSION`, and `GATEWAY_MIN_PROTOCOL_VERSION`
|
||||
|
||||
Unknown frame types are preserved as raw payloads for forward compatibility.
|
||||
|
||||
## Versioning + compatibility
|
||||
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that
|
||||
do not include its current protocol.
|
||||
- The Swift models keep unknown frame types to avoid breaking older clients.
|
||||
|
||||
## Schema patterns and conventions
|
||||
|
||||
@@ -1219,7 +1219,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins. Daily reset freshness uses the session row's `sessionStartedAt`; idle reset freshness uses `lastInteractionAt`. Background/system-event writes such as heartbeat, cron wakeups, exec notifications, and gateway bookkeeping can update `updatedAt`, but they do not keep daily/idle sessions fresh.
|
||||
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
|
||||
- **`mainKey`**: legacy field. Runtime always uses `"main"` for the main direct-chat bucket.
|
||||
- **`agentToAgent.maxPingPongTurns`**: maximum reply-back turns between agents during agent-to-agent exchanges (integer, range: `0`–`5`). `0` disables ping-pong chaining.
|
||||
- **`agentToAgent.maxPingPongTurns`**: maximum reply-back turns between agents during agent-to-agent exchanges (integer, range: `0`-`20`, default: `5`). `0` disables ping-pong chaining.
|
||||
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins.
|
||||
- **`maintenance`**: session-store cleanup + retention controls.
|
||||
- `mode`: `warn` emits warnings only; `enforce` applies cleanup.
|
||||
|
||||
@@ -15,7 +15,7 @@ Examples below are aligned with the current config schema. For the exhaustive re
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/.openclaw/workspace" },
|
||||
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
}
|
||||
```
|
||||
@@ -26,14 +26,21 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
|
||||
```json5
|
||||
{
|
||||
identity: {
|
||||
name: "Clawd",
|
||||
theme: "helpful assistant",
|
||||
emoji: "🦞",
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Clawd",
|
||||
theme: "helpful assistant",
|
||||
emoji: "🦞",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
@@ -83,12 +90,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
},
|
||||
},
|
||||
|
||||
// Identity
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
// Identity is per agent — set it on agents.list[].identity below.
|
||||
|
||||
// Logging
|
||||
logging: {
|
||||
@@ -307,6 +309,11 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
{
|
||||
id: "main",
|
||||
default: true,
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
// inherits defaults.skills -> github, weather
|
||||
groupChat: {
|
||||
mentionPatterns: ["@openclaw", "openclaw"],
|
||||
@@ -513,7 +520,7 @@ example `~/.agents/skills/manager -> ~/Projects/manager/skills`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/.openclaw/workspace" },
|
||||
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
|
||||
channels: {
|
||||
whatsapp: { allowFrom: ["+15555550123"] },
|
||||
telegram: {
|
||||
@@ -605,11 +612,13 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
fallbacks: ["minimax/MiniMax-M2.7"],
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
fallbacks: ["minimax/MiniMax-M2.7"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -619,13 +628,20 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
|
||||
|
||||
```json5
|
||||
{
|
||||
identity: {
|
||||
name: "WorkBot",
|
||||
theme: "professional assistant",
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/work-openclaw",
|
||||
elevated: { enabled: false },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/work-openclaw",
|
||||
elevatedDefault: "off",
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "WorkBot",
|
||||
theme: "professional assistant",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
@@ -644,9 +660,11 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "lmstudio/my-local-model" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "lmstudio/my-local-model" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
|
||||
@@ -232,7 +232,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="2b. OpenCode provider overrides">
|
||||
If you've added `models.providers.opencode`, `opencode-zen`, or `opencode-go` manually, it overrides the built-in OpenCode catalog from `@mariozechner/pi-ai`. That can force models onto the wrong API or zero out costs. Doctor warns so you can remove the override and restore per-model API routing + costs.
|
||||
If you've added `models.providers.opencode`, `opencode-zen`, or `opencode-go` manually, it overrides the built-in OpenCode catalog from `@earendil-works/pi-ai`. That can force models onto the wrong API or zero out costs. Doctor warns so you can remove the override and restore per-model API routing + costs.
|
||||
</Accordion>
|
||||
<Accordion title="2c. Browser migration and Chrome MCP readiness">
|
||||
If your browser config still points at the removed Chrome extension path, doctor normalizes it to the current host-local Chrome MCP attach model:
|
||||
|
||||
@@ -44,7 +44,7 @@ Client → Gateway:
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 4,
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "cli",
|
||||
@@ -182,7 +182,7 @@ roles still need scopes under their own role prefix.
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 4,
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "ios-node",
|
||||
@@ -631,7 +631,9 @@ terminal summary, and sanitized error text.
|
||||
## Versioning
|
||||
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that
|
||||
do not include its current protocol. Native clients use a v3 lower bound so
|
||||
additive v4 clients can still reach v3 gateways.
|
||||
- Schemas + models are generated from TypeBox definitions:
|
||||
- `pnpm protocol:gen`
|
||||
- `pnpm protocol:gen:swift`
|
||||
@@ -645,6 +647,7 @@ stable across protocol v4 and are the expected baseline for third-party clients.
|
||||
| Constant | Default | Source |
|
||||
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
|
||||
| `MIN_CLIENT_PROTOCOL_VERSION` | `3` | `src/gateway/protocol/version.ts` |
|
||||
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
|
||||
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) |
|
||||
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
|
||||
|
||||
@@ -112,9 +112,13 @@ Both resolve from process env at activation time. SecretRef details are document
|
||||
|
||||
## Logging
|
||||
|
||||
| Variable | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `OPENCLAW_LOG_LEVEL` | Override log level for both file and console (e.g. `debug`, `trace`). Takes precedence over `logging.level` and `logging.consoleLevel` in config. Invalid values are ignored with a warning. |
|
||||
| Variable | Purpose |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `OPENCLAW_LOG_LEVEL` | Override log level for both file and console (e.g. `debug`, `trace`). Takes precedence over `logging.level` and `logging.consoleLevel` in config. Invalid values are ignored with a warning. |
|
||||
| `OPENCLAW_DEBUG_MODEL_TRANSPORT` | Emit targeted model request/response timing diagnostics at `info` level without enabling global debug logs. |
|
||||
| `OPENCLAW_DEBUG_MODEL_PAYLOAD` | Model payload diagnostics: `summary`, `tools`, or `full-redacted`. `full-redacted` is capped and redacted but may include prompt/message text. |
|
||||
| `OPENCLAW_DEBUG_SSE` | Streaming diagnostics: `events` for first/done timing, `peek` to include the first five redacted SSE events. |
|
||||
| `OPENCLAW_DEBUG_CODE_MODE` | Code-mode model-surface diagnostics, including provider-tool hiding and exec/wait-only enforcement. |
|
||||
|
||||
### `OPENCLAW_HOME`
|
||||
|
||||
|
||||
@@ -144,6 +144,19 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="If two providers expose the same model id, which one does /model use?">
|
||||
`/model provider/model` selects that exact provider route for the session.
|
||||
|
||||
For example, `qianfan/deepseek-v4-flash` and `deepseek/deepseek-v4-flash` are different model refs even though both contain `deepseek-v4-flash`. OpenClaw should not silently switch from one provider to the other just because the bare model id matches.
|
||||
|
||||
A user-selected `/model` ref is also strict for fallback policy. If that selected provider/model is unavailable, the reply fails visibly instead of answering from `agents.defaults.model.fallbacks`. Configured fallback chains still apply to configured defaults, cron job primaries, and auto-selected fallback state.
|
||||
|
||||
If a run that started from a non-session override is allowed to use fallback, OpenClaw tries the requested provider/model first, then configured fallbacks, and only then the configured primary. That prevents duplicate bare model ids from jumping directly back to the default provider.
|
||||
|
||||
See [Models](/concepts/models) and [Model failover](/concepts/model-failover).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I use GPT 5.5 for daily tasks and Codex 5.5 for coding?">
|
||||
Yes. Treat model choice and runtime choice separately:
|
||||
|
||||
|
||||
+17
-2
@@ -340,6 +340,21 @@ telegram-live`) or directly from a pull request comment:
|
||||
@Mantis telegram scenarios=telegram-status-command,telegram-mentioned-message-reply
|
||||
```
|
||||
|
||||
`Mantis Telegram Desktop Proof` is the agentic native Telegram Desktop
|
||||
before/after wrapper for PR visual proof. Start it from the Actions UI with
|
||||
freeform `instructions`, through `Mantis Scenario` (`scenario_id:
|
||||
telegram-desktop-proof`), or from a PR comment:
|
||||
|
||||
```text
|
||||
@Mantis telegram desktop proof
|
||||
```
|
||||
|
||||
The Mantis agent reads the PR, decides what Telegram-visible behavior proves the
|
||||
change, runs the real-user Crabbox Telegram Desktop proof lane on baseline and
|
||||
candidate refs, iterates until the native GIFs are useful, writes a paired
|
||||
`motionPreview` manifest, and posts the same 2-column GIF table through the
|
||||
Mantis GitHub App when `pr_number` is set.
|
||||
|
||||
- `pnpm openclaw qa mantis telegram-desktop-builder`
|
||||
- Leases or reuses a Crabbox Linux desktop, installs native Telegram Desktop, configures OpenClaw with a leased Telegram SUT bot token, starts the gateway, and records screenshot/MP4 evidence from the visible VNC desktop.
|
||||
- Defaults to `--credential-source convex` so workflows only need the Convex broker secret. Use `--credential-source env` with the same `OPENCLAW_QA_TELEGRAM_*` variables as `pnpm openclaw qa telegram`.
|
||||
@@ -538,8 +553,8 @@ Think of the suites as "increasing realism" (and increasing flakiness/cost):
|
||||
|
||||
Native dependency policy:
|
||||
|
||||
- Default test installs skip optional native Discord opus builds. Discord voice receive uses the pure-JS `opusscript` decoder, and `@discordjs/opus` stays in `ignoredBuiltDependencies` so local tests and Testbox lanes do not compile the native addon.
|
||||
- Use a dedicated Discord voice performance or live lane if you intentionally need to compare a native opus build. Do not add `@discordjs/opus` back to the default `onlyBuiltDependencies`; that makes unrelated install/test loops compile native code.
|
||||
- Default test installs skip optional native Discord opus builds. Discord voice receive uses the pure-JS `opusscript` decoder, and `@discordjs/opus` stays disabled in `allowBuilds` so local tests and Testbox lanes do not compile the native addon.
|
||||
- Use a dedicated Discord voice performance or live lane if you intentionally need to compare a native opus build. Do not set `@discordjs/opus` to `true` in the default `allowBuilds`; that makes unrelated install/test loops compile native code.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Projects, shards, and scoped lanes">
|
||||
|
||||
@@ -409,12 +409,13 @@ See [ClawDock](/install/clawdock) for the full helper guide.
|
||||
|
||||
1. **Persist `/home/node`**: `export OPENCLAW_HOME_VOLUME="openclaw_home"`
|
||||
2. **Bake system deps**: `export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq"`
|
||||
3. **Install Playwright browsers**:
|
||||
3. **Bake Playwright Chromium**: `export OPENCLAW_INSTALL_BROWSER=1`
|
||||
4. **Or install Playwright browsers into a persisted volume**:
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli \
|
||||
node /app/node_modules/playwright-core/cli.js install chromium
|
||||
```
|
||||
4. **Persist browser downloads**: use `OPENCLAW_HOME_VOLUME` or
|
||||
5. **Persist browser downloads**: use `OPENCLAW_HOME_VOLUME` or
|
||||
`OPENCLAW_EXTRA_MOUNTS`. OpenClaw auto-detects the Docker image's
|
||||
Playwright-managed Chromium on Linux.
|
||||
|
||||
|
||||
@@ -175,6 +175,39 @@ You can override both via the **`OPENCLAW_LOG_LEVEL`** environment variable (e.g
|
||||
`--verbose` only affects console output and WS log verbosity; it does not change
|
||||
file log levels.
|
||||
|
||||
### Targeted model transport diagnostics
|
||||
|
||||
When debugging provider calls, use targeted environment flags instead of raising
|
||||
all logs to `debug`:
|
||||
|
||||
```bash
|
||||
OPENCLAW_DEBUG_MODEL_TRANSPORT=1 openclaw gateway
|
||||
OPENCLAW_DEBUG_MODEL_PAYLOAD=tools OPENCLAW_DEBUG_SSE=events openclaw gateway
|
||||
```
|
||||
|
||||
Available flags:
|
||||
|
||||
- `OPENCLAW_DEBUG_MODEL_TRANSPORT=1`: emit request start, fetch response, SDK
|
||||
headers, first streaming event, stream completion, and transport errors at
|
||||
`info` level.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=summary`: include a bounded request payload
|
||||
summary in model request logs.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=tools`: include all model-facing tool names in
|
||||
the payload summary.
|
||||
- `OPENCLAW_DEBUG_MODEL_PAYLOAD=full-redacted`: include a redacted, capped JSON
|
||||
payload snapshot. Use only while debugging; secrets are redacted but prompts
|
||||
and message text may still be present.
|
||||
- `OPENCLAW_DEBUG_SSE=events`: emit first-event and stream-completion timing.
|
||||
- `OPENCLAW_DEBUG_SSE=peek`: also emit the first five redacted SSE event
|
||||
payloads, capped per event.
|
||||
- `OPENCLAW_DEBUG_CODE_MODE=1`: emit code-mode model-surface diagnostics,
|
||||
including when native provider tools are hidden because code mode owns the
|
||||
tool surface.
|
||||
|
||||
These flags log through normal OpenClaw logging, so `openclaw logs --follow`
|
||||
and the Control UI Logs tab show them. Without the flags, the same diagnostics
|
||||
remain available at `debug` level.
|
||||
|
||||
### Trace correlation
|
||||
|
||||
File logs are JSONL. When a log call carries a valid diagnostic trace context,
|
||||
|
||||
+6
-6
@@ -23,10 +23,10 @@ OpenClaw uses the pi SDK to embed an AI coding agent into its messaging gateway
|
||||
|
||||
```json
|
||||
{
|
||||
"@mariozechner/pi-agent-core": "0.73.0",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"@mariozechner/pi-coding-agent": "0.73.0",
|
||||
"@mariozechner/pi-tui": "0.73.0"
|
||||
"@earendil-works/pi-agent-core": "0.74.0",
|
||||
"@earendil-works/pi-ai": "0.74.0",
|
||||
"@earendil-works/pi-coding-agent": "0.74.0",
|
||||
"@earendil-works/pi-tui": "0.74.0"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -171,7 +171,7 @@ import {
|
||||
DefaultResourceLoader,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd: resolvedWorkspace,
|
||||
@@ -518,7 +518,7 @@ OpenClaw also has a local TUI mode that uses pi-tui components directly:
|
||||
|
||||
```typescript
|
||||
// src/tui/tui.ts
|
||||
import { ... } from "@mariozechner/pi-tui";
|
||||
import { ... } from "@earendil-works/pi-tui";
|
||||
```
|
||||
|
||||
This provides the interactive terminal experience similar to pi's native mode.
|
||||
|
||||
@@ -499,6 +499,30 @@ const video = await api.runtime.mediaUnderstanding.describeVideoFile({
|
||||
filePath: "/tmp/inbound-video.mp4",
|
||||
cfg: api.config,
|
||||
});
|
||||
|
||||
const extraction = await api.runtime.mediaUnderstanding.extractStructuredWithModel({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
input: [
|
||||
{
|
||||
type: "image",
|
||||
buffer: receiptImageBuffer,
|
||||
fileName: "receipt.png",
|
||||
mime: "image/png",
|
||||
},
|
||||
{ type: "text", text: "Use the printed fields as the source of truth." },
|
||||
],
|
||||
instructions: "Return entities and searchable tags.",
|
||||
schemaName: "example.evidence",
|
||||
jsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entities: { type: "array", items: { type: "string" } },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
cfg: api.config,
|
||||
});
|
||||
```
|
||||
|
||||
For audio transcription, plugins can use either the media-understanding runtime
|
||||
@@ -517,6 +541,11 @@ Notes:
|
||||
|
||||
- `api.runtime.mediaUnderstanding.*` is the preferred shared surface for
|
||||
image/audio/video understanding.
|
||||
- `extractStructuredWithModel(...)` is the plugin-facing seam for bounded
|
||||
provider-owned image-first extraction. Include at least one image input;
|
||||
text inputs are supplemental context.
|
||||
product plugins own their routes and schemas while OpenClaw owns the
|
||||
provider/runtime boundary.
|
||||
- Uses core media-understanding audio configuration (`tools.media.audio`) and provider fallback order.
|
||||
- Returns `{ text: undefined }` when no transcription output is produced (for example skipped/unsupported input).
|
||||
- `api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias.
|
||||
|
||||
@@ -17,9 +17,9 @@ selection, OpenClaw dynamic tools, approvals, media delivery, and the visible
|
||||
transcript mirror.
|
||||
|
||||
The normal setup uses canonical OpenAI model refs such as `openai/gpt-5.5`.
|
||||
Do not configure `openai-codex/gpt-*` model refs. `openai-codex` is the auth
|
||||
profile provider for Codex OAuth or Codex API-key profiles, not the model
|
||||
provider prefix for new agent config.
|
||||
Do not configure `openai-codex/gpt-*` model refs. Put OpenAI agent auth order
|
||||
under `auth.order.openai`; older `openai-codex:*` profiles and
|
||||
`auth.order.openai-codex` entries remain supported for existing installs.
|
||||
|
||||
OpenClaw starts Codex app-server threads with Codex native code mode and
|
||||
code-mode-only enabled. That keeps deferred/searchable OpenClaw dynamic tools
|
||||
@@ -101,21 +101,37 @@ turn resolves the harness from current config.
|
||||
The quickstart config is the minimum viable Codex harness config. Set Codex
|
||||
harness options in OpenClaw config, and use the CLI only for Codex auth:
|
||||
|
||||
| Need | Set | Where |
|
||||
| -------------------------------------- | ------------------------------------------------------------------ | ------------------------------ |
|
||||
| Enable the harness | `plugins.entries.codex.enabled: true` | OpenClaw config |
|
||||
| Keep an allowlisted plugin install | Include `codex` in `plugins.allow` | OpenClaw config |
|
||||
| Route OpenAI agent turns through Codex | `agents.defaults.model` or `agents.list[].model` as `openai/gpt-*` | OpenClaw agent config |
|
||||
| Sign in with Codex OAuth | `openclaw models auth login --provider openai-codex` | CLI auth profile |
|
||||
| Fail closed when Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | OpenClaw model/provider config |
|
||||
| Use direct OpenAI API traffic | Provider or model `agentRuntime.id: "pi"` with normal OpenAI auth | OpenClaw model/provider config |
|
||||
| Tune app-server behavior | `plugins.entries.codex.config.appServer.*` | Codex plugin config |
|
||||
| Enable native Codex plugin apps | `plugins.entries.codex.config.codexPlugins.*` | Codex plugin config |
|
||||
| Enable Codex Computer Use | `plugins.entries.codex.config.computerUse.*` | Codex plugin config |
|
||||
| Need | Set | Where |
|
||||
| -------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| Enable the harness | `plugins.entries.codex.enabled: true` | OpenClaw config |
|
||||
| Keep an allowlisted plugin install | Include `codex` in `plugins.allow` | OpenClaw config |
|
||||
| Route OpenAI agent turns through Codex | `agents.defaults.model` or `agents.list[].model` as `openai/gpt-*` | OpenClaw agent config |
|
||||
| Sign in with Codex OAuth | `openclaw models auth login --provider openai-codex` | CLI auth profile |
|
||||
| Add API-key backup for Codex runs | `openai:*` API-key profile listed after subscription auth in `auth.order.openai` | CLI auth profile + OpenClaw config |
|
||||
| Fail closed when Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | OpenClaw model/provider config |
|
||||
| Use direct OpenAI API traffic | Provider or model `agentRuntime.id: "pi"` with normal OpenAI auth | OpenClaw model/provider config |
|
||||
| Tune app-server behavior | `plugins.entries.codex.config.appServer.*` | Codex plugin config |
|
||||
| Enable native Codex plugin apps | `plugins.entries.codex.config.codexPlugins.*` | Codex plugin config |
|
||||
| Enable Codex Computer Use | `plugins.entries.codex.config.computerUse.*` | Codex plugin config |
|
||||
|
||||
Use `openai/gpt-*` model refs for Codex-backed OpenAI agent turns.
|
||||
`openai-codex` is only the auth-profile provider name for Codex OAuth and
|
||||
Codex API-key profiles. Do not write new `openai-codex/gpt-*` model refs.
|
||||
Use `openai/gpt-*` model refs for Codex-backed OpenAI agent turns. Prefer
|
||||
`auth.order.openai` for subscription-first/API-key-backup ordering. Existing
|
||||
`openai-codex:*` auth profiles and `auth.order.openai-codex` remain valid, but
|
||||
do not write new `openai-codex/gpt-*` model refs.
|
||||
|
||||
```json5
|
||||
{
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:user@example.com", "openai:api-key-backup"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
In that shape, both profiles still run through Codex for `openai/gpt-*` agent
|
||||
turns. The API key is only an auth fallback, not a request to switch to PI or
|
||||
plain OpenAI Responses.
|
||||
|
||||
The rest of this page covers common variants users must choose between:
|
||||
deployment shape, fail-closed routing, guardian approval policy, native Codex
|
||||
@@ -384,7 +400,8 @@ For upload mechanics and runtime-level diagnostics boundaries, see
|
||||
|
||||
Auth is selected in this order:
|
||||
|
||||
1. An explicit OpenClaw Codex auth profile for the agent.
|
||||
1. Ordered OpenAI auth profiles for the agent, preferably under
|
||||
`auth.order.openai`. Existing `openai-codex:*` profile ids remain valid.
|
||||
2. The app-server's existing account in that agent's Codex home.
|
||||
3. For local stdio app-server launches only, `CODEX_API_KEY`, then
|
||||
`OPENAI_API_KEY`, when no app-server account is present and OpenAI auth is
|
||||
@@ -399,6 +416,11 @@ login instead of inherited child-process env. WebSocket app-server connections
|
||||
do not receive Gateway env API-key fallback; use an explicit auth profile or the
|
||||
remote app-server's own account.
|
||||
|
||||
If a subscription profile hits a Codex usage limit, OpenClaw records the reset
|
||||
time when Codex reports one and tries the next ordered auth profile for the same
|
||||
Codex run. When the reset time passes, the subscription profile becomes eligible
|
||||
again without changing the selected `openai/gpt-*` model or Codex runtime.
|
||||
|
||||
If a deployment needs additional environment isolation, add those variables to
|
||||
`appServer.clearEnv`:
|
||||
|
||||
|
||||
@@ -137,8 +137,9 @@ are emitted with `open_world_enabled: true`; OpenClaw does not expose a separate
|
||||
plugin open-world policy knob and does not maintain per-plugin destructive
|
||||
tool-name deny lists.
|
||||
|
||||
Tool approval mode is prompted by default for plugin apps because OpenClaw does
|
||||
not have an interactive app-elicitation UI in this same-thread path.
|
||||
Tool approval mode is automatic by default for plugin apps so non-destructive
|
||||
read tools can run without a same-thread approval UI. Destructive tools remain
|
||||
controlled by each app's `destructive_enabled` policy.
|
||||
|
||||
## Destructive action policy
|
||||
|
||||
|
||||
@@ -69,16 +69,14 @@ not publish the inspector binary from the main `openclaw` package.
|
||||
|
||||
### Maintainer acceptance lane
|
||||
|
||||
Use Blacksmith Testbox for the installable-package acceptance lane when validating
|
||||
the external inspector against OpenClaw plugin packages. Run it from a clean
|
||||
OpenClaw checkout after the package is built:
|
||||
Use Crabbox-backed Blacksmith Testbox for the installable-package acceptance
|
||||
lane when validating the external inspector against OpenClaw plugin packages.
|
||||
Run it from a clean OpenClaw checkout after the package is built:
|
||||
|
||||
```sh
|
||||
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
|
||||
blacksmith testbox run --id <tbx_id> "pnpm install && pnpm build && npm exec --yes @openclaw/plugin-inspector@0.1.0 -- ./extensions/telegram --json"
|
||||
blacksmith testbox run --id <tbx_id> "npm exec --yes @openclaw/plugin-inspector@0.1.0 -- ./extensions/discord --json"
|
||||
blacksmith testbox run --id <tbx_id> "npm exec --yes @openclaw/plugin-inspector@0.1.0 -- <clawhub-plugin-dir> --json"
|
||||
blacksmith testbox stop <tbx_id>
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json --shell -- "pnpm install && pnpm build && npm exec --yes @openclaw/plugin-inspector@0.1.0 -- ./extensions/telegram --json"
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json --shell -- "npm exec --yes @openclaw/plugin-inspector@0.1.0 -- ./extensions/discord --json"
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json --shell -- "npm exec --yes @openclaw/plugin-inspector@0.1.0 -- <clawhub-plugin-dir> --json"
|
||||
```
|
||||
|
||||
Keep this lane opt-in for maintainers because it installs an external npm
|
||||
|
||||
@@ -140,18 +140,52 @@ generic contracts; Plan Mode can use them, but so can approval workflows,
|
||||
workspace policy gates, background monitors, setup wizards, and UI companion
|
||||
plugins.
|
||||
|
||||
| Method | Contract it owns |
|
||||
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.registerSessionExtension(...)` | Plugin-owned, JSON-compatible session state projected through Gateway sessions |
|
||||
| `api.enqueueNextTurnInjection(...)` | Durable exactly-once context injected into the next agent turn for one session |
|
||||
| `api.registerTrustedToolPolicy(...)` | Bundled/trusted pre-plugin tool policy that can block or rewrite tool params |
|
||||
| `api.registerToolMetadata(...)` | Tool catalog display metadata without changing the tool implementation |
|
||||
| `api.registerCommand(...)` | Scoped plugin commands; command results can set `continueAgent: true`; Discord native commands support `descriptionLocalizations` |
|
||||
| `api.registerControlUiDescriptor(...)` | Control UI contribution descriptors for session, tool, run, or settings surfaces |
|
||||
| `api.registerRuntimeLifecycle(...)` | Cleanup callbacks for plugin-owned runtime resources on reset/delete/reload paths |
|
||||
| `api.registerAgentEventSubscription(...)` | Sanitized event subscriptions for workflow state and monitors |
|
||||
| `api.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)` | Per-run plugin scratch state cleared on terminal run lifecycle |
|
||||
| `api.registerSessionSchedulerJob(...)` | Plugin-owned session scheduler job records with deterministic cleanup |
|
||||
| Method | Contract it owns |
|
||||
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.session.state.registerSessionExtension(...)` | Plugin-owned, JSON-compatible session state projected through Gateway sessions |
|
||||
| `api.session.workflow.enqueueNextTurnInjection(...)` | Durable exactly-once context injected into the next agent turn for one session |
|
||||
| `api.registerTrustedToolPolicy(...)` | Bundled/trusted pre-plugin tool policy that can block or rewrite tool params |
|
||||
| `api.registerToolMetadata(...)` | Tool catalog display metadata without changing the tool implementation |
|
||||
| `api.registerCommand(...)` | Scoped plugin commands; command results can set `continueAgent: true`; Discord native commands support `descriptionLocalizations` |
|
||||
| `api.session.controls.registerControlUiDescriptor(...)` | Control UI contribution descriptors for session, tool, run, or settings surfaces |
|
||||
| `api.lifecycle.registerRuntimeLifecycle(...)` | Cleanup callbacks for plugin-owned runtime resources on reset/delete/reload paths |
|
||||
| `api.agent.events.registerAgentEventSubscription(...)` | Sanitized event subscriptions for workflow state and monitors |
|
||||
| `api.runContext.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)` | Per-run plugin scratch state cleared on terminal run lifecycle |
|
||||
| `api.session.workflow.registerSessionSchedulerJob(...)` | Cleanup metadata for plugin-owned scheduler jobs; does not schedule work or create task records |
|
||||
| `api.session.workflow.sendSessionAttachment(...)` | Bundled-only host-mediated file attachment delivery to the active direct-outbound session route |
|
||||
| `api.session.workflow.scheduleSessionTurn(...)` / `unscheduleSessionTurnsByTag(...)` | Bundled-only Cron-backed scheduled session turns plus tag-based cleanup |
|
||||
| `api.session.controls.registerSessionAction(...)` | Typed session actions clients can dispatch through the Gateway |
|
||||
|
||||
Use the grouped namespaces for new plugin code:
|
||||
|
||||
- `api.session.state.registerSessionExtension(...)`
|
||||
- `api.session.workflow.enqueueNextTurnInjection(...)`
|
||||
- `api.session.workflow.registerSessionSchedulerJob(...)`
|
||||
- `api.session.workflow.sendSessionAttachment(...)`
|
||||
- `api.session.workflow.scheduleSessionTurn(...)`
|
||||
- `api.session.workflow.unscheduleSessionTurnsByTag(...)`
|
||||
- `api.session.controls.registerSessionAction(...)`
|
||||
- `api.session.controls.registerControlUiDescriptor(...)`
|
||||
- `api.agent.events.registerAgentEventSubscription(...)`
|
||||
- `api.agent.events.emitAgentEvent(...)`
|
||||
- `api.runContext.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)`
|
||||
- `api.lifecycle.registerRuntimeLifecycle(...)`
|
||||
|
||||
The equivalent flat methods remain available as deprecated compatibility
|
||||
aliases for existing plugins. Do not add new plugin code that calls
|
||||
`api.registerSessionExtension`, `api.enqueueNextTurnInjection`,
|
||||
`api.registerControlUiDescriptor`, `api.registerRuntimeLifecycle`,
|
||||
`api.registerAgentEventSubscription`, `api.emitAgentEvent`,
|
||||
`api.setRunContext`, `api.getRunContext`, `api.clearRunContext`,
|
||||
`api.registerSessionSchedulerJob`, `api.registerSessionAction`,
|
||||
`api.sendSessionAttachment`, `api.scheduleSessionTurn`, or
|
||||
`api.unscheduleSessionTurnsByTag` directly.
|
||||
|
||||
`scheduleSessionTurn(...)` is a session-scoped convenience over the Gateway
|
||||
Cron scheduler. Cron owns timing and creates the background task record when the
|
||||
turn runs; the Plugin SDK only constrains the target session, plugin-owned
|
||||
naming, and cleanup. Use `api.runtime.tasks.managedFlows` inside the scheduled
|
||||
turn when the work itself needs durable multi-step Task Flow state.
|
||||
|
||||
The contracts intentionally split authority:
|
||||
|
||||
|
||||
@@ -217,6 +217,11 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
<Accordion title="api.runtime.tasks.managedFlows">
|
||||
Bind a Task Flow runtime to an existing OpenClaw session key or trusted tool context, then create and manage Task Flows without passing an owner on every call.
|
||||
|
||||
Task Flow tracks durable multi-step workflow state. It is not a scheduler:
|
||||
use Cron or `api.session.workflow.scheduleSessionTurn(...)` for future
|
||||
wakeups, then use `managedFlows` from the scheduled turn when that work
|
||||
needs flow state, child tasks, waits, or cancellation.
|
||||
|
||||
```typescript
|
||||
const taskFlow = api.runtime.tasks.managedFlows.fromToolContext(ctx);
|
||||
|
||||
@@ -300,6 +305,34 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
filePath: "/tmp/inbound-file.pdf",
|
||||
cfg: api.config,
|
||||
});
|
||||
|
||||
// Structured image extraction through a specific provider/model.
|
||||
// Include at least one image; text inputs are supplemental context.
|
||||
const evidence = await api.runtime.mediaUnderstanding.extractStructuredWithModel({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
input: [
|
||||
{
|
||||
type: "image",
|
||||
buffer: receiptImageBuffer,
|
||||
fileName: "receipt.png",
|
||||
mime: "image/png",
|
||||
},
|
||||
{ type: "text", text: "Prefer the printed total over handwritten notes." },
|
||||
],
|
||||
instructions: "Extract vendor, total, and searchable tags.",
|
||||
schemaName: "receipt.evidence",
|
||||
jsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vendor: { type: "string" },
|
||||
total: { type: "number" },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
required: ["vendor", "total"],
|
||||
},
|
||||
cfg: api.config,
|
||||
});
|
||||
```
|
||||
|
||||
Returns `{ text: undefined }` when no output is produced (e.g. skipped input).
|
||||
|
||||
@@ -306,7 +306,7 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
|
||||
| `plugin-sdk/media-mime` | Narrow MIME normalization, file-extension mapping, MIME detection, and media-kind helpers |
|
||||
| `plugin-sdk/media-store` | Narrow media store helpers such as `saveMediaBuffer` |
|
||||
| `plugin-sdk/media-generation-runtime` | Shared media-generation failover helpers, candidate selection, and missing-model messaging |
|
||||
| `plugin-sdk/media-understanding` | Media understanding provider types plus provider-facing image/audio helper exports |
|
||||
| `plugin-sdk/media-understanding` | Media understanding provider types plus provider-facing image/audio/structured-extraction helper exports |
|
||||
| `plugin-sdk/text-chunking` | Text and markdown chunking/render helpers, markdown table conversion, directive-tag stripping, and safe-text utilities |
|
||||
| `plugin-sdk/text-chunking` | Outbound text chunking helper |
|
||||
| `plugin-sdk/speech` | Speech provider types plus provider-facing directive, registry, validation, OpenAI-compatible TTS builder, and speech helper exports |
|
||||
|
||||
+11
-9
@@ -43,17 +43,19 @@ OpenClaw ships a bundled `fal` provider for hosted image and video generation.
|
||||
The bundled `fal` image-generation provider defaults to
|
||||
`fal/fal-ai/flux/dev`.
|
||||
|
||||
| Capability | Value |
|
||||
| -------------- | -------------------------- |
|
||||
| Max images | 4 per request |
|
||||
| Edit mode | Enabled, 1 reference image |
|
||||
| Size overrides | Supported |
|
||||
| Aspect ratio | Supported |
|
||||
| Resolution | Supported |
|
||||
| Output format | `png` or `jpeg` |
|
||||
| Capability | Value |
|
||||
| -------------- | ----------------------------------------------------------- |
|
||||
| Max images | 4 per request |
|
||||
| Edit mode | Flux: 1 reference image; GPT Image 2: 10; Nano Banana 2: 14 |
|
||||
| Size overrides | Supported |
|
||||
| Aspect ratio | Supported for generate and GPT Image 2/Nano Banana 2 edit |
|
||||
| Resolution | Supported |
|
||||
| Output format | `png` or `jpeg` |
|
||||
|
||||
<Warning>
|
||||
The fal image edit endpoint does **not** support `aspectRatio` overrides.
|
||||
Flux image-to-image requests do **not** support `aspectRatio` overrides. GPT
|
||||
Image 2 and Nano Banana 2 edit requests use fal's `/edit` endpoint and accept
|
||||
aspect-ratio hints.
|
||||
</Warning>
|
||||
|
||||
Use `outputFormat: "png"` when you want PNG output. fal does not declare an
|
||||
|
||||
@@ -226,8 +226,8 @@ The bundled `google` plugin also registers video generation through the shared
|
||||
|
||||
- Default video model: `google/veo-3.1-fast-generate-preview`
|
||||
- Modes: text-to-video, image-to-video, and single-video reference flows
|
||||
- Supports `aspectRatio`, `resolution`, and `audio`
|
||||
- Current duration clamp: **4 to 8 seconds**
|
||||
- Supports `aspectRatio` (`16:9`, `9:16`) and `resolution` (`720P`, `1080P`); audio output is not supported by Veo today
|
||||
- Supported durations: **4, 6, or 8 seconds** (other values snap to the nearest allowed value)
|
||||
|
||||
To use Google as the default video provider:
|
||||
|
||||
|
||||
+72
-39
@@ -17,8 +17,8 @@ default; direct OpenAI API-key auth remains available for non-agent OpenAI
|
||||
surfaces such as images, embeddings, speech, and realtime.
|
||||
|
||||
- **Agent models** - `openai/*` models through the Codex runtime; sign in with
|
||||
`openai-codex` auth for ChatGPT/Codex subscription use, or configure an
|
||||
`openai-codex` API-key profile when you intentionally want API-key auth.
|
||||
Codex auth for ChatGPT/Codex subscription use, or configure a Codex-compatible
|
||||
OpenAI API-key backup when you intentionally want API-key auth.
|
||||
- **Non-agent OpenAI APIs** - direct OpenAI Platform access with usage-based
|
||||
billing through `OPENAI_API_KEY` or OpenAI API-key onboarding.
|
||||
- **Legacy config** - `openai-codex/*` model refs are repaired by
|
||||
@@ -32,32 +32,34 @@ changing config.
|
||||
|
||||
## Quick choice
|
||||
|
||||
| Goal | Use | Notes |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-5.5` | Default OpenAI agent setup. Sign in with `openai-codex` auth. |
|
||||
| Direct API-key billing for agent models | `openai/gpt-5.5` plus an `openai-codex` API-key profile | Use `auth.order.openai-codex` to prefer that profile. |
|
||||
| Direct API-key billing through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select a normal `openai` API-key profile. |
|
||||
| Latest ChatGPT Instant API alias | `openai/chat-latest` | Direct API-key only. Moving alias for experiments, not the default. |
|
||||
| ChatGPT/Codex subscription auth through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select an `openai-codex` auth profile for the compatibility route. |
|
||||
| Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. |
|
||||
| Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. |
|
||||
| Goal | Use | Notes |
|
||||
| ---------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-5.5` | Default OpenAI agent setup. Sign in with Codex auth. |
|
||||
| Direct API-key billing for agent models | `openai/gpt-5.5` plus a Codex-compatible API-key profile | Use `auth.order.openai` to place the backup after subscription auth. |
|
||||
| Direct API-key billing through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select a normal `openai` API-key profile. |
|
||||
| Latest ChatGPT Instant API alias | `openai/chat-latest` | Direct API-key only. Moving alias for experiments, not the default. |
|
||||
| ChatGPT/Codex subscription auth through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select an `openai-codex` auth profile for the compatibility route. |
|
||||
| Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. |
|
||||
| Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. |
|
||||
|
||||
## Naming map
|
||||
|
||||
The names are similar but not interchangeable:
|
||||
|
||||
| Name you see | Layer | Meaning |
|
||||
| --------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `openai` | Provider prefix | Canonical OpenAI model route; agent turns use the Codex runtime. |
|
||||
| `openai-codex` | Auth/profile prefix | OpenAI Codex OAuth/subscription auth profile provider. |
|
||||
| `codex` plugin | Plugin | Bundled OpenClaw plugin that provides native Codex app-server runtime and `/codex` chat controls. |
|
||||
| provider/model `agentRuntime.id: codex` | Agent runtime | Force the native Codex app-server harness for matching embedded turns. |
|
||||
| `/codex ...` | Chat command set | Bind/control Codex app-server threads from a conversation. |
|
||||
| `runtime: "acp", agentId: "codex"` | ACP session route | Explicit fallback path that runs Codex through ACP/acpx. |
|
||||
| Name you see | Layer | Meaning |
|
||||
| --------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `openai` | Provider prefix | Canonical OpenAI model route; agent turns use the Codex runtime. |
|
||||
| `openai-codex` | Legacy auth/profile prefix | Older OpenAI Codex OAuth/subscription profile namespace. Existing profiles and `auth.order.openai-codex` still work. |
|
||||
| `codex` plugin | Plugin | Bundled OpenClaw plugin that provides native Codex app-server runtime and `/codex` chat controls. |
|
||||
| provider/model `agentRuntime.id: codex` | Agent runtime | Force the native Codex app-server harness for matching embedded turns. |
|
||||
| `/codex ...` | Chat command set | Bind/control Codex app-server threads from a conversation. |
|
||||
| `runtime: "acp", agentId: "codex"` | ACP session route | Explicit fallback path that runs Codex through ACP/acpx. |
|
||||
|
||||
This means a config can intentionally contain both `openai/*` model refs and
|
||||
`openai-codex` auth profiles. `openclaw doctor --fix` rewrites legacy
|
||||
`openai-codex/*` model refs to the canonical OpenAI model route.
|
||||
This means a config can intentionally contain `openai/*` model refs while auth
|
||||
profiles still point at Codex-compatible credentials. Prefer `auth.order.openai`
|
||||
for new config; existing `openai-codex:*` profiles and `auth.order.openai-codex`
|
||||
remain supported. `openclaw doctor --fix` rewrites legacy `openai-codex/*` model
|
||||
refs to the canonical OpenAI model route.
|
||||
|
||||
<Note>
|
||||
GPT-5.5 is available through both direct OpenAI Platform API-key access and
|
||||
@@ -152,15 +154,16 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
| Model ref | Runtime config | Route | Auth |
|
||||
| ---------------------- | -------------------------- | --------------------------- | ---------------- |
|
||||
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | `openai-codex` profile |
|
||||
| `openai/gpt-5.4-mini` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | `openai-codex` profile |
|
||||
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile |
|
||||
| `openai/gpt-5.4-mini` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile |
|
||||
| `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime | `openai` profile or selected `openai-codex` profile |
|
||||
|
||||
<Note>
|
||||
`openai/*` agent models use the Codex app-server harness. To use API-key
|
||||
auth for an agent model, create an `openai-codex` API-key profile and order
|
||||
it with `auth.order.openai-codex`; `OPENAI_API_KEY` remains the direct
|
||||
fallback for non-agent OpenAI API surfaces.
|
||||
auth for an agent model, create a Codex-compatible API-key profile and order
|
||||
it with `auth.order.openai`; `OPENAI_API_KEY` remains the direct fallback for
|
||||
non-agent OpenAI API surfaces. Older `auth.order.openai-codex` entries still
|
||||
work.
|
||||
</Note>
|
||||
|
||||
### Config example
|
||||
@@ -239,7 +242,7 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
| Model ref | Runtime config | Route | Auth |
|
||||
|-----------|----------------|-------|------|
|
||||
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or selected `openai-codex` profile |
|
||||
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or ordered `openai` auth profile |
|
||||
| `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile |
|
||||
| `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Existing `openai-codex` profile |
|
||||
|
||||
@@ -251,10 +254,11 @@ Choose your preferred auth method and follow the setup steps.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Keep using the `openai-codex` provider id for auth/profile commands. The
|
||||
`openai-codex/*` model prefix is legacy config repaired by doctor. For the
|
||||
common subscription plus native runtime setup, sign in with `openai-codex`
|
||||
but keep the model ref as `openai/gpt-5.5`.
|
||||
The `openai-codex/*` model prefix is legacy config repaired by doctor. For
|
||||
the common subscription plus native runtime setup, sign in with Codex auth
|
||||
but keep the model ref as `openai/gpt-5.5`. New config should put OpenAI
|
||||
agent auth order under `auth.order.openai`; older `auth.order.openai-codex`
|
||||
entries remain valid.
|
||||
</Note>
|
||||
|
||||
### Config example
|
||||
@@ -270,6 +274,29 @@ Choose your preferred auth method and follow the setup steps.
|
||||
}
|
||||
```
|
||||
|
||||
With an API-key backup, keep the model on `openai/gpt-5.5` and put the
|
||||
auth order under `openai`. OpenClaw will try the subscription first, then
|
||||
the API key, while staying on the Codex harness:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: { entries: { codex: { enabled: true } } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.5" },
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
order: {
|
||||
openai: [
|
||||
"openai-codex:user@example.com",
|
||||
"openai:api-key-backup",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Onboarding no longer imports OAuth material from `~/.codex`. Sign in with browser OAuth (default) or the device-code flow above — OpenClaw manages the resulting credentials in its own agent auth store.
|
||||
</Note>
|
||||
@@ -309,8 +336,9 @@ Choose your preferred auth method and follow the setup steps.
|
||||
openclaw models status --probe --probe-provider openai-codex
|
||||
```
|
||||
|
||||
`openai-codex` remains the auth/profile provider id. `openai/*` is the
|
||||
model route for OpenAI agent turns through Codex.
|
||||
`openai/*` is the model route for OpenAI agent turns through Codex. The
|
||||
`openai-codex` auth/profile provider id remains accepted for existing
|
||||
profiles and CLI listing.
|
||||
|
||||
### Status indicator
|
||||
|
||||
@@ -366,11 +394,12 @@ Choose your preferred auth method and follow the setup steps.
|
||||
## Native Codex app-server auth
|
||||
|
||||
The native Codex app-server harness uses `openai/*` model refs plus omitted
|
||||
runtime config or provider/model `agentRuntime.id: "codex"`, but its auth is still
|
||||
account-based. OpenClaw
|
||||
selects auth in this order:
|
||||
runtime config or provider/model `agentRuntime.id: "codex"`, but its auth is
|
||||
still account-based. OpenClaw selects auth in this order:
|
||||
|
||||
1. An explicit OpenClaw `openai-codex` auth profile bound to the agent.
|
||||
1. Ordered OpenAI auth profiles for the agent, preferably under
|
||||
`auth.order.openai`. Existing `openai-codex:*` profiles and
|
||||
`auth.order.openai-codex` remain valid for older installs.
|
||||
2. The app-server's existing account, such as a local Codex CLI ChatGPT sign-in.
|
||||
3. For local stdio app-server launches only, `CODEX_API_KEY`, then
|
||||
`OPENAI_API_KEY`, when the app-server reports no account and still requires
|
||||
@@ -382,7 +411,11 @@ or embeddings. Env API-key fallback is only the local stdio no-account path; it
|
||||
is not sent to WebSocket app-server connections. When a subscription-style Codex
|
||||
profile is selected, OpenClaw also keeps `CODEX_API_KEY` and `OPENAI_API_KEY`
|
||||
out of the spawned stdio app-server child and sends the selected credentials
|
||||
through the app-server login RPC.
|
||||
through the app-server login RPC. When that subscription profile is blocked by a
|
||||
Codex usage limit, OpenClaw can rotate to the next ordered `openai:*` API-key
|
||||
profile without changing the selected model or dropping out of the Codex
|
||||
harness. Once the subscription reset time passes, the subscription profile is
|
||||
eligible again.
|
||||
|
||||
## Image generation
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ only when Package Acceptance should intentionally prove a different package.
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Target resolution | **Job:** `Resolve target ref`<br />**Child workflow:** none<br />**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.<br />**Rerun:** rerun the umbrella if this fails. |
|
||||
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, channel contracts, Node 22 compatibility, `check`, `check-additional`, build smoke, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
|
||||
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, and plugin prerelease Docker lanes.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
|
||||
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
|
||||
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. With `run_release_soak=true` or `release_profile=full`, also runs exhaustive live/E2E suites and Docker release-path chunks.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
|
||||
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
|
||||
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
|
||||
|
||||
@@ -200,7 +200,7 @@ The store is safe to edit, but the Gateway is the authority: it may rewrite or r
|
||||
|
||||
## Transcript structure (`*.jsonl`)
|
||||
|
||||
Transcripts are managed by `@mariozechner/pi-coding-agent`'s `SessionManager`.
|
||||
Transcripts are managed by `@earendil-works/pi-coding-agent`'s `SessionManager`.
|
||||
|
||||
The file is JSONL:
|
||||
|
||||
@@ -376,6 +376,7 @@ You can observe compaction and session state via:
|
||||
- `/status` (in any chat session)
|
||||
- `openclaw status` (CLI)
|
||||
- `openclaw sessions` / `sessions --json`
|
||||
- Gateway logs (`pnpm gateway:watch` or `openclaw logs --follow`): `embedded run auto-compaction start` + `complete`
|
||||
- Verbose mode: `🧹 Auto-compaction complete` + compaction count
|
||||
|
||||
---
|
||||
|
||||
+22
-14
@@ -120,13 +120,24 @@ Example:
|
||||
```json5
|
||||
{
|
||||
logging: { level: "info" },
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
workspace: "~/.openclaw/workspace",
|
||||
thinkingDefault: "high",
|
||||
timeoutSeconds: 1800,
|
||||
// Start with 0; enable later.
|
||||
heartbeat: { every: "0m" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-opus-4-6" },
|
||||
workspace: "~/.openclaw/workspace",
|
||||
thinkingDefault: "high",
|
||||
timeoutSeconds: 1800,
|
||||
// Start with 0; enable later.
|
||||
heartbeat: { every: "0m" },
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
default: true,
|
||||
groupChat: {
|
||||
mentionPatterns: ["@openclaw", "openclaw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
@@ -136,11 +147,6 @@ Example:
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
groupChat: {
|
||||
mentionPatterns: ["@openclaw", "openclaw"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
scope: "per-sender",
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
@@ -174,8 +180,10 @@ Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
heartbeat: { every: "30m" },
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: { every: "30m" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,7 +104,13 @@ browser binaries as shown below.
|
||||
#### Docker Playwright install
|
||||
|
||||
If your Gateway runs in Docker, avoid `npx playwright` (npm override conflicts).
|
||||
Use the bundled CLI instead:
|
||||
For custom images, bake Chromium into the image:
|
||||
|
||||
```bash
|
||||
OPENCLAW_INSTALL_BROWSER=1 ./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
For an existing image, install through the bundled CLI instead:
|
||||
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli \
|
||||
|
||||
@@ -62,13 +62,13 @@ If the agent is sandboxed, the browser tool defaults to the sandbox. To allow ho
|
||||
}
|
||||
```
|
||||
|
||||
Then target the host browser:
|
||||
Then open the host browser yourself (CLI invocations always run against the host browser):
|
||||
|
||||
```bash
|
||||
openclaw browser open https://x.com --browser-profile openclaw --target host
|
||||
openclaw browser open https://x.com --browser-profile openclaw
|
||||
```
|
||||
|
||||
Or disable sandboxing for the agent that posts updates.
|
||||
The agent's `browser` tool calls can then target the host once `sandbox.browser.allowHostControl: true` is set. Alternatively, disable sandboxing for the agent that posts updates.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
+11
-4
@@ -23,7 +23,7 @@ When you send:
|
||||
OpenClaw:
|
||||
|
||||
1. snapshots the current session context,
|
||||
2. runs a separate **tool-less** model call,
|
||||
2. runs a separate ephemeral side query,
|
||||
3. answers only the side question,
|
||||
4. leaves the main run alone,
|
||||
5. does **not** write the BTW question or answer to session history,
|
||||
@@ -33,17 +33,24 @@ The important mental model is:
|
||||
|
||||
- same session context
|
||||
- separate one-shot side query
|
||||
- no tool calls
|
||||
- same native harness transport when the session uses a native harness
|
||||
- no future context pollution
|
||||
- no transcript persistence
|
||||
|
||||
For Codex harness sessions, BTW stays inside Codex by forking the active
|
||||
app-server thread as an ephemeral side thread. That keeps Codex OAuth and native
|
||||
thread behavior intact while still isolating the side answer from the parent
|
||||
transcript. Like Codex `/side`, the side thread keeps the current Codex
|
||||
permissions and native tool surface, with guardrails that tell the model not to
|
||||
treat inherited parent-thread work as active instructions. Non-Codex runtimes
|
||||
keep the older direct one-shot path.
|
||||
|
||||
## What it does not do
|
||||
|
||||
`/btw` does **not**:
|
||||
|
||||
- create a new durable session,
|
||||
- continue the unfinished main task,
|
||||
- run tools or agent tool loops,
|
||||
- write BTW question/answer data to transcript history,
|
||||
- appear in `chat.history`,
|
||||
- survive a reload.
|
||||
@@ -60,7 +67,7 @@ explicitly telling the model:
|
||||
|
||||
- answer only the side question,
|
||||
- do not resume or complete the unfinished main task,
|
||||
- do not emit tool calls or pseudo-tool calls.
|
||||
- do not steer the parent conversation.
|
||||
|
||||
That keeps BTW isolated from the main run while still making it aware of what
|
||||
the session is about.
|
||||
|
||||
@@ -90,7 +90,7 @@ backend emits it.
|
||||
| ---------- | --------------------------------------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| ComfyUI | `workflow` | Yes (1 image, workflow-configured) | `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` for cloud |
|
||||
| DeepInfra | `black-forest-labs/FLUX-1-schnell` | Yes (1 image) | `DEEPINFRA_API_KEY` |
|
||||
| fal | `fal-ai/flux/dev` | Yes | `FAL_KEY` |
|
||||
| fal | `fal-ai/flux/dev` | Yes (model-specific limits) | `FAL_KEY` |
|
||||
| Google | `gemini-3.1-flash-image-preview` | Yes | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |
|
||||
| LiteLLM | `gpt-image-2` | Yes (up to 5 input images) | `LITELLM_API_KEY` |
|
||||
| MiniMax | `image-01` | Yes (subject reference) | `MINIMAX_API_KEY` or MiniMax OAuth (`minimax-portal`) |
|
||||
@@ -107,13 +107,13 @@ Use `action: "list"` to inspect available providers and models at runtime:
|
||||
|
||||
## Provider capabilities
|
||||
|
||||
| Capability | ComfyUI | DeepInfra | fal | Google | MiniMax | OpenAI | Vydra | xAI |
|
||||
| --------------------- | ------------------ | --------- | ----------------- | -------------- | --------------------- | -------------- | ----- | -------------- |
|
||||
| Generate (max count) | Workflow-defined | 4 | 4 | 4 | 9 | 4 | 1 | 4 |
|
||||
| Edit / reference | 1 image (workflow) | 1 image | 1 image | Up to 5 images | 1 image (subject ref) | Up to 5 images | - | Up to 5 images |
|
||||
| Size control | - | ✓ | ✓ | ✓ | - | Up to 4K | - | - |
|
||||
| Aspect ratio | - | - | ✓ (generate only) | ✓ | ✓ | - | - | ✓ |
|
||||
| Resolution (1K/2K/4K) | - | - | ✓ | ✓ | - | - | - | 1K, 2K |
|
||||
| Capability | ComfyUI | DeepInfra | fal | Google | MiniMax | OpenAI | Vydra | xAI |
|
||||
| --------------------- | ------------------ | --------- | ------------------------- | -------------- | --------------------- | -------------- | ----- | -------------- |
|
||||
| Generate (max count) | Workflow-defined | 4 | 4 | 4 | 9 | 4 | 1 | 4 |
|
||||
| Edit / reference | 1 image (workflow) | 1 image | Flux: 1; GPT: 10; NB2: 14 | Up to 5 images | 1 image (subject ref) | Up to 5 images | - | Up to 5 images |
|
||||
| Size control | - | ✓ | ✓ | ✓ | - | Up to 4K | - | - |
|
||||
| Aspect ratio | - | - | ✓ | ✓ | ✓ | - | - | ✓ |
|
||||
| Resolution (1K/2K/4K) | - | - | ✓ | ✓ | - | - | - | 1K, 2K |
|
||||
|
||||
## Tool parameters
|
||||
|
||||
@@ -241,7 +241,9 @@ reference images. Pass a reference image path or URL:
|
||||
```
|
||||
|
||||
OpenAI, OpenRouter, Google, and xAI support up to 5 reference images via the
|
||||
`images` parameter. fal, MiniMax, and ComfyUI support 1.
|
||||
`images` parameter. fal supports 1 reference image for Flux image-to-image, up
|
||||
to 10 for GPT Image 2 edits, and up to 14 for Nano Banana 2 edits. MiniMax and
|
||||
ComfyUI support 1.
|
||||
|
||||
## Provider deep dives
|
||||
|
||||
|
||||
@@ -89,10 +89,10 @@ can satisfy the MiniMax Search bearer credential.
|
||||
|
||||
## Supported parameters
|
||||
|
||||
MiniMax Search supports:
|
||||
|
||||
- `query`
|
||||
- `count` (OpenClaw trims the returned result list to the requested count)
|
||||
| Parameter | Type | Constraints | Description |
|
||||
| --------- | ------- | ----------- | --------------------------------------------------------------------------- |
|
||||
| `query` | string | required | Search query string. |
|
||||
| `count` | integer | 1-10 | Number of results to return. OpenClaw trims the returned list to this size. |
|
||||
|
||||
Provider-specific filters are not currently supported.
|
||||
|
||||
|
||||
@@ -50,8 +50,14 @@ Auth is scoped by agent: each agent has its own `agentDir` auth store at `~/.ope
|
||||
"scope": "agent"
|
||||
},
|
||||
"tools": {
|
||||
"allow": ["read"],
|
||||
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"]
|
||||
"allow": ["read", "message"],
|
||||
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"],
|
||||
"message": {
|
||||
"crossContext": {
|
||||
"allowWithinProvider": false,
|
||||
"allowAcrossProviders": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -75,7 +81,7 @@ Auth is scoped by agent: each agent has its own `agentDir` auth store at `~/.ope
|
||||
**Result:**
|
||||
|
||||
- `main` agent: runs on host, full tool access.
|
||||
- `family` agent: runs in Docker (one container per agent), only `read` tool.
|
||||
- `family` agent: runs in Docker (one container per agent), only `read` and current-conversation message sends.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Example 2: Work agent with shared sandbox">
|
||||
|
||||
@@ -463,7 +463,9 @@ Examples:
|
||||
Unlike normal chat:
|
||||
|
||||
- it uses the current session as background context,
|
||||
- it runs as a separate **tool-less** one-shot call,
|
||||
- in Codex harness sessions, it runs as an ephemeral Codex side thread with the
|
||||
current Codex permissions and native tool surface,
|
||||
- in non-Codex sessions, it keeps the older direct one-shot side-call behavior,
|
||||
- it does not change future session context,
|
||||
- it is not written to transcript history,
|
||||
- it is delivered as a live side result instead of a normal assistant message.
|
||||
|
||||
@@ -7,9 +7,14 @@ read_when:
|
||||
- You are implementing or debugging tool discovery for PI runs
|
||||
---
|
||||
|
||||
Tool Search gives PI agents one compact way to discover and call large tool
|
||||
catalogs. It is useful when the run has many available tools but the model is
|
||||
likely to need only a few of them.
|
||||
Tool Search is an experimental OpenClaw PI-agent feature. It gives PI agents one
|
||||
compact way to discover and call large tool catalogs. It is useful when the run
|
||||
has many available tools but the model is likely to need only a few of them.
|
||||
|
||||
This page documents OpenClaw PI Tool Search. It is not the Codex-native tool
|
||||
search or dynamic-tools surface. Codex-native code mode, tool search, deferred
|
||||
dynamic tools, and nested tool calls are stable Codex harness surfaces and do
|
||||
not depend on `tools.toolSearch`.
|
||||
|
||||
When enabled for PI, the model receives one `tool_search_code` tool by default.
|
||||
That tool runs a short JavaScript body in an isolated Node subprocess with an
|
||||
@@ -29,9 +34,10 @@ client-provided tools. The model does not see every full schema up front.
|
||||
Instead, it searches compact descriptors, describes one selected tool when it
|
||||
needs the exact schema, and calls that tool through OpenClaw.
|
||||
|
||||
Codex harness runs do not receive these OpenClaw Tool Search controls. OpenClaw
|
||||
passes product capabilities to Codex as dynamic tools, and Codex owns native
|
||||
code mode, native tool search, deferred dynamic tools, and nested tool calls.
|
||||
Codex harness runs do not receive these experimental OpenClaw Tool Search
|
||||
controls. OpenClaw passes product capabilities to Codex as dynamic tools, and
|
||||
Codex owns the stable native code mode, native tool search, deferred dynamic
|
||||
tools, and nested tool calls.
|
||||
|
||||
## How a turn runs
|
||||
|
||||
@@ -64,6 +70,9 @@ shape the model sees. If the current runtime cannot launch the isolated Node
|
||||
code-mode child process, the default `code` mode falls back to `tools` before
|
||||
catalog compaction.
|
||||
|
||||
Both modes are experimental. Prefer direct tool exposure for small PI tool
|
||||
catalogs, and prefer the Codex-native stable surfaces for Codex harness runs.
|
||||
|
||||
There is no separate source-selection config. When Tool Search is enabled, the
|
||||
catalog includes eligible OpenClaw, MCP, and client tools after normal policy
|
||||
filtering.
|
||||
|
||||
@@ -76,6 +76,17 @@ function expectWrapperToContainPathSuffix(wrapper: string, pathSuffix: string[])
|
||||
}
|
||||
}
|
||||
|
||||
async function expectPathMissing(targetPath: string): Promise<void> {
|
||||
let error: unknown;
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
restoreEnv("CODEX_HOME");
|
||||
@@ -121,9 +132,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain(JSON.stringify(installedBinPath));
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
await expect(
|
||||
fs.access(path.join(agentDir, "acp-auth", "codex", "auth.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
|
||||
});
|
||||
|
||||
it("keeps generated wrappers usable when chmod is rejected by the state filesystem", async () => {
|
||||
@@ -373,12 +382,8 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("CODEX_HOME: codexHome");
|
||||
expect(wrapper).not.toContain(sourceCodexHome);
|
||||
await expect(
|
||||
fs.access(path.join(agentDir, "acp-auth", "codex-source", "auth.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(
|
||||
fs.access(path.join(agentDir, "acp-auth", "codex", "auth.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex-source", "auth.json"));
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
|
||||
});
|
||||
|
||||
it("copies only trusted Codex project declarations into the isolated Codex home", async () => {
|
||||
|
||||
@@ -64,6 +64,17 @@ function rewriteLine(line, mcpServers) {
|
||||
}
|
||||
}
|
||||
|
||||
export function createTargetSpawnOptions(platform = process.platform) {
|
||||
const options = {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
};
|
||||
if (platform === "win32") {
|
||||
options.windowsHide = true;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function isMainModule() {
|
||||
const mainPath = process.argv[1];
|
||||
if (!mainPath) {
|
||||
@@ -75,10 +86,7 @@ function isMainModule() {
|
||||
function main() {
|
||||
const { targetCommand, mcpServers } = decodePayload(process.argv.slice(2));
|
||||
const target = splitCommandLine(targetCommand);
|
||||
const child = spawn(target.command, target.args, {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
});
|
||||
const child = spawn(target.command, target.args, createTargetSpawnOptions());
|
||||
|
||||
if (!child.stdin || !child.stdout) {
|
||||
throw new Error("Failed to create MCP proxy stdio pipes");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
@@ -28,6 +29,21 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("mcp-proxy", () => {
|
||||
it("hides the target MCP process window on Windows only", async () => {
|
||||
const moduleUrl = pathToFileURL(proxyPath).href;
|
||||
const { createTargetSpawnOptions } = (await import(moduleUrl)) as {
|
||||
createTargetSpawnOptions: (platform?: NodeJS.Platform) => Record<string, unknown>;
|
||||
};
|
||||
|
||||
expect(createTargetSpawnOptions("win32")).toEqual({
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
windowsHide: true,
|
||||
});
|
||||
expect(createTargetSpawnOptions("darwin")).not.toHaveProperty("windowsHide");
|
||||
expect(createTargetSpawnOptions("linux")).not.toHaveProperty("windowsHide");
|
||||
});
|
||||
|
||||
it("injects configured MCP servers into ACP session bootstrap requests", async () => {
|
||||
const echoServerPath = await makeTempScript(
|
||||
"echo-server.cjs",
|
||||
|
||||
@@ -667,20 +667,17 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
expect(leaseStore.store.save).toHaveBeenCalledTimes(2);
|
||||
const leases = Array.from(leaseStore.leases.values());
|
||||
expect(leases).toHaveLength(1);
|
||||
expect(leases[0]).toMatchObject({
|
||||
gatewayInstanceId: "gateway-test",
|
||||
sessionKey: "agent:codex:acp:binding:test",
|
||||
rootPid: 777,
|
||||
state: "open",
|
||||
wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs",
|
||||
});
|
||||
const lease = leases[0];
|
||||
expect(lease?.gatewayInstanceId).toBe("gateway-test");
|
||||
expect(lease?.sessionKey).toBe("agent:codex:acp:binding:test");
|
||||
expect(lease?.rootPid).toBe(777);
|
||||
expect(lease?.state).toBe("open");
|
||||
expect(lease?.wrapperPath).toBe("/tmp/openclaw/acpx/codex-acp-wrapper.mjs");
|
||||
expect(launchCommands[0]).toContain("OPENCLAW_ACPX_LEASE_ID=");
|
||||
expect(launchCommands[0]).toContain("OPENCLAW_GATEWAY_INSTANCE_ID=gateway-test");
|
||||
expect(savedRecords[0]?.agentCommand).toBe(CODEX_ACP_WRAPPER_COMMAND);
|
||||
expect(savedRecords[0]).toMatchObject({
|
||||
openclawGatewayInstanceId: "gateway-test",
|
||||
openclawLeaseId: leases[0]?.leaseId,
|
||||
});
|
||||
expect(savedRecords[0]?.openclawGatewayInstanceId).toBe("gateway-test");
|
||||
expect(savedRecords[0]?.openclawLeaseId).toBe(lease?.leaseId);
|
||||
});
|
||||
|
||||
it("keeps reusable persistent ACP launch commands stable across ensures", async () => {
|
||||
@@ -757,10 +754,9 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
openclawWrapperRoot: "/tmp/openclaw/acpx",
|
||||
});
|
||||
|
||||
await expect(wrappedStore.load("agent:codex:acp:binding:test")).resolves.toMatchObject({
|
||||
openclawGatewayInstanceId: "gateway-test",
|
||||
openclawLeaseId: "lease-loaded",
|
||||
});
|
||||
const loadedRecord = await wrappedStore.load("agent:codex:acp:binding:test");
|
||||
expect(loadedRecord?.openclawGatewayInstanceId).toBe("gateway-test");
|
||||
expect(loadedRecord?.openclawLeaseId).toBe("lease-loaded");
|
||||
});
|
||||
|
||||
it("merges the lease for the current ACPX session process when old leases exist", async () => {
|
||||
@@ -801,10 +797,9 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
openclawWrapperRoot: "/tmp/openclaw/acpx",
|
||||
});
|
||||
|
||||
await expect(wrappedStore.load("agent:codex:acp:binding:test")).resolves.toMatchObject({
|
||||
openclawGatewayInstanceId: "gateway-test",
|
||||
openclawLeaseId: "lease-current",
|
||||
});
|
||||
const loadedRecord = await wrappedStore.load("agent:codex:acp:binding:test");
|
||||
expect(loadedRecord?.openclawGatewayInstanceId).toBe("gateway-test");
|
||||
expect(loadedRecord?.openclawLeaseId).toBe("lease-current");
|
||||
});
|
||||
|
||||
it("uses matching leases before legacy pid cleanup on close", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createMantleAnthropicStreamFn,
|
||||
@@ -30,6 +30,13 @@ function createTestDeps() {
|
||||
};
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`Expected ${label} to be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("createMantleAnthropicStreamFn", () => {
|
||||
it("uses authToken bearer auth for Mantle Anthropic requests", () => {
|
||||
const stream = { kind: "anthropic-stream" };
|
||||
@@ -46,31 +53,24 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
});
|
||||
|
||||
expect(result).toBe(stream);
|
||||
expect(deps.createClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
apiKey: null,
|
||||
authToken: "bedrock-bearer-token",
|
||||
baseURL: "https://bedrock-mantle.us-east-1.api.aws/anthropic",
|
||||
defaultHeaders: expect.objectContaining({
|
||||
accept: "application/json",
|
||||
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
|
||||
"X-Test": "model-header",
|
||||
"X-Caller": "caller-header",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(deps.stream).toHaveBeenCalledWith(
|
||||
model,
|
||||
context,
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
authToken: "bedrock-bearer-token",
|
||||
}),
|
||||
}),
|
||||
thinkingEnabled: false,
|
||||
}),
|
||||
const clientOptions = requireRecord(deps.createClient.mock.calls[0]?.[0], "client options");
|
||||
expect(clientOptions.apiKey).toBeNull();
|
||||
expect(clientOptions.authToken).toBe("bedrock-bearer-token");
|
||||
expect(clientOptions.baseURL).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic");
|
||||
const defaultHeaders = requireRecord(clientOptions.defaultHeaders, "default headers");
|
||||
expect(defaultHeaders.accept).toBe("application/json");
|
||||
expect(defaultHeaders["anthropic-beta"]).toBe("fine-grained-tool-streaming-2025-05-14");
|
||||
expect(defaultHeaders["X-Test"]).toBe("model-header");
|
||||
expect(defaultHeaders["X-Caller"]).toBe("caller-header");
|
||||
|
||||
expect(deps.stream.mock.calls[0]?.[0]).toBe(model);
|
||||
expect(deps.stream.mock.calls[0]?.[1]).toBe(context);
|
||||
const streamOptions = requireRecord(deps.stream.mock.calls[0]?.[2], "stream options");
|
||||
const client = requireRecord(streamOptions.client, "stream client");
|
||||
expect(requireRecord(client.options, "stream client options").authToken).toBe(
|
||||
"bedrock-bearer-token",
|
||||
);
|
||||
expect(streamOptions.thinkingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("omits unsupported Opus 4.7 sampling and reasoning overrides", () => {
|
||||
@@ -85,14 +85,11 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
reasoning: "high",
|
||||
});
|
||||
|
||||
expect(deps.stream).toHaveBeenCalledWith(
|
||||
model,
|
||||
context,
|
||||
expect.objectContaining({
|
||||
temperature: undefined,
|
||||
thinkingEnabled: false,
|
||||
}),
|
||||
);
|
||||
expect(deps.stream.mock.calls[0]?.[0]).toBe(model);
|
||||
expect(deps.stream.mock.calls[0]?.[1]).toBe(context);
|
||||
const streamOptions = requireRecord(deps.stream.mock.calls[0]?.[2], "stream options");
|
||||
expect(streamOptions.temperature).toBeUndefined();
|
||||
expect(streamOptions.thinkingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes Mantle provider URLs to the Anthropic endpoint", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
||||
import { streamAnthropic } from "@mariozechner/pi-ai/anthropic";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import type { Api, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
||||
import { streamAnthropic } from "@earendil-works/pi-ai/anthropic";
|
||||
|
||||
const MANTLE_ANTHROPIC_BETA = "fine-grained-tool-streaming-2025-05-14";
|
||||
type AnthropicOptions = ConstructorParameters<typeof Anthropic>[0];
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.95.1",
|
||||
"@aws/bedrock-token-generator": "^1.1.0",
|
||||
"@mariozechner/pi-ai": "0.73.1"
|
||||
"@earendil-works/pi-ai": "0.74.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@aws-sdk/client-bedrock": "3.1045.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1045.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.39",
|
||||
"@mariozechner/pi-ai": "0.73.1",
|
||||
"@earendil-works/pi-ai": "0.74.0",
|
||||
"@smithy/shared-ini-file-loader": "4.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import { streamSimple } from "@earendil-works/pi-ai";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
@@ -122,7 +122,7 @@ function createGuardrailWrapStreamFn(
|
||||
|
||||
/**
|
||||
* Mirrors the shipped pi-ai Bedrock `supportsPromptCaching` matcher.
|
||||
* Keep this in sync with node_modules/@mariozechner/pi-ai/dist/providers/amazon-bedrock.js.
|
||||
* Keep this in sync with node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.
|
||||
*/
|
||||
function matchesPiAiPromptCachingModelId(modelId: string): boolean {
|
||||
const id = modelId.toLowerCase();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAssistantMessageEventStream, type Model } from "@mariozechner/pi-ai";
|
||||
import { createAssistantMessageEventStream, type Model } from "@earendil-works/pi-ai";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -69,18 +69,17 @@ describe("anthropic-vertex provider plugin", () => {
|
||||
}),
|
||||
} as never);
|
||||
|
||||
expect(result).toEqual({
|
||||
provider: {
|
||||
api: "anthropic-messages",
|
||||
apiKey: "gcp-vertex-credentials",
|
||||
baseUrl: "https://europe-west4-aiplatform.googleapis.com",
|
||||
headers: { "x-test-header": "1" },
|
||||
models: [
|
||||
expect.objectContaining({ id: "claude-opus-4-6" }),
|
||||
expect.objectContaining({ id: "claude-sonnet-4-6" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!result || !("provider" in result)) {
|
||||
throw new Error("expected single provider catalog result");
|
||||
}
|
||||
expect(result.provider.api).toBe("anthropic-messages");
|
||||
expect(result.provider.apiKey).toBe("gcp-vertex-credentials");
|
||||
expect(result.provider.baseUrl).toBe("https://europe-west4-aiplatform.googleapis.com");
|
||||
expect(result.provider.headers).toEqual({ "x-test-header": "1" });
|
||||
expect(result.provider.models.map((model) => model.id)).toEqual([
|
||||
"claude-opus-4-6",
|
||||
"claude-sonnet-4-6",
|
||||
]);
|
||||
});
|
||||
|
||||
it("owns Anthropic-style replay policy", async () => {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
||||
"@mariozechner/pi-agent-core": "0.73.1",
|
||||
"@mariozechner/pi-ai": "0.73.1"
|
||||
"@earendil-works/pi-agent-core": "0.74.0",
|
||||
"@earendil-works/pi-ai": "0.74.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAssistantMessageEventStream, type Model } from "@mariozechner/pi-ai";
|
||||
import { createAssistantMessageEventStream, type Model } from "@earendil-works/pi-ai";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
@@ -141,13 +141,10 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
|
||||
void streamFn(model, { messages: [] }, {});
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledWith(
|
||||
model,
|
||||
{ messages: [] },
|
||||
expect.objectContaining({
|
||||
maxTokens: 128000,
|
||||
}),
|
||||
);
|
||||
const transportOptions = streamAnthropicMock.mock.calls[0]?.[2] as
|
||||
| { maxTokens?: number }
|
||||
| undefined;
|
||||
expect(transportOptions?.maxTokens).toBe(128000);
|
||||
});
|
||||
|
||||
it("clamps explicit maxTokens to the selected model limit", () => {
|
||||
@@ -157,13 +154,10 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
|
||||
void streamFn(model, { messages: [] }, { maxTokens: 999999 });
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledWith(
|
||||
model,
|
||||
{ messages: [] },
|
||||
expect.objectContaining({
|
||||
maxTokens: 128000,
|
||||
}),
|
||||
);
|
||||
const transportOptions = streamAnthropicMock.mock.calls[0]?.[2] as
|
||||
| { maxTokens?: number }
|
||||
| undefined;
|
||||
expect(transportOptions?.maxTokens).toBe(128000);
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to max effort for adaptive Opus models", () => {
|
||||
@@ -173,14 +167,11 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledWith(
|
||||
model,
|
||||
{ messages: [] },
|
||||
expect.objectContaining({
|
||||
thinkingEnabled: true,
|
||||
effort: "max",
|
||||
}),
|
||||
);
|
||||
const transportOptions = streamAnthropicMock.mock.calls[0]?.[2] as
|
||||
| { effort?: string; thinkingEnabled?: boolean }
|
||||
| undefined;
|
||||
expect(transportOptions?.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions?.effort).toBe("max");
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to xhigh effort for Opus 4.7", () => {
|
||||
@@ -190,14 +181,11 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledWith(
|
||||
model,
|
||||
{ messages: [] },
|
||||
expect.objectContaining({
|
||||
thinkingEnabled: true,
|
||||
effort: "xhigh",
|
||||
}),
|
||||
);
|
||||
const transportOptions = streamAnthropicMock.mock.calls[0]?.[2] as
|
||||
| { effort?: string; thinkingEnabled?: boolean }
|
||||
| undefined;
|
||||
expect(transportOptions?.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions?.effort).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("applies Anthropic cache-boundary shaping before forwarding payload hooks", async () => {
|
||||
@@ -266,13 +254,12 @@ describe("createAnthropicVertexStreamFn", () => {
|
||||
|
||||
void streamFn(model, { messages: [] }, { maxTokens: Number.NaN });
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledWith(
|
||||
model,
|
||||
{ messages: [] },
|
||||
expect.not.objectContaining({
|
||||
maxTokens: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(streamAnthropicMock).toHaveBeenCalledTimes(1);
|
||||
const [calledModel, payload, transportOptions] = streamAnthropicMock.mock.calls[0] ?? [];
|
||||
expect(calledModel).toBe(model);
|
||||
expect(payload).toEqual({ messages: [] });
|
||||
expect(transportOptions).toBeTypeOf("object");
|
||||
expect(Object.hasOwn(transportOptions as object, "maxTokens")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AnthropicVertex as AnthropicVertexSdk } from "@anthropic-ai/vertex-sdk";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import {
|
||||
streamAnthropic as streamAnthropicDefault,
|
||||
type AnthropicOptions,
|
||||
type Model,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@earendil-works/pi-ai";
|
||||
import {
|
||||
applyAnthropicPayloadPolicyToParams,
|
||||
resolveAnthropicPayloadPolicy,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.73.1"
|
||||
"@earendil-works/pi-ai": "0.74.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -33,34 +33,32 @@ function collectLegacyExtendedLevelIds(levels: readonly { id: string }[] | undef
|
||||
return ids;
|
||||
}
|
||||
|
||||
function levelIds(levels: readonly { id: string }[] | undefined): string[] {
|
||||
return (levels ?? []).map((level) => level.id);
|
||||
}
|
||||
|
||||
describe("anthropic provider policy public artifact", () => {
|
||||
it("normalizes Anthropic provider config", () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
provider: "anthropic",
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
const normalized = normalizeConfig({
|
||||
provider: "anthropic",
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
|
||||
},
|
||||
});
|
||||
expect(normalized.api).toBe("anthropic-messages");
|
||||
expect(normalized.baseUrl).toBe("https://api.anthropic.com");
|
||||
});
|
||||
|
||||
it("normalizes Claude CLI provider config", () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
provider: "claude-cli",
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
const normalized = normalizeConfig({
|
||||
provider: "claude-cli",
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
|
||||
},
|
||||
});
|
||||
expect(normalized.api).toBe("anthropic-messages");
|
||||
});
|
||||
|
||||
it("does not normalize non-Anthropic provider config", () => {
|
||||
@@ -96,22 +94,20 @@ describe("anthropic provider policy public artifact", () => {
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(nextConfig.agents?.defaults?.contextPruning).toMatchObject({
|
||||
mode: "cache-ttl",
|
||||
ttl: "1h",
|
||||
});
|
||||
expect(nextConfig.agents?.defaults?.contextPruning?.mode).toBe("cache-ttl");
|
||||
expect(nextConfig.agents?.defaults?.contextPruning?.ttl).toBe("1h");
|
||||
});
|
||||
|
||||
it("exposes Claude Opus 4.7 thinking levels without loading the full provider plugin", () => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-opus-4-7",
|
||||
}),
|
||||
).toMatchObject({
|
||||
levels: expect.arrayContaining([{ id: "xhigh" }, { id: "adaptive" }, { id: "max" }]),
|
||||
defaultLevel: "off",
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-opus-4-7",
|
||||
});
|
||||
const ids = levelIds(profile?.levels);
|
||||
expect(ids).toContain("xhigh");
|
||||
expect(ids).toContain("adaptive");
|
||||
expect(ids).toContain("max");
|
||||
expect(profile?.defaultLevel).toBe("off");
|
||||
});
|
||||
|
||||
it("keeps adaptive-only Claude profiles aligned with the runtime provider", () => {
|
||||
@@ -120,13 +116,11 @@ describe("anthropic provider policy public artifact", () => {
|
||||
modelId: "claude-opus-4-6",
|
||||
});
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
levels: expect.arrayContaining([{ id: "adaptive" }]),
|
||||
defaultLevel: "adaptive",
|
||||
});
|
||||
if (!profile) {
|
||||
throw new Error("Expected Anthropic policy profile");
|
||||
}
|
||||
expect(levelIds(profile.levels)).toContain("adaptive");
|
||||
expect(profile.defaultLevel).toBe("adaptive");
|
||||
expect(collectLegacyExtendedLevelIds(profile.levels)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__testing,
|
||||
@@ -110,7 +110,7 @@ describe("anthropic stream wrappers", () => {
|
||||
it("composes the anthropic provider stream chain from extra params", () => {
|
||||
const captured = runComposedAnthropicProviderStream("sk-ant-api-123");
|
||||
expect(captured.headers?.["anthropic-beta"]).toContain(CONTEXT_1M_BETA);
|
||||
expect(captured.payload).toMatchObject({ service_tier: "auto" });
|
||||
expect(captured.payload).toEqual({ service_tier: "auto" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import { streamSimple } from "@earendil-works/pi-ai";
|
||||
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
applyAnthropicPayloadPolicyToParams,
|
||||
|
||||
@@ -20,19 +20,21 @@ describe("arcee provider plugin", () => {
|
||||
providers: [provider],
|
||||
choice: "arceeai-api-key",
|
||||
});
|
||||
expect(directChoice).toMatchObject({
|
||||
provider: { id: "arcee" },
|
||||
method: { id: "arcee-platform" },
|
||||
});
|
||||
if (!directChoice) {
|
||||
throw new Error("expected direct Arcee auth choice");
|
||||
}
|
||||
expect(directChoice.provider.id).toBe("arcee");
|
||||
expect(directChoice.method.id).toBe("arcee-platform");
|
||||
|
||||
const orChoice = resolveProviderPluginChoice({
|
||||
providers: [provider],
|
||||
choice: "arceeai-openrouter",
|
||||
});
|
||||
expect(orChoice).toMatchObject({
|
||||
provider: { id: "arcee" },
|
||||
method: { id: "openrouter" },
|
||||
});
|
||||
if (!orChoice) {
|
||||
throw new Error("expected OpenRouter Arcee auth choice");
|
||||
}
|
||||
expect(orChoice.provider.id).toBe("arcee");
|
||||
expect(orChoice.method.id).toBe("openrouter");
|
||||
});
|
||||
|
||||
it("stores the OpenRouter onboarding path under the OpenRouter auth profile", async () => {
|
||||
@@ -58,14 +60,12 @@ describe("arcee provider plugin", () => {
|
||||
toApiKeyCredential: () => null,
|
||||
} as never);
|
||||
|
||||
expect(config?.auth?.profiles?.["openrouter:default"]).toMatchObject({
|
||||
provider: "openrouter",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(config?.models?.providers?.arcee).toMatchObject({
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
api: "openai-completions",
|
||||
});
|
||||
const openRouterProfile = config?.auth?.profiles?.["openrouter:default"];
|
||||
expect(openRouterProfile?.provider).toBe("openrouter");
|
||||
expect(openRouterProfile?.mode).toBe("api_key");
|
||||
const arceeConfig = config?.models?.providers?.arcee;
|
||||
expect(arceeConfig?.baseUrl).toBe("https://openrouter.ai/api/v1");
|
||||
expect(arceeConfig?.api).toBe("openai-completions");
|
||||
expect(config?.models?.providers?.arcee?.models?.map((model) => model.id)).toEqual([
|
||||
"arcee/trinity-mini",
|
||||
"arcee/trinity-large-preview",
|
||||
@@ -94,12 +94,11 @@ describe("arcee provider plugin", () => {
|
||||
"trinity-large-preview",
|
||||
"trinity-large-thinking",
|
||||
]);
|
||||
expect(
|
||||
catalogProvider.models?.find((model) => model.id === "trinity-large-thinking")?.compat,
|
||||
).toMatchObject({
|
||||
supportsTools: false,
|
||||
supportsReasoningEffort: false,
|
||||
});
|
||||
const thinkingCompat = catalogProvider.models?.find(
|
||||
(model) => model.id === "trinity-large-thinking",
|
||||
)?.compat;
|
||||
expect(thinkingCompat?.supportsTools).toBe(false);
|
||||
expect(thinkingCompat?.supportsReasoningEffort).toBe(false);
|
||||
});
|
||||
|
||||
it("builds the OpenRouter-backed Arcee AI model catalog", async () => {
|
||||
@@ -120,31 +119,27 @@ describe("arcee provider plugin", () => {
|
||||
"arcee/trinity-large-preview",
|
||||
"arcee/trinity-large-thinking",
|
||||
]);
|
||||
expect(
|
||||
catalogProvider.models?.find((model) => model.id === "arcee/trinity-large-thinking")?.compat,
|
||||
).toMatchObject({
|
||||
supportsTools: false,
|
||||
supportsReasoningEffort: false,
|
||||
});
|
||||
const thinkingCompat = catalogProvider.models?.find(
|
||||
(model) => model.id === "arcee/trinity-large-thinking",
|
||||
)?.compat;
|
||||
expect(thinkingCompat?.supportsTools).toBe(false);
|
||||
expect(thinkingCompat?.supportsReasoningEffort).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes Arcee OpenRouter models to vendor-prefixed runtime ids", async () => {
|
||||
const provider = await registerSingleProviderPlugin(arceePlugin);
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
modelId: "arcee/trinity-large-thinking",
|
||||
model: {
|
||||
provider: "arcee",
|
||||
id: "trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
id: "arcee/trinity-large-thinking",
|
||||
});
|
||||
const openRouterModel = provider.normalizeResolvedModel?.({
|
||||
modelId: "arcee/trinity-large-thinking",
|
||||
model: {
|
||||
provider: "arcee",
|
||||
id: "trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
},
|
||||
} as never);
|
||||
expect(openRouterModel?.id).toBe("arcee/trinity-large-thinking");
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
@@ -163,34 +158,28 @@ describe("arcee provider plugin", () => {
|
||||
it("canonicalizes stale OpenRouter /v1 config and transport metadata", async () => {
|
||||
const provider = await registerSingleProviderPlugin(arceePlugin);
|
||||
|
||||
expect(
|
||||
provider.normalizeConfig?.({
|
||||
provider: "arcee",
|
||||
providerConfig: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1/",
|
||||
models: [],
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
const normalizedConfig = provider.normalizeConfig?.({
|
||||
provider: "arcee",
|
||||
providerConfig: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1/",
|
||||
models: [],
|
||||
},
|
||||
} as never);
|
||||
expect(normalizedConfig?.baseUrl).toBe("https://openrouter.ai/api/v1");
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
modelId: "arcee/trinity-large-thinking",
|
||||
model: {
|
||||
provider: "arcee",
|
||||
id: "trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
id: "arcee/trinity-large-thinking",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
const normalizedModel = provider.normalizeResolvedModel?.({
|
||||
modelId: "arcee/trinity-large-thinking",
|
||||
model: {
|
||||
provider: "arcee",
|
||||
id: "trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
},
|
||||
} as never);
|
||||
expect(normalizedModel?.id).toBe("arcee/trinity-large-thinking");
|
||||
expect(normalizedModel?.baseUrl).toBe("https://openrouter.ai/api/v1");
|
||||
|
||||
expect(
|
||||
provider.normalizeTransport?.({
|
||||
|
||||
@@ -98,21 +98,28 @@ describe("buildAzureSpeechProvider", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(canonical).toEqual(
|
||||
expect.objectContaining({
|
||||
apiKey: "key",
|
||||
region: "eastus",
|
||||
baseUrl: "https://eastus.tts.speech.microsoft.com",
|
||||
voice: "en-US-AriaNeural",
|
||||
}),
|
||||
);
|
||||
expect(alias).toEqual(
|
||||
expect.objectContaining({
|
||||
apiKey: "alias-key",
|
||||
endpoint: "https://westus.tts.speech.microsoft.com/cognitiveservices/v1",
|
||||
baseUrl: "https://westus.tts.speech.microsoft.com",
|
||||
}),
|
||||
);
|
||||
expect(canonical).toEqual({
|
||||
apiKey: "key",
|
||||
region: "eastus",
|
||||
endpoint: undefined,
|
||||
baseUrl: "https://eastus.tts.speech.microsoft.com",
|
||||
voice: "en-US-AriaNeural",
|
||||
lang: "en-US",
|
||||
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
||||
voiceNoteOutputFormat: "ogg-24khz-16bit-mono-opus",
|
||||
timeoutMs: undefined,
|
||||
});
|
||||
expect(alias).toEqual({
|
||||
apiKey: "alias-key",
|
||||
region: undefined,
|
||||
endpoint: "https://westus.tts.speech.microsoft.com/cognitiveservices/v1",
|
||||
baseUrl: "https://westus.tts.speech.microsoft.com",
|
||||
voice: "en-US-JennyNeural",
|
||||
lang: "en-US",
|
||||
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
||||
voiceNoteOutputFormat: "ogg-24khz-16bit-mono-opus",
|
||||
timeoutMs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses provider-specific TTS directives", () => {
|
||||
|
||||
@@ -46,6 +46,10 @@ function warnMessages(): string[] {
|
||||
return logger.warn.mock.calls.map(([message]) => String(message));
|
||||
}
|
||||
|
||||
function expectWarnContaining(fragment: string) {
|
||||
expect(warnMessages().some((message) => message.includes(fragment))).toBe(true);
|
||||
}
|
||||
|
||||
function enableAdvertiserUnitMode(hostname = "test-host") {
|
||||
// Allow advertiser to run in unit tests.
|
||||
delete process.env.VITEST;
|
||||
@@ -407,17 +411,13 @@ describe("gateway bonjour advertiser", () => {
|
||||
expect(exceptionHandler).toBeTypeOf("function");
|
||||
|
||||
expect(handler?.(new Error("CIAO PROBING CANCELLED"))).toBe(true);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressing ciao cancellation"),
|
||||
);
|
||||
expectWarnContaining("suppressing ciao cancellation");
|
||||
|
||||
logger.warn.mockClear();
|
||||
expect(
|
||||
handler?.(new Error("Reached illegal state! IPV4 address change from defined to undefined!")),
|
||||
).toBe(true);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressing ciao interface assertion"),
|
||||
);
|
||||
expectWarnContaining("suppressing ciao interface assertion");
|
||||
|
||||
logger.warn.mockClear();
|
||||
expect(
|
||||
@@ -430,9 +430,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressing ciao netmask assertion"),
|
||||
);
|
||||
expectWarnContaining("suppressing ciao netmask assertion");
|
||||
|
||||
logger.warn.mockClear();
|
||||
expect(
|
||||
@@ -442,9 +440,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressing ciao self-probe race"),
|
||||
);
|
||||
expectWarnContaining("suppressing ciao self-probe race");
|
||||
|
||||
await started.stop();
|
||||
});
|
||||
@@ -470,9 +466,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
expect(createService).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressing ciao cancellation"),
|
||||
);
|
||||
expectWarnContaining("suppressing ciao cancellation");
|
||||
expect(warnMessages().some((message) => message.includes("restarting advertiser"))).toBe(true);
|
||||
expect(destroy).toHaveBeenCalledTimes(1);
|
||||
expect(advertise).toHaveBeenCalledTimes(2);
|
||||
@@ -709,9 +703,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
|
||||
await vi.advanceTimersByTimeAsync(25_000);
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("service stuck in announcing"),
|
||||
);
|
||||
expectWarnContaining("service stuck in announcing");
|
||||
expect(createService).toHaveBeenCalledTimes(2);
|
||||
expect(advertise).toHaveBeenCalledTimes(3);
|
||||
expect(destroy).toHaveBeenCalledTimes(1);
|
||||
@@ -737,9 +729,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
|
||||
await vi.advanceTimersByTimeAsync(55_000);
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("disabling advertiser after 1 stuck-state restart"),
|
||||
);
|
||||
expectWarnContaining("disabling advertiser after 1 stuck-state restart");
|
||||
expect(createService).toHaveBeenCalledTimes(2);
|
||||
expect(advertise).toHaveBeenCalledTimes(2);
|
||||
expect(destroy).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -100,10 +100,11 @@ describe("brave web search provider", () => {
|
||||
|
||||
const result = await tool.execute({ query: "OpenClaw docs" });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
error: "missing_brave_api_key",
|
||||
message:
|
||||
"web_search (brave) needs a Brave Search API key. Run `openclaw configure --section web` to store it, or set BRAVE_API_KEY in the Gateway environment. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -368,8 +369,10 @@ describe("brave web search provider", () => {
|
||||
date_before: "2026-03-01",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
error: "invalid_date_range",
|
||||
message: "date_after must be before date_before.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,8 +520,10 @@ describe("brave web search provider", () => {
|
||||
date_after: "2999-01-01",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
error: "invalid_date_range",
|
||||
message: "date_after cannot be in the future for Brave llm-context mode.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -543,8 +548,11 @@ describe("brave web search provider", () => {
|
||||
date_before: "2025-01-31",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
error: "unsupported_date_filter",
|
||||
message:
|
||||
"Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -82,12 +82,10 @@ function registerBrowserAutoEnableProbe(): BrowserAutoEnableProbe {
|
||||
describe("browser plugin", () => {
|
||||
it("exposes static browser metadata on the plugin definition", () => {
|
||||
expect(browserPluginReload).toEqual({ restartPrefixes: ["browser"] });
|
||||
expect(browserPluginNodeHostCommands).toEqual([
|
||||
expect.objectContaining({
|
||||
command: "browser.proxy",
|
||||
cap: "browser",
|
||||
}),
|
||||
]);
|
||||
expect(browserPluginNodeHostCommands).toHaveLength(1);
|
||||
expect(browserPluginNodeHostCommands[0]?.command).toBe("browser.proxy");
|
||||
expect(browserPluginNodeHostCommands[0]?.cap).toBe("browser");
|
||||
expect(typeof browserPluginNodeHostCommands[0]?.handle).toBe("function");
|
||||
expect(browserSecurityAuditCollectors).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -135,21 +133,19 @@ describe("browser plugin", () => {
|
||||
const { api, registerCli } = createApi();
|
||||
registerBrowserPlugin(api);
|
||||
|
||||
expect(registerCli).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
commands: ["browser"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "browser",
|
||||
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(registerCli).toHaveBeenCalledTimes(1);
|
||||
const registrar = registerCli.mock.calls[0]?.[0];
|
||||
expect(typeof registrar).toBe("function");
|
||||
expect(registerCli.mock.calls[0]?.[1]).toEqual({
|
||||
commands: ["browser"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "browser",
|
||||
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
await registrar({ program: {} as never });
|
||||
expect(runtimeApiMocks.registerBrowserCli).toHaveBeenCalledWith({});
|
||||
});
|
||||
@@ -158,10 +154,13 @@ describe("browser plugin", () => {
|
||||
const { api, registerGatewayMethod } = createApi();
|
||||
registerBrowserPlugin(api);
|
||||
|
||||
expect(registerGatewayMethod).toHaveBeenCalledWith("browser.request", expect.any(Function), {
|
||||
expect(registerGatewayMethod).toHaveBeenCalledTimes(1);
|
||||
expect(registerGatewayMethod.mock.calls[0]?.[0]).toBe("browser.request");
|
||||
const handler = registerGatewayMethod.mock.calls[0]?.[1];
|
||||
expect(typeof handler).toBe("function");
|
||||
expect(registerGatewayMethod.mock.calls[0]?.[2]).toEqual({
|
||||
scope: "operator.admin",
|
||||
});
|
||||
const handler = registerGatewayMethod.mock.calls[0]?.[1];
|
||||
await handler({ method: "browser.request" });
|
||||
expect(runtimeApiMocks.handleBrowserGatewayRequest).toHaveBeenCalledWith({
|
||||
method: "browser.request",
|
||||
@@ -181,7 +180,9 @@ describe("browser plugin", () => {
|
||||
registerBrowserPlugin(api);
|
||||
|
||||
const service = registerService.mock.calls[0]?.[0];
|
||||
expect(service).toMatchObject({ id: "browser-control" });
|
||||
expect(service?.id).toBe("browser-control");
|
||||
expect(typeof service?.start).toBe("function");
|
||||
expect(typeof service?.stop).toBe("function");
|
||||
expect(runtimeApiMocks.createBrowserPluginService).not.toHaveBeenCalled();
|
||||
|
||||
await service.start({ config: {}, stateDir: "/tmp/openclaw", logger: { warn: vi.fn() } });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
||||
import {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
browserAct,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { type WebSocket, WebSocketServer } from "ws";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import "../test-support/browser-security.mock.js";
|
||||
import {
|
||||
type AriaSnapshotNode,
|
||||
captureScreenshot,
|
||||
|
||||
@@ -107,9 +107,7 @@ describe("CDP screenshot params", () => {
|
||||
await captureScreenshot({ wsUrl: "ws://localhost:9222/devtools/page/X", format: "png" });
|
||||
|
||||
const call = requireSentMessage("Page.captureScreenshot");
|
||||
expect(call.params).toMatchObject({
|
||||
format: "png",
|
||||
});
|
||||
expect(call.params?.format).toBe("png");
|
||||
expect(call.params).not.toHaveProperty("fromSurface");
|
||||
expect(call.params).not.toHaveProperty("captureBeyondViewport");
|
||||
expect(call.params).not.toHaveProperty("clip");
|
||||
@@ -152,27 +150,23 @@ describe("CDP screenshot params", () => {
|
||||
}
|
||||
|
||||
// Expand: uses saved DPR, mobile defaults to false
|
||||
expect(firstSetCall.params).toMatchObject({
|
||||
width: 1200,
|
||||
height: 3000,
|
||||
deviceScaleFactor: 2,
|
||||
mobile: false,
|
||||
});
|
||||
expect(firstSetCall.params?.width).toBe(1200);
|
||||
expect(firstSetCall.params?.height).toBe(3000);
|
||||
expect(firstSetCall.params?.deviceScaleFactor).toBe(2);
|
||||
expect(firstSetCall.params?.mobile).toBe(false);
|
||||
|
||||
// Clear is called first in the finally block
|
||||
requireSentMessage("Emulation.clearDeviceMetricsOverride");
|
||||
const captureCall = requireSentMessage("Page.captureScreenshot");
|
||||
expect(captureCall.params).toMatchObject({ captureBeyondViewport: true });
|
||||
expect(captureCall.params?.captureBeyondViewport).toBe(true);
|
||||
|
||||
// Viewport drifted after clear → re-apply saved dimensions
|
||||
expect(secondSetCall.params).toMatchObject({
|
||||
width: 800,
|
||||
height: 600,
|
||||
deviceScaleFactor: 2,
|
||||
mobile: false,
|
||||
screenWidth: 800,
|
||||
screenHeight: 600,
|
||||
});
|
||||
expect(secondSetCall.params?.width).toBe(800);
|
||||
expect(secondSetCall.params?.height).toBe(600);
|
||||
expect(secondSetCall.params?.deviceScaleFactor).toBe(2);
|
||||
expect(secondSetCall.params?.mobile).toBe(false);
|
||||
expect(secondSetCall.params?.screenWidth).toBe(800);
|
||||
expect(secondSetCall.params?.screenHeight).toBe(600);
|
||||
});
|
||||
|
||||
it("fullPage on non-emulated tab: clears and does NOT re-apply emulation", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clickChromeMcpElement,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
openChromeMcpTab,
|
||||
resetChromeMcpSessionsForTest,
|
||||
setChromeMcpSessionFactoryForTest,
|
||||
takeChromeMcpScreenshot,
|
||||
} from "./chrome-mcp.js";
|
||||
|
||||
type ToolCall = {
|
||||
@@ -78,6 +80,15 @@ function createFakeSession(): ChromeMcpSession {
|
||||
],
|
||||
};
|
||||
}
|
||||
if (name === "take_screenshot") {
|
||||
const filePath = typeof args?.filePath === "string" ? args.filePath : undefined;
|
||||
const format = args?.format === "jpeg" ? "jpeg" : "png";
|
||||
if (!filePath) {
|
||||
throw new Error("missing filePath");
|
||||
}
|
||||
await fs.writeFile(`${filePath}.${format}`, Buffer.from(`screenshot:${format}`));
|
||||
return { content: [{ type: "text", text: `Saved screenshot to ${filePath}.${format}.` }] };
|
||||
}
|
||||
throw new Error(`unexpected tool ${name}`);
|
||||
});
|
||||
|
||||
@@ -127,6 +138,19 @@ describe("chrome MCP page parsing", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads screenshot files with the extension written by chrome-devtools-mcp", async () => {
|
||||
const factory: ChromeMcpSessionFactory = async () => createFakeSession();
|
||||
setChromeMcpSessionFactoryForTest(factory);
|
||||
|
||||
await expect(
|
||||
takeChromeMcpScreenshot({
|
||||
profileName: "chrome-live",
|
||||
targetId: "1",
|
||||
format: "jpeg",
|
||||
}),
|
||||
).resolves.toEqual(Buffer.from("screenshot:jpeg"));
|
||||
});
|
||||
|
||||
it("adds --userDataDir when an explicit Chromium profile path is configured", () => {
|
||||
expect(buildChromeMcpArgs("/tmp/brave-profile")).toEqual([
|
||||
"-y",
|
||||
@@ -708,4 +732,32 @@ describe("chrome MCP page parsing", () => {
|
||||
await expectation;
|
||||
expect(closeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("honors abort signals while waiting for ephemeral availability probes", async () => {
|
||||
const closeMock = vi.fn().mockResolvedValue(undefined);
|
||||
const factory: ChromeMcpSessionFactory = async () =>
|
||||
({
|
||||
client: {
|
||||
callTool: vi.fn(),
|
||||
listTools: vi.fn(),
|
||||
close: closeMock,
|
||||
connect: vi.fn(),
|
||||
},
|
||||
transport: {
|
||||
pid: 123,
|
||||
},
|
||||
ready: new Promise<void>(() => {}),
|
||||
}) as unknown as ChromeMcpSession;
|
||||
setChromeMcpSessionFactoryForTest(factory);
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const promise = ensureChromeMcpAvailable("chrome-live", undefined, {
|
||||
ephemeral: true,
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
ctrl.abort(new Error("status budget exhausted"));
|
||||
|
||||
await expect(promise).rejects.toThrow(/status budget exhausted/);
|
||||
expect(closeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user