db

package
v0.3.15 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: AGPL-3.0 Imports: 29 Imported by: 0

Documentation

Overview

Package db is the PostgreSQL data source for ARTEX (取代旧 graph 单文件 SQLite)。 它打开连接、应用 schema、并 seed 内置 agent 与变量目录。

Index

Constants

View Source
const (
	// 企业范围不限制规则条数:逐个 IP / 域名录入的范围动辄上千条,封顶只会逼用户
	// 拆成多个企业。请求体大小(server 侧 maxCompanyMutationBodyBytes)仍然兜底。
	//
	// Raw and normalized textual scope payloads are bounded by Unicode rune
	// count so multi-byte input is treated consistently by the API and DB layer.
	MaxCompanyScopeRawRunes   = 1024
	MaxCompanyScopeValueRunes = 1024
)
View Source
const (
	KindBegin   = "begin"   // DEPRECATED: legacy task root; new tasks seed an origin fact (KindFact + StateOrigin) instead
	KindGoal    = "goal"    // a task objective
	KindIntent  = "intent"  // an exploration direction (planner-generated)
	KindFact    = "fact"    // a worker's exploration result/conclusion (incl. negative results), tied to its intent
	KindFinding = "finding" // a confirmed vulnerability (report_finding), distinct from a fact
	KindHint    = "hint"
	KindDigest  = "digest" // a compressed fold of cold intents/facts (cold-digest-spec §1); lossless — members kept, restorable by id
)

Exploration node kinds (exploration_nodes.kind).

View Source
const (
	StateDigestActive     = "active"
	StateDigestSuperseded = "superseded"
)

Digest node states (kind='digest'). A digest is 'active' while it renders in graph_overview; major compaction retires a merged-away segment to 'superseded' (its covers edges repointed to the new digest) — cold-digest-spec §5.1.

View Source
const (
	RelSpawns      = "spawns"
	RelDerivedFrom = "derived_from"
	RelYields      = "yields"
	RelProves      = "proves"
	RelCovers      = "covers" // digest --covers--> member (cold-digest-spec §1); source of truth for "which digest folds node X"
)

Exploration edge relations (exploration_edges.rel).

View Source
const (
	FindingPending       = "pending"        // 待处理
	FindingInProgress    = "in_progress"    // 处理中
	FindingConfirmed     = "confirmed"      // 已确认(真实漏洞,未修复)
	FindingResolved      = "resolved"       // 已处理
	FindingFixed         = "fixed"          // 已修复
	FindingFalsePositive = "false_positive" // 误报
	FindingIgnored       = "ignored"        // 忽略
	FindingDuplicate     = "duplicate"      // 重复
	FindingRiskAccepted  = "risk_accepted"  // 风险接受
)

Finding triage states (findings.status).

View Source
const (
	SeverityCritical = "critical" // 严重
	SeverityHigh     = "high"     // 高
	SeverityMedium   = "medium"   // 中
	SeverityLow      = "low"      // 低
)

Finding severity levels (findings.severity).

View Source
const (
	NotifyStatePending = "pending" // 待发
	NotifyStateSending = "sending" // 已被某个 dispatcher 领取,租约未到期
	NotifyStateSent    = "sent"    // 已送达
	NotifyStateFailed  = "failed"  // 重试耗尽或永久失败,可手动重发
	NotifyStateSkipped = "skipped" // 渠道已停用,不再发送
)

投递状态。

View Source
const (
	NotifyModeRealtime = "realtime"
	NotifyModeDigest   = "digest"
)

推送模式。

View Source
const (
	TaskArchiveFormatVersion       = 3
	TaskArchiveLegacyFormatVersion = 1
	TaskArchiveLLMRecordsPath      = "database/llm_records.ndjson"
)
View Source
const (
	ArchiveQueued = "archive_queued"
	Archiving     = "archiving"
	ArchiveFailed = "archive_failed"
	ArchiveReady  = "ready"
	RestoreQueued = "restore_queued"
	Restoring     = "restoring"
	RestoreFailed = "restore_failed"
	DeleteQueued  = "delete_queued"
	Deleting      = "deleting"
	DeleteFailed  = "delete_failed"
)
View Source
const (
	MaxTaskAssetMutationCount = 100
	MaxTaskAssetSummaryRunes  = 500
)
View Source
const (
	MaxTaskTemplateNameRunes = 120
	MaxTaskTemplateTextRunes = 16000
)
View Source
const FindingRetestAgentKey = "retester"
View Source
const FindingUnassignedAsset = "__none__"

FindingUnassignedAsset 是「未关联资产」的节点 key,也是列表接口的筛选哨兵: 命中 asset_ids 为空、或所指资产已被删除的发现。

View Source
const FindingUnassignedTask = "__unassigned__"

FindingUnassignedTask is the task filter sentinel for findings whose task is absent. That includes rows created without a task and rows retained after their originating task was deleted (the findings FK is ON DELETE SET NULL).

View Source
const IntentBlockedLLMQuota = "llm_quota_exhausted"
View Source
const MaxDigestBatchSize = 500

MaxDigestBatchSize 是单个汇总批次一次最多合并多少条投递。

存在的理由是资源:一个汇总周期内如果扫出几万个漏洞(完全可能——一次全量扫描 就能做到),不设上界的话领取会把全部行读进内存、渲染成一条超长消息, 然后被渠道的长度上限截掉大半——既浪费内存,又**静默丢失**被截掉的那些漏洞。 设上界后,超出的部分留在库里成为下一个批次,下个周期自然发出去,不会丢。

取 500 的依据:它是渲染成消息后在企微 4096 字节上限内还"有内容可读"的量级; 再大也只是让截断发生在更靠后的位置而已。

View Source
const MaxNotifyAttempts = 3

MaxNotifyAttempts 是一条投递的最大尝试次数(含首次)。 定义在这里而非投递引擎里:它是状态机自身的策略,引擎只是执行者。

View Source
const MaxTaskCategoryBatchSize = 100

MaxTaskCategoryBatchSize bounds one batch move so a single request cannot lock an unbounded number of task rows.

View Source
const MaxTaskCategoryNameRunes = 80
View Source
const MaxTaskCompanyCount = 32

MaxTaskCompanyCount bounds the number of company asset scopes attached to a task. Company scopes are prompt context, and their currently attributed assets are snapshotted into the task at creation time.

View Source
const MaxTaskSourceCount = 8

MaxTaskSourceCount bounds the amount of live inherited context one task can pull into every planner/main-agent prompt. Inheritance is intentionally direct only; keeping the fan-in bounded also prevents a single create request from multiplying graph and asset-context queries without limit.

View Source
const MinPlanHeartbeatSeconds = 600

CreateTask creates an exploration + task in one transaction and returns the task. timeoutSeconds is the task-level wall-clock budget (0 = 不限时); deadline_at is stamped later at first real run (see engine), not here. MinPlanHeartbeatSeconds 是 planner 心跳间隔的下限 = 默认 = 10min。 低于它(含缺省 0 / 负值 / 误配的小值)一律抬到 10min,防止把 planner 打爆。

View Source
const StateIntentDeleted = "deleted"

StateIntentDeleted marks an intent the user假删除(soft delete): it drops out of the frontier and graph_overview like other terminal states, but keeps its node and full lineage. The delete reason lives in exploration_nodes.delete_reason.

View Source
const StateOrigin = "origin"

StateOrigin marks the task root fact (KindFact) seeded at task creation — the exploration graph's origin. Every intent traces back to it, so "an intent must connect to a fact" holds uniformly from the very first intent. Worker-produced facts use state 'confirmed', so this never collides.

Variables

View Source
var (
	ErrCompanyNameConflict = errors.New("company name already exists")
	ErrCompanyNotFound     = errors.New("company not found")
)
View Source
var (
	ErrActiveLLMProfileDelete      = errors.New("cannot delete the active LLM profile; activate another profile first")
	ErrLLMProfileReferencesChanged = errors.New("LLM profile references changed while deleting; retry the request")
	ErrLLMProfileNotFound          = errors.New("LLM profile not found")
)
View Source
var (
	ErrEvidenceConflict = errors.New("流量证据已变更,请刷新后重试")
	ErrFindingNotFound  = errors.New("漏洞不存在")
	ErrEvidenceNotFound = errors.New("流量证据不存在")
)
View Source
var (
	ErrTaskArchiveNotFound       = errors.New("task archive not found")
	ErrTaskArchiveIneligible     = errors.New("task must be paused or terminal before archiving")
	ErrTaskArchiveQueued         = errors.New("queued task must be paused before archiving")
	ErrTaskArchiveDependent      = errors.New("task is inherited by a live task")
	ErrTaskArchiveState          = errors.New("task archive state does not allow this operation")
	ErrTaskArchiveDeleteBlocked  = errors.New("task archive is required by another archive")
	ErrTaskArchiveFormatMismatch = errors.New("task archive format is not supported")
)
View Source
var (
	ErrTaskAssetInvalid       = errors.New("invalid task asset association")
	ErrTaskAssetTaskNotFound  = errors.New("task not found")
	ErrTaskAssetAssetNotFound = errors.New("asset not found")
)
View Source
var (
	ErrTaskCategoryInvalid      = errors.New("invalid task category")
	ErrTaskCategoryNameConflict = errors.New("task category name already exists")
	ErrTaskCategoryNotFound     = errors.New("task category not found")
	ErrTaskCategoryTaskNotFound = errors.New("task not found")
)
View Source
var (
	ErrTaskTemplateInvalid      = errors.New("invalid task template")
	ErrTaskTemplateNameConflict = errors.New("task template name already exists")
	ErrTaskTemplateNotFound     = errors.New("task template not found")
)
View Source
var (
	ErrTaskCompanyIDsInvalid = errors.New("invalid task company ids")
	ErrTaskCompanyNotFound   = errors.New("task company not found")
)
View Source
var ErrAssetIPInvalid = errors.New("invalid asset ip")

ErrAssetIPInvalid marks a non-address value in an asset's ip field. Both insert_assets and the asset API report it per item with the item index, so one bad entry never costs the rest of the batch.

View Source
var ErrFindingOriginUnavailable = errors.New("finding origin is no longer available")

ErrFindingOriginUnavailable means a retained finding no longer has a live owning task and finding node from which a follow-up intent can be derived.

View Source
var ErrIntentStateConflict = errors.New("intent state changed concurrently")

ErrIntentStateConflict marks a failed compare-and-set transition. Callers use it to distinguish an expected control race (HTTP 409) from a storage failure.

View Source
var ErrInterceptExecutionUnavailable = errors.New("未找到可唯一关联的原始工具调用;记录可能已删除,或旧审批没有保存关联 ID")
View Source
var ErrInterceptSessionDeleted = errors.New("对应会话或执行记录已被删除或不存在")
View Source
var ErrInterceptTaskDeleted = errors.New("任务已被删除或归档")
View Source
var ErrInvalidChatMentionCursor = errors.New("分页位置无效,请重新搜索")
View Source
var ErrNotificationChannelNotFound = errors.New("通知渠道不存在")

ErrNotificationChannelNotFound 渠道不存在。

View Source
var ErrRetestNotRunning = errors.New("本次复测已结束或尚未开始,请从漏洞详情发起新的复测")
View Source
var ErrSideBusy = errors.New("当前会话已有旁路问题正在回答")
View Source
var ErrSideParentGone = errors.New("旁路父会话已删除或归档")

Functions

func AddFindingTrafficTx

func AddFindingTrafficTx(tx *sql.Tx, findingID int64, prepared []PreparedTrafficEvidence) error

func AssetInterceptKindLabel

func AssetInterceptKindLabel(kind string) string

AssetInterceptKindLabel 返回 kind 的中文标签,用于给 agent 的说明消息。

func DSN

func DSN() (dsn, source string, err error)

DSN resolves the PostgreSQL connection string and reports where it came from. Precedence: env ARTEX_PG_DSN > config file (config.json). There is no built-in default — it errors if neither source is configured.

func DomainKey

func DomainKey(fqdn string) string

func EndpointKey

func EndpointKey(siteID int64, method, urlTemplate string) string

func IPKey

func IPKey(ip string) string

func InsertEvidenceSnapshotTx

func InsertEvidenceSnapshotTx(tx *sql.Tx, s TrafficEvidenceSnapshot) error

func IsTaskArchiveFormatSupported

func IsTaskArchiveFormatSupported(version int) bool

func IsTerminal

func IsTerminal(status string) bool

IsTerminal reports whether a task status is a terminal (finished) state. 单一真源,替换散落各处的 done/failed 硬编码判定。

func LockFindingEvidenceTx

func LockFindingEvidenceTx(tx *sql.Tx, findingID int64, version *int64) error

func LockTaskEvidenceTx

func LockTaskEvidenceTx(tx *sql.Tx, taskID int64) error

LockTaskEvidenceTx serializes writes with archive queueing (which locks the same task row). Once queued, its snapshot must not acquire new evidence.

func NormalizeICP

func NormalizeICP(value string) string

NormalizeICP removes every Unicode whitespace character and folds case. ICP matching intentionally performs no fuzzy or punctuation normalization.

func NormalizeParamName

func NormalizeParamName(name string) string

NormalizeParamName 归一化参数名(endpoint.params 元素的「相同引用」判定)。 规则:lower + trim,不做同义词合并(userId/user_id/uid 视为不同)。写入与查询共享此实现, 保证「按参数名查同公司接口」可复现。

func NormalizeTaskCompanyIDs

func NormalizeTaskCompanyIDs(ids []int64) ([]int64, error)

NormalizeTaskCompanyIDs validates IDs and removes duplicates while retaining the user's first-seen order.

func ParameterKey

func ParameterKey(endpointID int64, location, name string) string

func ParseDSL

func ParseDSL(s string) (*astNode, error)

ParseDSL parses a DSL query string into an expression tree.

Syntax:

field=value      fuzzy match (ILIKE '%value%')
field==value     exact match
field!=value     exclude fuzzy
port>8080        numeric comparison
bare word        full-text fuzzy across all main text fields

Operators: AND OR (case-insensitive), parentheses for grouping. AND binds tighter than OR.

func ParseExpID

func ParseExpID(s string) int64

ParseExpID turns the exploration-id segment parsed from a session string into an int64 (0 when empty/non-numeric, e.g. chat sessions keyed by conversation id).

func PortKey

func PortKey(ipID int64, proto string, port int) string

func RecordNotificationEventTx added in v0.3.15

func RecordNotificationEventTx(ctx context.Context, tx *sql.Tx, kind string, findingID int64, snap notify.Snapshot) bool

RecordNotificationEventTx 在调用方的事务里**尽力**写入一条推送事件。

这是漏洞写入路径上唯一的通知相关改动:一次 INSERT,不读任何表、不认识渠道、 不跑过滤。事务提交即保证「漏洞落库」与「推送任务存在」原子一致, 不存在提交成功却没入队、消息永久丢失的窗口。

两个关键设计,都不是随手写的:

  1. **为什么用 SAVEPOINT**:PostgreSQL 里事务内任一语句报错会让整个事务进入 aborted 状态,此后所有语句(含 COMMIT)一律失败。所以「忽略这条 INSERT 的错误、让调用方继续提交」在 PG 里是做不到的——除非用保存点把错误隔离在 这一条语句上。没有保存点,就只剩「整笔回滚」这一个选项。

  2. **为什么整笔回滚是错的**:推送是便利功能,漏洞记录才是产品本身。一个通知 表的问题(旧库未迁移、磁盘瞬时故障)不该让高危漏洞存不进库。所以这里隔离 错误、记日志、返回 false,让漏洞写入照常提交——代价是丢掉这一条推送。 返回 bool 而非 error 是刻意的:调用方不该把它当作会影响写入成败的错误。

func RootDomain

func RootDomain(host string) (root string, isApex bool)

RootDomain returns the registrable domain (eTLD+1) for a host and whether the host itself IS that apex (§3.1). Edge cases (§3.1 边界处理): an IP literal or a host publicsuffix can't classify (localhost / internal / non-ICANN TLD) is returned unchanged as its own root with isApex=true — best-effort, never treated as a subdomain.

func ServiceKey

func ServiceKey(portID int64, svcName string) string

func SetFindingStatusTx added in v0.3.15

func SetFindingStatusTx(ctx context.Context, tx *sql.Tx, id int64, status string) (from string, found bool, changed bool, notified bool, err error)

SetFindingStatusTx 在**调用方的事务**内更新漏洞状态并登记状态变更推送事件。

抽成事务级函数是为了让所有改状态的路径共用同一套语义——此前只有 patchFinding 走带通知的版本,而**复测结论为「已修复」时**(finding_retests 里那条 `UPDATE findings SET status=...`)是直接写库的,于是配了 `on_status_change` 的渠道对这类状态流转完全收不到推送:界面上状态悄悄变了, 运维要到打开平台才发现。

返回 from=变更前状态、found=漏洞是否存在、changed=状态是否真的变了、 notified=事件是否登记成功(登记失败不影响状态更新,见 RecordNotificationEventTx)。

func SiteKey

func SiteKey(scheme, host string, port int) string

func SplitURL

func SplitURL(raw, method string) (scheme, host string, port int, urlTemplate string, params []string, err error)

SplitURL parses a raw URL into scheme/host/port/urlTemplate/params for building site/endpoint/param keys.

func TechKey

func TechKey(name, version string) string

func TemplatePath

func TemplatePath(path string) string

TemplatePath collapses high-cardinality path segments into placeholders so the graph is not flooded by instances: /user/123 -> /user/{id}.

func TrafficSnapshotID

func TrafficSnapshotID(snapshot TrafficEvidenceSnapshot) string

func ValidChatMentionKind

func ValidChatMentionKind(kind string) bool

func ValidFindingStatus

func ValidFindingStatus(s string) bool

ValidFindingStatus reports whether s is a known triage state.

func ValidNotifyMode added in v0.3.15

func ValidNotifyMode(m string) bool

ValidNotifyMode 白名单校验推送模式(与 findings.status 同理:不用 DB CHECK, 便于后续扩展)。

func ValidSeverity

func ValidSeverity(s string) bool

ValidSeverity reports whether s is a known severity level.

func ValidTrafficRole

func ValidTrafficRole(role string) bool

func ValidateAssetIP

func ValidateAssetIP(value string) error

ValidateAssetIP keeps hostnames out of assets.ip. Network attribution casts that column to inet (see try_inet in schema.sql), so a hostname stored here is silently invisible to every IP/CIDR scope rule — the asset simply never gets attributed and nobody can tell why. The message states the fix rather than just the fault, so an agent can correct the item on its next turn. An empty value is accepted: the ip field is optional for service and endpoint assets.

func ValidateCompanyScopeInputBounds

func ValidateCompanyScopeInputBounds(inputs []ScopeInput) error

ValidateCompanyScopeInputBounds applies request-wide limits before parsing. Store methods call it again so non-HTTP callers cannot bypass the limits. 只约束单条规则的长度,不限制条数。

func ValidateDSL

func ValidateDSL(dsl string) error

ValidateDSL parses and compiles a DSL expression without touching the database. HTTP callers use it to distinguish client syntax errors from query failures, which must remain server errors.

Types

type ActiveFindingRetest

type ActiveFindingRetest struct {
	ID             int64  `json:"id"`
	FindingID      int64  `json:"finding_id,string"`
	ConversationID int64  `json:"conversation_id"`
	Status         string `json:"status"`
}

ActiveFindingRetest is the small status payload polled by the findings list. Finding IDs use the same string representation as the findings API.

type Activity

