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
282 lines
11 KiB
Go
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
|
|
}
|