entity

package
v0.37.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Index

Constants

View Source
const (
	StageBacklog = "backlog"
	StageReady   = "ready"
	StageRunning = "running"
	StageReview  = "review"
	StageDone    = "done"
	StageBlocked = "blocked"
)

Board stages. Columns map onto these; the set itself is fixed.

View Source
const (
	GateModeOff      = "off"
	GateModeWarning  = "warning"
	GateModeBlocking = "blocking"
)

Gate modes for evidence-gated completion. "warning" exists so a board can be switched on without immediately blocking everyone — soft launch first, enforce once the evidence habit is established.

View Source
const (
	ClaimUnclaimed = "unclaimed"
	ClaimClaimed   = "claimed"
	ClaimStarted   = "started"
	ClaimComplete  = "complete"
	ClaimFailed    = "failed"
)

Task claim states, distinct from Stage: Stage is where a human sees the card, ClaimState is whether a worker holds it.

View Source
const (
	DelegationQueued          = "queued"
	DelegationRunning         = "running"
	DelegationDone            = "done"
	DelegationFailed          = "failed"
	DelegationInterrupted     = "interrupted"
	DelegationStoppedMaxTurns = "stopped_max_turns"
	DelegationStoppedBudget   = "stopped_budget"
)

Delegation status values. Strings (not iota) so rows stay self-describing for ad-hoc queries.

The four terminal-by-failure states are deliberately distinct because they mean different things to both the leader and the operator:

  • DelegationInterrupted — a HUMAN stopped it.
  • DelegationStoppedMaxTurns — the per-sub-agent turn cap hit.
  • DelegationStoppedBudget — the per-root aggregate budget ran out.
  • DelegationFailed — a runtime error.

Collapsing them loses the ability to tell "the user changed their mind" from "the agent ran away", which is exactly the distinction the leader needs in order to decide whether retrying is sensible.

SubAgentStatuses below is the single source of truth; the TypeScript union is generated against it by a test so the two lists cannot drift.

View Source
const (
	IncidentInvestigating = "investigating"
	IncidentConfirmed     = "confirmed"
	IncidentEscalated     = "escalated"
	IncidentClosed        = "closed"
)

Incident status values. Strings so rows stay self-describing for ad-hoc queries, like every other status in this schema.

"escalated" is deliberately distinct from "closed": an investigation that ran out of iterations, runtime or ideas is a different thing from one somebody finished, and the difference is what tells an operator whether to look.

View Source
const (
	MessageAsk   = "ask"
	MessageTell  = "tell"
	MessageReply = "reply"
)

Message kinds.

A reply is its own row rather than a column on the ask, so a tree's messages read as one conversation in creation order instead of a set of question objects with answers hidden inside them.

View Source
const (
	MessageQueued    = "queued"
	MessageDelivered = "delivered"
	MessageAnswered  = "answered"
	MessageDropped   = "dropped"
)

Message statuses. "delivered" means handed to the recipient's turn; "answered" is reserved for an ask whose reply exists.

View Source
const (
	ScheduledKindOnce      = "once"
	ScheduledKindRecurring = "recurring"
)

Schedule kind.

View Source
const (
	ScheduledStatusPending   = "pending" // one-shot, not yet fired
	ScheduledStatusActive    = "active"  // recurring, live
	ScheduledStatusDone      = "done"    // finished (one-shot fired, or recurring hit its stop condition)
	ScheduledStatusCancelled = "cancelled"
	ScheduledStatusFailed    = "failed"
)

Schedule status constants. Kept as strings so the wire/DB form stays stable across builds.

View Source
const (
	ScheduledByAI   = "ai"
	ScheduledByUser = "user"
	ScheduledByAPI  = "api"
)

ScheduledCreatedBy values.

View Source
const (
	VisibilityPublic  = pkgentity.VisibilityPublic
	VisibilityPrivate = pkgentity.VisibilityPrivate
)
View Source
const (
	HomeViewCompact  = "compact"
	HomeViewDetailed = "detailed"
)
View Source
const LeaderHandle = "main"

LeaderHandle is the address of the conversation owner (the MAIN agent). Reserved: a profile literally named "main" must not be able to take the leader's address and inherit the authority that goes with it.

View Source
const RootParentID = uint(0)

RootParentID is the sentinel parent_id value for root-level items (direct children of an instance root). Using 0 instead of NULL avoids SQLite's NULL-inequality issue in unique indexes.

View Source
const SSOProviderGoogle = "google"

SSOProviderGoogle is the provider key for Google OAuth.

Variables

BoardStages is the canonical ordered stage list.

DelegationStatuses is the complete, ordered status set. Kept in one place so the enum-drift test and any UI mapping read the same list.

IncidentStatuses is the complete set, in the order a UI should show it.

MessageKinds is the complete kind set, so a UI mapping and any validation read the same list.

View Source
var StructToConfigs = pkgentity.StructToConfigs

StructToConfigs is re-exported from pkg/entity — see that package for the tag grammar and reflection rules.

View Source
var VisibleWhenMatches = pkgentity.VisibleWhenMatches

VisibleWhenMatches is re-exported from pkg/entity — evaluates a visible_when predicate against a values map.

Functions

func IsTerminalDelegationStatus added in v0.36.0

func IsTerminalDelegationStatus(s string) bool

IsTerminalDelegationStatus reports whether no further work will happen on a delegation in this status. Used by Interrupt to short-circuit before killing anything.

func IsTerminalIncidentStatus added in v0.36.1

func IsTerminalIncidentStatus(s string) bool

IsTerminalIncidentStatus reports whether an investigation has stopped.

Types

type AgentBoard added in v0.36.0