type Activity struct {
	ID        int64           `json:"id"`
	NodeID    *int64          `json:"node_id,omitempty"`
	Worker    string          `json:"worker,omitempty"`
	Kind      string          `json:"kind,omitempty"`
	Tool      string          `json:"tool,omitempty"`
	ToolUseID string          `json:"tool_use_id,omitempty"`
	IsError   bool            `json:"is_error"`
	Summary   string          `json:"summary,omitempty"`
	Detail    string          `json:"-"` // full blob; lazy-loaded, omitted from list payloads
	Metadata  json.RawMessage `json:"metadata,omitempty"`
	CreatedAt time.Time       `json:"created_at"`
	// Token usage — set only on the terminal kind='result' record; nil elsewhere.
	InputTokens      *int  `json:"input_tokens,omitempty"`
	OutputTokens     *int  `json:"output_tokens,omitempty"`
	CacheReadTokens  *int  `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens *int  `json:"cache_write_tokens,omitempty"`
	SourceTaskID     int64 `json:"source_task_id,omitempty"`
	Inherited        bool  `json:"inherited,omitempty"`
	// MainSeg is the main-agent conversation segment (nil for non-mainagent rows;
	// nil/0 == the original session). Lets the UI route a mainagent row to its segment.
	MainSeg *int `json:"main_seg,omitempty"`
}

Activity is one worker execution step (= old task_activity).

type ActivitySessionFilter

type ActivitySessionFilter struct {
	Worker  string
	NodeID  *int64
	Main    bool // main-agent session
	MainSeg *int // segment for the main session (nil == current, resolved by caller; 0 == original)
}

ActivitySessionFilter selects one UI "session" within a task's activity stream. Exactly one field is meaningful:

  • Main == true → the main-agent session (worker="mainagent") for one segment (MainSeg; nil/0 = the original segment, which also matches legacy NULL rows).
  • Worker != "" → filter by worker name (Plan = "planner"). Goal Agent 的第 0 轮拆解也以 worker="planner" 落库,故 Plan 会话完整覆盖 Goal+Planner。
  • NodeID != nil → a Worker session, filtered by node_id (= intent id).

A zero value (all empty) matches the whole task (no session filter).

type Agent

type Agent struct {
	ID               int64  `json:"id"`
	Key              string `json:"key"`
	Name             string `json:"name"`
	Description      string `json:"description"`
	Role             string `json:"role"`
	Builtin          bool   `json:"builtin"`
	Enabled          bool   `json:"enabled"`
	LLMProfileID     *int64 `json:"llm_profile_id"`    // 绑定的 LLM 配置;nil=跟随任务/会话 pin,再回退全局激活
	MaxTurns         int    `json:"max_turns"`         // 单次运行最大轮次;0=不限制
	RunSecs          int    `json:"run_seconds"`       // worker 单次运行墙钟上限(秒);0=不限制
	WebSearch        bool   `json:"web_search"`        // 是否启用网络搜索(受系统全局开关门控)
	InteractiveShell bool   `json:"interactive_shell"` // 是否启用交互式 shell(持久 PTY 会话工具族)
	WrapupPrompt     string `json:"wrapup_prompt"`     // 收尾提示词(超时/步数耗尽时的 settlement 提示);空=用代码内置默认
	WrapupMaxTurns   int    `json:"wrapup_max_turns"`  // 收尾阶段自身的轮数预算;0=用代码内置默认(按 agent)
	// 任务级超时收尾词(与 per-run 两套;仅 worker/planner 用);空/0=用代码内置默认。
	TaskTimeoutWrapupPrompt   string `json:"task_timeout_wrapup_prompt"`
	TaskTimeoutWrapupMaxTurns int    `json:"task_timeout_wrapup_max_turns"`
	// P3 触发后处理策略(仅自定义 agent 有意义):
	// TriggerRunMode  serial|parallel — 串行排队 / 每次触发各自并发一个会话
	// TriggerMergeMode by_task|all|none — 仅 serial 用:同任务合并 / 全部合并 / 不合并
	// TriggerMaxParallel — 仅 parallel 用的每 agent 并发上限;0=不限
	TriggerRunMode     string `json:"trigger_run_mode"`
	TriggerMergeMode   string `json:"trigger_merge_mode"`
	TriggerMaxParallel int    `json:"trigger_max_parallel"`
}

type AgentTrigger

type AgentTrigger struct {
	ID                 int64      `json:"id"`
	AgentKey           string     `json:"agent_key"`
	Enabled            bool       `json:"enabled"`
	IntervalSec        int        `json:"interval_sec"`
	OnFinding          bool       `json:"on_finding"`
	OnGoalMet          bool       `json:"on_goal_met"`
	OnTaskTimeout      bool       `json:"on_task_timeout"`
	OnToolCall         bool       `json:"on_tool_call"`
	OnTaskCreate       bool       `json:"on_task_create"`
	IntervalMessage    string     `json:"interval_message"` // 各触发条件的独立用户消息
	FindingMessage     string     `json:"finding_message"`
	GoalMessage        string     `json:"goal_message"`
	TaskTimeoutMessage string     `json:"task_timeout_message"`
	ToolCallMessage    string     `json:"tool_call_message"`
	TaskCreateMessage  string     `json:"task_create_message"`
	ToolNames          []string   `json:"tool_names"` // 选中的工具 key 列表(DB 存 JSON 文本)
	LastFire           *time.Time `json:"last_fire,omitempty"`
}

AgentTrigger is one P3 trigger attached to a custom agent. Six trigger conditions can be on at once: interval / on_finding / on_goal_met / on_task_timeout / on_tool_call / on_task_create. ToolNames scopes the tool-call trigger to a non-empty set of tool keys (empty is rejected at the API layer for on_tool_call).

type Asset

type Asset struct {
	ID         int64   `json:"id"`
	Type       string  `json:"type"`
	CompanyID  *int64  `json:"company_id,omitempty"`
	TaskIDs    []int64 `json:"task_ids"`
	Domain     string  `json:"domain,omitempty"`
	RootDomain string  `json:"root_domain,omitempty"`
	IP         string  `json:"ip,omitempty"`
	CSegment   string  `json:"c_segment,omitempty"`
	Port       *int    `json:"port,omitempty"`
	ICP        string  `json:"icp,omitempty"`
	// ip fields
	BoundDomains []string         `json:"bound_domains,omitempty"`
	OpenPorts    []map[string]any `json:"open_ports,omitempty"`
	// subdomain fields
	RecordType  string   `json:"record_type,omitempty"`
	RecordValue []string `json:"record_value,omitempty"`
	// app fields
	BundleID       string `json:"bundle_id,omitempty"`
	AppName        string `json:"app_name,omitempty"`
	Category       string `json:"category,omitempty"`
	AppDescription string `json:"app_description,omitempty"`
	AppICP         string `json:"app_icp,omitempty"`
	// service fields
	URL           string           `json:"url,omitempty"`
	ServiceType   string           `json:"service_type,omitempty"`
	ServiceName   string           `json:"service_name,omitempty"`
	FaviconMMH3   string           `json:"favicon_mmh3,omitempty"`
	StatusCode    *int             `json:"status_code,omitempty"`
	ContentLength *int64           `json:"content_length,omitempty"`
	PageTitle     string           `json:"page_title,omitempty"`
	Technologies  []string         `json:"technologies,omitempty"`
	Auth          []map[string]any `json:"auth,omitempty"`
	// endpoint fields
	Method string           `json:"method,omitempty"`
	Params []map[string]any `json:"params,omitempty"`
	// meta
	Extra             map[string]any `json:"extra,omitempty"`
	LastSeen          string         `json:"last_seen"`
	TaskSource        string         `json:"task_source,omitempty"`
	TaskSourceSummary string         `json:"task_source_summary,omitempty"`
	TaskSourceNodeID  *int64         `json:"task_source_node_id,omitempty"`
}

Asset is a row in the assets table.

func (*Asset) InterceptLabel

func (a *Asset) InterceptLabel() string

InterceptLabel 返回资产的简短标识,用于给 agent 的说明消息。

type AssetGateDecision

type AssetGateDecision struct {
	Allowed bool
	Reason  string // 被拒原因(不含资产标识);Allowed=true 时为空
}

AssetGateDecision 是「先拦截后允许」闸门对一组候选串的判定结果。

func EvaluateAssetGate

func EvaluateAssetGate(blockRules, allowRules []AssetInterceptRule, domains, ips, urls []string) AssetGateDecision

EvaluateAssetGate 执行任务级闸门判定:

  1. 命中任一启用的 blockRules → 拒绝(拦截原因)。
  2. 否则若 allowRules 存在启用项且都不命中 → 拒绝(不在允许范围)。
  3. 否则放行。

allowRules 为空/无启用项时,允许闸门不生效(即不启用白名单,全部放行), 避免「未配置允许规则」把所有资产挡掉。

type AssetInterceptHit

type AssetInterceptHit struct {
	Asset  *Asset
	Reason string // 可读原因
}

AssetInterceptHit 描述一个被闸门拒绝的资产(拦截命中 或 不在允许范围)。

func (AssetInterceptHit) Describe

func (h AssetInterceptHit) Describe() string

Describe 返回一条可读的说明:资产信息 + 原因。

type AssetInterceptRule

type AssetInterceptRule struct {
	ID      int64  `json:"id"`
	Enabled bool   `json:"enabled"`
	Kind    string `json:"kind"` // exact_domain|exact_ip|exact_url|fuzzy_domain|fuzzy_ip|fuzzy_url|cidr
	Pattern string `json:"pattern"`
	Note    string `json:"note"`
	Builtin bool   `json:"builtin"`
	// Action 仅用于任务级规则:'block'=拦截 'allow'=允许(白名单)。
	// 全局规则(asset_intercept_rules)不带此列,恒为空,视为拦截。
	Action    string    `json:"action,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AssetInterceptRule is one row of asset_intercept_rules — a global asset blocklist entry. Unlike intercept_rules (which matches tool name / input), these match the *target asset*: an exact/fuzzy domain·ip·url, or a CIDR range. This layer only stores rules; the matching/enforcement logic lives elsewhere.

func MatchAssetInterceptRules

func MatchAssetInterceptRules(rules []AssetInterceptRule, domains, ips, urls []string) (AssetInterceptRule, string, bool)

MatchAssetInterceptRules 返回第一条命中给定 域名/IP/URL 候选串的启用规则,及命中的具体值。 供 insert_assets 用原始输入(尚未落库的 assetInputItem)匹配。

func (AssetInterceptRule) Reason

func (r AssetInterceptRule) Reason() string

Reason 返回一条可读的命中原因,形如:命中资产拦截规则 [域名(模糊): .gov.cn](备注)。

type AssetRef

type AssetRef struct {
	ID           int64  `json:"id"`
	Kind         string `json:"kind"`
	State        string `json:"state"`
	Summary      string `json:"summary"`
	SourceTaskID int64  `json:"source_task_id,omitempty"`
	Inherited    bool   `json:"inherited,omitempty"`
}

AssetRef is a compact exploration node (intent/fact/finding) that references an asset via an anchor — for the coverage graph's node drawer.

type AssetStore

type AssetStore struct {
	// contains filtered or unexported fields
}

AssetStore operates on the assets table.

func (*AssetStore) AddAgentScope

func (s *AssetStore) AddAgentScope(taskID int64, kind, value, reason, source string) (TaskScope, error)

AddAgentScope parses a (kind,value) pair and records it as task scope. source is "agent" when called from an LLM tool, "manual" from the UI. company: value = company name or id (must already exist). root_domain/subdomain: value = a domain. ip/cidr: value = an IP or CIDR (bare IP → /32,/128).

func (*AssetStore) AddAutoScope

func (s *AssetStore) AddAutoScope(taskID int64, assetType, domain, rawURL, ip string) error

AddAutoScope records the conservative task scope implied by ONE explicitly-inserted asset item (source='auto'). MUST be called only from insertAssets' top-level loop — never from a db-layer side effect (linkHostAssets), so派生资产不会盲目扩大范围。 Rule: scope granularity follows the asset's own type. taskID<=0 → no-op.

func (*AssetStore) AppendIPBoundDomain

func (s *AssetStore) AppendIPBoundDomain(ipStr, domain string) error

AppendIPBoundDomain appends a domain to an existing IP asset's bound_domains.

func (*AssetStore) AppendIPPort

func (s *AssetStore) AppendIPPort(ipStr string, port int, serviceName string) error

AppendIPPort appends a {port, service} entry to an existing IP asset's open_ports.

func (*AssetStore) AttachAssetsToTask

func (s *AssetStore) AttachAssetsToTask(taskID int64, assetIDs []int64, sourceSummary string) (TaskAssetMutation, error)

AttachAssetsToTask associates existing global assets with one live task and records an operator-authored source summary. Global asset rows are retained.

func (*AssetStore) BuildCoverageGraph

func (s *AssetStore) BuildCoverageGraph(taskID, _ int64) (*CoverageGraphData, error)

BuildCoverageGraph assembles the full coverage graph for a task and its direct read-only sources: every in-scope asset plus connector root domains/companies, with current-or-source fact anchors reflected in Tested. The legacy expID argument is retained for API compatibility; the task registry is authoritative.

func (*AssetStore) CheckAssetsIntercept

func (s *AssetStore) CheckAssetsIntercept(taskID int64, ids []int64) ([]AssetInterceptHit, error)

CheckAssetsIntercept 按 id 载入资产,逐个执行「先拦截后允许」闸门判定,返回所有 被拒的资产。拦截规则 = 全局 ∪ 任务级 block;允许规则 = 任务级 allow(仅本任务)。 无 id 时快速返回。用全局 GetByIDs(不受任务范围过滤)以保证拦截不被 scope 削弱。

func (*AssetStore) Companies

func (s *AssetStore) Companies() *CompanyStore

Companies returns the company store associated with this asset store.

func (*AssetStore) CountByCompany

func (s *AssetStore) CountByCompany(companyID int64, typ string) (int, error)

func (*AssetStore) CountByTask

func (s *AssetStore) CountByTask(taskID int64, typ string) (int, error)

func (*AssetStore) CountByType

func (s *AssetStore) CountByType(typ string) (int, error)

CountByType returns the total number of assets of a type (for server-side pagination).

func (*AssetStore) CountDSL

func (s *AssetStore) CountDSL(dsl, typ string, taskID int64) (int, error)

CountDSL returns the total number of assets matching a DSL expression (and optional type), for server-side pagination — same WHERE as QueryDSL, without LIMIT/OFFSET. taskID > 0 scopes the count to assets attached to that task.

func (*AssetStore) CountsByType

func (s *AssetStore) CountsByType() (map[string]int, error)

CountsByType returns asset counts per type.

func (*AssetStore) CountsByTypeForTask

func (s *AssetStore) CountsByTypeForTask(taskID int64) (map[string]int, error)

func (*AssetStore) CoverageEnabled

func (s *AssetStore) CoverageEnabled(taskID int64) bool

CoverageEnabled reports whether a task has the asset-coverage feature turned on (tasks.coverage_enabled). Missing row / error → true (fail open to the default), so unknown/legacy tasks keep the historical behavior. taskID<=0 → true.

func (*AssetStore) CreateTaskInterceptRule

func (s *AssetStore) CreateTaskInterceptRule(taskID int64, action, kind, pattern, note string, enabled bool) (AssetInterceptRule, error)

CreateTaskInterceptRule inserts a rule under a task.

func (*AssetStore) DeleteByCompanyID

func (s *AssetStore) DeleteByCompanyID(companyID int64) (int64, error)

DeleteByCompanyID hard-deletes all assets belonging to a company. Returns rows deleted.

func (*AssetStore) DeleteByHost

func (s *AssetStore) DeleteByHost(host string) (map[string]int64, error)

DeleteByHost hard-deletes every asset whose host exactly matches the given value: root_domain / subdomain / service / endpoint (they carry the host in domain or root_domain) plus an ip asset and its services/endpoints (ip column). The host is normalized the same way it is stored (DomainKey: lowercase/trim/strip trailing dot) so matching is exact, not fuzzy. Passing a root domain also removes its subdomains and their services/endpoints (they carry root_domain = that host); passing a subdomain/IP removes only that host's own assets. Referencing exploration_anchors rows are cleaned by ON DELETE CASCADE. Returns rows deleted, grouped by type.

func (*AssetStore) DeleteByIDs

func (s *AssetStore) DeleteByIDs(ids []int64) (int64, error)

DeleteByIDs hard-deletes assets by their IDs. Returns the number of rows deleted.

func (*AssetStore) DeleteByTaskID

func (s *AssetStore) DeleteByTaskID(taskID int64) (int64, error)

DeleteByTaskID removes assets owned only by taskID and detaches taskID from assets shared with other tasks. Full task deletion uses the coordinated transaction in DeleteTaskCascadePrepared; this method remains for callers that explicitly manage only asset associations.

func (*AssetStore) DeleteTaskInterceptRule

func (s *AssetStore) DeleteTaskInterceptRule(taskID, ruleID int64) (bool, error)

DeleteTaskInterceptRule removes a task's rule. Returns false if not found.

func (*AssetStore) DeleteTaskScope

func (s *AssetStore) DeleteTaskScope(taskID, scopeID int64) (bool, error)

DeleteTaskScope removes a single scope row by id, scoped to the given task. Returns whether a row was actually deleted.

func (*AssetStore) DetachAssetFromTask

func (s *AssetStore) DetachAssetFromTask(taskID, assetID int64) (bool, error)

DetachAssetFromTask removes only the task association. The global asset and exploration anchors remain available for historical blackboard auditing.

func (*AssetStore) FindingMetaByNodeID

func (a *AssetStore) FindingMetaByNodeID(taskID int64) (map[int64]FindingMeta, error)

FindingMetaByNodeID maps a task's finding node ids to their standalone-row metadata (status + anchored asset ids) via the asset store, so callers holding only an AssetStore (e.g. the agent ToolSet) can reach it without a raw *DB.

func (*AssetStore) GetByIDs

func (s *AssetStore) GetByIDs(ids []int64) ([]*Asset, error)

GetByIDs returns assets with the given ids (order preserved by id array order).

func (*AssetStore) GetByIDsInScope

func (s *AssetStore) GetByIDsInScope(taskID int64, ids []int64) ([]*Asset, error)

GetByIDsInScope is GetByIDs restricted to ids that BELONG to taskID's (and its direct source tasks') declared scope, so an agent cannot reach out-of-scope assets by id. taskID<=0 (non-task contexts) falls back to the global GetByIDs. Out-of-scope ids are silently dropped from the result (not an error).

func (*AssetStore) HostsByTask

func (s *AssetStore) HostsByTask(taskID int64) ([]string, error)

HostsByTask returns the exact HTTP host candidates attached to a task's assets. Domain/IP columns cover root domains, subdomains and non-HTTP services; URL covers HTTP services and endpoints.

func (*AssetStore) HostsByTaskWithSources

func (s *AssetStore) HostsByTaskWithSources(taskID int64) ([]string, error)

HostsByTaskWithSources resolves exact HTTP host candidates from assets that are attached to, anchored by, or in scope for the current task or a direct source. Traffic remains global and is not copied. This read helper must not be used for destructive task cleanup; HostsByTask intentionally retains that narrower, task-owned behavior.

func (*AssetStore) HostsForTaskDeletion

func (s *AssetStore) HostsForTaskDeletion(taskID, explorationID int64) ([]string, error)

HostsForTaskDeletion returns hosts that belong to the task being deleted and are not referenced by any other live task. Current-task candidates include both task_ids ownership and exploration anchors so legacy seeded assets (which were anchor-only) are covered. Protection is host-wide: if another live task references any asset for a candidate host, that host's global traffic remains.

func (*AssetStore) IntentAssets

func (s *AssetStore) IntentAssets(taskID int64) ([]IntentAsset, error)

IntentAssets returns all local worker targets plus immutable targets from the task's direct sources. Inherited non-terminal intents remain hidden, matching the existing source-aware session contract.

func (*AssetStore) ListAssetInterceptRules

func (s *AssetStore) ListAssetInterceptRules() ([]AssetInterceptRule, error)

ListAssetInterceptRules 是 *DB 同名方法的透传,让只持有 AssetStore 的调用方 (如 agent 工具)也能读取规则。

func (*AssetStore) ListTaskInterceptRules

func (s *AssetStore) ListTaskInterceptRules(taskID int64) ([]AssetInterceptRule, error)

ListTaskInterceptRules returns a task's rules (both block and allow) as AssetInterceptRule (Builtin always false; Action carries block/allow). taskID <= 0 returns nothing.

func (*AssetStore) ListTaskScope

func (s *AssetStore) ListTaskScope(taskID int64) ([]TaskScope, error)

ListTaskScope returns all scope rows for a task.

func (*AssetStore) ListTaskScopeWithSources

func (s *AssetStore) ListTaskScopeWithSources(taskID int64) ([]TaskScope, error)

ListTaskScopeWithSources returns the current task's scope followed by the scopes of its direct source tasks. TaskScope.TaskID preserves provenance.

func (*AssetStore) ListUntestedAssets

func (s *AssetStore) ListUntestedAssets(taskID, expID int64, typ string, limit, offset int) ([]CoverageAsset, int, error)

ListUntestedAssets returns a task's in-scope, not-yet-tested assets, optionally filtered by asset type, paginated. Returns the page + the total count. limit<=0 → 10.

func (*AssetStore) ListUntestedAssetsWithSources

func (s *AssetStore) ListUntestedAssetsWithSources(taskID int64, typ string, limit, offset int) ([]CoverageAsset, int, error)

ListUntestedAssetsWithSources is the direct-source-aware backlog query used by inherited tasks. Source fact anchors remove assets from the backlog.

func (*AssetStore) QueryByCompany

func (s *AssetStore) QueryByCompany(companyID int64, typ string, limit, offset int) ([]*Asset, error)

QueryByCompany returns assets for a company, optionally filtered by type. limit <= 0 means no limit.

func (*AssetStore) QueryByTask

func (s *AssetStore) QueryByTask(taskID int64, typ string, limit, offset int) ([]*Asset, error)

QueryByTask returns assets rows that have a given task_id in task_ids.

func (*AssetStore) QueryByType

func (s *AssetStore) QueryByType(typ string, limit, offset int) ([]*Asset, error)

QueryByType returns assets rows of a given type, newest first.

func (*AssetStore) QueryDSL

func (s *AssetStore) QueryDSL(dsl, typ string, taskID int64, limit, offset int) ([]*Asset, error)

QueryDSL executes a DSL query string against the asset store. typ is an optional asset type filter applied independently of the DSL expression. taskID > 0 scopes results to assets attached to that task and hydrates each row's per-task source metadata (as QueryByTask does).

func (*AssetStore) QueryDSLInScope

func (s *AssetStore) QueryDSLInScope(dsl, typ string, taskID int64, limit, offset int) ([]*Asset, error)

QueryDSLInScope is QueryDSL restricted to assets that BELONG to taskID's (and its direct source tasks') declared scope — membership, not literal value: a root_domain scope returns every subdomain / service / endpoint under it. This is the agent-facing list_assets path, so an agent queries the task's relevant assets instead of the whole shared库. taskID<=0 (non-task contexts: Auto / pentest / chat) has no scope to honor and falls back to the plain global QueryDSL. Rows carry the same per-task source metadata as QueryByTask.

func (*AssetStore) RegisterTaskAssetScopes

func (s *AssetStore) RegisterTaskAssetScopes(taskID int64, inputs []ScopeInput) (TaskAssetScopeMutation, error)

RegisterTaskAssetScopes accepts the same structured scope rules as enterprise assets. The entire request is atomic: invalid input or any storage failure leaves both global assets and task scope unchanged.

func (*AssetStore) SetTaskAssetSource

func (s *AssetStore) SetTaskAssetSource(taskID, assetID int64, source, summary string, sourceNodeID *int64) error

SetTaskAssetSource improves the generic trigger-created provenance for one existing task association. It never creates or deletes an asset.

func (*AssetStore) TaskCoverage

func (s *AssetStore) TaskCoverage(taskID, expID int64) (*Coverage, error)

TaskCoverage computes rough per-type coverage for a task. taskID indexes task_scope + assets; expID indexes the fact anchors. Reference figure only.

func (*AssetStore) TaskCoverageWithSources

func (s *AssetStore) TaskCoverageWithSources(taskID int64) (*Coverage, error)

TaskCoverageWithSources computes one coverage view over the union of the current task and its direct sources: source scopes and anchored assets extend the denominator, while fact anchors count as tested. No row is copied.

func (*AssetStore) TaskInterceptRulesSplit

func (s *AssetStore) TaskInterceptRulesSplit(taskID int64) (block, allow []AssetInterceptRule, err error)

TaskInterceptRulesSplit loads a task's rules and splits them into block and allow sets, for the enforcement gate.

func (*AssetStore) ToggleTaskInterceptRule

func (s *AssetStore) ToggleTaskInterceptRule(taskID, ruleID int64, enabled bool) error

ToggleTaskInterceptRule flips the enabled state of a task's rule.

func (*AssetStore) UpdateTaskInterceptRule

func (s *AssetStore) UpdateTaskInterceptRule(taskID, ruleID int64, action, kind, pattern, note string, enabled bool) (AssetInterceptRule, error)

UpdateTaskInterceptRule replaces the editable fields of a task's rule (scoped by task_id so a rule can only be edited through its owning task).

func (*AssetStore) UpsertApp

func (s *AssetStore) UpsertApp(req UpsertAppReq) (int64, error)

UpsertApp idempotently inserts or merges an app asset.

func (*AssetStore) UpsertEndpoint

func (s *AssetStore) UpsertEndpoint(req UpsertEndpointReq) (int64, error)

UpsertEndpoint inserts or merges an endpoint asset. Domain, port, root_domain are auto-extracted from the URL.

func (*AssetStore) UpsertHTTPService

func (s *AssetStore) UpsertHTTPService(req UpsertHTTPServiceReq) (int64, error)

UpsertHTTPService inserts or merges an HTTP service asset. Domain, port, service_name, and root_domain are auto-extracted from URL.

func (*AssetStore) UpsertIP

func (s *AssetStore) UpsertIP(req UpsertIPReq) (int64, error)

UpsertIP idempotently inserts or merges an IP asset.

func (*AssetStore) UpsertOtherService

func (s *AssetStore) UpsertOtherService(req UpsertOtherServiceReq) (int64, error)

UpsertOtherService inserts or merges a non-HTTP service asset.

func (*AssetStore) UpsertRootDomain

func (s *AssetStore) UpsertRootDomain(req UpsertRootDomainReq) (int64, error)

UpsertRootDomain idempotently inserts or merges a root domain asset.

func (*AssetStore) UpsertSubdomain

func (s *AssetStore) UpsertSubdomain(req UpsertSubdomainReq) (id int64, err error)

UpsertSubdomain idempotently inserts or merges a subdomain asset and triggers side effects: root_domain upsert + IP bound_domains update.

type AuthItem

type AuthItem struct {
	Type        string `json:"type,omitempty"`
	Username    string `json:"username,omitempty"`
	Password    string `json:"password,omitempty"`
	Token       string `json:"token,omitempty"`
	Description string `json:"description,omitempty"`
}

AuthItem is one entry in the auth array.

type ChatMention

type ChatMention struct {
	Kind        string `json:"kind"`
	ID          int64  `json:"id"`
	Label       string `json:"label"`
	Description string `json:"description"`
}

ChatMention is a lightweight search result. Details are read again on send.

type ChatMentionPage

type ChatMentionPage struct {
	Items      []ChatMention `json:"items"`
	NextCursor string        `json:"next_cursor,omitempty"`
}

type CommandRecord

type CommandRecord struct {
	ID        int64     `json:"id"`
	ExpID     int64     `json:"exploration_id"`
	Worker    string    `json:"worker"`
	Tool      string    `json:"tool"`
	Command   string    `json:"command"`
	Output    string    `json:"output"`
	IsError   bool      `json:"is_error"`
	CreatedAt time.Time `json:"created_at"`
}

CommandRecord is a paired tool_use + tool_result from the activity table (any tool, not just Bash). Command holds the raw tool input (JSON).

type Company

type Company struct {
	ID        int64   `json:"id"`
	Name      string  `json:"name"`
	NKey      string  `json:"nkey"`
	CreatedAt string  `json:"created_at"`
	UpdatedAt string  `json:"updated_at"`
}

Company is a row in the companies table.

type CompanyScopeValidationError

type CompanyScopeValidationError struct{ Message string }

CompanyScopeValidationError identifies a client-correctable scope error. Storage and transaction failures are returned as ordinary errors instead.

func (*CompanyScopeValidationError) Error

type CompanyStore

type CompanyStore struct {
	// contains filtered or unexported fields
}

CompanyStore operates on the companies + company_scope tables.

func (*CompanyStore) AddScope

func (s *CompanyStore) AddScope(companyID int64, lines []string, reason string) (added, skipped, invalid int, errors []string)

AddScope parses and inserts scope lines for a company, then reattributes assets. Returns counts of added, skipped, and invalid lines.

func (*CompanyStore) AddScopeInputs

func (s *CompanyStore) AddScopeInputs(companyID int64, inputs []ScopeInput, reason string) (added, skipped, invalid int, errors []string)

AddScopeInputs inserts structured scope rules. Empty kinds use the automatic CIDR/IP/ICP/domain/keyword classification used by AddScope.

func (*CompanyStore) AddScopeInputsChecked

func (s *CompanyStore) AddScopeInputsChecked(companyID int64, inputs []ScopeInput, reason string) (
	added, skipped, invalid int, validationErrors []string, err error,
)

AddScopeInputsChecked inserts structured scope rules while keeping input validation separate from storage and transaction errors.

func (*CompanyStore) CreateCompanyWithScope

func (s *CompanyStore) CreateCompanyWithScope(name, logo string, inputs []ScopeInput, reason string) (
	id int64, added, skipped, invalid int, validationErrors []string, err error,
)

CreateCompanyWithScope creates a company without updating an existing row. The company, its valid initial scope rules, and derived asset attribution are committed atomically. Invalid inputs retain the legacy partial-validation contract and are reported without preventing valid rules from being stored.

func (*CompanyStore) DeleteCompany

func (s *CompanyStore) DeleteCompany(id int64) error

DeleteCompany deletes a company and re-evaluates automatic ownership against the remaining companies in the same transaction. Explicitly-owned assets are detached by the FK and may then fall back to a remaining scope match.

func (*CompanyStore) DeleteCompanyWithAssets

func (s *CompanyStore) DeleteCompanyWithAssets(id int64, deleteAssets bool) (assetsDeleted int64, err error)

DeleteCompanyWithAssets deletes a company and optionally all of its assets in one transaction, then re-evaluates ownership against the remaining companies.

func (*CompanyStore) GetCompany

func (s *CompanyStore) GetCompany(id int64) (*Company, error)

GetCompany returns one company by id (nil if not found).

func (*CompanyStore) GetCompanyByName

func (s *CompanyStore) GetCompanyByName(name string) (*Company, error)

GetCompanyByName returns one company by normalized name (nil if not found).

func (*CompanyStore) GetScope

func (s *CompanyStore) GetScope(companyID int64) ([]ScopeRule, error)

GetScope returns all scope rules for a company.

func (*CompanyStore) ListCompanies

func (s *CompanyStore) ListCompanies() ([]*CompanyWithScope, error)

ListCompanies returns all companies with scope and asset count.

func (*CompanyStore) MalformedIPAssetWarning

func (s *CompanyStore) MalformedIPAssetWarning() (string, error)

MalformedIPAssetWarning reports assets with an unparseable ip outside of any mutation, letting the API attach the warning to a scope response without widening the mutation signatures — an unrelated data problem is not one of this request's validation errors.

func (*CompanyStore) RecomputeAttribution

func (s *CompanyStore) RecomputeAttribution() error

RecomputeAttribution rebuilds only scope-derived ownership. Explicit company links are immutable under scope edits. Precedence is domain, IP/CIDR, then normalized exact ICP; keyword rules never attribute assets.

func (*CompanyStore) ResolveCompany

func (s *CompanyStore) ResolveCompany(rootDomain, ipStr string) (*int64, error)

ResolveCompany returns the company_id for a given root_domain and/or ip, or nil if no scope rule matches. Mirrors the attribution logic used at asset insert time.

func (*CompanyStore) ResolveCompanyWithICP

func (s *CompanyStore) ResolveCompanyWithICP(rootDomain, ipStr, icp string) (*int64, error)

ResolveCompanyWithICP mirrors RecomputeAttribution for insert-time ownership. ICP is consulted only after domain and IP/CIDR fail to match.

func (*CompanyStore) UpdateScope

func (s *CompanyStore) UpdateScope(companyID int64, lines []string, reason string) (added, invalid int, errs []string)

UpdateScope replaces all scope rules for a company and reattributes.

func (*CompanyStore) UpdateScopeInputs

func (s *CompanyStore) UpdateScopeInputs(companyID int64, inputs []ScopeInput, reason string) (added, invalid int, errs []string)

UpdateScopeInputs replaces all rules with a structured set.

func (*CompanyStore) UpdateScopeInputsChecked

