Repository navigation
Commit a7f241b
authored
Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code (#3)
* plugins(antianqi/openclaw-acp-bridge): add OpenClaw ACP peer bridge v0.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.
* fix: refuse non-loopback ACP_BASE_URL in smoke test (review #4)
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)
* fix: add CI workflow and stop the smoke test from failing offline (review #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
* fix: align auth docs with the SDK's actual contract (review #3)
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.
* fix: align Skills and README with the actual SDK (review #1 + #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 #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.
* fix(security): refuse HTTP redirects on token-bearing requests + real 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)
* fix: bundle the HTTP client so the runtime path is the reviewed path (review #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.
* fix(security): route health() through the bundled request primitive + 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.
* fix(security): drop 'localhost' from the loopback allow-list (round-5)
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.
---------
Co-authored-by: antianqi <antianqi@users.noreply.github.com>1 parent 2544eb6 commit a7f241b
11 files changed
Lines changed: 2144 additions & 0 deletions
File tree
- .github/workflows
- plugins/antianqi/openclaw-acp-bridge
- client
- scripts
- skills
- acp-collab
- acp-task-dispatch
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
0 commit comments