mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-06 19:00:01 +02:00
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>
316 lines
9.0 KiB
Go
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
|
|
}
|