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.
Scope of search
Section titled “Scope of search”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.
Callers found
Section titled “Callers found”| Caller (repo:file:line) | Params sent | Verdict |
|---|---|---|
| petrova-codes:cli/src/probes/rocky.ts:91 | agent, 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:103 | agent, 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 keys | OK |
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).
Verdict
Section titled “Verdict”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.
Blast radius (bounded)
Section titled “Blast radius (bounded)”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.