fix(teams): grant a delegated lead read access to its own team - #1537
fix(teams): grant a delegated lead read access to its own team#1537yatul wants to merge 3 commits into
Conversation
|
Converting to draft — this approach is wrong and I found out by deploying it. Flagging before anyone spends review time on it. Setting if isArtifactDelegation {
if req.TeamWorkspace != "" ||
!validateDelegationArtifactWorkspace(req.DelegationID, req.DelegateInputsPath, req.DelegateOutputsPath) {
return contextSetupResult{}, fmt.Errorf("invalid delegation artifact workspace")
}and the team-workspace application below it is gated on The invariant makes sense: The idea still seems right; the mechanism has to change. What the lead needs is the read allowance ( Two shapes, and since the guard is deliberate I would rather you pick:
I lean towards (1). Happy to push it once you say which. Sorry for the noise — should have caught this before opening. |
clark-cant
left a comment
There was a problem hiding this comment.
Review: fix(teams): give a delegated lead its own team workspace
Summary: Resolves #1535 by populating the delegated lead's TeamWorkspace from its own team context, restoring read access to team files that was always available to team members but missing from the agent-link delegation path. Stacked on #1530 (already merged).
Risk level: Low — focused Go changes (+450/-16 across 14 source files) with comprehensive regression tests. The 400+ webui dist files are an unrelated UI rebuild bundled in.
Mandatory gates:
- Duplicate / prior implementation: clear — no overlapping PR found; #1530 (prerequisite) already merged
- Project standards: aligned — uses existing patterns (ListTeams walk, allowedWithTeamWorkspace validation, OriginChannelFromCtx helpers)
- Strategic necessity: clear value — completes the delegated-lead capability chain (dispatch → routing → workspace access)
Findings:
- Suggestion: The PR bundles ~400 webui dist asset files unrelated to the Go fix. Consider separating UI rebuilds into dedicated commits/PRs for cleaner history and easier bisect.
- Suggestion: The multi-team rule (agent leading >1 team gets no workspace) is correct defensive behavior. Worth documenting this constraint in team architecture docs.
Verdict: Approve — well-designed narrow fix with proper test coverage (8 test cases covering resolution, isolation, hermeticity). No Critical or Important findings.
Posted by /ck:review-pr at 2026-08-31T20:44:00Z
019cea1 to
1c76c4f
Compare
…ry channel
A delegated run is delivered on the internal "delegate" channel while its real
origin is preserved separately — buildAgentLinkRunRequest is explicit about it:
// preserves the origin's authorization-bearing identity while keeping
// delegation on its internal delivery channel.
Channel: "delegate",
WorkspaceChannel: req.Channel,
WorkspaceChatID: req.ChatID,
The team tools did not consult that origin, so a task created by a lead reached
through delegate was stamped with the delivery channel. Nothing is registered
for it, so every notification about that task — completion, failure, blocker
escalation, ask_user — was dropped:
unknown channel for outbound message channel=delegate
The delegatee's first answer still arrived, because it travels back as the
delegation result rather than through a channel; everything the lead said after
the delegation closed was lost. One session produced 16 such drops, including a
blocker escalation the user needed to see.
Add OriginChannelFromCtx / OriginChatIDFromCtx next to the existing workspace
scope propagation helpers, and use them where team scoping and notification
routing are decided: task records, list/search scoping, dispatch fallbacks,
event payloads, escalation tasks, ask_user and leader notifications. The
resolution is the identity when no delegation origin is present, so
non-delegated flows are unchanged.
Deliberately not touched: the channel used in authorization decisions
(checkTeamAccess, requireLead, approve/reject lead bypass). Those ask "how did
this call arrive", not "where should the answer go", and widening them is a
separate question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion Triage on nextlevelbuilder#1529 named two gates this PR had not met: regression coverage for delegated lead task completion, and a guard that unrelated origins cannot receive the notification. The existing tests only covered what create persists. TestDelegatedLeadCompletionNotifiesOrigin completes a task raised in a delegated context and asserts the completion event is addressed to the caller's origin. Reverting team_event_helpers.go to the delivery channel fails it with "addressed to delegate/system". TestCompletionNotificationStaysWithinItsOwnOrigin puts two tasks from different origins on one board, completes one, and asserts no completion notification carries the other origin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes nextlevelbuilder#1535. Second attempt: the first one set RunRequest.TeamWorkspace on the delegated run, which loop_context.go rejects outright and deliberately so — that field also becomes ToolWorkspace, which would displace the exchange outputs directory. Every delegation then died at setup with "invalid delegation artifact workspace". This does not go near that field. The separation the lead needs already exists in the code. A lead addressed directly resolves its team at loop_context.go:285-321 and gets ctx = tools.WithToolTeamWorkspace(ctx, wsDir) ctx = tools.WithToolTeamRoot(ctx, teamRoot) with no WithToolWorkspace call — a read allowance, not an override. Only the !isArtifactDelegation gate keeps a delegated lead out of it. So the grant is made inside the artifact branch instead, from the same inputs, and the guard on req.TeamWorkspace stays exactly as strict as it was. Both paths are needed. The workspace alone covers only the lead's own chat leaf; the deliverable in the report sits at teams/<id>/system/review-....md, written by a member under a different chat scope. buildAllowedPrefixes adds the team root for reads and not for writes (filesystem.go:366-370), which is precisely the asymmetry this case wants: the lead reads the team's output, per-chat write isolation is untouched. Team ID is deliberately not set. It switches on the workspace interceptor's write validation, file-change broadcast and task attachment (workspace_ interceptor.go:36,114,203) — none of which a delegated run should trigger, and none of which reading needs. Ambiguity is not resolved silently, as triage asked: an agent leading more than one active team gets nothing. store.GetTeamForAgent would answer in one call but it is ORDER BY (lead_agent_id = $1) DESC LIMIT 1 and would quietly pick a team. Hermeticity is unchanged: send_file and message still refuse inside an artifact run, publication still happens through delegation outputs on completion. Tests: TestInjectContext_DelegatedLeadReadsTeamWithoutDisplacingOutputs drives injectContext through the guard that broke the first attempt and pins both halves — the run is not refused, and ToolWorkspace stays the outputs directory. Resolution rules, the shared/isolated path shapes, read-not-write on the team root, and send_file staying blocked are covered alongside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1c76c4f to
9137664
Compare
|
Ready for review — reworked and out of draft. I put this in draft on 27 August because the first approach was wrong: it set Neither turned out to be necessary, so I did not wait for the answer. The read-allowance-without-override separation already exists in the code: a lead addressed directly gets Two things worth your attention: The team root is not optional. The workspace alone covers only the lead's own chat leaf, and the deliverable in the report lives under another member's chat scope.
I also verified it running, not only under test — deployed and drove a real delegation end to end, including the lead reading the exact path from the report and staging a file into its delegation outputs. My previous attempt passed eight unit tests and still broke everything, so I no longer treat green tests as evidence here. There is now a test that crosses the guard those eight missed. |
|
Flagging that the approval on this PR is stale, rather than quietly merging on it. The review was submitted on 31 August against
What is on the branch now takes a different route: the read-allowance-without-override separation already exists in Re-review should be cheap — the whole change is three source files plus tests, and the one that matters is I would rather this went in on a review of the code that is actually here. |
Fixes #1535. Implements the direction from your triage — give the delegated lead its own team context rather than loosening
send_file/message— but through a different mechanism than my first attempt.Stacked on #1530. The team workspace path is derived from the origin chat, which is exactly what #1530 corrects; based on
devalone the lead would resolve a different directory than its own tasks use. Review only the last commit, or merge #1530 first and this becomes a three-file diff.The mechanism
My first attempt set
RunRequest.TeamWorkspaceon the delegated run. That is rejected outright, and deliberately —internal/agent/loop_context.go:233:The invariant is right: that field does not only grant read access, it also becomes
ToolWorkspace, which would displace the exchange outputs directory. Deployed, every delegation died at setup. This version does not go near the field.The separation the lead needs already exists in the code. A lead addressed directly resolves its team at
loop_context.go:285-321and getswith no
WithToolWorkspacecall — a read allowance, not an override. Only the!isArtifactDelegationgate keeps a delegated lead out of it. So the grant is made inside the artifact branch instead, from the same inputs. The guard above stays exactly as strict as it was, and no new field is added toRunRequest.Both paths are needed
The workspace alone covers only the lead's own chat leaf. The deliverable in the report sits at
teams/<id>/system/review-....md— written by a member under a different chat scope.buildAllowedPrefixesadds the team root for reads and not for writes (internal/tools/filesystem.go:366-370), which is precisely the asymmetry this case wants: the lead reads the team's output, per-chat write isolation is untouched.What is deliberately not granted
ToolTeamIDis not set. It switches on the workspace interceptor's write validation, file-change broadcast and task attachment (internal/tools/workspace_interceptor.go:36,114,203) — none of which a delegated run should trigger, and none of which reading needs.Hermeticity is unchanged:
send_fileandmessagestill refuse inside an artifact run, and publication still happens through delegation outputs on completion. There is a test pinning that.The multi-team rule
As requested, ambiguity is not resolved silently: an agent leading more than one active team gets nothing.
store.GetTeamForAgentwould answer in one call, but it isORDER BY (lead_agent_id = $1) DESC LIMIT 1— it would quietly pick a team, which is the choice this must not make on its own.Tests
TestInjectContext_DelegatedLeadReadsTeamWithoutDisplacingOutputsTestInjectContext_DelegatedNonLeadGainsNothingSingleLedTeamResolves,MultipleLedTeamsResolveNothing,NonLeadResolvesNothing,InactiveTeamIgnoredSharedTeamCollapsesToRootSeparateLeadsGetSeparateWorkspacesStoreFailureResolvesNothing,NoTeamStoreResolvesNothingTestTeamRootDoesNotWidenWritesTestSendFileStaysBlockedInDelegationArtifactRunThe first one is the one that matters: my previous attempt had eight green unit tests and still broke every delegation, because not one of them crossed the guard in
injectContext. This one drivesinjectContextitself and asserts both halves — the run is not refused, andToolWorkspaceis still the exchange outputs directory. Reverting the change fails it.go test ./cmd/... ./internal/tools/ ./internal/agent/... ./internal/gateway/... ./internal/http/...is green.Verified running, not just under test
Given how the first attempt failed, unit tests alone are not evidence. Built and deployed to a live cluster:
invalid delegation artifact workspaceis gone;teams/<id>/system/review-....md, the path from the report, in a chat scope that is not its own — reachable only through the team root;outputs/— the sanctioned publication route the report said was unreachable — byte count matching the source.Note on the branch
Force-pushed twice: once to replace the first approach, and once to strip 411 built
internal/webui/distassets that rode in from #1530's commit. See the note on #1530. Nothing else moved; the diff is now the three files this change actually touches, plus #1530's.