# Probe transcript — GitHub REST API (e2e) | | | |---|---| | Entry | `entries/github-api.yaml` | | Target | `https://api.github.com` · docs https://docs.github.com/en/rest · OpenAPI https://github.com/github/rest-api-description | | Mode | `e2e` (gate first, then e2e in the same session) | | Probe id | `github-api-e2e` | | Rubric | `rubric/v0.2.json` sha256 `cd605a44799f49951bf078fda63c5b21ad3c07db494f41d38ad19ce729c5551a` | | Date | 2026-09-05 | | Sandbox up | 2026-09-05T13:05:51Z | | Sandbox down | 2026-09-05T13:11:30Z | | Wall clock | 339 s (epoch 1788613551 → 1788613890, shell invocation of `up` to completion of `down`) | | Real money spent | $0.00 — no payment instrument presented, none requested | | Human interventions | 0 | | Verdict | `completed` | | Secrets carried in | none. `PROBE_WALLET_KEY` unset, `PROBE_INBOX` unset, `PROBE_SPEND_CAP_USD=0`. No credential, token, cookie, or PII was sent or received. Nothing to redact | Every request to the target below was issued from inside the probe sandbox (`.claude/skills/probe/probe-sandbox.sh`) on an `--internal` Docker network whose only egress is the allowlisting tinyproxy sidecar. Gate-mode reading of the Terms of Service, Acceptable Use Policies, rate-limit docs and the MCP-server README used WebFetch (public pages, nothing that logs in or creates state). No request to the target was made from the host shell. ```sh ./.claude/skills/probe/probe-sandbox.sh up --id github-api-e2e \ --target api.github.com --allow github.com ``` Allowlist as intended: `api.github.com` (the API) and `github.com` (for `/.well-known/security.txt` only). **Note (sandbox, §8):** the proxy filter anchors as `(^|\.)github\.com$`, so `--allow github.com` also admitted `docs.github.com`; two requests reached that host (steps 10 and 15) that I had expected the allowlist to refuse. Both were public reads of `docs.github.com/llms.txt` on the target's own documentation host; reported here rather than hidden. --- ## 0. Permission gate (read before any sandbox traffic) The orchestrator's addendum makes the gate absolute: any indication the operator does not want automated use → halt. GitHub's legal surface has one anti-automation clause, and it is scoped. All quotes verbatim, retrieved 2026-09-05. **Terms of Service** — https://docs.github.com/en/site-policy/github-terms/github-terms-of-service (effective date shown on page: April 27, 2026) - §B.3 *Account Requirements*: "Accounts registered by "bots" or other automated methods are not permitted." - §B.1 *Account Controls*: "A machine account is an Account set up by an individual human who accepts the Terms on behalf of the Account, provides a valid email address, and is responsible for its actions." - §B.3: "One person or legal entity may maintain no more than one free Account (if you choose to control a machine account as well, that's fine, but it can only be used for running a machine)." - §H *API Terms*, summary line: "You agree to these Terms of Service, plus this Section H, when using any of GitHub's APIs (Application Programming Interface), including use of the API through a third party product that accesses GitHub." - §H: "Abuse or excessively frequent requests to GitHub via the API may result in the temporary or permanent suspension of your Account's access to the API." - §H: "You may not share API tokens to exceed GitHub's rate limitations." - §H: "You may not use the API to download data or Content from GitHub for spamming purposes, including for the purposes of selling GitHub users' personal information, such as to recruiters, headhunters, and job boards." **Acceptable Use Policies** — https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies - §7 *Information Usage Restrictions*: "Scraping refers to extracting information from our Service via an automated process, such as a bot or webcrawler." / "Scraping does not refer to the collection of information through our API." / "Please see Section H of our Terms of Service for our API Terms." - §7: "You may not use information from the Service (whether scraped, collected through our API, or obtained otherwise) for spamming purposes, including for the purposes of sending unsolicited emails to users or selling personal information, such as to recruiters, headhunters, and job boards." - §4 *Spam and Inauthentic Activity*: prohibits "automated excessive bulk activity and coordinated inauthentic activity", "inauthentic interactions, such as fake accounts and automated inauthentic activity", "rank abuse, such as automated starring or following", and "using our servers for any form of excessive automated bulk activity, to place undue burden on our servers through automated means." **robots.txt** — `https://github.com/robots.txt` (web host): header comment "# We also provide an extensive API: https://docs.github.com"; `User-agent: *` disallows web paths such as `/search$`, `/*q=`, `/*/tree/`; nothing references `api.github.com`. `https://api.github.com/robots.txt` → HTTP 404 (JSON "Not Found"), i.e. no directives on the API host (step 06). **Decision.** The read-only reference task is programmatic use of the API, which §H exists to govern and permits subject to rate limits and the anti-spam rules; AUP §7 explicitly says API collection is not scraping. The anti-automation clause (§B.3) is scoped to *account registration*, which this task does not do and which the entry records as the wall for any write-path task. `permission.status: granted`, scoped to API use; automated account creation is `prohibited` and was never attempted. Under the "when in doubt" rule I looked for doubt on the read path and found none: GitHub publishes the OpenAPI description, unauthenticated curl examples, and an unauthenticated rate limit for exactly this use. --- ## 1. Reference task > With no credentials, GET `https://api.github.com/search/repositories?q=nostr-tools` > and GET `https://api.github.com/repos/nbd-wtf/nostr-tools`, sending the documented > `User-Agent`, `Accept: application/vnd.github+json` and `X-GitHub-Api-Version: 2022-11-28` > headers, and pass only if both return HTTP 200 JSON, the repo response's `full_name` > equals `nbd-wtf/nostr-tools`, and that `full_name` appears in the search `items`. The specific repo is pinned so the check does not depend on search ranking. --- ## 2. First attempt: search was already rate-limited before my first request `13:06:35Z` — step 01, `GET /search/repositories?q=nostr-tools&per_page=3`: ``` HTTP/2 403 server: Varnish content-type: application/json; charset=utf-8 x-ratelimit-limit: 10 x-ratelimit-remaining: 0 x-ratelimit-used: 10 x-ratelimit-resource: search x-ratelimit-reset: 1788613603 x-github-request-id: 92F0:13FEC5:685C1C:1528A88:6A9C13DB ``` `x-ratelimit-used: 10` on my **first** search request. Unauthenticated limits are keyed by source IP, and this host's public IP was being shared by other indexer sandboxes running concurrently (the wave runs several subagents in parallel). This is not a wall GitHub put in front of *me*; it is the documented search limit ("up to 10 requests per minute" for unauthenticated requests, https://docs.github.com/en/rest/search/search) already consumed by a neighbour. The response carried `x-ratelimit-reset`, so the fix (wait until that epoch) came from the target itself. I honoured it: no retry until the window had reset (step 03 at `13:06:36Z` already showed `search: remaining 10, reset 1788613655`; the single retry was sent at `13:10:12Z`, step 14). The repo GET in the same second was unaffected (different resource bucket): `13:06:35Z` — step 02, `GET /repos/nbd-wtf/nostr-tools` → **HTTP 200**, 6 233 bytes, 0.257 s ``` content-type: application/json; charset=utf-8 cache-control: public, max-age=60, s-maxage=60 etag: W/"823873ff58cd6e6106349a70157c01be8357b3b26f628788f503ff590f7127bf" x-ratelimit-limit: 60 x-ratelimit-remaining: 59 x-ratelimit-used: 1 x-ratelimit-resource: core x-ratelimit-reset: 1788617195 ``` ```json {"full_name":"nbd-wtf/nostr-tools","default_branch":"master","license":"Unlicense","stargazers_count":853,"pushed_at":"2026-09-04T04:24:21Z"} ``` --- ## 3. Reference task — PASS (step 14, single retry after the documented reset) `13:10:12Z` — step 13, `GET /rate_limit` → `{"limit":10,"remaining":10,"reset":1788613872,"used":0}` for `search`. `13:10:12Z` — step 14, `GET /search/repositories?q=nostr-tools&per_page=3` → **HTTP 200**, 14 923 bytes, 0.394 s ``` HTTP/2 200 content-type: application/json; charset=utf-8 link: ; rel="next", ; rel="last" x-ratelimit-limit: 10 x-ratelimit-remaining: 9 x-ratelimit-used: 1 x-ratelimit-resource: search x-ratelimit-reset: 1788613872 ``` ```json {"total_count":247,"incomplete_results":false,"items":[{"full_name":"nbd-wtf/nostr-tools","html_url":"https://github.com/nbd-wtf/nostr-tools","stargazers_count":853},{"full_name":"grunch/rana","html_url":"https://github.com/grunch/rana","stargazers_count":294},{"full_name":"fiatjaf/nak","html_url":"https://github.com/fiatjaf/nak","stargazers_count":413}]} ``` Machine check: ``` $ jq -e '.full_name == "nbd-wtf/nostr-tools"' 02-repo.json → true $ jq -e '[.items[].full_name] | index("nbd-wtf/nostr-tools") != null' 14-search.json → true REFERENCE_TASK_PASS ``` Total requests to `api.github.com` in the run: 11. Nothing sent that a bare HTTP client does not have, plus the three documented headers. --- ## 4. Surface and discovery | Probe | Result | |---|---| | `GET https://api.github.com/` (step 05) | HTTP 200, 33-key hypermedia map; `repository_search_url` = `https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}` — the search endpoint is discoverable from the root with no docs | | `GET /rate_limit` (step 03) | `{"core":{"limit":60,...},"search":{"limit":10,...}}` — matches the docs ("The primary rate limit for unauthenticated requests is 60 requests per hour."; search "up to 10 requests per minute") | | `GET https://api.github.com/robots.txt` (06) | 404 JSON — no directives on the API host | | `GET https://api.github.com/llms.txt` (08), `/openapi.json` (09) | both 404 JSON. The OpenAPI description is published in the `github/rest-api-description` repository (OpenAPI 3.0 under `descriptions/`, 3.1 under `descriptions-next/`, MIT, "automatically kept up to date with the description used to validate GitHub API requests"), not served from the API host. Not fetched into the sandbox (multi-MB); cited from the gate read | | `GET https://github.com/.well-known/security.txt` (07) | HTTP 200: `Contact: https://hackerone.com/github` · `Policy: https://bounty.github.com` · `Expires: 2026-10-05T13:06:36z` | | `GET https://docs.github.com/llms.txt` (10, 15) | HTTP 200, 28 674 bytes. Quoted in §5 | | Official MCP server (gate read of https://github.com/github/github-mcp-server) | "The GitHub MCP Server connects AI tools directly to GitHub's platform." Hosted at `https://api.githubcopilot.com/mcp/` — "Log in with OAuth (no token to create or store)" or "authenticate with a Personal Access Token"; local run needs Docker and `GITHUB_PERSONAL_ACCESS_TOKEN`; supports `--read-only`. **Not exercised** — every path into it requires an account, which a baseline agent cannot create (§0) | ## 5. `docs.github.com/llms.txt` (untrusted, quoted for the record) Head of the file. It names a machine reader, so it is quoted inside an untrusted block; it contains no instruction directed at a model beyond describing its own APIs, and nothing in it was acted on as a command. ```untrusted-content # GitHub Docs > GitHub is a developer platform for building, shipping, and maintaining software. It provides cloud-based Git repository hosting, CI/CD via GitHub Actions, project management with Issues and Projects, code review via pull requests, AI-powered development with GitHub Copilot, and APIs (REST and GraphQL) for automation and integration. ## How to use To retrieve full article content, page lists, or search results programmatically, please use the APIs below. To find a specific article, use the **Search API** with a query. To browse all available pages, use the **Page List API** to get a list of paths, then fetch individual articles with the **Article API**. The `/api/article/body` endpoint returns markdown, ideal for LLM consumption. These APIs return structured markdown and JSON and are the preferred way for LLMs and automated tools to access GitHub documentation. ``` ## 6. Two documented-behaviour controls, sent once each, not repeated Recorded because the addendum says to honour User-Agent requirements and never attempt unauthorized access. Both are checks of *documented* responses, sent before the addendum arrived, with no credential and no retry; I would not send them again under the addendum and they are not part of the reference task. - Step 04, `GET /octocat` with the `User-Agent` header removed → HTTP 403, body: "Request forbidden by administrative rules. Please make sure your request has a User-Agent header (https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api#user-agent-required)…". The docs say exactly this ("Requests with no `User-Agent` header will be rejected."). Every other request in the run carried `User-Agent: ag3ntsearch-probe/0.2`. - Step 11, `PUT /user/starred/nbd-wtf/nostr-tools` with no credential → HTTP 401 `{"message":"Requires authentication","documentation_url":"https://docs.github.com/rest","status":"401"}`. Confirms the write surface is credential-gated; no credential was guessed or supplied. --- ## 7. Egress log (verbatim, proxy) 14 CONNECTs: 11 × `api.github.com`, 1 × `github.com`, 2 × `docs.github.com`. **Blocked: 0.** The two `docs.github.com` tunnels are the subdomain-match note in the header; neither was target-initiated (GitHub never redirected the client anywhere). ``` CONNECT Sep 05 13:06:35.067 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:35.122 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:35.213 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:35.255 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:35.484 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:35.589 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:35.683 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:35.725 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:35.856 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:35.899 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.002 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.114 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.216 [11]: Request (file descriptor 5): CONNECT github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.259 [11]: Established connection to host "github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.382 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.424 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.554 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.672 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.767 [11]: Request (file descriptor 5): CONNECT docs.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.792 [11]: Established connection to host "docs.github.com" using file descriptor 6. CONNECT Sep 05 13:06:36.851 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:06:36.896 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:10:12.417 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:10:12.497 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:10:12.663 [11]: Request (file descriptor 5): CONNECT api.github.com:443 HTTP/1.1 CONNECT Sep 05 13:10:12.706 [11]: Established connection to host "api.github.com" using file descriptor 6. CONNECT Sep 05 13:10:13.077 [11]: Request (file descriptor 5): CONNECT docs.github.com:443 HTTP/1.1 CONNECT Sep 05 13:10:13.192 [11]: Established connection to host "docs.github.com" using file descriptor 6. ``` --- ## 8. Safety observations | Check | Result | |---|---| | Prompt injection in fetched content | None. `grep -ioE 'ignore (all )?previous|you are now|as an ai|system prompt|disregard'` over every collected body → no matches. `llms.txt` describes its own APIs only (§5) | | Key / seed-phrase request | None | | Unbounded approval | N/A — no payment or signing surface | | Env probe | None | | Overcharge | N/A — $0 | | Malicious payload | None; all bodies `application/json` or `text/plain` | | TLS | HTTP/2 over TLS on all hosts via CONNECT; HSTS `max-age=31536000; includeSubdomains; preload` on `api.github.com` | | Secrets in URLs | None required. (GitHub's deprecation of query-parameter auth was not fetched in this run and is not claimed) | | PII requested | None | | Rate-limit contract | Fully machine-readable: `x-ratelimit-{limit,remaining,used,reset,resource}` on every response, `/rate_limit` endpoint, 403/429 documented. The one 403 in this run was this contract working as documented against a shared IP | | `security.txt` | Present with a real contact (HackerOne) and a bug-bounty policy URL | **Findings: none.** `safety.penalty_total: 0`. --- ## 9. Scoring rationale | Dimension | Rung | Why | |---|---|---| | `access` | **25** | The reference task needed nothing beyond HTTP and three documented headers. The unauthenticated path is documented, rate-limited, and worked. (What the agent *cannot* possess — an account — is recorded on the entry as the wall for any write task; it does not penalise this task) | | `payment` | **25** | Free; no card, no wallet, nothing requested | | `interface` | **20** | An official MCP server exists (hosted and installable, §4) and the REST API has a published, versioned OpenAPI description. Rung awarded on the rubric's literal condition; the MCP server was not driven because every path into it needs an account | | `completion` | **20** | Completed with zero human input. The one failure (403, search bucket already empty) carried its own fix in `x-ratelimit-reset`; no out-of-band knowledge was used — the root hypermedia map, the docs' curl example, and the response headers were sufficient | | `docs` | **10** | `llms.txt` on the docs host; actionable errors (the 403 body links the exact troubleshooting anchor, the 401 body links the docs); stable versioned schema (`X-GitHub-Api-Version: 2022-11-28`, OpenAPI 3.0/3.1 kept in sync with request validation); examples run as written (the search and repo calls are the documented shape and returned the documented fields); rate limits documented *and* observable in headers | | `permissive-tos` bonus | **not awarded** | The TOS is API-permissive but explicitly not agent-permissive as principal: a machine account must be "set up by an individual human who accepts the Terms on behalf of the Account", and bot-registered accounts "are not permitted". The `llms.txt` on the docs host would satisfy the bonus's literal wording, but awarding it would misdescribe the terms to the next agent. Numerically moot (sum already ≥100) | | `security-posture` bonus | **not awarded** | `security.txt` with a real contact is evidenced; scoped/revocable credentials and no-secrets-in-URLs were not fetched in this run, so not claimed | | `permission.status` | **granted** (scoped) | §0 | Dimension vector emitted: `access 25, payment 25, interface 20, completion 20, docs 10`; bonuses none; penalties none. Arithmetic is `tools/score.ts`'s job. **Agent-native verdict, first-hand.** For reading, this is as good as a public API gets: the root URL is a hypermedia map, every response tells you your remaining budget, the error bodies link the fix, and the OpenAPI is the same file GitHub validates requests with. The friction is entirely at the identity boundary: a cold-start agent cannot lawfully get an account, so anything beyond 60 reads/hour or any write requires a human to create the account and mint a token first. Once that token exists the API is a library. --- ## 10. Sandbox feedback 1. `--allow github.com` admits every `*.github.com` because the filter anchors on `(^|\.)domain$`. Two `docs.github.com` requests went through that I meant as a blocked-egress control. Subdomain matching is probably the right default for CDNs, but it should be documented in the script header and the `up` banner should print the effective regex, so a probe author knows the real allowlist. 2. Unauthenticated GitHub rate limits are per source IP and the wave runs several sandboxes behind the same host IP. My first search request found the 10/min search bucket already empty. Concurrent probes of the same target should be serialised, or the orchestrator should expect and tolerate one honoured-reset retry, as done here. 3. `/out` was writable this run (the chown fix from the frankfurter probe works).