mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* feat(workspace): revoke a member's runtimes when they leave or are removed Previously, leaving or being removed from a workspace only deleted the member row — every runtime the departed user owned in that workspace remained in the DB, kept its daemon_token valid, and stayed reachable to the workspace's other members. The departed user lost access but their machine kept doing work. This change converges the runtime state in the same transaction as the member-row deletion: agents pinned to those runtimes are archived, in-flight tasks are cancelled (so the daemon's per-task status poller interrupts the running agent gracefully), the runtimes are forced offline, and the daemon_token rows are deleted. After commit the DaemonTokenCache is invalidated and agent:archived / daemon:register events fire so connected clients reconcile immediately. Server-side state convergence is the production safety net; the daemon_token revoke takes effect once the mdt_ flow is live (today most daemons fall back to PAT/JWT, and the member-row deletion is what stops those requests via requireWorkspaceMember). Daemon-side handling (recognising the resulting 401/404 and tearing down the local pairing for that workspace) lands in a follow-up. Co-authored-by: multica-agent <github@multica.ai> * fix(workspace): also cancel tasks for archived agents on member revoke CancelAgentTasksByRuntime only matched tasks whose runtime_id was in the revoked set, missing a real path: agent.runtime_id can be reassigned via UpdateAgent, but agent_task_queue.runtime_id keeps the value from when the task was queued. So an agent currently bound to the leaving member's runtime gets archived correctly, but its older tasks still pinned to a prior runtime stay 'queued' — and ClaimAgentTask does not gate on agent.archived_at, so those orphaned tasks remain claimable by the prior runtime. Replace CancelAgentTasksByRuntime with CancelAgentTasksByRuntimeOrAgent, which OR-matches runtime_ids and the archived agent IDs in one UPDATE. Pass the archived agent IDs through from revokeAndRemoveMember. Adds TestDeleteMember_CancelsTasksFromAgentReassignment as a regression guard: same agent, two runtimes, the older task on the surviving runtime must end up cancelled while the surviving runtime stays online. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
113 lines
3.1 KiB
Go
113 lines
3.1 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: daemon_token.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const createDaemonToken = `-- name: CreateDaemonToken :one
|
|
INSERT INTO daemon_token (token_hash, workspace_id, daemon_id, expires_at)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, token_hash, workspace_id, daemon_id, expires_at, created_at
|
|
`
|
|
|
|
type CreateDaemonTokenParams struct {
|
|
TokenHash string `json:"token_hash"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
DaemonID string `json:"daemon_id"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
}
|
|
|
|
func (q *Queries) CreateDaemonToken(ctx context.Context, arg CreateDaemonTokenParams) (DaemonToken, error) {
|
|
row := q.db.QueryRow(ctx, createDaemonToken,
|
|
arg.TokenHash,
|
|
arg.WorkspaceID,
|
|
arg.DaemonID,
|
|
arg.ExpiresAt,
|
|
)
|
|
var i DaemonToken
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.TokenHash,
|
|
&i.WorkspaceID,
|
|
&i.DaemonID,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteDaemonTokensByWorkspaceAndDaemons = `-- name: DeleteDaemonTokensByWorkspaceAndDaemons :many
|
|
DELETE FROM daemon_token
|
|
WHERE workspace_id = $1
|
|
AND daemon_id = ANY($2::text[])
|
|
RETURNING token_hash
|
|
`
|
|
|
|
type DeleteDaemonTokensByWorkspaceAndDaemonsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
DaemonIds []string `json:"daemon_ids"`
|
|
}
|
|
|
|
// Deletes every daemon_token row matching the (workspace_id, daemon_id)
|
|
// pairs implied by `daemon_ids`. Used by the member-revocation flow to
|
|
// nuke tokens for all runtimes a leaving member owned in one shot.
|
|
// Returns token_hash so the caller can invalidate auth.DaemonTokenCache
|
|
// before the 10-minute TTL expires — without that invalidate, a daemon
|
|
// can keep using its stale token until cache eviction even though the
|
|
// DB row is gone.
|
|
func (q *Queries) DeleteDaemonTokensByWorkspaceAndDaemons(ctx context.Context, arg DeleteDaemonTokensByWorkspaceAndDaemonsParams) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, deleteDaemonTokensByWorkspaceAndDaemons, arg.WorkspaceID, arg.DaemonIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []string{}
|
|
for rows.Next() {
|
|
var token_hash string
|
|
if err := rows.Scan(&token_hash); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, token_hash)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const deleteExpiredDaemonTokens = `-- name: DeleteExpiredDaemonTokens :exec
|
|
DELETE FROM daemon_token
|
|
WHERE expires_at <= now()
|
|
`
|
|
|
|
func (q *Queries) DeleteExpiredDaemonTokens(ctx context.Context) error {
|
|
_, err := q.db.Exec(ctx, deleteExpiredDaemonTokens)
|
|
return err
|
|
}
|
|
|
|
const getDaemonTokenByHash = `-- name: GetDaemonTokenByHash :one
|
|
SELECT id, token_hash, workspace_id, daemon_id, expires_at, created_at FROM daemon_token
|
|
WHERE token_hash = $1 AND expires_at > now()
|
|
`
|
|
|
|
func (q *Queries) GetDaemonTokenByHash(ctx context.Context, tokenHash string) (DaemonToken, error) {
|
|
row := q.db.QueryRow(ctx, getDaemonTokenByHash, tokenHash)
|
|
var i DaemonToken
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.TokenHash,
|
|
&i.WorkspaceID,
|
|
&i.DaemonID,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|