func (s *CompanyStore) UpdateScopeInputsChecked(companyID int64, inputs []ScopeInput, reason string) (
	added, invalid int, validationErrors []string, err error,
)

UpdateScopeInputsChecked replaces all rules while separating validation feedback from storage and transaction failures.

func (*CompanyStore) UpsertByName

func (s *CompanyStore) UpsertByName(name string) (int64, error)

UpsertByName creates the company if it doesn't exist, then returns its id.

func (*CompanyStore) UpsertCompany

func (s *CompanyStore) UpsertCompany(name, logo string) (id int64, created bool, err error)

UpsertCompany creates or updates a company by name. Returns the id and whether a new row was created.

type CompanyWithScope

type CompanyWithScope struct {
	Company
	Scope      []ScopeRule `json:"scope"`
	AssetCount int         `json:"asset_count"`
}

CompanyWithScope extends Company with its scope rules and asset count.

type Constraint

type Constraint struct {
	ID        int64     `json:"id"`
	Kind      string    `json:"kind"` // allow | deny
	Text      string    `json:"text"`
	Origin    string    `json:"origin,omitempty"` // goals | human | system
	CreatedAt time.Time `json:"created_at"`
}

Constraint is one operator-authored operation constraint for a task: kind=allow (permitted operations) or kind=deny (forbidden operations), free-text. Stored in task_constraints, keyed by exploration_id (cascades with the exploration).

type ConvTokenSummary

type ConvTokenSummary struct {
	LLMProfileID     *int64 `json:"llm_profile_id"`
	CreatedAt        string `json:"created_at"`
	InputTokens      int    `json:"input_tokens"`
	OutputTokens     int    `json:"output_tokens"`
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
}

ConvTokenSummary is one conversation's token total (sum of its kind='result' rows) with its profile + created_at, used to merge conversation usage into the dashboard's per-profile / daily token stats (which otherwise cover only tasks).

type Conversation

type Conversation struct {
	ID           int64      `json:"id"`
	AgentKey     string     `json:"agent_key"`
	Title        string     `json:"title"`
	LLMProfileID *int64     `json:"llm_profile_id,omitempty"`
	Pinned       bool       `json:"pinned"`
	PinnedAt     *time.Time `json:"pinned_at,omitempty"`
	CreatedAt    time.Time  `json:"created_at"`
	UpdatedAt    time.Time  `json:"updated_at"`
}

Conversation is one ChatGPT-style chat thread bound to an agent key. It lives independent of the pentest exploration graph — see schema.sql §I.

type ConversationPatch

type ConversationPatch struct {
	Title  *string
	Pinned *bool
}

ConversationPatch updates only the fields whose pointers are non-nil.

type Coverage

type Coverage struct {
	Enabled     bool             `json:"enabled"`     // 资产覆盖度功能是否开启;false 时其余字段为零值
	ScopeRows   int              `json:"scope_rows"`  // 0 → 范围未锚定
	Denominator int              `json:"denominator"` // 范围内资产数
	Tested      int              `json:"tested"`      // 已测(约)
	Pct         *float64         `json:"pct"`         // 覆盖度;分母 0 时 null
	ByType      []CoverageByType `json:"by_type"`     // 按资产类型的 总数/已测
}

Coverage is a task's rough asset test coverage — a reference figure for the agent, NOT a precise metric. Denominator = assets matching any active task_scope row; Tested = those anchored to at least one fact node in the exploration.

type CoverageAsset

type CoverageAsset struct {
	ID    int64  `json:"id"`
	Type  string `json:"type"`
	Label string `json:"label"`
}

CoverageAsset is one in-scope asset (used for the untested backlog sample).

type CoverageByType

type CoverageByType struct {
	Type   string `json:"type"`
	Total  int    `json:"total"`
	Tested int    `json:"tested"`
}

CoverageByType is per-asset-type coverage: total in scope vs tested.

type CoverageGraphData

type CoverageGraphData struct {
	Nodes []CoverageGraphNode `json:"nodes"`
	Edges []CoverageGraphEdge `json:"edges"`
}

CoverageGraphData is the whole graph for one task.

type CoverageGraphEdge

type CoverageGraphEdge struct {
	Src string `json:"src"`
	Dst string `json:"dst"`
}

CoverageGraphEdge is a child→parent containment link (endpoint→service→ subdomain/ip→root_domain→company, app→company).

type CoverageGraphNode

type CoverageGraphNode struct {
	Key         string `json:"key"`
	Kind        string `json:"kind"` // company|root_domain|subdomain|ip|service|app|endpoint
	Label       string `json:"label"`
	Tested      bool   `json:"tested"`
	InScope     bool   `json:"in_scope"`
	AssetID     int64  `json:"asset_id,omitempty"` // 0 for company / synthetic root
	CompanyID   int64  `json:"company_id,omitempty"`
	Domain      string `json:"domain,omitempty"`
	RootDomain  string `json:"root_domain,omitempty"`
	IP          string `json:"ip,omitempty"`
	URL         string `json:"url,omitempty"`
	Port        int    `json:"port,omitempty"`
	ServiceType string `json:"service_type,omitempty"`
	AppName     string `json:"app_name,omitempty"`
	PageTitle   string `json:"page_title,omitempty"`
	StatusCode  int    `json:"status_code,omitempty"`
}

CoverageGraphNode is one node of the asset coverage graph. Node identity is a string key: an asset row is "a:<id>", a company is "c:<id>", and a root domain with no asset row of its own is the synthetic "r:<domain>". Out-of-scope connector nodes (roots pulled in only to link subdomains, and the companies above them) carry InScope=false and are rendered gray.

type DB

type DB struct{ *sql.DB }

DB wraps the shared *sql.DB. PG handles its own connection pool + concurrency (MVCC), so unlike the old SQLite store there is no process-wide write mutex.

func Open

func Open(dsn string) (*DB, error)

Open connects, applies the schema (idempotent), and seeds builtin rows.

func (*DB) ActiveProfile

func (d *DB) ActiveProfile() (*LLMProfile, error)

ActiveProfile returns the default (active) profile with its api key, or nil.

func (*DB) AddAgentToToolBinding

func (d *DB) AddAgentToToolBinding(agentKey string, keys []string) error

AddAgentToToolBinding adds an agent key to the given tools' `agents` arrays if not already present (idempotent). Used to give the built-in Auto agent its default toolset on existing DBs.

func (*DB) AddFinding

func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error)

AddFinding inserts a finding into the standalone findings table. taskID and nodeID may be 0 (stored as NULL). name may be "" (frontend falls back to vulnclass). Returns the new finding id.

func (*DB) AddNotificationEvent added in v0.3.15

func (d *DB) AddNotificationEvent(ctx context.Context, kind string, findingID int64, snap notify.Snapshot) (int64, error)

AddNotificationEvent 是 InsertNotificationEventTx 的独立事务版本,供不在 既有事务里的调用点使用(如渠道的「发送测试消息」,它没有真实 finding)。

func (*DB) AgentBindingCounts

func (d *DB) AgentBindingCounts() (mcp map[int64]int, skill map[int64]int, tools map[string]int, err error)

AgentBindingCounts returns per-agent binding counts in a few grouped queries (NO N+1): visible MCP servers and skills keyed by agent id, and bound tools keyed by agent key (tools.agents is a JSONB array of agent keys). Missing keys mean zero. Used to show "MCP N · Skill N · 工具 N" on the agent cards.

func (*DB) AgentSkillNames

func (d *DB) AgentSkillNames(agentID int64) ([]string, error)

AgentSkillNames returns the skill directory names visible to an agent.

func (*DB) AgentVisible

func (d *DB) AgentVisible(agentID int64, kind string) ([]int64, error)

AgentVisible returns the resource ids of a kind visible to an agent.

func (*DB) AppendConvActivity

func (d *DB) AppendConvActivity(convID int64, a Activity) (int64, error)

AppendConvActivity records one step of a conversation (human message or an agent execution step) and returns its id. Mirrors ExplorationStore.AppendActivity but keyed by conversation_id. Reuses the Activity struct (NodeID is ignored here).

func (*DB) AppendTaskArchiveWarning

func (d *DB) AppendTaskArchiveWarning(id int64, warning string) error

func (*DB) ArchivedAggregateStats

func (d *DB) ArchivedAggregateStats() ([]json.RawMessage, error)

ArchivedAggregateStats returns compact summaries used by global dashboards so cold data does not disappear from historical totals.

func (*DB) Assets

func (d *DB) Assets() *AssetStore

Assets returns the asset store.

func (*DB) BuildFindingAssetTree

func (d *DB) BuildFindingAssetTree(f FindingFilter) (*FindingAssetTree, error)

BuildFindingAssetTree 按当前筛选构建资产树。AssetScope 自身不参与(否则树会随 选中节点塌缩成一条链)。

func (*DB) ClaimDigestBatch added in v0.3.15

func (d *DB) ClaimDigestBatch(ctx context.Context, channelID int64, limit int, lease time.Duration) ([]*NotificationDelivery, error)

ClaimDigestBatch 领取某渠道当前到期的待发投递,作为一个汇总批次, 单批最多 MaxDigestBatchSize 条。

同批次的所有投递共享 batch_id,用集合里的最小 id 作批次号(稳定、可读、 无需额外序列)。重试时用 COALESCE 保留原批次号,使「这批 N 条是一起发的」 在多次重试后依然成立。

按 id 升序取前 N 条而非随机取:最早产生的投递最先发出去,积压时不会出现 「新漏洞先发、老漏洞永远排在后面」的饥饿。

func (*DB) ClaimRealtimeDeliveries added in v0.3.15

func (d *DB) ClaimRealtimeDeliveries(ctx context.Context, channelID int64, limit int, lease time.Duration) ([]*NotificationDelivery, error)

ClaimRealtimeDeliveries 领取某渠道一批到期的实时投递,最多 limit 条。

刻意按**单个渠道**领取而不是「全局领一批再挑着发」:限流闸在投递引擎里按渠道 维护,只有先知道这个渠道这一轮还能发几条、再去领同样多的行,限流才不会消耗 重试次数。若反过来先领后弃,被限流挡下的行已经被计过一次 attempts, 3 次预算会被纯粹的等待耗光,最后落进 failed。

条件含「租约已过期的 sending」——那是崩溃自愈的落点。lease 必须显著大于单次 投递的最坏耗时(渠道 HTTP 客户端超时 15 秒),否则同一行会被两个 dispatcher 同时投递。同时挡掉已停用渠道:停用操作已把存量投递标记为 skipped, 这里再拦一道,避免停用与领取并发时的漏网。

func (*DB) ClaimTaskArchiveJob

func (d *DB) ClaimTaskArchiveJob(ctx context.Context) (*TaskArchive, error)

ClaimTaskArchiveJob claims one persistent FIFO item for the single archive worker. It returns nil when the queue is empty.

func (*DB) ClearLLMHealth

func (d *DB) ClearLLMHealth(profileID int64) error

ClearLLMHealth drops one profile's state (manual "recover now" from the UI).

func (*DB) ClearSideHistory

func (d *DB) ClearSideHistory(ctx context.Context, key string) error

func (*DB) Companies

func (d *DB) Companies() *CompanyStore

Companies returns the company store.

func (*DB) CompleteIntercept

func (d *DB) CompleteIntercept(id int64, runID, toolUseID, status, output string, truncated bool) error

CompleteIntercept only updates the exact recorded call after it was allowed. A blocked tool_result must never be presented as a failed execution.

func (*DB) CompleteTaskArchive

func (d *DB) CompleteTaskArchive(
	archiveID int64,
	snapshot *TaskArchiveSnapshot,
	archivePath, sha256 string,
	originalSize, compressedSize int64,
) error

CompleteTaskArchive performs the hot-store compaction only after the external package has been fully written and checksummed. The task/exploration rows remain as minimal ID stubs; all heavyweight task-owned rows move into the package.

func (*DB) CompleteTaskArchiveRestore

func (d *DB) CompleteTaskArchiveRestore(archiveID int64) error

CompleteTaskArchiveRestore removes compact metadata after every external component has been verified and the package has been consumed.

func (*DB) ConvActivityDetail

func (d *DB) ConvActivityDetail(convID, id int64) (string, error)

ConvActivityDetail lazily returns the full detail blob for one step.

func (*DB) ConvActivityList

func (d *DB) ConvActivityList(convID, sinceID int64, limit int) ([]Activity, int64, error)

ConvActivityList returns a conversation's steps after sinceID (exclusive) with the summary-only column set (detail is lazy-loaded via ConvActivityDetail).

func (*DB) ConvActivityPage

func (d *DB) ConvActivityPage(convID, before int64, limit int) ([]Activity, bool, error)

ConvActivityPage returns one page for reverse (newest-first) pagination: up to `limit` steps ending before id `before` (exclusive; before<=0 = the latest page), returned in ASCENDING id order. hasMore reports whether still-older steps exist before the returned window, so the client can stop loading earlier history on scroll-up. Summary-only columns (detail is lazy-loaded via ConvActivityDetail).

func (*DB) ConversationTokenSummaries

func (d *DB) ConversationTokenSummaries() ([]ConvTokenSummary, error)

ConversationTokenSummaries returns one row per conversation with its summed result-row token usage (0 for conversations with no completed run yet).

func (*DB) CreateAgent

func (d *DB) CreateAgent(key, name, description string) (*Agent, error)

CreateAgent inserts a custom (builtin=false) conversational agent with role 'assistant'. Returns the new row. Callers validate key/name upstream; the DB enforces key charset + uniqueness and the role check constraint.

func (*DB) CreateAssetInterceptRule

func (d *DB) CreateAssetInterceptRule(kind, pattern, note string, enabled bool) (AssetInterceptRule, error)

CreateAssetInterceptRule inserts a new user rule (builtin is always false here).

func (*DB) CreateConversation

func (d *DB) CreateConversation(agentKey, title string, llmProfileID *int64) (*Conversation, error)

CreateConversation opens a new chat thread for agentKey with an initial title. llmProfileID may be nil to use the globally active profile.

func (*DB) CreateCustomTool

func (d *DB) CreateCustomTool(t *Tool) error

CreateCustomTool inserts a user-defined tool (system=false) with an execution spec. Fails if the key already exists.

func (*DB) CreateDecidedIntercept

func (d *DB) CreateDecidedIntercept(ruleID, convID int64, taskID, agentName, toolName string, input []byte, status, reason string, audits ...*InterceptAudit) (int64, error)

CreateDecidedIntercept inserts an intercept_pending row ALREADY in a final state (status = 'allowed' | 'denied'), decided_at stamped now. Used to log allow/deny rule matches for observability — they don't block and need no user action, so unlike CreateInterceptPending (which starts 'pending') this records the outcome directly.

func (*DB) CreateExploration

func (d *DB) CreateExploration(description, goal string) (int64, error)

CreateExploration creates a new exploration root and returns its id.

func (*DB) CreateFindingRetest

func (d *DB) CreateFindingRetest(ctx context.Context, findingID int64, notes string) (*FindingRetest, *Conversation, bool, error)

CreateFindingRetest atomically snapshots the source, creates its conversation and persists the first message. A finding row lock deduplicates simultaneous clicks across clients; an existing active run is returned without dispatching.

func (*DB) CreateInterceptPending

func (d *DB) CreateInterceptPending(ruleID, convID int64, taskID, agentName, toolName string, input []byte, reason string, audits ...*InterceptAudit) (int64, error)

CreateInterceptPending inserts a pending approval record and returns its ID. convID == 0 → conversation_id stored as NULL (background task). taskID == "" → task_id stored as NULL.

func (*DB) CreateInterceptRule

func (d *DB) CreateInterceptRule(name, matchTarget, matchType, pattern, action, message string, priority int, enabled bool, timeoutEnabled bool, timeoutSeconds int, timeoutAction string) (InterceptRule, error)

CreateInterceptRule inserts a new rule.

func (*DB) CreateTask

func (d *DB) CreateTask(description, goal string, llmProfileID *int64, timeoutSeconds, planHeartbeatSeconds int) (*Task, error)

func (*DB) CreateTaskCategory

func (d *DB) CreateTaskCategory(name string) (*TaskCategory, error)

func (*DB) CreateTaskTemplate

func (d *DB) CreateTaskTemplate(in TaskTemplateInput) (*TaskTemplate, error)

CreateTaskTemplate inserts one globally reusable preset.

func (*DB) CreateTaskWithOptions

func (d *DB) CreateTaskWithOptions(description, goal string, opts TaskCreateOptions) (*Task, error)

CreateTaskWithOptions creates an exploration, task, direct source relations, and the ordered task LLM chain in one transaction.

func (*DB) CreateTrigger

func (d *DB) CreateTrigger(t *AgentTrigger) (*AgentTrigger, error)

CreateTrigger inserts a trigger for agentKey and returns it.

func (*DB) CurrentPrompt

func (d *DB) CurrentPrompt(agentID int64) (string, error)

CurrentPrompt returns the agent's active template text ("" if none set yet).

func (*DB) CurrentSideRequest

func (d *DB) CurrentSideRequest(ctx context.Context, key string) (*sidequestion.Exchange, error)

func (*DB) DecideInterceptPending

func (d *DB) DecideInterceptPending(id int64, status string) error

DecideInterceptPending updates a pending record's status (allowed/denied/timeout).

func (*DB) DeferDeliveries added in v0.3.15

func (d *DB) DeferDeliveries(ctx context.Context, ids []int64, reason string) error

DeferDeliveries 把一批投递退回 pending、立即可再领,并**撤销领取时计的那一次尝试**。

用途只有一个:汇总消息按渠道长度上限分段发送时,没装进本条的条目要留到下一批。 那不是失败,所以不该消耗重试预算——领取时 attempts 已经乐观地 +1 了, 这里必须减回去。否则一个 500 条的积压会按每段 20 条切成 25 段, 尾部条目在第 3 段就被 MaxNotifyAttempts 判成 failed,而它们从未出过任何错。

GREATEST(...,0) 兜住「有人手工重发把 attempts 清零后又走到这里」的情况, 不让计数变成负数。

func (*DB) DeleteAgent

func (d *DB) DeleteAgent(key string) error

DeleteAgent removes a custom agent. Built-in agents are protected by the builtin=false guard. agent_prompts / agent_prompt_vars / visibility rows cascade via FK; tools.agents bindings for the key are cleaned by the caller.

func (*DB) DeleteAssetInterceptRule

func (d *DB) DeleteAssetInterceptRule(id int64) error

DeleteAssetInterceptRule removes a rule (built-in rules are deletable too).

func (*DB) DeleteConversation

func (d *DB) DeleteConversation(id int64) error

DeleteConversation removes a thread; its activities cascade via FK.

func (*DB) DeleteConversations

func (d *DB) DeleteConversations(ids []int64) ([]int64, error)

DeleteConversations removes existing threads in one statement and returns the ids that were actually present. Child activities and trigger runs cascade.

func (*DB) DeleteCustomTool

func (d *DB) DeleteCustomTool(key string) error

DeleteCustomTool removes a custom tool (system=false only; built-ins protected).

func (*DB) DeleteFinding

func (d *DB) DeleteFinding(id int64) (n int64, err error)

DeleteFinding removes a finding entirely: the standalone findings row and its originating exploration node (kind='finding'), so it disappears from the findings list, the per-task 发现 Tab, and the exploration graph alike. Deleting the node cascades its edges + node_assets and nulls any activity referencing it. Returns rows affected (0 = no finding with that id).

func (*DB) DeleteFindingsByTask

func (d *DB) DeleteFindingsByTask(taskID int64) (int64, error)

DeleteFindingsByTask removes all findings rows of a task. The originating exploration finding nodes are cascade-deleted separately when the task's exploration subgraph is dropped. Returns rows deleted.

func (*DB) DeleteInterceptRule

func (d *DB) DeleteInterceptRule(id int64) error

DeleteInterceptRule removes a rule.

func (*DB) DeleteLLMRecords

func (d *DB) DeleteLLMRecords(task string) (int64, error)

DeleteLLMRecords removes every LLM record for one exact task_id — the same match the page's task picker/filter uses. Returns rows deleted.

func (*DB) DeleteMCP

func (d *DB) DeleteMCP(id int64) error

func (*DB) DeleteNotificationChannel added in v0.3.15

func (d *DB) DeleteNotificationChannel(ctx context.Context, id int64) error

DeleteNotificationChannel 删除渠道。其投递历史随外键级联删除 (渠道配置都没了,历史无从解读)。

func (*DB) DeleteProfile

func (d *DB) DeleteProfile(id int64) error

func (*DB) DeleteProfileContext

func (d *DB) DeleteProfileContext(ctx context.Context, id int64) error

DeleteProfileContext removes a non-default profile while preserving the task failover cursor. Reference changes are retried because a task, agent, or conversation may start pointing at the profile between the initial scan and the profile row lock. A bound prevents a continuously changing workload from keeping an HTTP request alive forever.

func (*DB) DeleteSkillVisibility

func (d *DB) DeleteSkillVisibility(skillName string) error

DeleteSkillVisibility removes all visibility rows for a skill (called on skill delete).

func (*DB) DeleteTask

func (d *DB) DeleteTask(id int64) error

DeleteTask preserves the historical behavior: remove the task and its exploration subgraph while retaining global assets.

func (*DB) DeleteTaskArchiveStub

func (d *DB) DeleteTaskArchiveStub(archiveID int64) error

DeleteTaskArchiveStub permanently removes the cold task after its package has been staged for deletion. Dependency protection is rechecked transactionally.

func (*DB) DeleteTaskCascade

func (d *DB) DeleteTaskCascade(id int64, deleteAssets, deleteFindings bool, deleteLLMRecords ...bool) (TaskDeleteResult, error)

DeleteTaskCascade hard-deletes a task and its exploration subgraph (nodes/edges/activity via ON DELETE CASCADE). Optional standalone findings and assets owned only by this task are deleted in the same transaction; shared assets are retained and only have this task id detached. deleteLLMRecords is optional to preserve callers of the original two-option API.

func (*DB) DeleteTaskCascadePrepared

func (d *DB) DeleteTaskCascadePrepared(
	id int64,
	deleteAssets, deleteFindings, deleteLLMRecords bool,
	prepare func(TaskDeletePreparation) error,
) (TaskDeleteResult, error)

DeleteTaskCascadePrepared coordinates reversible external deletion with the PostgreSQL cascade. When prepare is non-nil, host ownership is resolved and prepare is invoked inside this transaction while asset and anchor writers are excluded. The locks remain held through commit, closing the window where a host could become shared after its traffic had already been staged.

prepare must only stage reversible work. Any returned error or later database error rolls PostgreSQL back; the caller remains responsible for rolling back external work that its callback staged successfully.

func (*DB) DeleteTaskCategory

func (d *DB) DeleteTaskCategory(id int64) (bool, error)

DeleteTaskCategory moves affected tasks to the uncategorized bucket through the tasks.category_id ON DELETE SET NULL foreign key.

func (*DB) DeleteTaskTemplate

func (d *DB) DeleteTaskTemplate(id int64) (bool, error)

DeleteTaskTemplate deletes one preset and reports whether it existed.

func (*DB) DeleteTrigger

func (d *DB) DeleteTrigger(id int64) error

DeleteTrigger removes a trigger.

func (*DB) DeleteTriggersForAgent

func (d *DB) DeleteTriggersForAgent(agentKey string) error

DeleteTriggersForAgent removes all triggers of an agent (custom agent delete).

func (*DB) Dequeue

func (d *DB) Dequeue(id int64, clearMode bool) error

Dequeue removes the concurrency hold. clearMode is false when a user pauses a queued task, preserving whether its next admission must bootstrap or resume.

func (*DB) DigestBatchDue added in v0.3.15

func (d *DB) DigestBatchDue(ctx context.Context, channelID int64, minAge time.Duration) (bool, error)

DigestBatchDue 报告该渠道是否已攒够一个到期批次:存在待发投递,且**最老的那条** 年龄已达到汇总周期。

判定依据是最老投递的年龄而非墙上时钟:这样刚建好的渠道不会因为对齐到整点而 立刻吐出一条只有一条的「汇总」,积压很久的批次也不会再白等一轮。

与 ClaimDigestBatch 分开是因为语义不同:本函数只回答「该不该发」, 而领取要拿走该渠道**全部**待发行(包括尚未满年龄的那些)——否则一个周期 会被拆成多条消息,汇总就失去意义了。

func (*DB) EditFindingTraffic

func (d *DB) EditFindingTraffic(ctx context.Context, findingID, bindingID, version int64, role, note *string, remove bool, order []int64) error

func (*DB) Enqueue

func (d *DB) Enqueue(id int64, mode string) error

Enqueue places a task at the tail of the persistent admission queue. Repeating the operation while it is already queued keeps its original FIFO position.

func (*DB) EnsureLLMRecordsTable

func (d *DB) EnsureLLMRecordsTable() error

EnsureLLMRecordsTable creates the llm_records table if it does not exist.

func (*DB) EnsureLLMUsageTable

func (d *DB) EnsureLLMUsageTable() error

EnsureLLMUsageTable creates the llm_usage metering table if it does not exist.

func (*DB) ExistingSideRequest

func (d *DB) ExistingSideRequest(ctx context.Context, key, client string) (*sidequestion.Exchange, error)

func (*DB) Exploration

func (d *DB) Exploration(id int64) *ExplorationStore

Exploration returns a handle bound to an exploration id.

func (*DB) ExplorationDiag

func (d *DB) ExplorationDiag(expID int64) (expExists bool, taskRefs int, maxExpID int64, err error)

ExplorationDiag probes, at the moment of an activity FK violation (23503), why the parent row is unreachable. It reports whether the exploration row still exists, how many task rows reference it (a live task should keep exactly 1 — and RESTRICT on tasks.exploration_id means the exploration CANNOT be deleted while that row lives), and the current MAX(explorations.id). Together these tell apart the failure modes:

  • expExists=false, taskRefs=0 → the whole row set is gone (DB reset / wrong DB).
  • expExists=false, taskRefs=1 → impossible under RESTRICT; would mean a broken FK.
  • expExists=true → the write's expID is NOT this exploration (stale/ wrong id in the in-memory store); compare against Store.ID().

func (*DB) FailDeliveries added in v0.3.15

func (d *DB) FailDeliveries(ctx context.Context, ids []int64, errMsg string) error

FailDeliveries 把一批投递标记为最终失败,等待人工在投递历史里重发。

