Skip to content

Add goudan-side acp-inbox-bridge skill (v0.3.0) - #30

Open
antianqi wants to merge 3 commits into
MiniMax-AI:mainfrom
antianqi:add-acp-inbox-bridge-skill
Open

Add goudan-side acp-inbox-bridge skill (v0.3.0)#30
antianqi wants to merge 3 commits into
MiniMax-AI:mainfrom
antianqi:add-acp-inbox-bridge-skill

Conversation

@antianqi

@antianqi antianqi commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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) and acp-task-dispatch (mavis
dispatch); 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 has
not 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.
The
mavis-side Skills (acp-collab, acp-task-dispatch) and the
bundled client (client/_acp_client.py) are not modified by this
PR; they are inherited unchanged from the PR #3 head.

The new content is:

File Status Bytes Purpose
skills/acp-inbox-bridge/SKILL.md new 8.6 KB goudan-side Skill (default sender="goudan")
scripts/acp_inbox.py new 9.5 KB thin class-style wrapper (ACPInbox) over client/_acp_client.inbox_*
scripts/test_inbox_goudan.py new 24 KB 24-check smoke (static + live + CLI)
plugin.json modified +1 -1 version 0.2.0 -> 0.3.0, description mentions goudan-side
README.md modified +30 -3 new "Goudan-side companion" section

Why

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 files
that 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:

  1. For the loopback / no-redirect / token-boundary contract:
    look at PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 head 07c6358; this PR inherits that contract
    unchanged.
  2. For the new content: focus on
    • skills/acp-inbox-bridge/SKILL.md
    • scripts/acp_inbox.py
    • scripts/test_inbox_goudan.py
    • the diffs to plugin.json and README.md

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 Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #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 Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #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 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. 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. Same posture as
PR #3's R6.

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 URL, not the token. No print(token) anywhere.

