toolplane: one tool for the reads, so asking a new question is not a release - #35
Merged
github-actions[bot] merged 1 commit intoSep 10, 2026
Merged
Conversation
…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
force-pushed
the
feat/reading-is-not-the-dangerous-part
branch
from
September 10, 2026 10:06
e053343 to
6af3234
Compare
traviswu-bigstack
marked this pull request as ready for review
September 10, 2026 10:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The three hand-written cube-cos-api reads become keys of one tool,
cube_cos_read, over a catalogue of 21 paths. NewTool.Catalogfield,CubeCOSReadsandcubeCOSReadsHeldBackas 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
Paramsalready 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, asGetis. Query parameters are not expressible, sowatch=trueand 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.
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:settingsand 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.csvduplicates.Opt-in alone would leave a read the API gains tomorrow silently unreachable, so the held-back set is data and
TestEverySimpleGetIsAdmittedOrHeldBackrequires 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, andtool-0010measures whether the model reports a cut it was told about.Reads are served at every level,
observeincluded. This widensobserveclusters; reading is whatobserveis for, andTestTheCatalogueIsServedAtObservepins 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 vetclean.Docs
bigstack-handbook#612 — as-built in
read-only-plane-end-to-end.md.