Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

401 lines
15 KiB
Go

package storesqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
)
var ErrSubmitClaimTurnConflict = errors.New("workspace agent submit claim canonical turn conflict")
type SubmitClaim struct {
WorkspaceID string
AgentSessionID string
ClientSubmitID string
Status string
CanonicalTurnID string
TurnID string
MetadataJSON string
CreatedAtUnixMS int64
UpdatedAtUnixMS int64
}
type SubmitClaimPrepare struct {
WorkspaceID string
AgentSessionID string
ClientSubmitID string
CanonicalTurnID string
MetadataJSON string
NowUnixMS int64
}
func (s *Store) PrepareSubmitClaim(ctx context.Context, input SubmitClaimPrepare) (SubmitClaim, bool, error) {
input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
input.AgentSessionID = strings.TrimSpace(input.AgentSessionID)
input.ClientSubmitID = strings.TrimSpace(input.ClientSubmitID)
input.CanonicalTurnID = strings.TrimSpace(input.CanonicalTurnID)
if strings.TrimSpace(input.MetadataJSON) == "" {
input.MetadataJSON = "{}"
}
var metadata map[string]any
metadataErr := json.Unmarshal([]byte(input.MetadataJSON), &metadata)
if input.WorkspaceID == "" || input.AgentSessionID == "" || input.ClientSubmitID == "" || input.CanonicalTurnID == "" || input.NowUnixMS <= 0 {
return SubmitClaim{}, false, fmt.Errorf("invalid workspace agent submit claim")
}
if metadataErr != nil || metadata == nil {
return SubmitClaim{}, false, fmt.Errorf("invalid workspace agent submit claim metadata")
}
// Enforce the privacy boundary at the durable API as well as at Host call
// sites: claims retain only the closed provenance needed before the full
// submission envelope is available.
claimMetadata := make(map[string]any, 1)
if mode, ok := metadata["uiMode"].(string); ok && (mode == "os" || mode == "agent") {
claimMetadata["uiMode"] = mode
}
encodedMetadata, err := json.Marshal(claimMetadata)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("encode workspace agent submit claim metadata: %w", err)
}
input.MetadataJSON = string(encodedMetadata)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("begin prepare submit claim: %w", err)
}
defer func() { _ = tx.Rollback() }()
if claim, found, err := getSubmitClaimTx(
ctx,
tx,
input.WorkspaceID,
input.AgentSessionID,
input.ClientSubmitID,
); err != nil {
return SubmitClaim{}, false, err
} else if found {
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit duplicate submit claim read: %w", err)
}
return claim, false, nil
}
if err := requireSessionForkSourceWritableTx(
ctx,
tx,
input.WorkspaceID,
input.AgentSessionID,
); err != nil {
return SubmitClaim{}, false, err
}
result, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO workspace_agent_submit_claims
(workspace_id, agent_session_id, client_submit_id, status, turn_id, created_at_unix_ms, updated_at_unix_ms, canonical_turn_id, metadata_json)
VALUES (?, ?, ?, 'prepared', NULL, ?, ?, ?, ?)`, input.WorkspaceID, input.AgentSessionID, input.ClientSubmitID, input.NowUnixMS, input.NowUnixMS, input.CanonicalTurnID, input.MetadataJSON)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("prepare submit claim: %w", err)
}
created, err := rowsWereAffected(result, "prepare submit claim")
if err != nil {
return SubmitClaim{}, false, err
}
claim, ok, err := getSubmitClaimTx(
ctx,
tx,
input.WorkspaceID,
input.AgentSessionID,
input.ClientSubmitID,
)
if err != nil {
return SubmitClaim{}, false, err
}
if !ok {
return SubmitClaim{}, false, fmt.Errorf("prepared submit claim disappeared before it could be read")
}
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit prepare submit claim: %w", err)
}
return claim, created, nil
}
func (s *Store) AcceptSubmitClaim(ctx context.Context, workspaceID, agentSessionID, clientSubmitID, turnID string, nowUnixMS int64) (SubmitClaim, bool, error) {
workspaceID, agentSessionID, clientSubmitID, turnID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), strings.TrimSpace(clientSubmitID), strings.TrimSpace(turnID)
if workspaceID == "" || agentSessionID == "" || clientSubmitID == "" || turnID == "" || nowUnixMS <= 0 {
return SubmitClaim{}, false, fmt.Errorf("invalid accepted submit claim")
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("begin accept submit claim: %w", err)
}
defer func() { _ = tx.Rollback() }()
claim, ok, err := getSubmitClaimTx(ctx, tx, workspaceID, agentSessionID, clientSubmitID)
if err != nil {
return SubmitClaim{}, false, err
}
if !ok {
return SubmitClaim{}, false, fmt.Errorf("accepted submit claim does not exist")
}
if claim.CanonicalTurnID == "" || claim.CanonicalTurnID != turnID || (claim.Status == "accepted" && claim.TurnID != turnID) {
return claim, false, fmt.Errorf(
"%w: claim canonical=%q accepted=%q returned=%q",
ErrSubmitClaimTurnConflict,
claim.CanonicalTurnID,
claim.TurnID,
turnID,
)
}
if claim.Status == "accepted" {
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit accepted submit claim replay: %w", err)
}
return claim, false, nil
}
if claim.Status == "rejected" {
return claim, false, fmt.Errorf("%w: rejected submit claim cannot be accepted", ErrSubmitClaimTurnConflict)
}
if claim.Status != "prepared" {
return claim, false, fmt.Errorf("%w: unsupported submit claim status %q", ErrSubmitClaimTurnConflict, claim.Status)
}
if err := requireSessionForkSourceWritableTx(ctx, tx, workspaceID, agentSessionID); err != nil {
return SubmitClaim{}, false, err
}
result, err := tx.ExecContext(ctx, `UPDATE workspace_agent_submit_claims SET status='accepted', turn_id=?, updated_at_unix_ms=?
WHERE workspace_id=? AND agent_session_id=? AND client_submit_id=? AND status='prepared' AND canonical_turn_id=?`, turnID, nowUnixMS, workspaceID, agentSessionID, clientSubmitID, turnID)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("accept submit claim: %w", err)
}
updated, err := rowsWereAffected(result, "accept submit claim")
if err != nil {
return SubmitClaim{}, false, err
}
claim, ok, err = getSubmitClaimTx(ctx, tx, workspaceID, agentSessionID, clientSubmitID)
if err != nil {
return SubmitClaim{}, false, err
}
if !ok {
return SubmitClaim{}, false, fmt.Errorf("accepted submit claim disappeared before it could be read")
}
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit accept submit claim: %w", err)
}
return claim, updated, nil
}
// RejectSubmitClaim records a definitive provider rejection against the
// canonical Turn that already owns the submit. Rejection is terminal and
// idempotent: a replay of the same claim never downgrades an accepted claim or
// allocates a second Turn.
func (s *Store) RejectSubmitClaim(ctx context.Context, workspaceID, agentSessionID, clientSubmitID, turnID string, nowUnixMS int64) (SubmitClaim, bool, error) {
workspaceID, agentSessionID, clientSubmitID, turnID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), strings.TrimSpace(clientSubmitID), strings.TrimSpace(turnID)
if workspaceID == "" || agentSessionID == "" || clientSubmitID == "" || turnID == "" || nowUnixMS <= 0 {
return SubmitClaim{}, false, fmt.Errorf("invalid rejected submit claim")
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("begin reject submit claim: %w", err)
}
defer func() { _ = tx.Rollback() }()
claim, ok, err := getSubmitClaimTx(ctx, tx, workspaceID, agentSessionID, clientSubmitID)
if err != nil {
return SubmitClaim{}, false, err
}
if !ok {
return SubmitClaim{}, false, fmt.Errorf("rejected submit claim does not exist")
}
if claim.CanonicalTurnID == "" || claim.CanonicalTurnID != turnID ||
(claim.Status != "prepared" && claim.TurnID != turnID) {
return claim, false, fmt.Errorf(
"%w: claim canonical=%q rejected=%q returned=%q",
ErrSubmitClaimTurnConflict,
claim.CanonicalTurnID,
claim.TurnID,
turnID,
)
}
if claim.Status == "accepted" {
return claim, false, fmt.Errorf("%w: accepted submit claim cannot be rejected", ErrSubmitClaimTurnConflict)
}
if claim.Status == "rejected" {
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit rejected submit claim replay: %w", err)
}
return claim, false, nil
}
if claim.Status != "prepared" {
return claim, false, fmt.Errorf("%w: unsupported submit claim status %q", ErrSubmitClaimTurnConflict, claim.Status)
}
if err := requireSessionForkSourceWritableTx(ctx, tx, workspaceID, agentSessionID); err != nil {
return SubmitClaim{}, false, err
}
result, err := tx.ExecContext(ctx, `UPDATE workspace_agent_submit_claims SET status='rejected', turn_id=?, updated_at_unix_ms=?
WHERE workspace_id=? AND agent_session_id=? AND client_submit_id=? AND status='prepared' AND canonical_turn_id=?`, turnID, nowUnixMS, workspaceID, agentSessionID, clientSubmitID, turnID)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("reject submit claim: %w", err)
}
updated, err := rowsWereAffected(result, "reject submit claim")
if err != nil {
return SubmitClaim{}, false, err
}
claim, ok, err = getSubmitClaimTx(ctx, tx, workspaceID, agentSessionID, clientSubmitID)
if err != nil {
return SubmitClaim{}, false, err
}
if !ok {
return SubmitClaim{}, false, fmt.Errorf("rejected submit claim disappeared before it could be read")
}
if !updated && claim.Status == "prepared" {
return claim, false, fmt.Errorf("%w: prepared submit claim was not rejected", ErrSubmitClaimTurnConflict)
}
if err := tx.Commit(); err != nil {
return SubmitClaim{}, false, fmt.Errorf("commit reject submit claim: %w", err)
}
return claim, updated, nil
}
func (s *Store) DeleteSubmitClaim(ctx context.Context, workspaceID, agentSessionID, clientSubmitID string) (bool, error) {
workspaceID, agentSessionID, clientSubmitID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), strings.TrimSpace(clientSubmitID)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return false, fmt.Errorf("begin delete submit claim: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, found, err := getSubmitClaimTx(ctx, tx, workspaceID, agentSessionID, clientSubmitID); err != nil {
return false, err
} else if !found {
if err := tx.Commit(); err != nil {
return false, fmt.Errorf("commit absent submit claim delete: %w", err)
}
return false, nil
}
if err := requireSessionForkSourceWritableTx(ctx, tx, workspaceID, agentSessionID); err != nil {
return false, err
}
result, err := tx.ExecContext(ctx, `DELETE FROM workspace_agent_submit_claims WHERE workspace_id=? AND agent_session_id=? AND client_submit_id=?`, workspaceID, agentSessionID, clientSubmitID)
if err != nil {
return false, fmt.Errorf("delete submit claim: %w", err)
}
deleted, err := rowsWereAffected(result, "delete submit claim")
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
return false, fmt.Errorf("commit delete submit claim: %w", err)
}
return deleted, nil
}
func (s *Store) GetSubmitClaim(ctx context.Context, workspaceID, agentSessionID, clientSubmitID string) (SubmitClaim, bool, error) {
return s.getSubmitClaim(
ctx,
strings.TrimSpace(workspaceID),
strings.TrimSpace(agentSessionID),
strings.TrimSpace(clientSubmitID),
)
}
// FindSubmitClaimByCanonicalTurn resolves the opaque provider recovery token
// owned by Tutti. More than one claim for a canonical Turn is ambiguous and
// fails closed instead of selecting by creation order.
func (s *Store) FindSubmitClaimByCanonicalTurn(
ctx context.Context,
workspaceID, agentSessionID, canonicalTurnID string,
) (SubmitClaim, bool, error) {
workspaceID = strings.TrimSpace(workspaceID)
agentSessionID = strings.TrimSpace(agentSessionID)
canonicalTurnID = strings.TrimSpace(canonicalTurnID)
if s == nil || s.db == nil || workspaceID == "" ||
agentSessionID == "" || canonicalTurnID == "" {
return SubmitClaim{}, false, nil
}
rows, err := s.db.QueryContext(ctx, `
SELECT workspace_id, agent_session_id, client_submit_id, status,
canonical_turn_id, turn_id, metadata_json, created_at_unix_ms, updated_at_unix_ms
FROM workspace_agent_submit_claims
WHERE workspace_id = ? AND agent_session_id = ? AND canonical_turn_id = ?
AND status IN ('prepared', 'accepted')
ORDER BY client_submit_id
LIMIT 2
`, workspaceID, agentSessionID, canonicalTurnID)
if err != nil {
return SubmitClaim{}, false, fmt.Errorf(
"find submit claim by canonical turn: %w",
err,
)
}
defer rows.Close()
var result SubmitClaim
count := 0
for rows.Next() {
claim, _, scanErr := scanSubmitClaim(rows)
if scanErr != nil {
return SubmitClaim{}, false, scanErr
}
result = claim
count++
}
if err := rows.Err(); err != nil {
return SubmitClaim{}, false, fmt.Errorf(
"iterate submit claims by canonical turn: %w",
err,
)
}
if count > 1 {
return SubmitClaim{}, false, ErrSubmitClaimTurnConflict
}
return result, count == 1, nil
}
func (s *Store) getSubmitClaim(ctx context.Context, workspaceID, agentSessionID, clientSubmitID string) (SubmitClaim, bool, error) {
return scanSubmitClaim(s.db.QueryRowContext(
ctx,
`SELECT workspace_id, agent_session_id, client_submit_id, status, canonical_turn_id, turn_id, metadata_json, created_at_unix_ms, updated_at_unix_ms
FROM workspace_agent_submit_claims WHERE workspace_id=? AND agent_session_id=? AND client_submit_id=?`,
workspaceID,
agentSessionID,
clientSubmitID,
))
}
func getSubmitClaimTx(
ctx context.Context,
tx *sql.Tx,
workspaceID, agentSessionID, clientSubmitID string,
) (SubmitClaim, bool, error) {
return scanSubmitClaim(tx.QueryRowContext(
ctx,
`SELECT workspace_id, agent_session_id, client_submit_id, status, canonical_turn_id, turn_id, metadata_json, created_at_unix_ms, updated_at_unix_ms
FROM workspace_agent_submit_claims WHERE workspace_id=? AND agent_session_id=? AND client_submit_id=?`,
workspaceID,
agentSessionID,
clientSubmitID,
))
}
func scanSubmitClaim(row rowScanner) (SubmitClaim, bool, error) {
var claim SubmitClaim
var canonicalTurnID sql.NullString
var turnID sql.NullString
err := row.Scan(
&claim.WorkspaceID,
&claim.AgentSessionID,
&claim.ClientSubmitID,
&claim.Status,
&canonicalTurnID,
&turnID,
&claim.MetadataJSON,
&claim.CreatedAtUnixMS,
&claim.UpdatedAtUnixMS,
)
if err == sql.ErrNoRows {
return SubmitClaim{}, false, nil
}
if err != nil {
return SubmitClaim{}, false, fmt.Errorf("get submit claim: %w", err)
}
if turnID.Valid {
claim.TurnID = turnID.String
}
if canonicalTurnID.Valid {
claim.CanonicalTurnID = canonicalTurnID.String
}
return claim, true, nil
}