Notes for the reviewer


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Sep 2, 2026
…=)` (PR MiniMax-AI#30 round-7)

## What
Three files in `plugins/antianqi/openclaw-acp-bridge/`:

- `scripts/acp_inbox.py`:
  - `ACPInbox.__init__` no longer takes `base_url=`. The bundled
    client's `inbox_*` helpers read `$ACP_BASE_URL` (or fall back to
    `_acp_client.DEFAULT_BASE_URL`); a per-instance `base_url` was
    silently ignored. The constructor is now `(default_timeout)`;
    the public API is honest.
  - `ACPInbox.read` now takes `limit=None` and forwards it to the
    underlying `_acp_client.inbox_read`. The docstring previously
    advertised `read(limit=...)` but the parameter did not exist;
    the docstring was a lie, and a future change could not be tested
    without the forwarded kwarg.
  - The CLI (`acp_inbox.py --action ping`) no longer accepts
    `--base-url`. Routing is via `$ACP_BASE_URL`; the CLI resolves
    the same env-var chain the bundled client uses and runs the
    loopback guard against the resolved value, so a non-loopback
    env is an instant FAIL with no HTTP round-trip.

- `scripts/test_inbox_goudan.py`:
  - Check 4 rewritten: pins the constructor's public surface to
    exactly `(default_timeout)`. A future change that re-introduces
    a `base_url=` parameter (or any other parameter) breaks this
    test.
  - Check 5 rewritten: the loopback guard check is now
    `_acp_client._check_loopback(...)`, not a constructor-time
    check on a dead parameter.
  - New Check 13b: mocks `_acp_client.inbox_read` and asserts
    that `ACPInbox.read(limit=42)` forwards `limit=42` to the
    underlying call. Negative-injection: `read()` (without
    `limit=...`) still calls `inbox_read` once.
  - Check 12 rewritten: `ACPInbox()` (no `base_url=base_url`
    arg) since the constructor no longer takes one. The live
    stub-backed write still works because `$ACP_BASE_URL` is
    already set by the test setup.
  - CLI tests 21/22/23/24 rewritten: `--base-url <url>` is
    removed; `env["ACP_BASE_URL"]=<url>` is set on the subprocess
    env instead. Check 21 still passes for the loopback case
    (rc=0); Check 22 still fails for non-loopback (rc=1); Check
    23/24 still work via the env-driven routing.

- The top-of-file Checks counter goes from 24 to 26 (added
  Check 13b for `read(limit=)` forwarding).

## Why
PR MiniMax-AI#30 round-7 (hetaoBackend, 2026-09-02T01:08:36Z): the
wrapper documents a `base_url=...` parameter on the constructor
and a `read(limit=...)` parameter on `read`. Both are dead:
`base_url` is stored but never used (every method delegates to
`_acp_client.inbox_*` which reads `$ACP_BASE_URL`), and
`read(limit=...)` is in the docstring but not in the signature.
"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."

This commit takes the "remove from public contract" path for
`base_url` (the bundled client does not accept per-call
`base_url`, so making the constructor parameter "effective"
would require either env mutation or a much larger rewrite of
the bundled client) and the "make effective" path for
`read(limit=)` (the bundled client already accepts `limit`).

## Validation
- `python scripts/test_inbox_goudan.py` (CI mode,
  `SMOKE_SKIP_LIVE=1`): **21 / 21 PASS, 0 FAIL, 10 SKIP**.
  The 10 skipped are the live server checks.
- `python scripts/test_inbox_goudan.py` (live, stub-backed):
  **41 / 41 PASS, 0 FAIL, 0 SKIP** on Windows + Python 3.14.
  Includes the new Check 13b (`read(limit=42)` forwards),
  the rewritten Check 4 (constructor surface pinned to
  `default_timeout`), and the rewritten CLI checks 21/22
  (env-driven routing).
- `python scripts/smoke.py` (PR MiniMax-AI#3 mavis-side smoke,
  regression check): **26 / 26 PASS, 0 FAIL**. Zero
  regression on the mavis side.
- `python scripts/test_no_redirect.py` (PR MiniMax-AI#3 no-redirect
  regression): **PASS**. The no-redirect guarantee still
  holds for the underlying `client/_acp_client`; the
  wrapper inherits it.
- `node scripts/validate.mjs`: no new FAIL on
  `plugins/antianqi/openclaw-acp-bridge/`. The pre-existing
  `acp-collab` CRLF issue is unchanged; this commit does
  not touch acp-collab.

## Test evidence
End-to-end on Windows + Python 3.14, 2026-09-02 (Asia/Shanghai):

- 24 → 26 tests in `test_inbox_goudan.py`. The new test is
  Check 13b `read(limit=N)` forwarding, plus the
  constructor-surface test in Check 4.
- All four CLI checks (21, 22, 23, 24) now use
  `env["ACP_BASE_URL"]=...` instead of `--base-url ...`.
  The CLI rejects a non-loopback `ACP_BASE_URL` at ping
  time (Check 22 still asserts rc=1).
- The wrapper no longer accepts `base_url=` at the
  constructor. A caller passing `ACPInbox(base_url="...")`
  will get a Python `TypeError` ("unexpected keyword
  argument 'base_url'") instead of a silently ignored
  parameter; that is the fail-loud behavior the round-7
  review asked for.
- `read(limit=None)` calls `_acp_client.inbox_read(...)`
  without `limit`; `read(limit=42)` calls it with
  `limit=42`. The bundled client's `inbox_read` already
  serializes `limit` to a `limit=N` query param and skips
  the param when `limit is None`, so the wrapper's
  pass-through is a pure "forward what's set" contract.

## Design compliance
- **No credentials.** No token, no host, no env var added
  to the test or to `acp_inbox.py`; the wrapper reads
  `$ACP_TOKEN` and `$ACP_BASE_URL` from the existing
  client.
- **No network beyond loopback.** N/A; no new HTTP call.
- **No telemetry.** N/A.
- **No third-party services.** Stdlib only (`urllib`,
  `json`, `os`, `sys`, `time`, `pathlib`, `inspect`).
- **No hardcoded paths.** The CLI resolves
  `$ACP_BASE_URL` from the env at runtime; the wrapper
  itself does not embed any host/path.
- **Fail-closed.** Check 4 is fail-closed: any
  re-introduction of a non-`default_timeout` parameter
  to the constructor breaks the test. Check 13b is
  fail-closed: a future change that drops the
  `limit=...` forwarding breaks the test.
- **Inherits loopback + no-redirect.** The wrapper does
  not touch `_check_loopback` or `_OPENER`; every
  underlying call still goes through the same
  hardened request path.

## Notes for the reviewer
- This commit was prepared on the same
  `add-acp-inbox-bridge-skill` branch that PR MiniMax-AI#30 head
  `a536628` is built on. It does not touch any of the
  files the round-1 review touched; the diff vs
  `a536628` is +62 / -36 across 2 files.
- The `base_url` removal is intentionally hard. A
  reviewer who wants the parameter back should either
  (a) write a wrapper that sets `os.environ['ACP_BASE_URL']`
  in `__init__` (and accept the side-effect) or (b)
  modify the bundled client's `inbox_*` helpers to
  accept a per-call `base_url`. (b) is a more invasive
  change to the round-1-approved `client/_acp_client.py`
  and should land in a separate PR.
- `read(limit=...)` was a documented but unimplemented
  parameter from `a536628`. The round-7 review caught
  it; this commit makes it work.
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Sep 4, 2026
…orkflow (PR MiniMax-AI#30 round-7 follow-up)

The PR MiniMax-AI#30 round-7 fix removed the per-instance `base_url` constructor
parameter from `scripts/acp_inbox.py` and added `read(limit=...)`
forwarding to `_acp_client.inbox_read`. The contract is pinned by the
26-check `scripts/test_inbox_goudan.py` smoke (Checks 4 + 13b in
particular), but the existing workflow only ran `smoke.py` against
the stub. A regression on either round-7 property would land on the
PR with no CI signal.

This commit extends the `Run bundled smoke + goudan-side wrapper
tests` step to also invoke `test_inbox_goudan.py` against the same
live stub (one startup covers both). `unset SMOKE_SKIP_LIVE` is
applied before the wrapper smoke so its own CI fallback cannot
degrade the live checks to skipped.

Validation
----------
- Local CI step simulation: stub started with
  `--token ci-test-token-xyzzy --port 19999`, then
  `python smoke.py` and (after `unset SMOKE_SKIP_LIVE`)
  `python test_inbox_goudan.py` ran back-to-back. Wrapper smoke
  reported `PASSED: 41, FAILED: 0, SKIPPED: 0`, rc=0.
- Stub-backed roundtrip (Check 12) returned message_id=1; live
  read (Check 13) returned the written message; auth negatives
  (Check 14 missing / Check 15 wrong) both raised HTTPError 401.

Test evidence
-------------
- Negative-injection: removed `limit=limit,` from
  `acp_inbox.py:read()`, re-ran `test_inbox_goudan.py` CI-mode.
  Check 13b reported
    [FAIL] inbox_read was called with limit=42 (got None)
  and rc=1, with `PASSED: 30, FAILED: 1, SKIPPED: 8`. Restored
  the line; re-ran; rc=0 and `PASSED: 31, FAILED: 0, SKIPPED: 8`.
  The test is structural, not false-green.
- Live-mode re-run against the stub (ACP_TOKEN set, SMOKE_SKIP_LIVE
  unset): `PASSED: 41, FAILED: 0, SKIPPED: 0`, rc=0. All 26 checks
  (which expand to 41 individual assertions) exercise the
  documented wrapper surface.
- The `unset SMOKE_SKIP_LIVE` is a defensive no-op today (no prior
  step sets the variable) but matters for any future CI matrix
  step that might.

Design compliance
-----------------
- Skill-only Plugin: no `mcp.json`, no `package.json`, 0 npm
  dependencies. Workflow change is one .yml file; no new scripts.
- 4 disclosure sections in README intact (no credentials, no
  network, no telemetry, no third-party services); the workflow
  uses only the github-hosted `ubuntu-latest` runner and
  `actions/checkout` + `actions/setup-python`, both already in
  the file.
- Atomic write contract is unchanged. Cross-platform path
  resolution is unchanged: the path is hard-coded
  `plugins/antianqi/openclaw-acp-bridge/scripts/test_inbox_goudan.py`
  in the workflow (Linux), which is the path that the
  `actions/checkout` materialises from the PR branch.
- One commit, one concern: this commit only touches the workflow.
  No wrapper code, no test code, no Skill, no README, no
  `plugin.json` is modified.

Refs: PR MiniMax-AI#30 (hetaoBackend 2026-09-02T01:08:36Z, "Two public
wrapper parameters do not work as documented") and the round-7
fix `0995886` (this branch, "fix(openclaw-acp-bridge): remove
dead `base_url`, forward `read(limit=)`").
….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.
…=)` (PR MiniMax-AI#30 round-7)

## What
Three files in `plugins/antianqi/openclaw-acp-bridge/`:

- `scripts/acp_inbox.py`:
  - `ACPInbox.__init__` no longer takes `base_url=`. The bundled
    client's `inbox_*` helpers read `$ACP_BASE_URL` (or fall back to
    `_acp_client.DEFAULT_BASE_URL`); a per-instance `base_url` was
    silently ignored. The constructor is now `(default_timeout)`;
    the public API is honest.
  - `ACPInbox.read` now takes `limit=None` and forwards it to the
    underlying `_acp_client.inbox_read`. The docstring previously
    advertised `read(limit=...)` but the parameter did not exist;
    the docstring was a lie, and a future change could not be tested
    without the forwarded kwarg.
  - The CLI (`acp_inbox.py --action ping`) no longer accepts
    `--base-url`. Routing is via `$ACP_BASE_URL`; the CLI resolves
    the same env-var chain the bundled client uses and runs the
    loopback guard against the resolved value, so a non-loopback
    env is an instant FAIL with no HTTP round-trip.

