Files
multica/server/pkg/db/generated/runtime_profile.sql.go
LinYushen 52e76e7b23 MUL-3284: server API + daemon (custom runtime PR2) (#4149)
* MUL-3284: add runtime_profile schema (custom runtime PR1)

Schema-only foundation for custom runtimes. Additive migration 120:

- New workspace-level `runtime_profile` table: the shared, team-visible
  definition of a custom runtime (e.g. an in-house Codex wrapper).
  protocol_family is CHECK-constrained to the exact backend list in
  agent.New() (server/pkg/agent/agent.go). The only args column is
  `fixed_args` (args every agent on the runtime must inherit); there is
  deliberately no generic per-agent args field — those stay on
  agent.custom_args.
- `agent_runtime.profile_id` (nullable, FK -> runtime_profile ON DELETE
  CASCADE): NULL = built-in runtime, non-NULL = a registered instance of
  a custom profile.
- Partial unique index agent_runtime_workspace_daemon_profile_key on
  (workspace_id, daemon_id, profile_id) WHERE profile_id IS NOT NULL.

The legacy UNIQUE (workspace_id, daemon_id, provider) constraint is left
INTACT so the existing registration upsert
(ON CONFLICT (workspace_id, daemon_id, provider) in runtime.sql) keeps
resolving its arbiter and the server stays green. Converting that key to
a partial (WHERE profile_id IS NULL) index and making the upsert
profile-aware is PR2's registration work, not this migration.

Verified up + down against Postgres 17: full `migrate up` applies 120;
schema shows the table, column, partial index and intact legacy
constraint; functional checks pass (partial index blocks dup
(ws,daemon,profile), allows same profile on another daemon; CHECK and
display_name uniqueness reject bad input; legacy ON CONFLICT still
resolves; profile delete cascades to instances); down/up round-trip is
clean.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-3284: drop DB FKs/cascade from runtime_profile migration (review fix)

Per review (house rule: no new database foreign keys / cascades; relational
integrity lives in the application layer):

- runtime_profile.workspace_id: drop REFERENCES workspace ON DELETE CASCADE
  -> plain UUID NOT NULL.
- runtime_profile.created_by: drop REFERENCES "user" ON DELETE SET NULL
  -> plain UUID.
- agent_runtime.profile_id: drop REFERENCES runtime_profile ON DELETE CASCADE
  -> plain UUID.

CHECK constraints, UNIQUE (workspace_id, display_name), the workspace index,
and the partial unique index agent_runtime_workspace_daemon_profile_key are
unchanged. The legacy UNIQUE (workspace_id, daemon_id, provider) constraint
remains untouched.

Behavioral consequence: the database no longer auto-removes a profile's
agent_runtime instance rows on profile delete. That cleanup moves into PR2's
profile-delete path. Up-migration comments document this; down-migration
comment no longer references FKs/cascade.

Re-verified on Postgres 17: migrate up applies 120; no FK constraints exist on
the new columns; partial index still blocks dup (ws,daemon,profile_id); CHECK
and display_name uniqueness still reject bad input; deleting a profile now
leaves the runtime row orphaned (proving cascade is gone); down/up round-trip
clean with the legacy constraint intact.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-3284 PR2 (server): runtime_profile CRUD + profile-aware registration

Server/DB half of the custom-runtime feature.

- Migration 121: convert the legacy UNIQUE (workspace_id, daemon_id, provider)
  constraint on agent_runtime into a partial unique index scoped to built-in
  rows (WHERE profile_id IS NULL). With 120's partial index on profile_id this
  lets one daemon host the built-in provider AND custom profiles of the same
  protocol family without collision.
- Queries: runtime_profile CRUD; ListEnabledRuntimeProfilesForWorkspace
  (daemon-facing); CountAgentsByProfile + DeleteAgentRuntimesByProfile for the
  app-layer cascade; profile-aware UpsertAgentRuntimeWithProfile; the built-in
  UpsertAgentRuntime ON CONFLICT now spells out WHERE profile_id IS NULL so it
  targets the right partial index. sqlc regenerated.
- agent.SupportedTypes / IsSupportedType: single-source protocol_family
  whitelist, in lockstep with agent.New and the migration 120 CHECK.
- Handlers + routes: runtime_profile CRUD (member-read, admin-write) with
  protocol_family whitelist validation, display_name uniqueness (409), and
  fixed_args validation (no generic per-agent args — iron rule); a
  daemon-token endpoint GET /api/daemon/workspaces/{id}/runtime-profiles;
  DeleteRuntimeProfile does the app-layer cascade (delete instance rows then
  profile, in one tx) and refuses (409) while active agents are bound.
- DaemonRegister accepts an optional per-runtime profile_id: validates the
  profile belongs to the workspace and is enabled, registers via the
  profile-aware upsert, and skips legacy hostname merge for custom rows.
  AgentRuntimeResponse now carries profile_id.

Verified on Postgres 17: migrate up through 121; built-in + custom codex
coexist on one daemon; both upsert arbiters are idempotent; delete-by-profile
cascade removes only the custom instance; migrate down reverses 121 then 120
and replays clean. go build ./... and go vet pass; handler test package
compiles.

Daemon-side wiring (fetch profiles, PATH-resolve command_name, register with
profile_id, exec uses command_name) lands in a follow-up commit on this branch.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-3284 PR2 (daemon): pull profiles, PATH-resolve, register, exec command

Daemon-side half of custom runtime profiles, against the server contract on
this branch.

- client.go: GetRuntimeProfiles(workspaceID) -> GET
  /api/daemon/workspaces/{id}/runtime-profiles (mirrors GetWorkspaceRepos);
  RuntimeProfile / RuntimeProfilesResponse types.
- types.go: Runtime gains profile_id (parsed from the register response so
  runtimeIndex carries it).
- daemon.go:
  * appendProfileRuntimes — called inside registerRuntimesForWorkspace before
    the empty-runtimes guard. Best-effort fetch (older server 404s are logged
    and swallowed; never fails registration). Per enabled profile: resolve
    command_name via PATH (exec.LookPath, behind a `lookPath` test hook),
    skip+log when absent, best-effort version probe, record the resolved
    absolute path keyed by profile_id, and append a registration entry
    {name, type=protocol_family, version, status:online, profile_id}. A
    custom-only host (no built-in agents) still registers.
  * profileCommandPaths map (guarded by d.mu) + recordProfileCommandPath /
    customCommandPathForRuntime helpers.
  * runTask: looks up the claimed task's RuntimeID -> profile command path and
    overrides the executable path, synthesizing an AgentEntry so a custom
    runtime runs even when the host has no built-in agent of the same
    provider. provider (=protocol_family) is unchanged so agent.New still
    selects the right backend.
- Tests: GetRuntimeProfiles request shape; profile runtime appended + path
  recorded (custom-only host); profile skipped when command not on PATH;
  profiles-fetch-404 is best-effort; customCommandPathForRuntime bookkeeping.
- agent: lockstep test pinning SupportedTypes to agent.New and the migration
  120 protocol_family CHECK.

Iron rule honored: profile carries no generic per-agent args. fixed_args are
parsed and carried but intentionally NOT wired into the launch command yet
(optional/best-effort; explicit TODO(MUL-3284) in appendProfileRuntimes).

Verified: go build ./... clean; go vet ./internal/daemon/... clean;
go test ./internal/daemon/... pass (existing + 5 new); full
go test ./internal/handler/ suite passes against a migrated Postgres 17;
agent lockstep test passes.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-3284 PR2: profile delete runs full archived-agent cascade (fix 500)

Review fix. DeleteRuntimeProfile previously guarded only on ACTIVE agents, but
agent.runtime_id is ON DELETE RESTRICT — a profile whose runtimes had only
ARCHIVED agents passed the guard, then DeleteAgentRuntimesByProfile hit the FK
and the handler 500'd.

Now it mirrors the mature runtime-delete cascade (DeleteAgentRuntime): in one
transaction it enumerates the profile's runtime rows, refuses (409) any with
active agents or active squads led by archived agents, then for each runtime
pauses autopilots pinned to its archived agents, drops archived squads led by
them, and hard-deletes the archived agents before removing the runtime rows
and the profile. No code path can now fall through to a raw FK error.

- queries: ListAgentRuntimeIDsByProfile (sqlc regen). Reuses the existing
  per-runtime teardown queries (CountActiveSquadsWithArchivedLeadersByRuntime,
  ListArchivedAgentIDsByRuntime, PauseAutopilotsByAgentAssignees,
  DeleteSquadsByArchivedAgentsOnRuntime, DeleteArchivedAgentsByRuntime).
- tests: TestDeleteRuntimeProfile_ArchivedAgentCascade (archived-only profile
  deletes cleanly: 204, runtime + archived agent + profile gone) and
  TestDeleteRuntimeProfile_ActiveAgentBlocks (active agent → 409, survives).

Verified against Postgres 17: both new tests pass; full handler suite, daemon
tests, and agent lockstep test pass; go vet clean.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-17 11:33:09 +08:00

371 lines
11 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: runtime_profile.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countAgentsByProfile = `-- name: CountAgentsByProfile :one
SELECT count(*) FROM agent a
JOIN agent_runtime ar ON ar.id = a.runtime_id
WHERE ar.profile_id = $1 AND a.archived_at IS NULL
`
// Counts active (non-archived) agents bound to any runtime instance of this
// profile. The profile-delete path uses this to refuse deletion (409) while
// agents still depend on it, mirroring the runtime-delete guard.
func (q *Queries) CountAgentsByProfile(ctx context.Context, profileID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countAgentsByProfile, profileID)
var count int64
err := row.Scan(&count)
return count, err
}
const createRuntimeProfile = `-- name: CreateRuntimeProfile :one
INSERT INTO runtime_profile (
workspace_id,
display_name,
protocol_family,
command_name,
description,
fixed_args,
visibility,
created_by,
enabled
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at
`
type CreateRuntimeProfileParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
DisplayName string `json:"display_name"`
ProtocolFamily string `json:"protocol_family"`
CommandName string `json:"command_name"`
Description pgtype.Text `json:"description"`
FixedArgs []byte `json:"fixed_args"`
Visibility string `json:"visibility"`
CreatedBy pgtype.UUID `json:"created_by"`
Enabled bool `json:"enabled"`
}
// Custom Runtime profiles (MUL-3284). Workspace-level definitions of a custom
// runtime; see migration 120 for the table. Relational integrity (workspace,
// created_by) is enforced in the application layer — there are no DB FKs.
func (q *Queries) CreateRuntimeProfile(ctx context.Context, arg CreateRuntimeProfileParams) (RuntimeProfile, error) {
row := q.db.QueryRow(ctx, createRuntimeProfile,
arg.WorkspaceID,
arg.DisplayName,
arg.ProtocolFamily,
arg.CommandName,
arg.Description,
arg.FixedArgs,
arg.Visibility,
arg.CreatedBy,
arg.Enabled,
)
var i RuntimeProfile
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteAgentRuntimesByProfile = `-- name: DeleteAgentRuntimesByProfile :many
DELETE FROM agent_runtime
WHERE profile_id = $1
RETURNING id, workspace_id, owner_id, daemon_id, provider
`
type DeleteAgentRuntimesByProfileRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
OwnerID pgtype.UUID `json:"owner_id"`
DaemonID pgtype.Text `json:"daemon_id"`
Provider string `json:"provider"`
}
// Application-layer cascade: migration 120 dropped the DB ON DELETE CASCADE, so
// the profile-delete path must remove the profile's registered runtime
// instances itself. Returns the deleted rows so the caller can broadcast /
// audit. Runs inside the same transaction as DeleteRuntimeProfile.
func (q *Queries) DeleteAgentRuntimesByProfile(ctx context.Context, profileID pgtype.UUID) ([]DeleteAgentRuntimesByProfileRow, error) {
rows, err := q.db.Query(ctx, deleteAgentRuntimesByProfile, profileID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []DeleteAgentRuntimesByProfileRow{}
for rows.Next() {
var i DeleteAgentRuntimesByProfileRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.OwnerID,
&i.DaemonID,
&i.Provider,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteRuntimeProfile = `-- name: DeleteRuntimeProfile :exec
DELETE FROM runtime_profile
WHERE id = $1 AND workspace_id = $2
`
type DeleteRuntimeProfileParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) DeleteRuntimeProfile(ctx context.Context, arg DeleteRuntimeProfileParams) error {
_, err := q.db.Exec(ctx, deleteRuntimeProfile, arg.ID, arg.WorkspaceID)
return err
}
const getRuntimeProfile = `-- name: GetRuntimeProfile :one
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
WHERE id = $1
`
func (q *Queries) GetRuntimeProfile(ctx context.Context, id pgtype.UUID) (RuntimeProfile, error) {
row := q.db.QueryRow(ctx, getRuntimeProfile, id)
var i RuntimeProfile
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getRuntimeProfileForWorkspace = `-- name: GetRuntimeProfileForWorkspace :one
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
WHERE id = $1 AND workspace_id = $2
`
type GetRuntimeProfileForWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetRuntimeProfileForWorkspace(ctx context.Context, arg GetRuntimeProfileForWorkspaceParams) (RuntimeProfile, error) {
row := q.db.QueryRow(ctx, getRuntimeProfileForWorkspace, arg.ID, arg.WorkspaceID)
var i RuntimeProfile
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listAgentRuntimeIDsByProfile = `-- name: ListAgentRuntimeIDsByProfile :many
SELECT id FROM agent_runtime
WHERE profile_id = $1
`
// Enumerates the runtime instance rows registered against a profile. The
// profile-delete cascade walks these so it can run the same archived-agent /
// archived-squad / autopilot teardown the runtime-delete path uses before
// removing each runtime row — agent.runtime_id is ON DELETE RESTRICT, so a
// bare delete would 500 whenever an archived agent still references the row.
func (q *Queries) ListAgentRuntimeIDsByProfile(ctx context.Context, profileID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listAgentRuntimeIDsByProfile, profileID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var id pgtype.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listEnabledRuntimeProfilesForWorkspace = `-- name: ListEnabledRuntimeProfilesForWorkspace :many
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
WHERE workspace_id = $1 AND enabled = true
ORDER BY created_at ASC
`
// Daemon-facing list: only enabled profiles are candidates for a daemon to
// resolve on PATH and register. Ordered for stable output.
func (q *Queries) ListEnabledRuntimeProfilesForWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]RuntimeProfile, error) {
rows, err := q.db.Query(ctx, listEnabledRuntimeProfilesForWorkspace, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []RuntimeProfile{}
for rows.Next() {
var i RuntimeProfile
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRuntimeProfiles = `-- name: ListRuntimeProfiles :many
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
WHERE workspace_id = $1
ORDER BY created_at ASC
`
func (q *Queries) ListRuntimeProfiles(ctx context.Context, workspaceID pgtype.UUID) ([]RuntimeProfile, error) {
rows, err := q.db.Query(ctx, listRuntimeProfiles, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []RuntimeProfile{}
for rows.Next() {
var i RuntimeProfile
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateRuntimeProfile = `-- name: UpdateRuntimeProfile :one
UPDATE runtime_profile
SET display_name = COALESCE($1, display_name),
command_name = COALESCE($2, command_name),
description = COALESCE($3, description),
fixed_args = COALESCE($4, fixed_args),
visibility = COALESCE($5, visibility),
enabled = COALESCE($6, enabled),
updated_at = now()
WHERE id = $7 AND workspace_id = $8
RETURNING id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at
`
type UpdateRuntimeProfileParams struct {
DisplayName pgtype.Text `json:"display_name"`
CommandName pgtype.Text `json:"command_name"`
Description pgtype.Text `json:"description"`
FixedArgs []byte `json:"fixed_args"`
Visibility pgtype.Text `json:"visibility"`
Enabled pgtype.Bool `json:"enabled"`
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Partial update via COALESCE: NULL args leave the column unchanged. The
// protocol_family is intentionally NOT updatable — changing the underlying
// backend of an existing profile would silently repoint every agent bound to
// it onto a different protocol; callers create a new profile instead.
func (q *Queries) UpdateRuntimeProfile(ctx context.Context, arg UpdateRuntimeProfileParams) (RuntimeProfile, error) {
row := q.db.QueryRow(ctx, updateRuntimeProfile,
arg.DisplayName,
arg.CommandName,
arg.Description,
arg.FixedArgs,
arg.Visibility,
arg.Enabled,
arg.ID,
arg.WorkspaceID,
)
var i RuntimeProfile
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.DisplayName,
&i.ProtocolFamily,
&i.CommandName,
&i.Description,
&i.FixedArgs,
&i.Visibility,
&i.CreatedBy,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}