fix(rate-limit-guard): scope the tee's account forward-pass claim to what the writer does - #1734
Conversation
…ass claim to what the filter does
The reader contract described the tee writer's forward-pass filter
accurately and then drew a conclusion broader than the filter supports.
Two bullets promised that the release adding an account identifier
"upgrades this file without a plugin change" / "costs no plugin change".
The filter is `to_entries` over the root object only, with
`test("account"; "i")` matching a substring of the key, and it has no
else branch. The promise holds only for a field that is both top-level
and `account`-named; a nested field (`user.account_uuid`) or one named
`user`, `identity`, `org`, or `seat` is dropped, and dropped silently —
in a contract that fail-closes on every other unresolvable input.
Both bullets now state the filter's actual reach, name the silent drop,
and scope the no-change claim to that one shape. The tee-shape bullet
owns the statement; the invariant bullet points at it instead of
restating it. `statusline-tee.sh` is unchanged — widening the filter is
a design question owned by TODO(#1218).
Closes #1685.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the value-scope of a passed key
Review found the first pass fixed the least-read of four surfaces and
introduced one error of its own.
Two surfaces still carried the unqualified promise: README.md's
known-gap bullet ("the wrapper automatically adopts any future
account-identifying field the schema grows") and statusline-tee.sh's
header comment, whose "so ..." inference did not carry forward the
qualifiers its own parenthetical stated. The README is the plugin's
front door, and the harm the item names — a planner reading "the writer
is already ready" and skipping the filter — routes through it at least
as readily as through the reference doc.
The first pass also implied that nesting defeats the forward-pass. It
does not. Selection is on the top-level key name alone, and a selected
key crosses with its whole value, nested objects included; the drop is
caused by no top-level key matching, not by depth. Since a passed key
may therefore carry an object of arbitrary user/AI-influenced strings,
the untrusted-value discipline now covers the subtree rather than a
scalar.
Also unified on "writer change" (the contract had split between "writer
change" and "filter change"), and dropped two phrasings inherited from
the item body that do not survive contact with the code: the filter also
selects rate_limits, so it is not "the whole of" the forward-pass, and
`map(select(...))` has no branch to lack an else.
statusline-tee.sh's behavior is unchanged — the jq filter is
byte-identical to main and only its header comment moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 56s —— View job PR Review
This is a doc-only correctness fix. I verified each prose claim against the jq filter byte-for-byte. Here's the full read. jq filter verificationThe filter at
Reader contract ("Tee file shape")
Reader contract ("Invariants")
README
CHANGELOGThe Version bump0.3.4 → 0.3.5 is appropriate. The playbook requires a Version collision note (flagged in PR body): PR #1692 also claims Observations (non-blocking)
VerdictThe documentation now faithfully represents the filter's behavior. No executable surface changed. Scope is tight, all four overstating surfaces are corrected, no site is missed, and the CHANGELOG is accurate. This is ready to merge on its own merits — the only external dependency is the version collision with #1692 being resolved at merge time. |
|
Claude finished @kyle-sexton's task in 2m 51s —— View job Security Review
No security issues introduced by this PR. Findings below. Scope of executable changesThe only executable file touched is Data-flow review of the tee script (pre-existing, confirmed clean)The script reads raw Security-positive changes in the documentationThe contract corrections in
Neither the injection surface nor the trust model changed; the documentation now matches the code's actual behavior instead of promising broader coverage. Pre-existing observations (not introduced by this PR, noted for completeness)
Both observations predate this PR and are outside its scope. |
🤖 Agent-authored (autonomous worker lane).
Closes #1685.
Summary
The plugin told readers that the release adding an account identifier upgrades the tee file for free. The writer cannot guarantee that.
scripts/statusline-tee.shselects on the top-level key name only —to_entriesover the root object — matchingaccountas a case-insensitive substring, and an unmatched key is dropped with no diagnostic. So the promise holds only when the new field's own top-level key name containsaccount. A field calleduser,identity,org, orseat, or anaccount_uuidburied inside a non-matching object, vanishes silently — in a contract that fail-closes on every other unresolvable input.The doc now follows the code.
statusline-tee.sh's behavior is unchanged: the jq filter is byte-identical tomainand only its header comment moved. Widening the filter is a design question owned byTODO(#1218), which the issue and triage both scoped out of this correction.What changed
Four surfaces carried the same overstatement, not the one the issue quotes. The issue quotes only the reader contract's Invariants bullet; fixing that alone would have shipped a document contradicting its own Tee-file-shape section, and would have left the claim standing on the plugin's front door.
reference/reader-contract.md"Tee file shape"reference/reader-contract.md"Invariants" (the quoted one)README.mdknown-gap bulletscripts/statusline-tee.shheader commentso ...inference did not carry them forwardOne correction the issue did not anticipate. An earlier draft of this PR implied that nesting defeats the forward-pass. It does not: a selected top-level key crosses with its whole value, nested objects included (
account_info: {uuid, display_name}arrives complete). The drop is caused by no top-level key matching, not by depth. Because a passed key may therefore carry an object of arbitrary user/AI-influenced strings, the untrusted-value discipline is restated to cover the subtree rather than a scalar — the parse-with-a-JSON-parser, never-interpolate rule applies to all of it.Terminology unified on "writer change"; the contract had split between "writer change" and "filter change" for the same fact.
Two phrasings from the issue body were rejected as not surviving contact with the code: the filter also selects
rate_limits, so it is not "the whole of" the forward-pass; andmap(select(...))has no branch to lack an else — the accurate statement is that unmatched keys are dropped with no diagnostic.Verification
This is a doc-only prose correction with no executable surface. No test can reach the claim, and I did not invent coverage for it. Verification is the read of the filter against the new prose, plus the gates below. The one executable file touched changed only a comment;
git diff origin/main -- scripts/statusline-tee.shshows two comment hunks and nothing else.check-changelog-parity.sh --checkcheck-changelog-parity.sh --check-bump origin/maingenerate-catalog.mjs --checkvalidate-plugins.shvalidate-plugin-contracts.mjscheck-cross-plugin-source-drift.sh --checkcheck-changed-skills.sh origin/maincheck-shell-portability.sh origin/mainshellcheck+bash -nonstatusline-tee.shmarkdownlint-cli2on all 3 changed.mdThe operable floor (the block the three lane skills inline byte-identically per loop-lane convention §6) is untouched, so no lane-skill fan-out is needed and §6 needs nothing.
CHANGELOG.md:146deliberately still carries the old wording. It is a historical[0.2.0]release note, not a live surface — changelog entries are immutable history. Not a missed site.Reviewer note — version collision with #1692
This PR takes
rate-limit-guard0.3.4 → 0.3.5. PR #1692 also claims 0.3.5 for this plugin. Whichever merges second must rebase and re-bump, andcheck-changelog-parity.sh --check-bumpwill fail for the loser until it does. Not pre-emptively jumping to 0.3.6, since that guesses at merge order and would leave a version gap.The bump is warranted by
docs/MIGRATION-PLAYBOOK.md:373-375— aversionbump is the only delivery vehicle, so an unbumped plugin never delivers a corrected reference to consumers. Note--check-bumponly fires when a version changes; it does not itself compel one.Related
TODO(#1218)— widening the filter is that issue's call, explicitly not this one's. rate-limit-guard: same-machine account rotation is real operation — tee file's single-account assumption feeds wrong windows to running lanes (account-identity design) #1218 specifies no field name, so this PR makes no claim about whether its eventual field will satisfy the existing filter.plugins/rate-limit-guard/reference/reader-contract.mddocs/conventions/loop-lane/README.md🤖 Generated with Claude Code