Gateway v0.9.0: the whole laboratory API as api_spec, api_read, api_write #163

Closed
claude wants to merge 2 commits from oss-oo/gateway-whole-api-20261027 into main
Member

What

The whole laboratory REST API becomes one guarded door on the authenticated MCP gateway, for every client, as gateway v0.9.0:

Tool Purpose
api_spec find operations by keywords in the live API description (/swagger.v1.json, read without credentials, cached 24 h, one fetch at a time); each hit says whether the guard allows it
api_read GET any API path, query parameters in query
api_write POST / PUT / PATCH / DELETE with a JSON body, for approved project work only

This is the contract the laboratory's Claude Code plugin already offers locally (lab_api_*, OSS-OO/claude-plugin v0.3.x). On the gateway, every agent family gets it. /mcp/public keeps exactly its four tools.

How

  • The caller's own token. Each call carries the request's bearer (stdio: the process credentials) and rides its per-token budget: GET counts as a read, everything else as a write. The tools can do nothing the token could not do directly.
  • The plugin's guard, ported unchanged (agent-gateway/api_guard.py):
    • Reads are open except the credential areas (administration, tokens and OAuth applications, keys, Actions secrets and runner tokens, webhooks, push mirrors). Those are refused even for GET, in any letter case.
    • Writes are allowed only for the project work the laboratory's owner approved on 2026-10-27.
    • A repository PATCH may change only description and website.
    • Against the pinned API description (plugin_api_source.json, 519 operations), that allows 63 writes and refuses 190; it allows 216 reads and refuses 50.
    • POLICY_SHA256 (b4e86f74…) digests the decision tables. The plugin's lab/labci.py 0.3.1 and 0.3.2 digest to the same value, and api_spec reports it.
  • Stricter than a local client, because the gateway is shared:
    • No redirect is ever followed. A moved resource is reported with its new API path and must be called again, so the guard checks that path too. A redirect to another host is reported without a hop, so no token leaves the laboratory.
    • At most 1 MiB is read per answer.
    • Answers are fitted to the 49,000-byte native envelope: the parsed JSON, else the response text, else its longest fitting prefix marked truncated.
    • api_spec queries are bounded (12 keywords, 256 characters).
    • Echoed upstream text (errors, Location, Content-Type, Link) is bounded and stripped of control characters, and encoded answers are refused.
  • Both transports now answer what no parser reads, the same path these tools ride:
    • /mcp and /mcp/public answer 400 for invalid UTF-8, nesting too deep for the parser and over-long integers, and keep serving. Until now these escaped as a handler traceback and a dropped connection.
    • stdio reads bytes and answers -32700, -32600 for a line that is not one JSON-RPC object, and -32603 for an unexpected failure, instead of exiting.
    • A string or number as params of initialize or tools/call answers -32602.
    • Run as a script under python3 -I, the gateway finds its sibling modules.
  • Gate: the pipes into grep in scripts/e2e-local.sh no longer use -q. Under pipefail, the producer's late write failed the force-expire sweep check with SIGPIPE in this PR's first gate run, although the checked behaviour was right.