func (*DB) FailPendingRetestForConversation

func (d *DB) FailPendingRetestForConversation(conversationID int64, reason string) error

FailPendingRetestForConversation seals a conversation's unfinished retest when the runner could not even load it — the retest ID is unknown on that path, so the conversation ID is the only handle. Without it a transient read error leaves the row 'pending' forever: the findings list keeps showing 复测中 and every later 发起复测 is deduped against a run that is not happening, with only a process restart (RecoverFindingRetests) able to clear it.

func (*DB) FailTaskArchiveJob

func (d *DB) FailTaskArchiveJob(id int64, activeState string, cause error) error

func (*DB) FanOutPendingEvents added in v0.3.15

func (d *DB) FanOutPendingEvents(ctx context.Context, limit int) (eventCount, deliveryCount int, err error)

FanOutPendingEvents 把尚未分派的漏洞事件按当前启用的渠道展开成投递任务, 返回本轮处理的事件数与新建的投递数。

整轮操作在一个事务里:事件用 FOR UPDATE SKIP LOCKED 领取,多个进程同时跑 也各自领到不同的行(项目里归档队列的领取用的是同一套手法,见 db/task_archives.go 的 completeNextArchiveJob)。

过滤匹配刻意放在 Go 侧而非 SQL:渠道的过滤条件是一组可选字段的 JSONB, 用 SQL 表达六种组合的匹配会让查询难以维护,而渠道数量是「人手配的几条」, 全量加载后在内存里逐条比对更快也更好测。

未命中任何渠道的事件同样会被标记 fanned_out ——否则它会永远留在待分派集合里, 每个 tick 被重扫一遍。

func (*DB) FindingIDByNodeID

func (d *DB) FindingIDByNodeID(nodeID int64) (id int64, err error)

func (*DB) FindingMetaByNodeID

func (d *DB) FindingMetaByNodeID(taskID int64) (map[int64]FindingMeta, error)

FindingMetaByNodeID maps a task's finding node ids to their standalone-row metadata, so the per-task view (which reads exploration nodes) can show and edit the same status — and the same anchored assets — as the global 发现 page.

func (*DB) FindingRetestForConversation

func (d *DB) FindingRetestForConversation(ctx context.Context, conversationID int64) (*FindingRetest, error)

func (*DB) FindingStats

func (d *DB) FindingStats() (*FindingStats, error)

FindingStats returns whole-table counts (by severity + pending) and the sorted set of distinct vuln classes.

func (*DB) FinishFindingRetest

func (d *DB) FinishFindingRetest(id int64, status, reason string) error

FinishFindingRetest seals the result. Cancellation/failure takes precedence over a staged verdict so an interrupted test cannot appear successfully fixed. Only a newly completed fixed verdict updates triage, in the same transaction.

func (*DB) GetAgentByKey

func (d *DB) GetAgentByKey(key string) (*Agent, error)

func (*DB) GetBool

func (d *DB) GetBool(key string, def bool) bool

GetBool returns the boolean setting, or def when unset/unparseable.

func (*DB) GetConversation

func (d *DB) GetConversation(id int64) (*Conversation, error)

GetConversation returns one thread (nil, nil if absent).

func (*DB) GetFinding

func (d *DB) GetFinding(id int64) (*DBFinding, error)

GetFinding returns a single finding row (with task_description joined and the full Markdown report), or nil when no row has that id. Unlike the list queries it also selects `report` — that column is only needed on the detail page.

func (*DB) GetFindingTraffic

func (d *DB) GetFindingTraffic(ctx context.Context, findingID int64) (out *FindingTraffic, err error)

func (*DB) GetInterceptDetail

func (d *DB) GetInterceptDetail(id int64) (*InterceptDetail, error)

func (*DB) GetInterceptExecution

func (d *DB) GetInterceptExecution(id int64) (*InterceptExecution, error)

func (*DB) GetInterceptPending

func (d *DB) GetInterceptPending(id int64) (*InterceptPending, error)

GetInterceptPending returns one pending record (nil if absent).

func (*DB) GetLLMRecord

func (d *DB) GetLLMRecord(id int64) (*LLMRecord, error)

GetLLMRecord returns a single LLM record with full request/response bodies.

func (*DB) GetSchedState

func (d *DB) GetSchedState(key string) (string, error)

func (*DB) GetSetting

func (d *DB) GetSetting(key string) (value string, ok bool, err error)

GetSetting returns the stored value and ok=false when the key is unset.

func (*DB) GetTask

func (d *DB) GetTask(id int64) (*Task, error)

GetTask returns one alive task (nil if not found/deleted).

func (*DB) GetTaskArchive

func (d *DB) GetTaskArchive(id int64) (*TaskArchive, error)

func (*DB) GetTaskArchiveByTask

func (d *DB) GetTaskArchiveByTask(taskID int64) (*TaskArchive, error)

func (*DB) GetTaskCategory

func (d *DB) GetTaskCategory(id int64) (*TaskCategory, error)

func (*DB) GetTaskTemplate

func (d *DB) GetTaskTemplate(id int64) (*TaskTemplate, error)

GetTaskTemplate returns nil when id does not exist.

func (*DB) GetTool

func (d *DB) GetTool(key string) (*Tool, error)

GetTool fetches one tool row (nil if absent).

func (*DB) GoalCountsAll

func (d *DB) GoalCountsAll() (map[int64]GoalCounts, error)

GoalCountsAll returns goal totals (total / met) per exploration id, one query for all tasks — used by listTasks so the task list shows progress for every task.

func (*DB) InsertLLMRecord

func (d *DB) InsertLLMRecord(r *LLMRecord) error

InsertLLMRecord stores one LLM call record.

func (*DB) InsertLLMUsage

func (d *DB) InsertLLMUsage(u *LLMUsage) error

InsertLLMUsage appends one metering row. Best-effort: callers log and continue on error (a lost metering row must never break the LLM call).

func (*DB) InsertLog

func (d *DB) InsertLog(level, tag, text string) (int64, error)

InsertLog appends one log line and returns its auto-assigned id.

func (*DB) InsertSettingIfAbsent added in v0.3.15

func (d *DB) InsertSettingIfAbsent(key, value string) (inserted bool, err error)

InsertSettingIfAbsent 只在 key 尚未存在时写入,已存在则原样保留并返回 inserted=false。 给 auth.password_hash 这类"只允许首次设置"的键用:判定落在数据库的主键约束上, 调用方先 GetSetting 再写的那种检查就只是快速失败路径——读出错或并发撞车时, 已有的值也不会被 ON CONFLICT DO UPDATE 顺手覆盖掉。

func (*DB) InsertSkillUsage

func (d *DB) InsertSkillUsage(u *SkillUsage) error

InsertSkillUsage appends one ledger row. Best-effort: callers log and continue on error (a lost metering row must never break a skill invocation).

func (*DB) InsertToolUsage

func (d *DB) InsertToolUsage(u *ToolUsage) error

InsertToolUsage appends one ledger row. Runtime callers treat metering as best-effort so a statistics failure never interrupts the tool itself.

func (*DB) InterruptSideRequests

func (d *DB) InterruptSideRequests(ctx context.Context) error

func (*DB) IsTaskArchiveRestored

func (d *DB) IsTaskArchiveRestored(id int64) (bool, error)

func (*DB) JudgeUsageStats added in v0.3.15

func (d *DB) JudgeUsageStats(days int) (JudgeUsage, error)

JudgeUsageStats returns the fallback judge's all-time token totals and a per-day series over the past `days` days (default 30). Sourced from the always-on llm_usage ledger, so it is accurate across pool rotation and interrupted/failed judge calls. days only bounds the daily series; totals are all-time.

func (*DB) LLMRetryPolicy

func (d *DB) LLMRetryPolicy() LLMRetryPolicy

LLMRetryPolicy reads the global retry policy. A missing or unparseable value yields the zero policy — i.e. every layer on its built-in default.

func (*DB) LLMTasks

func (d *DB) LLMTasks() ([]LLMTask, error)

LLMTasks returns distinct non-empty task_ids with record counts, most recent first — powers the LLM-records page's task picker.

func (*DB) LastActivityAll

func (d *DB) LastActivityAll() (map[int64]int64, error)

LastActivityAll returns the unix time of the most recent activity per exploration (exploration_id → max created_at epoch), one query for all tasks. Persisted (unlike Engine.LastActivity's in-memory map), so it survives restarts and gives终态任务 a stable "ran until" time for computing run duration.

func (*DB) ListActiveFindingRetests

func (d *DB) ListActiveFindingRetests(ctx context.Context) ([]ActiveFindingRetest, error)

func (*DB) ListAgents

func (d *DB) ListAgents() ([]*Agent, error)

func (*DB) ListAllIntercepts

func (d *DB) ListAllIntercepts(limit int) ([]InterceptApprovalRow, error)

ListAllIntercepts returns up to limit intercept_pending rows (newest first) joined with conversation and rule info.

func (*DB) ListAllInterceptsPage

func (d *DB) ListAllInterceptsPage(page, size int, filter InterceptApprovalFilter) ([]InterceptApprovalRow, int, error)

ListAllInterceptsPage returns one 1-based page and the total matching count.

func (*DB) ListAssetInterceptRules

func (d *DB) ListAssetInterceptRules() ([]AssetInterceptRule, error)

ListAssetInterceptRules returns all rules, built-ins first then newest first.

func (*DB) ListCommands

func (d *DB) ListCommands(expID *int64, q string, page, size int) ([]CommandRecord, int, error)

ListCommands returns tool executions (tool_use + paired tool_result) across all explorations, with optional filtering and pagination. Covers every tool, not just Bash; q matches the tool name or its input.

func (*DB) ListConversations

func (d *DB) ListConversations() ([]*Conversation, error)

ListConversations returns all threads, most-recently-updated first.

func (*DB) ListCustomTools

func (d *DB) ListCustomTools() ([]*Tool, error)

ListCustomTools returns only the user-defined (system=false) tools.

func (*DB) ListEnabledTriggers

func (d *DB) ListEnabledTriggers() ([]*AgentTrigger, error)

ListEnabledTriggers returns all enabled triggers (for the scheduler).

func (*DB) ListFindingGroups

func (d *DB) ListFindingGroups(f FindingFilter, page, pageSize int) ([]FindingGroup, int, int, error)

ListFindingGroups returns a page of task groups matching the same filters as ListFindingsPage. The group count and finding count are independent totals so clients can page groups without losing the exact export/selection count.

func (*DB) ListFindingRetests

func (d *DB) ListFindingRetests(findingID int64) ([]*FindingRetest, error)

func (*DB) ListFindings

func (d *DB) ListFindings(limit int) ([]*DBFinding, error)

ListFindings returns all findings (newest first), joined with task description. Kept for the dashboard's summary; the paginated 发现 page uses ListFindingsPage.

func (*DB) ListFindingsForExport

func (d *DB) ListFindingsForExport(f FindingFilter, ids []int64) ([]*DBFinding, error)

ListFindingsForExport returns findings for the 发现 page 导出功能,携带完整 report 字段、不分页。ids 非空时按这批 finding id 精确导出(勾选导出),忽略 filter;ids 为空时按 filter 导出(导出当前筛选/全部)。结果按严重等级降序、 再按时间倒序,与「导出汇总报告」的分组顺序一致。

func (*DB) ListFindingsPage

func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding, int, error)

ListFindingsPage returns one page of findings matching the filter, plus the total count of matching rows (for the frontend pager). page is 1-based.

func (*DB) ListInterceptRules

func (d *DB) ListInterceptRules() ([]InterceptRule, error)

ListInterceptRules returns all rules ordered by priority DESC then id.

func (*DB) ListLLMRecords

func (d *DB) ListLLMRecords(model, session, task string, page, size int) ([]LLMRecord, int, error)

ListLLMRecords returns paginated LLM records with optional filters.

func (*DB) ListLogsBefore

func (d *DB) ListLogsBefore(beforeID int64, limit int) ([]*DBLog, error)

ListLogsBefore returns up to limit rows with id < beforeID, oldest-first.

func (*DB) ListMCP

func (d *DB) ListMCP() ([]*MCPServer, error)

func (*DB) ListNotificationChannels added in v0.3.15

func (d *DB) ListNotificationChannels(ctx context.Context) ([]*NotificationChannel, error)

ListNotificationChannels 返回全部渠道实例,启用的排在前面、同级按 id。 排序放在 SQL 里是为了让 UI 与 dispatcher 看到同一个稳定顺序。

func (*DB) ListNotificationDeliveries added in v0.3.15

func (d *DB) ListNotificationDeliveries(ctx context.Context, f NotificationDeliveryFilter, page, pageSize int) ([]*NotificationDelivery, int, error)

ListNotificationDeliveries 分页返回投递历史,新的在前。

func (*DB) ListPendingIntercepts

func (d *DB) ListPendingIntercepts() ([]InterceptPending, error)

ListPendingIntercepts returns all unresolved approval requests, newest first.

func (*DB) ListProfiles

func (d *DB) ListProfiles() ([]*LLMProfile, error)

func (*DB) ListPromptVersions

func (d *DB) ListPromptVersions(agentID int64) ([]PromptVersion, error)

func (*DB) ListTaskArchives

func (d *DB) ListTaskArchives(search, state string, page, size int) (TaskArchivePage, error)

func (*DB) ListTaskCategories

func (d *DB) ListTaskCategories() ([]*TaskCategory, error)

func (*DB) ListTaskIntercepts

func (d *DB) ListTaskIntercepts(taskID string) ([]InterceptApprovalRow, error)

ListTaskIntercepts returns all intercept_pending rows for a specific task (newest first).

func (*DB) ListTaskInterceptsPage

func (d *DB) ListTaskInterceptsPage(taskID string, page, size int, filter InterceptApprovalFilter) ([]InterceptApprovalRow, int, error)

ListTaskInterceptsPage is the paginated variant of ListTaskIntercepts.

func (*DB) ListTaskTemplates

func (d *DB) ListTaskTemplates() ([]*TaskTemplate, error)

ListTaskTemplates returns the most recently maintained templates first.

func (*DB) ListTasks

func (d *DB) ListTasks() ([]*Task, error)

ListTasks returns alive tasks with pinned tasks first, then newest ids.

func (*DB) ListTools

func (d *DB) ListTools() ([]*Tool, error)

ListTools returns the whole tool catalog, ordered by key.

func (*DB) ListTriggersFor

func (d *DB) ListTriggersFor(agentKey string) ([]*AgentTrigger, error)

ListTriggersFor returns an agent's triggers.

func (*DB) LoadLLMHealth

func (d *DB) LoadLLMHealth() ([]LLMHealth, error)

LoadLLMHealth returns the profiles still in an UNEXPIRED cooling-off window. Expired rows are deliberately skipped: after a restart a profile that has finished cooling should be treated as healthy again and re-probed on its next call, not resurrected as broken.

func (*DB) MCPToolNames

func (d *DB) MCPToolNames(serverID int64) ([]string, error)

MCPToolNames returns the cached tool names for a server (empty until discovered).

func (*DB) MCPToolsDetailed

func (d *DB) MCPToolsDetailed(serverID int64) ([]MCPTool, error)

MCPToolsDetailed returns the cached tools (name + description) for a server.

func (*DB) MarkDeliveriesSent added in v0.3.15

func (d *DB) MarkDeliveriesSent(ctx context.Context, ids []int64) error

MarkDeliveriesSent 把一批投递标记为已送达。

func (*DB) MarkTaskLLMProfileQuotaExhausted

func (d *DB) MarkTaskLLMProfileQuotaExhausted(taskID, profileID int64, reason string) (TaskLLMTransition, error)

MarkTaskLLMProfileQuotaExhausted advances the shared task cursor once. A late in-flight error from an older profile records that entry as exhausted but does not advance past the profile another call already selected.

func (*DB) MarkTaskLLMProfileQuotaExhaustedAtRevision

func (d *DB) MarkTaskLLMProfileQuotaExhaustedAtRevision(taskID, profileID, revision int64, reason string) (TaskLLMTransition, error)

MarkTaskLLMProfileQuotaExhaustedAtRevision applies a provider failure only when it belongs to the chain snapshot used to start that call.

func (*DB) MetGoals

func (d *DB) MetGoals() ([]TaskEvent, error)

MetGoals returns all met goals across live tasks (the scheduler filters out the ones it already fired for via the persisted fired-set).

func (*DB) MissingSkillStats

func (d *DB) MissingSkillStats(limit int) ([]SkillStat, error)

MissingSkillStats returns the skill names agents asked for that do not exist, most-requested first — the "wished it existed" gap list. Names come from the model so they are shown as-is (already length-capped at insert time).

func (*DB) NewFindingsSince

func (d *DB) NewFindingsSince(lastID int64) ([]TaskEvent, error)

NewFindingsSince returns findings with node id > lastID across all live tasks, ordered by id (monotonic watermark → no double-fire).

func (*DB) NewTasksSince

func (d *DB) NewTasksSince(lastID int64) ([]TaskEvent, error)

NewTasksSince returns tasks created with id > lastID (excluding deleted), ordered by id (monotonic watermark → no double-fire across restarts). Triggered agent runs are conversations, not tasks, so this never fires on its own output.

func (*DB) NewToolCallsSince

func (d *DB) NewToolCallsSince(lastID int64) ([]TaskEvent, error)

NewToolCallsSince returns completed tool calls (a tool_result row) with activity id > lastID across all live tasks, ordered by id (monotonic watermark → no double-fire). It is driven by tool_result rows (the tool finished, so both input and output are available) and joins back to the paired tool_use row for the input. Only task-execution activity is scanned — triggered agent runs are conversations (conversation_activities), so a tool-call trigger never fires on its own output.

func (*DB) NotificationAssetNames added in v0.3.15

func (d *DB) NotificationAssetNames(ctx context.Context, ids []int64) ([]string, error)

NotificationAssetNames 把资产 id 解析成简短展示名,供推送消息使用。

返回顺序与入参一致、长度可能小于入参(不存在的 id 被跳过)。保持入参顺序是 为了让同一条漏洞的消息在多次投递里资产顺序稳定——否则重试后收到的消息里 资产次序变了,会被误读成「资产变了」。

func (*DB) NotificationChannelByID added in v0.3.15

func (d *DB) NotificationChannelByID(ctx context.Context, id int64) (*NotificationChannel, error)

NotificationChannelByID 取单个渠道。

func (*DB) NotificationStatsSnapshot added in v0.3.15

func (d *DB) NotificationStatsSnapshot(ctx context.Context) (*NotificationStats, error)

NotificationStatsSnapshot 汇总通知系统的健康度。 BacklogAgeMS 是「推送是不是卡住了」最直接的指标——比 pending 计数有用得多, 因为积压 3 条和积压 3 条的差别可以是从 3 秒到 3 小时。

func (*DB) PatchTaskTemplate

func (d *DB) PatchTaskTemplate(id int64, patch TaskTemplatePatch) (*TaskTemplate, error)

PatchTaskTemplate atomically changes only the supplied fields. Keeping the merge in one UPDATE prevents concurrent disjoint PATCH requests from losing each other's changes.

func (*DB) PoolProfiles

func (d *DB) PoolProfiles() ([]*LLMProfile, error)

PoolProfiles returns the failover chain in run order, api keys included: the active profile first, then every other keyed profile that isn't excluded, by priority DESC (id ASC to stay stable). Profiles without an api key can't serve a request, so they never enter the chain. The ordering IS the policy — callers walk the slice front to back.

func (*DB) ProfileByID

func (d *DB) ProfileByID(id int64) (*LLMProfile, error)

ProfileByID returns one profile with its api key by id, or nil if not found. Used to run a task on a specific (non-default) LLM profile.

func (*DB) PromptVars

func (d *DB) PromptVars(agentID int64) ([]PromptVar, error)

func (*DB) QueueTaskArchive

func (d *DB) QueueTaskArchive(taskID int64) (*TaskArchive, error)

QueueTaskArchive validates lifecycle and direct inheritance while holding the task row. A failed archive can be explicitly retried through the same API.

func (*DB) QueueTaskArchiveDelete

func (d *DB) QueueTaskArchiveDelete(id int64) (*TaskArchive, error)

func (*DB) QueueTaskArchiveRestore

func (d *DB) QueueTaskArchiveRestore(id int64) (*TaskArchive, error)

func (*DB) RecentLogs

func (d *DB) RecentLogs(limit int) ([]*DBLog, error)

RecentLogs returns the most recent limit rows, oldest-first.

func (*DB) RecentSkillCalls

func (d *DB) RecentSkillCalls(skill string, limit int) ([]SkillCall, error)

RecentSkillCalls returns the most recent invocations of one skill, newest first.

func (*DB) RecordFindingRetestResult

func (d *DB) RecordFindingRetestResult(ctx context.Context, conversationID int64, verdict, summary, evidence string) error

RecordFindingRetestResult never accepts a finding ID: ownership comes from the runtime conversation. Identical retries are safe; a second verdict is refused.

func (*DB) RecoverFindingRetests

func (d *DB) RecoverFindingRetests() error

func (*DB) RecoverTaskArchiveJobs

func (d *DB) RecoverTaskArchiveJobs() error

RecoverTaskArchiveJobs keeps restore/delete resumable after an unclean shutdown. An interrupted archive requires an explicit retry: automatic startup retries can otherwise form a crash loop when the prior process was killed by resource limits.

func (*DB) RefreshToolDefaults

func (d *DB) RefreshToolDefaults(key, desc string, schema json.RawMessage) error

RefreshToolDefaults updates a system tool's model-facing description + schema to the code defaults, PRESERVING the user's agent binding + enabled flag. Used by one-time migrations to propagate a code schema change (SeedTool is first-insert-only, so a new parameter added in code otherwise never reaches an already-seeded row). No-op for custom tools or unknown keys.

func (*DB) RemoveAgentFromTool

func (d *DB) RemoveAgentFromTool(agentKey, toolKey string) error

RemoveAgentFromTool strips one agent key from a SINGLE tool's `agents` array — used by one-time migrations that change a tool's default binding on existing DBs (SeedTool is first-insert-only, so a changed default never reaches a seeded row).

func (*DB) RemoveAgentFromToolBindings

func (d *DB) RemoveAgentFromToolBindings(agentKey string) error

RemoveAgentFromToolBindings strips an agent key from every tool's `agents` JSONB array — called when a custom agent is deleted so no tool keeps a dangling binding. Uses jsonb `-` (remove array element) guarded by `?` (membership).

func (*DB) RenameConversation

func (d *DB) RenameConversation(id int64, title string) error

RenameConversation sets a thread's title. Kept for automatic first-message titles and compatibility with existing callers.

func (*DB) RenameTaskCategory

func (d *DB) RenameTaskCategory(id int64, name string) (*TaskCategory, error)

func (*DB) ReplaceTaskLLMProfiles

func (d *DB) ReplaceTaskLLMProfiles(taskID int64, profileIDs []int64, activeProfileID int64) error

ReplaceTaskLLMProfiles atomically replaces and resets the explicit task chain. activeProfileID=0 selects the first entry. An empty list restores the existing agent-binding/global fallback behavior. 终态(done/failed/timeout)任务同样允许改链:任务结束后主 Agent 对话仍会走这条链, 链上模型不可用时必须能换,否则已完成任务就再也没法交互了。

func (*DB) RescheduleDeliveries added in v0.3.15

func (d *DB) RescheduleDeliveries(ctx context.Context, ids []int64, delay time.Duration, errMsg string) error

RescheduleDeliveries 把一批投递退回 pending 并推后重试时间。

退回 pending 而不是引入新的中间状态,是为了让「还剩几次机会」只由一个地方 表达(MaxNotifyAttempts),避免状态机的分支随重试策略膨胀。

func (*DB) ResetPromptToDefault

func (d *DB) ResetPromptToDefault(agentID int64, tmpl string) (int, error)

ResetPromptToDefault appends the code-default template as a new version and points current at it — the explicit "恢复为内置默认" action.

func (*DB) ResolveIntercept

func (d *DB) ResolveIntercept(id int64, status, action, reason string) (bool, error)

ResolveIntercept atomically settles a pending request. A timeout cannot overwrite a human decision and repeat decisions cannot rewrite history.

func (*DB) ResourceAgents

func (d *DB) ResourceAgents(kind string, resourceID int64) ([]int64, error)

ResourceAgents returns the agent ids that can see a resource.

func (*DB) RestoreTaskArchive

func (d *DB) RestoreTaskArchive(archiveID int64, snapshot *TaskArchiveSnapshot, remainingTimeoutSeconds int64) ([]string, error)

RestoreTaskArchive restores PostgreSQL rows from a verified manifest. It is idempotent for accounting/traffic retry scenarios and returns non-fatal warnings for global objects that intentionally are not recreated.

func (*DB) RestoreTaskArchiveWithLLMRecords

func (d *DB) RestoreTaskArchiveWithLLMRecords(
	archiveID int64,
	snapshot *TaskArchiveSnapshot,
	remainingTimeoutSeconds int64,
	llmRecords io.Reader,
) ([]string, error)

RestoreTaskArchiveWithLLMRecords restores a v2 package whose heavyweight LLM record history is stored as a sequence of JSON objects outside manifest.json.

func (*DB) RetryNotificationDelivery added in v0.3.15

func (d *DB) RetryNotificationDelivery(ctx context.Context, id int64) error

RetryNotificationDelivery 手动重发一条投递:重置为 pending、清零重试计数、 立即到期。清计数是刻意的——人工点「重发」意味着前几次失败的原因已被处理, 再拿旧计数限制它没有道理。

func (*DB) SaveLLMHealth

func (d *DB) SaveLLMHealth(h LLMHealth) error

SaveLLMHealth upserts one profile's circuit-breaker state.

func (*DB) SaveMCP

func (d *DB) SaveMCP(m *MCPServer) (int64, error)

func (*DB) SaveMCPTools

func (d *DB) SaveMCPTools(serverID int64, tools []MCPTool) error

SaveMCPTools replaces the cached tool list for a server (called after discovery).

func (*DB) SaveNotificationChannel added in v0.3.15

func (d *DB) SaveNotificationChannel(ctx context.Context, c *NotificationChannel) (int64, error)

SaveNotificationChannel 新建或更新一个渠道。

更新时只覆盖调用方显式给出的字段(非 nil / 非空),这样前端可以提交局部 修改的抽屉表单,而不必回传 config 里那些它没展示的字段——回传反而会造成 「掩码值把真实密钥覆盖掉」的事故。

func (*DB) SaveProfile

func (d *DB) SaveProfile(p *LLMProfile) (int64, error)

SaveProfile inserts (id==0) or updates a profile. Empty apiKey on update keeps existing.

func (*DB) SavePrompt

func (d *DB) SavePrompt(agentID int64, template, note, by string) (int, error)

SavePrompt appends a new version and points current_prompt_id at it.

func (*DB) SaveSideMemory

func (d *DB) SaveSideMemory(ctx context.Context, e sidequestion.Exchange, memory sidequestion.Memory) error

func (*DB) SaveSideSnapshot

func (d *DB) SaveSideSnapshot(ctx context.Context, s sidequestion.Snapshot) error

func (*DB) SearchChatMentions

func (d *DB) SearchChatMentions(ctx context.Context, kind, query string) ([]ChatMention, error)

SearchChatMentions searches the shared catalog, just like the asset/finding pages. Values remain SQL parameters; %, _ and backslash are literal search text.

func (*DB) SearchChatMentionsPage

func (d *DB) SearchChatMentionsPage(ctx context.Context, kind, query, cursor string) (ChatMentionPage, error)

SearchChatMentionsPage uses the last result's stable sort key rather than an offset, so loading later pages does not repeatedly skip all earlier results.

func (*DB) SeedPromptIfEmpty

func (d *DB) SeedPromptIfEmpty(agentID int64, tmpl string) error

SeedPromptIfEmpty writes the code-default template as the agent's first prompt version ONLY when it has none yet (current_prompt_id IS NULL). Mirrors SeedTool's first-insert-only philosophy: a user's edited prompt is never clobbered on restart. Idempotent — a no-op once any version exists.

func (*DB) SeedTool

func (d *DB) SeedTool(key, desc string, schema, agents json.RawMessage) error

SeedTool inserts a built-in tool's code-defined defaults ONCE. ON CONFLICT DO NOTHING: an existing row (possibly edited in the UI) is never overwritten on startup — that's what keeps page edits from being wiped every restart. Use UpsertToolForce for an explicit "reset to code default".

func (*DB) SetActiveProfile

func (d *DB) SetActiveProfile(id int64) error

SetActiveProfile makes one profile the global default (single-default invariant).

func (*DB) SetAgentInteractiveShell

func (d *DB) SetAgentInteractiveShell(key string, on bool) error

SetAgentInteractiveShell toggles whether an agent gets the interactive shell (持久 PTY 会话) tool family + Bash 提示词联动(见 docs/交互式shell设计.md §14.2).

func (*DB) SetAgentLLMProfile

func (d *DB) SetAgentLLMProfile(key string, id *int64) error

SetAgentLLMProfile binds an agent to a specific LLM profile (id != nil), or clears the binding (id == nil) so the agent follows the task/conversation pin, else the global active profile. Precedence at runtime: agent binding → task/conv pin → active.

func (*DB) SetAgentMaxTurns

func (d *DB) SetAgentMaxTurns(key string, maxTurns int) error

SetAgentMaxTurns updates an agent's max_turns (0 = unlimited).

func (*DB) SetAgentRunSeconds

func (d *DB) SetAgentRunSeconds(key string, runSecs int) error

SetAgentRunSeconds updates an agent's run_seconds wall-clock budget (0 = unlimited).

func (*DB) SetAgentSkillVisibility

func (d *DB) SetAgentSkillVisibility(agentID int64, names []string) error

SetAgentSkillVisibility replaces all skill visibility for an agent.

func (*DB) SetAgentTaskTimeoutWrapup

func (d *DB) SetAgentTaskTimeoutWrapup(key, prompt string, maxTurns int) error

SetAgentTaskTimeoutWrapup stores an agent's task-timeout wrap-up prompt (empty = use code built-in default; only worker/planner have one) and its turn budget (0 = default). Resolved at runtime by resolveTaskTimeoutWrapup / …Turns.

func (*DB) SetAgentTriggerBehavior

func (d *DB) SetAgentTriggerBehavior(key, runMode, mergeMode string, maxParallel int) error

SetAgentTriggerBehavior stores an agent's P3 trigger post-processing策略: runMode(serial|parallel) / mergeMode(by_task|all|none) / maxParallel(parallel 用,0=不限)。 枚举做白名单校验,非法值回落默认,避免脏数据把调度 pump 带偏。

func (*DB) SetAgentVisibilityKind

func (d *DB) SetAgentVisibilityKind(agentID int64, kind string, resourceIDs []int64) error

SetAgentVisibilityKind replaces the full set of visible resources of a kind for an agent (agent-side bulk write). Bidirectional with the resource-side view — both read/write the same agent_visibility rows.

func (*DB) SetAgentWebSearch

func (d *DB) SetAgentWebSearch(key string, on bool) error

SetAgentWebSearch toggles whether an agent uses network search (still gated by the global web-search master switch + backend/key config).

func (*DB) SetAgentWrapupMaxTurns

func (d *DB) SetAgentWrapupMaxTurns(key string, n int) error

SetAgentWrapupMaxTurns stores the wrap-up phase's own turn budget. 0 means "use the code built-in default" — resolved at runtime by resolveWrapupTurns.

func (*DB) SetAgentWrapupPrompt

func (d *DB) SetAgentWrapupPrompt(key, prompt string) error

SetAgentWrapupPrompt stores an agent's wrap-up (settlement) prompt. Empty string means "use the code built-in default" — resolved at runtime by resolveWrapup.

func (*DB) SetBool

func (d *DB) SetBool(key string, val bool) error

SetBool stores a boolean setting as "true"/"false".

func (*DB) SetFindingName

func (d *DB) SetFindingName(id int64, name string) (int64, error)

SetFindingName updates one finding's 漏洞名称 (+ node payload sync). Empty name is allowed — the frontend falls back to the vuln class for display.

func (*DB) SetFindingReportByNodeID

func (d *DB) SetFindingReportByNodeID(nodeID int64, report string) (int64, error)

SetFindingReportByNodeID sets the Markdown report on the standalone finding row whose node_id matches — report_finding returns that node id, so an agent tool can address the finding it just created. Returns rows affected (0 when no row).

func (*DB) SetFindingReportVersionByNodeID

func (d *DB) SetFindingReportVersionByNodeID(ctx context.Context, nodeID int64, report string, version *int64) (n int64, err error)

func (*DB) SetFindingSeverity

func (d *DB) SetFindingSeverity(id int64, severity string) (int64, error)

SetFindingSeverity updates one finding's severity (+ node payload sync). Returns rows affected (0 when no finding has that id).

func (*DB) SetFindingStatus

func (d *DB) SetFindingStatus(id int64, status string) (int64, error)

SetFindingStatus updates one finding's triage state. Returns rows affected.

底层 setter:只改状态、不登记推送事件。生产代码改状态请走 SetFindingStatusWithNotify —— 直接调本函数会让「状态变更推送」静默失效。 保留它是为了让不关心通知的用例(参数校验、复测流程)能单独驱动状态。

func (*DB) SetFindingStatusWithNotify added in v0.3.15

func (d *DB) SetFindingStatusWithNotify(ctx context.Context, id int64, status string) (from string, found bool, notified bool, err error)

SetFindingStatusWithNotify 更新漏洞处置状态,并在同一事务里登记一条状态变更 推送事件。

返回 from=变更前的状态;found=漏洞是否存在;notified=事件是否登记成功。

三条刻意的行为:

  • 状态未实际变化时不登记事件。前端抽屉重复提交同一个值、或自动化脚本 幂等重放,都不该产出推送噪音。
  • 漏洞不存在时返回 found=false 且不做任何写入,由调用方翻译成 404。
  • 事件登记失败不影响状态更新(见 RecordNotificationEventTx 的保存点说明), 所以 notified=false 时状态已经改成功了,调用方不应因此报错。

func (*DB) SetFindingVulnClass

func (d *DB) SetFindingVulnClass(id int64, vulnclass string) (int64, error)

SetFindingVulnClass updates one finding's 漏洞类别 (+ node payload sync).

func (*DB) SetLLMRetryPolicy

func (d *DB) SetLLMRetryPolicy(p LLMRetryPolicy) error

SetLLMRetryPolicy persists the global retry policy (values are clamped first).

func (*DB) SetNotificationChannelEnabled added in v0.3.15

func (d *DB) SetNotificationChannelEnabled(ctx context.Context, id int64, enabled bool) error

SetNotificationChannelEnabled 切换启停。

停用一个渠道时,把它尚未发出的投递一并标记为 skipped:否则重新启用后 会突然收到一批「停用期间积压」的旧漏洞,时效已失且容易误判为新增。

func (*DB) SetParentRef

func (d *DB) SetParentRef(id int64, parentRef string) error

SetParentRef records a task's parent task id (编排 agent spawn_task 关联).

func (*DB) SetPaused

func (d *DB) SetPaused(id int64, paused bool) error

SetPaused persists a task's paused flag.

func (*DB) SetQueued

func (d *DB) SetQueued(id int64, queued bool) error

SetQueued is the compatibility helper used by older callers and tests. New scheduling code should use Enqueue/Dequeue so FIFO metadata is explicit.

func (*DB) SetSchedState

func (d *DB) SetSchedState(key, value string) error

func (*DB) SetSetting

func (d *DB) SetSetting(key, value string) error

SetSetting upserts a setting value.

func (*DB) SetStatus

func (d *DB) SetStatus(id int64, status string) error

SetStatus updates a task's lifecycle status. Entering a terminal state (done/failed/timeout) stamps completed_at once (COALESCE keeps the first stamp stable); moving back to a non-terminal state clears it, so a re-run has no stale finish time.

func (*DB) SetTaskCategory

func (d *DB) SetTaskCategory(taskID int64, categoryID *int64) (*TaskCategory, error)

SetTaskCategory updates one live task. A nil category means uncategorized.

func (*DB) SetTasksCategory

func (d *DB) SetTasksCategory(taskIDs []int64, categoryID *int64) ([]int64, *TaskCategory, error)

SetTasksCategory moves several tasks into one category (nil = uncategorized) inside a single transaction, so a half-applied batch is never observable. It returns the ids that were actually updated — ids missing from that slice were deleted between selection and submit — plus the refreshed category row whose task_count already reflects this move.

func (*DB) SetTerminalStatusGuarded

func (d *DB) SetTerminalStatusGuarded(id int64, status string) (won bool, err error)

SetTerminalStatusGuarded sets a terminal status only when the task is NOT already terminal, so a completed↔timeout race resolves to the first writer (won=true). Returns won=false (no error) when another terminal status already stuck — the caller then leaves it alone. Non-terminal transitions / re-run still use SetStatus.

func (*DB) SideHistory

func (d *DB) SideHistory(ctx context.Context, key string, before int64, limit int) ([]sidequestion.Exchange, error)

func (*DB) SideMemory

func (d *DB) SideMemory(ctx context.Context, e sidequestion.Exchange) (sidequestion.Memory, error)

func (*DB) SideReplay

func (d *DB) SideReplay(ctx context.Context, key string) ([]sidequestion.Exchange, error)

func (*DB) SideReplayPage

func (d *DB) SideReplayPage(ctx context.Context, e sidequestion.Exchange, after int64) ([]sidequestion.Exchange, error)

Unlike SideReplay's UI-era 20-row window, this cursor visits all unsummarized successful exchanges, in bounded pages and only before the admitted request.

func (*DB) SideRequest

func (d *DB) SideRequest(ctx context.Context, id string) (*sidequestion.Exchange, error)

func (*DB) SideSnapshot

func (d *DB) SideSnapshot(ctx context.Context, key string) (*sidequestion.Snapshot, error)

func (*DB) SkillAgents

func (d *DB) SkillAgents(skillName string) ([]int64, error)

SkillAgents returns the agent IDs that can see a skill.

func (*DB) SkillCallsByTask

func (d *DB) SkillCallsByTask(taskID int64) ([]SkillStat, error)

SkillCallsByTask counts a task's skill loads, most-used first. Powers a per-task view of which procedures its agents actually reached for.

func (*DB) SkillStats

func (d *DB) SkillStats() ([]SkillStat, error)

SkillStats aggregates the whole ledger grouped by skill, most-used first. Skills that were never invoked are absent — callers merge against the skill list on disk. Only resolved calls count; misses are reported separately by MissingSkillStats.

func (*DB) SnapshotTaskArchive

func (d *DB) SnapshotTaskArchive(taskID int64) (*TaskArchiveSnapshot, error)

SnapshotTaskArchive reads one repeatable PostgreSQL snapshot. Task-owned Agent writes are already quiescent at the server barrier; repeatable-read also keeps the asset and accounting views mutually consistent during serialization.

func (*DB) SnapshotTaskArchiveWithLLMRecords

func (d *DB) SnapshotTaskArchiveWithLLMRecords(taskID int64, llmRecords io.Writer) (*TaskArchiveSnapshot, error)

SnapshotTaskArchiveWithLLMRecords streams the heavyweight record history to llmRecords while all other task-owned data is read from the same repeatable PostgreSQL snapshot.

func (*DB) StampFirstRun

func (d *DB) StampFirstRun(id int64, timeoutSeconds int) (*time.Time, error)

StampFirstRun records a task's first-real-run moment and computes its absolute deadline (= now + timeoutSeconds). Idempotent: only stamps when first_run_at is still NULL, so restarts / re-entries keep the original clock. timeoutSeconds<=0 leaves deadline_at NULL (不限时). Returns the resulting deadline (nil = 不限/未变).

func (*DB) StartFindingRetest

func (d *DB) StartFindingRetest(ctx context.Context, id int64) (bool, error)

func (*DB) StartSideRequest

func (d *DB) StartSideRequest(ctx context.Context, s sidequestion.Snapshot, clientID, question string) (*sidequestion.Exchange, bool, error)

func (*DB) TaskArchiveBlockers

func (d *DB) TaskArchiveBlockers() (map[int64]int64, error)

TaskArchiveBlockers returns one live direct dependent for every source task that cannot currently be archived. Dependents already queued for archiving do not block their source because the FIFO worker will compact them first.

func (*DB) TaskCompanyIDs

func (d *DB) TaskCompanyIDs(taskID int64) ([]int64, error)

TaskCompanyIDs returns the companies whose asset scopes are available to the task. The task_scope rows remain the single source of truth.

func (*DB) TaskLLMProfiles

func (d *DB) TaskLLMProfiles(taskID int64) ([]TaskLLMProfile, error)

func (*DB) TaskListMetricsAll

func (d *DB) TaskListMetricsAll() (map[int64]TaskListMetrics, error)

TaskListMetricsAll returns list aggregates for every live task in one query. The lateral lookups use the per-exploration indexes instead of grouping the complete activity history on every poll.

func (*DB) TaskSourceIDs

func (d *DB) TaskSourceIDs(taskID int64) ([]int64, error)

func (*DB) TaskSources

func (d *DB) TaskSources(taskID int64) ([]TaskSource, error)

func (*DB) TimedOutTasksSince

func (d *DB) TimedOutTasksSince(lastID int64) ([]TaskEvent, error)

TimedOutTasksSince returns tasks that reached status='timeout' with id > lastID, ordered by id (monotonic watermark → no double-fire across restarts).

func (*DB) ToggleAssetInterceptRule

func (d *DB) ToggleAssetInterceptRule(id int64, enabled bool) error

ToggleAssetInterceptRule flips the enabled state of a rule.

func (*DB) ToggleInterceptRule

func (d *DB) ToggleInterceptRule(id int64, enabled bool) error

ToggleInterceptRule flips the enabled state of a rule.

func (*DB) ToggleSkillVisibility

func (d *DB) ToggleSkillVisibility(agentID int64, skillName string, on bool) error

ToggleSkillVisibility sets one (agent, skill_name) visibility on/off.

func (*DB) ToggleVisibility

func (d *DB) ToggleVisibility(agentID int64, kind string, resourceID int64, on bool) error

ToggleVisibility sets one (agent, kind, resource) visibility on/off (idempotent).

func (*DB) TokenByModel

func (d *DB) TokenByModel(taskID string) ([]ModelTokenStat, error)

TokenByModel aggregates a task's LLM token usage grouped by model, most-used first, from the always-on llm_usage ledger. taskID is the task registry id. Accurate even with per-agent model bindings, pool rotation/failover, and interrupted runs, since every call (success or error) is metered.

func (*DB) TokenDailyAll

func (d *DB) TokenDailyAll(days int) ([]DailyTokenBucket, error)

TokenDailyAll aggregates token consumption by calendar day (UTC) for the past `days` days across all explorations. Only kind='result' rows carry token counts (the terminal per-run summary), so there is no double-counting.

func (*DB) TokenTotalsAll

func (d *DB) TokenTotalsAll() (map[int64]TokenUsage, error)

TokenTotalsAll returns the whole-task token total for every exploration in one query (exploration_id → total), so the task list can show per-task consumption without a query per row.

func (*DB) ToolStats

func (d *DB) ToolStats(expID *int64, q string) ([]ToolStat, error)

ToolStats counts executions grouped by tool under the same filters ListCommands takes. Unpaginated on purpose: the tally describes the whole filtered set, not the page currently on screen.

func (*DB) ToolUsageCounts

func (d *DB) ToolUsageCounts() (map[string]int, error)

ToolUsageCounts returns invocation totals keyed by catalog tool key. Entries without calls are absent; API callers merge the result into the tools catalog.

func (*DB) TouchConversation

func (d *DB) TouchConversation(id int64) error

TouchConversation bumps updated_at so the thread floats to the top of the list.

func (*DB) TouchTriggerFire

func (d *DB) TouchTriggerFire(id int64) error

TouchTriggerFire records an interval trigger's fire time (now).

func (*DB) UpdateAgentMeta

func (d *DB) UpdateAgentMeta(key, name, description string) error

UpdateAgentMeta updates a custom agent's display name + description. Built-in agents are left untouched (guarded by the caller / the builtin flag).

func (*DB) UpdateAssetInterceptRule

func (d *DB) UpdateAssetInterceptRule(id int64, kind, pattern, note string, enabled bool) (AssetInterceptRule, error)

UpdateAssetInterceptRule replaces the editable fields of an existing rule.

func (*DB) UpdateConversation

func (d *DB) UpdateConversation(id int64, patch ConversationPatch) (*Conversation, error)

UpdateConversation applies a partial title/pin mutation and returns the updated row. Pinning an already-pinned conversation preserves its original pin order.

func (*DB) UpdateConversationProfile

func (d *DB) UpdateConversationProfile(id int64, llmProfileID *int64) error

UpdateConversationProfile sets (or clears) the LLM profile override for a conversation.

func (*DB) UpdateCustomTool

func (d *DB) UpdateCustomTool(t *Tool) error

UpdateCustomTool updates a custom tool's editable fields (kind/exec/deferred + desc/schema/agents/enabled). Only touches system=false rows.

func (*DB) UpdateInterceptRule

func (d *DB) UpdateInterceptRule(id int64, name, matchTarget, matchType, pattern, action, message string, priority int, enabled bool, timeoutEnabled bool, timeoutSeconds int, timeoutAction string) (InterceptRule, error)

UpdateInterceptRule replaces all editable fields of an existing rule.

func (*DB) UpdateSideRequest

func (d *DB) UpdateSideRequest(ctx context.Context, e sidequestion.Exchange) (bool, error)

Conditional updates cannot resurrect deleted history or overwrite a terminal cancellation with a late provider callback.

func (*DB) UpdateTask

func (d *DB) UpdateTask(id int64, patch TaskPatch) (*Task, error)

UpdateTask applies a partial task name/pin mutation and returns the updated row. Re-pinning an already pinned task preserves its original position.

func (*DB) UpdateTaskArchiveProgress

func (d *DB) UpdateTaskArchiveProgress(id int64, phase string, progress int) error

func (*DB) UpdateTaskTemplate

func (d *DB) UpdateTaskTemplate(id int64, in TaskTemplateInput) (*TaskTemplate, error)

UpdateTaskTemplate replaces the editable fields of one preset.

func (*DB) UpdateTool

func (d *DB) UpdateTool(key, desc string, schema, agents json.RawMessage, enabled bool) error

UpdateTool saves the page-editable fields. key is never changed (it is welded to the Go handler). system tools: the caller must keep the schema structure — only per-param description/default and the agent binding are meant to move.

func (*DB) UpdateTrigger

func (d *DB) UpdateTrigger(t *AgentTrigger) error

UpdateTrigger updates a trigger's fields (not last_fire).

func (*DB) UpsertToolForce

func (d *DB) UpsertToolForce(key, desc string, schema, agents json.RawMessage) error

UpsertToolForce overwrites a tool row with the given code-default values (used by the per-tool "reset" action). It resets description/schema/agents and re-enables the tool, but keeps system=true.

func (*DB) UsageByProfile

func (d *DB) UsageByProfile() ([]ProfileUsage, error)

UsageByProfile returns global token spend grouped by profile name, most-used first. profile_name may be empty for calls made on env/non-persisted configs.

func (*DB) UsageDaily

func (d *DB) UsageDaily(days int) ([]ProfileDayUsage, error)

UsageDaily returns per-(profile, day) token buckets for the past `days` days (default 365 when days<=0), so the dashboard can slice by profile + range.

func (*DB) WithEvidenceTx

func (d *DB) WithEvidenceTx(ctx context.Context, fn func(*sql.Tx) error) error

type DBFinding

type DBFinding struct {
	TrafficCount          int
	EvidenceVersion       int64
	ReportEvidenceVersion int64
	TrafficBindings       []FindingTrafficBinding // populated only for export

	ID              int64
	TaskID          *int64
	NodeID          *int64
	VulnClass       string
	Name            string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass
	Severity        string
	Summary         string
	Evidence        string
	Worker          string
	AssetIDs        []int64
	Status          string
	Report          string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带
	CreatedAt       time.Time
	TaskDescription string // populated via LEFT JOIN on tasks
}

DBFinding is a row in the standalone findings table. It persists across task deletion unless the caller explicitly requests related finding cleanup.

type DBLog

type DBLog struct {
	ID        int64
	CreatedAt time.Time
	Level     string
	Tag       string
	Text      string
}

DBLog is one persisted backend log row from the server_logs table.

type DailyTokenBucket

type DailyTokenBucket struct {
	Day             string `json:"day"` // "YYYY-MM-DD"
	InputTokens     int    `json:"input_tokens"`
	OutputTokens    int    `json:"output_tokens"`
	CacheReadTokens int    `json:"cache_read_tokens"`
}

DailyTokenBucket is one day's global token aggregate across all tasks.

type DirectSourceStore

type DirectSourceStore struct {
	Task  TaskSource
	Store *ExplorationStore
}

DirectSourceStore binds one directly related task to its exploration store. It is intentionally a read-side helper: callers keep using the receiver store for every graph mutation, frontier lookup, and intent claim.

type Edge

type Edge struct {
	From int64
	Rel  string
	To   int64
}

Edge is one row from exploration_edges.

type ExplorationStore

type ExplorationStore struct {
	// contains filtered or unexported fields
}

ExplorationStore is a handle onto one exploration's reasoning graph + activity.

func (*ExplorationStore) ActiveDigests

func (s *ExplorationStore) ActiveDigests() ([]*Node, error)

ActiveDigests returns the exploration's live digest nodes (state='active'), oldest first.

func (*ExplorationStore) ActivityByIDs

func (s *ExplorationStore) ActivityByIDs(ids []int64) ([]Activity, error)

ActivityByIDs loads full detail for specific step ids (the trace drill-down), scoped to this exploration. thinking rows are skipped (never returned, even if their id is asked for). Order is ascending id, not the input order.

func (*ExplorationStore) ActivityByIDsForTerminalIntents

func (s *ExplorationStore) ActivityByIDsForTerminalIntents(ids []int64) ([]Activity, error)

ActivityByIDsForTerminalIntents is the inherited-detail read boundary. It deliberately excludes node-less planner/main rows, non-intent activities, live source work, and thinking. Local callers that need legacy behavior use ActivityByIDs instead.

func (*ExplorationStore) ActivityByIDsWithSources

func (s *ExplorationStore) ActivityByIDsWithSources(ids []int64) ([]Activity, error)

ActivityByIDsWithSources loads local step details with the legacy behavior. For direct sources it only returns rows attached to terminal intents, preventing an arbitrary global activity id from exposing source planner/main transcripts.

func (*ExplorationStore) ActivityDetail

func (s *ExplorationStore) ActivityDetail(id int64) (string, error)

ActivityDetail lazily returns the full detail blob for one step.

func (*ExplorationStore) ActivityDetailWithSources

func (s *ExplorationStore) ActivityDetailWithSources(id int64) (string, error)

ActivityDetailWithSources keeps the legacy local-task lookup (including local thinking rows), while inherited details are restricted to terminal worker intents. Source planner/main rows have no node id and must never become part of inherited context.

func (*ExplorationStore) ActivityList

func (s *ExplorationStore) ActivityList(nodeID *int64, sinceID int64, limit int) ([]Activity, int64, error)

ActivityList returns steps after sinceID (exclusive), optionally filtered by node. Returns items and the new cursor (max id seen).

func (*ExplorationStore) ActivityListForTerminalIntent

func (s *ExplorationStore) ActivityListForTerminalIntent(nodeID, sinceID int64, limit int) ([]Activity, int64, error)

ActivityListForTerminalIntent is the inherited, incremental-read boundary. The intent state check and activity read happen in one statement so a source intent reopened between API-level checks cannot expose its new in-flight trace. Model reasoning and accounting rows are never inherited.

func (*ExplorationStore) ActivityListWithSources

func (s *ExplorationStore) ActivityListWithSources(nodeID, sinceID int64, limit int) ([]Activity, int64, error)

ActivityListWithSources is the source-aware equivalent used by get_worker_output. Node ids are global, so the first owning exploration is unambiguous even when the work has not emitted any activity yet.

func (*ExplorationStore) ActivityMaxID

func (s *ExplorationStore) ActivityMaxID() (int64, error)

ActivityMaxID returns the task's current maximum activity id (0 when empty). It is the task-level snapshot cursor handed to the SSE stream so history (id<=cursor) and the live tail (id>cursor) meet with no gap — deliberately task-level, not per session, since one SSE covers the whole task.

func (*ExplorationStore) ActivityPage

func (s *ExplorationStore) ActivityPage(f ActivitySessionFilter, before int64, limit int) ([]Activity, bool, error)

ActivityPage returns one page for reverse (newest-first) pagination of a session: up to `limit` steps ending before id `before` (exclusive; before<=0 = the latest page), returned in ASCENDING id order for direct display. hasMore reports whether still-older steps exist before the returned window (so the client can stop loading on scroll-up). Summary-only columns — detail is lazy-loaded via ActivityDetail.

func (*ExplorationStore) ActivityPageForTerminalIntent

func (s *ExplorationStore) ActivityPageForTerminalIntent(nodeID, before int64, limit int) ([]Activity, bool, error)

ActivityPageForTerminalIntent is the inherited session-history boundary. It combines ownership, terminal-state and row-kind checks in one query, closing the race where a previously completed source intent is reopened before paging.

func (*ExplorationStore) ActivityTrace

func (s *ExplorationStore) ActivityTrace(nodeID int64, limit int) ([]Activity, error)

ActivityTrace lists one work's steps (by intent/node id) for the trace tools: summaries only (detail is lazy — fetch via ActivityByIDs). thinking/usage rows are dropped so the caller sees the work's actions + observations, not the model's internal reasoning or token-accounting noise.

func (*ExplorationStore) ActivityTraceForTerminalIntent

func (s *ExplorationStore) ActivityTraceForTerminalIntent(nodeID int64, limit int) ([]Activity, error)

ActivityTraceForTerminalIntent returns one inherited work trace only while its source intent is still terminal at query time.

func (*ExplorationStore) ActivityTraceSearch

func (s *ExplorationStore) ActivityTraceSearch(nodeID *int64, q string, limit int) ([]Activity, error)

ActivityTraceSearch finds steps whose summary OR detail matches q (case-insensitive), returning summaries only. nodeID != nil scopes to one work; nil searches across ALL works (node_id IS NOT NULL — planner/main steps have no node_id, so this cleanly means "worker traces"). thinking/usage rows dropped.

func (*ExplorationStore) ActivityTraceSearchAllWithSources

func (s *ExplorationStore) ActivityTraceSearchAllWithSources(excludeNodeID int64, q string, limit int) ([]Activity, error)

ActivityTraceSearchAllWithSources searches local worker traces plus every direct source. The local owner is excluded only from the current exploration; inherited traces are immutable historical context.

func (*ExplorationStore) ActivityTraceSearchExcluding

func (s *ExplorationStore) ActivityTraceSearchExcluding(excludeNodeID int64, q string, limit int) ([]Activity, error)

ActivityTraceSearchExcluding keyword-searches every node's steps in this exploration EXCEPT one node's own (excludeNodeID) — so a worker's search_all_worker_traces doesn't return its own in-progress trace (which is already in its context). excludeNodeID<=0 → no exclusion (searches all nodes).

func (*ExplorationStore) ActivityTraceSearchForTerminalIntent

func (s *ExplorationStore) ActivityTraceSearchForTerminalIntent(nodeID int64, q string, limit int) ([]Activity, error)

ActivityTraceSearchForTerminalIntent is the scoped inherited search variant; terminal eligibility is evaluated by the same statement that reads the rows.

func (*ExplorationStore) ActivityTraceSearchTerminalIntents

func (s *ExplorationStore) ActivityTraceSearchTerminalIntents(q string, limit int) ([]Activity, error)

ActivityTraceSearchTerminalIntents searches inherited worker history without exposing planner/main rows or work that is still open/running in the source.

func (*ExplorationStore) ActivityTraceSearchWithSources

func (s *ExplorationStore) ActivityTraceSearchWithSources(nodeID int64, q string, limit int) ([]Activity, error)

ActivityTraceSearchWithSources performs the scoped worker-trace search used by get_worker_trace. A node id resolves to at most one exploration because exploration node ids are global.

func (*ExplorationStore) ActivityTraceWithSources

func (s *ExplorationStore) ActivityTraceWithSources(nodeID int64, limit int) ([]Activity, error)

ActivityTraceWithSources returns a work trace when its intent belongs to the current exploration or a direct source. It never searches indirect sources.

func (*ExplorationStore) AddConstraint

func (s *ExplorationStore) AddConstraint(kind, text, origin string) (int64, error)

AddConstraint inserts one constraint (kind must be allow|deny) and returns its id.

func (*ExplorationStore) AddDigest

func (s *ExplorationStore) AddDigest(payload map[string]any, memberIDs []int64) (int64, error)

AddDigest writes one digest node and its covers edges (digest→member) in a single transaction. payload is the digest body + member_ids + generation + signature (see cold-digest §1). Returns the new digest id.

func (*ExplorationStore) AddFindingFollowUpIntent

func (s *ExplorationStore) AddFindingFollowUpIntent(findingID, findingNodeID int64, description string, audit Activity) (int64, Activity, error)

AddFindingFollowUpIntent atomically creates a priority-10 human intent from a live finding node, copies that finding's asset anchors, records the finding --derived_from--> intent lineage edge, and persists its audit activity. The returned activity is the committed row and can be broadcast as-is without calling AppendActivity again.

func (*ExplorationStore) AddGoal

func (s *ExplorationStore) AddGoal(payload map[string]any, origin string) (int64, error)

AddGoal writes a goal node (state open).

func (*ExplorationStore) AddIntent

func (s *ExplorationStore) AddIntent(payload map[string]any, priority int, anchors []int64, origin string) (int64, error)

AddIntent is a convenience: an open intent.

func (*ExplorationStore) AddNode

func (s *ExplorationStore) AddNode(kind string, payload map[string]any, priority int, state, origin string, anchors []int64) (int64, error)

AddNode writes a typed reasoning node with optional anchors to asset ids.

func (*ExplorationStore) AddStandaloneFinding

func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error)

AddStandaloneFinding writes a finding to the standalone findings table, which persists across task deletion (task_id / node_id become NULL when the task or exploration node is deleted). taskID and nodeID may be 0 (stored as NULL).

func (*ExplorationStore) Anchor

func (s *ExplorationStore) Anchor(nodeID, assetID int64) error

Anchor records a node→asset reference edge (many-to-many): it captures which global assets an exploration node touched, as lineage/provenance. The asset graph itself is global and shared — anchoring no longer gates visibility (any task may read any asset via search); it only preserves the reasoning trail. Idempotent.

func (*ExplorationStore) AppendActivity

func (s *ExplorationStore) AppendActivity(a Activity) (int64, error)

AppendActivity records one worker step and returns its id.

func (*ExplorationStore) ApplyStampOps

func (s *ExplorationStore) ApplyStampOps(ops []StampOp) error

ApplyStampOps writes a batch of cold_since_round changes in one transaction.

func (*ExplorationStore) AssetRefs

func (s *ExplorationStore) AssetRefs(assetID int64) ([]AssetRef, error)

AssetRefs returns the intents / facts / findings in THIS exploration anchored to the given asset id (newest first) — what this task tested / concluded about it.

func (*ExplorationStore) AssetRefsWithSources

func (s *ExplorationStore) AssetRefsWithSources(assetID int64) ([]AssetRef, error)

AssetRefsWithSources returns anchored nodes from this exploration and each direct source. Inherited entries retain their owning task id so API/UI callers can present them as immutable context.

func (*ExplorationStore) BumpRound

func (s *ExplorationStore) BumpRound() (int64, error)

BumpRound advances this exploration's planner-round counter and returns the new value (§2.3). Called once per planner wake-up.

func (*ExplorationStore) CancelIntent

func (s *ExplorationStore) CancelIntent(id int64) (IntentCleanup, error)

CancelIntent 物理删除一个意图,以及"仅由该意图支撑"的全部独占子孙节点——从该意图沿 yields/derived_from 向下可达、且所有父节点(所有指向它的边的源)都落在删除集内的节点。 goal 与 origin fact 永不删除;还被删除集之外的意图/digest 引用的共享节点也保留,以免破坏 其它分支、产生断链。被删的每个 intent 先做 token rollup(保留不可逆计量)并清理其 activity 与副会话;被删的 finding 先删 findings 表行(node_id FK 为 ON DELETE SET NULL,否则留孤儿)。 边随节点 CASCADE 清理,整个清理保持在一个事务内。调用方须先停掉运行中的 worker 防止其后续写入。

func (*ExplorationStore) ClaimIntent

func (s *ExplorationStore) ClaimIntent(id int64, owner string) (bool, error)

ClaimIntent atomically moves an open intent to running. Returns true if claimed.

func (*ExplorationStore) ColdStamps

func (s *ExplorationStore) ColdStamps() (map[int64]*int64, error)

ColdStamps returns cold_since_round for every foldable node (intent/fact): id → *round (nil when the node is hot / unstamped). Used to compute the ≥R debounce and to know which stamps to set/clear this round.

func (*ExplorationStore) CompareAndSetIntentState

func (s *ExplorationStore) CompareAndSetIntentState(id int64, expected, state string) (bool, error)

CompareAndSetIntentState transitions one local intent only when its current state still matches expected. It is the state boundary used by pause/resume and worker settlement, so a stale API read cannot overwrite a concurrent finish.

func (*ExplorationStore) ContentVersions

func (s *ExplorationStore) ContentVersions() (map[int64]int, error)

ContentVersions returns id → content_version for all nodes (§5.3 signature).

func (*ExplorationStore) CountFinishedIntents

func (s *ExplorationStore) CountFinishedIntents() (int, error)

CountFinishedIntents counts this exploration's intents in a terminal explored state (done/blocked/exhausted) — graph_overview's done_intents_total, so the planner knows recent_done_intents (capped at a recent window) is a truncated view and stays cautious about "already tried" dedup. Excludes 'stopped' (killed/deleted), matching exactly what recent_done_intents surfaces.

func (*ExplorationStore) CountOpenIntents added in v0.3.14

func (s *ExplorationStore) CountOpenIntents() (int, error)

CountOpenIntents counts this exploration's open intents — graph_overview's frontier_open, so the planner knows open_intents (capped at the top-N by priority) is a truncated view and there may be more claimable work.

func (*ExplorationStore) CoveredMembers

func (s *ExplorationStore) CoveredMembers() (map[int64]int64, error)

CoveredMembers maps member id → covering digest id, for ACTIVE digests only (§6.1 point 1). A node with no entry is not currently folded. Should a member carry covers edges from two digests (a torn major write), the lowest digest id wins deterministically — callers dedupe on this.

func (*ExplorationStore) CurrentMainSeg

func (s *ExplorationStore) CurrentMainSeg() (int, error)

CurrentMainSeg returns the highest main-session segment (0 when none created yet).

func (*ExplorationStore) DeleteConstraint

func (s *ExplorationStore) DeleteConstraint(id int64) error

DeleteConstraint removes a constraint; scoped to this exploration. Returns an error if no such constraint exists.

func (*ExplorationStore) DeleteGoal

func (s *ExplorationStore) DeleteGoal(id int64) error

DeleteGoal hard-deletes a goal node; scoped to kind='goal' so it can never drop an intent/fact by id. The edges (spawns) and anchors reference it with ON DELETE CASCADE, so they go with it; activity.node_id is ON DELETE SET NULL. Returns an error if no such goal exists in this exploration.

func (*ExplorationStore) DigestMembers

func (s *ExplorationStore) DigestMembers(digestID int64) ([]int64, error)

DigestMembers returns the member ids a digest covers (covers edges), sorted.

func (*ExplorationStore) DirectSourceStores

func (s *ExplorationStore) DirectSourceStores() ([]DirectSourceStore, error)

DirectSourceStores resolves the live, direct task relations for this exploration. It deliberately queries on every call: relations disappear when a source task is deleted, and inherited context must not retain stale rows. Sources of a source are never expanded.

func (*ExplorationStore) DiscardOpenIntent

func (s *ExplorationStore) DiscardOpenIntent(id int64) error

DiscardOpenIntent compensates a follow-up creation when task admission fails. The task execution gate must still be held by the caller, so the intent cannot be claimed between this check and deletion. Edges and anchors cascade with the node; activity is deleted explicitly because its node FK otherwise becomes NULL.

func (*ExplorationStore) Edges

func (s *ExplorationStore) Edges(limit int) ([]Edge, error)

Edges lists exploration edges (for graph viz).

func (*ExplorationStore) EdgesTouching

func (s *ExplorationStore) EdgesTouching(ids []int64) ([]Edge, error)

EdgesTouching returns every edge with one end among ids — the 播报板 uses it to spell out where a node came from and what it produced.

func (*ExplorationStore) FactsYielded

func (s *ExplorationStore) FactsYielded(intentID int64) ([]int64, error)

FactsYielded returns the ids of fact nodes an intent produced (intent --yields--> fact), oldest first. Used to spell out the round's incremental facts to the planner alongside the finished worker's output. Best-effort: returns nil for an intent with no facts (or a missing one).

func (*ExplorationStore) FindingIntents

func (s *ExplorationStore) FindingIntents() (map[int64]int64, error)

FindingIntents maps each finding id to the intent that produced it (the intent --yields--> finding edge; report_finding links it). Findings with no producing intent are absent. Precise (JOIN, no edge-scan limit).

func (*ExplorationStore) FindingIntentsTerminal

func (s *ExplorationStore) FindingIntentsTerminal() (map[int64]int64, error)

FindingIntentsTerminal is the inherited-history variant: it returns finding lineage only when the producing intent has reached an immutable terminal state. This keeps a source task's live worker topology private.

func (*ExplorationStore) FindingIntentsWithSources

func (s *ExplorationStore) FindingIntentsWithSources() (map[int64]int64, error)

FindingIntentsWithSources combines finding lineage for the current exploration and each direct source. Node ids are globally unique.

func (*ExplorationStore) FindingLineage

func (s *ExplorationStore) FindingLineage(nodeID int64) ([]*Node, []Edge, error)

FindingLineage returns the sub-DAG that leads FROM the exploration root TO the given node: the node itself plus all its ancestors (nodes reverse-reachable by following edges backward), and every edge whose both endpoints are in that set. Used to render "how this finding was reached" — origin → … → finding. Returns empty (not error) for a nonexistent node.

func (*ExplorationStore) Frontier

func (s *ExplorationStore) Frontier(limit int) ([]*Node, error)

Frontier returns open intents ordered by priority desc then id (FIFO tiebreak).

func (*ExplorationStore) GetNode

func (s *ExplorationStore) GetNode(id int64) (*Node, error)

GetNode returns one node of this exploration by id (nil, nil if not found).

func (*ExplorationStore) GetNodeWithSources

func (s *ExplorationStore) GetNodeWithSources(id int64) (*Node, error)

GetNodeWithSources reads a node only when it belongs to this exploration or one of its direct sources. Inherited nodes are tagged so tool callers can keep them read-only and show their provenance.

func (*ExplorationStore) HasActiveIntent

func (s *ExplorationStore) HasActiveIntent() (bool, error)

HasActiveIntent reports whether this exploration has any intent still in play (state open OR running). Used to decide whether to kick the FIRST planner round: a seeded task's intent may already have been claimed (open→running) by a worker before the check, so counting only 'open' (Frontier) races with worker claim and wrongly kicks a redundant first round. Counting open+running is race-proof.

func (*ExplorationStore) HasOpenGoal

func (s *ExplorationStore) HasOpenGoal() (bool, error)

HasOpenGoal reports whether this exploration still has any goal in state 'open'. false ⇒ 所有目标已 met/abandoned(或本任务无目标)⇒ 进入 goalless(人工直投)分支: planner 停跑,任务是否结束改由 frontier 是否抽干决定。met 与 abandoned 都算"已了结"。

func (*ExplorationStore) ID

func (s *ExplorationStore) ID() int64
func (s *ExplorationStore) Link(from int64, rel string, to int64) error

Link adds a typed exploration edge (idempotent).

func (*ExplorationStore) ListByKind

func (s *ExplorationStore) ListByKind(kind string, limit int) ([]*Node, error)

ListByKind lists nodes of a kind (newest first).

func (*ExplorationStore) ListByKindPage

func (s *ExplorationStore) ListByKindPage(kind string, before int64, limit int) ([]*Node, bool, error)

ListByKindPage returns one newest-first page of nodes of a kind for reverse pagination: up to `limit` nodes with id < `before` (before<=0 = the newest page). hasMore reports whether still-older nodes exist, so a client (e.g. the worker session list) can page past the old fixed cap instead of losing older intents.

func (*ExplorationStore) ListByKindPageWithSources

func (s *ExplorationStore) ListByKindPageWithSources(kind string, before int64, limit int, q string) (nodes []*Node, hasMore bool, total int, err error)

ListByKindPageWithSources is the paginated, keyword-filterable sibling of ListByKindWithSources: it returns one newest-first page (id < before, before<=0 = newest) spanning this exploration and its direct sources, plus hasMore and the filtered total across all of them. Node ids are globally unique, so merging each store's own page and re-sorting by id DESC yields the true global page; fetching limit+1 per store guarantees the merged top-`limit` is complete.

func (*ExplorationStore) ListByKindWithSources

func (s *ExplorationStore) ListByKindWithSources(kind string, limit int) ([]*Node, error)

ListByKindWithSources returns local nodes followed by nodes from each direct source in relation order. The limit remains per exploration, matching the existing ListByKind contract while ensuring one large task cannot hide all inherited context from another source.

func (*ExplorationStore) ListConstraints

func (s *ExplorationStore) ListConstraints() ([]Constraint, error)

ListConstraints returns this exploration's constraints, allow before deny, oldest first within each group (stable render order for the prompt block + UI).

func (*ExplorationStore) ListMainSessions

func (s *ExplorationStore) ListMainSessions() ([]MainSession, error)

ListMainSessions returns every main-session segment newest-first, always including the implicit original segment 0. Segment 0 carries no stored timestamp.

func (*ExplorationStore) NewMainSession

func (s *ExplorationStore) NewMainSession() (MainSession, error)

NewMainSession creates the next main-session segment (seq = current+1) and returns it. It touches only main_sessions — the task's exploration graph/assets/goal are untouched, so the new session starts with a clean transcript over the same task.

func (*ExplorationStore) NodeAssets

func (s *ExplorationStore) NodeAssets(ids []int64) (map[int64][]int64, error)

NodeAssets maps each of the given node ids → the asset ids it is anchored to (exploration_anchors). Used to group cold digests by asset (§6.2 index).

func (*ExplorationStore) Nodes

func (s *ExplorationStore) Nodes(limit int) ([]*Node, error)

Nodes lists all nodes (for graph viz).

func (*ExplorationStore) NodesByIDs

func (s *ExplorationStore) NodesByIDs(ids []int64) ([]*Node, error)

NodesByIDs loads the given nodes of this exploration in id order. Used to resolve the neighbours of a 播报板 page without fetching the whole graph.

func (*ExplorationStore) NodesPage

func (s *ExplorationStore) NodesPage(f NodeFilter, page, size int) ([]*Node, int, error)

NodesPage returns one 1-based page of this exploration's nodes plus the total matching count. The 播报板 reads the graph as a time series, so it pages in SQL rather than pulling the whole graph like Nodes does. Ordering is by id, which is BIGSERIAL and therefore creation order — stable when several nodes share a created_at second.

func (*ExplorationStore) OriginFactID

func (s *ExplorationStore) OriginFactID() (int64, error)

OriginFactID returns this exploration's root fact id — the KindFact node with state='origin' seeded at task creation (the task root). Every intent traces back to it. Returns 0 (no error) when none exists (legacy explorations created under the old 'begin' root, or none yet).

func (*ExplorationStore) PopulateFindingTrafficIDs

func (s *ExplorationStore) PopulateFindingTrafficIDs(nodes []*Node) error

PopulateFindingTrafficIDs only enriches already-visible nodes. It performs no discovery or ID guessing, and leaves the legacy node ID unchanged.

func (*ExplorationStore) RecordFinding

func (s *ExplorationStore) RecordFinding(ctx context.Context, in RecordFindingInput) (out *RecordedFinding, err error)

RecordFinding is the atomic legacy/no-recorder path used by tools and tests.

func (*ExplorationStore) ReopenBlockedIntents

func (s *ExplorationStore) ReopenBlockedIntents() (int64, error)

ReopenBlockedIntents flips EVERY 'blocked' intent in this exploration back to 'open' (batch rerun after e.g. an LLM/network outage that blocked many at once). Returns the number reopened. Workers resume from their transcripts; kept graph writes remain.

func (*ExplorationStore) ReopenIntent

func (s *ExplorationStore) ReopenIntent(id int64) (bool, error)

ReopenIntent flips ONE not-successfully-finished intent (blocked/exhausted/stopped) back to 'open' so a worker re-claims it (graph writes it already produced stay). The worker resumes from its prior LLM transcript instead of restarting from scratch. done/open/running are left untouched. Returns whether a row changed. Used by "重跑".

func (*ExplorationStore) ReopenIntentsByBlockedReason

func (s *ExplorationStore) ReopenIntentsByBlockedReason(reason string) (int64, error)

func (*ExplorationStore) ResetRunningIntents

func (s *ExplorationStore) ResetRunningIntents() (int64, error)

SetIntentState updates an intent node's state. ResetRunningIntents returns any intent left in 'running' back to 'open' so it is re-claimed. Called on startup: a 'running' intent with no live worker (a backend restart or crashed worker goroutine left it stuck) would otherwise spin forever in the UI. Workers resume from their transcript, so reopening is safe.

func (*ExplorationStore) Root

func (s *ExplorationStore) Root() (description, goal string, err error)

Root returns the exploration's description and goal.

func (*ExplorationStore) RoundNo

func (s *ExplorationStore) RoundNo() (int64, error)

RoundNo returns the current planner-round counter.

func (*ExplorationStore) SetIntentBlockedReason

func (s *ExplorationStore) SetIntentBlockedReason(id int64, reason string) error

func (*ExplorationStore) SetIntentState

func (s *ExplorationStore) SetIntentState(id int64, state string) error

func (*ExplorationStore) SetNodeState

func (s *ExplorationStore) SetNodeState(id int64, state string) error

SetNodeState updates any node's state (never deletes). content_version bumps so a folded member's state flip invalidates its digest's cached body (§5.3).

func (*ExplorationStore) SoftDeleteIntent

func (s *ExplorationStore) SoftDeleteIntent(id int64, reason string) (string, error)

SoftDeleteIntent 假删除一个待领/运行中/已暂停的意图:置 state='deleted' 并把用户填写的 删除原因记入 delete_reason 字段,保留意图节点及其全部产出/血缘(不再像旧实现那样在图上 另挂一条 fact)。返回删除前意图的 summary,供 planner 通知使用。副会话随删除态一并清理。 调用方须先停掉运行中的 worker,避免其后续写入。

func (*ExplorationStore) Stats

func (s *ExplorationStore) Stats() (map[string]int, error)

Stats returns node counts grouped by kind (for dashboard).

func (*ExplorationStore) SupersedeDigests

func (s *ExplorationStore) SupersedeDigests(ids []int64) error

SupersedeDigests retires digest nodes (state→superseded) AND removes their covers edges, atomically, so the active-coverage set (CoveredMembers) never double-counts a member during a major recompaction (§5.1). The digest node itself is kept (node_detail can still resolve it).

func (*ExplorationStore) TaskID

func (s *ExplorationStore) TaskID() (int64, error)

TaskID returns the live task bound to this exploration. Explorations created directly in tests or maintenance code have no task and return zero.

func (*ExplorationStore) TokenStatsBySession

func (s *ExplorationStore) TokenStatsBySession() ([]SessionTokenUsage, error)

TokenStatsBySession aggregates every persisted completed run, independent of activity-history pagination. Main/planner use fixed keys; Worker executions use their intent node id, even when a different work#N process handled a retry.

func (*ExplorationStore) TokenStatsByWorker

func (s *ExplorationStore) TokenStatsByWorker() ([]TokenUsage, error)

TokenStatsByWorker sums token usage per worker for this exploration (from the kind='result' records that carry usage). Used by the per-agent token display.

func (*ExplorationStore) TokenTotal

func (s *ExplorationStore) TokenTotal() (TokenUsage, error)

TokenTotal sums token usage across ALL workers for this exploration (whole-task total). Same source as TokenStatsByWorker (kind='result' rows), just ungrouped.

func (*ExplorationStore) UpdateConstraint

func (s *ExplorationStore) UpdateConstraint(id int64, kind, text string) error

UpdateConstraint rewrites a constraint's kind + text; scoped to this exploration. Returns an error if no such constraint exists.

func (*ExplorationStore) UpdateGoalPayload

func (s *ExplorationStore) UpdateGoalPayload(id int64, text, vulnclass string) error

UpdateGoalPayload rewrites a goal node's payload text (and optional vulnclass); scoped to kind='goal' so it can never mutate an intent/fact by id. Returns an error if no such goal exists in this exploration.

type Expr

type Expr struct {
	Field string // empty = bare-text full-text search
	Op    string // "=", "==", "!=", ">", ">=", "<", "<="
	Value string
}

Expr is one leaf DSL clause.

type FindingAssetNode

type FindingAssetNode struct {
	Key       string `json:"key"`
	Parent    string `json:"parent,omitempty"`
	Kind      string `json:"kind"` // company|root_domain|subdomain|ip|service|app|endpoint|none
	Label     string `json:"label"`
	AssetID   int64  `json:"asset_id,omitempty"`
	CompanyID int64  `json:"company_id,omitempty"`
	// Self 是直接挂在该资产上的发现数;Total 含全部子孙且按 finding 去重
	// (一个发现挂多个资产时,只在共同祖先上计一次)。
	Self        int       `json:"self"`
	Total       int       `json:"total"`
	Critical    int       `json:"critical"`
	High        int       `json:"high"`
	Medium      int       `json:"medium"`
	Low         int       `json:"low"`
	LastFoundAt time.Time `json:"last_found_at"`
}

FindingAssetNode 是资产树的一个节点。Key 与覆盖图同构:资产行是 "a:<id>"、 企业是 "c:<id>"、没有资产行的根域名是合成的 "r:<domain>"、未关联桶是 "__none__"。

type FindingAssetTree

type FindingAssetTree struct {
	Nodes        []FindingAssetNode `json:"nodes"`
	FindingTotal int                `json:"finding_total"`
	// Truncated=true 表示为控制体积丢弃了 DroppedKinds 里的层级。
	Truncated    bool     `json:"truncated"`
	DroppedKinds []string `json:"dropped_kinds,omitempty"`
}

FindingAssetTree 是整棵树的一次性快照。Nodes 已排好序:同一父节点下按发现数 降序、标签升序,「未关联资产」恒在最后。

type FindingFilter

type FindingFilter struct {
	Severity  string // high | medium | low
	Status    string // pending | false_positive | ignored | resolved
	VulnClass string
	TaskID    string // 任务 id(字符串形式;空/非法 = 不按任务筛选)
	Query     string // 名称/类型/摘要/证据/报告正文的模糊检索关键词
	Sort      string // "severity" | "time"
	// AssetScope 是资产树的节点 key(a:<id> / c:<id> / r:<domain> / __none__),
	// 选中一个节点等于选中它的整棵子树。空 = 不按资产筛选。
	AssetScope string
	// contains filtered or unexported fields
}

FindingFilter narrows a paginated findings query. Empty-string fields mean "no filter on that column". Sort is "severity" (severity desc, then newest) or anything else (newest first).

type FindingGroup

type FindingGroup struct {
	TaskID          *int64    `json:"task_id"`
	TaskName        string    `json:"task_name"` // 可选任务名称;空=未命名
	TaskDescription string    `json:"task_description"`
	TaskStatus      string    `json:"task_status"`
	Count           int       `json:"count"`
	Critical        int       `json:"critical"`
	High            int       `json:"high"`
	Medium          int       `json:"medium"`
	Low             int       `json:"low"`
	LastFoundAt     time.Time `json:"last_found_at"`
}

FindingGroup is one task-level bucket in the global findings view. TaskID is nil for both findings that never had a task and findings retained after task deletion; those records intentionally share one "unassigned/deleted" bucket.

type FindingMeta

type FindingMeta struct {
	TrafficCount int

	ID       int64
	Status   string
	AssetIDs []int64
}

FindingMeta is the standalone-row data (id, triage state, anchored assets) the per-task view grafts onto its exploration-node findings.

type FindingRetest

type FindingRetest struct {
	ID             int64           `json:"id"`
	FindingID      int64           `json:"finding_id"`
	ConversationID *int64          `json:"conversation_id"`
	Status         string          `json:"status"`
	Verdict        string          `json:"verdict"`
	Notes          string          `json:"notes"`
	Snapshot       json.RawMessage `json:"snapshot,omitempty"`
	Summary        string          `json:"summary"`
	Evidence       string          `json:"evidence"`
	Error          string          `json:"error"`
	CreatedAt      time.Time       `json:"created_at"`
	StartedAt      *time.Time      `json:"started_at"`
	FinishedAt     *time.Time      `json:"finished_at"`
}

FindingRetest is an immutable historical test once its conversation turn ends. Snapshot is only loaded for the agent, never sent with the history list.

func (*FindingRetest) InitialMessage

func (r *FindingRetest) InitialMessage() string

type FindingSeverityCounts added in v0.3.14

type FindingSeverityCounts struct{ Critical, High, Medium, Low int }

FindingSeverityCounts breaks a task's findings down by severity for the task list (severity 白名单外/为空的记录不计入任一档)。

type FindingStats

type FindingStats struct {
	Total       int                 `json:"total"`
	Pending     int                 `json:"pending"`
	Critical    int                 `json:"critical"`
	High        int                 `json:"high"`
	Medium      int                 `json:"medium"`
	Low         int                 `json:"low"`
	VulnClasses []string            `json:"vulnclasses"`
	Tasks       []FindingTaskOption `json:"tasks"` // 有漏洞的任务(供「按任务」下拉)
}

FindingStats is the whole-table aggregate powering the 发现 page's stat cards and vuln-class filter — computed server-side so it stays exact regardless of pagination.

type FindingTaskOption

type FindingTaskOption struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"` // 可选任务名称;空=未命名
	Description string `json:"description"`
	Count       int    `json:"count"`
}

FindingTaskOption is one entry in the 发现 page's 任务 filter: a task that has at least one finding, with its description and finding count. Description is empty when the task has since been deleted (finding rows persist), so the frontend falls back to the id.

type FindingTraffic

type FindingTraffic struct {
	FindingID     int64                   `json:"finding_id,string"`
	Version       int64                   `json:"version"`
	ReportVersion int64                   `json:"report_version"`
	Bindings      []FindingTrafficBinding `json:"bindings"`
}

func FindingTrafficTx

func FindingTrafficTx(tx *sql.Tx, findingID int64) (*FindingTraffic, error)

type FindingTrafficBinding

type FindingTrafficBinding struct {
	ID         int64                   `json:"id,string"`
	FindingID  int64                   `json:"finding_id,string"`
	SnapshotID string                  `json:"snapshot_id"`
	Role       string                  `json:"role"`
	Note       string                  `json:"note"`
	Position   int                     `json:"position"`
	CreatedAt  time.Time               `json:"created_at"`
	Snapshot   TrafficEvidenceSnapshot `json:"snapshot"`
}

type GoalCounts

type GoalCounts struct{ Total, Met int }

GoalCounts is the goal summary for one exploration.

type IntentAsset

type IntentAsset struct {
	IntentID      int64  `json:"intent_id"`
	AssetID       int64  `json:"asset_id"`
	Type          string `json:"type"`
	Label         string `json:"label"`
	Source        string `json:"source"`
	SourceSummary string `json:"source_summary"`
	SourceNodeID  *int64 `json:"source_node_id,omitempty"`
	SourceTaskID  int64  `json:"source_task_id"`
	Inherited     bool   `json:"inherited"`
}

IntentAsset describes an asset explicitly anchored to a worker intent.

type IntentCleanup

type IntentCleanup struct {
	Intents    int64 `json:"intents"`
	Facts      int64 `json:"facts"`
	Findings   int64 `json:"findings"`
	Activities int64 `json:"activities"`
}

IntentCleanup summarizes the blackboard records removed when a user cancels one worker intent. Global assets and traffic are deliberately not part of this operation; exploration anchors disappear only because their owning nodes do.

type InterceptApprovalFilter

type InterceptApprovalFilter struct {
	Status         string
	DecisionSource string
}

InterceptApprovalFilter combines exact status and decision-source filters. Empty fields include all values.

type InterceptApprovalRow

type InterceptApprovalRow struct {
	InterceptPending
	ConvTitle    string `json:"conv_title"`
	ConvAgentKey string `json:"conv_agent_key"`
	RuleName     string `json:"rule_name"`
}

InterceptApprovalRow is intercept_pending enriched with conversation and rule info.

type InterceptAudit

type InterceptAudit struct {
	RunID            string                  `json:"run_id,omitempty"`
	ToolUseID        string                  `json:"tool_use_id,omitempty"`
	Correlation      string                  `json:"correlation"` // exact | ambiguous | unavailable
	InputDigest      string                  `json:"input_digest"`
	UserMessage      string                  `json:"user_message"`
	UserTruncated    bool                    `json:"user_truncated,omitempty"`
	Context          []InterceptContextEntry `json:"context"`
	ContextTruncated bool                    `json:"context_truncated,omitempty"`
	CapturedAt       time.Time               `json:"captured_at"`
	ModelFallback    bool                    `json:"model_fallback,omitempty"`
	ModelInput       json.RawMessage         `json:"model_input,omitempty"`
	ModelInputDigest string                  `json:"model_input_digest,omitempty"`
	InitialAction    string                  `json:"initial_action"`
	InitialReason    string                  `json:"initial_reason"`
	EffectiveAction  string                  `json:"effective_action,omitempty"`
	DecisionReason   string                  `json:"decision_reason,omitempty"`
	RuleName         string                  `json:"rule_name,omitempty"`
	ConfigDigest     string                  `json:"config_digest,omitempty"`
	ProfileID        int64                   `json:"profile_id,omitempty"`
	ExecutionStatus  string                  `json:"execution_status"`
	Output           string                  `json:"output,omitempty"`
	OutputTruncated  bool                    `json:"output_truncated,omitempty"`
	ExecutionEndedAt *time.Time              `json:"execution_ended_at,omitempty"`
}

InterceptAudit is captured at review time. It is deliberately excluded from polling/list responses; old rows have no audit instead of reconstructed data.

type InterceptContextEntry

type InterceptContextEntry struct {
	Kind      string `json:"kind"`
	Tool      string `json:"tool,omitempty"`
	ToolUseID string `json:"tool_use_id,omitempty"`
	Text      string `json:"text"`
	IsError   bool   `json:"is_error,omitempty"`
	Truncated bool   `json:"truncated,omitempty"`
}

InterceptContextEntry is a bounded, recorded session event, not model reasoning.

type InterceptDetail

type InterceptDetail struct {
	InterceptApprovalRow
	Audit *InterceptAudit `json:"audit"`
}

type InterceptExecution

type InterceptExecution struct {
	ConversationID *int64     `json:"conversation_id,omitempty"`
	TaskID         *string    `json:"task_id,omitempty"`
	Session        string     `json:"session"`
	Seq            int64      `json:"seq"`
	Items          []Activity `json:"-"`
}

InterceptExecution is a navigation target read from original activity rows. It is not model context and never falls back to matching command text.

type InterceptPending

type InterceptPending struct {
	ID             int64           `json:"id"`
	RuleID         *int64          `json:"rule_id"`
	ConversationID *int64          `json:"conversation_id"`
	TaskID         *string         `json:"task_id"`
	AgentName      string          `json:"agent_name"`
	ToolName       string          `json:"tool_name"`
	ToolInput      json.RawMessage `json:"tool_input"`
	Status         string          `json:"status"`
	DecisionSource string          `json:"decision_source"`
	Reason         string          `json:"reason"` // 规则 message 或模型判定理由(前缀 [模型])
	DecidedAt      *time.Time      `json:"decided_at"`
	CreatedAt      time.Time       `json:"created_at"`
}

InterceptPending is one row of intercept_pending.

type InterceptRule

type InterceptRule struct {
	ID             int64     `json:"id"`
	Name           string    `json:"name"`
	Enabled        bool      `json:"enabled"`
	Priority       int       `json:"priority"`
	MatchTarget    string    `json:"match_target"`
	MatchType      string    `json:"match_type"`
	Pattern        string    `json:"pattern"`
	Action         string    `json:"action"`
	Message        string    `json:"message"`
	TimeoutEnabled bool      `json:"timeout_enabled"`
	TimeoutSeconds int       `json:"timeout_seconds"`
	TimeoutAction  string    `json:"timeout_action"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

InterceptRule is one row of intercept_rules.

type JudgeDayUsage added in v0.3.15

type JudgeDayUsage struct {
	Date         string `json:"date"` // YYYY-MM-DD (UTC)
	Calls        int    `json:"calls"`
	InputTokens  int    `json:"input_tokens"`
	OutputTokens int    `json:"output_tokens"`
}

JudgeDayUsage is one UTC-day token bucket for the intercept fallback judge, for the config page's recent-spend sparkline.

type JudgeUsage added in v0.3.15

type JudgeUsage struct {
	Calls            int             `json:"calls"`
	InputTokens      int             `json:"input_tokens"`
	OutputTokens     int             `json:"output_tokens"`
	CacheReadTokens  int             `json:"cache_read_tokens"`
	CacheWriteTokens int             `json:"cache_write_tokens"`
	Daily            []JudgeDayUsage `json:"daily"`
}

JudgeUsage is the cumulative token spend of the intercept fallback judge (worker='judge' rows in the always-on ledger), plus a recent daily series.

type LLMHealth

type LLMHealth struct {
	ProfileID int64      `json:"profile_id"`
	Fails     int        `json:"fails"`      // consecutive failures; cleared on success
	Trips     int        `json:"trips"`      // total trips, drives the backoff ladder
	OpenUntil *time.Time `json:"open_until"` // nil/past = closed (healthy)
	LastError string     `json:"last_error"`
	LastAt    time.Time  `json:"last_at"`
}

LLMHealth is one profile's circuit-breaker state.

type LLMProfile

type LLMProfile struct {
	ID            int64   `json:"id"`
	Name          string  `json:"name"`
	Format        string  `json:"format"`
	BaseURL       string  `json:"base_url,omitempty"`
	Proxy         string  `json:"proxy,omitempty"` // LLM 出站代理(http/https/socks5);空=用环境变量
	Model         string  `json:"model"`
	APIKey        string  `json:"-"` // never serialized to UI
	APIKeyHint    string  `json:"api_key_hint,omitempty"`
	RatePerSecond float64 `json:"rate_per_second"`
	RatePerMinute float64 `json:"rate_per_minute"`
	// ContextWindowK is the model's context window in K tokens, used to size
	// compaction thresholds. 0 = use a 200K default; capped at 1000 (1M).
	ContextWindowK int `json:"context_window_k"`
	// ThinkingType 独立控制思考「开关」(thinking.type):"" = 不发送(默认);
	// "disabled" = 显式关闭; "enabled" = 开启. 与 ReasoningEffort 解耦.
	ThinkingType string `json:"thinking_type"`
	// ReasoningEffort 独立控制思考「强度」:"" = 不发送(默认);
	// "low"/"medium"/"high"/"xhigh"/"max" = 对应强度. 见 agent.Config.NewProvider.
	ReasoningEffort string `json:"reasoning_effort"`
	IsDefault       bool   `json:"is_default"`
	// Priority orders the failover chain: higher goes first. The ACTIVE profile
	// (IsDefault) always heads the chain regardless of this value.
	Priority int `json:"priority"`
	// PoolExclude=true keeps this profile out of the failover chain — it stays
	// usable when an agent/task binds it explicitly, it just never gets picked up
	// as a fallback target.
	PoolExclude bool `json:"pool_exclude"`
	// Streaming selects the wire protocol: true (default) = streaming (SSE);
	// false = real non-streaming (stream:false, single JSON response via
	// Provider.Complete). Non-streaming sidesteps flaky gateway SSE at the cost of
	// live in-run progress. Maps to agent.Config.Stream.
	Streaming bool `json:"streaming"`
	// MaxTokens caps a single reply's output in tokens. 0 = send no cap and let
	// the endpoint's own default apply (the historical behaviour). Unlike
	// ContextWindowK — the model's total capacity, used locally to size compaction
	// — this value travels with every request.
	MaxTokens int `json:"max_tokens"`
	// MaxTokensField picks the request key carrying MaxTokens, for format
	// "openai" only: "" = max_tokens (default); "max_completion_tokens" = the
	// newer key, which OpenAI's reasoning models require and whose budget covers
	// reasoning tokens plus visible output. Ignored by anthropic and
	// openai-responses, which name the field themselves.
	MaxTokensField string `json:"max_tokens_field"`
	// SessionHeaderKey, when non-empty, names a custom HTTP header sent on every
	// request built from this profile; its value is the current run's session id
	// (chat conversation / worker intent). For gateways that key prompt caching
	// or sticky routing off a session-id header. "" = not sent. Maps to
	// agent.Config.SessionHeaderKey.
	SessionHeaderKey string `json:"session_header_key"`
	// Retry overrides this profile's share of the retry ladder. Zero value =
	// inherit the global policy (LLMRetryPolicy), so an untouched profile behaves
	// exactly as before. See RetryOverride.
	Retry RetryOverride `json:"retry"`
}

type LLMRecord

type LLMRecord struct {
	ID           int64     `json:"id"`
	Ts           time.Time `json:"ts"`
	Model        string    `json:"model"`
	ProfileName  string    `json:"profile_name"`
	SessionID    string    `json:"session_id"`
	TaskID       string    `json:"task_id"`
	Worker       string    `json:"worker"`
	LatencyMs    int       `json:"latency_ms"`
	InputTokens  int       `json:"input_tokens"`
	OutputTokens int       `json:"output_tokens"`
	CacheRead    int       `json:"cache_read"`
	CacheWrite   int       `json:"cache_write"`
	Status       string    `json:"status"`
	Error        string    `json:"error,omitempty"`
	RequestBody  string    `json:"request_body,omitempty"`
	ResponseBody string    `json:"response_body,omitempty"`
	// RawRequest / RawResponse are the untouched HTTP bodies exchanged with the
	// provider — the request as buildBody() sent it (full tool schemas included)
	// and the raw SSE frames. RequestBody/ResponseBody above are the normalized
	// view, which drops tool schemas and tool_use blocks entirely. Empty for
	// records written before this was added, or when the call never reached HTTP.
	RawRequest  string `json:"raw_request,omitempty"`
	RawResponse string `json:"raw_response,omitempty"`
}

LLMRecord is one recorded LLM API call (request + response).

type LLMRetryPolicy

type LLMRetryPolicy struct {
	// Connect:SDK 建连重试(连接重置/超时/429/5xx,流开始前)。默认 3 次、指数退避。
	Connect RetryRule `json:"connect"`
	// Empty:SDK 空响应重试(完成但无 content block,仅 openai 格式)。默认 2 次、指数退避。
	Empty RetryRule `json:"empty"`
	// Stream:同 provider 安全窗口重试(未交付输出前的断流重放)。默认 2 次、0.5s 起指数(封顶 4s)。
	Stream RetryRule `json:"stream"`
	// Breaker:轮询熔断。Attempts=连续几次瞬时失败触发熔断(默认 3,-1=瞬时失败不熔断,
	// 硬失败如余额不足/密钥失效仍立即熔断);IntervalMS=固定冷却时长(0=默认 1/5/30min 梯度)。
	Breaker RetryRule `json:"breaker"`
	// Intent:worker 以 model_error 收场后的整条意图重跑。默认 2 次、固定 3s。
	Intent RetryRule `json:"intent"`
}

LLMRetryPolicy holds the五层 retry configuration. Connect/Empty/Stream are the per-request layers (a profile may override them, see LLMProfile.Retry); Breaker and Intent are process-wide by nature and live only here.

func (LLMRetryPolicy) Clamped

func (p LLMRetryPolicy) Clamped() LLMRetryPolicy

Clamped returns the policy with every rule clamped.

type LLMTask

type LLMTask struct {
	TaskID string `json:"task_id"`
	Count  int    `json:"count"`
}

LLMTask is one distinct task with its LLM-record count.

type LLMUsage

type LLMUsage struct {
	TaskID        string `json:"task_id"`        // task registry id (matches llm_records.task_id)
	ExplorationID int64  `json:"exploration_id"` // exploration id parsed from the session (0 = unknown/non-task)
	Worker        string `json:"worker"`         // agent lane: worker / planner / mainagent / goals
	Model         string `json:"model"`
	ProfileName   string `json:"profile_name"`
	LatencyMs     int    `json:"latency_ms"`
	InputTokens   int    `json:"input_tokens"`
	OutputTokens  int    `json:"output_tokens"`
	CacheRead     int    `json:"cache_read"`
	CacheWrite    int    `json:"cache_write"`
	Status        string `json:"status"` // ok | error
}

LLMUsage is one lightweight LLM-call metering row — the always-on usage ledger, distinct from llm_records (which stores full request/response bodies and is a gated debug feature). One row per completion call, written on both success and error, so token accounting is complete even for interrupted/failed runs. Carries only the dimensions needed to slice token spend (model / profile / task / agent), never any prompt or response content.

type MCPServer

type MCPServer struct {
	ID        int64           `json:"id"`
	Name      string          `json:"name"`
	Transport string          `json:"transport"`
	Command   string          `json:"command,omitempty"`
	Args      json.RawMessage `json:"args"`
	Env       json.RawMessage `json:"env"`
	URL       string          `json:"url,omitempty"`
	Enabled   bool            `json:"enabled"`
	Insecure  bool            `json:"insecure"`        // http: skip TLS cert verification (self-signed servers, issue #108)
	Tools     []string        `json:"tools,omitempty"` // cached tool names (mcp_tools_cache)
}

type MCPTool

type MCPTool struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

MCPTool is one cached tool of an MCP server (name + description).

type MainSession

type MainSession struct {
	Seq       int       `json:"seq"`
	CreatedAt time.Time `json:"created_at"`
}

MainSession is one resettable main-agent conversation segment of a task. Segment 0 is the original session (implicit, never stored); further segments are created by "新建会话" to start the main agent on a clean transcript while the task's graph, assets and goal stay shared.

type ModelTokenStat

type ModelTokenStat struct {
	Model            string `json:"model"`
	Calls            int    `json:"calls"`
	InputTokens      int    `json:"input_tokens"`
	OutputTokens     int    `json:"output_tokens"`
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
}

ModelTokenStat is one model's aggregated token usage for a task, summed from the llm_usage metering ledger (see db/llm_usage.go). Calls is the number of LLM calls that hit this model.

type Node

type Node struct {
	FindingID     int64           `json:"finding_id,omitempty"` // populated by finding-aware reads; never inferred from ID
	FindingNodeID int64           `json:"finding_node_id,omitempty"`
	TrafficCount  int             `json:"traffic_count,omitempty"`
	ID            int64           `json:"id"`
	Kind          string          `json:"kind"`
	Payload       json.RawMessage `json:"payload"`
	Priority      int             `json:"priority"`
	State         string          `json:"state"`
	Origin        string          `json:"origin,omitempty"`
	Owner         string          `json:"owner,omitempty"`
	BlockedReason string          `json:"blocked_reason,omitempty"`
	DeleteReason  string          `json:"delete_reason,omitempty"` // 仅意图假删除(state='deleted')时非空
	Anchors       []int64         `json:"anchors,omitempty"`
	CreatedAt     time.Time       `json:"created_at"`
	SourceTaskID  int64           `json:"source_task_id,omitempty"`
	Inherited     bool            `json:"inherited,omitempty"`
}

Node is a typed reasoning node (= old task_nodes). kind ∈ goal|intent|finding|hint.

type NodeFilter

type NodeFilter struct {
	Kinds  []string // exploration_nodes.kind
	States []string // exploration_nodes.state
	Query  string   // case-insensitive substring over payload text / origin
	Asc    bool     // true = oldest first (replay); default newest first (broadcast)
}

NodeFilter narrows a NodesPage query. Empty fields include every value.

type NotificationChannel added in v0.3.15

type NotificationChannel struct {
	ID     int64           `json:"id"`
	Name   string          `json:"name"`
	Kind   string          `json:"kind"`
	Mode   string          `json:"mode"`
	Config json.RawMessage `json:"config"`
	Filter json.RawMessage `json:"filter"`
	// Enabled 用指针是为了区分「没传这个字段」与「显式传 false」——
	// 前端开关控件只提交被改动的字段。
	Enabled    *bool     `json:"enabled,omitempty"`
	RatePerMin int       `json:"rate_per_min"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

NotificationChannel 是一个渠道实例配置。Config 与 Filter 保持原始 JSON, 解析交给 notify 包——db 层不理解它们的字段含义。

func (*NotificationChannel) IsEnabled added in v0.3.15

func (c *NotificationChannel) IsEnabled() bool

IsEnabled 返回渠道是否启用;Enabled 为 nil(未加载)时按启用处理。

type NotificationDelivery added in v0.3.15

type NotificationDelivery struct {
	ID            int64           `json:"id"`
	EventID       int64           `json:"event_id"`
	ChannelID     int64           `json:"channel_id"`
	State         string          `json:"state"`
	Attempts      int             `json:"attempts"`
	NextAttemptAt time.Time       `json:"next_attempt_at"`
	LastError     string          `json:"last_error"`
	BatchID       *int64          `json:"batch_id,omitempty"`
	CreatedAt     time.Time       `json:"created_at"`
	SentAt        *time.Time      `json:"sent_at,omitempty"`
	Snapshot      json.RawMessage `json:"snapshot,omitempty"`
	// 联合加载的渲染上下文,不进 JSON(由 server 层组装 DTO)。
	Channel *NotificationChannel `json:"-"`
	// FindingID/EventKind 从事件带出,供历史列表直接跳转漏洞详情。
	FindingID int64  `json:"finding_id,string"`
	EventKind string `json:"event_kind"`
	// ChannelName/ChannelKind 是列表展示用的冗余字段,省掉前端二次查询。
	ChannelName string `json:"channel_name"`
	ChannelKind string `json:"channel_kind"`
}

NotificationDelivery 是一条投递任务,含渲染所需的渠道配置与事件快照。

type NotificationDeliveryFilter added in v0.3.15

type NotificationDeliveryFilter struct {
	ChannelID int64
	State     string
	EventKind string
}

NotificationDeliveryFilter 是投递历史的查询条件。

type NotificationEvent added in v0.3.15

type NotificationEvent struct {
	ID        int64           `json:"id"`
	Kind      string          `json:"kind"`
	FindingID int64           `json:"finding_id"`
	Snapshot  json.RawMessage `json:"snapshot"`
	CreatedAt time.Time       `json:"created_at"`
}

NotificationEvent 是一条事件事实。

type NotificationStats added in v0.3.15

type NotificationStats struct {
	Channels     int   `json:"channels"`
	ChannelsOn   int   `json:"channels_on"`
	Pending      int   `json:"pending"`
	Failed       int   `json:"failed"`
	SentToday    int   `json:"sent_today"`
	BacklogAgeMS int64 `json:"backlog_age_ms"` // 最老的待发投递距今毫秒数
}

NotificationStats 是通知页顶部的概览计数。

type ParamItem

type ParamItem struct {
	Location string `json:"location"`
	Name     string `json:"name"`
	Value    string `json:"value,omitempty"`
	Type     string `json:"type,omitempty"`
}

ParamItem is one entry in the params array.

type ParsedScope

type ParsedScope struct {
	Kind   string // "domain" | "ip" | "cidr" | "icp" | "keyword"
	Domain string // normalized registrable/root domain (kind=domain)
	Net    string // normalized CIDR, single IP as /32 or /128 (kind=ip|cidr)
	Value  string // normalized text (kind=icp|keyword)
	Raw    string // original input line
}

ParsedScope is one parsed asset-scope entry. Its kind selects Domain, Net, or Value. Used internally by the company scope parsers and CompanyStore.

func ParseAutoScopeLine

func ParseAutoScopeLine(line string) (ParsedScope, error)

ParseAutoScopeLine classifies one untyped textarea line. Network-looking and domain-looking values remain strict so malformed ranges do not silently become Agent keywords; all other non-empty text is a keyword.

func ParseScopeInput

func ParseScopeInput(input ScopeInput) (ParsedScope, error)

ParseScopeInput validates an explicitly typed rule. Legacy callers can omit Kind and use the same automatic classification as the single-textarea UI.

func ParseScopeLine

func ParseScopeLine(line string) (ParsedScope, error)

ParseScopeLine classifies and validates one scope line (root domain / IP / CIDR). Guardrails reject bare TLDs and over-broad networks so a rule can never swallow the internet. IP ranges must be expressed as CIDR.

type PortService

type PortService struct {
	Port    int    `json:"port"`
	Service string `json:"service,omitempty"`
}

PortService is one entry in open_ports: {"port":22,"service":"ssh"}.

type PreparedTrafficEvidence

type PreparedTrafficEvidence struct {
	Ref      TrafficRef
	Snapshot TrafficEvidenceSnapshot
}

type ProfileDayUsage

type ProfileDayUsage struct {
	ProfileName     string `json:"profile_name"`
	Date            string `json:"date"` // YYYY-MM-DD (UTC)
	InputTokens     int    `json:"input_tokens"`
	OutputTokens    int    `json:"output_tokens"`
	CacheReadTokens int    `json:"cache_read_tokens"`
}

ProfileDayUsage is one (profile, UTC calendar day) token bucket for the daily chart. Unlike the activity-based chart, ts is the real call time, so this is actual per-day consumption rather than tokens bucketed by task creation date.

type ProfileUsage

type ProfileUsage struct {
	ProfileName      string `json:"profile_name"`
	Calls            int    `json:"calls"`
	Tasks            int    `json:"tasks"`
	InputTokens      int    `json:"input_tokens"`
	OutputTokens     int    `json:"output_tokens"`
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
}

ProfileUsage aggregates the whole ledger's token spend for one LLM profile (global, all tasks). Powers the dashboard's per-profile token card (new source).

type PromptVar

type PromptVar struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Example     string `json:"example"`
	Source      string `json:"source"`
}

