fix(tui): let a resolved fleet member slot win over the legacy role label - #5945
fix(tui): let a resolved fleet member slot win over the legacy role label#5945gaord wants to merge 5 commits into
Conversation
Hmbown
left a comment
There was a problem hiding this comment.
Reviewed at e6868d1df. The bug you describe is real and I reproduced it. I am requesting changes only because the fix flips the precedence unconditionally, and the mirror case silently widens authority in the other direction — which is the same harm your own test comment names.
What I verified
Detached worktree, RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --lib -- fleet::worker_runtime:
- On the PR head: 70 passed; 0 failed.
- Your test against the pre-fix
effective_fleet_role_with_source(I splicedagent_profile_member_slot_overrides_legacy_role_labelonto the505fc7a27^version of the file): FAILS,left: Some("manager"),right: Some("reviewer"). Somember:reviewerreally did fall through to the"manager"label. Confirmed.
I also traced why it is reachable: freeze_fleet_task_members (worker_runtime.rs:217-221) deliberately keeps a divergent worker.role when an explicit worker.agent_profile selector is present — it only canonicalizes the string:
worker.agent_profile = Some(format!("member:{}", profile.id));
if explicit_selector.is_none() {
worker.role = Some(snapshot.role.clone());
} else if let Some(role) = worker.role.as_mut() {
*role = canonical_public_role_name(role.trim()); // divergence preserved
}So a task can carry agent_profile: member:X and role: Y past freezing with X.role != Y, and nothing rejects it. Someone has to lose. Today the label wins; after this PR the member wins.
The concern: the mirror case widens write authority
I added a throwaway probe test (member alice whose slot is implement; task worker.role = "reviewer") and ran it against both trees. Same input, fleet_task_to_worker_spec_with_profiles:
spec.role |
spec.agent_type |
runtime_profile.permissions.write |
|
|---|---|---|---|
origin/main |
reviewer |
Reviewer |
false |
| this PR | implement |
Builder |
true |
That is a read-only task quietly becoming write-capable. It is the exact failure your test guards against —
"reviewer authority must not be silently widened to a write-capable worker"— just approached from the other side. Net, the change trades one silent widening for another rather than closing the class.
(For completeness: manager is not a canonical FleetRole, so it falls to FleetRole::Worker via fleet_role_to_agent_type — write-capable. Your case is genuinely a widening and genuinely worth fixing. I am not disputing the direction, only the unconditional rule.)
What I would like instead
Any one of these closes the class rather than rotating it:
- Fail closed at freeze time (my preference). In
freeze_fleet_task_members, whenexplicit_selector.is_some()andworker.rolenames a different posture thanprofile.role.name,bail!the way the surrounding code already bails for unknown selectors and ambiguous members (worker_runtime.rs:168-199). A conflicting spec is an authoring error; resolving it silently either way is how you get a worker running with authority nobody wrote down. This also keepseffective_fleet_role_with_sourcehonest — by the time it runs, there is no conflict left to arbitrate. - Narrower wins. If you want conflicts to stay legal, resolve to the less authoritative posture rather than to a fixed side. That satisfies your case (reviewer < manager) and mine (reviewer < implement) with one rule.
- If the team's settled answer really is "the member slot is always authoritative", then say so where it is enforced and make it visible: the freeze path should overwrite
worker.rolewithsnapshot.rolein the explicit-selector branch too, instead of preserving a label that is now dead. Right now the code carefully preserves a field that the new precedence guarantees will never be read — that is a trap for the next reader.
Whichever you pick, please add the inverse-direction regression test alongside agent_profile_member_slot_overrides_legacy_role_label. The current test only pins the widening you fixed, so nothing stops the symmetric one from being reintroduced.
Smaller notes
- The
role_sourcevalue stays"agent_profile.role"in the new branch, which is right, but with the fallthrough gone"task.role"is now only reachable when no member resolved at all. Worth saying that in the comment — it is a meaningful narrowing of what that receipt string means, and receipts are read by people debugging exactly this. - DCO:
505fc7a27has noSigned-off-by:trailer (the merge commite6868d1dfdoes not either).Check Signed-off-byis advisory in.github/workflows/dco.yml, so CI is green, but CONTRIBUTING asks for it —git commit --amend -s. - No locale concern here; nothing user-visible was added.
Thanks for chasing this one down to a concrete fleet-e12f3160 repro — the diagnosis is right and the comment you wrote explaining the old condition is genuinely better than what it replaced.
…abel `effective_fleet_role_with_source` only consulted the resolved agent_profile when `worker.agent_profile` was empty. A task whose role label is "manager" but whose agent_profile selects `member:reviewer` therefore fell through to the legacy `worker.role` label and was treated as a write-capable manager — which never leased (fleet-e12f3160). Prefer the resolved member's canonical slot (reviewer/builder/...) over the legacy role label, keeping the label only as a fallback when no member was resolved.
…ected member Follow-up to Hmbown#5945 (review: implement option 1, fail closed at freeze). The precedence flip in the parent commit is right for the reported case (`member:reviewer` + label `manager` must run as a reviewer), but taken alone it widens authority in the mirror case: member `alice` in the `implement` slot with a task label of `reviewer` went from reviewer/write=false on main to implement/write=true. A read-only task quietly becoming write-capable is not a fix. Root cause is that `freeze_fleet_task_members` kept a divergent `worker.role` next to an explicit `worker.agent_profile` selector and only canonicalized the string, so a spec could carry two postures past freezing and nothing rejected it. - `freeze_fleet_task_members` now bails when an explicit selector is present and `worker.role` names a different posture than the selected member's role, the same way it already bails for unknown or ambiguous selectors. The message names both postures. Comparison is on the canonical public role name, so casing and legacy aliases of the member's own role are not conflicts. - `effective_fleet_role_with_source` keeps the member-first precedence; its comment now says the conflict is settled at freeze and that a `role_source` of "task.role" means no member resolved at all. - Tests: mirror-direction regression (implement member + reviewer label must not become write-capable; errors at freeze), a conflicting spec is rejected naming both postures, and an alias/casing label of the member's own role freezes cleanly. - CHANGELOG entry under Unreleased / Fixed, crediting @gaord. Gate: cargo fmt -p codewhale-tui -- --check: clean cargo clippy -p codewhale-tui --lib --all-targets -- -D warnings: clean cargo test -p codewhale-tui --lib -- fleet::worker_runtime fleet:: 366 passed, 0 failed cargo test -p codewhale-tui --lib 11854 passed, 0 failed, 13 ignored Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJrzNAmppg4vt3LNJbaeri Signed-off-by: CodeWhale Bot <bot@codewhale.net>
e6868d1 to
e81b683
Compare
| let label = canonical_public_role_name(label); | ||
| if label != snapshot.role { |
There was a problem hiding this comment.
🟡 Equivalent role aliases block runs
When worker.role uses an equivalent alias, canonical_public_role_name can differ from the selected member’s label. fleet_role_to_agent_type maps pairs like coordinator and manager to one posture. The string comparison rejects these valid runs and case-only custom-role variants.
| let label = canonical_public_role_name(label); | |
| if label != snapshot.role { | |
| let label = canonical_public_role_name(label); | |
| if fleet_role_to_agent_type(Some(&label)) != roster_member_agent_type(profile) { |
Was this helpful? React with 👍 or 👎 to provide feedback.
| ### Fixed | ||
|
|
||
| - A Fleet task that selects a roster member with `worker.agent_profile` now | ||
| runs with that member's posture. The launch-time resolver only consulted the | ||
| resolved member when the legacy `worker.role` label was absent, so a task | ||
| labelled `manager` that selected `member:reviewer` ran as a write-capable | ||
| manager instead of a reviewer and was never leased. The member's canonical | ||
| slot now wins whenever one resolved; the label remains the posture only when | ||
| no member resolved at all. To keep the fix from widening authority in the | ||
| mirror case (a read-only label on a write-capable member), a spec whose | ||
| `worker.role` names a different posture than the selected member's role is | ||
| rejected at run creation with a message naming both postures; casing and | ||
| legacy aliases of the member's own role are still accepted (#5945, thanks | ||
| @gaord). |
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
Replying to @Hmbown's review (requested at the original head What changed since the reviewed head ( Two deliberate strictness notes:
The original author's scenario ( CI on the current head ( |
Summary
Fix
effective_fleet_role_with_source: it only consulted the resolved agent_profile whenworker.agent_profilewas empty, so a task whose role label is "manager" but whose agent_profile selectsmember:reviewerfell through to the legacyworker.rolelabel and ran with the wrong (write-capable) authority. Prefer the resolved member slot; keep the legacy label only as a fallback.Testing
Note: main currently has 5 pre-existing nonminimal_bool clippy errors, unrelated to this change.
No-Issue: fleet role precedence fix from a community report; no tracking issue
Maintainer follow-up
Recovered onto current
mainand finished per the review (option 1, fail closed at freeze). Commite81b6837on this branch, on top of @gaord's original commit (kept verbatim, author preserved).freeze_fleet_task_membersnowbail!s when an explicitworker.agent_profileselector is present andworker.rolenames a different posture than the selected member's role, alongside the existing unknown/ambiguous-selector bails. The message names both postures. Comparison usescanonical_public_role_name, so casing and legacy aliases of the member's own role are not conflicts.effective_fleet_role_with_sourceis kept; its comment now states that the conflict is settled at freeze and that arole_sourceof"task.role"means no roster member resolved at all.legacy_role_label_never_widens_into_a_member_write_slot(the mirror case:implementmember +reviewerlabel errors at freeze and persists nothing),conflicting_member_and_role_label_is_rejected_at_freeze_naming_both_postures, androle_label_alias_of_the_selected_member_role_is_not_a_conflict. The author'sagent_profile_member_slot_overrides_legacy_role_labelis unchanged and still passes.crates/tui/CHANGELOG.mdsynced).Gate, as run:
cargo fmt -p codewhale-tui -- --check: cleancargo clippy -p codewhale-tui --lib --all-targets -- -D warnings: cleancargo test -p codewhale-tui --lib -- fleet::worker_runtime fleet::: 366 passed, 0 failedcargo test -p codewhale-tui --lib: 11854 passed, 0 failed, 13 ignored