FleetApp.sendMessage parses the request body before the authorization gate #689
Closed
opened 2026-10-03 22:27:09 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/702-4f5c7f-2
worker/715-5c43fc-1
worker/721-70f9ea-5
worker/718-99362b-2
worker/task-15-af0d10-12
worker/task-16-50a702-13
worker/task-12-4d0479-9
worker/task-13-823ce2-10
worker/705-ticket-owner-af9928-8
worker/703-list-collaborators-9c06c2-7
worker/669-example-truth-0b303d-6
worker/669-collab-deliverability-9ba859-3
worker/669-collab-reload-report-2a21bd-4
worker/669-7e80a6-1
worker/669-unit-d-efbbd7-1
worker/669-1b786a-1
worker/669-1d1d9f-1
worker/692-4afb9d-2
worker/689-02fced-13
worker/693-cf23fa-14
worker/677-fix-lead-collision-f69073-12
worker/638-fix-overmask-dbb1bf-11
worker/675-5b7478-4
worker/669-unit-a-70cc8f-3
worker/677-8cdaaf-5
worker/638-a7b391-1
worker/683-4536d6-2
worker/651-a75bbe-8
worker/680-20607d-7
worker/664-c12e95-3
worker/668-08534d-4
worker/672-0f2469-2
worker/670-7d1022-1
worker/661-ac7c28-2
worker/664-37fb9b-3
worker/663-remove-3arg-read-3f6783-1
worker/659-remove-dead-backcompat-ba5e6f-1
worker/637-revision-60a488-23
worker/656-redact-regression-tests-892903-19
worker/637-context-gauge-threshold-466eb5-16
worker/639-redact-line-numbers-de4ac4-17
worker/641-set-reformat-guard-6f96a4-18
worker/642-herdr-guard-scope-5de0e4-15
worker/650-javadoc-scope-95f3b3-14
worker/612-01e9f7-13
worker/612-a-r4-quarantine-outage-7ab0e8-5
worker/612-a-r9-r11-capacity-coverage-peers-cfcc79-7
worker/612-a-r10-loophealth-ccc872-8
worker/612-a-r12-turnregistrar-9e3bb7-9
worker/612-a-r5-leadconfigdir-9e70cf-6
lead/config-edit-redact-anchor-wording
worker/config-edit-seam-ca8dc1-1
worker/612-r67-630-lifecycle-290b8d-3
worker/629-625-ports-seams-da7d5d-4
worker/612-r12-exhaustion-f37cd7-1
worker/612-r38-amqp-24b083-2
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#689
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
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?
Found while reviewing PR #687 (#669 Unit A). Filed rather than held, because the authorization
split in #687 is correct on its own and I merged it. This is the one thing in that PR I did not
want to lose.
What changed
PR #687 moved the JSON body parse in
FleetApp.sendMessageto before theallow(...)call.On
origin/mainthe order was the other way round.origin/main:After #687:
The reason was sound:
turnIddecides whether the call is aSENDor anANSWER, and theauthorization check now takes that action. But the effect is that an unauthorized caller's request
body is read and parsed into a Jackson tree before the gate refuses it.
Why it does not need to be this way
I checked
Authzon the PR branch. All three send actions carry the same grant:A reviewer confirmed this across the whole table: 156 (role, action, target) pairs compared against
origin/main, 0 mismatches. So todayturnIdchanges nothing about the decision, and thereordering buys nothing. It is only needed if these grants ever diverge.
What I did not measure
I did not measure how large a body Jetty or Javalin accepts here.
maxRequestSizeis notconfigured anywhere in this repo, so Javalin 6.7.0's default applies, and I did not look up what
that default is. A reviewer reported it as 1,000,000 bytes; I am repeating that as its claim, not
as a number I checked.
I also did not drive this with a real HTTP request. The reviewer I assigned read the code but did
not run anything, because it understood the reviewer contract to forbid running tests. That is a
defect in my brief, not in its work. So the DoS reading below is not confirmed by measurement —
what is confirmed is the ordering change, which I read in both versions of the file.
Practical risk
Low, in my judgement.
bind.hostis127.0.0.1, so the caller must already be a local process,and a local process can do worse things than make the daemon parse some JSON. I merged #687 on that
basis. The reason to fix it anyway is the principle: do not do attacker-controlled work before the
authorization gate, because the grants are meant to diverge later and the ordering will then matter.
Suggested fix
Authorize twice, cheaply. Check the coarse
SENDgrant first, with no body read. Then parse. Then,if
turnIdis present, checkANSWERas well. That restoresorigin/main's ordering and stillenforces the finer action, and it keeps working if the two grants diverge.
Setting a route-appropriate
maxRequestSizewould be worth doing regardless, but it is a separatechange.
Also worth checking in the same pass
Two pre-existing things, neither introduced by #687, both reported by workers and both left alone
on purpose:
FleetApp.allow()leaves outREADandMETRICSfrom the audit-log "allowed" trail, whileFleetMcp.denyFor()leaves out onlyREAD. #687 preserved that asymmetry and addedTASK_READto each list in its existing style. If one of the two is wrong, it was wrong before #687.
SENDfor the authorization check and then returns 400. Itraced it and it reaches neither
messages.answernormessages.send, so nothing acts on theunparsed body. That is fine today, but it is a second place that stops being obviously fine if
the grants diverge.
Delegated, with the fix approach fixed by me
Worker
term_65cf596dc279d50on branchworker/689-02fced-13, tickettask-13.I re-read the code myself before briefing, rather than trusting this ticket's own quote. On
mainat2eb2d61the defect is live exactly as filed, atFleetApp.java:633-642:mapper.readTree(ctx.body())runs, thenturnIdis lifted, thenallow(...). The route's action split is atFleetApp.java:69, where a blank or nullturnIdmaps toSENDand a present one toANSWER.Note the path in the original report is wrong in one detail: the file is
fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java, not.../api/FleetApp.java. Nothing else in the report changed on re-reading.The approach is this ticket's own suggested fix, and it is my decision, not the worker's: check the coarse
SENDgrant first with no body read, then parse, then checkANSWERas well whenturnIdis present. No grant inAuthzchanges — all three send actions carry the same grant today and this must not alter who can call what.Acceptance, as briefed. Three properties, each with a required control:
SENDgrant gets no body parse. Control: the same caller, granted, reaches the body.SENDallowed andANSWERdenied, aturnIdrequest is refused while a plain one succeeds. Flipping only theANSWERarm must change only theturnIdshape. That control is what proves two separate checks rather than one renamed check.messages.answernormessages.send.The worker was told to say plainly if it could not drive one of these as a unit test, instead of reporting an unexercised property as held.
Scope fence.
FleetApp.javaand its own tests only.FleetConfig.javais held by another worker right now (#677), so I excluded it explicitly to avoid a collision in shared test files.Still not measured, and not in this unit
The DoS reading remains unmeasured. I did not drive this with an HTTP request either, and I did not look up Javalin 6.7.0's default
maxRequestSize. The reviewer's 1,000,000-byte figure is still that reviewer's claim, repeated, not a number anyone here has checked. Setting a route-appropriatemaxRequestSizestays a separate change and is not in this unit.The two pre-existing items in the "also worth checking" section above are not in this unit either: the
allow()/denyFor()audit-log asymmetry, and the malformed-body fallback pickingSEND. Both predate #687 and neither blocks the ordering fix.Why this is being done now
#669 Unit B gives
COLLABORATORa different send grant, and at that moment this stops being cosmetic. Unit B also touchesFleetConfig.java's validators, which the #677 worker holds, so Unit B is not delegated yet. This fix lands first, on its own, which is the order #669 asked for.Correction for the worker on
task-13. Read this before you continue. It overrides the brief.PR #694's fix is correct and I am keeping it. Build verified by me, trial-merged onto
origin/mainatbfee23ain a throwaway worktree,rm -rf target/surefire-reports,mvn -o clean install: 1948 tests, 0 failures, BUILD SUCCESS,exit=0, 173 report files. (1945 is the current baseline, not 1942 —mainmoved when #691 landed. 1945 + 3 = 1948.)One thing needs adding before I merge.
The
answerGatePassescall site is not pinned, only the helper isI deleted the call site from
sendMessage:and ran
mvn -o -Dtest='FleetAppAuthTest,FleetAppTest' test:All 56 passed with the ANSWER gate removed entirely. So the suite cannot tell whether
sendMessagestill calls it. A unit test on the extracted helper proves the helper; it says nothing about the caller. That matters here more than usual, because the gate is behaviourally invisible today — the one thing that could notice it disappearing is a test, and no test does.That is the exact risk this ticket was filed to close. Between now and #669 Unit B, a refactor could drop that call and every build stays green; at the moment Unit B gives
COLLABORATORa different grant, the ANSWER gate is silently gone.Your "impossible today" was right about grants and too strong overall
You were correct that no real caller can produce "SEND allowed, ANSWER denied": I read it too,
Authz.permitsis astaticcall fromFleetApp.allow(), so there is no seam to substitute and the scenario is unreachable without editingAuthz. Extracting the helper to unit-test that case was a reasonable call and I am not asking you to undo it.But the call site is observable by another route you did not consider.
allow()writes to the audit trail on the allowed path:ANSWERis none of those three, so a granted primary posting a body with aturnIdmust produce two allowed entries — oneSEND, oneANSWER. With the call site deleted, onlySENDappears.The project already has the instrument:
fleetd/src/test/java/dev/ltms/fleet/testing/CapturedLog.java, andAuditLogTestalready observes theauditlogger.What to add
One test: a granted primary posts a body carrying a
turnId, and the captured audit trail contains an allowed entry for bothSENDandANSWER.Two controls, and report both:
git diffonFleetApp.javais empty. This is the control that matters — it is the one my check above failed.turnIdmust produce aSENDentry and noANSWERentry. That stops the new test passing just because something somewhere logsANSWER.Keep every test you already wrote. Properties 1 and 3 are genuinely pinned end-to-end and I verified the reasoning behind property 1 — a denied caller with a
turnIdbody refused onSENDrather thanANSWERreally does prove the ordering, because the 403 detail string interpolates the action name.Unchanged
Still
FleetApp.javaand its own tests only. Still no change toAuthzor any grant. Still nogit add -A. Same build procedure, and the baseline is now 1945.Push to the same branch and the same PR; do not open a second one. Reply with the new test's name, both controls as raw output, and the fresh build tail.
Fixed and merged. Closing.
PR #694 merged to
mainasedbd8d8, pushed (d0688c8..edbd8d8).The shipped order in
FleetApp.sendMessageis now:Coarse
SENDfirst with no body read, then parse, then 400 on a malformed body, then the finerANSWERcheck whenturnIdis present. That restores the pre-#687 ordering and keeps the finer action. No grant inAuthzchanged.The call site is now pinned, and I verified the control myself
This is the part the first round missed.
answerGatePasseswas extracted so property 2 could be unit-tested, but a test on the helper says nothing about its caller — and the gate is behaviourally invisible today, because all three send grants are identical. So nothing could notice it disappearing.Before the second commit, I deleted the call site and ran
mvn -o -Dtest='FleetAppAuthTest,FleetAppTest' test:After it, the same deletion:
Exactly one failure, and it is the new test. Restored afterwards;
git diffonFleetApp.javaempty.The route it uses is the audit trail:
allow()logs an allowed entry for every granted action exceptREAD,METRICSandTASK_READ, andANSWERis none of those, so a grantedturnIdrequest must log bothSENDandANSWER. The second control is in the same test — a plain request must log exactlyList.of("SEND"), so the test cannot pass because something else happens to logANSWER.One number worth correcting
The worker reported 1946 tests and explained it as "1945 baseline + 1". The total was right for its own branch and the arithmetic was wrong: its branch is based on
2eb2d61(1942) and adds 4 tests across its two commits, so 1942 + 4 = 1946. That it equals currentmain's own total is a coincidence, and the coincidence is what made the wrong arithmetic look right.My build, trial-merging the branch onto current
main(d0688c8, itself at 1946): 1950 tests, 0 failures, BUILD SUCCESS,exit=0, 173*.xmlreport files. 1946 + 4 = 1950. Merged tree hasha5acb30112cd0b4a45c89dcb92446cfe54703549equals the tree I built.What remains unmeasured, unchanged from this ticket's filing
maxRequestSize. The 1,000,000-byte figure is still one reviewer's claim, repeated. What is established is the ordering, read in the code and now pinned by a test.maxRequestSizeis still unset and still worth doing as its own change.allow()/denyFor()audit-log asymmetry, and the malformed-body fallback. Both predate #687.Why this mattered
#669 Unit B gives
COLLABORATORa different send grant. At that moment this ordering stops being cosmetic, and so does the ANSWER gate existing at all. Both are now in place and both are defended by a test that fails if they are removed.