Skip to content

toolplane: one tool for the reads, so asking a new question is not a release - #35

Merged
github-actions[bot] merged 1 commit into
developfrom
feat/reading-is-not-the-dangerous-part
Sep 10, 2026
Merged

github-actions[bot] merged 1 commit into
developfrom
feat/reading-is-not-the-dangerous-part

Conversation

@traviswu-bigstack

Copy link
Copy Markdown
Contributor

What

The three hand-written cube-cos-api reads become keys of one tool, cube_cos_read, over a catalogue of 21 paths. New Tool.Catalog field, CubeCOSReads and cubeCOSReadsHeldBack as data, and the vendored operation list gains methods so a read can be checked as a read.

Why

Nine tools against an API that declares 102 paths made every "can you also check X" a new allowlist entry, a build and a deploy. That is a release cycle, not a security property. The property is that a caller cannot express a request the allowlist does not contain — and a finite set of twenty-one reads has it exactly as a finite set of three does.

A key is a map lookup, which is the finite-value-set rule Params already applies, in its strongest form: no grammar to get wrong, and nothing outside the set to reject because a value outside the set is not a key. The method is GET structurally, as Get is. Query parameters are not expressible, so watch=true and the event stream behind it are unreachable by construction rather than by exclusion.

Reviewer notes

One tool, not twenty-one. Twenty-one names and descriptions would sit in front of the model every turn for reads differing only in which noun they return, and a tool spec is part of the SaaS's prompt stamp — so a tool per path would move that stamp every time the catalogue grew. One tool with a key set costs one name and one enum.

⚠️ This moves the prompt stamp (the tool set changed). Both baseline arms are already non-comparable since slice 3 added a fifth stamp; the re-record is batched and nothing here touches evals/baseline.json.

Admission is opt-in, and visibility is enforced. A path is reachable because it is in CubeCOSReads, not because the API serves it — an exclusion list would make the next sensitive endpoint upstream reachable the day it shipped. Held back: settings and the delivery settings under it (SMTP credentials, webhook URLs), integrations/* (storage-vendor logins), licenses, me, notifications, supportFiles (unbounded — a 32 KiB slice of an archive is not a read), grafana/* (payloads that may embed a token), and the two .csv duplicates.

Opt-in alone would leave a read the API gains tomorrow silently unreachable, so the held-back set is data and TestEverySimpleGetIsAdmittedOrHeldBack requires the two sets to cover every zero-parameter GET. A new upstream read fails a test naming it.

How a reviewer sees the whole surface: two maps in allowlist.go, one admitted with its paths and one held back with its reasons, and a test that they are exhaustive and disjoint.

Testdata now records METHOD PATH. A path-only list could not tell a read from a write sharing a URL, which is exactly what a catalogue claiming to reach only reads needs checked.

One fetch, not two. Both read forms end in fetch, so the result cap, the truncation marker and the audit record are written once — a second copy is a second place for the marker to go missing, and tool-0010 measures whether the model reports a cut it was told about.

Reads are served at every level, observe included. This widens observe clusters; reading is what observe is for, and TestTheCatalogueIsServedAtObserve pins it.

Deletion hole: unchanged in kind, wider in scope — a path dropped upstream still passes against the snapshot, now across 21 paths rather than 3. The failure is a 404 at runtime, not an unintended reach.

Breaks proved, each watched to fail a named test and restored: the catalogue key check short-circuited (TestAKeyOutsideTheCatalogueIsRefused), and the unfilled-placeholder validation disabled (TestACatalogueEntryMustBeAWellFormedRead/unfilled_placeholder).

go test ./... green, gofmt/go vet clean.

Docs

bigstack-handbook#612 — as-built in read-only-plane-end-to-end.md.

…release

Three hand-written cube-cos-api reads against an API that declares 102 paths
made every "can you also check X" a new allowlist entry, a build and a deploy.
That is a release cycle, not a security property: the property is that a caller
cannot express a request the allowlist does not contain, and a finite set of
twenty-one reads has it exactly as a finite set of three does.

Catalog is that set — a key the caller supplies, mapped to the path it selects.
A key is a map lookup, which is the finite-value-set rule Params already
applies, in its strongest form: there is no grammar to get wrong and nothing
outside the set to reject, because a value outside the set is not a key. The
method is GET, structurally, for the same reason Get is; query parameters are
not expressible, so watch=true and the event stream behind it are unreachable
by construction rather than by exclusion.

One tool rather than twenty-one. Twenty-one names and descriptions sit in front
of the model on every turn for reads that differ only in which noun they return,
and tool specs are what the SaaS fingerprints as its prompt stamp — so a tool
per path would move that stamp every time the catalogue grew. One tool with a
key set costs one name and one enum, and adding a read moves the enum.

Admission is opt-in: a path is reachable because it is written in CubeCOSReads,
not because the API serves it. The opposite rule fails the wrong way — the next
sensitive endpoint upstream would be reachable the day it shipped. Settings,
integrations and licenses are held back because they carry SMTP passwords,
storage logins, webhook URLs and license keys, none of which help diagnose a
cluster and all of which would land in a transcript; support bundles because a
32 KiB slice of an archive is not a read; the .csv variants because they
duplicate reads already admitted.

Opt-in alone would leave a read the API gains tomorrow silently unreachable, so
cubeCOSReadsHeldBack is data and a test requires the two sets to cover every
zero-parameter GET between them. A new upstream read then fails a test naming
it, and somebody classifies it. Admitting stays deliberate; ignoring stops being
possible.

The vendored operation list now records METHOD PATH rather than paths alone, so
a Get: entry naming a path the API only POSTs to is a bug the check can see. It
also lets the catalogue be checked as reads specifically, which is its whole
claim.

Both read forms end in one fetch, so the result cap, the truncation marker and
the audit record are written once. A second copy would be a second place for the
marker to go missing, and tool-0010 measures whether the model reports a cut it
was told about.

Signed-off-by: Travis Wu <travis.wu@bigstack.co>
@traviswu-bigstack
traviswu-bigstack force-pushed the feat/reading-is-not-the-dangerous-part branch from e053343 to 6af3234 Compare September 10, 2026 10:06
@traviswu-bigstack
traviswu-bigstack marked this pull request as ready for review September 10, 2026 10:10
@traviswu-bigstack traviswu-bigstack added the done Apply to a PR to trigger the fast-forward merge label Sep 10, 2026
@github-actions
github-actions Bot merged commit 6af3234 into develop Sep 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

done Apply to a PR to trigger the fast-forward merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant