# Probe transcript — Hacker News API (e2e) | | | |---|---| | Entry | `entries/hacker-news.yaml` | | Target | API `https://hacker-news.firebaseio.com/v0` · docs https://github.com/HackerNews/API | | Mode | `e2e` | | Probe id | `hacker-news-e2e` | | Rubric | `rubric/v0.2.json` sha256 `cd605a44799f49951bf078fda63c5b21ad3c07db494f41d38ad19ce729c5551a` | | Date | 2026-09-05 | | Sandbox up | 2026-09-05T13:12:19Z | | Sandbox down | 2026-09-05T13:12:22Z | | Wall clock | 3 s (epoch 1788613939 → 1788613942; from the shell invocation that brought the sandbox up to completion of teardown) | | 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`. Nothing to redact — no credential, token, cookie, or PII was sent or received | | User-Agent sent | `ag3ntsearch-probe/0.2 (https://ag3ntsearch.com; agent-nativeness index probe) curl` on every request | Every request below was issued from inside the probe sandbox (`.claude/skills/probe/probe-sandbox.sh`), on an `--internal` Docker network whose only egress is an allowlisting tinyproxy sidecar. No request to any target host was made from the host shell. ```sh ./.claude/skills/probe/probe-sandbox.sh up --id hacker-news-e2e --target hacker-news.firebaseio.com ``` Allowlist: `hacker-news.firebaseio.com`. Nothing else — see §0 for why the website and the TOS host were deliberately left off. --- ## 0. Permission gate (gate mode, before any sandbox traffic) Read via WebFetch on 2026-09-05, all public pages: - **https://www.ycombinator.com/legal/** — "Welcome to the Y Combinator website (including all subdomains, the "Site"), which is operated by Y Combinator" and, under Intellectual Property Rights: "In connection with your use of the Site you will not engage in or use any data mining, robots, scraping or similar data gathering or extraction methods." No clause mentions the API. The document names Hacker News only to incorporate the HN Guidelines and to exclude HN submissions from "Personal Information". - **https://news.ycombinator.com/newsguidelines.html** — no sentence mentions bots, automation, scraping, crawlers, the API, or programmatic access. - **https://news.ycombinator.com/robots.txt** — `User-Agent: *`, `Crawl-delay: 30`, Disallow on `/collapse? /context? /fave? /flag? /hide? /login /logout /r? /reply? /submitlink? /vote? /x?`. Interactive endpoints only; `/item` is not disallowed. - **https://hacker-news.firebaseio.com/robots.txt** — `User-agent: *` / `Allow: /*.json$` / `Allow: /*.json?*$` / `Disallow: /` (re-fetched from the sandbox in §1, byte-identical). - **https://github.com/HackerNews/API** (README) — "In partnership with Firebase, we're making the public Hacker News data available in near real time." … "There is currently no rate limit." … "Please email api@ycombinator.com if you find any bugs." No license or terms sentence for the data. Decision: `unstated`. The API host is not a ycombinator.com subdomain, so the TOS's anti-robot clause does not reach it on its own terms; the operator published the API for programs and its bot policy on the API host allows exactly the paths the API serves. But no terms instrument grants use of the API, and the only terms instrument on the property forbids robots on the sibling website — an agent is relying on a scope reading, not on a written permission. Not `prohibited`: nothing on the API host or in the README indicates the operator does not want programmatic clients there; the opposite. Consequences applied to the run: - **Zero requests to news.ycombinator.com or www.ycombinator.com** from the sandbox. The 30-second Crawl-delay makes even three fetches a 60-second wait for no new evidence; the quotes above are gate reads. - **Only `*.json` paths on the API host.** `/llms.txt` and `/.well-known/security.txt` are disallowed by that host's robots.txt and were not requested. Their existence is recorded as unverified, not absent. --- ## 1. Reference task — PASS > With no credentials of any kind, GET > `https://hacker-news.firebaseio.com/v0/topstories.json`, take the first integer > id in the returned array, GET `https://hacker-news.firebaseio.com/v0/item/.json`, > and confirm the response is HTTP 200 JSON whose `"id"` equals that id and whose > `"title"` is a non-empty string. Authored for this run (no gate-mode entry existed). It is two requests because the API has no "top story" endpoint — the list is ids only, and the README is explicit that the client resolves them. ### 1.0 robots.txt on the API host (first request, before anything else) ``` GET https://hacker-news.firebaseio.com/robots.txt http=200 time=0.112s size=60 ``` ``` User-agent: * Allow: /*.json$ Allow: /*.json?*$ Disallow: / ``` ### 1.1 `GET /v0/topstories.json` ``` http=200 time=0.164s size=4501 ``` Response headers (verbatim; first two lines are the sandbox's own proxy): ``` HTTP/1.1 200 Connection established Proxy-agent: tinyproxy/1.11.2 HTTP/1.1 200 OK Server: nginx Date: Sat, 05 Sep 2026 13:12:20 GMT Content-Type: application/json; charset=utf-8 Content-Length: 4501 Connection: keep-alive access-control-allow-origin: * Cache-Control: no-cache Strict-Transport-Security: max-age=31556926; includeSubDomains; preload ``` Body (first bytes): `[49570669,49563355,49571634,49568506,49569896,49575034,…` — 500 ids (`jq length` → 500, matching "Up to 500 top and new stories"). `first_id=49570669`. ### 1.2 `GET /v0/item/49570669.json` ``` http=200 time=0.180s size=619 ``` Body (verbatim, `kids` array of 44 ids elided): ```json {"by":"negura","descendants":313,"id":49570669,"score":585,"time":1788558721,"title":"Actively exploited sandbox RCE in all Chromium versions","type":"story","url":"https://nvd.nist.gov/vuln/detail/cve-2026-85046"} ``` Machine check: ``` $ jq -e --argjson id 49570669 '(.id==$id) and (.title|type=="string") and (.title|length>0)' true REFERENCE_TASK_PASS ``` No `Authorization` header, no cookie, no API key, no query token. Two requests, 0.34 s combined. --- ## 2. Do the docs run as written? The README's one worked example is `https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty`. ``` http=200 time=0.107s size=456 ``` ```json { "by" : "dhouston", "descendants" : 71, "id" : 8863, "kids" : [ 9224, 8917, 8884, … ], "score" : 104, "time" : 1175714200, "title" : "My YC app: Dropbox - Throw away your USB drive", "type" : "story", "url" : "http://www.getdropbox.com/u/2/screencast.html" } ``` Runs verbatim, pretty-printed as `?print=pretty` documents. 1/1. --- ## 3. Error shapes (undocumented; learned by hitting them) | Request | Status | Body | Note | |---|---|---|---| | `GET /v0/item/0.json` (nonexistent id) | **200** | `null` | A miss is a success with a four-byte body. Nothing in the README says so. | | `GET /v0/nope.json` (undocumented path) | **401** | `{"error":"Permission denied"}` | Firebase security-rules response. Looks like an auth wall; the documented surface has none. | Neither behaviour is in the README. A client that treats 401 as "get a key" would go looking for a key that does not exist. This is the main reason `docs` is 4. --- ## 4. Machine-readable surface None. No OpenAPI/JSON Schema, no `llms.txt` (not requested — robots-disallowed path; see §0), no MCP server. The README is the entire documentation and it is a human-facing markdown page. It is candid about that: "The v0 API is essentially a dump of our in-memory data structures. … It's not the ideal public API, but it's the one we could release in the time we had." `interface: 12` (documented REST, no schema file). --- ## 5. Rate limits README: "There is currently no rate limit." Observed: no `RateLimit-*`, `X-RateLimit-*`, or `Retry-After` header on any response (`grep -iE "ratelimit|retry-after|x-rate"` over all headers → no match). Six requests in ~1 s were served without throttling. No attempt was made to find a ceiling (observe, never exploit). --- ## 6. Egress log Collected via `probe-sandbox.sh collect` to `transcripts/tmp-hacker-news-e2e/egress.log` (gitignored), reproduced in full here. 7 CONNECTs: 6 to the single allowlisted host, 1 operator-initiated control test that was refused. ``` DEBUG Sep 05 13:12:19.744 [11]: sending stored logs NOTICE Sep 05 13:12:19.746 [11]: Initializing tinyproxy ... NOTICE Sep 05 13:12:19.746 [11]: Reloading config file (/etc/tinyproxy/tinyproxy.conf) WARNING Sep 05 13:12:19.748 [11]: You SHOULD set a UserName in the config file. Using current user instead. NOTICE Sep 05 13:12:19.748 [11]: Reloading config file finished DEBUG Sep 05 13:12:19.749 [11]: done sending stored logs CONNECT Sep 05 13:12:20.122 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.123 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.157 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:20.260 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.274 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.327 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:20.429 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.430 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.466 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:20.627 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.630 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.664 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:20.738 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.739 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.780 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:20.882 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:20.883 [11]: Request (file descriptor 5): CONNECT hacker-news.firebaseio.com:443 HTTP/1.1 CONNECT Sep 05 13:12:20.918 [11]: Established connection to host "hacker-news.firebaseio.com" using file descriptor 6. CONNECT Sep 05 13:12:21.108 [11]: Connect (file descriptor 5): 172.24.0.3 CONNECT Sep 05 13:12:21.109 [11]: Request (file descriptor 5): CONNECT example.com:443 HTTP/1.1 NOTICE Sep 05 13:12:21.111 [11]: Proxying refused on filtered domain "example.com" ``` **Blocked attempts: 1, and it was mine, not the target's.** `CONNECT example.com:443` → `Proxying refused on filtered domain "example.com"`; client side `curl: (7) CONNECT tunnel failed, response 403`. Deliberate fail-closed control test. **Target-caused blocked egress attempts: 0.** The target never redirected or referenced any host outside its own; the only off-host URLs in its output are the story `url` fields, which are data and were not followed. --- ## 7. Safety observations | Check | Result | |---|---| | Prompt injection / hidden text | None. `grep -oiE "ignore (all )?previous\|you are now\|as an ai\|system prompt\|disregard …\|display: *none\|visibility: *hidden\|"` over all six response bodies → no match. Zero occurrences of `llm`, `assistant`, `ai agent` in any body. | | Key / seed-phrase request | None. | | Unbounded approval | N/A — no payment or signing surface. | | Env probe | None. | | Overcharge | N/A — $0. | | Malicious payload | None. Every response was `application/json`. | | TLS | HTTPS via CONNECT on every request; HSTS `max-age=31556926; includeSubDomains; preload`. | | Secrets in URLs | None exist in this system. | | PII requested | None. Story bodies contain public HN usernames (`by`), which is the data the API exists to serve. | | CORS | `access-control-allow-origin: *` — browser-side agents can call it directly. | | `security.txt` | **Not requested** (robots-disallowed non-.json path). Unverified. | | `robots.txt` (API host) | Allows bots on `*.json` only. Honoured. | **Findings: none.** `safety.penalty_total: 0`. --- ## 8. Scoring rationale | Dimension | Rung | Why | |---|---|---| | `access` | **25** | Nothing beyond HTTP. No key exists on the documented surface; the one 401 was for an undocumented path. | | `payment` | **25** | Free; no instrument presented or requested. | | `interface` | **12** | Documented REST, no schema file, no MCP. | | `completion` | **20** | Reference task completed, zero human input, zero out-of-band knowledge — the README specified both endpoints and the id-list-then-resolve pattern. | | `docs` | **4** | One README; the example runs; "no rate limit" is stated; but no schema, no llms.txt, no error table, and the two error shapes observed (200 `null`, 401 on a typo) are undocumented and misleading. "Thin" is the honest rung. | | `permissive-tos` | not awarded | The API host's robots.txt Allow is a crawler directive; the only TOS instrument on the property is anti-robot (scoped to a different host). No llms.txt, no bot policy statement. | | `security-posture` | not awarded | security.txt unverified (not requested). | | `permission.status` | `unstated` | See §0. | Dimension vector emitted: `access 25, payment 25, interface 12, completion 20, docs 4`; bonuses none; penalties none. Arithmetic is `tools/score.ts`'s job. --- ## 9. Sandbox feedback 1. `/out` was writable this run — the chown-at-`up` fix from the frankfurter probe works; `collect` returned every artifact. 2. The proxy logs a plain-HTTP `GET http://…` request line verbatim (seen in the arXiv run, same wave) but only `CONNECT host:443` for HTTPS — fine, but it means the egress log is path-level evidence only for cleartext traffic. 3. No problems.