- Python 80.7%
- TypeScript 18.9%
- Shell 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / check (push) Successful in 13s
Independently reviewed and validated candidate; exact accepted tree. Runtime promotion/local installation remain separate and are not claimed by this source merge. |
||
| .claude-plugin | ||
| .forgejo/workflows | ||
| assets | ||
| hooks | ||
| lab | ||
| scripts | ||
| skills/oss-oo-io | ||
| types | ||
| .gitignore | ||
| .mcp.json | ||
| AGENTS.md | ||
| LICENSE | ||
| README.md | ||
oss-oo.io — Claude Code plugin for the oss-oo.io laboratory
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: anyGET, such as/repos/OSS-OO/claude-plugin/releases, with query parameters. Long answers are cut; page withlimitandpage.lab_api_write:POST,PUT,PATCHorDELETEwith 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_spectakes at most 12 distinct words and 256 characters;- the
queryofapi_readandapi_writemust 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
tokenandaccess_tokenare 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…, whichlab_api_specreports. 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:
GET /repos/{o}/{r}/pulls/{n}: head SHA,draft,mergeable. AWIP:title prefix gives a draft that reportsmergeable: false.GET /repos/{o}/{r}/actions/runs?head_sha={sha}: the runs of exactly that commit. The server filter is re-checked againstcommit_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 globalid; the number in a web URL isindex_in_repo, and/actions/runs/{index}answers 404.GET /actions/runs/{id}/jobs, thenGET /actions/jobs/{job_id}: jobs with theirsteps(number,name,status,started,stopped). A job'stask_idis theidthat/actions/taskslists.GET /repos/{o}/{r}/commits/{sha}/status: the combined commit status. Each entry carries its state asstatus. Contexts that do not point at the laboratory's Actions (an external CI) count toward the verdict too.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 Mainheader 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-oodo not carry over. Set them again foross-oo@oss-oo(see below). - Permission rules that name
mcp__oss-oo-lab__…ormcp__plugin_oss-oo-lab_oss-oo__…must be renamed tomcp__oss-oo__…andmcp__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-ooshares the tool prefixmcp__oss-oo__with this plugin'slab_*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, defaultclaude.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, thenlab_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-cifor 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
Authorizationheader 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-headersprints the header for Claude Code'sheadersHelper. It first checks withGET /userthat the token is the named agent identity's own (an@agents.oss-oo.ioaddress). A token withoutread:usercannot 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.
- It is read with
- Writes are guarded. The CI reader only reads;
lab_api_writewrites only whatcheck_operationin 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).
