Dashboard users (and other tenant-authenticated admin/operator/owner
actors) dispatching team tasks to agents in Telegram/Discord groups
previously hit the empty-sender DENY rule when the assigned agent
tried to write files — because dashboard users have tenant identities
(e.g. "viettx") that don't match the numeric file-writer grants stored
per channel.
Two-part fix:
1. CheckFileWriterPermission / CheckCronPermission add an RBAC bypass
at the store layer: if ctx carries RoleFromContext in {admin,
operator, owner}, skip the per-user grant check. Tenant RBAC already
pre-authenticated these callers at the gateway edge; requiring a
redundant per-channel grant blocks legitimate admin work.
2. Role propagation through the full dispatch chain:
ctx (RoleFromContext)
→ RunRequest.Role
→ SubagentTask.OriginRole / AnnounceMetadata.OriginRole
/ DelegateRequest.Role
→ InboundMessage.Metadata[MetaOriginRole]
→ subagentAnnounceRouting.Role / teammateRole
→ RunRequest.Role (re-ingress)
→ store.WithRole(ctx, ...) in loop_context
Covers subagent announce, delegate announce, teammate dispatch
(WS dashboard, tool), session_send, and processNormalMessage's
synthetic-sender override path. team_tasks_create persists
origin_role for deferred dispatches.
Viewer and empty roles fall through to the existing sender-based path
(no bypass).
Regression tests:
- A.10 AdminRoleBypass: admin/operator/owner in ctx allowed even with
empty sender (normally DENY).
- A.11 ViewerRoleDoesNotBypass: viewer and empty roles still hit DENY.
Builds + integration + unit tests green.
Resolves issue #915 (Telegram group write_file permission denied after
/addwriter) by reconstructing the real sender on announce re-ingress
and migrating ownership/audit call sites off the scope-coupled UserID.
Propagation chain (new MetaOriginSenderID key):
SubagentTask.OriginSenderID
→ AnnounceMetadata.OriginSenderID
→ InboundMessage.Metadata[MetaOriginSenderID]
→ subagentAnnounceRouting.SenderID
→ RunRequest.SenderID
Same chain for delegate_tool.announceToParent, team_tool_dispatch,
sessions_send, team_tasks_blocker escalation.
ACTOR migration (owner/audit now uses ActorIDFromContext):
- publish_skill: skill owner = individual sender
- skill_manage create/patch/delete: owner = sender; ownership check
accepts actor or legacy UserID for pre-migration rows
- delegate_tool: DelegateRequest.UserID + DomainEvent.UserID = actor
- team_tasks_blocker: blocker attribution + escalation = actor
- team_tool_cache: team access-policy check = actor (fixes per-user
allow/deny lists in group chats)
Scope-intentional (unchanged, commented): cron.go, team_tasks_create.go.
- Token cost tracking: accumulate input/output tokens per subagent,
include in announce messages and persist to DB
- Per-edition rate limits: MaxSubagentConcurrent/Depth on Edition struct,
tenant-scoped concurrency enforcement in Spawn/RunSync
- WaitAll action: spawn(action=wait, timeout=N) blocks until all
children complete, returns merged summary
- Auto-retry: configurable MaxRetries (default 2) with linear backoff
for transient LLM failures
- Producer-consumer announce queue: merges staggered subagent results
into single LLM run (same pattern as team task announces)
- Raw metadata in bus messages to prevent double-formatting
- Fire-and-forget DB persistence with detached context + tenant scope
- Split oversized files for <200 line compliance