Files
multica/server/pkg/db/generated/workspace.sql.go
J ee2fd02879 feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).

owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
  accountable = agent owner, originator stays NULL (audit-only, authz untouched),
  Source.Precise()==false. finalizeAttribution's one-way invariant already allows
  accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
  GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
  explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
  chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
  owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
  workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
  run does not start). So no run is left without an accountable human, and a
  compliance workspace can block unattributable runs instead.

manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
  down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
  actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
  daemon's prompt context but must attribute to whoever clicked rerun, not the
  original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
  the rerun path only (keeping the shared CreateAgentTask insert untouched), so
  system retry (retry_of_task_id) and human rerun stay separable in reporting.

Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-09 20:01:00 +08:00

316 lines
9.0 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: workspace.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createWorkspace = `-- name: CreateWorkspace :one
INSERT INTO workspace (name, slug, description, context, issue_prefix)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed
`
type CreateWorkspaceParams struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description pgtype.Text `json:"description"`
Context pgtype.Text `json:"context"`
IssuePrefix string `json:"issue_prefix"`
}
func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error) {
row := q.db.QueryRow(ctx, createWorkspace,
arg.Name,
arg.Slug,
arg.Description,
arg.Context,
arg.IssuePrefix,
)
var i Workspace
err := row.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.Description,
&i.Settings,
&i.CreatedAt,
&i.UpdatedAt,
&i.Context,
&i.Repos,
&i.IssuePrefix,
&i.IssueCounter,
&i.AvatarUrl,
&i.AttributionFailClosed,
)
return i, err
}
const deleteWorkspace = `-- name: DeleteWorkspace :exec
WITH ws_installations AS (
SELECT id FROM channel_installation WHERE workspace_id = $1
),
cleared_chat_sessions AS (
DELETE FROM channel_chat_session_binding WHERE installation_id IN (SELECT id FROM ws_installations)
RETURNING chat_session_id
),
cleared_outbound_cards AS (
-- channel_outbound_card_message is keyed by chat_session_id (no FK); its own
-- chat_session rows cascade away with the workspace, so reach the cards through
-- the just-removed chat-session bindings, which still carry the id.
DELETE FROM channel_outbound_card_message
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
),
cleared_inbound_dedup AS (
DELETE FROM channel_inbound_message_dedup WHERE installation_id IN (SELECT id FROM ws_installations)
),
cleared_audit AS (
-- Purge, don't detach: the workspace is gone and channel_inbound_audit has no
-- workspace_id and no reaper, so a detached (NULL) row would be permanently
-- unattributable. (Reclaim, where the workspace survives, still detaches.)
DELETE FROM channel_inbound_audit WHERE installation_id IN (SELECT id FROM ws_installations)
),
cleared_user_bindings AS (
DELETE FROM channel_user_binding WHERE workspace_id = $1
),
cleared_binding_tokens AS (
DELETE FROM channel_binding_token WHERE workspace_id = $1
),
cleared_installations AS (
DELETE FROM channel_installation WHERE workspace_id = $1
),
deleted_pending_check_suites AS (
DELETE FROM github_pending_check_suite WHERE workspace_id = $1
)
DELETE FROM workspace WHERE workspace.id = $1
`
// The channel_* tables carry NO FK to workspace (MUL-3515 §4), so — unlike the
// CASCADE-backed tables the DELETE below sweeps — they are not cleaned up
// implicitly. Remove this workspace's channel installations, and every dependent
// row of each, here so a deleted workspace never leaves an orphaned installation
// occupying its bot's (channel_type, config->>'app_id') routing slot, which would
// make that bot un-rebindable anywhere until an operator hand-deletes the row
// (#4810). All in one statement so it commits atomically with the workspace row.
func (q *Queries) DeleteWorkspace(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteWorkspace, id)
return err
}
const getWorkspace = `-- name: GetWorkspace :one
SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed FROM workspace
WHERE id = $1
`
func (q *Queries) GetWorkspace(ctx context.Context, id pgtype.UUID) (Workspace, error) {
row := q.db.QueryRow(ctx, getWorkspace, id)
var i Workspace
err := row.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.Description,
&i.Settings,
&i.CreatedAt,
&i.UpdatedAt,
&i.Context,
&i.Repos,
&i.IssuePrefix,
&i.IssueCounter,
&i.AvatarUrl,
&i.AttributionFailClosed,
)
return i, err
}
const getWorkspaceAttributionFailClosed = `-- name: GetWorkspaceAttributionFailClosed :one
SELECT attribution_fail_closed FROM workspace
WHERE id = $1
`
// Lean read of the fail-closed attribution policy for the enqueue hot path
// (MUL-4302 §3.5), avoiding a full workspace-row fetch.
func (q *Queries) GetWorkspaceAttributionFailClosed(ctx context.Context, id pgtype.UUID) (bool, error) {
row := q.db.QueryRow(ctx, getWorkspaceAttributionFailClosed, id)
var attribution_fail_closed bool
err := row.Scan(&attribution_fail_closed)
return attribution_fail_closed, err
}
const getWorkspaceBySlug = `-- name: GetWorkspaceBySlug :one
SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed FROM workspace
WHERE slug = $1
`
func (q *Queries) GetWorkspaceBySlug(ctx context.Context, slug string) (Workspace, error) {
row := q.db.QueryRow(ctx, getWorkspaceBySlug, slug)
var i Workspace
err := row.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.Description,
&i.Settings,
&i.CreatedAt,
&i.UpdatedAt,
&i.Context,
&i.Repos,
&i.IssuePrefix,
&i.IssueCounter,
&i.AvatarUrl,
&i.AttributionFailClosed,
)
return i, err
}
const incrementIssueCounter = `-- name: IncrementIssueCounter :one
UPDATE workspace SET issue_counter = issue_counter + 1
WHERE id = $1
RETURNING issue_counter
`
func (q *Queries) IncrementIssueCounter(ctx context.Context, id pgtype.UUID) (int32, error) {
row := q.db.QueryRow(ctx, incrementIssueCounter, id)
var issue_counter int32
err := row.Scan(&issue_counter)
return issue_counter, err
}
const listWorkspaces = `-- name: ListWorkspaces :many
SELECT w.id, w.name, w.slug, w.description, w.settings,
w.created_at, w.updated_at, w.context, w.repos,
w.issue_prefix, w.issue_counter, w.avatar_url, w.attribution_fail_closed
FROM member m
JOIN workspace w ON w.id = m.workspace_id
WHERE m.user_id = $1
ORDER BY w.created_at ASC
`
func (q *Queries) ListWorkspaces(ctx context.Context, userID pgtype.UUID) ([]Workspace, error) {
rows, err := q.db.Query(ctx, listWorkspaces, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Workspace{}
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.Description,
&i.Settings,
&i.CreatedAt,
&i.UpdatedAt,
&i.Context,
&i.Repos,
&i.IssuePrefix,
&i.IssueCounter,
&i.AvatarUrl,
&i.AttributionFailClosed,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWorkspacesWithRepos = `-- name: ListWorkspacesWithRepos :many
SELECT id, repos FROM workspace
WHERE repos IS NOT NULL AND repos <> '[]'::jsonb
ORDER BY id
`
type ListWorkspacesWithReposRow struct {
ID pgtype.UUID `json:"id"`
Repos []byte `json:"repos"`
}
// Workspaces with a non-empty repo registry, to route a webhook to the repo's
// owning workspace. ORDER BY id keeps the resolver's tie-break stable on replay.
func (q *Queries) ListWorkspacesWithRepos(ctx context.Context) ([]ListWorkspacesWithReposRow, error) {
rows, err := q.db.Query(ctx, listWorkspacesWithRepos)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListWorkspacesWithReposRow{}
for rows.Next() {
var i ListWorkspacesWithReposRow
if err := rows.Scan(&i.ID, &i.Repos); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateWorkspace = `-- name: UpdateWorkspace :one
UPDATE workspace SET
name = COALESCE($2, name),
description = COALESCE($3, description),
context = COALESCE($4, context),
settings = COALESCE($5, settings),
repos = COALESCE($6, repos),
issue_prefix = COALESCE($7, issue_prefix),
avatar_url = COALESCE($8, avatar_url),
updated_at = now()
WHERE id = $1
RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed
`
type UpdateWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Context pgtype.Text `json:"context"`
Settings []byte `json:"settings"`
Repos []byte `json:"repos"`
IssuePrefix pgtype.Text `json:"issue_prefix"`
AvatarUrl pgtype.Text `json:"avatar_url"`
}
func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) {
row := q.db.QueryRow(ctx, updateWorkspace,
arg.ID,
arg.Name,
arg.Description,
arg.Context,
arg.Settings,
arg.Repos,
arg.IssuePrefix,
arg.AvatarUrl,
)
var i Workspace
err := row.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.Description,
&i.Settings,
&i.CreatedAt,
&i.UpdatedAt,
&i.Context,
&i.Repos,
&i.IssuePrefix,
&i.IssueCounter,
&i.AvatarUrl,
&i.AttributionFailClosed,
)
return i, err
}