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

282 lines
11 KiB
Go

package workspace
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
modelpolicybiz "github.com/tutti-os/tutti/services/tuttid/biz/modelpolicy"
)
// ErrModelPolicyNotFound reports a missing model usage policy row.
var ErrModelPolicyNotFound = errors.New("model usage policy not found")
// ErrModelPolicyReferenced reports that a policy could not be deleted because an
// agent binding still references it (enforced by the bindings foreign key).
var ErrModelPolicyReferenced = errors.New("model usage policy is still referenced")
func (s *SQLiteStore) ListModelPolicies(ctx context.Context, workspaceID string) ([]modelpolicybiz.Policy, error) {
if s == nil || s.readDB == nil {
return nil, errors.New("workspace database is not initialized")
}
rows, err := s.readDB.QueryContext(ctx, `
SELECT workspace_id, policy_id, name, execution_plan_id, execution_model,
planning_plan_id, planning_model, review_plan_id, review_model,
review_rule_enabled, review_rule_trigger, review_rule_max_runs, review_rule_max_total_tokens,
created_at_unix_ms, updated_at_unix_ms
FROM model_usage_policies
WHERE workspace_id = ?
ORDER BY created_at_unix_ms ASC, policy_id ASC
`, workspaceID)
if err != nil {
return nil, fmt.Errorf("list model policies: %w", err)
}
defer rows.Close()
var policies []modelpolicybiz.Policy
for rows.Next() {
policy, err := scanModelPolicy(rows)
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("list model policy rows: %w", err)
}
return policies, nil
}
func (s *SQLiteStore) GetModelPolicy(ctx context.Context, workspaceID string, policyID string) (modelpolicybiz.Policy, error) {
if s == nil || s.readDB == nil {
return modelpolicybiz.Policy{}, errors.New("workspace database is not initialized")
}
row := s.readDB.QueryRowContext(ctx, `
SELECT workspace_id, policy_id, name, execution_plan_id, execution_model,
planning_plan_id, planning_model, review_plan_id, review_model,
review_rule_enabled, review_rule_trigger, review_rule_max_runs, review_rule_max_total_tokens,
created_at_unix_ms, updated_at_unix_ms
FROM model_usage_policies
WHERE workspace_id = ? AND policy_id = ?
`, workspaceID, policyID)
policy, err := scanModelPolicy(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return modelpolicybiz.Policy{}, ErrModelPolicyNotFound
}
return modelpolicybiz.Policy{}, err
}
return policy, nil
}
func (s *SQLiteStore) PutModelPolicy(ctx context.Context, policy modelpolicybiz.Policy) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
_, err := s.writeDB.ExecContext(ctx, `
INSERT INTO model_usage_policies (
workspace_id, policy_id, name, execution_plan_id, execution_model,
planning_plan_id, planning_model, review_plan_id, review_model,
review_rule_enabled, review_rule_trigger, review_rule_max_runs, review_rule_max_total_tokens,
created_at_unix_ms, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, policy_id) DO UPDATE SET
name = excluded.name,
execution_plan_id = excluded.execution_plan_id,
execution_model = excluded.execution_model,
planning_plan_id = excluded.planning_plan_id,
planning_model = excluded.planning_model,
review_plan_id = excluded.review_plan_id,
review_model = excluded.review_model,
review_rule_enabled = excluded.review_rule_enabled,
review_rule_trigger = excluded.review_rule_trigger,
review_rule_max_runs = excluded.review_rule_max_runs,
review_rule_max_total_tokens = excluded.review_rule_max_total_tokens,
updated_at_unix_ms = excluded.updated_at_unix_ms
`, policy.WorkspaceID, policy.ID, policy.Name,
policy.Execution.ModelPlanID, policy.Execution.Model,
policy.Planning.ModelPlanID, policy.Planning.Model,
policy.Review.ModelPlanID, policy.Review.Model,
boolInt(policy.ReviewRule.Enabled), string(policy.ReviewRule.Trigger),
policy.ReviewRule.MaxRunsPerSession, policy.ReviewRule.MaxTotalTokensPerSession,
unixMs(policy.CreatedAt), unixMs(policy.UpdatedAt))
if err != nil {
return fmt.Errorf("put model policy: %w", err)
}
return nil
}
func (s *SQLiteStore) DeleteModelPolicy(ctx context.Context, workspaceID string, policyID string) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
result, err := s.writeDB.ExecContext(ctx, `
DELETE FROM model_usage_policies
WHERE workspace_id = ? AND policy_id = ?
`, workspaceID, policyID)
if err != nil {
// A binding foreign key (ON DELETE RESTRICT) atomically blocks deletion
// while any agent binding still references the policy.
if isSQLiteForeignKeyConstraintError(err) {
return ErrModelPolicyReferenced
}
return fmt.Errorf("delete model policy: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("delete model policy result: %w", err)
}
if affected == 0 {
return ErrModelPolicyNotFound
}
return nil
}
// ListModelPoliciesByPlan reports policies referencing one plan through any
// role so plan deletion can be blocked while consumers remain.
func (s *SQLiteStore) ListModelPoliciesByPlan(ctx context.Context, workspaceID string, planID string) ([]modelpolicybiz.Policy, error) {
if s == nil || s.readDB == nil {
return nil, errors.New("workspace database is not initialized")
}
rows, err := s.readDB.QueryContext(ctx, `
SELECT workspace_id, policy_id, name, execution_plan_id, execution_model,
planning_plan_id, planning_model, review_plan_id, review_model,
review_rule_enabled, review_rule_trigger, review_rule_max_runs, review_rule_max_total_tokens,
created_at_unix_ms, updated_at_unix_ms
FROM model_usage_policies
WHERE workspace_id = ?
AND (execution_plan_id = ? OR planning_plan_id = ? OR review_plan_id = ?)
ORDER BY policy_id ASC
`, workspaceID, planID, planID, planID)
if err != nil {
return nil, fmt.Errorf("list model policies by plan: %w", err)
}
defer rows.Close()
var policies []modelpolicybiz.Policy
for rows.Next() {
policy, err := scanModelPolicy(rows)
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("list model policies by plan rows: %w", err)
}
return policies, nil
}
func (s *SQLiteStore) GetModelPolicySessionOverride(ctx context.Context, workspaceID string, agentSessionID string) (modelpolicybiz.SessionOverride, error) {
if s == nil || s.readDB == nil {
return modelpolicybiz.SessionOverride{}, errors.New("workspace database is not initialized")
}
row := s.readDB.QueryRowContext(ctx, `
SELECT workspace_id, agent_session_id, disabled, model_policy_id, updated_at_unix_ms
FROM model_policy_session_overrides
WHERE workspace_id = ? AND agent_session_id = ?
`, workspaceID, agentSessionID)
var override modelpolicybiz.SessionOverride
var disabled int
var updatedAtUnixMS int64
if err := row.Scan(&override.WorkspaceID, &override.AgentSessionID, &disabled, &override.ModelPolicyID, &updatedAtUnixMS); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return modelpolicybiz.SessionOverride{}, sql.ErrNoRows
}
return modelpolicybiz.SessionOverride{}, fmt.Errorf("get model policy session override: %w", err)
}
override.Disabled = disabled != 0
override.UpdatedAt = time.UnixMilli(updatedAtUnixMS).UTC()
return override, nil
}
func (s *SQLiteStore) PutModelPolicySessionOverride(ctx context.Context, override modelpolicybiz.SessionOverride) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
_, err := s.writeDB.ExecContext(ctx, `
INSERT INTO model_policy_session_overrides (
workspace_id, agent_session_id, disabled, model_policy_id, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, agent_session_id) DO UPDATE SET
disabled = excluded.disabled,
model_policy_id = excluded.model_policy_id,
updated_at_unix_ms = excluded.updated_at_unix_ms
`, override.WorkspaceID, override.AgentSessionID, boolInt(override.Disabled), override.ModelPolicyID, unixMs(override.UpdatedAt))
if err != nil {
return fmt.Errorf("put model policy session override: %w", err)
}
return nil
}
func (s *SQLiteStore) GetAgentSessionAcceptance(ctx context.Context, workspaceID string, agentSessionID string) (modelpolicybiz.Acceptance, error) {
if s == nil || s.readDB == nil {
return modelpolicybiz.Acceptance{}, errors.New("workspace database is not initialized")
}
row := s.readDB.QueryRowContext(ctx, `
SELECT workspace_id, agent_session_id, state, review_run_id, updated_at_unix_ms
FROM agent_session_acceptance
WHERE workspace_id = ? AND agent_session_id = ?
`, workspaceID, agentSessionID)
var acceptance modelpolicybiz.Acceptance
var state string
var updatedAtUnixMS int64
if err := row.Scan(&acceptance.WorkspaceID, &acceptance.AgentSessionID, &state, &acceptance.ReviewRunID, &updatedAtUnixMS); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return modelpolicybiz.Acceptance{}, sql.ErrNoRows
}
return modelpolicybiz.Acceptance{}, fmt.Errorf("get agent session acceptance: %w", err)
}
acceptance.State = modelpolicybiz.AcceptanceState(state)
acceptance.UpdatedAt = time.UnixMilli(updatedAtUnixMS).UTC()
return acceptance, nil
}
func (s *SQLiteStore) PutAgentSessionAcceptance(ctx context.Context, acceptance modelpolicybiz.Acceptance) error {
if s == nil || s.writeDB == nil {
return errors.New("workspace database is not initialized")
}
// user_accepted is terminal: once a user has accepted the work, concurrent
// automation (agent_claimed / auto_checked writes) must never downgrade it.
// The stickiness is enforced here at the write boundary — not by a racy
// read-then-write in the service — so it holds under concurrency. The
// literal mirrors modelpolicybiz.AcceptanceUserAccepted.
_, err := s.writeDB.ExecContext(ctx, `
INSERT INTO agent_session_acceptance (
workspace_id, agent_session_id, state, review_run_id, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, agent_session_id) DO UPDATE SET
state = excluded.state,
review_run_id = excluded.review_run_id,
updated_at_unix_ms = excluded.updated_at_unix_ms
WHERE agent_session_acceptance.state <> 'user_accepted'
`, acceptance.WorkspaceID, acceptance.AgentSessionID, string(acceptance.State), acceptance.ReviewRunID, unixMs(acceptance.UpdatedAt))
if err != nil {
return fmt.Errorf("put agent session acceptance: %w", err)
}
return nil
}
func scanModelPolicy(row managedProviderScanner) (modelpolicybiz.Policy, error) {
var policy modelpolicybiz.Policy
var reviewEnabled int
var reviewTrigger string
var createdAtUnixMS int64
var updatedAtUnixMS int64
if err := row.Scan(&policy.WorkspaceID, &policy.ID, &policy.Name,
&policy.Execution.ModelPlanID, &policy.Execution.Model,
&policy.Planning.ModelPlanID, &policy.Planning.Model,
&policy.Review.ModelPlanID, &policy.Review.Model,
&reviewEnabled, &reviewTrigger, &policy.ReviewRule.MaxRunsPerSession, &policy.ReviewRule.MaxTotalTokensPerSession,
&createdAtUnixMS, &updatedAtUnixMS); err != nil {
return modelpolicybiz.Policy{}, err
}
policy.ReviewRule.Enabled = reviewEnabled != 0
policy.ReviewRule.Trigger = modelpolicybiz.ReviewTrigger(reviewTrigger)
policy.CreatedAt = time.UnixMilli(createdAtUnixMS).UTC()
policy.UpdatedAt = time.UnixMilli(updatedAtUnixMS).UTC()
return policy, nil
}