type AgentBoard struct {
	ID          string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	Key         string `gorm:"type:varchar(128);uniqueIndex;not null" json:"key"`
	Name        string `gorm:"type:varchar(256);not null;default:''" json:"name"`
	Description string `gorm:"type:text;not null;default:''" json:"description"`
	// Columns is a JSON array of {id,title,stage,order}. Presentation
	// only — the state machine reads Stage, never the column id.
	Columns string `gorm:"type:text;not null;default:'[]'" json:"columns"`
	// SquadKey scopes which agents claim from this board.
	SquadKey string `gorm:"type:varchar(128);not null;default:''" json:"squad_key"`
	// GateMode controls evidence-gated completion: off | warning | blocking.
	GateMode string `gorm:"type:varchar(16);not null;default:'off'" json:"gate_mode"`
	// AutoDelegate makes entering the `ready` stage enqueue work
	// automatically — the column-as-automation-trigger model.
	AutoDelegate bool   `gorm:"not null;default:false" json:"auto_delegate"`
	Disabled     bool   `gorm:"not null;default:false" json:"disabled"`
	CreatedBy    string `gorm:"type:varchar(128);not null;default:''" json:"created_by"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

AgentBoard is one task board.

func (AgentBoard) TableName added in v0.36.0

func (AgentBoard) TableName() string

type AgentChannel added in v0.9.0

type AgentChannel struct {
	ID        string  `gorm:"primaryKey;type:varchar(64)"`
	Type      string  `gorm:"type:varchar(32);not null;index"`
	Name      string  `gorm:"type:varchar(128);not null;default:'default'"`
	UserID    *string `gorm:"type:varchar(36);index"`
	Enabled   bool    `gorm:"not null;default:true"`
	Config    string  `gorm:"type:text;not null;default:'{}'"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (AgentChannel) TableName added in v0.9.0

func (AgentChannel) TableName() string

type AgentDelegation added in v0.36.0

type AgentDelegation struct {
	ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	// RootID is the id of the top-most delegation in this tree; equal to
	// ID when Depth == 0. Turn budget is pooled across the whole root.
	RootID string `gorm:"type:varchar(64);not null;index" json:"root_id"`
	// ParentSessionID is the leader session that called wick_delegate.
	ParentSessionID string `gorm:"type:varchar(128);not null;index" json:"parent_session_id"`
	ParentAgent     string `gorm:"type:varchar(128);not null;default:''" json:"parent_agent"`
	ProfileKey      string `gorm:"type:varchar(128);not null" json:"profile_key"`
	// ChildSessionID is the isolated execution context spawned for this
	// delegation. A real session (own transcript + store), just hidden
	// from the conversation list.
	ChildSessionID string `gorm:"type:varchar(128);not null;index" json:"child_session_id"`
	ChildAgent     string `gorm:"type:varchar(128);not null;default:''" json:"child_agent"`

	Task  string `gorm:"type:text;not null;default:''" json:"task"`
	Depth int    `gorm:"not null;default:0" json:"depth"`
	Mode  string `gorm:"type:varchar(16);not null;default:'sync'" json:"mode"`
	// AncestorKeys is the JSON-encoded profile-key chain from root to
	// parent. The cycle guard rejects a delegation whose profile already
	// appears here, which is what stops A→B→A from looping forever.
	AncestorKeys string `gorm:"type:text;not null;default:'[]'" json:"ancestor_keys"`

	// DeliverySink routes an async result: "channel" posts back to the
	// originating thread, "session" re-prompts the leader (collect),
	// "none" leaves it for the monitor only.
	DeliverySink string `gorm:"type:varchar(32);not null;default:''" json:"delivery_sink"`
	// Detached marks an async delegation that must outlive its leader.
	// A detached sub-agent is NOT torn down when the leader goes idle or
	// is killed — it was fired deliberately to run on its own.
	Detached bool `gorm:"not null;default:false" json:"detached"`
	// Collected marks an async result the leader has already picked up
	// via wick_delegate_collect, so a second call does not re-deliver it.
	Collected bool `gorm:"not null;default:false" json:"collected"`

	// ProjectID is the scope the delegation ran in, copied from the parent
	// session. Roles are scoped, so resolving ProfileKey later without it
	// would find the GLOBAL role of the same name — a different prompt and
	// a different tool list, with nothing to signal the mismatch.
	ProjectID string `gorm:"type:varchar(64);not null;default:'';index" json:"project_id"`

	Workspace     string `gorm:"type:varchar(16);not null;default:'shared'" json:"workspace"`
	WorkspacePath string `gorm:"type:text;not null;default:''" json:"workspace_path"`
	// WorkspaceNote records why a requested workspace mode was not honoured
	// (e.g. worktree asked for on a non-git project, fell back to shared).
	// Surfaced to the leader so a silent downgrade never looks like success.
	WorkspaceNote string `gorm:"type:text;not null;default:''" json:"workspace_note"`
	// SquadKey links this delegation to the named squad that produced it.
	SquadKey string `gorm:"type:varchar(128);not null;default:'';index" json:"squad_key"`
	// UserSteered marks a delegation a human sent a message into
	// mid-flight. The result is no longer purely the sub-agent's own work,
	// and the leader is told so rather than being handed a steered answer
	// as though the role produced it unaided.
	UserSteered bool `gorm:"not null;default:false" json:"user_steered"`

	Status    string `gorm:"type:varchar(32);not null;index" json:"status"`
	TurnsUsed int    `gorm:"not null;default:0" json:"turns_used"`
	MaxTurns  int    `gorm:"not null;default:0" json:"max_turns"`

	// Token accounting. Populated when the provider reports usage in its
	// stream; 0 means "this provider did not tell us", never "free".
	//
	// Usage is recorded BEFORE any cancellation check, deliberately: a
	// turn that gets cancelled still consumed tokens, and skipping the
	// write there silently under-reports spend.
	InputTokens  int `gorm:"not null;default:0" json:"input_tokens"`
	OutputTokens int `gorm:"not null;default:0" json:"output_tokens"`
	TokensUsed   int `gorm:"not null;default:0" json:"tokens_used"`
	// MaxTokens is the per-delegation token cap, 0 = uncapped by the
	// delegation itself (the per-tree budget still applies).
	MaxTokens int `gorm:"not null;default:0" json:"max_tokens"`
	// Result carries the final answer on success and the PARTIAL text on
	// interrupt / turn-exhaustion. Never empty-on-stop by design: the
	// leader receives partial work as a normal tool result rather than an
	// error, because an error tends to trigger a blind retry right after
	// the user asked for a stop.
	Result   string `gorm:"type:text;not null;default:''" json:"result"`
	ErrorMsg string `gorm:"type:text;not null;default:''" json:"error_msg"`
	// ResultJSON is the structured envelope a sub-agent reported through
	// report_result, or one reconstructed from Result when it never
	// called. Result stays authoritative for humans; this column exists
	// so a supervisor can act on an answer without re-reading prose.
	ResultJSON string `gorm:"type:text;not null;default:''" json:"result_json,omitempty"`

	// TriggeredBy is the human user id at the root of this tree. Drives
	// both tag inheritance and interrupt authorization; it is NOT the
	// leader agent's identity.
	TriggeredBy string     `gorm:"type:varchar(128);not null;default:'';index" json:"triggered_by"`
	StartedAt   time.Time  `json:"started_at"`
	EndedAt     *time.Time `json:"ended_at,omitempty"`

	// Handle is this instance's address inside its tree ("reviewer-2").
	// Unique per RootID and never reused, because a handle that could be
	// recycled would let a message land on a different agent than the one
	// the sender was talking to.
	Handle string `gorm:"type:varchar(64);not null;default:''" json:"handle"`
	// HopCount is the number of consecutive agent-to-agent messages in
	// this tree since a human last spoke. Stored on the ROOT row only, and
	// reset only by a human turn — a leader that could reset its own limit
	// would not be limited.
	HopCount int `gorm:"not null;default:0" json:"hop_count"`

	// Blocked marks a RUNNING delegation whose agent is waiting on a
	// synchronous child rather than working. A blocked row does not hold a
	// parallel slot — without this, a serial room deadlocks the moment a
	// sub-agent delegates: the parent owns the only slot while its child
	// queues behind it, and neither can ever proceed.
	Blocked bool `gorm:"not null;default:false" json:"blocked"`
	// ContextText preserves the caller's `context` argument so a delegation
	// that waits in the queue can be started later exactly as it was
	// requested. Without it a queued row would lose the background its
	// caller supplied, and the sub-agent would start on a thinner task than
	// the one that was asked for.
	ContextText string `gorm:"type:text;not null;default:''" json:"-"`
	// MemoryMode is the resolved choice of what this sub-agent was told
	// about the rest of the conversation. Stored so a delegation that
	// waits in the queue is started with the memory its caller asked for,
	// not with whatever the default happens to be by then.
	MemoryMode string `gorm:"type:varchar(32);not null;default:''" json:"memory_mode,omitempty"`
	// IncidentID links this delegation to its tree's incident record, once
	// one exists. This column is the whole of what a separate agent_tasks
	// table would have been: status, timestamps, error and once-only
	// collection already live on this row, under test.
	IncidentID string `gorm:"type:varchar(64);not null;default:'';index" json:"incident_id,omitempty"`
	// Iteration is the checker round this delegation belongs to, stamped
	// from the incident when it was created. Round completion is a query
	// over it rather than a judgement.
	Iteration int `gorm:"not null;default:0" json:"iteration"`
	// IntakeReasked records that the mechanical gate already sent this
	// sub-agent one follow-up about malformed evidence. Bounded at one:
	// unbounded re-asking turns a turn budget into an argument about
	// formatting.
	IntakeReasked bool `gorm:"not null;default:false" json:"intake_reasked,omitempty"`
	// CollectNudged records that the leader has already been woken once
	// about this uncollected async result. Without it a sweep every
	// fifteen seconds becomes a wake every fifteen seconds.
	CollectNudged bool `gorm:"not null;default:false" json:"collect_nudged,omitempty"`
}

AgentDelegation is the audit + control record for exactly one wick_delegate call. It is written before the child spawns and updated as the child progresses, so an interrupted or crashed run still leaves a durable row behind.

RootID + Depth are what keep recursion bounded: the governor counts aggregate turns per RootID and refuses to spawn past MaxDepth. A tree without those two columns has no enforceable stopping condition.

func (AgentDelegation) TableName added in v0.36.0

func (AgentDelegation) TableName() string

type AgentEvidence added in v0.36.1

type AgentEvidence struct {
	ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	// IncidentID + Fingerprint is the dedup key. The same log line found
	// by two agents is one piece of evidence, and the second finder is
	// not an error.
	IncidentID  string `gorm:"type:varchar(64);not null;index:idx_evidence_dedup,unique" json:"incident_id"`
	Fingerprint string `gorm:"type:varchar(64);not null;index:idx_evidence_dedup,unique" json:"fingerprint"`
	// DelegationID and Role record WHO found it, so a contradiction can be
	// traced back to the agent that reported each side of it.
	DelegationID string    `gorm:"type:varchar(64);not null;index" json:"delegation_id"`
	Role         string    `gorm:"type:varchar(128);not null;default:''" json:"role"`
	Kind         string    `gorm:"type:varchar(32);not null" json:"kind"`
	Source       string    `gorm:"type:text;not null;default:''" json:"source"`
	Excerpt      string    `gorm:"type:text;not null;default:''" json:"excerpt"`
	CreatedAt    time.Time `json:"created_at"`
}

AgentEvidence is one verifiable excerpt an investigator quoted.

Deduplicated per incident by fingerprint, and by a DATABASE CONSTRAINT rather than a read-then-write: two investigators finishing at the same moment would both pass a "does this exist?" check and both insert.

func (AgentEvidence) TableName added in v0.36.1

func (AgentEvidence) TableName() string

type AgentIncident added in v0.36.1

type AgentIncident struct {
	ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	// RootID ties the incident to a delegation tree. Unique: one
	// investigation per conversation, and the index is what makes
	// concurrent lazy creation safe.
	RootID      string `gorm:"type:varchar(64);not null;uniqueIndex" json:"root_id"`
	ProjectID   string `gorm:"type:varchar(64);not null;default:'';index" json:"project_id"`
	TriggeredBy string `gorm:"type:varchar(128);not null;default:'';index" json:"triggered_by"`

	Status string `gorm:"type:varchar(32);not null;index" json:"status"`
	// Iteration counts completed checker rounds.
	Iteration int    `gorm:"not null;default:0" json:"iteration"`
	Title     string `gorm:"type:text;not null;default:''" json:"title"`
	UserIssue string `gorm:"type:text;not null;default:''" json:"user_issue"`
	Summary   string `gorm:"type:text;not null;default:''" json:"summary"`
	// StopReason records WHY an investigation stopped. A loop that ends
	// without saying why is indistinguishable from one still running.
	StopReason string `gorm:"type:text;not null;default:''" json:"stop_reason"`

	// Embedded JSON collections: small, always read whole, never queried
	// by element. Evidence is NOT one of these — it is appended
	// concurrently by many delegations and deduplicated across them,
	// which is exactly what makes it a table instead.
	Hypotheses      string `gorm:"type:text;not null;default:'[]'" json:"hypotheses"`
	MissingEvidence string `gorm:"type:text;not null;default:'[]'" json:"missing_evidence"`
	NextActions     string `gorm:"type:text;not null;default:'[]'" json:"next_actions"`
	ClientContext   string `gorm:"type:text;not null;default:'{}'" json:"client_context"`

	// EvidenceAtRoundStart snapshots the evidence count when the current
	// round began, so "did this round establish anything new?" is a
	// subtraction rather than a judgement call.
	EvidenceAtRoundStart int `gorm:"not null;default:0" json:"evidence_at_round_start"`
	// DryRounds counts consecutive completed rounds that added no
	// evidence. Two in a row stops the loop.
	DryRounds int `gorm:"not null;default:0" json:"dry_rounds"`

	FinalSummary string    `gorm:"type:text;not null;default:''" json:"final_summary"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

AgentIncident is what one delegation tree has established so far.

Created LAZILY: a tree that never records evidence and never touches the incident op leaves no row. Auto-creating at spawn would put an incident behind every delegation tree in the product, most of which are code reviews; requiring an explicit "open" would put a step in front of a model that will sometimes skip it, leaving every later write to handle "no incident".

func (AgentIncident) TableName added in v0.36.1

func (AgentIncident) TableName() string

type AgentMessage added in v0.36.1

type AgentMessage struct {
	ID         string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	RootID     string `gorm:"type:varchar(64);not null;index:idx_agent_messages_inbox,priority:1" json:"root_id"`
	FromHandle string `gorm:"type:varchar(64);not null" json:"from_handle"`
	ToHandle   string `gorm:"type:varchar(64);not null;index:idx_agent_messages_inbox,priority:2" json:"to_handle"`
	Body       string `gorm:"type:text;not null" json:"body"`
	Kind       string `gorm:"type:varchar(16);not null" json:"kind"`
	// ReplyTo points a reply at the ask it answers.
	ReplyTo string `gorm:"type:varchar(64);index" json:"reply_to,omitempty"`
	// AutoReply marks a reply wick synthesised from the recipient's final
	// turn because it never called reply explicitly. Surfaced so a reader
	// can tell a deliberate answer from a salvaged one — the salvaged kind
	// often does not actually answer the question.
	AutoReply bool   `gorm:"not null;default:false" json:"auto_reply,omitempty"`
	Status    string `gorm:"type:varchar(16);not null;index:idx_agent_messages_inbox,priority:3" json:"status"`
	// Hop records the tree's hop counter when this message was sent. Kept
	// for audit; the live counter lives on the root delegation row.
	Hop         int        `gorm:"not null;default:0" json:"hop"`
	CreatedAt   time.Time  `json:"created_at"`
	DeliveredAt *time.Time `json:"delivered_at,omitempty"`
}

AgentMessage is one message between two agents inside a delegation tree.

RootID scopes addressing: a handle only means something inside its own tree, so a message can never reach an agent in someone else's conversation — and a leader cannot re-prompt a session it does not own.

FromHandle is written by the SERVER from the calling session, never taken from model input. A model that could name its own sender could claim to be the leader and inherit the authority that comes with it.

func (AgentMessage) TableName added in v0.36.1

func (AgentMessage) TableName() string

type AgentProfile added in v0.36.0

type AgentProfile struct {
	ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	// ProjectID scopes this role. Empty = global: reachable from every
	// project. Non-empty = owned by that project and invisible elsewhere.
	// A project row whose Key matches a global row SHADOWS it for sessions
	// in that project; the global row is untouched for everyone else.
	// See delegation.ResolveScoped.
	ProjectID string `gorm:"type:varchar(64);not null;default:'';index:idx_profile_scope,unique,priority:1" json:"project_id"`
	// Key is the stable handle the LLM passes to wick_delegate. Unique
	// within a scope, lowercase-kebab by convention ("code-reviewer").
	Key         string `gorm:"type:varchar(128);not null;index:idx_profile_scope,unique,priority:2" json:"key"`
	Name        string `gorm:"type:varchar(256);not null;default:''" json:"name"`
	Description string `gorm:"type:text;not null;default:''" json:"description"`
	Icon        string `gorm:"type:varchar(32);not null;default:''" json:"icon"`

	// Provider is the agent runtime ("claude" | "codex" | "gemini").
	// Resolved through the existing pool factory — a sub-agent is a
	// normal provider spawn, not a new runtime.
	Provider string `gorm:"type:varchar(64);not null" json:"provider"`
	// Model is a provider-specific model id. Empty = provider default.
	Model string `gorm:"type:varchar(128);not null;default:''" json:"model"`
	// SystemPrompt is injected into the child spawn via
	// provider.SpawnOptions.Preset (--append-system-prompt or the
	// per-provider equivalent).
	SystemPrompt string `gorm:"type:text;not null;default:''" json:"system_prompt"`

	// AllowedTagIDs is an OPTIONAL narrowing filter, stored as a JSON
	// array of tag ids. Empty = inherit the triggering user's tags in
	// full. Non-empty = intersect with them. See delegation.EffectiveTags.
	AllowedTagIDs string `gorm:"type:text;not null;default:'[]'" json:"allowed_tag_ids"`
	// AllowedNativeTools is a JSON array of provider-native tool names.
	//
	// NOT WIRED. The value is stored and returned by the API, but nothing
	// forwards it to the spawn as --allowedTools, so setting it has no
	// effect on what a sub-agent can call. Do not present it as a control
	// until a spawn path reads it.
	AllowedNativeTools string `gorm:"type:text;not null;default:'[]'" json:"allowed_native_tools"`
	// StrictMCP is intended to drop the host's own MCP servers
	// (~/.claude.json) from the child spawn.
	//
	// NOT WIRED, and it is NOT a security boundary today. Whether a spawn
	// passes --strict-mcp-config is decided globally by the
	// WICK_STRICT_MCP environment variable (see
	// internal/agents/provider/claude/spawn.go), identically for leaders
	// and sub-agents. This field is read by nobody, so a sub-agent
	// inherits whatever MCP servers the host CLI has configured
	// regardless of what a profile says.
	StrictMCP bool `gorm:"not null;default:true" json:"strict_mcp"`

	DefaultMaxTurns int `gorm:"not null;default:12" json:"default_max_turns"`
	// DefaultMode is "async" unless a role opts into "sync". Empty is
	// read as async too (see delegation.NormalizeMode): only an explicit
	// "sync" makes a caller block, so a role created without an opinion
	// runs in the background.
	DefaultMode string `gorm:"type:varchar(16);not null;default:'async'" json:"default_mode"`
	// DefaultWorkspace is "shared" today. "worktree" lands in Phase 3.
	DefaultWorkspace string `gorm:"type:varchar(16);not null;default:'shared'" json:"default_workspace"`
	// DefaultMemoryMode decides what this role is told about the rest of
	// the conversation beyond its own task: no_history, state_summary
	// (the default), relevant_chunks, or full_history. Empty means the
	// system default rather than "nothing".
	DefaultMemoryMode string `gorm:"type:varchar(32);not null;default:''" json:"default_memory_mode"`
	// CanDelegate marks a profile as eligible to be a leader, i.e. to
	// call wick_delegate itself (nested delegation). Forced false for
	// providers without MCP tool-use.
	CanDelegate bool `gorm:"not null;default:false" json:"can_delegate"`
	// AllowTakeOver lets a human send messages straight into a running
	// sub-agent of this role. Off by default: steering a sub-agent means
	// the result is no longer purely that role's own work, so it is opted
	// into per role rather than granted everywhere. Delegations that were
	// steered are flagged UserSteered and the leader is told.
	AllowTakeOver bool `gorm:"not null;default:false" json:"allow_take_over"`
	// DefaultMaxTokens caps token spend for one delegation of this role.
	// 0 = uncapped by the profile; the per-tree budget still applies.
	DefaultMaxTokens int `gorm:"not null;default:0" json:"default_max_tokens"`

	// Locked freezes this role's behaviour. While true, no edit and no
	// delete is accepted from ANY surface — web UI or MCP. Unlocking is a
	// UI-only action, so an agent can never widen its own definition.
	// Distinct from Disabled: a disabled role is switched off, a locked
	// role is switched in stone.
	Locked bool `gorm:"not null;default:false" json:"locked"`

	Disabled  bool      `gorm:"not null;default:false" json:"disabled"`
	CreatedBy string    `gorm:"type:varchar(128);not null;default:''" json:"created_by"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AgentProfile is a reusable sub-agent role: the identity half of the multi-agent feature. One row = one role ("researcher", "code-reviewer") that any leader can delegate to, across every conversation.

A profile is NOT a grant. Access always attaches to the human who triggered the root delegation; AllowedTagIDs can only NARROW that set, never widen it (see delegation.EffectiveTags). Storing a broad tag list here does not hand the sub-agent anything the triggering user lacks.

Generalisation of Presets: a preset is only a system prompt, while a profile also pins provider + model + tool ACL + turn budget.

func (AgentProfile) TableName added in v0.36.0

func (AgentProfile) TableName() string

type AgentSquad added in v0.36.0

type AgentSquad struct {
	ID          string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	Key         string `gorm:"type:varchar(128);uniqueIndex;not null" json:"key"`
	Name        string `gorm:"type:varchar(256);not null;default:''" json:"name"`
	Description string `gorm:"type:text;not null;default:''" json:"description"`
	// LeaderProfileKey is the role that orchestrates. Empty = any profile
	// with can_delegate may lead this squad.
	LeaderProfileKey string `gorm:"type:varchar(128);not null;default:''" json:"leader_profile_key"`
	// MemberProfileKeys is a JSON array of profile keys the leader may
	// delegate to while this squad is active.
	MemberProfileKeys string `gorm:"type:text;not null;default:'[]'" json:"member_profile_keys"`
	Disabled          bool   `gorm:"not null;default:false" json:"disabled"`
	CreatedBy         string `gorm:"type:varchar(128);not null;default:''" json:"created_by"`
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

AgentSquad is a named, fixed team: one leader profile plus a roster of member profiles it may delegate to.

Without a squad, a leader can reach every profile its caller's tags allow, which is fine for ad-hoc work but vague for a recurring job ("the release crew"). A squad narrows that to a deliberate line-up, so the same task always runs against the same roles.

A squad only ever NARROWS. Membership never grants access the calling human lacks — the tag intersection still applies to every member.

func (AgentSquad) TableName added in v0.36.0

func (AgentSquad) TableName() string

type AgentTask added in v0.36.0

type AgentTask struct {
	ID      string `gorm:"primaryKey;type:varchar(64)" json:"id"`
	BoardID string `gorm:"type:varchar(64);not null;index" json:"board_id"`
	Title   string `gorm:"type:varchar(512);not null;default:''" json:"title"`
	Body    string `gorm:"type:text;not null;default:''" json:"body"`
	// Stage drives the state machine; ColumnID is only where it renders.
	Stage    string `gorm:"type:varchar(32);not null;index" json:"stage"`
	ColumnID string `gorm:"type:varchar(64);not null;default:''" json:"column_id"`
	Position int    `gorm:"not null;default:0" json:"position"`
	// ProfileKey is the role that should execute this task.
	ProfileKey string `gorm:"type:varchar(128);not null;default:''" json:"profile_key"`
	Priority   int    `gorm:"not null;default:0" json:"priority"`

	ClaimState string     `gorm:"type:varchar(32);not null;default:'unclaimed';index" json:"claim_state"`
	ClaimedBy  string     `gorm:"type:varchar(128);not null;default:''" json:"claimed_by"`
	ClaimedAt  *time.Time `json:"claimed_at,omitempty"`
	// DelegationID links a started task to the delegation executing it.
	DelegationID string `gorm:"type:varchar(64);not null;default:''" json:"delegation_id"`

	Result string `gorm:"type:text;not null;default:''" json:"result"`
	// Evidence is the proof of completion a gated board demands (test
	// output, a report, a link). Checked against GateMode on the move
	// into a terminal stage.
	Evidence string `gorm:"type:text;not null;default:''" json:"evidence"`
	ErrorMsg string `gorm:"type:text;not null;default:''" json:"error_msg"`

	CreatedBy string `gorm:"type:varchar(128);not null;default:''" json:"created_by"`
	CreatedAt time.Time
	UpdatedAt time.Time
	EndedAt   *time.Time `json:"ended_at,omitempty"`
}

AgentTask is one unit of work on a board.

Claiming is what makes concurrent workers safe: a worker takes a task with a guarded update (unclaimed → claimed for exactly this worker), so two agents polling simultaneously cannot both win the same task. The guard lives in the SQL WHERE clause, not in an application-level check that a future caller could forget.

func (AgentTask) TableName added in v0.36.0

func (AgentTask) TableName() string

type Bookmark

type Bookmark struct {
	UserID    string `gorm:"primaryKey;type:varchar(36)"`
	ToolPath  string `gorm:"primaryKey;type:varchar(255)"`
	CreatedAt time.Time
}

Bookmark marks a tool as a favorite for a specific user. Bookmarked tools appear in a dedicated "Bookmarks" group on the home page in addition to any group/category they already belong to.

type Config

type Config = pkgentity.Config

Config is re-exported from pkg/entity so existing internal callers keep working. Module authors should import pkg/entity directly.

type Connector added in v0.4.0

type Connector struct {
	ID    string `gorm:"type:varchar(36);primaryKey"`
	Key   string `gorm:"type:varchar(100);index;not null"`
	Label string `gorm:"type:varchar(255);not null"`
	// Description is per-instance free text an admin writes to guide the AI:
	// when to use this specific instance, team notes, constraints. It is
	// appended to the module's built-in Meta.Description in the MCP surface
	// (connector_list / connector_get) so the agent reads both. Empty by
	// default; purely additive.
	Description string `gorm:"type:text"`
	Disabled    bool   `gorm:"default:false"`
	// RateLimitRPM caps how many times this connector instance may be
	// called per minute across all users. 0 means unlimited. Enforced
	// in-process via a sliding-window counter — not distributed.
	RateLimitRPM int    `gorm:"default:0"`
	CreatedBy    string `gorm:"type:varchar(36)"`
	// AllowOthersConfigure controls whether non-admin users who have tag
	// access to this instance can edit its credentials and settings.
	// Default false — only admins can configure. When true, any user
	// whose tags grant access to this row can also edit it.
	AllowOthersConfigure bool `gorm:"default:false"`
	// AllowOthersConnectSSO controls whether non-admin users who have tag
	// access to this instance can connect their own OAuth account.
	// Only meaningful when the connector's OAuthMeta is non-nil.
	// Default false — only admins can initiate the OAuth flow.
	AllowOthersConnectSSO bool `gorm:"default:false"`
	// EnableSSO controls whether this instance participates in the OAuth
	// flow at all. When false (default) the instance uses manually-entered
	// credentials (bot token, API key, service account). When true the
	// "Connect Account" button is shown and the OAuth flow is active.
	// Only meaningful when the connector's OAuthMeta is non-nil.
	EnableSSO bool `gorm:"default:false"`
	// MultiAccount controls whether each OAuth connect creates a new
	// instance row (true) or replaces the token on this row (false).
	// Only meaningful when EnableSSO=true.
	// false (default) — one row, one identity; reconnect replaces token.
	// true — each user that connects gets their own auto-created row
	// labelled "{Connector} – @{displayName}".
	MultiAccount bool `gorm:"default:false"`
	// AllowSessionConfig opts THIS instance into per-session cloning (the
	// Config tab + wick_session_workspace). Default false — admins flip it
	// per instance. Only meaningful when the connector's module declares
	// AllowSessionConfig=true (the capability); the UI only shows the
	// toggle for capable connectors. Effective gate: module-capable AND
	// this instance flag on.
	AllowSessionConfig bool `gorm:"default:false"`
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

Connector is one row per running connector — the runtime pairing of a code-registered connector definition (identified by Key) with a credential set, label, and creator.

Connector definitions live in code (see pkg/connector and the internal/connectors registry); this entity is what the admin UI reads, writes, and duplicates. MCP exposes one tool per Connector row per enabled operation.

Key references the code definition's slug (e.g. "loki", "github") — it is NOT unique on this table. Multiple Connector rows share the same Key when admins duplicate a definition into multiple instances (Loki Prod, Loki Staging, Loki Dev). Uniqueness lives on ID; the admin UI distinguishes siblings by Label.

Code-registered Modules survive deletion: when bootstrap runs and finds zero rows for a registered Key, it auto-creates a fresh row (empty Configs, Label = Meta.Name). Admins who delete every row for a connector therefore get an empty-but-working row back on restart; duplicates and edits to existing rows are untouched.

Per-field credential / endpoint values live on the central configs table (owner = "connector:{id}"), one row per field declared on the connector's Creds struct. Wick reflects the typed Creds into rows at boot via entity.StructToConfigs and reconciles them on every connector instance create. Reading credentials goes through connectors.Service.LoadConfigs; the configs.Service cache makes the per-field shape as cheap as a JSON unmarshal but keeps each value query-able and individually editable.

Disabled hides the row from MCP tools/list and the admin UI list view (admins can re-enable from the manager). The tag-filter system (the existing ToolTag table, addressed by path "/connectors/{id}", joined against UserTag) gates which authenticated users see this row at all — Disabled is the orthogonal "off switch" for the whole row.

Tag association reuses ToolTag (with ToolPath = "/connectors/{id}") rather than introducing a connector-specific link table; jobs do the same with "/jobs/{path}". A future rename of ToolTag/SetToolTags into a generic entity-tag API is tracked separately.

func (*Connector) BeforeCreate added in v0.4.0

func (c *Connector) BeforeCreate(tx *gorm.DB) error

type ConnectorAccount added in v0.16.0

type ConnectorAccount struct {
	ID             string `gorm:"type:varchar(36);primaryKey"`
	ConnectorID    string `gorm:"type:varchar(36);not null;index"`
	WickUserID     string `gorm:"type:varchar(36);index"`
	ExternalUserID string `gorm:"type:varchar(255);index"`
	DisplayName    string `gorm:"type:varchar(255);not null"`
	AccessToken    string `gorm:"type:text;not null"`
	// DisabledOps is a JSON array of operation keys disabled for this
	// account. Empty = all ops allowed. Example: ["send_message","delete_message"]
	DisabledOps string `gorm:"type:text;default:''"`
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

ConnectorAccount stores one OAuth-connected user account per connector instance. Many accounts can live under one instance (when MultiAccount=true); at most one when MultiAccount=false (upsert by ConnectorID).

AccessToken is stored as-is — the configs encryption layer handles masking in the admin UI via the connector's Configs.UserToken secret field. The token here is the source of truth for execution; the connector reads it via c.Cfg("user_token") which resolves from this table at call time.

ConnectorAccount stores one OAuth-connected user account per connector instance. WickUserID is the wick platform user who initiated the OAuth flow. ExternalUserID is the provider-side user ID returned by GetUserIdentity (e.g. Slack U01ABCDEF, Google sub claim) — used for token lookup by the channel layer when routing inbound messages to sessions.

func (*ConnectorAccount) BeforeCreate added in v0.16.0

func (a *ConnectorAccount) BeforeCreate(tx *gorm.DB) error

type ConnectorOperation added in v0.4.0

type ConnectorOperation struct {
	ConnectorID  string `gorm:"primaryKey;type:varchar(36)"`
	OperationKey string `gorm:"primaryKey;type:varchar(100)"`
	Enabled      bool   `gorm:"default:true"`
	// AdminOnly restricts this operation to admin users only. Non-admin
	// MCP callers receive a 403-equivalent error before Execute runs.
	// Default: false (all authenticated users may call the operation).
	AdminOnly bool `gorm:"default:false"`
	// SystemDisabled is set by the health-check mechanism when the
	// configured credential lacks the upstream permissions an operation
	// needs (e.g. missing OAuth scope). It is orthogonal to Enabled —
	// effective availability is `Enabled AND NOT SystemDisabled`. The
	// admin UI locks the manual Enable/Disable toggle while this is
	// true; the only way to clear it is to fix the upstream permission
	// and re-run the health check. Default: false.
	SystemDisabled bool `gorm:"default:false"`
	// SystemDisabledReason is the human-readable explanation surfaced
	// alongside the lock — e.g. "needs scope: chat:write". Empty when
	// SystemDisabled is false.
	SystemDisabledReason string `gorm:"type:text"`
	UpdatedAt            time.Time
}

ConnectorOperation stores the enable state of one operation on one connector row. Operations are declared in code (Module.Operations); this table records whether the admin opted them in (or out) for a specific connector row.

Default rule applied when a connector row is created:

  • Operation.Destructive == false → Enabled = true
  • Operation.Destructive == true → Enabled = false (admin opt-in)

Rows for ops the admin has not touched yet may be missing; readers fall back to the default rule above when no row exists. Toggling in the UI inserts or updates a row.

type ConnectorRun added in v0.4.0

type ConnectorRun struct {
	// String columns are text, not varchar(n): in Postgres text and varchar
	// share storage/perf, so a length cap is pure constraint — and the caps
	// here kept biting (e.g. ConnectorID held "sw_"+uuid = 39 > varchar(36)).
	ID           string `gorm:"type:text;primaryKey"`
	ConnectorID  string `gorm:"type:text;not null;index:idx_run_connector_started,priority:1"`
	OperationKey string `gorm:"type:text;not null"`
	UserID       string `gorm:"type:text;index:idx_run_user_started,priority:1"`
	// SessionID is the agent/live-session this op ran under (empty for direct
	// PAT calls with no session). Persisted so a session_close can find and abort
	// every in-flight run bound to the session, and so a stale-run reaper /
	// audit can attribute a run to its session. Indexed for the by-session lookup.
	SessionID    string             `gorm:"type:text;index:idx_run_session"`
	Source       ConnectorRunSource `gorm:"type:text;not null"`
	RequestJSON  string             `gorm:"type:text"`
	ResponseJSON string             `gorm:"type:text"`
	Status       ConnectorRunStatus `gorm:"type:text;not null;index:idx_run_status_started,priority:1"`
	ErrorMsg     string             `gorm:"type:text"`
	LatencyMs    int
	HTTPStatus   int
	IPAddress    string    `gorm:"type:text;index:idx_run_ip_started,priority:1"`
	UserAgent    string    `gorm:"type:text"`
	ParentRunID  *string   `gorm:"type:text;index"`
	StartedAt    time.Time `` /* 218-byte string literal not displayed */
	EndedAt      *time.Time
	CreatedAt    time.Time
}

ConnectorRun records one execution of one operation on one connector row. Written once per MCP tools/call, panel-test click, or retry, so admins can audit traffic, debug failures, and replay buggy calls.

RequestJSON stores the input arguments the caller (LLM or admin) passed in. Credentials are NOT in this column — they live on the Connector row itself, joined at exec time. Replaying a run rebuilds the call from RequestJSON + the current Connector.Configs, so a retry honors any cred edits the admin has made since the original.

ResponseJSON is the JSON-marshaled return value of ExecuteFunc. Large responses may be truncated by the writer (defense in depth); readers should treat the value as opaque.

IPAddress and UserAgent capture the calling client's network identity at the time of the run. These are recorded for security observability — feeding a future allowlist/blocklist surface — and for incident triage. They are best-effort: behind a proxy the IP is whatever X-Forwarded-For policy the deploy resolves to, and a PAT-using script may not send a recognizable UA at all.

ParentRunID is non-nil only when Source == ConnectorRunSourceRetry, pointing to the run this one re-played. There is no FK constraint — the parent may be deleted by retention, leaving the lineage dangling (the UI tolerates the gap).

Retention: rows older than the configured retention window are purged by a scheduled cleanup job (default 7 days). The single-column index on StartedAt keeps the purge query cheap.

Index strategy (composite, listed by query they serve):

  • (connector_id, started_at DESC) → "recent runs for this connector"
  • (user_id, started_at DESC) → "user activity timeline"
  • (status, started_at DESC) → "recent errors" filter
  • (ip_address, started_at DESC) → "activity from this IP" (future allow/block UX)
  • started_at → retention purge
  • parent_run_id → retry lineage trace

func (*ConnectorRun) BeforeCreate added in v0.4.0

func (r *ConnectorRun) BeforeCreate(tx *gorm.DB) error

type ConnectorRunSource added in v0.4.0

type ConnectorRunSource string

ConnectorRunSource describes how a ConnectorRun was triggered.

const (
	// ConnectorRunSourceMCP marks runs initiated by an LLM client through
	// the /mcp endpoint.
	ConnectorRunSourceMCP ConnectorRunSource = "mcp"
	// ConnectorRunSourceTest marks runs initiated from the panel-test
	// view in the admin UI (Postman-style manual exec).
	ConnectorRunSourceTest ConnectorRunSource = "test"
	// ConnectorRunSourceRetry marks runs that replay the request payload
	// of a previous run, identified by ConnectorRun.ParentRunID.
	ConnectorRunSourceRetry ConnectorRunSource = "retry"
)

type ConnectorRunStatus added in v0.4.0

type ConnectorRunStatus string

ConnectorRunStatus describes the outcome of a ConnectorRun.

const (
	ConnectorRunStatusRunning ConnectorRunStatus = "running"
	ConnectorRunStatusSuccess ConnectorRunStatus = "success"
	ConnectorRunStatusError   ConnectorRunStatus = "error"
	// ConnectorRunStatusCancelled is a run aborted before it finished — either
	// its session was closed (session_close cancels every in-flight op bound to
	// that session) or a stale-run reaper reclaimed a row whose op never
	// returned. Distinct from "error" so the UI can show "aborted" rather than
	// implying the operation itself failed.
	ConnectorRunStatusCancelled ConnectorRunStatus = "cancelled"
)

type ConnectorState added in v0.27.2

type ConnectorState struct {
	Key       string `gorm:"primaryKey"`
	Enabled   bool   `gorm:"default:true"`
	UpdatedAt time.Time
}

ConnectorState is the DB overlay for a connector TYPE's enable/disable switch — distinct from the per-instance entity.Connector.Disabled row flag. It applies to ANY connector key, built-in or plugin: a missing row means enabled (default-on), Enabled=false hides the whole connector type from the LLM surface (every instance + every operation) while the manager UI still shows it with a "Disabled" badge so it can be turned back on.

Note the difference from entity.PluginState (internal/entity/plugin.go), which gates plugin registration/spawn at the reloader level for plugins only. ConnectorState is a type-level visibility switch for all connectors.

type CustomConnector added in v0.17.0

type CustomConnector struct {
	ID          string `gorm:"type:varchar(36);primaryKey"`
	Key         string `gorm:"type:varchar(100);uniqueIndex;not null"`
	Name        string `gorm:"type:varchar(255);not null"`
	Description string `gorm:"type:text"`
	// Icon is an emoji, an inline <svg>, or a data:image/...;base64
	// payload (validated to 32KB) — rendered as text or <img> by the UI.
	Icon       string                `gorm:"type:text"`
	Source     CustomConnectorSource `gorm:"type:varchar(16);not null"`
	SourceMeta string                `gorm:"type:text"`
	Configs    string                `gorm:"type:text;not null;default:'[]'"`
	Ops        string                `gorm:"type:text;not null;default:'[]'"`
	CreatedBy  string                `gorm:"type:varchar(36)"`
	// SingleInstance locks the def to one row (Meta.Fixed). Default off:
	// custom connectors behave like built-ins — admins add/duplicate
	// instance rows, each with its own credentials.
	SingleInstance bool `gorm:"default:false"`
	// AllowSessionConfig is the capability flag mirrored onto
	// Module.AllowSessionConfig: when true this def's configs (base_url,
	// keys, …) may be overridden per agent session. Default off; an admin
	// still has to enable the per-instance toggle before any override is
	// accepted. Only meaningful for curl/manual API defs — leave off for
	// oauth/sso-backed MCP defs whose config is a user token.
	AllowSessionConfig bool `gorm:"default:false"`
	Disabled           bool `gorm:"default:false"`
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

CustomConnector is one admin-built custom connector definition — the whole definition in a single row. Built-in connectors live in Go code under internal/connectors/* and register through RegisterBuiltins; custom ones live here and are replayed into the same registry at boot (and on admin save) by internal/connectors/custom. From the MCP surface the two are indistinguishable — same tool_id shape, same audit trail, same encrypted-fields layer.

Key shares the namespace with built-in connector Meta.Keys; the save path validates uniqueness across both so a custom def can never shadow a built-in module. Instance rows, per-instance config values, per-op enable state, and run history all ride the existing tables (connectors, configs, connector_operations, connector_runs) exactly like a built-in.

Configs holds the per-instance field schema as a JSON array of custom.DefField ({key, label, widget, secret, required, default, desc}) — mirroring what entity.StructToConfigs produces so a connector.Module can be assembled without Go reflection. Ops holds the operations as a JSON array of custom.DefOp; array order is display order. SourceMeta keeps provenance (chosen category tag, MCP server id). Raw AI-parser pastes are never persisted.

func (*CustomConnector) BeforeCreate added in v0.17.0

func (d *CustomConnector) BeforeCreate(tx *gorm.DB) error

func (CustomConnector) TableName added in v0.17.0

func (CustomConnector) TableName() string

type CustomConnectorMCPServer added in v0.17.0

type CustomConnectorMCPServer struct {
	ID          string `gorm:"type:varchar(36);primaryKey"`
	Label       string `gorm:"type:varchar(255);not null"`
	Transport   string `gorm:"type:varchar(16);not null;default:'http'"`
	URL         string `gorm:"type:text;not null"`
	AuthScheme  string `gorm:"type:varchar(20);not null;default:'none'"`
	AuthSecret  string `gorm:"type:text"`
	AuthHeaders string `gorm:"type:text"`
	AuthExtra   string `gorm:"type:text"`
	Headers     string `gorm:"type:text"`
	// ExcludedTools is a JSON array of tool names the connector must NOT
	// expose. The exclusion model is opt-out: everything the server
	// lists is an operation unless its name is in here.
	ExcludedTools string `gorm:"type:text;not null;default:'[]'"`
	// ServerInfo is the JSON {name, version} the server reported on the
	// last successful initialize — admin-facing only (edit form,
	// def_get); deliberately never exposed to the LLM via wick_list.
	ServerInfo string `gorm:"type:text"`
	LastTestAt *time.Time
	LastTestOK bool `gorm:"default:false"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

CustomConnectorMCPServer is one MCP server registered as a custom connector source (and runtime proxy target). One server row = one connector definition: every tool the server lists is exposed as an operation automatically, minus the names in ExcludedTools — nothing per-tool is persisted, so tools added on the server side appear after a module rebuild (boot / reload / server save) without any wick-side change. Wick is a forwarder only: it stores the streamable-HTTP URL plus auth material and fires per-call JSON-RPC (initialize / tools/list / tools/call) over the shared HTTP client. No process spawn, no lifecycle — stdio transports are out of scope (the Transport column is reserved so a future value doesn't need a migration). Tool catalogs are never cached: every module build re-hits tools/list live.

AuthScheme picks how the outbound call authenticates:

  • "none" — Content-Type/Accept only.
  • "bearer" — AuthSecret (stored encrypted under the master key, decrypted per request) as Authorization: Bearer.
  • "custom_header" — AuthHeaders JSON ([{key, value, secret}]); secret values stored encrypted.
  • "sso" — no stored secret; wick mints a short-lived ED25519 JWT for the calling user per request (AuthExtra JSON: {audience, ttl_seconds}) and sends it as X-Wick-User.

Headers carries extra non-auth rows (routing, tenancy) appended on top of the scheme's headers for every call, same JSON shape as AuthHeaders.

LastTestAt/LastTestOK gate the save flow: a row may only be created after at least one successful initialize + tools/list round-trip, so half-broken servers never pollute the table.

func (*CustomConnectorMCPServer) BeforeCreate added in v0.17.0

func (m *CustomConnectorMCPServer) BeforeCreate(tx *gorm.DB) error

func (CustomConnectorMCPServer) TableName added in v0.17.0

func (CustomConnectorMCPServer) TableName() string

type CustomConnectorSource added in v0.17.0

type CustomConnectorSource string

CustomConnectorSource describes where a custom connector definition came from. Display-only — the generic executor behaves identically for all three; each op's mcp_source decides the proxy path.

const (
	CustomConnectorSourceCurl   CustomConnectorSource = "curl"
	CustomConnectorSourceMCP    CustomConnectorSource = "mcp"
	CustomConnectorSourceManual CustomConnectorSource = "manual"
)

type DataTable added in v0.13.1

type DataTable struct {
	Slug        string `gorm:"type:varchar(80);primaryKey"`
	Name        string `gorm:"type:varchar(255);not null"`
	Description string `gorm:"type:text"`
	Mode        string `gorm:"type:varchar(20);not null;default:'strict'"` // strict | lax
	SchemaJSON  string `gorm:"type:jsonb;not null;default:'{}'"`
	AccessJSON  string `gorm:"type:jsonb;default:'{}'"`
	NextRowID   int64  `gorm:"not null;default:0"`     // per-slug monotonic row id allocator
	CreatedBy   string `gorm:"type:varchar(36);index"` // owner user id (Schema.UserID); access via owner:<slug> tag
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

DataTable is the schema metadata row, one per logical data table. SchemaJSON carries the full Schema body (columns with stable ids, next_col_id counter, access, retention) — see internal/agents/workflow/datatable for the typed shape.

All data rows live in DataTableRow keyed by Slug. There are no per-table physical tables — wick stays at two tables total (wick_data_tables + wick_data_table_rows) regardless of how many logical tables a user creates.

func (DataTable) TableName added in v0.13.1

func (DataTable) TableName() string

type DataTableRow added in v0.13.1

type DataTableRow struct {
	TableSlug string    `gorm:"type:varchar(80);primaryKey;not null;index:idx_dtr_slug_created,priority:1"`
	ID        int64     `gorm:"primaryKey;not null;autoIncrement:false"`
	Data      string    `gorm:"type:jsonb;not null;default:'{}'"`
	CreatedAt time.Time `gorm:"not null;index:idx_dtr_slug_created,priority:2,sort:desc"`
	UpdatedAt time.Time `gorm:"not null"`
}

DataTableRow is one row of one logical data table. Data is JSONB keyed by column id (the stable "cN" identifier from the schema), so renaming a column updates only the schema row, never the data.

Primary key is composite (TableSlug, ID). ID is allocated by the app from DataTable.NextRowID under SELECT FOR UPDATE so concurrent inserts never collide.

func (DataTableRow) TableName added in v0.13.1

func (DataTableRow) TableName() string

type HealthComponent

type HealthComponent struct {
	Database HealthState `json:"database"`
}

type HealthState

type HealthState string
const (
	HealthStateOK   HealthState = "ok"
	HealthStateFail HealthState = "fail"
)

type Job

type Job struct {
	ID            string    `gorm:"type:varchar(36);primaryKey"`
	Key           string    `gorm:"type:varchar(100);uniqueIndex;not null"`
	Name          string    `gorm:"type:varchar(255);not null"`
	Description   string    `gorm:"type:text"`
	Icon          string    `gorm:"type:varchar(10)"`
	Schedule      string    `gorm:"type:varchar(100)"` // cron expression
	Enabled       bool      `gorm:"default:false"`
	MaxRuns       int       `gorm:"default:0"`  // 0 = unlimited, admin-managed
	MaxTimeoutMin int       `gorm:"default:30"` // auto-cancel after N minutes; 0 = no timeout
	TotalRuns     int       `gorm:"default:0"`
	LastStatus    JobStatus `gorm:"type:varchar(20);default:'idle'"`
	LastRunAt     *time.Time
	CreatedBy     string `gorm:"type:varchar(36)"`
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

Job is a background job definition whose schedule and lifecycle are managed via the DB. Code-defined jobs bootstrap a row on startup; admins can tweak the cron expression, enable/disable, and cap the run count.

func (*Job) BeforeCreate

func (j *Job) BeforeCreate(tx *gorm.DB) error

type JobRun

type JobRun struct {
	ID          string     `gorm:"type:varchar(36);primaryKey"`
	JobID       string     `gorm:"type:varchar(36);index;not null"`
	Status      RunStatus  `gorm:"type:varchar(20);not null"`
	Result      string     `gorm:"type:text"`
	TriggeredBy RunTrigger `gorm:"type:varchar(20);not null"`
	UserID      string     `gorm:"type:varchar(36)"`
	StartedAt   time.Time
	EndedAt     *time.Time
	CreatedAt   time.Time
}

JobRun stores the result of a single execution of a Job.

func (*JobRun) BeforeCreate

func (r *JobRun) BeforeCreate(tx *gorm.DB) error

type JobStatus

type JobStatus string

JobStatus represents the current state of a job.

const (
	JobStatusIdle    JobStatus = "idle"
	JobStatusRunning JobStatus = "running"
)

type OAuthAuthorizationCode added in v0.4.0

type OAuthAuthorizationCode struct {
	ID                  string `gorm:"type:varchar(36);primaryKey"`
	Code                string `gorm:"type:varchar(64);uniqueIndex;not null"`
	ClientID            string `gorm:"type:varchar(64);index;not null"`
	UserID              string `gorm:"type:varchar(36);not null"`
	RedirectURI         string `gorm:"type:varchar(512);not null"`
	Scope               string `gorm:"type:varchar(255)"`
	CodeChallenge       string `gorm:"type:varchar(128);not null"`
	CodeChallengeMethod string `gorm:"type:varchar(10);not null"`
	Used                bool   `gorm:"default:false"`
	ExpiresAt           time.Time
	CreatedAt           time.Time
}

OAuthAuthorizationCode is the short-lived PKCE authorization code minted at /oauth/authorize and consumed at /oauth/token.

Code is the opaque string sent in the redirect query string. It's indexed for the lookup at /token but not unique — we let the database flag duplicates if two requests collide (statistically near impossible with 32 random bytes).

CodeChallenge / Method come from the original /authorize request; /token verifies the client's code_verifier against them per RFC 7636.

Used flips to true on first /token redemption to prevent replay. We keep the row for audit instead of deleting; PurgeExpired sweeps later.

func (*OAuthAuthorizationCode) BeforeCreate added in v0.4.0

func (c *OAuthAuthorizationCode) BeforeCreate(tx *gorm.DB) error

func (OAuthAuthorizationCode) TableName added in v0.4.0

func (OAuthAuthorizationCode) TableName() string

type OAuthClient added in v0.4.0

type OAuthClient struct {
	ID           string `gorm:"type:varchar(36);primaryKey"`
	ClientID     string `gorm:"type:varchar(64);uniqueIndex;not null"`
	Name         string `gorm:"type:varchar(255)"`
	RedirectURIs string `gorm:"type:text;not null"` // JSON array
	CreatedBy    string `gorm:"type:varchar(36)"`
	CreatedAt    time.Time
}

OAuthClient is one Dynamic Client Registration record (RFC 7591).

MCP clients (Claude.ai web, Claude Desktop, Cursor) call POST /oauth/register without prior coordination, hand wick a name + redirect_uris, and receive back the ClientID. There is no client secret in this flow — every client is treated as a public client using PKCE per RFC 7636 (which the MCP authorization spec mandates).

RedirectURIs is a JSON-encoded array of allowed redirect URIs; /oauth/authorize verifies the requested URI is one of them. We store as JSON to keep gorm migrations simple — the list is small and read whole every time.

CreatedBy is non-nil only when the registration happened while a user was logged in (rare — DCR usually fires before any wick session exists). Useful for admin auditing.

func (*OAuthClient) BeforeCreate added in v0.4.0

func (c *OAuthClient) BeforeCreate(tx *gorm.DB) error

func (OAuthClient) TableName added in v0.4.0

func (OAuthClient) TableName() string

TableName pins the table name. GORM's default naming would lower- case + snake_case "OAuth" into "o_auth", giving "o_auth_clients". We override so raw SQL in oauth.Repo.ListGrantsByUser stays readable ("oauth_clients", not "o_auth_clients").

type OAuthToken added in v0.4.0

type OAuthToken struct {
	ID            string  `gorm:"type:varchar(36);primaryKey"`
	TokenHash     string  `gorm:"type:varchar(64);uniqueIndex;not null"`
	Kind          string  `gorm:"type:varchar(10);not null"` // access | refresh
	ClientID      string  `gorm:"type:varchar(64);index;not null"`
	UserID        string  `gorm:"type:varchar(36);index;not null"`
	Scope         string  `gorm:"type:varchar(255)"`
	ParentTokenID *string `gorm:"type:varchar(36);index"` // refresh chain ancestor
	ExpiresAt     time.Time
	RevokedAt     *time.Time `gorm:"index"`
	LastUsedAt    *time.Time
	CreatedAt     time.Time
}

OAuthToken is one issued access or refresh token. Stored opaque (32 hex chars), hashed at rest just like PersonalAccessToken — the plaintext only crosses the wire on the /token response.

Kind is "access" or "refresh". A code redemption mints both: the access has a short TTL (~1h), the refresh a long one (~30d) and is rotated on every use (RFC 6749 §6 + best-current-practice).

ParentTokenID chains refresh-token rotation: when a refresh redeems, the new refresh row carries the previous row's ID here. RevokedAt stamps a row when its child is minted, so reuse of an old refresh is detectable (and the whole chain should be revoked — a sign the token was leaked).

func (*OAuthToken) BeforeCreate added in v0.4.0

func (t *OAuthToken) BeforeCreate(tx *gorm.DB) error

func (OAuthToken) TableName added in v0.4.0

func (OAuthToken) TableName() string

type PersonalAccessToken added in v0.4.0

type PersonalAccessToken struct {
	ID         string `gorm:"type:varchar(36);primaryKey"`
	UserID     string `gorm:"type:varchar(36);not null;index"`
	Name       string `gorm:"type:varchar(120);not null"`
	TokenHash  string `gorm:"type:varchar(64);not null;uniqueIndex"`
	Last4      string `gorm:"type:varchar(8);not null"`
	CreatedAt  time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time `gorm:"index"`
}

PersonalAccessToken is a static bearer credential a user generates from /profile/mcp. The plaintext token is shown to the user exactly once at creation time; only the SHA-256 hash is persisted so it can be looked up on incoming MCP requests but never reconstructed.

Token wire format: "wick_pat_" + 32 hex chars. Last4 stores the last 4 characters of the random suffix so the UI can render a stable "wick_pat_****abcd" preview without keeping the secret around.

LastUsedAt is best-effort — written by the MCP middleware on successful auth. RevokedAt nil means active; non-nil hides the row from active-token queries while keeping the audit trail intact.

func (*PersonalAccessToken) BeforeCreate added in v0.4.0

func (t *PersonalAccessToken) BeforeCreate(tx *gorm.DB) error

func (*PersonalAccessToken) Masked added in v0.4.0

func (t *PersonalAccessToken) Masked() string

Masked returns the display form for list views: prefix + asterisks + last 4 characters. Never exposes the secret.

type PluginState added in v0.26.0

type PluginState struct {
	Key       string `gorm:"primaryKey"`
	Enabled   bool   `gorm:"default:true"`
	UpdatedAt time.Time
}

PluginState is the DB overlay for connector plugin enable/disable. A missing row means enabled (default-on); Enabled=false suppresses the plugin from registration and spawning. The on-disk scan stays the source of truth for which plugins exist.

type ProviderStorage added in v0.11.0

type ProviderStorage struct {
	ID            uint   `gorm:"primaryKey;autoIncrement"`
	ProviderType  string `gorm:"type:varchar(32);not null;uniqueIndex:idx_provider_path"`
	InstanceName  string `gorm:"type:varchar(128);not null;uniqueIndex:idx_provider_path"`
	RelPath       string `gorm:"type:varchar(1024);not null;uniqueIndex:idx_provider_path"` // absolute filesystem path
	ParentID      uint   `gorm:"default:0;index"`                                           // 0 = root (RootParentID)
	Name          string `gorm:"type:varchar(512)"`                                         // basename only
	IsDir         bool   `gorm:"default:false"`
	Content       []byte
	Size          int       `gorm:"default:0"`                 // byte length of Content; lets listings skip loading the blob
	ContentHash   string    `gorm:"type:varchar(64);not null"` // SHA-256 hex; "" for dirs
	SyncedAt      time.Time `gorm:"not null"`
	RetentionDays int       `gorm:"not null;default:0"` // 0 = never purge
}

ProviderStorage holds one synced file or folder entry for a provider instance. Mode "folder" produces multiple rows (one per file); mode "single" produces exactly one. content_hash gates writes — only changed files are re-upserted.

Adjacency-list layout: each row knows its parent via ParentID (0 = root). Folder rows have IsDir=true, Content=nil, ContentHash="".

func (ProviderStorage) TableName added in v0.11.0

func (ProviderStorage) TableName() string

type ProviderStorageSource added in v0.11.0

type ProviderStorageSource struct {
	ID           uint   `gorm:"primaryKey;autoIncrement"`
	ProviderType string `gorm:"type:varchar(32);not null;index:idx_source_provider"`
	InstanceName string `gorm:"type:varchar(128);not null;index:idx_source_provider"`
	Label        string `gorm:"type:varchar(128);not null"` // e.g. "claude workspace", "credentials"
	SyncPath     string `gorm:"type:varchar(1024);not null"`
	// Mode is one of:
	//   "folder"  — include the folder tree at SyncPath
	//   "single"  — include the single file at SyncPath
	//   "exclude" — skip paths matching SyncPath (literal abs path or
	//               glob with * / ** / ?). Stops walks for any include
	//               source on the same instance and purges DB rows.
	Mode          string    `gorm:"type:varchar(16);not null;default:'folder'"`
	RetentionDays int       `gorm:"not null;default:0"` // 0 = never purge (include only)
	Enabled       bool      `gorm:"not null;default:true"`
	CreatedAt     time.Time `gorm:"not null"`
	UpdatedAt     time.Time `gorm:"not null"`
}

ProviderStorageSource is one configured sync source per provider instance. Multiple sources can exist per instance (e.g. credentials folder + sessions folder). The Manager reads this table at boot (RestoreAll) and at Start (background tickers).

func (ProviderStorageSource) TableName added in v0.11.0

func (ProviderStorageSource) TableName() string

type PushSubscription added in v0.15.0

type PushSubscription struct {
	ID          string `gorm:"type:varchar(36);primaryKey"`
	UserID      string `gorm:"type:varchar(36);not null;index"`
	User        User   `gorm:"foreignKey:UserID"`
	Endpoint    string `gorm:"not null;uniqueIndex;type:text"`
	P256dh      string `gorm:"not null;type:text"`
	Auth        string `gorm:"not null;type:text"`
	UserAgent   string `gorm:"type:text"`
	DeviceLabel string `gorm:"type:varchar(160)"`
	CreatedAt   time.Time
	UpdatedAt   time.Time
	LastSeenAt  *time.Time
	DisabledAt  *time.Time `gorm:"index"`
}

PushSubscription is one browser/device endpoint for notifications. A single user can have many rows: desktop Chrome, Android Chrome, Safari, Firefox, and so on. Endpoint is globally unique because vendors issue it per subscription.

func (*PushSubscription) BeforeCreate added in v0.15.0

func (s *PushSubscription) BeforeCreate(tx *gorm.DB) error

type RunStatus

type RunStatus string

RunStatus represents the outcome of a single job execution.

const (
	RunStatusRunning RunStatus = "running"
	RunStatusSuccess RunStatus = "success"
	RunStatusError   RunStatus = "error"
)

type RunTrigger

type RunTrigger string

RunTrigger describes how a run was initiated.

const (
	RunTriggerManual RunTrigger = "manual"
	RunTriggerCron   RunTrigger = "cron"
)

type SSOProvider

type SSOProvider struct {
	ID           uint   `gorm:"primaryKey"`
	Provider     string `gorm:"uniqueIndex;type:varchar(32);not null"` // "google"
	ClientID     string `gorm:"type:varchar(255)"`
	ClientSecret string `gorm:"type:varchar(255)"`
	Enabled      bool   `gorm:"default:false"`
	// AllowedDomains is a comma-separated list of email domains allowed
	// to sign in through this provider (e.g. "abc.com,abc.net").
	// Empty string means no restriction — any email from the provider is
	// accepted. Matching is case-insensitive.
	AllowedDomains string `gorm:"type:text"`
	CreatedAt      time.Time
	UpdatedAt      time.Time
}

SSOProvider holds one OAuth/SSO provider's configuration. The callback URL is never stored — it's derived at runtime from app_variables.app_url + "/auth/callback".

func (SSOProvider) TableName

func (SSOProvider) TableName() string

type ScheduledMessage added in v0.29.0

type ScheduledMessage struct {
	ID string `gorm:"type:varchar(40);primaryKey"`
	// SessionID is the session the message is delivered into.
	SessionID string `gorm:"type:varchar(128);index;not null"`
	// OwnerUserID is who the schedule belongs to — copied from the target
	// session's Meta.UserID at create time (empty for legacy/unowned
	// sessions). Access control and the dashboard scope on this.
	OwnerUserID string `gorm:"type:varchar(36);index"`
	// CreatedBy records how the schedule was made: "ai" (agent scheduled
	// itself), "user" (dashboard), or "api" (external caller).
	CreatedBy string `gorm:"type:varchar(16)"`
	// SourceSessionID is the session the schedule was requested from. Usually
	// equals SessionID, but kept explicit so provenance is preserved when a
	// schedule targets a different session.
	SourceSessionID string `gorm:"type:varchar(128)"`
	// AgentName is the pool agent to route the delivered message to; default
	// "main".
	AgentName string `gorm:"type:varchar(64);not null;default:'main'"`
	// Message is the prompt injected as a role=user turn when the schedule
	// fires.
	Message string `gorm:"type:text;not null"`

	// Kind is "once" (fire a single time) or "recurring" (fire repeatedly on
	// Interval/Cron until cancelled or a stop condition is met).
	Kind string `gorm:"type:varchar(16);not null;default:'once'"`
	// RunAt is the NEXT concrete fire time (UTC) the runner claims on. For a
	// one-shot it is the single fire time; for a recurring schedule it is
	// advanced after each fire. Named RunAt (not NextRunAt) so the runner's
	// claim query — run_at <= now — is uniform across both kinds.
	RunAt time.Time `gorm:"index;not null"`
	// IntervalMs, when > 0 on a recurring schedule, is the fixed gap between
	// fires in milliseconds ("every 5m"). Mutually exclusive with Cron.
	IntervalMs int64 `gorm:"default:0"`
	// Cron, when set on a recurring schedule, is a 5-field cron expression
	// (min hour dom mon dow) picking fire minutes ("0 9 * * 1"). Mutually
	// exclusive with IntervalMs.
	Cron string `gorm:"type:varchar(128)"`

	// Paused, when true on a recurring schedule, suspends firing without
	// deleting the row. Resume clears it and recomputes RunAt.
	Paused bool `gorm:"default:false"`
	// MaxRuns > 0 caps the number of fires for a recurring schedule; after
	// the RunCount reaches it the schedule finishes (status=done). 0 = no cap.
	MaxRuns int `gorm:"default:0"`
	// EndsAt, when non-nil, stops a recurring schedule once RunAt passes it.
	EndsAt *time.Time
	// RunCount is how many times this schedule has fired.
	RunCount int `gorm:"default:0"`
	// LastRunAt is when it last fired (nil until the first fire).
	LastRunAt *time.Time

	// Status: pending | active | done | cancelled | failed.
	//   once:      pending → done   (or failed / cancelled)
	//   recurring: active  → active … → done  (max_runs/ends_at) / cancelled / failed
	Status string `gorm:"type:varchar(16);index;not null;default:'pending'"`
	// Attempts counts delivery attempts on the CURRENT fire (reset each fire).
	Attempts int `gorm:"default:0"`
	// LastError holds the most recent delivery failure reason.
	LastError string `gorm:"type:text"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

ScheduledMessage is one future message injection into an agent session. The agent can schedule itself ("check back at 12:40") or a user/scheduler can queue a nudge into a session that has gone idle. When run_at passes, the schedule runner delivers Message into SessionID through the normal pool send path (role=user, source="schedule") — so a fired schedule behaves exactly like a regular inbound message: it spawns the session if idle, or queues behind an in-flight turn if busy.

Provenance is first-class: OwnerUserID records WHO the schedule belongs to (copied from the target session's owner at create time) and drives both access control and the dashboard's per-owner view. SourceSessionID records the session the request originated in, so "who asked" survives even when a schedule targets a different session than the one that created it.

func (*ScheduledMessage) BeforeCreate added in v0.29.0

func (s *ScheduledMessage) BeforeCreate(tx *gorm.DB) error

func (*ScheduledMessage) IsRecurring added in v0.29.0

func (s *ScheduledMessage) IsRecurring() bool

IsRecurring reports whether this schedule repeats.

func (*ScheduledMessage) LiveStatus added in v0.29.0

func (s *ScheduledMessage) LiveStatus() string

LiveStatus is the status a fresh row of this kind starts in: recurring schedules are "active", one-shots are "pending".

type Session

type Session struct {
	Token     string    `gorm:"primaryKey;type:varchar(64)"`
	UserID    string    `gorm:"type:uuid;not null;index"`
	User      User      `gorm:"foreignKey:UserID"`
	ExpiresAt time.Time `gorm:"not null"`
	CreatedAt time.Time
}

type Skill added in v0.17.0

type Skill struct {
	ID        string  `gorm:"type:varchar(36);primaryKey"`
	Name      string  `gorm:"uniqueIndex;not null"`
	IsSystem  bool    `gorm:"not null;default:false"`
	CreatedBy *string `gorm:"column:created_by;type:varchar(36)"`
	FilePath  string  `gorm:"column:file_path"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (*Skill) BeforeCreate added in v0.17.0

func (s *Skill) BeforeCreate(tx *gorm.DB) error

type Tag

type Tag struct {
	ID          string `gorm:"type:varchar(36);primaryKey"`
	Name        string `gorm:"uniqueIndex;type:varchar(100);not null"`
	Description string `gorm:"type:varchar(500)"`
	IsGroup     bool   `gorm:"default:false"`
	IsFilter    bool   `gorm:"default:false"`
	IsSystem    bool   `gorm:"default:false"`
	SortOrder   int    `gorm:"default:0"`
	CreatedAt   time.Time
	DisplayName string `gorm:"-"`
}

Tag is a first-class label that can be attached to users and tools. Renaming a Tag propagates automatically because associations store TagID, not the name.

A Tag has two orthogonal-but-combinable purposes:

  • Access filter: when IsFilter is true and the tag is attached to a Private tool, only users who carry the same tag may access it. Tool-tags without IsFilter are purely cosmetic for access (they don't restrict who can enter).
  • Group on home: when IsGroup is true, tools carrying the tag are rendered together on the home page under Name. A tool with multiple group tags appears in each group.

A tag can set any combination of IsGroup and IsFilter independently.

IsSystem marks a tag as code-owned: it can only be assigned to tool/job/connector entities by code (via DefaultTags seeding), never by an admin from the UI to a user. The intent is to gate built-in maintenance items (e.g. the connector-runs-purge job) behind a tag no end user can carry — combined with IsFilter=true, this hides the item from /manager/* for everyone except admin (who bypasses the tag-filter rule wholesale).

func (*Tag) BeforeCreate

func (t *Tag) BeforeCreate(tx *gorm.DB) error

type ToolPermission

type ToolPermission struct {
	ToolPath   string         `gorm:"primaryKey;type:varchar(255)"`
	Visibility ToolVisibility `gorm:"type:varchar(50);default:'private'"`
	// Disabled hides the tool from every user (including admins) and makes
	// direct hits to /tools/{slug}/* return 404. Admins re-enable from
	// /admin/tools.
	Disabled  bool `gorm:"default:false"`
	UpdatedAt time.Time
}

ToolPermission stores the per-tool visibility override set by an admin. If no row exists for a tool path the code falls back to the tool's declared default visibility.

type ToolTag

type ToolTag struct {
	ToolPath string `gorm:"primaryKey;type:varchar(255)"`
	TagID    string `gorm:"primaryKey;type:varchar(36);index"`
}

ToolTag links a tool to a Tag. When a tool is Private and at least one ToolTag exists, only users carrying one of those tags may access it.

type ToolVisibility

type ToolVisibility = pkgentity.ToolVisibility

ToolVisibility is re-exported from pkg/entity so existing callers continue to work after the public contract moved out of internal.

type User

type User struct {
	ID           string `gorm:"type:varchar(36);primaryKey"`
	Email        string `gorm:"uniqueIndex;not null"`
	Name         string `gorm:"not null"`
	Avatar       string
	Role         UserRole     `gorm:"type:varchar(50);default:'user'"`
	IsOwner      bool         `gorm:"column:is_owner;not null;default:false"`
	Approved     bool         `gorm:"default:false"`
	PasswordHash string       `gorm:"type:varchar(255)"`
	Metadata     UserMetadata `gorm:"type:jsonb"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

func (*User) BeforeCreate

func (u *User) BeforeCreate(tx *gorm.DB) error

func (*User) CanSeeAllSessions added in v0.17.0

func (u *User) CanSeeAllSessions() bool

func (*User) IsAdmin

func (u *User) IsAdmin() bool

type UserMetadata

type UserMetadata struct {
	// HomeView picks the tool grid density: "compact" (icon+name) or
	// "detailed" (wider cards with description). Empty means compact.
	HomeView string `json:"home_view,omitempty"`

	// Theme picks the UI color palette. Values are Theme.ID from
	// internal/pkg/ui/theme.go ("light", "dark", "dracula", …).
	// Empty means "no preference" — guests follow the device
	// `prefers-color-scheme`, logged-in users can pick in the navbar.
	Theme string `json:"theme,omitempty"`

	// LightTheme / DarkTheme remember the last light- and dark-mode
	// theme the user picked from the dropdown, so the navbar toggle
	// can switch straight back to that variant instead of the generic
	// "light"/"dark" defaults. Values are Theme.ID.
	LightTheme string `json:"light_theme,omitempty"`
	DarkTheme  string `json:"dark_theme,omitempty"`

	// PinnedAgentProjectID is the agents Project this user pinned as
	// their personal default. One per user. When set, opening the agents
	// tool lands scoped to this project. Empty = unpinned. See
	// internal/planning/archive/project/design.md.
	PinnedAgentProjectID string `json:"pinned_agent_project_id,omitempty"`

	// PushPermission stores the last browser notification permission
	// state reported by the browser prompt ("granted" or "denied").
	PushPermission   string     `json:"push_permission,omitempty"`
	PushPermissionAt *time.Time `json:"push_permission_at,omitempty"`
}

UserMetadata is the free-form preferences bag stored as JSON on the user row. Add fields here when a new per-user preference is needed — all consumers should default to the zero value when a field is unset so existing rows (NULL metadata) keep working without a backfill.

func (UserMetadata) HomeViewOrDefault

func (m UserMetadata) HomeViewOrDefault() string

HomeViewOrDefault returns a valid HomeView value, falling back to compact when unset or unrecognized.

func (*UserMetadata) Scan

func (m *UserMetadata) Scan(value any) error

func (UserMetadata) Value

func (m UserMetadata) Value() (driver.Value, error)

type UserRole

type UserRole string
const (
	RoleAdmin UserRole = "admin"
	RoleUser  UserRole = "user"
)

type UserTag

type UserTag struct {
	UserID string `gorm:"primaryKey;type:uuid"`
	TagID  string `gorm:"primaryKey;type:varchar(36);index"`
}

UserTag assigns a Tag to a user. Only approved users may carry tags.

type Workflow added in v0.14.20

type Workflow struct {
	// ID is the stable UUID minted by the canvas / MCP create flow.
	// Matches Workflow.ID on the in-memory side.
	ID      string `gorm:"primaryKey;type:varchar(64)"`
	Name    string `gorm:"type:varchar(256);not null;default:''"`
	Enabled bool   `gorm:"not null;default:false"`
	Version int    `gorm:"not null;default:0"`
	// BodyPublished is the last-published JSON body. Mutated only by
	// the publish path. Empty until first publish.
	BodyPublished string `gorm:"type:text;not null;default:''"`
	// BodyDraft is the in-progress edit. Mutated on every save. Empty
	// when there is no draft (cleared on publish + discard).
	BodyDraft string `gorm:"type:text;not null;default:''"`
	HasDraft  bool   `gorm:"not null;default:false"`
	// EnvValues is the runtime config blob: a JSON object of
	// {key: value} where secrets are stored as wick_enc_ ciphertext and
	// kvlist/picker values as JSON strings — same shape as configs.value.
	// Current-only: NOT snapshotted into workflow_versions, so changing a
	// channel target or rotating a secret never bloats the history. The
	// env SCHEMA (which fields exist, their widgets) lives in the body;
	// only the VALUES live here. See internal/docs/workflow/11-env-secrets.md.
	EnvValues string `gorm:"type:text;not null;default:''"`
	CreatedBy string `gorm:"type:varchar(128);default:''"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

Workflow is the DB representation of a workflow document. Body columns hold the canonical JSON; metadata columns (Name, Enabled) duplicate body fields so the list page can render without parsing every row.

func (Workflow) TableName added in v0.14.20

func (Workflow) TableName() string

type WorkflowTestCase added in v0.14.20

type WorkflowTestCase struct {
	ID         uint   `gorm:"primaryKey;autoIncrement"`
	WorkflowID string `gorm:"type:varchar(64);not null;index:idx_wtc_workflow_name,unique,priority:1"`
	Name       string `gorm:"type:varchar(256);not null;index:idx_wtc_workflow_name,unique,priority:2"`
	Body       string `gorm:"type:text;not null"`
	UpdatedAt  time.Time
}

WorkflowTestCase mirrors the file-based `__tests__/<name>.json` fixtures. Migrated alongside the body so workflow tests survive the move and stay editable through the SPA.

func (WorkflowTestCase) TableName added in v0.14.20

func (WorkflowTestCase) TableName() string

type WorkflowVersion added in v0.14.20

type WorkflowVersion struct {
	ID         uint      `gorm:"primaryKey;autoIncrement" json:"id"`
	WorkflowID string    `gorm:"type:varchar(64);not null;index" json:"workflow_id"`
	Kind       string    `gorm:"type:varchar(16);not null;index" json:"kind"` // "draft" | "published"
	Body       string    `gorm:"type:text;not null" json:"body"`
	Message    string    `gorm:"type:varchar(512);default:''" json:"message"`
	CreatedBy  string    `gorm:"type:varchar(128);default:''" json:"created_by"`
	CreatedAt  time.Time `json:"created_at"`
}

WorkflowVersion captures one immutable snapshot of a workflow at a point in time. Two flavours:

  • Kind = "draft" — written on every save while the user is editing. Retention policy: keep the last N per workflow (default 50, configurable).
  • Kind = "published" — written on Publish. Retained forever; this is the audit trail the History UI surfaces as restorable revisions.

`Message` is an optional human-readable label users attach when they publish ("fix slack template", "add retry"). `CreatedBy` is the authenticated user id captured by the handler.

func (WorkflowVersion) TableName added in v0.14.20

func (WorkflowVersion) TableName() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL