Follow the gateway's whole-API contract: no redirects, one pinned policy (0.3.3) #9

Merged
claude merged 5 commits from oss-oo/gateway-contract-align into main 2026-10-27 18:58:41 +00:00
Member

What

The laboratory's MCP gateway is getting this plugin's whole-API contract (api_spec, api_read, api_write) with a copy of this guard in agent-gateway/api_guard.py. This change brings the plugin in line with that copy and hardens what the review found on the way.

  1. No redirect is followed, for any method or 3xx status. Up to 0.3.2, urllib followed same-origin redirects. That had four effects:

    • it resent the token;
    • the guard never saw the new path;
    • a redirected POST turned into a GET that was reported as HTTP 200 POST;
    • other 3xx answers counted as success.

    Now a resource the laboratory moved is reported with its new API path ("call again with that path or the new repository name"), and any other redirect only as a fact. urllib does not even read the Location any more, and a malformed one is reported, not raised.

  2. One pinned policy. policy_sha256() digests the decision tables the way the gateway does. test_the_guard_policy_is_the_gateways pins b4e86f74…, the value the gateway pins. lab_api_spec reports it. A further test pins that no write route carries regex flags, which the digest does not cover.

  3. The token never shows. Should the laboratory send it back, the core replaces it as the answer arrives, before anything is cut or printed. That covers bodies, titles, errors and redirect notes, and a token spelled with JSON escapes too.

    • Server text in errors is bounded (300 characters, as in the gateway) and printable.
    • A keyring entry that is not an API token is refused without being quoted: under 16 characters, or containing a space, a line break or non-ASCII.
    • A request http.client will not write is reported with a fixed message, because its error would quote the header.
  4. Clean errors, never a traceback, for:

    • a cut answer;
    • JSON nested too deeply (deep answers are shown compactly);
    • an API description that is not one, with odd entries skipped as in the gateway and a best-effort cache of the bytes as read.
  5. Query values without a JSON spelling (NaN, infinities, numbers too long to print) are refused before anything is sent, by api_call itself.

Version 0.3.3. The README and the skill say what a moved resource looks like and what the digest covers.

Evidence

  • Gate (sh scripts/check.sh): exit 0. It runs 460 Python tests, the asset check, claude plugin validate and 18 mod tests. CI is green on every head.
  • Review: a fresh read-only subagent reviewed the change in three rounds plus a final look.
    • Each round's findings were fixed in their own commit: a malformed Location traceback that could carry the token, the token surviving a cut, deep or odd answers, and test gaps.
    • Verdict: mergeable.
  • Mutation check: the whole tree with a green baseline. Each of 42 deliberate breakages fails the suite, among them:
    • 0.3.2's same-origin following;
    • urllib reading the Location;
    • each redirect override dropped;
    • the token replaced too late or only once;
    • a widened write route;
    • a case-sensitive credential area;
    • a flag on a write route;
    • an unchecked keyring entry.
  • Live, as claude against https://oss-oo.io on 2026-10-27. Three paths: plugin 0.3.2, this branch, and the gateway's own handler, run locally from its branch.
    • Decisions: over the live API description (519 operations) every guard decision is equal. Both allow 216 reads and refuse 50, and allow 63 writes and refuse 190.
    • Data: the same reads give the same data digests on all three paths.
    • Refusals: hooks, admin, a repository delete and a PATCH of private are refused by all three without a request.
    • Writes: PUT of the same topics answers 204 on all three paths, and the topics are unchanged afterwards.
    • Redirect: OSS-OO/forge was renamed and answers 301.
      • 0.3.2 returned the other repository's data for GET /repos/OSS-OO/forge.
      • 0.3.2 reported POST /repos/OSS-OO/forge/issues (empty body) as HTTP 200, with an issue list.
      • This branch and the gateway report the move.
    • CI reader: this branch's CI reader needs no redirect. A PR's runs, jobs, job detail, job log, commit status and /user all answered 200.

Not in this change

Once the gateway is deployed, this plugin's lab_api_* and the gateway's api_* overlap. Both enforce the same policy (same digest). The owner decides whether to keep both, prefer the gateway, or retire lab_api_*; nothing is removed here.

🤖 Generated with Claude Code

