feat(grants): answer a refused grant with 403, not the 400 a typo gets #880
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!880
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/hs68-grant-denial-403"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
IsGrantDenialis wired, and answering #825's three questions changed what the fix is.First, a correction to the headline
Nothing in this repository retries it.
Submitalready handled a denial correctly before this branch: it writes the job record, transitions it tofailedwith the outcomenot permitted, emitsjob.denied, and returns. There is no retry loop, no backoff, and no second attempt.enqueueis never reached.The bare
return erratjobrunner.go:169that the issue points at is insidepermits, whose only caller is that sameSubmit, six lines above the code that records the denial.The retry the issue is right about is the caller's, and a status code invites it. That is the real defect and it is the one fixed here.
The three questions
1. What the retry costs today. Zero model calls and zero tool rounds - the denial is refused before the job is queued, so nothing executes. What it costs is a caller re-sending a request that can never succeed, and on HTTP the service told it to.
2. Whether the caller sees anything different. No, and this is the #449 family exactly as the issue anticipated. Every grant denial fell to the
defaultarm ofwriteJobError:The same answer an unknown job kind gets, and the same answer a malformed body gets. 400 asserts the request was wrong. For a grant the deployment does not hold and will not hold, that is a true-sounding sentence pointing at the wrong thing: the caller reads "fix your request", fixes nothing, and tries again.
The Discord path is the same shape - every
Submiterror becomesjob could not be accepted, so a refused member and a member who hit a full queue read the identical notice.3. Whether wiring it is the whole fix. No, and the issue was right that acting on the classification is the part worth designing. The classification is one line. What it enables:
403with a new code,sirens_echo.jobs.not_permitted, and the phrasejob kind is not permitted.503for a full queue is untouched and remains the one job refusal that is this service's fault rather than the caller's.Why 403 leaks nothing
Worth stating, because this repo collapses not-found and not-owner into one
404on purpose so an id cannot be probed for.That collapse protects other principals' records. This one would protect nothing: a principal learns only about its own grant, and
GrantedKindsexists specifically to answer that question without making them discover it by being refused. The denial's reason string stays out of the response body, so403carries the fact and not the table.Changes
IsGrantDenialcalled fromwriteJobErrorand the Discord command path.exceptionJobNotPermittedadded,faultCaller, stagejobs. Catalog bound 34 to 35.TestARefusedGrantIsForbiddenRatherThanABadRequestasserts the 403 and that a malformed request no longer answers the same way, which is the property that was missing rather than the code itself.IsGrantDenialdrops out ofunwiredCapabilities, leavingCommandFromPromptas the last row.docs/sirens-echo-grant-refusals.mdis new.docs/sirens-echo-grants.mdpoints at it and stays at its line cap.One thing I am flagging rather than deciding. The Discord wording is a member-facing string and Content Creator owns those. I matched the existing notice's plainness rather than choosing a voice, and a different phrasing is a one-line change.
just gatepasses.closes #825