Carry the credential owner into scheduled spec_task runs - #2900
Merged
Conversation
A scheduled spec_task authenticated as the app owner, so a fleet of triggers written under one service key all ran on that account's Claude subscription. CronTrigger gains CredentialOwnerID, passed to CreateTaskRequest exactly as a hand-dispatched task does; the existing delegation check still gates whether the grant applies. Spec-Ref: helix-specs@5d2aa66:002391_fix-these-critical-bugs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2899, which routed Claude credentials per owner but only for tasks
created through an orchestrator's dispatch call. That PR named the hole it left:
This closes it.
The change
CronTriggergainsCredentialOwnerID, andexecuteSpecTaskActionpasses it toCreateTaskRequestexactly as a hand-dispatched run does. Everything downstreamalready works — the same delegation gate added in #2899 still decides whether the
grant is real, so a trigger naming someone who has not delegated their
subscription to the org falls back rather than borrowing their quota.
Credential resolution only. The task is still created by, owned by, and
attributed to the trigger's app owner; nothing runs as the credential owner.
Empty — which is every trigger that exists today — keeps current behaviour
exactly, so this is inert until something writes the field.
Who sets it
Whatever writes the trigger. For HelixOS that is its reconciler, which knows each
bot's owner and writes it at reconcile time (paired PR in
helixml/helixos).Deriving it at fire time is not an option even in principle: Helix has no concept
of a HelixOS bot, so there is nothing on this side to derive an owner from. The
trigger row is the only thing that crosses the boundary.
Nothing in non-test server code constructs a
types.CronTrigger{}, and thetrigger UI edits app-config triggers rather than the
TriggerConfigurationrowsan orchestrator writes, so there is no path that would silently drop the field
back out on a round trip.
Testing
go build ./api/...,go vet, andgo test ./pkg/trigger/cron/...all pass.TestExecuteCronTask_SpecTaskActionCarriesCredentialOwner: the owner namedon the trigger reaches
CreateTaskRequest, whileUserIDstays the app owner —pinning that this is credential routing and not an ownership change
not invent an owner
Not tested end to end. No Helix stack was available, so no scheduled trigger
was fired against a live desktop to watch it pick up the named owner's
subscription. The link this PR adds is unit-tested; the resolution it hands off to
was covered by #2899.