## What The laboratory's MCP gateway is getting this plugin's whole-API contract (`api_spec`, `api_read`, `api_write`) with a copy of this guard in `agent-gateway/api_guard.py`. This change brings the plugin in line with that copy and hardens what the review found on the way. 1. **No redirect is followed**, for any method or 3xx status. Up to 0.3.2, urllib followed same-origin redirects. That had four effects: - it resent the token; - the guard never saw the new path; - a redirected `POST` turned into a `GET` that was reported as `HTTP 200 POST`; - other 3xx answers counted as success. Now a resource the laboratory moved is reported with its new API path ("call again with that path or the new repository name"), and any other redirect only as a fact. urllib does not even read the `Location` any more, and a malformed one is reported, not raised. 2. **One pinned policy.** `policy_sha256()` digests the decision tables the way the gateway does. `test_the_guard_policy_is_the_gateways` pins `b4e86f74…`, the value the gateway pins. `lab_api_spec` reports it. A further test pins that no write route carries regex flags, which the digest does not cover. 3. **The token never shows.** Should the laboratory send it back, the core replaces it as the answer arrives, before anything is cut or printed. That covers bodies, titles, errors and redirect notes, and a token spelled with JSON escapes too. - Server text in errors is bounded (300 characters, as in the gateway) and printable. - A keyring entry that is not an API token is refused without being quoted: under 16 characters, or containing a space, a line break or non-ASCII. - A request http.client will not write is reported with a fixed message, because its error would quote the header. 4. **Clean errors, never a traceback**, for: - a cut answer; - JSON nested too deeply (deep answers are shown compactly); - an API description that is not one, with odd entries skipped as in the gateway and a best-effort cache of the bytes as read. 5. **Query values** without a JSON spelling (NaN, infinities, numbers too long to print) are refused before anything is sent, by `api_call` itself. Version 0.3.3. The README and the skill say what a moved resource looks like and what the digest covers. ## Evidence - **Gate** (`sh scripts/check.sh`): exit 0. It runs 460 Python tests, the asset check, `claude plugin validate` and 18 mod tests. CI is green on every head. - **Review:** a fresh read-only subagent reviewed the change in three rounds plus a final look. - Each round's findings were fixed in their own commit: a malformed `Location` traceback that could carry the token, the token surviving a cut, deep or odd answers, and test gaps. - Verdict: mergeable. - **Mutation check:** the whole tree with a green baseline. Each of 42 deliberate breakages fails the suite, among them: - 0.3.2's same-origin following; - urllib reading the `Location`; - each redirect override dropped; - the token replaced too late or only once; - a widened write route; - a case-sensitive credential area; - a flag on a write route; - an unchecked keyring entry. - **Live**, as `claude` against https://oss-oo.io on 2026-10-27. Three paths: plugin 0.3.2, this branch, and the gateway's own handler, run locally from its branch. - **Decisions:** over the live API description (519 operations) every guard decision is equal. Both allow 216 reads and refuse 50, and allow 63 writes and refuse 190. - **Data:** the same reads give the same data digests on all three paths. - **Refusals:** hooks, admin, a repository delete and a `PATCH` of `private` are refused by all three without a request. - **Writes:** `PUT` of the same topics answers 204 on all three paths, and the topics are unchanged afterwards. - **Redirect:** `OSS-OO/forge` was renamed and answers 301. - 0.3.2 returned the other repository's data for `GET /repos/OSS-OO/forge`. - 0.3.2 reported `POST /repos/OSS-OO/forge/issues` (empty body) as `HTTP 200`, with an issue list. - This branch and the gateway report the move. - **CI reader:** this branch's CI reader needs no redirect. A PR's runs, jobs, job detail, job log, commit status and `/user` all answered 200. ## Not in this change Once the gateway is deployed, this plugin's `lab_api_*` and the gateway's `api_*` overlap. Both enforce the same policy (same digest). The owner decides whether to keep both, prefer the gateway, or retire `lab_api_*`; nothing is removed here. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude agent added 1 commit 2026-10-27 17:29:46 +00:00
Follow the gateway's whole-API contract: no redirects, one pinned policy
All checks were successful
ci / check (pull_request) Successful in 11s
c4ffbe75e6
The laboratory's MCP gateway is getting the plugin's whole-API contract
(api_spec, api_read, api_write) with a copy of this guard. This change
brings the plugin in line with that copy.

- No redirect is followed, for any method. Until now urllib followed
  same-origin redirects: it resent the token, the guard never checked
  the new path, a redirected POST became a GET that was reported as
  "HTTP 200 POST", and other 3xx answers counted as success. A resource
  the laboratory moved (a renamed repository) is now reported with its
  new API path to call again, any other redirect only as a fact. Server
  text in errors is shown bounded, printable and without the token.
- policy_sha256() digests the decision tables the way the gateway does
  (agent-gateway/api_guard.py). A test pins the value the gateway pins,
  b4e86f74..., so a policy changed on one side alone fails a check;
  lab_api_spec reports it like the gateway's api_spec.
- Query values without a JSON spelling (NaN, infinities, numbers too
  long to print) are refused cleanly, as the gateway refuses them.

Version 0.3.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent added 1 commit 2026-10-27 17:57:44 +00:00
Review fixes: no traceback on any redirect, the token never printed
All checks were successful
ci / check (pull_request) Successful in 13s
11a6a1e905
- urllib no longer reads a redirect's Location (http_error_30x return
  None), and the redirect note survives a Location that no URL parser
  reads. A malformed or crafted Location was a traceback before, one
  that could carry the token.
- A plain-text error body loses the token before it is cut to 200
  bytes, so no part of a token survives the cut.
- main() replaces the token in everything it prints (bodies, titles,
  errors), should the laboratory ever send it back. The README now
  says exactly that and no more.
- api_call converts query values itself (JSON spelling; NaN,
  infinities and over-long numbers refused), so the library path
  matches the CLI and the gateway.
- A cut answer (IncompleteRead), JSON nested too deeply in an answer or
  an error body, and an API description that is not a JSON document
  with paths are clean errors, not tracebacks. The description's error
  text is bounded and printable.
- The moved note fits the CI tools too ("call again with that path or
  the new repository name"). A test pins that no write route carries
  regex flags, which the shared digest does not cover, and the README
  says what the digest covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent added 1 commit 2026-10-27 18:24:38 +00:00
Review round 2: the token goes before any cut, deep answers shown
All checks were successful
ci / check (pull_request) Successful in 13s
dd1d6d0007
- Every answer loses the token as it arrives in Api._request, before
  anything cuts it, and api_call replaces it again before its
  40,000-character cut, so neither a cut nor a JSON escape leaves part
  of it in what lab_api_read shows.
- An answer nested deeper than the indenting encoder goes (about 1000
  levels) is shown compactly. main() turns a RecursionError into a
  clean refusal.
- The API description is cached as the bytes read, best effort (an
  unwritable cache costs a reread). Its odd entries are skipped, as the
  gateway skips them.
- http.client refuses a URL with a space (InvalidURL) or a header with a
  line break before it connects; that is now reported as a request that
  could not be sent, never with its message, which may quote the token.
- Tests close the gaps the review's mutations found: an unwritable
  request, an invalid or deep cache, a deep description, an unbounded
  moved path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent added 1 commit 2026-10-27 18:49:07 +00:00
Review round 3: a token-shaped keyring entry, the last test gaps
All checks were successful
ci / check (pull_request) Successful in 13s
cadf2db3c5
- keyring_token refuses an entry that is no API token (under 16
  characters, a space, a line break, non-ASCII) without quoting it.
  Replacing the token in every answer garbles nothing only for a token
  that cannot be ordinary text, and a line break would otherwise reach
  the Authorization header.
- A RecursionError is reported neutrally, as an answer or a body
  nested too deeply to handle.
- Tests pin what the review's mutations showed unpinned: every
  occurrence of the token is replaced, as the answer arrives and before
  either cut; the 300-character bound; line, control and direction
  characters; a numeric operationId; a cached {} or paths list read
  again; non-ASCII in the compact view. The deep-answer test no longer
  depends on where CPython's indenting encoder stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent added 1 commit 2026-10-27 18:58:08 +00:00
Tests: an interpreter-independent deep answer, no literal RLO in source
All checks were successful
ci / check (pull_request) Successful in 12s
8a5af5e1cd
- The deep-answer test reads the answer uncut (--max-chars=0), so it
  holds whether the indenting encoder or the compact one renders it.
- The right-to-left override in the control-character test is written
  as an escape, so editors and diff views do not reorder the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent merged commit e77e53d627 into main 2026-10-27 18:58:41 +00:00
claude agent deleted branch oss-oo/gateway-contract-align 2026-10-27 18:58:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OSS-OO/claude-plugin!9
No description provided.