feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)

Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)

- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
  FK/cascade). Capture the creating member at both trigger-create sites (schedule +
  webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
  carries no human authorization — same authz-safe divergence as rule_owner),
  accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
  dispatch and the create_issue path (bridging issue → active run → trigger_id).
  Legacy triggers with no recorded creator, and agent-created triggers, degrade to
  rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
  tests (member creator → trigger_owner; creatorless → rule_owner fallback).

Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
J
2026-07-10 14:12:55 +08:00
co-authored by multica-agent
parent b8096107da
commit 249090260f
16 changed files with 278 additions and 28 deletions
@@ -46,6 +46,9 @@ export function AttributionBadge({
case "comment_source":
sourceLabel = t(($) => $.execution_log.attribution.source_comment_source);
break;
case "trigger_owner":
sourceLabel = t(($) => $.execution_log.attribution.source_trigger_owner);
break;
case "rule_owner":
sourceLabel = t(($) => $.execution_log.attribution.source_rule_owner);
break;
+1
View File
@@ -395,6 +395,7 @@
"source_direct_human": "Direct member action",
"source_delegation": "Delegated from another run",
"source_comment_source": "From a comment thread",
"source_trigger_owner": "Autopilot trigger creator",
"source_rule_owner": "Autopilot rule publisher",
"source_owner_fallback": "No precise owner — attributed to the agent owner",
"source_backfill": "Backfilled attribution",
+1
View File
@@ -379,6 +379,7 @@
"source_direct_human": "メンバーの直接操作",
"source_delegation": "別の実行から委任",
"source_comment_source": "コメントスレッドから",
"source_trigger_owner": "オートパイロットのトリガー作成者",
"source_rule_owner": "オートパイロットのルール公開者",
"source_owner_fallback": "正確な責任者なし — エージェント所有者に帰属",
"source_backfill": "バックフィルされた帰属",
+1
View File
@@ -379,6 +379,7 @@
"source_direct_human": "멤버 직접 작업",
"source_delegation": "다른 실행에서 위임됨",
"source_comment_source": "댓글 스레드에서",
"source_trigger_owner": "오토파일럿 트리거 작성자",
"source_rule_owner": "오토파일럿 규칙 게시자",
"source_owner_fallback": "정확한 담당자 없음 — 에이전트 소유자에게 귀속",
"source_backfill": "백필된 귀속",
@@ -379,6 +379,7 @@
"source_direct_human": "成员直接操作",
"source_delegation": "由其他运行委托",
"source_comment_source": "来自评论线程",
"source_trigger_owner": "Autopilot 触发器创建者",
"source_rule_owner": "Autopilot 规则发布者",
"source_owner_fallback": "无精确责任人 —— 归到 agent 所有者",
"source_backfill": "回填的归因",
+34 -3
View File
@@ -42,8 +42,18 @@ const (
// agent/system-authored comment; the human is resolved through
// comment.source_task_id (a special case of delegation, MUL-4302 §3.3).
SourceCommentSource Source = "comment_source"
// SourceRuleOwner — an autopilot trigger enqueued the run; the accountable
// human is the publisher of the rule's active version (MUL-4302 §3.4).
// SourceTriggerOwner — an autopilot schedule/webhook trigger enqueued the run;
// the accountable human is the member who CREATED that specific trigger (set up
// the schedule / registered the webhook). Preferred over rule_owner: a run is
// accountable to whoever armed the trigger that fired it, not to whoever last
// published the rule (MUL-4302; Bohan's refinement). originator stays NULL — an
// autonomous fire carries no human authorization — so this is the same authz-safe
// audit-only divergence as rule_owner.
SourceTriggerOwner Source = "trigger_owner"
// SourceRuleOwner — an autopilot trigger enqueued the run but its creator is not
// recoverable (a trigger created before per-trigger creators were recorded); the
// accountable human degrades to the publisher of the rule's active version
// (MUL-4302 §3.4). Precise, but coarser than trigger_owner.
SourceRuleOwner Source = "rule_owner"
// SourceOwnerFallback — nothing above resolved a human, so attribution
// degrades to the agent owner. This is DEGRADED, not compliance-grade, and
@@ -63,7 +73,7 @@ const (
// attribution-coverage health metric (MUL-4302 §9).
func (src Source) Precise() bool {
switch src {
case SourceDirectHuman, SourceDelegation, SourceCommentSource, SourceRuleOwner:
case SourceDirectHuman, SourceDelegation, SourceCommentSource, SourceTriggerOwner, SourceRuleOwner:
return true
default:
return false
@@ -335,6 +345,27 @@ func RuleOwner(publisherUserID, ruleVersionID pgtype.UUID, evidenceKind Evidence
return finalizeAttribution(r)
}
// TriggerOwner builds attribution for an autopilot schedule/webhook run keyed to
// the human who created the firing trigger (MUL-4302; Bohan's refinement). Like
// RuleOwner, no human authorized the run, so UserID (originator) stays NULL and
// only the audit-accountable side is set — to creatorUserID, the trigger's member
// creator. Evidence is caller-supplied (autopilot_run for run_only, the issue for
// create_issue). An invalid creator degrades to unattributed so callers that lost
// the creator fall back to rule_owner rather than fabricating a human.
func TriggerOwner(creatorUserID pgtype.UUID, evidenceKind EvidenceKind, evidenceRefID pgtype.UUID) Result {
r := Result{
EvidenceKind: evidenceKind,
EvidenceRefID: evidenceRefID,
}
if creatorUserID.Valid {
r.Source = SourceTriggerOwner
r.AccountableUserID = creatorUserID
} else {
r.Source = SourceUnattributed
}
return finalizeAttribution(r)
}
// OwnerFallback degrades an UNATTRIBUTED result to owner_fallback (MUL-4302 §3.5):
// the agent owner becomes the accountable human so no run is left without one, but
// this is a DEGRADED label (Source.Precise() == false) and must be surfaced
@@ -287,8 +287,9 @@ func TestAccountableMirrorsOriginatorInvariant(t *testing.T) {
// Authorization must never be forged from the audit side: a non-NULL
// accountable with a NULL originator is allowed (divergence), but the
// reverse — originator set from accountable — must never happen implicitly.
if r.AccountableUserID.Valid && !r.UserID.Valid && r.Source != SourceRuleOwner && r.Source != SourceOwnerFallback {
t.Errorf("result[%d]: accountable set with NULL originator only allowed for rule_owner/owner_fallback, got source=%q", i, r.Source)
if r.AccountableUserID.Valid && !r.UserID.Valid &&
r.Source != SourceRuleOwner && r.Source != SourceTriggerOwner && r.Source != SourceOwnerFallback {
t.Errorf("result[%d]: accountable set with NULL originator only allowed for trigger_owner/rule_owner/owner_fallback, got source=%q", i, r.Source)
}
}
}
@@ -341,6 +342,38 @@ func TestRuleOwner(t *testing.T) {
}
}
func TestTriggerOwner(t *testing.T) {
// The divergence case (MUL-4302; Bohan): an autopilot schedule/webhook run has
// NO authorizing human (originator NULL) but IS accountable to the member who
// created the firing trigger.
got := TriggerOwner(human, EvidenceAutopilotRun, issue)
if got.Source != SourceTriggerOwner {
t.Fatalf("source = %q, want trigger_owner", got.Source)
}
if !got.Source.Precise() {
t.Errorf("trigger_owner must be a precise source")
}
if got.UserID.Valid {
t.Errorf("trigger_owner must NOT set originator (authorization stays NULL), got %v", got.UserID)
}
if got.AccountableUserID != human {
t.Errorf("accountable should be the trigger creator, got %v", got.AccountableUserID)
}
if got.EvidenceKind != EvidenceAutopilotRun || got.EvidenceRefID != issue {
t.Errorf("evidence should be carried through")
}
// No creator (unrecoverable) must not fabricate a human — degrades to
// unattributed so the caller can fall back to rule_owner.
none := TriggerOwner(pgtype.UUID{}, EvidenceAutopilotRun, issue)
if none.Source != SourceUnattributed {
t.Errorf("missing creator must degrade to unattributed, got %q", none.Source)
}
if none.UserID.Valid || none.AccountableUserID.Valid {
t.Errorf("missing creator must carry no human on either side")
}
}
func TestOwnerFallback(t *testing.T) {
// Unattributed → owner_fallback: accountable = owner, originator stays NULL,
// source is degraded (not precise).
@@ -386,7 +419,7 @@ func TestUnattributed(t *testing.T) {
}
func TestSourcePrecise(t *testing.T) {
precise := []Source{SourceDirectHuman, SourceDelegation, SourceCommentSource, SourceRuleOwner}
precise := []Source{SourceDirectHuman, SourceDelegation, SourceCommentSource, SourceTriggerOwner, SourceRuleOwner}
degraded := []Source{SourceOwnerFallback, SourceBackfill, SourceUnattributed, Source("")}
for _, s := range precise {
if !s.Precise() {
+8
View File
@@ -1314,6 +1314,10 @@ func (h *Handler) CreateAutopilotTrigger(w http.ResponseWriter, r *http.Request)
NextRunAt: nextRunAt,
Label: ptrToText(req.Label),
WebhookToken: webhookToken,
// Record the creating member so runs this trigger fires attribute to them
// (source=trigger_owner, MUL-4302).
CreatedByType: pgtype.Text{String: "member", Valid: publisherID.Valid},
CreatedByID: publisherID,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create trigger")
@@ -1376,6 +1380,10 @@ func (h *Handler) createWebhookTriggerWithMintedToken(
WebhookToken: pgtype.Text{String: token, Valid: true},
Provider: pgtype.Text{String: provider, Valid: provider != ""},
EventFilters: eventFilters,
// Record the creating member so runs this webhook fires attribute to
// them (source=trigger_owner, MUL-4302).
CreatedByType: pgtype.Text{String: "member", Valid: publisherID.Valid},
CreatedByID: publisherID,
})
if err != nil {
tx.Rollback(ctx)
@@ -176,6 +176,93 @@ func TestEnqueueTaskForIssueWithHandoffAttributesToActor(t *testing.T) {
}
}
// TestTriggerOwnerAttribution_ScheduleTriggerCreator is the acceptance test for
// trigger_owner (MUL-4302; Bohan): an autopilot schedule/webhook run is accountable
// to the member who CREATED the firing trigger, with originator NULL (no human
// authorized the autonomous fire).
func TestTriggerOwnerAttribution_ScheduleTriggerCreator(t *testing.T) {
pool := newResolveOriginatorPool(t)
ctx := context.Background()
q := db.New(pool)
workspaceID, creatorID, agentID, _ := seedAttributionFixture(t, pool)
var autopilotID string
if err := pool.QueryRow(ctx, `
INSERT INTO autopilot (workspace_id, title, assignee_id, execution_mode, created_by_type, created_by_id)
VALUES ($1, 'trigger-owner-ap', $2, 'run_only', 'member', $3) RETURNING id`,
workspaceID, agentID, creatorID).Scan(&autopilotID); err != nil {
t.Fatalf("seed autopilot: %v", err)
}
t.Cleanup(func() { pool.Exec(context.Background(), `DELETE FROM autopilot WHERE id = $1`, autopilotID) })
// Schedule trigger created by the member.
var triggerID string
if err := pool.QueryRow(ctx, `
INSERT INTO autopilot_trigger (autopilot_id, kind, enabled, cron_expression, created_by_type, created_by_id)
VALUES ($1, 'schedule', true, '0 * * * *', 'member', $2) RETURNING id`,
autopilotID, creatorID).Scan(&triggerID); err != nil {
t.Fatalf("seed trigger: %v", err)
}
got := triggerOwnerAttribution(ctx, q,
util.MustParseUUID(triggerID), util.MustParseUUID(workspaceID), util.MustParseUUID(autopilotID),
attribution.EvidenceAutopilotRun, util.MustParseUUID(autopilotID))
if got.Source != attribution.SourceTriggerOwner {
t.Fatalf("source = %q, want trigger_owner", got.Source)
}
if got.UserID.Valid {
t.Errorf("trigger_owner is audit-only; originator must stay NULL, got %s", util.UUIDToString(got.UserID))
}
if !got.AccountableUserID.Valid || got.AccountableUserID.Bytes != util.MustParseUUID(creatorID).Bytes {
t.Errorf("accountable = %s, want trigger creator %s", util.UUIDToString(got.AccountableUserID), creatorID)
}
}
// TestTriggerOwnerAttribution_LegacyTriggerFallsBackToRuleOwner verifies the
// backward-compat path Bohan signed off on: a trigger with no recorded creator
// (created before this migration) degrades to the rule publisher (rule_owner),
// never fabricating a human.
func TestTriggerOwnerAttribution_LegacyTriggerFallsBackToRuleOwner(t *testing.T) {
pool := newResolveOriginatorPool(t)
ctx := context.Background()
q := db.New(pool)
workspaceID, publisherID, agentID, _ := seedAttributionFixture(t, pool)
var autopilotID string
if err := pool.QueryRow(ctx, `
INSERT INTO autopilot (workspace_id, title, assignee_id, execution_mode, created_by_type, created_by_id)
VALUES ($1, 'legacy-trigger-ap', $2, 'run_only', 'member', $3) RETURNING id`,
workspaceID, agentID, publisherID).Scan(&autopilotID); err != nil {
t.Fatalf("seed autopilot: %v", err)
}
t.Cleanup(func() { pool.Exec(context.Background(), `DELETE FROM autopilot WHERE id = $1`, autopilotID) })
// Trigger with NO creator recorded (pre-migration style).
var triggerID string
if err := pool.QueryRow(ctx, `
INSERT INTO autopilot_trigger (autopilot_id, kind, enabled, cron_expression)
VALUES ($1, 'schedule', true, '0 * * * *') RETURNING id`,
autopilotID).Scan(&triggerID); err != nil {
t.Fatalf("seed trigger: %v", err)
}
// Active rule version so the fallback resolves to rule_owner (the publisher).
if _, err := pool.Exec(ctx, `
INSERT INTO autopilot_rule_version (autopilot_id, workspace_id, published_by_type, published_by_id)
VALUES ($1, $2, 'member', $3)`, autopilotID, workspaceID, publisherID); err != nil {
t.Fatalf("seed rule version: %v", err)
}
got := triggerOwnerAttribution(ctx, q,
util.MustParseUUID(triggerID), util.MustParseUUID(workspaceID), util.MustParseUUID(autopilotID),
attribution.EvidenceAutopilotRun, util.MustParseUUID(autopilotID))
if got.Source != attribution.SourceRuleOwner {
t.Fatalf("source = %q, want rule_owner (legacy trigger falls back)", got.Source)
}
if !got.AccountableUserID.Valid || got.AccountableUserID.Bytes != util.MustParseUUID(publisherID).Bytes {
t.Errorf("accountable = %s, want rule publisher %s", util.UUIDToString(got.AccountableUserID), publisherID)
}
}
// TestEnqueueTaskForIssueAutopilotOriginStampsRuleOwner is the acceptance test for
// rule_owner (MUL-4302 §3.4): an autopilot-origin issue's run has NO authorizing
// human (originator_user_id stays NULL) but IS accountable to the publisher of the
+6 -6
View File
@@ -650,16 +650,16 @@ func (s *AutopilotService) dispatchRunOnly(ctx context.Context, ap db.Autopilot,
// the triggering member is direct_human and becomes BOTH originator (so the run
// carries their authorization context) and accountable (MUL-4302 §4). A
// schedule / webhook trigger has no human — originator_user_id stays NULL and
// the audit-accountable human is the rule_owner, the publisher of the active
// rule version, with rule_version_id recording the snapshot (MUL-4302 §3.4); a
// missing version/publisher degrades to unattributed (see ruleOwnerAttribution).
// Either way evidence points at the autopilot run and the row is never a
// NULL-source bypass.
// the audit-accountable human is the member who CREATED the firing trigger —
// trigger_owner, resolved from run.TriggerID (MUL-4302; Bohan's refinement) —
// degrading to the rule version publisher (rule_owner) when that creator is not
// recoverable, then to unattributed. Either way evidence points at the autopilot
// run and the row is never a NULL-source bypass.
var autopilotAttr attribution.Result
if actorUserID.Valid {
autopilotAttr = attribution.DirectHumanRun(actorUserID, attribution.EvidenceAutopilotRun, run.ID)
} else {
autopilotAttr = ruleOwnerAttribution(ctx, s.Queries, ap.WorkspaceID, ap.ID, attribution.EvidenceAutopilotRun, run.ID)
autopilotAttr = triggerOwnerAttribution(ctx, s.Queries, run.TriggerID, ap.WorkspaceID, ap.ID, attribution.EvidenceAutopilotRun, run.ID)
}
// If no precise human resolved (a version-less autopilot), degrade to
// owner_fallback (accountable = agent owner), or skip the dispatch when the
+28 -4
View File
@@ -380,13 +380,19 @@ func (s *TaskService) attributionForIssueTask(ctx context.Context, issue db.Issu
}
// Autopilot-origin issues (origin_id is the autopilot id) from a schedule /
// webhook trigger: no human authorized the run, so originator stays NULL, but it
// is accountable to the publisher of the autopilot's active rule version —
// rule_owner (MUL-4302 §3.4). Resolved the same way run_only dispatch resolves
// is accountable to the human who CREATED the firing trigger — trigger_owner
// (MUL-4302; Bohan's refinement), degrading to the rule publisher when that
// creator is not recoverable. Resolved the same way run_only dispatch resolves
// it, so both autopilot execution modes attribute identically. (A manual trigger
// carries an actor and is already handled above.)
// carries an actor and is already handled above.) The issue only stores the
// autopilot id, so bridge issue → active run → trigger_id to find the trigger.
if s != nil && s.Queries != nil && issue.OriginType.Valid &&
issue.OriginType.String == "autopilot" && issue.OriginID.Valid {
return ruleOwnerAttribution(ctx, s.Queries, issue.WorkspaceID, issue.OriginID, attribution.EvidenceIssueAssignment, issue.ID)
var triggerID pgtype.UUID
if run, err := s.Queries.GetAutopilotRunByIssue(ctx, issue.ID); err == nil {
triggerID = run.TriggerID
}
return triggerOwnerAttribution(ctx, s.Queries, triggerID, issue.WorkspaceID, issue.OriginID, attribution.EvidenceIssueAssignment, issue.ID)
}
facts := attribution.DirectFacts{
IssueID: issue.ID,
@@ -438,6 +444,24 @@ func ruleOwnerAttribution(ctx context.Context, q *db.Queries, workspaceID, autop
return attribution.RuleOwner(publisher, ver.ID, evidenceKind, evidenceRefID)
}
// triggerOwnerAttribution resolves an autopilot schedule/webhook run to the human
// who CREATED the firing trigger (MUL-4302; Bohan's refinement). triggerID is the
// autopilot_run's trigger_id. When it resolves to a member-created trigger, the run
// is trigger_owner-accountable to that member. A trigger with no recorded creator
// (created before per-trigger creators were captured) or an agent-created trigger
// degrades to ruleOwnerAttribution (rule publisher, then owner_fallback) — the same
// coarser behavior autopilots had before this change, so nothing regresses. Never
// errors: attribution must not fail an enqueue.
func triggerOwnerAttribution(ctx context.Context, q *db.Queries, triggerID, workspaceID, autopilotID pgtype.UUID, evidenceKind attribution.EvidenceKind, evidenceRefID pgtype.UUID) attribution.Result {
if q != nil && triggerID.Valid {
if trig, err := q.GetAutopilotTrigger(ctx, triggerID); err == nil &&
trig.CreatedByType.Valid && trig.CreatedByType.String == "member" && trig.CreatedByID.Valid {
return attribution.TriggerOwner(trig.CreatedByID, evidenceKind, evidenceRefID)
}
}
return ruleOwnerAttribution(ctx, q, workspaceID, autopilotID, evidenceKind, evidenceRefID)
}
// ErrAttributionFailClosed signals that a run resolved to no precise accountable
// human and the enqueue is REFUSED rather than started. It covers three cases, all
// of which mean "we cannot guarantee an accountable human for this run" (MUL-4302
@@ -0,0 +1,2 @@
ALTER TABLE autopilot_trigger DROP COLUMN IF EXISTS created_by_id;
ALTER TABLE autopilot_trigger DROP COLUMN IF EXISTS created_by_type;
@@ -0,0 +1,28 @@
-- Human Attribution — per-trigger creator (MUL-4302; Bohan's refinement of the
-- autopilot attribution rule). An autopilot run must be accountable to the human
-- who created the SPECIFIC trigger that fired it — the member who set up the
-- schedule, or who registered the webhook — not the publisher of the whole rule
-- (the previous rule_owner behavior). autopilot_trigger did not record a creator,
-- so this adds one. The resolver reads it as source=trigger_owner (accountable =
-- this member, originator NULL because a scheduled/webhook fire carries no human
-- authorization at fire time — same authz-safe divergence as rule_owner).
--
-- Constraints (MUL-4302 §7 + workspace DB rules): NO foreign key, NO cascade
-- (integrity is app-layer), nullable with no default so the ALTER is a fast
-- metadata-only change (no table rewrite). NULL means "no creator recorded":
-- every trigger that predates this migration, which the resolver degrades to the
-- old rule_owner behavior (then owner_fallback) rather than fabricating a human.
-- created_by_type is 'member' | 'agent' (mirrors autopilot.created_by_type); only
-- a 'member' creator becomes the accountable human, an 'agent'-created trigger
-- degrades to rule_owner like any other agent action.
ALTER TABLE autopilot_trigger
ADD COLUMN created_by_type TEXT NULL;
ALTER TABLE autopilot_trigger
ADD COLUMN created_by_id UUID NULL;
COMMENT ON COLUMN autopilot_trigger.created_by_type IS
'Actor type that created this trigger: member | agent. Paired with created_by_id. Consumed only for attribution (source=trigger_owner) — never authorization. NULL on pre-migration triggers (MUL-4302).';
COMMENT ON COLUMN autopilot_trigger.created_by_id IS
'The member/agent that created this trigger. For a member creator this is the accountable human of runs this trigger fires (source=trigger_owner). No FK, app-layer integrity. NULL on pre-migration triggers, which degrade to rule_owner (MUL-4302).';
+34 -10
View File
@@ -382,13 +382,15 @@ func (q *Queries) CreateAutopilotTask(ctx context.Context, arg CreateAutopilotTa
const createAutopilotTrigger = `-- name: CreateAutopilotTrigger :one
INSERT INTO autopilot_trigger (
autopilot_id, kind, enabled, cron_expression, timezone,
next_run_at, webhook_token, label, provider, event_filters
next_run_at, webhook_token, label, provider, event_filters,
created_by_type, created_by_id
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
COALESCE($9::text, 'generic'),
$10
) RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters
$10,
$11, $12
) RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id
`
type CreateAutopilotTriggerParams struct {
@@ -402,6 +404,8 @@ type CreateAutopilotTriggerParams struct {
Label pgtype.Text `json:"label"`
Provider pgtype.Text `json:"provider"`
EventFilters []byte `json:"event_filters"`
CreatedByType pgtype.Text `json:"created_by_type"`
CreatedByID pgtype.UUID `json:"created_by_id"`
}
func (q *Queries) CreateAutopilotTrigger(ctx context.Context, arg CreateAutopilotTriggerParams) (AutopilotTrigger, error) {
@@ -416,6 +420,8 @@ func (q *Queries) CreateAutopilotTrigger(ctx context.Context, arg CreateAutopilo
arg.Label,
arg.Provider,
arg.EventFilters,
arg.CreatedByType,
arg.CreatedByID,
)
var i AutopilotTrigger
err := row.Scan(
@@ -434,6 +440,8 @@ func (q *Queries) CreateAutopilotTrigger(ctx context.Context, arg CreateAutopilo
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
@@ -695,7 +703,7 @@ func (q *Queries) GetAutopilotRunByTriggerAndPlanned(ctx context.Context, arg Ge
}
const getAutopilotTrigger = `-- name: GetAutopilotTrigger :one
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters FROM autopilot_trigger
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id FROM autopilot_trigger
WHERE id = $1
`
@@ -718,12 +726,14 @@ func (q *Queries) GetAutopilotTrigger(ctx context.Context, id pgtype.UUID) (Auto
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
const getWebhookTriggerByToken = `-- name: GetWebhookTriggerByToken :one
SELECT t.id, t.autopilot_id, t.kind, t.enabled, t.cron_expression, t.timezone, t.next_run_at, t.webhook_token, t.label, t.last_fired_at, t.created_at, t.updated_at, t.provider, t.signing_secret, t.event_filters, a.workspace_id AS autopilot_workspace_id
SELECT t.id, t.autopilot_id, t.kind, t.enabled, t.cron_expression, t.timezone, t.next_run_at, t.webhook_token, t.label, t.last_fired_at, t.created_at, t.updated_at, t.provider, t.signing_secret, t.event_filters, t.created_by_type, t.created_by_id, a.workspace_id AS autopilot_workspace_id
FROM autopilot_trigger t
JOIN autopilot a ON a.id = t.autopilot_id
WHERE t.kind = 'webhook'
@@ -746,6 +756,8 @@ type GetWebhookTriggerByTokenRow struct {
Provider string `json:"provider"`
SigningSecret pgtype.Text `json:"signing_secret"`
EventFilters []byte `json:"event_filters"`
CreatedByType pgtype.Text `json:"created_by_type"`
CreatedByID pgtype.UUID `json:"created_by_id"`
AutopilotWorkspaceID pgtype.UUID `json:"autopilot_workspace_id"`
}
@@ -773,6 +785,8 @@ func (q *Queries) GetWebhookTriggerByToken(ctx context.Context, webhookToken pgt
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
&i.AutopilotWorkspaceID,
)
return i, err
@@ -947,7 +961,7 @@ func (q *Queries) ListAutopilotSubscribers(ctx context.Context, autopilotID pgty
const listAutopilotTriggers = `-- name: ListAutopilotTriggers :many
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters FROM autopilot_trigger
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id FROM autopilot_trigger
WHERE autopilot_id = $1
ORDER BY created_at ASC
`
@@ -980,6 +994,8 @@ func (q *Queries) ListAutopilotTriggers(ctx context.Context, autopilotID pgtype.
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
); err != nil {
return nil, err
}
@@ -1183,7 +1199,7 @@ SET webhook_token = $2,
updated_at = now()
WHERE id = $1
AND kind = 'webhook'
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id
`
type RotateAutopilotTriggerWebhookTokenParams struct {
@@ -1213,6 +1229,8 @@ func (q *Queries) RotateAutopilotTriggerWebhookToken(ctx context.Context, arg Ro
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
@@ -1305,7 +1323,7 @@ SET signing_secret = $2,
updated_at = now()
WHERE id = $1
AND kind = 'webhook'
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id
`
type SetAutopilotTriggerSigningSecretParams struct {
@@ -1337,6 +1355,8 @@ func (q *Queries) SetAutopilotTriggerSigningSecret(ctx context.Context, arg SetA
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
@@ -1346,7 +1366,7 @@ UPDATE autopilot_trigger
SET webhook_token = $2,
updated_at = now()
WHERE id = $1
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id
`
type SetAutopilotTriggerWebhookTokenParams struct {
@@ -1378,6 +1398,8 @@ func (q *Queries) SetAutopilotTriggerWebhookToken(ctx context.Context, arg SetAu
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
@@ -1732,7 +1754,7 @@ UPDATE autopilot_trigger SET
event_filters = COALESCE($7, event_filters),
updated_at = now()
WHERE id = $1
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, created_by_type, created_by_id
`
type UpdateAutopilotTriggerParams struct {
@@ -1772,6 +1794,8 @@ func (q *Queries) UpdateAutopilotTrigger(ctx context.Context, arg UpdateAutopilo
&i.Provider,
&i.SigningSecret,
&i.EventFilters,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
+4
View File
@@ -238,6 +238,10 @@ type AutopilotTrigger struct {
Provider string `json:"provider"`
SigningSecret pgtype.Text `json:"signing_secret"`
EventFilters []byte `json:"event_filters"`
// Actor type that created this trigger: member | agent. Paired with created_by_id. Consumed only for attribution (source=trigger_owner) — never authorization. NULL on pre-migration triggers (MUL-4302).
CreatedByType pgtype.Text `json:"created_by_type"`
// The member/agent that created this trigger. For a member creator this is the accountable human of runs this trigger fires (source=trigger_owner). No FK, app-layer integrity. NULL on pre-migration triggers, which degrade to rule_owner (MUL-4302).
CreatedByID pgtype.UUID `json:"created_by_id"`
}
type ChannelBindingToken struct {
+4 -2
View File
@@ -122,12 +122,14 @@ WHERE id = $1;
-- name: CreateAutopilotTrigger :one
INSERT INTO autopilot_trigger (
autopilot_id, kind, enabled, cron_expression, timezone,
next_run_at, webhook_token, label, provider, event_filters
next_run_at, webhook_token, label, provider, event_filters,
created_by_type, created_by_id
) VALUES (
$1, $2, $3, sqlc.narg('cron_expression'), sqlc.narg('timezone'),
sqlc.narg('next_run_at'), sqlc.narg('webhook_token'), sqlc.narg('label'),
COALESCE(sqlc.narg('provider')::text, 'generic'),
sqlc.narg('event_filters')
sqlc.narg('event_filters'),
sqlc.narg('created_by_type'), sqlc.narg('created_by_id')
) RETURNING *;
-- name: UpdateAutopilotTrigger :one