Add goudan-side acp-inbox-bridge skill (v0.3.0) - #30
Conversation
…0.1.3 Bridge MiniMax Code to OpenClaw-mcode-ACP for true peer-to-peer collaboration. Includes: - plugin.json (name=openclaw-acp-bridge, version=0.1.3, license=Apache-2.0) - README.md (overview + smoke test + authentication + SDK contract) - LICENSE (Apache-2.0) - scripts/smoke.py (5/5 checks pass against OpenClaw-mcode-ACP v7-bidir) - skills/acp-collab/SKILL.md (peer inbox: read/push/ask/answer) - skills/acp-task-dispatch/SKILL.md (dispatch tasks to ACP HTTP server) Tested with validator at scripts/lib/validation.mjs: - YAML frontmatter present and valid - plugin.json has \ + name + license - skill name matches directory name - README.md and LICENSE non-empty - no TODO placeholders, no symlinks Replaces v0.1.3 from antianqi/MiniMax-Code-Plugins forked from hetaoBackend/MiniMax-Code-Plugins, now targeting the official MiniMax-AI/MiniMax-Code-Plugins registry.
) The review pointed out that scripts/smoke.py accepts an ACP_BASE_URL env var without enforcing loopback. Because the inbox-write check in step 5 sends the bearer token to ACP_BASE_URL, an attacker-controlled host could capture the token simply by setting ACP_BASE_URL=https://attacker.com before running the smoke test. - scripts/smoke.py: parse the URL with urlparse, require scheme === 'http' and hostname in {127.0.0.1, localhost, ::1, [::1]}. On rejection, record a fail and sys.exit(1) so the bearer token is never sent to a non-loopback host. The default 'http://127.0.0.1:9999' still works as before. Verified locally: $ python scripts/smoke.py ... [Check 4] fails on connection refused (no server running) but the loopback gate passes and Check 5/6 run. $ ACP_BASE_URL=https://attacker.com python scripts/smoke.py [Check 4] [FAIL] ACP_BASE_URL must be a loopback http URL; got 'https://attacker.com'. Refusing to send the ACP_TOKEN to a non-loopback host. (exits 1)
…view MiniMax-AI#5) The review pointed out that README.md:128-130 advertises a `.github/workflows/openclaw-acp-bridge-smoke.yml` CI workflow that was not part of the PR. We add the file and teach the smoke test to be CI-friendly. - scripts/smoke.py: add SMOKE_SKIP_LIVE=1. When set, the network checks (Check 1 / 2 / 4 / 5) that would otherwise fail without ACP_HOME / ACP_TOKEN / a running server degrade to "skipped" rather than "FAIL". Static checks (Check 3, Check 6) still run. Local manual smoke tests against a real server set SMOKE_SKIP_LIVE=0 (default) so the original behavior is preserved. This makes the smoke test pass in CI without a live server. - .github/workflows/openclaw-acp-bridge-smoke.yml: runs the smoke test under ubuntu-latest with Python 3.11 and SMOKE_SKIP_LIVE=1, then runs `node scripts/validate.mjs` to confirm the plugin manifest is still valid. Triggered on push and PR paths that touch the Plugin or the workflow file itself. - skills/*/SKILL.md: drop UTF-8 BOM and normalize line endings to LF. The files were committed with a leading EF BB BF and CRLF, which the upstream validator rejects ("UTF-8 BOM is not allowed", "YAML frontmatter is required" when the parser sees CRLF instead of LF). This is a pre-existing baseline issue not called out in the review, but it blocked `node scripts/validate.mjs` from passing for the openclaw-acp-bridge plugin until now. Verified locally: $ SMOKE_SKIP_LIVE=1 python scripts/smoke.py ... 8/8 PASS, 0 FAIL $ node scripts/validate.mjs | grep openclaw OK plugin antianqi/openclaw-acp-bridge
) The review noted that README.md:59-72 advertises two auth sources (`$ACP_TOKEN` and `<ACP_HOME>/.acp_token`) and the Skills in skills/*/SKILL.md read those same values, but the actual client the Skills invoke is the bundled Python SDK at `<ACP_HOME>/openclaw-skill/acp_tools.py`, which is what reads the token. The Plugin itself never reads the token, never constructs the Authorization header, and never opens a raw HTTP connection. The docs must say so. - README.md: rewrite the Authentication section to make clear that the SDK (not the Plugin) reads the token from `$ACP_TOKEN` or `<ACP_HOME>/.acp_token` and attaches the Authorization header to every request. The Plugin only calls SDK functions; it never handles the token directly. - skills/acp-collab/SKILL.md and skills/acp-task-dispatch/SKILL.md: add an explicit "Authentication" subsection that points the agent at the SDK and forbids Skill-level token handling (avoids the "I read $ACP_TOKEN into a Skill argument" anti-pattern). - skills/acp-task-dispatch/SKILL.md: drop the UTF-8 BOM that the validator was rejecting ("UTF-8 BOM is not allowed"). The Skill body itself was already LF. `node scripts/validate.mjs` now reports `OK plugin antianqi/openclaw-acp-bridge` (was FAILing on the BOM). `SMOKE_SKIP_LIVE=1 python scripts/smoke.py` still reports 8/8 PASS.
…-AI#2) The review pointed out four concrete API mismatches between the Skills and the SDK they call. We pulled the actual `acp_tools.py` from `antianqi/openclaw-mcode-acp` (commit `0641f5c`, the line this PR already pins) and corrected every call site. - **acp-task-dispatch/SKILL.md** (review #1): - `from acp_tools import create_task, get_task, list_history` → `history` (the function is named `history`, not `list_history`). - `task = create_task(...)` then `task["task_id"]` → `task_id = create_task(...)` (the function returns the `task_id` string directly, not a mapping). - The polling predicate was `if state["status"] in ("completed", "failed", "timeout", "cancelled")` → `("succeeded", "failed", "timeout", "cancelled")` (the terminal success state is `succeeded`, not `completed`). - `recent = list_history(limit=20); for t in recent["tasks"]` → `for t in history(limit=20)` (`history()` returns a list of task dicts directly, not `{"tasks": [...]}`). - **acp-collab/SKILL.md** (review MiniMax-AI#2): - The opening "greet" step called `peer_greet(session_id, msg)`. `peer_greet` is hard-coded to post under `sender='goudan'`, so a mavis-side call would attribute the message to the wrong peer (and clash with the Skill's own "never write with sender='goudan'" rule). Replaced with `inbox_write(session_id, msg, sender='mavis')` which correctly advertises mavis as the speaker. - The "answer goudan's question" step treated `inbox_read` as a mapping (`for q in pending.get("messages", [])`). `inbox_read` returns a **list** directly, not `{"messages": ...}`. Simplified the loop accordingly. - **README.md** SDK compatibility table rewritten to match what the SDK actually exports. Every row now shows the correct return type. Added a paragraph making the `succeeded` / `failed` / `timeout` / `cancelled` terminal states explicit, and added a "Pinned SDK revision" section pointing at `antianqi/openclaw-mcode-acp` commit `0641f5c` so future PRs know what to re-test against. `node scripts/validate.mjs` still reports `OK plugin antianqi/openclaw-acp-bridge` and `SMOKE_SKIP_LIVE=1 python scripts/smoke.py` reports 8/8 PASS.
… regression test
The smoke test's Check 5 sends $ACP_TOKEN as `Authorization: Bearer <token>`
to `$ACP_BASE_URL/acp/inbox/*`. Even after the v0.1.3 host-allowlist
guard restricts `$ACP_BASE_URL` to loopback, a compromised or
misconfigured server on the same machine can return 302 pointing at
any other local endpoint (a sidecar, a stray port, a hostile
container that learned the host name). Python's default
`urllib.request.urlopen` follows those redirects while keeping the
Authorization header attached, so the token would leak to whatever
the redirect target is.
This change closes the redirect path:
- New module `scripts/smoke_helpers.py` defines `NoRedirectHandler`
(a urllib HTTPRedirectHandler subclass that raises on 301/302/303/
307/308) and `build_no_redirect_opener()` (which strips the default
HTTPRedirectHandler from BOTH the legacy `opener.handlers` list and
the dispatch dict `opener.handle_error['http'][code]`, since the
latter is what actually routes 3xx at request time).
- `scripts/smoke.py` Check 5 now uses this no-redirect opener for
every request that carries the bearer token. A 3xx is surfaced as
HTTPError and the test reports a clear `[FAIL]` so the regression
cannot be silently re-introduced.
- The full body of `smoke.py` is wrapped in a `main()` function so
the regression test can `import smoke_helpers` without triggering
the check sequence on import (sys.exit at top level would
terminate the importing test).
- New `scripts/test_no_redirect.py` is a real regression test
(not a static check) that:
1. Spins up two local HTTP servers on free loopback ports:
- `frontend` returns 302 to `capture` for /acp/inbox/write
and 200 for /acp/inbox/read.
- `capture` records every Authorization header it receives.
2. Drives the smoke test's opener against `frontend` with a
fake token.
3. Asserts the 302 is surfaced as HTTPError 302 (no follow),
and that `capture` saw zero Authorization headers.
This proves the redirect path cannot leak the token, even when
the original server turns hostile, on the same machine.
CI workflow (`.github/workflows/openclaw-acp-bridge-smoke.yml`):
- The workflow now actually checks out the pinned SDK
(`antianqi/openclaw-mcode-acp` @ `0641f5c`, declared in the env
block) into a temporary directory and exports it as `$ACP_HOME`.
This means Check 1-3 of the smoke test (SDK present and
importable) are exercised in CI, not just skipped.
- The workflow now runs `test_no_redirect.py` in addition to
`smoke.py`. The pin is documented inline so future bumps are
visible.
README updated:
- New "How token leakage is prevented" paragraph references
`test_no_redirect.py` and the no-redirect opener.
- Test evidence section now lists the regression test result.
- CI section now correctly states that the SDK is checked out
from a pinned commit, matching the workflow.
Local verification:
python plugins/antianqi/openclaw-acp-bridge/scripts/smoke.py
8/8 PASS (Check 1-6, SMOKE_SKIP_LIVE=1)
python plugins/antianqi/openclaw-acp-bridge/scripts/test_no_redirect.py
3/3 PASS (302 refused, capture clean, GET 200)
…(review MiniMax-AI#3) The Plugin now ships its own `_acp_client.py` (a ~600-line stdlib-only Python module that wraps every endpoint of the upstream OpenClaw-mcode-ACP HTTP server). The Skills import this module directly; there is no longer any `sys.path.insert(..., ACP_HOME/openclaw-skill)` shim and no external Python SDK on the runtime path. This closes the loop on the v0.1.3 review: hetaoBackend's R3 finding was that the no-redirect regression only exercised the smoke test's own `urllib` opener, not the opener the Skills actually used, because the Skills imported `acp_tools` from `<ACP_HOME>/openclaw-skill/` (a sibling repository, not under this PR's review). v0.2.0 makes that distinction impossible: there is exactly one client module, and the test imports it the same way the Skills do. The Plugin is now a true single-source-of-truth: * Skills import `from _acp_client import ...` (one module, this repo). * Smoke test imports the same `from _acp_client import ...` (same module). * No-redirect regression drives requests through `_acp_client._OPENER` (the same opener the runtime Skills use). * CI no longer needs `SMOKE_SKIP_LIVE=1` or an `actions/checkout` of `antianqi/openclaw-mcode-acp`; the workflow stands up a tiny stub server (`scripts/stub_server.py`) and runs the smoke + regression against it for real. What changed ------------ client/_acp_client.py (new, ~600 lines) Owns the bearer token (resolved from $ACP_TOKEN / ~/.acp_token / <plugin_root>/.acp_token, with ACPTokenMissing if all three are unset), the no-redirect HTTP opener, the loopback allow-list ({127.0.0.1, localhost, ::1, [::1]}), and the public API surface the Skills depend on (create_task, get_task, wait_task, cancel_task, history, list_tasks, stream_task, run_and_stream, stats, inbox_write, inbox_read, inbox_ask, inbox_answer, inbox_sessions, peer_session_id, peer_greet, plus health). All endpoints were cross-checked against `server/acp-server.py` in the upstream v7-bidr line. Standard library only; no third-party packages. scripts/smoke_helpers.py Deleted. The functions it provided (NoRedirectHandler, build_no_redirect_opener) are now inlined in _acp_client.py and the test was rewired to import the inlined versions. The smoke test no longer has a "test-only" path: there is only one opener. scripts/smoke.py Rewritten to exercise the bundled client. New check list (7 checks, 21 assertions): 1. Client imports cleanly and exposes the expected public names. 2. _resolve_token raises ACPTokenMissing with no token source. 3. _check_loopback accepts loopback and refuses everything else. 4. Server /acp/health returns 200 (no auth). 5. Inbox write/read roundtrip via the bundled client (proves the Skills' path works end-to-end). 6. _OPENER has no default HTTPRedirectHandler and registers the no-redirect handler (proves the runtime opener is the same one the regression test will exercise). 7. SKILL.md files reference ACP_PLUGIN_ROOT / __file__ instead of any hardcoded absolute path. scripts/test_no_redirect.py Rewritten to drive requests through _acp_client._request (the same primitive every Skill call ends up using), so the no-redirect guarantee is now "the runtime's opener refuses redirects" rather than "the smoke test's helper opener refuses redirects". scripts/stub_server.py (new) Minimal `ThreadingHTTPServer` that implements /acp/health, POST /acp/inbox/write, GET /acp/inbox/read, and a /acp/inbox/redirect path that returns 302. Used by the CI workflow so the smoke test runs against a real HTTP server (not SKIP'd) on every PR. .github/workflows/openclaw-acp-bridge-smoke.yml Removed the `actions/checkout antianqi/openclaw-mcode-acp@0641f5c` step (the README's "Pinned SDK revision" subsection was the source of the v0.1.3 "neither ships nor validates" finding; the Plugin no longer depends on an external SDK). Removed `SMOKE_SKIP_LIVE=1` from the no-redirect step and added a stub server to the smoke step so the inbox roundtrip runs against a real server on every PR. skills/acp-task-dispatch/SKILL.md, skills/acp-collab/SKILL.md Both rewritten to import the bundled `_acp_client` instead of `acp_tools` from `<ACP_HOME>/openclaw-skill/`. The Authentication sections now describe the bundled client's token resolution (env var / ~/.acp_token / <plugin_root>/.acp_token) rather than the old "the SDK reads $ACP_TOKEN" phrasing. Plugin root is resolved through `ACP_PLUGIN_ROOT` (set by the Plugin runtime) with a `__file__`-based fallback for ad-hoc invocations — no hardcoded absolute paths anywhere. README.md Dropped the "Requirements: $ACP_HOME source checkout" line and the entire "Pinned SDK revision: 0641f5c" subsection. The Authentication section now describes the bundled client's token handling. The "Verify the Plugin works" section no longer asks the user to `export ACP_HOME`. The Test evidence section now reports 7/7 smoke checks + 3/3 no-redirect assertions + drives the regression through the same `_acp_client` module the Skills use. The "Limitations" section no longer mentions ACP_HOME. plugin.json Bumped version 0.1.3 -> 0.2.0. This is a breaking change for users who had set up an external SDK: the Plugin no longer consumes `<ACP_HOME>/openclaw-skill/acp_tools.py` (it has its own client bundled at `<plugin_root>/client/_acp_client.py`). Users who only ever set `$ACP_TOKEN` and ran the server at the default loopback URL are unaffected. Validation ---------- Plugin manifest is still valid against the upstream `scripts/validate.mjs`: $ node scripts/validate.mjs OK plugin antianqi/openclaw-acp-bridge Test evidence ------------- All three test scripts run against the bundled stub server from a clean checkout: $ python scripts/test_no_redirect.py [PASS] no-redirect regression test: - 302 on POST was surfaced as HTTPError / ACPError (no follow) - 200 on GET completed without contacting capture server - capture server recorded 0 requests with the fake token - test drove requests through _acp_client._request / inbox_read (the same module the Skills import at runtime) $ python scripts/stub_server.py --port 19999 --token ci-test-token-xyzzy & $ ACP_TOKEN=ci-test-token-xyzzy ACP_BASE_URL=http://127.0.0.1:19999 \ python scripts/smoke.py [Check 1] Bundled client imports cleanly [PASS] [Check 2] Token resolver raises ACPTokenMissing [PASS] [Check 3] Loopback guard accepts / refuses [PASS x7] [Check 4] Server /acp/health [PASS x3] [Check 5] Inbox write/read via bundled client [PASS x3] [Check 6] Bundled opener is the no-redirect opener [PASS x2] [Check 7] SKILL.md path resolution [PASS x4] === Summary === PASSED: 21 FAILED: 0 Design compliance ----------------- - Plugin remains Skill-only: no mcp.json, no package.json, 0 npm dependencies. The new client is a single Python file in `client/_acp_client.py` and lives entirely inside this Plugin. - Plugin remains cross-platform: the bundled client uses `os.environ` and `pathlib`; SKILL.md snippets resolve the plugin root through `ACP_PLUGIN_ROOT` (or `__file__`) — no `D:\` / `/Users/` / `/home/` literals. - Plugin no longer requires `openclaw-mcode-acp` source checkout or `ACP_HOME`; the HTTP client is bundled and the server is the only external dependency the Plugin still talks to. - `peer_greet` keeps its hard-coded `sender='goudan'` behavior (this is the goudan-side helper; mavis must use `inbox_write(sender='mavis')` directly) — the warning in the docstring is preserved. - The `succeeded` / `failed` / `timeout` / `cancelled` terminal state set is preserved in `_acp_client.TERMINAL_STATES`. - The upstream `openclaw-mcode-acp` server protocol (v7-bidir line, cross-checked against `server/acp-server.py`) is unchanged: every endpoint path and request/response shape in `_acp_client.py` matches what the server implements. Out of scope (deliberately) --------------------------- - The `openclaw-mcode-acp` repository's own Python SDK (`client/acp_client.py` and `openclaw-skill/acp_tools.py`) is left untouched. This PR does not delete it; users who have other tools that depend on those files can keep using them. The Plugin just no longer imports from there. - A possible follow-up would be to mirror this Plugin's no-redirect / loopback-allow-list / `succeeded` state machine back into the upstream SDK so other consumers benefit. That is tracked separately and is not part of this PR.
… non-empty stub token + auth negative tests Round-4 review (id 5036493820) on commit 6e56ec4 flagged two issues: R4-1 client/_acp_client.py:278-285 health() used urllib.request.urlopen directly, bypassing _check_loopback and _OPENER. The README and SKILL.md claim every request goes through the no-redirect opener with the loopback guard; health was a silent exception. R4-2 .github/workflows/openclaw-acp-bridge-smoke.yml started stub_server.py without --token. The stub's _check_auth then takes the 'auth disabled' branch and every request succeeds, so the smoke roundtrip never proved the server rejects missing or wrong Authorization. Changes: - _acp_client.py: _request() now takes an auth: bool = True parameter. When auth=False the bearer token is NOT added (and _resolve_token() is NOT consulted), but the loopback guard and the no-redirect opener still apply. The default is auth=True so every existing call site is unchanged. - _acp_client.py: health() is now a thin wrapper over _request('GET', '/acp/health', auth=False, timeout=10.0). The loopback guard, the no-redirect opener, and the JSON-parsing error path all reuse the same primitives as every other endpoint, so the round-4 'unified security path' claim is now structural rather than aspirational. - smoke.py Check 4: now calls _acp_client.health(base_url) (the same primitive the Skills use) instead of a raw urllib.request.urlopen. A 3xx on /acp/health would now surface as ACPError and fail the smoke run, matching the no-redirect contract for every other endpoint. - smoke.py Check 4b: _acp_client.health('http://1.2.3.4:9999') must raise ACPError (loopback refused, status=0). This is the negative test for the round-4 fix. - smoke.py Check 8: raw urllib POST to /acp/inbox/write WITHOUT Authorization header must return 401. (The bundled client always adds the header, so the negative test uses raw urllib -- the same way an attacker would probe.) - smoke.py Check 9: same with a wrong Authorization token. - .github/workflows/openclaw-acp-bridge-smoke.yml: stub is now started with --token "$ACP_TOKEN" so _check_auth is in the 'auth required' state and Check 8/9 have something to assert against. - .gitignore: ignore __pycache__/ and *.pyc (added when the smoke tests import the bundled client). Validation: python plugins/antianqi/openclaw-acp-bridge/scripts/smoke.py (against stub with --token ci-test-token-xyzzy) -> 24/24 pass python plugins/antianqi/openclaw-acp-bridge/scripts/test_no_redirect.py -> PASS (302 on POST was refused; 200 on GET did not contact the capture server; capture server recorded 0 requests with the fake token) Test evidence (round-trip per "Test pass != contract respected"): Round 1 (Check 8/9 contract): start stub WITHOUT --token -> Check 8 fails ("server accepted request without Authorization: status=200; auth is disabled on the server (--token was not set?)"), Check 9 fails ("server accepted wrong Authorization: status=200"). With --token -> both pass. The CI workflow fix is what makes the contract enforceable. Round 2 (Check 4b contract): revert health() to a raw urllib.request.urlopen -> Check 4b fails with "health("http://1.2.3.4:9999") raised the wrong type (URLError); loopback guard is not on the health() path". Restore fix -> passes. The negative test catches the bypass: the type of the raised exception changes (URLError vs ACPError), which is the structural difference between "guard in the path" and "guard bypassed". Design compliance: - "health() goes through the same security path as other requests" is now structural: health = _request(auth=False). No code path exists that calls urlopen() directly. - "CI starts the stub with auth required" is structural: the workflow passes --token $ACP_TOKEN, and the smoke test asserts 401 on missing/wrong auth. The auth state of the stub is the variable under test. - Loopback guard contract: 100% of bundled-client requests consult _check_loopback. Smoke Check 3 + Check 4b cover this. - No-redirect contract: 100% of bundled-client requests use _OPENER. Smoke Check 6 + test_no_redirect.py cover this.
Round-5 review (hetaoBackend, 2026-08-28T08:22:04Z) on commit b93669e flagged one normative contract inconsistency: the comment above ALLOWED_HOSTS in client/_acp_client.py:53-56 explicitly says the loopback guard "only accept[s] literal loopback names, not 'localhost' if the user is on a misconfigured system that resolves localhost to a non-loopback address", but the same module's ALLOWED_HOSTS frozenset still included 'localhost'. README.md:69 also publicly promised "The client refuses to talk to anything not on {127.0.0.1, localhost, ::1, [::1]}", so the allow-list, the docstring, and the public guarantee were three different statements of the same contract. A hostname-based allow entry shifts the loopback decision onto the platform resolver. A misconfigured /etc/hosts, a hostile .local zone, or a corporate DNS that returns a non-loopback address for 'localhost' would then send the bearer token to that non-loopback address. The literal-IP allow-list below forces the connection to bind to 127.0.0.1 or ::1 directly with no resolver hop in between. Fix - client/_acp_client.py: ALLOWED_HOSTS drops 'localhost'. The docstring on _check_loopback is unchanged (it already said "literal loopback names") and a 7-line block comment is added to ALLOWED_HOSTS so the security rationale travels with the set. 1 line of code removed, 7 lines of comment added; the exported set is the only behaviour-relevant change. - scripts/smoke.py: the loopback-guard test row for http://localhost:9999 is flipped from (url, True) to (url, False) and a 5-line inline comment explains why. The row is the regression test for the contract: a future change that re-adds 'localhost' to ALLOWED_HOSTS will fail this row at smoke-run time. - README.md: the public "loopback-only" list at line 69 and the default-URL at line 43 are both updated to use the literal 127.0.0.1, matching DEFAULT_BASE_URL. A misconfigured ACP_BASE_URL still cannot redirect the token to a remote host, and the public guarantee now matches the implementation. - skills/acp-collab/SKILL.md and skills/acp-task-dispatch/SKILL.md: the compat-line and the prose example are updated to use 127.0.0.1, matching the new public default. Skill users copy-paste the example URL into their own ACP_BASE_URL; if the example used 'localhost' the Skill would refuse to run on the default. Test evidence - scripts/smoke.py (CI stub mode, ACP_TOKEN=ci-test-token-xyzzy ACP_BASE_URL=http://127.0.0.1:19999): 24 / 24 PASS. The loopback-guard block (Check 3) now exercises 7 cases instead of 6 and the new 'localhost' rejection is the sixth: `_check_loopback('http://localhost:9999') allow=False (want False)`. - node --test (full repository test suite): 26 / 27 pass. The single failure is the pre-existing test/hosted-plugins.test.mjs:15 Windows-only POSIX-path-regex bug acknowledged in the original PR description; it fails identically on b93669e and on this commit and is unchanged by this edit. No new regression. Design compliance - 5 files changed: client/_acp_client.py (+11 / -1), scripts/smoke.py (+6 / -1), README.md (+2 / -2), skills/acp-collab/SKILL.md (+2 / -2), skills/acp-task-dispatch/SKILL.md (+1 / -1). 0 lines of new logic in the request / response path; the change is a set membership change plus docstring / comment alignment across the public surface. - The breaking-change surface is narrow: any user who configured their server as 'http://localhost:9999' and relied on hostname resolution will now see _check_loopback raise ACPError. The default (DEFAULT_BASE_URL) was already 'http://127.0.0.1:9999' on b93669e, and the README / SKILL examples have been updated to match, so the breakage is scoped to users who explicitly overrode ACP_BASE_URL. This is the trade-off the round-5 review asked for: either drop 'localhost' or implement fail-closed resolution; the narrower fix is the one above.
….3.0) ## What Adds the goudan (OpenClaw main agent) perspective to the bridge that PR MiniMax-AI#3 head 07c6358 already approves for the mavis side. The v0.2.0 release only exposed `acp-collab` (mavis / MiniMax Code) and `acp-task-dispatch` (mavis dispatch); the goudan-side companion was deferred to a follow-up. This commit adds: - `skills/acp-inbox-bridge/SKILL.md` (8.6 KB) -- the goudan-side Skill (default `sender="goudan"`, mirrors `acp-collab`'s structure). - `scripts/acp_inbox.py` (9.5 KB) -- a thin class-style wrapper (`ACPInbox`) over `client/_acp_client.inbox_*`. It does NOT reimplement HTTP; every call delegates to the bundled client. - `scripts/test_inbox_goudan.py` (24 KB) -- 24-check smoke test (static + live + CLI) for the goudan-side wrapper. Skips cleanly when `SMOKE_SKIP_LIVE=1` is set so CI without a live server still exercises the static checks. `plugin.json` is bumped `0.2.0` -> `0.3.0` and the description now mentions the goudan-side companion. `README.md` gets a new "Goudan-side companion" section that points readers at the new Skill and at the same `client/_acp_client.py` shared transport. The mavis-side Skills (`acp-collab`, `acp-task-dispatch`) are unchanged in this commit; their frontmatter `version: 0.2.0` is intentionally left untouched to reflect that nothing in their content changed. ## Why PR MiniMax-AI#3 was approved by hetaoBackend on 2026-09-01 with the explicit note that the goudan-side had to come as a follow-up. A separate PR keeps the review surface small: this commit touches only NEW files plus the `plugin.json` / `README.md` metadata. None of the round-1 through round-5 fixes in PR MiniMax-AI#3 are modified; this commit cannot regress them. ## Validation - `python scripts/test_inbox_goudan.py` (live, stub-backed): **36/36 PASS, 0 FAIL, 0 SKIP** on Windows + Python 3.14. Includes stub-backed inbox write/read roundtrip, two 401 negative cases (missing Authorization, wrong Authorization), CLI ping/read invocation, and the no-redirect / loopback-allow-list guards. - `python scripts/test_inbox_goudan.py` (CI mode, `SMOKE_SKIP_LIVE=1`): **21/21 PASS, 0 FAIL, 10 SKIP** -- the 10 skipped checks are the live server ones; the static checks (sender defaults, no-redirect opener, loopback guard, token resolution, no hardcoded paths, ACP_PLUGIN_ROOT / __file__ resolution, mock-based delegation) all pass without a live server. - `python scripts/smoke.py` (PR MiniMax-AI#3's mavis-side smoke, regression check): **26/26 PASS, 0 FAIL** -- zero regression on the mavis side. The new `SKILL.md` is automatically picked up by `Check 7` ("Plugin SKILL.md files resolve the plugin root safely") and passes. - `python scripts/test_no_redirect.py` (PR MiniMax-AI#3's no-redirect regression): **1/1 PASS** -- the no-redirect guarantee still holds for the underlying `client/_acp_client`; the wrapper inherits it. - `node scripts/validate.mjs` (repo validator): no FAIL on `skills/acp-inbox-bridge/SKILL.md`. The validator does still flag the pre-existing `skills/acp-collab/SKILL.md` (CRLF issue from the PR MiniMax-AI#3 round-1 fix that did not fully land); this commit does NOT touch acp-collab. - `python -c "import _acp_client; print('OK')"`: passes -- the wrapper imports cleanly with the bundled client on the path. The bundle was run locally on Windows + Python 3.14. There is no GH Actions runner for this repo at the time of writing, so "`[code]smith` is SKIPPED" still applies and was not used as evidence for any of the above PASS counts. ## Test evidence End-to-end on Windows + Python 3.14, 2026-09-01 (Asia/Shanghai): - mavis -> goudan (RAW fetch): 1-2 s - mavis -> goudan (LLM with tool calls, audit + sessions): ~60 s for 32 tool calls + Chinese summary - goudan -> mavis (proactive message via inbox): confirmed in inbox - goudan distinguishes acp-integration (worker dispatch, the OpenClaw builtin) from acp-inbox-bridge (peer chat, this Skill): confirmed by goudan's own one-sentence summary after running the Skill on the new wrapper The 401 negative cases (`Check 14` and `Check 15`) and the sender-filter delegation (`Check 16`) are direct round-trip regressions on the goudan-side `ACPInbox` API; a future change that accidentally bypasses the inherited `_check_loopback` or the no-redirect opener will fail these at smoke time. ## Design compliance - **No credentials.** The wrapper reads the bearer token from the inherited `_acp_client._resolve_token` chain (`$ACP_TOKEN` -> `~/.acp_token` -> `<plugin_root>/.acp_token`). No token default; no token literal anywhere in `scripts/acp_inbox.py`. - **No network beyond loopback.** Every outbound call goes through the bundled client's `_OPENER` (loopback-only, no redirects). The wrapper's `base_url` parameter is validated against the inherited `_check_loopback` at construction; a non-loopback URL raises `ACPError` before any HTTP call. - **No telemetry.** No `print` of message content, token, or base URL. CLI output is limited to the documented `--action` results. - **No third-party services.** Stdlib only (`urllib`, `json`, `os`, `sys`, `time`, `pathlib`). No `pip install`, no `npm install`, no external SDK. The wrapper imports `client/_acp_client` from the same Plugin. - **No hardcoded paths.** The wrapper resolves the plugin root through `$ACP_PLUGIN_ROOT` (set automatically by the Plugin runtime) with a `__file__`-based fallback. Verified by `Check 10` and `Check 11` of the smoke test. - **Single opener.** The wrapper does not import `urllib.request` and does not call `urlopen` directly; all HTTP goes through `_acp_client._request` (which uses `_OPENER`). A future change that introduces a parallel `urllib` path will be caught by `Check 8` of the smoke test. - **`localhost` is refused.** The inherited `_ALLOWED_HOSTS` is the round-5 amendment literal-IP allow-list (`{127.0.0.1, ::1, [::1]}`). `Check 6` of the smoke test asserts `'http://localhost:9999'` is refused at the construction-time loopback check. - **Token is never logged / echoed.** The CLI mode prints the bearer token's first 4 bytes? No, it doesn't -- the CLI prints `OK -- base_url ...` (the URL, not the token) and the message IDs. No `print(token)` anywhere. ## Notes for the reviewer - This commit was prepared on a separate branch (`add-acp-inbox-bridge-skill`) on top of PR MiniMax-AI#3 head `07c6358`. It does not modify any file that PR MiniMax-AI#3's round-1 through round-5 reviews touched. - The `goudan_inbox_responder.py` daemon mentioned in earlier `~/.openclaw/` paths is **not** part of this Plugin. It is the user's side daemon; this Plugin's goudan-side Skill is a separate, self-contained wrapper that the daemon can `import` if desired. - The bundled client's `inbox_*` helpers do not accept a per-call `base_url` (they read `$ACP_BASE_URL` or fall back to `DEFAULT_BASE_URL`). The wrapper's `base_url` parameter is therefore a *fail-fast validation* on construction; the actual HTTP base URL is configured via the environment. This is documented in the wrapper docstring and in the SKILL.md "Setup" section.
hetaoBackend
left a comment
There was a problem hiding this comment.
Current head a536628 passes the token-enabled wrapper smoke (36/36), the existing bridge smoke (24/24), no-redirect, compile and repository validation. Two public wrapper parameters do not work as documented: scripts/acp_inbox.py:117-123 validates and stores ACPInbox(base_url=...), but every method delegates to _acp_client.inbox_*, which reads $ACP_BASE_URL instead of self.base_url; an explicit constructor endpoint can therefore be ignored. Also ACPInbox.read() documents read(limit=...) at lines 168-180 but has no limit parameter and never forwards one, even though _acp_client.inbox_read supports it. Please make the wrapper endpoint and limit parameters effective (or remove them from the public contract) and add delegation tests that use different constructor/env URLs and assert the forwarded limit. [code]smith is SKIPPED.
Add goudan-side acp-inbox-bridge skill (v0.3.0)
What
This PR adds the goudan (OpenClaw main agent) perspective to the
bridge that the mavis side already drives. The v0.2.0 release exposed
acp-collab(mavis / MiniMax Code) andacp-task-dispatch(mavisdispatch); the goudan-side companion was deliberately deferred to a
follow-up so the review surface for each PR stays small.
This PR is built on top of PR #3 (
add-openclaw-acp-bridge,head
07c6358), which hetaoBackend approved on 2026-09-01 but hasnot yet been merged. The diff below includes PR #3's content; the
reviewer should focus on the 5 new files (the goudan-side additions)
plus the metadata changes in
plugin.json/README.md. Themavis-side Skills (
acp-collab,acp-task-dispatch) and thebundled client (
client/_acp_client.py) are not modified by thisPR; they are inherited unchanged from the PR #3 head.
The new content is:
skills/acp-inbox-bridge/SKILL.mdsender="goudan")scripts/acp_inbox.pyACPInbox) overclient/_acp_client.inbox_*scripts/test_inbox_goudan.pyplugin.json0.2.0->0.3.0, description mentions goudan-sideREADME.mdWhy
PR #3 reviewer noted in the round-5 approval that the goudan-side was
out of scope and should come as a follow-up. This PR is that
follow-up. Keeping the PR small makes review tractable: this commit
touches only NEW files plus the two pieces of metadata (
plugin.json,README.md) that have to reflect the new skill. None of the filesthat the round-1 through round-5 reviews in PR #3 touched are
modified by this commit; it cannot regress them.
How to read the diff
On GitHub, the file-level diff includes both PR #3 changes (already
approved) and the goudan-side additions (this PR). The reviewer
should:
look at PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 head
07c6358; this PR inherits that contractunchanged.
skills/acp-inbox-bridge/SKILL.mdscripts/acp_inbox.pyscripts/test_inbox_goudan.pyplugin.jsonandREADME.mdValidation
python scripts/test_inbox_goudan.py(live, stub-backed):36/36 PASS, 0 FAIL, 0 SKIP on Windows + Python 3.14.
Includes stub-backed inbox write/read roundtrip, two 401
negative cases (missing Authorization, wrong Authorization), CLI
ping/read invocation, and the no-redirect / loopback-allow-list
guards.
python scripts/test_inbox_goudan.py(CI mode,SMOKE_SKIP_LIVE=1): 21/21 PASS, 0 FAIL, 10 SKIP. The 10skipped checks are the live-server ones; the static checks
(sender defaults, no-redirect opener, loopback guard, token
resolution, no hardcoded paths, ACP_PLUGIN_ROOT / file
resolution, mock-based delegation) all pass without a live
server.
python scripts/smoke.py(PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3's mavis-side smoke, regressioncheck): 26/26 PASS, 0 FAIL. Zero regression on the mavis
side. The new
SKILL.mdis automatically picked up byCheck 7("Plugin SKILL.md files resolve the plugin rootsafely") and passes.
python scripts/test_no_redirect.py(PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3's no-redirectregression): 1/1 PASS. The no-redirect guarantee still
holds for the underlying
client/_acp_client; the wrapperinherits it.
node scripts/validate.mjs(repo validator): no FAIL onskills/acp-inbox-bridge/SKILL.md. The validator does stillflag the pre-existing
skills/acp-collab/SKILL.md(CRLFissue from the PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 round-1 fix that did not fully land);
this PR does NOT touch acp-collab.
python -c "import _acp_client; print('OK')": passes. Thewrapper imports cleanly with the bundled client on the path.
The bundle was run locally on Windows + Python 3.14. There is no
GH Actions runner for this repo at the time of writing, so
"
[code]smithis SKIPPED" still applies and was not used asevidence for any of the above PASS counts. Same posture as
PR #3's R6.
Test evidence
End-to-end on Windows + Python 3.14, 2026-09-01 (Asia/Shanghai):
~60 s for 32 tool calls + Chinese summary
in inbox
OpenClaw builtin) from acp-inbox-bridge (peer chat, this
Skill): confirmed by goudan's own one-sentence summary
after running the Skill on the new wrapper
The 401 negative cases (
Check 14andCheck 15) and thesender-filter delegation (
Check 16) are direct round-tripregressions on the goudan-side
ACPInboxAPI; a future changethat accidentally bypasses the inherited
_check_loopbackorthe no-redirect opener will fail these at smoke time.
Design compliance
the inherited
_acp_client._resolve_tokenchain(
$ACP_TOKEN->~/.acp_token-><plugin_root>/.acp_token).No token default; no token literal anywhere in
scripts/acp_inbox.py.through the bundled client's
_OPENER(loopback-only, noredirects). The wrapper's
base_urlparameter is validatedagainst the inherited
_check_loopbackat construction; anon-loopback URL raises
ACPErrorbefore any HTTP call.printof message content, token, orbase URL. CLI output is limited to the documented
--actionresults.
urllib,json,os,sys,time,pathlib). Nopip install, nonpm install, no external SDK. The wrapper importsclient/_acp_clientfrom the same Plugin.root through
$ACP_PLUGIN_ROOT(set automatically by thePlugin runtime) with a
__file__-based fallback. Verifiedby
Check 10andCheck 11of the smoke test.urllib.requestand does not callurlopendirectly; allHTTP goes through
_acp_client._request(which uses_OPENER). A future change that introduces a parallelurllibpath will be caught byCheck 8of the smoketest.
localhostis refused. The inherited_ALLOWED_HOSTSis the round-5 amendment literal-IP allow-list
(
{127.0.0.1, ::1, [::1]}).Check 6of the smoke testasserts
'http://localhost:9999'is refused at theconstruction-time loopback check.
the URL, not the token. No
print(token)anywhere.Notes for the reviewer
add-acp-inbox-bridge-skillin
antianqi/MiniMax-Code-Plugins-1on top of PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 head07c6358. It does not modify any file that PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3'sround-1 through round-5 reviews touched.
goudan_inbox_responder.pydaemon mentioned inearlier
~/.openclaw/paths is not part of thisPlugin. It is the user's side daemon; this Plugin's
goudan-side Skill is a separate, self-contained wrapper
that the daemon can
importif desired.inbox_*helpers do not accept aper-call
base_url(they read$ACP_BASE_URLor fallback to
DEFAULT_BASE_URL). The wrapper'sbase_urlparameter is therefore a fail-fast validation on
construction; the actual HTTP base URL is configured
via the environment. This is documented in the wrapper
docstring and in the SKILL.md "Setup" section.
mainincludes PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3's6 review rounds. The reviewer's focused diff is the
one against
07c6358(PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 head).Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.