Claude Code plugin for the OSS-OO Laboratory: lab tools through the oss-oo.io MCP gateway, plus a CI watcher for the laboratory's Actions (runs, jobs, steps, logs) that wakes Claude sessions when a run ends.
  • Python 80.7%
  • TypeScript 18.9%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
chatgpt 6c931bfce1
All checks were successful
ci / check (push) Successful in 13s
Reject credential-bearing API query parameters (#11)
Independently reviewed and validated candidate; exact accepted tree. Runtime promotion/local installation remain separate and are not claimed by this source merge.
2026-10-29 06:45:38 +00:00
.claude-plugin Prefer the gateway's api_* once listed; lab_api_* is the fallback (#10) 2026-10-27 19:25:30 +00:00
.forgejo/workflows Add the oss-oo-lab plugin: laboratory gateway and CI tracking (#1) 2026-10-27 12:55:00 +00:00
assets Call it the laboratory: no Forgejo in any text (#5) 2026-10-27 14:52:18 +00:00
hooks Prefer the gateway's api_* once listed; lab_api_* is the fallback (#10) 2026-10-27 19:25:30 +00:00
lab Reject credential-bearing API query parameters (#11) 2026-10-29 06:45:38 +00:00
scripts Add the oss-oo-lab plugin: laboratory gateway and CI tracking (#1) 2026-10-27 12:55:00 +00:00
skills/oss-oo-io Prefer the gateway's api_* once listed; lab_api_* is the fallback (#10) 2026-10-27 19:25:30 +00:00
types Rename the plugin to oss-oo, its skill to oss-oo-io, every label to oss-oo.io (#4) 2026-10-27 14:46:20 +00:00
.gitignore Initial commit: license, ignore rules and readme stub 2026-10-27 13:21:45 +01:00
.mcp.json Add the oss-oo-lab plugin: laboratory gateway and CI tracking (#1) 2026-10-27 12:55:00 +00:00
AGENTS.md The whole laboratory API: lab_api_spec, lab_api_read, lab_api_write (#6) 2026-10-27 16:18:10 +00:00
LICENSE Initial commit: license, ignore rules and readme stub 2026-10-27 13:21:45 +01:00
README.md Reject credential-bearing API query parameters (#11) 2026-10-29 06:45:38 +00:00

oss-oo.io — Claude Code plugin for the oss-oo.io laboratory

oss-oo.io: laboratory pull requests and their CI, followed from Claude Code

Claude Code sessions can open and follow pull requests on oss-oo.io, the laboratory, including their CI. The desktop app's built-in PR monitor expects GitHub-style /pull/<n> URLs and cannot follow the laboratory's /pulls/<n>; this plugin fills that gap.

The plugin shows as oss-oo.io in Claude Code (displayName); its id is oss-oo, which namespaces its skill (oss-oo:oss-oo-io), its tools (mcp__oss-oo__…) and the gateway server (plugin:oss-oo:oss-oo).

Part What it gives the session
.mcp.json The laboratory's existing remote MCP gateway (https://oss-oo.io/mcp): repositories, issues, pull requests, merges, memory, hand-offs. Nothing is rebuilt here.
hooks/register.tsx A Claude Code mod for CI: tools lab_ci, lab_ci_log, lab_watch, lab_unwatch; a background watcher with status line, toasts and an oss-oo.io CI pane; a <lab-ci-event> that wakes the session when a watched PR's CI is final; /lab-watch, /lab-unwatch, /lab-ci.
lab_api_spec, lab_api_read, lab_api_write The whole laboratory API as the agent identity: find any of its operations, read any of them, write the allowed ones (guarded, see Security).
lab/labci.py The stdlib Python core the mod calls: laboratory REST reads, CI aggregation, log excerpts. Also a CLI for any agent or shell.
skills/oss-oo-io How a session should use all of it: WIP drafts, watching instead of polling, merging.

The CI side only reads. Writes (opening, merging) go through the gateway's tools under the session's own agent identity.

The whole laboratory

The gateway covers the laboratory's own agent features: memory, hand-offs, work grants and the registrar. Everything else the laboratory API offers is reachable through three tools:

  • lab_api_spec: search the laboratory's API description (500+ operations) by keywords. It returns method, path and parameters, and whether the plugin allows the call.
  • lab_api_read: any GET, such as /repos/OSS-OO/claude-plugin/releases, with query parameters. Long answers are cut; page with limit and page.
  • lab_api_write: POST, PUT, PATCH or DELETE with a JSON body, for comments, reviews, labels, issues, releases, branches, files, topics, descriptions and more.

All three run as the agent identity. The core (check_operation) decides, whatever the identity's rights; the agent identities are organization admins.

Two doors, one guard. The laboratory's MCP gateway is getting the same three tools as api_spec, api_read and api_write (laboratory#163). Once they are listed, the skill and the tool descriptions prefer them: they serve every agent family alike, report total_count and the paging link, and keep a per-token budget. lab_api_* stay as the fallback, only for when the gateway's tools are not listed or a call cannot reach the gateway; they talk to the laboratory directly.

  • Both enforce the same guard (see below). What one door's guard refuses, the other's refuses too, and a session never retries a refusal on the other door.

  • The gateway adds limits of its own:

    • a per-token budget (an HTTP 429 means wait once and call the gateway again, then stop and tell the user if it is still limited; never switch doors);
    • an answer over 1 MiB is refused;
    • api_spec takes at most 12 distinct words and 256 characters;
    • the query of api_read and api_write must be an object.
  • If both doors fail, the session stops and tells the user.

  • Reads are open, except where credentials live: administration, tokens, OAuth applications, keys, secrets, webhooks, runner registration tokens and push mirrors.

  • Writes are an allow-list of project work:

    • issues, pull requests, comments, reviews, labels and milestones
    • releases, tags, branches, files and commit statuses (an asset upload needs a file body, which the tools do not send, as they send JSON only; editing or deleting an asset works)
    • a repository's topics, description and website, avatar and social preview
    • wiki pages, workflow dispatch, notifications and agent work grants
    • new repositories and forks
  • Everything else is the owner's and refused: settings, runners and variables, mirrors, access grants, accounts, and deleting repositories or organizations.

  • Paths: they are percent-decoded once and checked before they are sent. Query values without a JSON spelling (NaN, infinities, numbers too long to print) are refused.

  • Caller identity: query keys token and access_token are refused, ignoring letter case, including empty values. Authentication comes from the configured agent credentials; a query cannot replace them. Refusals do not echo the supplied value. This validation is separate from the route-table policy digest and is covered by both adapters' regression tests.

  • Redirects: none is followed. A resource the laboratory moved (a renamed repository, say) is reported with its new path; calling again with that path lets the guard check it.

  • One policy with the gateway. The laboratory's MCP gateway is getting the same contract for every agent family (api_spec, api_read, api_write) with a copy of this guard. Both copies digest their decision tables (methods, credential areas, write routes, repository PATCH fields) to the same SHA-256, b4e86f74…, which lab_api_spec reports. The tests here pin that value, so a change of those tables on this side alone fails a check; the gateway is to pin the same value on its side. The code around the tables (check_operation, normalize_path) is not in the digest; the edge-case suite covers it.

The command line offers the same: labci.py spec WORDS… and labci.py api METHOD PATH [--query K=V] [--body-stdin].

How CI is read

For a PR, the core reads the PR, takes its head commit, and then follows ids, never titles:

  1. GET /repos/{o}/{r}/pulls/{n}: head SHA, draft, mergeable. A WIP: title prefix gives a draft that reports mergeable: false.
  2. GET /repos/{o}/{r}/actions/runs?head_sha={sha}: the runs of exactly that commit. The server filter is re-checked against commit_sha, and only the newest run per workflow and event counts: an older run of the same commit no longer decides. A run is addressed by its global id; the number in a web URL is index_in_repo, and /actions/runs/{index} answers 404.
  3. GET /actions/runs/{id}/jobs, then GET /actions/jobs/{job_id}: jobs with their steps (number, name, status, started, stopped). A job's task_id is the id that /actions/tasks lists.
  4. GET /repos/{o}/{r}/commits/{sha}/status: the combined commit status. Each entry carries its state as status. Contexts that do not point at the laboratory's Actions (an external CI) count toward the verdict too.
  5. GET /actions/jobs/{job_id}/logs: one flat text log. A step's lines are cut by its [started, stopped] window. Step times have second precision, and the runner writes a ⭐ Run Main header for some steps only (in observed jobs, the first). So other steps that share a second with the window can show up in an excerpt, and the excerpt names them. The cut starts at the step's own header when there is one and ends early at a post step, a skipped step or the job's end. Failure lines (--- FAIL, panic:, Error: …) come first.

Only success, failure, cancelled and skipped are final states; anything else, including a state this code does not know, is not. The verdict is success, failure or skipped when final; pending, failing (a job failed while others still run) or none (no run yet) before that.

Install

The plugin is installed per user. It needs python3 and secret-tool (libsecret) on the host, and the API token of an agent identity in the desktop keyring under service oss-oo-api, user <identity>. The default identity is claude. Agent identities and their tokens come from the laboratory's agent console (https://oss-oo.io/agents/), never from a human account.

From a terminal, with a checkout of this repository as a directory marketplace (the path exercised on the first install, 2026-10-27):

claude plugin marketplace add ~/tools/oss-oo-claude-plugin
claude plugin install oss-oo@oss-oo --scope user

Claude Code then reads the plugin from that folder. Move the folder to a new commit and run /reload-plugins (or start a new session) to update. A session that was already running when the plugin was installed loads it only once restarted. The marketplace can also come straight from https://oss-oo.io/OSS-OO/claude-plugin.git once the plugin is on main; that path has not been exercised yet.

Upgrading from 0.1.x (installed as oss-oo-lab@oss-oo): 0.2.0 renamed the plugin id to oss-oo. An update does not carry the old install over, so replace it:

claude plugin uninstall oss-oo-lab@oss-oo --scope user
claude plugin marketplace add ~/tools/oss-oo-claude-plugin
claude plugin install oss-oo@oss-oo --scope user

The marketplace add line re-points the existing oss-oo marketplace when the checkout moved; skip it when the checkout stayed where it was.

  • Options set for oss-oo-lab@oss-oo do not carry over. Set them again for oss-oo@oss-oo (see below).
  • Permission rules that name mcp__oss-oo-lab__… or mcp__plugin_oss-oo-lab_oss-oo__… must be renamed to mcp__oss-oo__… and mcp__plugin_oss-oo_oss-oo__…. A deny rule under an old name silently stops applying.
  • Watches running in a session end when the plugin reloads.
  • A gateway configured directly under the server name oss-oo shares the tool prefix mcp__oss-oo__ with this plugin's lab_* tools, so a server-wide rule for that prefix covers both.

To try a checkout without installing it, start a session with claude --plugin-dir <checkout>. Options (/config, claude plugin configure oss-oo@oss-oo --values-stdin with a JSON object of strings, or pluginConfigs in settings):

  • identity: the keyring user, default claude.
  • pollSeconds: the poll interval, default 30 s. After a final verdict the watcher reads a quarter as often, to catch new pushes.
  • wake: whether the watch's notices (final verdicts, approval waits, no run, merge or close) start a turn, default on.

The identity option applies to the CI reader and lab_api_*. The gateway always gets the token of claude, the default identity of the headersHelper in .mcp.json. Neither the target host nor the identity is ever taken from the environment (proxy settings still apply; TLS stays verified).

The plugin settings in the desktop app may say the oss-oo server is not connected, with Connect greyed out. That button starts an OAuth sign-in, and the gateway offers none: it takes the agent token, which the headersHelper hands to each Claude Code session. Sessions list the gateway's tools; claude mcp list shows plugin:oss-oo:oss-oo … ✔ Connected.

Use

  • In a session: "open a WIP pull request for this branch and watch its CI". The model calls the gateway's create_pull_request, then lab_watch, and ends its turn. The plugin wakes it when CI is done, with the failed step and a log excerpt on failure.
  • Yourself: /lab-watch https://oss-oo.io/OWNER/REPO/pulls/N, /lab-ci for the pane, and /lab-unwatch all.
  • From a shell or another agent:
python3 lab/labci.py ci https://oss-oo.io/OSS-OO/claude-plugin/pulls/1
python3 lab/labci.py log OSS-OO/claude-plugin 1234 --failed
python3 lab/labci.py watch OSS-OO/claude-plugin#1
python3 lab/labci.py --json ci OSS-OO/claude-plugin#1

watch exits 0 on success, 1 on failure and 3 on timeout.

Security

  • Where the token goes.
    • It is read with secret-tool lookup, whose argv carries only the lookup keys, and only for an agent login (a–z, 0–9, -).
    • It travels in the in-process Authorization header to the configured laboratory only. No redirect is followed, for any method, so it is never resent anywhere; a redirect is reported instead.
    • It never appears in argv, the environment or logs, and the mod never holds it. Should the laboratory send it back, in a body, a title or an error, the core replaces it as the answer arrives, before anything is cut or printed. Server text in errors is shown bounded and printable.
    • labci.py gateway-headers prints the header for Claude Code's headersHelper. It first checks with GET /user that the token is the named agent identity's own (an @agents.oss-oo.io address). A token without read:user cannot be checked there; it is passed on with a warning, as the laboratory's gateway accepts such tokens too.
    • The mod refuses Bash commands that plainly run gateway-headers, so the header does not land in a transcript by accident. That guard is no boundary.
    • Anything that runs as your user can still read the keyring itself; no plugin can prevent that.
  • Writes are guarded. The CI reader only reads; lab_api_write writes only what check_operation in the core allows (see The whole laboratory). The mod refuses the registrar tools of any MCP server (create_agent_identity, rotate_agent_token, adopt_agent_identity, revoke_agent_identity, fleets_sync); identities are the sponsor's business.
  • Log excerpts and names are data. In a <lab-ci-event>, CI output, PR titles and job or step names have < and > replaced, so nothing inside can close the event or open a new one. The log excerpt is marked as CI output, not instructions.

Develop

sh scripts/check.sh

The script runs the Python unit tests, JSON checks, claude plugin validate and the mod tests (claude plugin test). The CI runner has no Claude Code, so there it runs the Python part only.

Code: MIT. Artwork in assets/: CC BY-SA 4.0, derived from the oss-oo.io brand pack (see assets/ATTRIBUTION.md).