Skip to content

SP-1.6 caller-compat audit — GET /api/cairn/stones

SP-1.6 caller-compat audit — GET /api/cairn/stones

Section titled “SP-1.6 caller-compat audit — GET /api/cairn/stones”

Allowlist: sort, type, org, agent, archived, page, limit.

Literal path grep (api/cairn/stones) plus indirect-construction grep (cairn/stones, /stones, stonesPath, STONES, cairnet_stones_endpoint, endpoint+concat) across the entire ~/code/workspace tree (excluding node_modules, dist/, .claude/worktrees/). Targets explicitly checked per plan: petrova-codes/{cli,dashboard,api,host}, rocky-hq/cairnet frontend + its Next.js proxy route, and kahn-hq (incl. frontend/src/auth.ts soak/M2M client). Doc/markdown matches, the Pebble route definition (pebble/src/pebble/api/routes/cairn.py), and test files that hit the endpoint are excluded from blast-radius per task definition (tests updated in T2). No non-test Pebble component calls its own stones endpoint.

Caller (repo:file:line)Params sentVerdict
petrova-codes:cli/src/probes/rocky.ts:91agent, limit=100, sort=recent (literal template at line 91; ?since= deliberately not sent — comment L89-90)OK
petrova-codes:cli/src/verbs/rocky_status.ts:103agent, limit=100, sort=recent (literal template at line 103; independently verified from source, not assumed; ?since= not sent — comment L101-102)OK
rocky-hq:cairnet/src/lib/cairn-api.ts:97 (listStones)sort, type, org, agent, archived, page, limit only — every params.set key at L88-96 is from FeedFilters (cairnet/src/lib/types.ts:89-97, exactly {sort,type,org,agent,archived,page,limit}, no other keys); call sites feed/page.tsx:24 and feed/archive/page.tsx:16 pass only allowlisted keysOK
rocky-hq:cairnet/src/app/api/cairn/stones/route.ts:4-6 (Next.js GET proxy → proxy.ts proxyToBackend)forwards request.nextUrl.search verbatim; injects NO extra param (no cache-buster/tracing — proxy.ts builds new URL(PEBBLE_API_URL+path) and adds nothing). This is a public-at-edge proxy route: cairnet middleware matcher (middleware.ts:82-85, "/((?!_next/static|_next/image|favicon.ico|api/).*)") has a negative lookahead that excludes api/, so airlock auth middleware does NOT run on this route — it is reachable with arbitrary query params by any airlock-authenticated client (browser/curl/script), not solely the typed listStones caller. Verdict remains OK because the route forwards verbatim and injects nothing; the post-T2 strictness that surfaces through it is bounded (see below) and is intended in-contract SP-1.6 behaviour.OK

Non-GET / non-callers (recorded for completeness, out of blast-radius): petrova-codes:cli/src/cairnet/client.ts:22 is POST-only (postStone), not a GET caller. petrova-codes:cli/src/types.ts:134 is a type literal, not a call. kahn-hq/frontend/src/auth.ts touches only /api/config — it does NOT list cairn stones; no kahn-hq code references cairn/stones at all (all kahn-hq Step-1 hits were docs/findings/decisions markdown).

No violations — safe to enable the 400. No first-party/production code path emits a non-allowlisted param: the two CLI callers (cli/src/probes/rocky.ts:91, cli/src/verbs/rocky_status.ts:103) and listStones (cairnet/src/lib/cairn-api.ts:97) with its two call sites all send only allowlisted keys.

The route.ts proxy is public-at-edge: the cairnet middleware matcher (middleware.ts:82-85) excludes api/, so airlock auth middleware does not run on it and any airlock-authenticated cairnet client can hand-craft cairnet.devarno.cloud/api/cairn/stones?extra=1. Two bounding facts make this in-contract rather than a regression: Pebble GET /stones requires cairn:read (devarno-cloud/pebble/src/pebble/api/routes/cairn.py:255, Depends(require_permission("cairn:read"))), and the proxy forwards the session cookie and relays res.status verbatim. So the post-T2 blast radius is exactly: an authenticated user passing an extra param gets a Pebble 400 relayed back — the intended SP-1.6 strictness. No first-party caller, automated path, or unauthenticated client is affected.