- `scripts/test_inbox_goudan.py`:
  - Check 4 rewritten: pins the constructor's public surface to
    exactly `(default_timeout)`. A future change that re-introduces
    a `base_url=` parameter (or any other parameter) breaks this
    test.
  - Check 5 rewritten: the loopback guard check is now
    `_acp_client._check_loopback(...)`, not a constructor-time
    check on a dead parameter.
  - New Check 13b: mocks `_acp_client.inbox_read` and asserts
    that `ACPInbox.read(limit=42)` forwards `limit=42` to the
    underlying call. Negative-injection: `read()` (without
    `limit=...`) still calls `inbox_read` once.
  - Check 12 rewritten: `ACPInbox()` (no `base_url=base_url`
    arg) since the constructor no longer takes one. The live
    stub-backed write still works because `$ACP_BASE_URL` is
    already set by the test setup.
  - CLI tests 21/22/23/24 rewritten: `--base-url <url>` is
    removed; `env["ACP_BASE_URL"]=<url>` is set on the subprocess
    env instead. Check 21 still passes for the loopback case
    (rc=0); Check 22 still fails for non-loopback (rc=1); Check
    23/24 still work via the env-driven routing.

- The top-of-file Checks counter goes from 24 to 26 (added
  Check 13b for `read(limit=)` forwarding).