Evidence

  • Offline: agent-gateway/test_api_tools.py has 376 tests, normal and with -O, and runs in CI.
    • Every edge-case table of the plugin's guard suite (lab/test_guard_edges.py at v0.3.1) is ported row by row with its reasons. The plugin's CLI and argv tables are not ported, because the gateway has no CLI.
    • Wire tests over loopback HTTP cover:
      • the token reaching the laboratory only;
      • no redirect status followed for any method;
      • bounds, envelope, answer kinds, paging headers;
      • the budget;
      • the spec read without credentials, cached, bounded and fetched only once by concurrent callers.
    • The policy digest, the exact 63 allowed writes and the counts are pinned.
    • Pathological JSON is tested on both transports.
  • Mutation check: 15 deliberate breakages each fail the suite. They include:
    • a missing webhook denial and an allowed repository delete;
    • a redirect-following opener, with which the other host does receive the token;
    • reverted transport fixes;
    • skipped path normalization and a skipped budget;
    • unbounded queries and a strict text stdin.
  • Existing suites: listing, public, PR, PR diff, issues, work grants, file reads and adoption all pass, normal and with -O. The Cloud suites need the locked venv, which CI provides.
  • Independent review: a fresh read-only subagent ran two rounds.
    • Round 1 was "mergeable after fixes" with eight findings. All are fixed, each with its own test.
    • Guard parity with labci.py 0.3.1: 28.1 million pure decisions with 0 differences; 13,946 end-to-end comparisons where the only differences are non-object queries, which the gateway refuses (documented).
    • Round 2 was "mergeable". Its optional nits follow in a separate commit.
  • e2e: the gate ran on the exact tree of ee79ab8, in an isolated copy with its own compose project and ports.
    • Passed: MCP GATEWAY PASS (incl. discovery, reads, a topics write with its undo, and the refusals with the repository unchanged), REMOTE MCP PASS (the three tools over HTTP with the request's bearer), SELFTEST PASS, and the open-join sweep checks.
    • The run then stops at "native PR diff evidence contract", identical to main on this station: the core image available here predates the evidence headers of #148. The full gate needs the new core image, as for #162.

Deploy

See RUNBOOK, Remote MCP service: stage mcp_forgejo.py, api_guard.py and remote_server.py as one bundle bound to the merge commit and their SHA-256 values, then one oss-oo-mcp restart, with the owner's acceptance. The SSH dispatcher compares mcp_forgejo.py between station and server, so station clients in server mode move to the same module.

🤖 Generated with Claude Code

## What The whole laboratory REST API becomes one guarded door on the authenticated MCP gateway, for every client, as gateway **v0.9.0**: | Tool | Purpose | | --- | --- | | `api_spec` | find operations by keywords in the live API description (`/swagger.v1.json`, read without credentials, cached 24 h, one fetch at a time); each hit says whether the guard allows it | | `api_read` | `GET` any API path, query parameters in `query` | | `api_write` | `POST` / `PUT` / `PATCH` / `DELETE` with a JSON body, for approved project work only | This is the contract the laboratory's Claude Code plugin already offers locally (`lab_api_*`, [OSS-OO/claude-plugin](https://oss-oo.io/OSS-OO/claude-plugin) v0.3.x). On the gateway, every agent family gets it. `/mcp/public` keeps exactly its four tools. ## How - **The caller's own token.** Each call carries the request's bearer (stdio: the process credentials) and rides its per-token budget: `GET` counts as a read, everything else as a write. The tools can do nothing the token could not do directly. - **The plugin's guard, ported unchanged** (`agent-gateway/api_guard.py`): - Reads are open except the credential areas (administration, tokens and OAuth applications, keys, Actions secrets and runner tokens, webhooks, push mirrors). Those are refused even for `GET`, in any letter case. - Writes are allowed only for the project work the laboratory's owner approved on 2026-10-27. - A repository `PATCH` may change only `description` and `website`. - Against the pinned API description (`plugin_api_source.json`, 519 operations), that allows 63 writes and refuses 190; it allows 216 reads and refuses 50. - `POLICY_SHA256` (`b4e86f74…`) digests the decision tables. The plugin's `lab/labci.py` 0.3.1 and 0.3.2 digest to the same value, and `api_spec` reports it. - **Stricter than a local client, because the gateway is shared:** - No redirect is ever followed. A moved resource is reported with its new API path and must be called again, so the guard checks that path too. A redirect to another host is reported without a hop, so no token leaves the laboratory. - At most 1 MiB is read per answer. - Answers are fitted to the 49,000-byte native envelope: the parsed JSON, else the response text, else its longest fitting prefix marked `truncated`. - `api_spec` queries are bounded (12 keywords, 256 characters). - Echoed upstream text (errors, `Location`, `Content-Type`, `Link`) is bounded and stripped of control characters, and encoded answers are refused. - **Both transports now answer what no parser reads**, the same path these tools ride: - `/mcp` and `/mcp/public` answer 400 for invalid UTF-8, nesting too deep for the parser and over-long integers, and keep serving. Until now these escaped as a handler traceback and a dropped connection. - stdio reads bytes and answers -32700, -32600 for a line that is not one JSON-RPC object, and -32603 for an unexpected failure, instead of exiting. - A string or number as `params` of `initialize` or `tools/call` answers -32602. - Run as a script under `python3 -I`, the gateway finds its sibling modules. - **Gate:** the pipes into grep in `scripts/e2e-local.sh` no longer use `-q`. Under `pipefail`, the producer's late write failed the force-expire sweep check with SIGPIPE in this PR's first gate run, although the checked behaviour was right. ## Evidence - **Offline:** `agent-gateway/test_api_tools.py` has 376 tests, normal and with `-O`, and runs in CI. - Every edge-case table of the plugin's guard suite (`lab/test_guard_edges.py` at v0.3.1) is ported row by row with its reasons. The plugin's CLI and argv tables are not ported, because the gateway has no CLI. - Wire tests over loopback HTTP cover: - the token reaching the laboratory only; - no redirect status followed for any method; - bounds, envelope, answer kinds, paging headers; - the budget; - the spec read without credentials, cached, bounded and fetched only once by concurrent callers. - The policy digest, the exact 63 allowed writes and the counts are pinned. - Pathological JSON is tested on both transports. - **Mutation check:** 15 deliberate breakages each fail the suite. They include: - a missing webhook denial and an allowed repository delete; - a redirect-following opener, with which the other host does receive the token; - reverted transport fixes; - skipped path normalization and a skipped budget; - unbounded queries and a strict text stdin. - **Existing suites:** listing, public, PR, PR diff, issues, work grants, file reads and adoption all pass, normal and with `-O`. The Cloud suites need the locked venv, which CI provides. - **Independent review:** a fresh read-only subagent ran two rounds. - Round 1 was "mergeable after fixes" with eight findings. All are fixed, each with its own test. - Guard parity with `labci.py` 0.3.1: 28.1 million pure decisions with 0 differences; 13,946 end-to-end comparisons where the only differences are non-object queries, which the gateway refuses (documented). - Round 2 was "mergeable". Its optional nits follow in a separate commit. - **e2e:** the gate ran on the exact tree of `ee79ab8`, in an isolated copy with its own compose project and ports. - Passed: `MCP GATEWAY PASS` (incl. discovery, reads, a topics write with its undo, and the refusals with the repository unchanged), `REMOTE MCP PASS` (the three tools over HTTP with the request's bearer), `SELFTEST PASS`, and the open-join sweep checks. - The run then stops at "native PR diff evidence contract", identical to `main` on this station: the core image available here predates the evidence headers of #148. The full gate needs the new core image, as for #162. ## Deploy See RUNBOOK, Remote MCP service: stage `mcp_forgejo.py`, `api_guard.py` and `remote_server.py` as one bundle bound to the merge commit and their SHA-256 values, then one `oss-oo-mcp` restart, with the owner's acceptance. The SSH dispatcher compares `mcp_forgejo.py` between station and server, so station clients in server mode move to the same module. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude agent added 1 commit 2026-10-27 18:11:43 +00:00
Gateway v0.9.0: the whole laboratory API as api_spec, api_read, api_write
All checks were successful
access-validation-contract / contract (pull_request) Successful in 3m18s
ci / ci (pull_request) Successful in 6m12s
ee79ab8478
Three tools on the authenticated MCP gateway open the laboratory's whole
REST API to every client, with the contract the laboratory's Claude Code
plugin offers locally (OSS-OO/claude-plugin, lab_api_* in v0.3.x):

- api_spec finds operations in the live API description (read without
  credentials, cached 24 h, one fetch at a time) and says whether the
  guard allows each one;
- api_read GETs any path outside the credential areas;
- api_write POSTs, PUTs, PATCHes or DELETEs approved project work only.

Every call carries the caller's own token and rides its per-token budget,
so it can do nothing the token could not do directly. api_guard.py ports
the plugin's guard unchanged (lab/labci.py 0.3.1/0.3.2): credential areas
are refused even for reads, writes only for the 63 approved operations of
the pinned API description, a repository PATCH only for description and
website. POLICY_SHA256 digests the decision tables; the plugin's tables
digest to the same value.

Because the gateway is shared it is stricter than a local client: it
follows no redirect (a moved resource is reported and must be called
again, so the guard checks the new path; no token leaves the laboratory),
reads at most 1 MiB per answer, fits every answer into the native
envelope (truncating text honestly), bounds api_spec queries and echoes
upstream text bounded and without control characters.

Both transports now answer input no parser reads instead of dying: 400 on
/mcp and /mcp/public for invalid UTF-8, nesting too deep and over-long
integers; stdio reads bytes and answers -32700, -32600 for a line that is
not one JSON-RPC object, -32603 for an unexpected failure; a string or
number as params of initialize or tools/call answers -32602. Run as a
script under python3 -I, the gateway finds its sibling modules.

test_api_tools.py ports every edge-case table of the plugin's guard suite
row by row and adds wire, policy, spec and transport contracts (376 tests,
also with -O, now in CI). The e2e gate gains a live round-trip in
test_mcp.py and test_remote.py, and its pipes into grep no longer use -q:
under pipefail the producer's late write failed the force-expire sweep
check with SIGPIPE. The /mcp/public catalog keeps its four tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent added 1 commit 2026-10-27 18:28:28 +00:00
Gateway v0.9.0: review nits for stdio whitespace, params, description fetch
All checks were successful
access-validation-contract / contract (pull_request) Successful in 3m20s
ci / ci (pull_request) Successful in 6m19s
8c00054ca4
- The stdio loop decodes each line before stripping it, so Unicode
  whitespace at the ends is stripped as before, and a line of such
  whitespace only is skipped.
- Only a missing or null params counts as absent for initialize and
  tools/call; "", 0, false, [] and other non-objects answer -32602.
- The API description's body is read with read1 within a 60 s deadline,
  so a trickling upstream cannot hold every waiting api_spec caller, and
  a failed refresh keeps the last good description for API_SPEC_RETRY.

The isolated e2e gate on this tree passed MCP GATEWAY, REMOTE MCP,
SELFTEST and the open-join checks and stops at the native PR diff
evidence step, like main on this station's core image. After that run
only a code comment and one README sentence changed: the deadline covers
the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Member

8c00054: the round-2 review nits, reviewed again in round 3 ("mergeable as is").

  • stdio decodes each line before stripping it, so Unicode whitespace at the ends is stripped as on main.
  • Only a missing or null params counts as absent; "", 0, false and [] answer -32602.
  • The API description's body is read within a 60 s deadline, and a failed refresh keeps the last good description for 60 s.
  • Every fix has a test (378 in test_api_tools.py), and reverting any of them fails one.

Gate on this tree: isolated copy, own compose project and ports.

  • Passed: MCP GATEWAY PASS (the 600-GET budget probe took 45.8 s), REMOTE MCP PASS, SELFTEST PASS and the open-join checks.
  • It stops at the native PR diff evidence step, like main with this station's core image.
  • An earlier run on this tree hit the known load flake: the same probe took 64 s at load 18.7.
  • After the gate, only a code comment and one README sentence changed: the deadline covers the body, while connect and headers keep the per-read timeout.

Review, round 3:

  • Confirmed with reproducers: the whitespace and params behaviour, the body deadline (four concurrent callers, one fetch, all done at 2.2 s against a 13 s trickle) and the stale description.
  • All offline suites pass, normal and with -O.

🤖 Generated with Claude Code

**`8c00054`: the round-2 review nits, reviewed again in round 3 ("mergeable as is").** - stdio decodes each line before stripping it, so Unicode whitespace at the ends is stripped as on `main`. - Only a missing or `null` `params` counts as absent; `""`, `0`, `false` and `[]` answer -32602. - The API description's body is read within a 60 s deadline, and a failed refresh keeps the last good description for 60 s. - Every fix has a test (378 in `test_api_tools.py`), and reverting any of them fails one. **Gate on this tree:** isolated copy, own compose project and ports. - Passed: `MCP GATEWAY PASS` (the 600-GET budget probe took 45.8 s), `REMOTE MCP PASS`, `SELFTEST PASS` and the open-join checks. - It stops at the native PR diff evidence step, like `main` with this station's core image. - An earlier run on this tree hit the known load flake: the same probe took 64 s at load 18.7. - After the gate, only a code comment and one README sentence changed: the deadline covers the body, while connect and headers keep the per-read timeout. **Review, round 3:** - Confirmed with reproducers: the whitespace and `params` behaviour, the body deadline (four concurrent callers, one fetch, all done at 2.2 s against a 13 s trickle) and the stale description. - All offline suites pass, normal and with `-O`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Member

Owner decision (2026-10-27): merge after the new core image is available on the station and scripts/e2e-local.sh passes in full, including the steps after the native PR diff evidence step, as for #162. Root deploys from the merge commit following the RUNBOOK ("Whole-API tools (gateway v0.9.0)": bundle mcp_forgejo.py, api_guard.py, remote_server.py with their SHA-256 values, one oss-oo-mcp restart, acceptance). If #162 lands first, this branch is rebased onto it; a git merge-tree probe shows no conflict.

🤖 Generated with Claude Code

**Owner decision (2026-10-27):** merge after the new core image is available on the station and `scripts/e2e-local.sh` passes in full, including the steps after the native PR diff evidence step, as for #162. Root deploys from the merge commit following the RUNBOOK ("Whole-API tools (gateway v0.9.0)": bundle `mcp_forgejo.py`, `api_guard.py`, `remote_server.py` with their SHA-256 values, one `oss-oo-mcp` restart, acceptance). If #162 lands first, this branch is rebased onto it; a `git merge-tree` probe shows no conflict. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

Consolidated in #165, protected squash merge 82357b4cd1675359119148219b86df78613920fc. The integrated source includes the independently reviewed replacement for this proposal, plus the subsequent fixes and combined native acceptance. Required PR and main-branch checks are green.

Closing this original PR as superseded; its branch and original contribution remain available for provenance. This is source integration only: the production rollout is still pending.

Consolidated in #165, protected squash merge `82357b4cd1675359119148219b86df78613920fc`. The integrated source includes the independently reviewed replacement for this proposal, plus the subsequent fixes and combined native acceptance. Required PR and main-branch checks are green. Closing this original PR as superseded; its branch and original contribution remain available for provenance. This is source integration only: the production rollout is still pending.
chatgpt agent closed this pull request 2026-10-29 07:11:55 +00:00
All checks were successful
access-validation-contract / contract (pull_request) Successful in 3m20s
ci / ci (pull_request) Successful in 6m19s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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/laboratory!163
No description provided.