type PromptVersion

type PromptVersion struct {
	Version   int       `json:"version"`
	Template  string    `json:"template_text"`
	Note      string    `json:"note,omitempty"`
	CreatedAt time.Time `json:"ts"`
}

type RecordFindingInput

type RecordFindingInput struct {
	TaskID, ExplorationID, IntentID                      int64
	VulnClass, Name, Severity, Summary, Evidence, Worker string
	AssetIDs                                             []int64
}

type RecordedFinding

type RecordedFinding struct {
	FindingID int64           `json:"finding_id,string"`
	NodeID    int64           `json:"finding_node_id,string"`
	Traffic   *FindingTraffic `json:"traffic"`
}

func RecordFindingTx

func RecordFindingTx(ctx context.Context, tx *sql.Tx, in RecordFindingInput, prepared []PreparedTrafficEvidence) (*RecordedFinding, error)

ctx 由调用方传入本次事务所用的上下文(而非在内部取 context.Background): 事务内新加的推送事件写入同样应受调用方的取消与超时约束。

type RetryOverride

type RetryOverride struct {
	Connect RetryRule `json:"connect"`
	Empty   RetryRule `json:"empty"`
	Stream  RetryRule `json:"stream"`
}

RetryOverride is one profile's optional override of the three retry layers that are per-endpoint: 建连(connect) / 空响应(empty) / 同 provider 安全窗口 (stream). Each rule's zero value means "inherit the global policy"; see RetryRule for the -1 / 0 / >0 semantics.

func (RetryOverride) Clamped

func (o RetryOverride) Clamped() RetryOverride

Clamped bounds a profile's override the same way the global policy is bounded, so a hand-crafted API payload can't land a value the CHECK constraint rejects.

type RetryRule

type RetryRule struct {
	Attempts   int `json:"attempts"`
	IntervalMS int `json:"interval_ms"`
}

RetryRule is one layer's knob pair. The zero value means "unset":

Attempts   0 = 用内置默认次数; -1 = 关闭该层重试; >0 = 用该值
IntervalMS 0 = 用该层原本的间隔策略(通常是指数退避); >0 = 改用固定毫秒间隔

-1 是「显式关掉」而不是「0 次」,因为 0 已经被「未配置」占用了。

func (RetryRule) Clamped

func (r RetryRule) Clamped() RetryRule

Clamped returns the rule with out-of-range values pulled back into the sane band (attempts within [-1, 20], interval within [0, 1h]).

func (RetryRule) Interval

func (r RetryRule) Interval() time.Duration

Interval returns the configured fixed interval, or 0 when unset (caller keeps its own default ladder).

func (RetryRule) Or

func (r RetryRule) Or(fallback RetryRule) RetryRule

Or returns the rule with each unset field filled in from fallback. Used to layer a profile override on top of the global policy field by field, so a profile that only pins the interval still inherits the global count.

type ScopeInput

type ScopeInput struct {
	Kind  string `json:"kind,omitempty"`
	Value string `json:"value"`
}

ScopeInput is the structured API form for a company scope rule. Empty Kind uses the same automatic classification as the single-textarea UI.

type ScopeRule

type ScopeRule struct {
	ID        int64  `json:"id"`
	CompanyID int64  `json:"company_id"`
	Kind      string `json:"kind"`
	Domain    string `json:"domain,omitempty"`
	Net       string `json:"net,omitempty"`
	Value     string `json:"value,omitempty"`
	Raw       string `json:"raw"`
	Reason    string `json:"reason,omitempty"`
}

ScopeRule is one company_scope row.

type SessionTokenUsage

type SessionTokenUsage struct {
	Session          string `json:"session"`
	InputTokens      int    `json:"input_tokens"`
	OutputTokens     int    `json:"output_tokens"`
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
}

SessionTokenUsage is the authoritative completed-run total for one task UI session. Worker sessions are keyed by intent rather than the reusable work#N executor name, so retries and reassignment remain attached to the same row.

type SkillCall

type SkillCall struct {
	TS        time.Time `json:"ts"`
	AgentKey  string    `json:"agent_key"`
	TaskID    int64     `json:"task_id"`
	SessionID string    `json:"session_id"`
	ArgsLen   int       `json:"args_len"`
}

SkillCall is one row of a skill's recent-call list (detail panel).

type SkillStat

type SkillStat struct {
	Skill    string     `json:"skill"`
	Calls    int        `json:"calls"`
	Tasks    int        `json:"tasks"`     // distinct tasks that loaded it (chat runs excluded)
	Agents   []string   `json:"agents"`    // agent keys that loaded it, most-used first
	LastUsed *time.Time `json:"last_used"` // nil when never called
}

SkillStat is one skill's aggregate usage, for the skills page.

type SkillUsage

type SkillUsage struct {
	Skill         string `json:"skill"`          // skill directory name (matches agent_skill_visibility.skill_name)
	AgentKey      string `json:"agent_key"`      // worker / planner / mainagent / custom agent key
	TaskID        int64  `json:"task_id"`        // 0 for non-task runs (chat sessions)
	ExplorationID int64  `json:"exploration_id"` // 0 when unknown
	IntentID      int64  `json:"intent_id"`      // worker's intent node; 0 for planner/mainagent/chat
	SessionID     string `json:"session_id"`     // chat conversation id; empty for task runs
	ArgsLen       int    `json:"args_len"`
	Found         bool   `json:"found"` // false = the model named a skill that does not exist
}

SkillUsage is one Skill() invocation — the always-on skill call ledger. Written from the Skill meta-tool's OnInvoke hook (server/assembly.go), one row per load. Carries only dimensions (which skill, which agent, which task/session), never the caller's args text — args_len is kept so an "empty vs substantial context" split is still possible without storing prompt content, mirroring llm_usage.

Rows deliberately outlive their task: skill_usage has no foreign keys, so deleting a task keeps its skill statistics intact (same rationale as llm_usage).

type StampOp

type StampOp struct {
	ID    int64
	Set   bool
	Round int64
}

StampOp is one cold_since_round change: Set stamps Round; !Set clears it.

type Task

type Task struct {
	ID            int64      `json:"id"`
	Name          string     `json:"name"` // 可选任务名称;空=未命名
	CategoryID    *int64     `json:"category_id,omitempty"`
	CategoryName  string     `json:"category_name,omitempty"`
	Pinned        bool       `json:"pinned"`
	PinnedAt      *time.Time `json:"pinned_at,omitempty"`
	Description   string     `json:"description"`
	Goal          string     `json:"goal"`
	ExplorationID int64      `json:"exploration_id"`
	Status        string     `json:"status"`
	Paused        bool       `json:"paused"`
	Queued        bool       `json:"queued"`
	QueuedAt      *time.Time `json:"queued_at,omitempty"`
	QueueMode     string     `json:"queue_mode,omitempty"`
	LLMProfileID  *int64     `json:"llm_profile_id,omitempty"`
	// Task-level ordered LLM chain. LLMProfileID remains the compatibility alias
	// for ActiveLLMProfileID while older API clients still send one profile id.
	LLMProfileIDs      []int64    `json:"llm_profile_ids,omitempty"`
	ActiveLLMProfileID *int64     `json:"active_llm_profile_id,omitempty"`
	LLMChainRevision   int64      `json:"-"`
	LLMFailoverState   string     `json:"llm_failover_state,omitempty"`
	LLMFailoverReason  string     `json:"llm_failover_reason,omitempty"`
	SourceTaskIDs      []int64    `json:"source_task_ids,omitempty"`
	CompanyIDs         []int64    `json:"company_ids,omitempty"`
	ParentRef          string     `json:"parent_ref,omitempty"` // 父任务 id(编排 spawn 记录;空=顶层)
	CreatedAt          time.Time  `json:"created_at"`
	CompletedAt        *time.Time `json:"completed_at,omitempty"` // 进入终态(done/failed/timeout)的时刻;非终态为 nil
	// 任务级超时(见 docs/任务级超时与收尾设计.md)。
	TimeoutSeconds int        `json:"timeout_seconds"`        // 0=不限时
	FirstRunAt     *time.Time `json:"first_run_at,omitempty"` // 首次真正开始运行的时刻(非 created_at);nil=尚未运行
	DeadlineAt     *time.Time `json:"deadline_at,omitempty"`  // = first_run_at + timeout_seconds;nil=不限或未运行
	// planner 心跳触发间隔(秒):距上轮 plan 结束/任务开始满该值且期间无触发 → 触发一轮。
	// 下限=默认=300(5min),低于一律抬到 300(在 CreateTask 归一)。见 docs/planner-trigger-impl-plan.md
	PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"`
	// CoverageEnabled 是「资产覆盖度功能」总开关(默认 true)。false 时:不计算/不展示测试
	// 覆盖度、不自动累积 task_scope(source=auto)、不给 agent 开放 add_task_scope/
	// list_untested_assets、态势里不注入 coverage 块(scope 字段仍保留)。company 关联
	// (task_scope kind=company)与此开关无关,永不受影响。见 db/task_scope.go。
	CoverageEnabled bool `json:"coverage_enabled"`
}

Task is a row in the task registry (1:1 with an exploration).

type TaskArchive

type TaskArchive struct {
	ID                      int64           `json:"id"`
	TaskID                  int64           `json:"task_id"`
	State                   string          `json:"state"`
	Phase                   string          `json:"phase"`
	Progress                int             `json:"progress"`
	Error                   string          `json:"error,omitempty"`
	Warnings                json.RawMessage `json:"warnings"`
	FormatVersion           int             `json:"format_version"`
	ArchivePath             string          `json:"-"`
	SHA256                  string          `json:"sha256,omitempty"`
	OriginalSize            int64           `json:"original_size"`
	CompressedSize          int64           `json:"compressed_size"`
	TaskName                string          `json:"task_name"`
	TaskDescription         string          `json:"task_description"`
	TaskGoal                string          `json:"task_goal"`
	OriginalStatus          string          `json:"original_status"`
	CategoryIDSnapshot      *int64          `json:"category_id,omitempty"`
	CategoryNameSnapshot    string          `json:"category_name,omitempty"`
	SourceTaskIDs           []int64         `json:"source_task_ids"`
	RemainingTimeoutSeconds int64           `json:"remaining_timeout_seconds"`
	DataCounts              json.RawMessage `json:"data_counts"`
	AggregateStats          json.RawMessage `json:"aggregate_stats"`
	ArchivedAt              *time.Time      `json:"archived_at,omitempty"`
	RequestedAt             time.Time       `json:"requested_at"`
	CreatedAt               time.Time       `json:"created_at"`
	UpdatedAt               time.Time       `json:"updated_at"`
}

TaskArchive is the compact PostgreSQL record retained while a task is cold. Sensitive profile configuration and API keys are intentionally absent.

type TaskArchivePage

type TaskArchivePage struct {
	Items []TaskArchive `json:"items"`
	Total int           `json:"total"`
	Page  int           `json:"page"`
	Size  int           `json:"size"`
}

type TaskArchiveSnapshot

type TaskArchiveSnapshot struct {
	FormatVersion     int                        `json:"format_version"`
	CreatedAt         time.Time                  `json:"created_at"`
	TaskID            int64                      `json:"task_id"`
	ExplorationID     int64                      `json:"exploration_id"`
	SourceTaskIDs     []int64                    `json:"source_task_ids"`
	Hosts             []string                   `json:"hosts"`
	ExclusiveHosts    []string                   `json:"exclusive_hosts"`
	ExclusiveAssetIDs []int64                    `json:"exclusive_asset_ids"`
	Tables            map[string]json.RawMessage `json:"tables"`
	StreamedTables    map[string]string          `json:"streamed_tables,omitempty"`
	DataCounts        map[string]int64           `json:"data_counts"`
	AggregateStats    map[string]any             `json:"aggregate_stats"`
}

TaskArchiveSnapshot is serialized into manifest.json inside the cold package. Small tables remain JSON arrays in Tables. Large v2 tables are streamed to package files listed in StreamedTables so their size is not bounded by memory.

type TaskAssetMutation

type TaskAssetMutation struct {
	Requested int `json:"requested"`
	Attached  int `json:"attached"`
	Existing  int `json:"existing"`
}

TaskAssetMutation summarizes one attach request. Attached counts newly added associations; Existing counts requested assets that were already on the task.

type TaskAssetScopeMutation

type TaskAssetScopeMutation struct {
	Requested      int `json:"requested"`
	AssetsLinked   int `json:"assets_linked"`
	AssetsExisting int `json:"assets_existing"`
	ScopesAdded    int `json:"scopes_added"`
	ScopesExisting int `json:"scopes_existing"`
}

TaskAssetScopeMutation summarizes one free-form scope registration. Domain and IP entries create or reuse global assets; every entry also becomes an idempotent task_scope row.

type TaskCategory

type TaskCategory struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	NKey      string    `json:"-"`
	TaskCount int       `json:"task_count"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

TaskCategory is a globally reusable task grouping label.

type TaskCreateOptions

type TaskCreateOptions struct {
	Name                 string // 可选任务名称;空=未命名
	CategoryID           *int64
	SourceTaskIDs        []int64
	CompanyIDs           []int64
	LLMProfileIDs        []int64
	TimeoutSeconds       int
	PlanHeartbeatSeconds int
	// CoverageEnabled 是「资产覆盖度功能」开关;nil=默认开(true),让不关心该开关的创建
	// 路径(编排 spawn、老 API)沿用原行为。仅 web 创建任务时可显式传 false 关闭。
	CoverageEnabled *bool
	// InterceptRules 是任务级资产拦截规则,创建时随任务在同一事务内写入 task_intercept_rules。
	InterceptRules []TaskInterceptRuleInput
}

TaskCreateOptions contains the task data that must be committed atomically with the task/exploration row.

type TaskDeletePreparation

type TaskDeletePreparation struct {
	ExplorationID int64
	TrafficHosts  []string
}

TaskDeletePreparation is produced inside the PostgreSQL deletion transaction after asset and anchor writers have been excluded. Prepare callbacks may use TrafficHosts to stage an external traffic deletion before PostgreSQL commits.

type TaskDeleteResult

type TaskDeleteResult struct {
	AssetsDeleted     int64
	AssetsDetached    int64
	FindingsDeleted   int64
	LLMRecordsDeleted int64
}

TaskDeleteResult reports optional related-data cleanup performed in the same transaction as the task/exploration delete.

type TaskEvent

type TaskEvent struct {
	NodeID     int64  `json:"node_id"`
	TaskID     int64  `json:"task_id"`
	TaskDesc   string `json:"task_description"`
	TaskGoal   string `json:"task_goal"`
	Summary    string `json:"summary"`     // finding summary / goal text
	VulnClass  string `json:"vulnclass"`   // finding only
	Severity   string `json:"severity"`    // finding only
	Tool       string `json:"tool"`        // tool-call only: tool name
	ToolInput  string `json:"tool_input"`  // tool-call only: 入参(JSON 文本)
	ToolOutput string `json:"tool_output"` // tool-call only: 返回内容
	ToolIsErr  bool   `json:"tool_is_err"` // tool-call only: 工具返回是否为错误
}

TaskEvent is a finding/goal event carrying the owning task's info, used to compose the trigger message context.

type TaskInterceptRuleInput

type TaskInterceptRuleInput struct {
	Enabled bool   `json:"enabled"`
	Action  string `json:"action"`
	Kind    string `json:"kind"`
	Pattern string `json:"pattern"`
	Note    string `json:"note"`
}

TaskInterceptRuleInput is one task-level rule supplied at task creation. Action: 'block'=拦截 'allow'=允许(白名单);空视为 'block'。

type TaskLLMProfile

type TaskLLMProfile struct {
	ProfileID   int64      `json:"profile_id"`
	Position    int        `json:"position"`
	Status      string     `json:"status"`
	LastError   string     `json:"last_error,omitempty"`
	ExhaustedAt *time.Time `json:"exhausted_at,omitempty"`
}

TaskLLMProfile is one ordered entry in a task's explicit failover chain.

type TaskLLMTransition

type TaskLLMTransition struct {
	PreviousProfileID int64
	NextProfileID     *int64
	ChainExhausted    bool
	Advanced          bool
	// Stale means the chain was replaced after the failing call selected its
	// provider. The caller may retry a pre-stream request against the new chain,
	// but must not report or persist a transition for this result.
	Stale bool
}

TaskLLMTransition reports the shared task-level result of marking one profile quota-exhausted. NextProfileID is nil when the explicit chain is exhausted.

type TaskListMetrics

type TaskListMetrics struct {
	Tokens         TokenUsage
	LastActivity   int64
	Goals          GoalCounts
	RunningIntents int                   // kind='intent' 且 state='running' 的条数,即运行中 Worker 数
	Findings       FindingSeverityCounts // findings 表里该任务的漏洞数(按严重度分档)
}

TaskListMetrics contains the aggregates rendered in task lists.

type TaskPatch

type TaskPatch struct {
	Name   *string
	Pinned *bool
}

TaskPatch updates task list metadata without changing execution state.

type TaskScope

type TaskScope struct {
	ID        int64  `json:"id"`
	TaskID    int64  `json:"task_id"`
	Kind      string `json:"kind"` // company|root_domain|subdomain|ip|cidr|icp|keyword
	CompanyID *int64 `json:"company_id,omitempty"`
	// CompanyName is resolved for kind=company so callers can label a scope row
	// without a second lookup. Empty when the row is not a company reference.
	CompanyName string `json:"company_name,omitempty"`
	Domain      string `json:"domain,omitempty"`
	Net         string `json:"net,omitempty"`
	Value       string `json:"value,omitempty"`
	Source      string `json:"source"`
	Reason      string `json:"reason,omitempty"`
}

TaskScope is one row of a task's test scope — the coverage denominator and the per-task authorization edge. Rows come either from insertAssets (source='auto', conservative, one per explicitly-inserted asset) or from the add_task_scope tool (source='agent', for company / domain / network / ICP / keyword scope).

type TaskSource

type TaskSource struct {
	TaskID        int64
	ExplorationID int64
	Description   string
	Goal          string
	Status        string
}

TaskSource identifies one directly related task and its exploration.

type TaskTemplate

type TaskTemplate struct {
	ID             int64                    `json:"id"`
	Name           string                   `json:"name"`
	NKey           string                   `json:"-"`
	Description    string                   `json:"description"`
	Goal           string                   `json:"goal"`
	CategoryID     *int64                   `json:"category_id"`
	InterceptRules []TaskInterceptRuleInput `json:"intercept_rules"`
	CreatedAt      time.Time                `json:"created_at"`
	UpdatedAt      time.Time                `json:"updated_at"`
}

TaskTemplate is a reusable task preset (description/goal + optional category and task-level intercept/allow rules).

type TaskTemplateInput

type TaskTemplateInput struct {
	Name           string
	Description    string
	Goal           string
	CategoryID     *int64
	InterceptRules []TaskInterceptRuleInput
}

TaskTemplateInput is the create/update payload after normalization.

type TaskTemplatePatch

type TaskTemplatePatch struct {
	Name              *string
	Description       *string
	Goal              *string
	CategoryID        *int64
	SetCategoryID     bool
	InterceptRules    []TaskInterceptRuleInput
	SetInterceptRules bool
}

TaskTemplatePatch changes only fields flagged as set. Name/Description/Goal use non-nil pointers; CategoryID/InterceptRules use explicit Set flags (so a nil CategoryID can mean "clear" when SetCategoryID is true).

type TokenUsage

type TokenUsage struct {
	Worker           string `json:"worker"`
	InputTokens      int    `json:"input_tokens"`
	OutputTokens     int    `json:"output_tokens"`
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
}

TokenUsage is a per-worker token aggregate (TokenStatsByWorker).

type Tool

type Tool struct {
	Key         string          `json:"key"`
	System      bool            `json:"system"`
	Description string          `json:"description"`
	Schema      json.RawMessage `json:"schema"`
	Agents      []string        `json:"agents"`
	Enabled     bool            `json:"enabled"`
	Kind        string          `json:"kind"`     // builtin | command | script | http
	Exec        json.RawMessage `json:"exec"`     // 自定义工具执行规格(kind!=builtin)
	Deferred    bool            `json:"deferred"` // schema 延迟(走 SearchExtraTools/ExecuteExtraTool)
	Calls       int             `json:"calls"`    // runtime ledger aggregate; not stored in tools
}

Tool is one row of the built-in tool catalog. key + handler live in code; this row carries only the page-editable surface: description, parameter schema (structure read-only, per-param description/default editable), agent binding, and the on/off switch. See schema.sql §H and agent/toolcatalog.go.

type ToolStat

type ToolStat struct {
	Tool   string `json:"tool"`
	Total  int    `json:"total"`
	Errors int    `json:"errors"`
}

ToolStat is one tool's execution tally for the usage summary.

type ToolUsage

type ToolUsage struct {
	ToolKey       string `json:"tool_key"`
	AgentKey      string `json:"agent_key"`
	TaskID        int64  `json:"task_id"`
	ExplorationID int64  `json:"exploration_id"`
	IntentID      int64  `json:"intent_id"`
	SessionID     string `json:"session_id"`
}

ToolUsage is one catalog tool invocation. It stores attribution dimensions only: tool arguments and results are deliberately excluded from the ledger.

type TrafficEvidenceSnapshot

type TrafficEvidenceSnapshot struct {
	ID              string    `json:"id"`
	SourceTrafficID string    `json:"source_traffic_id"`
	CapturedAt      int64     `json:"captured_at"`
	URL             string    `json:"url"`
	Method          string    `json:"method"`
	Status          int       `json:"status"`
	ContentType     string    `json:"content_type"`
	ReqHead         string    `json:"req_head,omitempty"`
	RespHead        string    `json:"resp_head,omitempty"`
	ReqHash         string    `json:"req_hash"`
	RespHash        string    `json:"resp_hash"`
	ReqLen          int64     `json:"req_len"`
	RespLen         int64     `json:"resp_len"`
	CreatedAt       time.Time `json:"created_at"`
}

func ArchiveEvidenceSnapshots

func ArchiveEvidenceSnapshots(snapshot *TaskArchiveSnapshot) ([]TrafficEvidenceSnapshot, error)

func (TrafficEvidenceSnapshot) Normalize

Normalize makes the snapshot's text columns safe for PostgreSQL. URL and the head blocks come straight off the wire, so a target answering with a non-UTF-8 header (a GBK `Content-Disposition: filename=…`, a NUL byte) would otherwise abort the INSERT and roll back the whole finding — losing a confirmed finding over a malformed response header. Applied before hashing so the ID always matches the bytes that actually land in the table.

type TrafficRef

type TrafficRef struct {
	TrafficID string `json:"traffic_id"`
	Role      string `json:"role"`
	Note      string `json:"note"`
}

func NormalizeTrafficRefs

func NormalizeTrafficRefs(refs []TrafficRef) ([]TrafficRef, error)

type UpsertAppReq

type UpsertAppReq struct {
	Name        string
	BundleID    string
	Category    string
	Description string
	ICP         string
	CompanyID   *int64 // explicit override; nil = exact ICP auto-attribution when available
	TaskID      int64
}

UpsertAppReq is the input for UpsertApp.

type UpsertEndpointReq

type UpsertEndpointReq struct {
	URL    string
	Method string
	Params []map[string]any
	IP     string // optional
	TaskID int64
}

UpsertEndpointReq is the input for UpsertEndpoint.

type UpsertHTTPServiceReq

type UpsertHTTPServiceReq struct {
	URL           string
	Technologies  []string
	StatusCode    *int
	ContentLength *int64
	PageTitle     string
	FaviconMMH3   string
	Auth          []map[string]any
	IP            string // optional, from async DNS
	TaskID        int64
}

UpsertHTTPServiceReq is the input for UpsertHTTPService.

type UpsertIPReq

type UpsertIPReq struct {
	IP           string
	BoundDomains []string
	OpenPorts    []PortService
	TaskID       int64
}

UpsertIPReq is the input for UpsertIP.

type UpsertOtherServiceReq

type UpsertOtherServiceReq struct {
	Domain      string // domain or ip required
	IP          string
	Port        int
	ServiceName string
	Auth        []map[string]any
	TaskID      int64
}

UpsertOtherServiceReq is the input for UpsertOtherService.

type UpsertRootDomainReq

type UpsertRootDomainReq struct {
	Domain string
	ICP    string
	TaskID int64
}

UpsertRootDomainReq is the input for UpsertRootDomain.

type UpsertSubdomainReq

type UpsertSubdomainReq struct {
	Domain      string
	RecordType  string
	RecordValue []string
	ICP         string
	TaskID      int64
}

UpsertSubdomainReq is the input for UpsertSubdomain.

Jump to

Keyboard shortcuts

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