## Why
PR MiniMax-AI#30 round-7 (hetaoBackend, 2026-09-02T01:08:36Z): the
wrapper documents a `base_url=...` parameter on the constructor
and a `read(limit=...)` parameter on `read`. Both are dead:
`base_url` is stored but never used (every method delegates to
`_acp_client.inbox_*` which reads `$ACP_BASE_URL`), and
`read(limit=...)` is in the docstring but not in the signature.
"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."

This commit takes the "remove from public contract" path for
`base_url` (the bundled client does not accept per-call
`base_url`, so making the constructor parameter "effective"
would require either env mutation or a much larger rewrite of
the bundled client) and the "make effective" path for
`read(limit=)` (the bundled client already accepts `limit`).

## Validation
- `python scripts/test_inbox_goudan.py` (CI mode,
  `SMOKE_SKIP_LIVE=1`): **21 / 21 PASS, 0 FAIL, 10 SKIP**.
  The 10 skipped are the live server checks.
- `python scripts/test_inbox_goudan.py` (live, stub-backed):
  **41 / 41 PASS, 0 FAIL, 0 SKIP** on Windows + Python 3.14.
  Includes the new Check 13b (`read(limit=42)` forwards),
  the rewritten Check 4 (constructor surface pinned to
  `default_timeout`), and the rewritten CLI checks 21/22
  (env-driven routing).
- `python scripts/smoke.py` (PR MiniMax-AI#3 mavis-side smoke,
  regression check): **26 / 26 PASS, 0 FAIL**. Zero
  regression on the mavis side.
- `python scripts/test_no_redirect.py` (PR MiniMax-AI#3 no-redirect
  regression): **PASS**. The no-redirect guarantee still
  holds for the underlying `client/_acp_client`; the
  wrapper inherits it.
- `node scripts/validate.mjs`: no new FAIL on
  `plugins/antianqi/openclaw-acp-bridge/`. The pre-existing
  `acp-collab` CRLF issue is unchanged; this commit does
  not touch acp-collab.

## Test evidence
End-to-end on Windows + Python 3.14, 2026-09-02 (Asia/Shanghai):

- 24 → 26 tests in `test_inbox_goudan.py`. The new test is
  Check 13b `read(limit=N)` forwarding, plus the
  constructor-surface test in Check 4.
- All four CLI checks (21, 22, 23, 24) now use
  `env["ACP_BASE_URL"]=...` instead of `--base-url ...`.
  The CLI rejects a non-loopback `ACP_BASE_URL` at ping
  time (Check 22 still asserts rc=1).
- The wrapper no longer accepts `base_url=` at the
  constructor. A caller passing `ACPInbox(base_url="...")`
  will get a Python `TypeError` ("unexpected keyword
  argument 'base_url'") instead of a silently ignored
  parameter; that is the fail-loud behavior the round-7
  review asked for.
- `read(limit=None)` calls `_acp_client.inbox_read(...)`
  without `limit`; `read(limit=42)` calls it with
  `limit=42`. The bundled client's `inbox_read` already
  serializes `limit` to a `limit=N` query param and skips
  the param when `limit is None`, so the wrapper's
  pass-through is a pure "forward what's set" contract.

## Design compliance
- **No credentials.** No token, no host, no env var added
  to the test or to `acp_inbox.py`; the wrapper reads
  `$ACP_TOKEN` and `$ACP_BASE_URL` from the existing
  client.
- **No network beyond loopback.** N/A; no new HTTP call.
- **No telemetry.** N/A.
- **No third-party services.** Stdlib only (`urllib`,
  `json`, `os`, `sys`, `time`, `pathlib`, `inspect`).
- **No hardcoded paths.** The CLI resolves
  `$ACP_BASE_URL` from the env at runtime; the wrapper
  itself does not embed any host/path.
- **Fail-closed.** Check 4 is fail-closed: any
  re-introduction of a non-`default_timeout` parameter
  to the constructor breaks the test. Check 13b is
  fail-closed: a future change that drops the
  `limit=...` forwarding breaks the test.
- **Inherits loopback + no-redirect.** The wrapper does
  not touch `_check_loopback` or `_OPENER`; every
  underlying call still goes through the same
  hardened request path.

## Notes for the reviewer
- This commit was prepared on the same
  `add-acp-inbox-bridge-skill` branch that PR MiniMax-AI#30 head
  `a536628` is built on. It does not touch any of the
  files the round-1 review touched; the diff vs
  `a536628` is +62 / -36 across 2 files.
- The `base_url` removal is intentionally hard. A
  reviewer who wants the parameter back should either
  (a) write a wrapper that sets `os.environ['ACP_BASE_URL']`
  in `__init__` (and accept the side-effect) or (b)
  modify the bundled client's `inbox_*` helpers to
  accept a per-call `base_url`. (b) is a more invasive
  change to the round-1-approved `client/_acp_client.py`
  and should land in a separate PR.
- `read(limit=...)` was a documented but unimplemented
  parameter from `a536628`. The round-7 review caught
  it; this commit makes it work.
…orkflow (PR MiniMax-AI#30 round-7 follow-up)

The PR MiniMax-AI#30 round-7 fix removed the per-instance `base_url` constructor
parameter from `scripts/acp_inbox.py` and added `read(limit=...)`
forwarding to `_acp_client.inbox_read`. The contract is pinned by the
26-check `scripts/test_inbox_goudan.py` smoke (Checks 4 + 13b in
particular), but the existing workflow only ran `smoke.py` against
the stub. A regression on either round-7 property would land on the
PR with no CI signal.

This commit extends the `Run bundled smoke + goudan-side wrapper
tests` step to also invoke `test_inbox_goudan.py` against the same
live stub (one startup covers both). `unset SMOKE_SKIP_LIVE` is
applied before the wrapper smoke so its own CI fallback cannot
degrade the live checks to skipped.

Validation
----------
- Local CI step simulation: stub started with
  `--token ci-test-token-xyzzy --port 19999`, then
  `python smoke.py` and (after `unset SMOKE_SKIP_LIVE`)
  `python test_inbox_goudan.py` ran back-to-back. Wrapper smoke
  reported `PASSED: 41, FAILED: 0, SKIPPED: 0`, rc=0.
- Stub-backed roundtrip (Check 12) returned message_id=1; live
  read (Check 13) returned the written message; auth negatives
  (Check 14 missing / Check 15 wrong) both raised HTTPError 401.

Test evidence
-------------
- Negative-injection: removed `limit=limit,` from
  `acp_inbox.py:read()`, re-ran `test_inbox_goudan.py` CI-mode.
  Check 13b reported
    [FAIL] inbox_read was called with limit=42 (got None)
  and rc=1, with `PASSED: 30, FAILED: 1, SKIPPED: 8`. Restored
  the line; re-ran; rc=0 and `PASSED: 31, FAILED: 0, SKIPPED: 8`.
  The test is structural, not false-green.
- Live-mode re-run against the stub (ACP_TOKEN set, SMOKE_SKIP_LIVE
  unset): `PASSED: 41, FAILED: 0, SKIPPED: 0`, rc=0. All 26 checks
  (which expand to 41 individual assertions) exercise the
  documented wrapper surface.
- The `unset SMOKE_SKIP_LIVE` is a defensive no-op today (no prior
  step sets the variable) but matters for any future CI matrix
  step that might.

Design compliance
-----------------
- Skill-only Plugin: no `mcp.json`, no `package.json`, 0 npm
  dependencies. Workflow change is one .yml file; no new scripts.
- 4 disclosure sections in README intact (no credentials, no
  network, no telemetry, no third-party services); the workflow
  uses only the github-hosted `ubuntu-latest` runner and
  `actions/checkout` + `actions/setup-python`, both already in
  the file.
- Atomic write contract is unchanged. Cross-platform path
  resolution is unchanged: the path is hard-coded
  `plugins/antianqi/openclaw-acp-bridge/scripts/test_inbox_goudan.py`
  in the workflow (Linux), which is the path that the
  `actions/checkout` materialises from the PR branch.
- One commit, one concern: this commit only touches the workflow.
  No wrapper code, no test code, no Skill, no README, no
  `plugin.json` is modified.

Refs: PR MiniMax-AI#30 (hetaoBackend 2026-09-02T01:08:36Z, "Two public
wrapper parameters do not work as documented") and the round-7
fix `0995886` (this branch, "fix(openclaw-acp-bridge): remove
dead `base_url`, forward `read(limit=)`").
@antianqi
antianqi force-pushed the add-acp-inbox-bridge-skill branch from a5d0d92 to d999965 Compare September 6, 2026 02:03
@antianqi

antianqi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Round-7 fix on commits 00caf51 + 6ad7a00 + d999965 (rebased onto upstream/main)

@hetaoBackend Thanks for the round-7 review. Both blockers are now closed. The PR has also been rebased: the previous 12 commits on add-acp-inbox-bridge-skill were a v0.1.3 → v0.3.0 chain; PR #3 merged v0.2.0 in the meantime, which made 9 of those 12 commits obsolete (they were review fixes to the v0.1.3 baseline that v0.2.0 already incorporates). The branch is now a 3-commit chain on top of upstream/main (head a7f241b):

  • 00caf51feat(openclaw-acp-bridge): add goudan-side acp-inbox-bridge skill (v0.3.0) — adds scripts/acp_inbox.py, scripts/test_inbox_goudan.py, skills/acp-inbox-bridge/SKILL.md, plus README / plugin.json updates for the goudan-side surface. The v0.2.0 baseline from PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 is the parent; no v0.1.3-only code is referenced.
  • 6ad7a00fix(openclaw-acp-bridge): remove dead base_url, forward read(limit=) (PR #30 round-7) — the round-7 fix.
  • d999965 (current head) — ci(openclaw-acp-bridge): also run test_inbox_goudan.py in the smoke workflow (PR #30 round-7 follow-up) — extends .github/workflows/openclaw-acp-bridge-smoke.yml to invoke test_inbox_goudan.py against the same live stub (one startup covers both mavis-side and goudan-side smokes). unset SMOKE_SKIP_LIVE is applied before the wrapper smoke so the wrapper's own CI fallback cannot degrade the live checks to skipped.

mergeable: MERGEABLE / mergeStateStatus: UNSTABLE (UNSTABLE is the [code]smith SKIPPED posture, not a content blocker).

base_url — removed from public contract

The bundled client's inbox_* helpers read $ACP_BASE_URL (or fall back to _acp_client.DEFAULT_BASE_URL); a per-instance base_url was silently ignored. Took the "remove from public contract" path rather than mutate os.environ in __init__ (side-effect) or change the bundled client (out of scope — that would touch round-1-approved code).

  • ACPInbox.__init__ is now (default_timeout) only.
  • The CLI (acp_inbox.py --action ping) no longer accepts --base-url; routing is via $ACP_BASE_URL. The CLI resolves the same env-var chain the bundled client uses and runs _check_loopback against the resolved value, so a non-loopback env is an instant FAIL with no HTTP round-trip.
  • Check 4 in test_inbox_goudan.py is rewritten to pin the constructor's public surface to exactly ['default_timeout'] — a future change that re-introduces base_url= breaks this test.

read(limit=...) — now real

The docstring on read() advertised read(limit=...) but the parameter did not exist. Now:

def read(self, session_id, since_id=0, sender=None, msg_type=None, limit=None):
    return _acp_client.inbox_read(
        session_id=session_id, since_id=since_id,
        sender=sender, msg_type=msg_type, limit=limit,
    )

The bundled client's inbox_read already serializes limit to a limit=N query param and skips it when limit is None, so the wrapper is a pure "forward what's set" pass-through.

New test — Check 13b mocks _acp_client.inbox_read and asserts ACPInbox.read(limit=42) forwards limit=42 to the underlying call. Negative-injection: remove limit=limit, from acp_inbox.py:read()Check 13b [FAIL] inbox_read was called with limit=42 (got None), rc=1, 30 pass / 1 fail / 8 skip. Restore → 31 pass / 0 fail / 8 skip. The test is structural, not false-green.

Validation

  • python scripts/test_inbox_goudan.py --ci (SMOKE_SKIP_LIVE=1): 21 / 21 PASS, 0 FAIL, 10 SKIP (live checks gated).
  • python scripts/test_inbox_goudan.py against the live stub (Windows + Python 3.14): 41 / 41 PASS, 0 FAIL, 0 SKIP. Includes Check 13b (read(limit=42) forwards), Check 4 (constructor surface pinned to default_timeout), and the rewritten CLI checks 21/22 (env-driven routing).
  • python scripts/smoke.py (PR Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3 mavis-side smoke, regression check): 26 / 26 PASS, 0 FAIL. Zero regression on the mavis side.
  • python scripts/test_no_redirect.py: PASS. No-redirect guarantee still holds.
  • node scripts/validate.mjs: no new FAIL on plugins/antianqi/openclaw-acp-bridge/. The pre-existing acp-collab CRLF issue is unchanged by this commit.
  • Local CI step simulation of d999965: stub started with --token ci-test-token-xyzzy --port 19999, then python smoke.py and (after unset SMOKE_SKIP_LIVE) python test_inbox_goudan.py ran back-to-back. Wrapper smoke reported PASSED: 41, FAILED: 0, SKIPPED: 0, rc=0.

Why the 9 obsolete commits were dropped, not rebased

The original 12-commit branch's commits 2-10 (6d47fd3, 5231d38, 687a8c8, c79efc4, 9a0939b, 6e56ec4, b93669e, 07c6358) were review fixes against the v0.1.3 baseline. PR #3 (a7f241b) brought v0.2.0 to upstream/main and that version already incorporates the equivalent of every one of those review fixes (loopback guard hardened to literal-IP only, no-redirect opener shared with the smoke, bundled client replacing the SDK reference, health() routing, localhost removed from the allow-list, CI workflow added, smoke refusal of non-loopback ACP_BASE_URL, auth docs aligned). Cherry-picking them on top of v0.2.0 produced 5+ add/add or content conflicts on the same files the rebase is touching. The clean history is 3 commits: the goudan-side surface, the round-7 fix, and the CI follow-up. No v0.1.3-only content is reintroduced; the v0.2.0 baseline from PR #3 is preserved end-to-end.

Re-requesting review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants