71 Commits
Author SHA1 Message Date
BaaRai 101c92d516 Coverage ledger: review fixes (one-line capped fields, framed gap lines, per-run end_reason)
Normalise and length-check ledger fields in db.RecordCoverage, frame the planner gap lines in the data envelope, cap the wrap-up (25) and report (200) gap lists, cap current rows at 2000 per task. Replace the activity budget marker with tasks.end_reason, written with the guarded terminal status and cleared on revive. Generic 500 message in taskCoverage; early return on an empty ledger in the planner block.
2026-11-12 20:48:34 +01:00
BaaRai 6f3a59e205 Coverage ledger: record examined, ruled-out and untested risk classes
Workers record per (surface, risk_area) outcomes with the new record_coverage
tool (reported, no_issue_found, ruled_out, not_applicable, needs_follow_up).
Gaps are needs_follow_up entries, entries missing their required evidence or
reason, and goal vulnclass values with no entry (unrecorded_risk_class), so an
absent finding never reads as a clean target. Completeness marks timeout,
budget wrap-up, pause, abort and running tasks as incomplete with a caveat.

Surfaced in the planner context (one line per round, full gap list on the
timeout/budget wrap-up round), GET /api/tasks/{id}/coverage, the coverage card
and the Markdown report. Follows coverage_enabled; no new env var.
2026-11-12 20:16:23 +01:00
BaaRai cd8a830b53 docs(coverage): say "declared domain or IP root" for auto-scope roots (F8)
declaredScopeFilter treats only root_domain, subdomain, ip and cidr rows as
roots. A declared company, icp or keyword row does not make an auto row
count, but the add_task_scope description said "a goal target or scope
added here or manually". The description and the code comments now say
"declared domain or IP root" and name the kinds that act as roots.
2026-11-12 09:38:52 +01:00
BaaRai 37fab18809 fix(coverage): count auto-scope only under a declared root (F8)
Auto task_scope rows (source='auto') come from insert_assets and intent
asset_ids, i.e. from target-derived content. They now count toward the
coverage denominator and list_untested_assets only when they sit under a
root the same task declared (a non-auto row: goal target registered via
add_task_scope, or manual scope): the same or a deeper domain of a declared
root_domain/subdomain, or a net inside a declared ip/cidr.

The direct-source coverage view also stops counting an asset from an intent
anchor alone; fact anchors still count, so tested evidence is unchanged.
Without this, insert_assets put a planted host back into the backlog
through its intent anchor.

task_scope is described as "coverage scope, not enforced" in the code
comments, the add_task_scope description, the goal-decomposer tail and the
task overview UI, instead of "authorization boundary".

Audit F8, 0.7.0.
2026-11-12 07:37:58 +01:00
BaaRai 8c9c1fc0ab feat(findings): optional counterevidence and confidence_rationale
Add two optional free-text fields to findings so a reporter can record
what was tested and did not work (counterevidence) and why the
conclusion holds (confidence_rationale).

- db: idempotent ADD COLUMN IF NOT EXISTS, TEXT NOT NULL DEFAULT '',
  no CHECK; RecordFindingInput, the INSERT, the shared select list and
  all three scan sites carry them.
- agent: report_finding accepts both as optional strings; the worker
  and pentest prompts say to fill them when confidence is not high.
- server: FindingDTO (list and detail) carries both, omitted when empty;
  existing databases get the two schema properties added to the stored
  report_finding row once, without overwriting user edits.
- web: the finding detail page shows each section only when non-empty.

Both fields stay optional: a missing field never refuses a finding.
2026-11-11 23:43:12 +01:00
BaaRai 1f4cb65ca6 feat(intercept): target-gate schema, opt-in flag and approval requests (S3b 1.2-1.4)
Storage, flag and request path of the approval gate for asset targets. The
gate itself is not wired into the tool hook yet (steps 1.5 to 1.7): with this
change nothing is extracted, resolved or blocked, whatever the flag says.

Schema and store (1.2)
- tasks.target_gate, conversations.target_gate (default false).
- intercept_pending.target_key and a partial unique index: one open request
  per scope (task or conversation) and host:port.
- target_pins (no foreign keys: a pin outlives its task, its conversation and
  its request row) and target_gate_exemption_log. All DDL is idempotent; no
  CHECK constraints; the cascade on intercept_pending.conversation_id stays.
- db/target_pins.go: pin lookup with port precedence, list, revoke,
  find-or-create of a request, and ResolveTargetRequest, which settles the
  row, supersedes the old pin and writes the new version in one transaction.
- Deleting a task or an archive stub revokes its pins and settles its open
  target requests in the delete transaction. DeleteConversation and
  DeleteConversations revoke the pins of the removed conversations in the
  same transaction as the delete.

Opt-in flag (1.3)
- Per task through the create and metadata-patch paths, per conversation
  through create and patch; exposed in the task DTO. A retest conversation
  takes the flag of the finding's task, never its pins. A spawned task starts
  with the gate off.
- The task flag is atomic and read on every call through TargetScope.Enabled.
  TargetScope now carries the Interceptor that owns the gate.

Request, wait, decide (1.4)
- pendingManager keeps a list of waiters per request; resolve wakes all.
- RequestTargets: find-or-create per target, one card per request, wait on
  channel, poll, context and wait bound. A plain request is never settled by
  a call that leaves. A request merged with an ask is bound to its creating
  call and a timeout never allows it.
- Decide records the approver (subject of the session token) and the remote
  address for every human decision, classic asks included. On a target
  request "allowed" pins the displayed addresses; that is the only writer of
  pins.
- A request raised while the global egress proxy is set says the approval is
  name-only and its pin is stored with name_only_effective.
- Routes: GET/POST .../target-pins and .../target-pins/{pid}/revoke under
  /api/tasks/{id} and /api/conversations/{id}; decision_source=target filter.
- ValidateExemptionHost: an exemption written as an IP literal may be private
  unicast, never loopback, link-local or metadata, unspecified or multicast.
2026-11-10 23:05:47 +01:00
BaaRai c0e713d32f feat(engine): intent claim heartbeat, TTL and fencing token (S3c)
An intent claim was atomic but had no heartbeat, no expiry and no owner
token: startup reset every running intent to open unconditionally, so a
second instance on the same database stole live claims, and the worker
that lost its intent could still write the result.

- schema: last_heartbeat_at and unclaim_token on exploration_nodes
  (idempotent ADD COLUMN IF NOT EXISTS, nullable, no CHECK, no backfill).
- db: every claim (open->running and the paused->running message path)
  gets a fresh crypto/rand token and starts its lease in the same UPDATE.
  HeartbeatIntentClaim renews it for that token only.
- TTL: db.IntentClaimTTL = 90s, heartbeat every 15s
  (db.IntentClaimHeartbeatInterval), both on the database clock.
- takeover: ResetRunningIntents now reopens only claims whose heartbeat
  is older than the TTL, or NULL (rows claimed before the columns
  existed). The engine runs it per task every 15s as well as at startup.
- fencing: a heartbeat or result write whose token is not the current
  one returns db.ErrIntentClaimLost and writes nothing. All intent state
  writes in runIntent go through the fenced SettleIntentClaim /
  SettleIntentClaimBlocked; a rejected heartbeat cancels the stale run
  with the new work_claim_lost cause.

Behaviour change: after a restart, intents that were in flight are
resumed once their claim expires (at most 105s) instead of immediately.

Not in this change: fencing of the facts, findings and activity a worker
writes during its run; they are not guarded by the token.
2026-11-10 17:45:11 +01:00
BaaRai 8f50b81792 feat: record which actor decided an intercept approval
Add intercept_pending.decided_by (human | rule | model | system; empty while
pending or for rows decided before the column existed). Approval semantics are
unchanged: only the actor is recorded.

- rule-matched and model-judged rows written by CreateDecidedIntercept record
  rule or model;
- ResolveIntercept takes the actor explicitly and rejects any other value
  before touching the database;
- Decide (human allow/deny) records human; timeouts and work-cancelled
  denials record system. Work-cancelled is a system refusal, not a human one.
- decided_by is exposed on the pending and approval records and filterable
  with decided_by=human|rule|model|system (anything else is a 400).
- Legacy rows show decision_source as their actor when decided_by is empty.
  The filter uses the stored column only, so decided_by=model cannot match
  older human decisions or timeouts.
- Archive restore fills decided_by for archives that predate the column, and
  marks a restored pending row as system.
2026-11-09 22:17:56 +01:00
BaaRai f938ba00d9 feat: verified findings backend (S1)
Add a machine-checked verification state to findings, separate from the
human disposition (status). A finding is verified only when a proof or
verification traffic body is bound, the agent declares POE L3 or higher,
and every evidence quote appears verbatim, after whitespace
normalization, in one of those response bodies.

- schema: poe_level, verification_status, verification_note on findings
  (idempotent ADD COLUMN IF NOT EXISTS, no CHECK, no backfill).
- db: SetFindingVerification validates status and POE 0-4; the three
  fields are scanned with the finding rows and exposed in the DTO.
- update_finding_report: optional poe_level and evidence_quotes. The
  report is always saved; verification only decides the status. Read
  failures give not_assessed, never unverified.
- report agent guidance: declare POE and exact quotes from the bound body.

Not in this change: UI badges and filters, the blocked and false_positive
transitions, and the stored default report prompt for existing databases
(the guidance is appended at run time).
2026-11-09 20:21:27 +01:00
BaaRai 9481b8e962 fix(intercept): judge decisions were stored as unknown (prefix case mismatch)
Since e233c02 the judge writer prefixes its reason with "[model]" while
interceptSource, the approvalDecisionSource SQL fallback and the archive
restore checked "[Model]", so model-decided rows were inserted with
decision_source='unknown' and missed by the decision_source=model filter.

- db: add ModelReasonPrefix ("[model]") and isModelReason, which also
  accepts the pre-e233c02 "[模型]" prefix; readers compare
  case-insensitively (lower(reason) LIKE in SQL).
- intercept: the writer builds its reason through JudgeReason, using the
  shared constant.
- schema.sql: idempotent backfill of 'unknown' rows without a rule whose
  reason carries either prefix.
- tests: un-skip TestInterceptFilterHTTP (it failed only because of
  this), make the db filter fixture use the constant, and add a
  Postgres-free test pinning the writer output to interceptSource.

Fixes #156
2026-11-09 12:22:46 +01:00
BaaRai 13dde2da15 feat(llm): optional custom User-Agent for LLM requests
Add an optional per-profile User-Agent that overrides the header on every
outbound LLM request. When empty (the default), the existing behaviour is
unchanged and the default User-Agent is left untouched.

The value is trimmed and validated: control characters / newlines (header
injection) and values longer than 256 bytes are rejected. Wired through the
profile config struct, the Postgres llm_profiles schema, the profile API/DTO,
the connection-test path, and the LLM profile form, following the existing
session-header field. Addresses part of #154; the fixed-full-URL mode is not
included.
2026-11-08 17:11:43 +01:00
Glauber Portella 80a1ee96f6 ollama llm 2026-10-23 18:12:38 -03:00
BaaRai 89348b5512 feat(engine): per-intent LLM chain-head rotation (opt-in llm_rotate)
Implements the per-intent rotation shape from #85: a task created with
llm_rotate=true distributes worker intents round-robin over its LLM chain
heads, so lane A can hunt on one model while lane B hunts on another
(different lenses per intent, no extra tokens). Default stays the classic
ordered failover from the active head; planner/goals/mainagent keep the
active head always.

Mechanics: the engine pins the next chain head into the run context when a
worker claims an intent; the pin travels through the provider resolver
(hooks.current now takes ctx). A pin is only a dispatch preference - it must
belong to the task chain and never bypasses failover state (an exhausted
chain stays exhausted). Schema: tasks.llm_rotate boolean, default false,
idempotent ALTER; exposed on the create payload and the task DTO.

Closes #85 (the ensemble/P4 shape stays parked as before)
2026-10-21 15:00:16 +02:00
BaaRai a8f3c2c328 feat(rsi): evidence-gated lesson library in shadow mode
Extracts candidate lessons from terminal tasks (confirmed, negative,
retest caution), gates them on cue + action + evidence, and makes
extraction idempotent through a per-source evidence key.

ARTEX_RSI_LESSONS selects off / shadow / inject. Shadow logs what would
have been shown; inject appends a code-owned, data-not-instructions block
after the planner and worker system prompts. No prompt mutation and no
adoption: status stays candidate.

Credit: the Library admission rule comes from harness2 (Google Research,
Apache-2.0, arXiv 2610.14224); the gain taxonomy from awesome-rsi
(lobehub).
2026-10-21 14:25:35 +02:00
BaaRai 9f03b8c16a feat: per-task planner quiet-mode switch, editable mid-run
Quiet-mode was compile-time only; wanting it off mid-task (empty frontier, one long intent, idle lanes) was impossible. tasks.plan_quiet_mode (default on) gates the edge-round skip in runRound, PATCH /api/tasks/{id} flips it live (the next round reads it), creation accepts plan_quiet_mode, and the DTO exposes it.
2026-10-12 21:13:28 +02:00
BaaRai 67ff6ffffa feat: cost ledger (per-profile EUR pricing on token usage)
The token metering is real now (input, output, cache read/write per
profile), but it answers "how many tokens" while the question an operator
actually asks is "what did this run cost". Converting requires per-profile
prices, which nowhere exist.

Changes:
- llm_profiles gains four optional price columns (EUR per 1M tokens:
  input, output, cache read, cache write; 0/NULL = unset), migrated
  idempotently and round-tripped through SaveProfile / the profile API
  in one small auditable write
- the usage aggregation computes cost_eur per profile from the stored
  prices, in Go after the archived-task merge so cold-storage tokens are
  priced identically; profiles without prices keep cost_eur nil instead
  of showing a fake zero
- web types carry cost_eur and the price fields; the dashboard card and
  profile-dialog inputs consume them in a follow-up (the API serves the
  data already)
- integration test: priced profile + usage rows -> expected EUR to the
  cent (2M in @1.5 + 0.5M out @4 + 1M cache-read @0.15 = 5.15), plus a
  prices round-trip check

Note: the Go side activates on the next build; the schema migration is
idempotent at startup.
2026-10-12 08:20:37 +02:00
BaaRai 28e6618c71 feat: surface MCP server connect health to operators and agents
Every connect attempt (discovery, refresh, and the per-agent-run wiring) now
records its outcome on the server row: last_connect_at/ok/err, written through
a change-deduped update so per-run wiring does not amplify writes. The MCP page
shows a health chip per card (ok with time, or the failure reason), and a new
read-only mcp_status tool (bound to auto, worker and pentest) lets a worker
notice that a server it expected is down and say so instead of silently
degrading while the cached tool count keeps the card looking healthy.

Closes #38
2026-10-11 22:50:00 +02:00
BaaRai 24c8ea13c0 feat: rea reverse-engineering skill + per-server MCP HTTP request timeout
Add a rea skill (skill-gated MCP): agents get binary and application package
reverse engineering through a rea MCP server, with tools kept deferred so the
172-tool catalog never bloats the prompt until the skill loads.

Remote MCP servers now take timeout_seconds (default 120): analysis servers
with minutes-long tool calls were cut off mid-call by the fixed 120s client
timeout. Applies to the Streamable HTTP transport; wired through discovery,
the ScopeSentry sync client, the API and the MCP dialog.

Closes #8
2026-10-11 19:02:23 +02:00
BaaRai c177ab9d86 feat(tasks): per-task test-account credentials
Task credentials (#3): declare test accounts at task creation, and never
lose the accounts the agent registers on its own.

- task_credentials table (unique per task+domain+username; upsert
  overwrites the password in place), idempotent schema migration
- POST /api/tasks accepts credentials; GET/POST/DELETE
  /api/tasks/{id}/credentials serve the UI (manual add, e.g. relaying an
  OTP-verified account; delete)
- empty password = the "agent generates" mode: the entry surfaces in the
  worker's graph overview with an explicit instruction to register that
  account with a strong password and record it
- new tools save_task_credential / list_task_credentials for worker and
  pentest; the graph overview injects the task's credentials each round
  with a standing note: reuse matching entries, always record accounts
  you register
- New Task dialog: optional credentials section (domain, username,
  password, note) with a dedicated-test-accounts hint
- task detail: Credentials tab, passwords masked with explicit
  reveal/copy, agent-recorded badge, light polling so mid-run recordings
  appear

One-shot binding migration (settings-flag-guarded, same pattern as the
other seeds) grants the two tools to worker+pentest on databases seeded
before this change; fresh DBs get them via WorkerTools()/the pentest
catalog.

Closes #3
2026-10-09 14:28:08 +02:00
BaaRai 353a947285 fix(tasks): freeze the timeout budget while a task is paused
deadline_at is a wall-clock timestamp, so pause time counted against the
task timeout: pausing overnight (or shutting the machine down while
paused) made the deadline ring while parked, and resuming landed
straight into the settle sequence and a timeout terminal state.

ApplyTaskPause now stamps paused_at, and ApplyTaskAdmission credits the
paused interval back onto deadline_at in the same atomic UPDATE that
ends the pause (paused_at is cleared there too). The shifted deadline
flows to the live handle via the existing RETURNING mirror, so the
engine clock context picks it up. Matches the re-basing the
archive-restore path already did for paused tasks.

Migration is an idempotent ADD COLUMN; tasks paused before the upgrade
have no paused_at and keep the old behavior.

Fixes #1
2026-10-09 11:37:30 +02:00
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00
luckoneandClaude Code 4eb5311998 feat(notify): 漏洞发现支持推送到钉钉/飞书/企业微信等六种渠道
新增「系统 → 通知推送」页,把扫描发现的漏洞推送到钉钉、飞书、企业微信、
通用 Webhook、Telegram、邮件。同一类型可配任意多个机器人实例,每个实例独立
设启停、限流与过滤规则(最低级别 / 任务资产范围 / 漏洞类型关键词 / 是否接收
处置状态变更)。推送时机分实时与汇总两种模式,「高危实时、其余汇总」建两个
渠道即可表达,策略不写死在代码里。带投递历史与失败重发。

三个关键设计:

1. 写漏洞的事务只做一次盲 INSERT。notification_events 由 RecordFindingTx
   在同一事务内写入,保证「漏洞落库」与「推送任务存在」原子一致;这条 INSERT
   刻意不读渠道表、不跑用户的过滤规则,否则一条配错的过滤条件就能污染甚至
   中止事务,让高危漏洞存不进库。为把失败隔离在这一条语句上(PostgreSQL 中
   事务内任一语句报错会让整个事务作废、连 COMMIT 都失败),它被 SAVEPOINT
   包住,失败时只记日志。

2. 投递用租约领取而非长事务。FOR UPDATE SKIP LOCKED 领取后把行置为 sending、
   把 next_attempt_at 推到未来作为租约,提交后再做网络投递,因此投递期间不
   持有数据库锁;崩溃留下的 sending 行会被下一轮重新领取,自愈且不会无限重试。

3. 限流不消耗重试预算。先按渠道令牌桶算出本轮还能发几条,再按这个数量领取;
   顺序反过来会让被限流挡下的投递白计一次尝试,三次预算被等待耗光后落入失败。

审计并修复的四个问题:

- 推送凭据经错误信息外泄:http.Client.Do 返回的 *url.Error 会把完整 URL 打进
  错误文本,而钉钉 access_token、企业微信 key、飞书 hook id、Telegram
  /bot<token>/ 都就在 URL 里。该串流入 last_error(明文落库)、投递历史接口
  (绕过渠道掩码)、服务端日志与前端错误提示。现统一脱敏为 scheme://host
  + 底层原因,并拒绝跨主机重定向。
- 最低级别门槛打错字会让过滤器静默失效:未知级别序数为 0,判定退化成
  rank >= 0 恒真,用户以为限制「仅高危」实际全推。现于写入路径校验取值。
- 汇总批次无上限:一次领取把全部待发行读进内存、消息被渠道长度上限截掉大半
  且静默丢失。现单批上限 500 条,超出部分留库成为下一批。
- 邮件渠道把 SMTP 4xx(灰名单等临时失败)判成永久失败,导致这类最该重试的
  场景从不重试。现按应答码首位区分 4xx/5xx。

测试:notify 包 82% 覆盖(含用自建最小 SMTP 服务器驱动的完整邮件协议会话、
两种签名算法对 OpenSSL 基准值的比对、UTF-8 边界截断、以及模板能力边界的
反射断言);db 与 server 侧共 29 个用例跑真实 PostgreSQL,其中端到端用例
真发 HTTP 到假接收端并断言消息内容与投递状态。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:25:06 +08:00
Autumn-27 93431651be feat: 任务模板支持预设分类与任务级拦截/允许规则
- db: task_templates 加 category_id(FK task_categories, ON DELETE SET NULL)
  与 intercept_rules JSONB(规则快照);带幂等 ALTER 补旧库。
- db/task_templates.go: 用 JSONB 存取规则;PATCH 用 Set 标志区分改/清。
- server: 模板请求体加 category_id + intercept_rules,复用 buildTaskInterceptRules
  校验;PATCH 靠原始 key presence 判断是否更新(可清分类为 null)。
- web: 抽共享组件 asset-intercept-rules-editor(NativeSelect 版,避免 Sheet 内
  下拉 portal 误关抽屉),创建表单与模板管理器共用;模板管理器新增分类下拉与
  规则编辑区,另存为模板带当前分类/规则;应用模板回填分类与规则。
2026-09-19 09:27:03 -04:00
Autumn-27 bd1772c62f feat: 资产拦截接入执行链,新增任务级拦截/允许规则
在 agent 下发意图(add_intent)与插入资产(insert_assets)两处接入资产闸门;
并新增任务级规则(拦截 block + 允许 allow 白名单),创建任务时录入、任务详情
总览里可编辑。

- 匹配/执行(db/asset_intercept_match.go):MatchAssetInterceptRules 纯匹配
  (exact/fuzzy 的 域名·IP·URL + CIDR,URL host 会拆出归类);EvaluateAssetGate
  「先拦截后允许」闸门——命中拦截即禁止;未命中但存在启用的允许规则且都不命中→
  不允许测试;无启用允许规则则不启用白名单。附单元测试。
- 执行点:add_intent 命中即不下发意图;insert_assets 命中即跳过 Upsert 及
  AddAutoScope 副作用。均合并 全局∪任务级 规则。
- 任务级规则:表 task_intercept_rules(task_id FK CASCADE + action block/allow,
  幂等 ALTER 补旧库);db/task_intercept.go 提供 List/Split/CRUD/事务插入;
  创建任务随 intercept_rules 提交;CRUD 路由 /api/tasks/{id}/intercept-rules。
- 前端:新建任务抽屉多行录入(拦截/允许 + 类型,用 NativeSelect 避免 Sheet 内
  下拉 portal 导致误关抽屉);任务详情总览新增规则管理卡片(增删改+启用开关)。
2026-09-19 09:06:22 -04:00
Autumn-27 228674a2ce feat: 新增资产拦截规则(全局黑名单)增删改查
新增独立于命令拦截(intercept_rules)的资产拦截功能,仅提供规则的
存储与管理;具体匹配/拦截逻辑另行实现。

- db: 新增 asset_intercept_rules 表(schema.sql,幂等)与 CRUD 层
  (db/asset_intercept.go);kind 支持全等/模糊的 域名·IP·URL 及 CIDR
- db: 首启动 seed 内置模糊拦截政府(.gov/.gov.cn)与教育(.edu/.edu.cn)
  网站,settings 标记位门控,可禁用/删除且重启不复活
- server: /api/asset-intercept/rules 的 list/create/update/delete/toggle;
  cidr、exact_ip 做格式校验
- web: 新增「资产拦截」页(/system/intercept/assets)与侧边栏入口、
  api 与类型定义
2026-09-19 07:58:23 -04:00
Autumn-27 6f295f48f8 feat(intent): 意图删除支持假删除/真删除两种模式
删除入口区分两种模式(待领/运行中/已暂停的意图均可删除):

- 假删除(默认):意图置 state='deleted'、删除原因记入新字段 delete_reason,
  保留节点与全部产出/血缘,不再在图上另挂 fact。
- 真删除:物理删除意图及「仅由它支撑」的独占子孙(沿 yields/derived_from
  级联到叶子),避免孤立数据;共享节点、目标、任务根事实保留。被删意图先做
  token rollup 并清理 activity/副会话,被删 finding 先删 findings 行。

两种模式都照常用 cancelled 触发通知 planner(带删除前捕获的意图摘要 + 原因),
planner 文案不再声称原因挂为事实。

- DB: 新增 SoftDeleteIntent、重写 CancelIntent 为深度独占级联;schema 加
  delete_reason 列与 intent 的 deleted 状态(含旧库迁移)。
- 前端: 删除对话框加模式切换与动态说明,状态徽章/图标新增「已删除」,
  播报板展开展示删除原因。
2026-09-17 12:52:57 -04:00
RuoJi6 eee40408ee 精简动作审查输入并支持审批来源定位 (#125)
* fix: 精简审查背景并区分人工消息与调度输入

* fix: 模型审查仅提交当前调用与选定背景

* feat: 审批来源定位原会话并移除 Worker 审查背景
2026-09-15 23:51:37 +08:00
RuoJi6 29d378416e feat: support legacy SSE MCP servers (#117) 2026-09-15 00:56:16 +08:00
Autumn-27 5e944cf61b fix(mcp): http 传输支持跳过 TLS 证书校验(自签证书)
MCP server 走 HTTPS 且用自签证书时,客户端 TLS 校验失败,报
"tls: failed to verify certificate" 无法连接。

mcp_servers 增加 insecure 开关(默认 false),置 true 时远程 MCP
客户端使用 InsecureSkipVerify 的 transport。stdio 传输不受影响。

- mcphttp.New 增加 insecure 参数
- db: MCPServer.Insecure 字段 + schema 加列(补 ALTER 兼容旧库)
- create_mcp/update_mcp 工具与前端 http 配置项增加勾选框

Closes #108
2026-09-11 11:37:56 -04:00
ji ruo 7604c763f5 Merge upstream/main into codex/btw-side-question
同步官方 main 的任务会话与 spawn_task 更新,保留 /btw 旁路上下文能力。
2026-09-11 08:47:57 +08:00
Autumn-27 2954781cb7 feat(agent): 任务内主 Agent 支持多会话,可切换且上下文可重置
主 Agent 会话原为一个任务恒定单例,对话历史随 transcript 只增不减、
上下文越滚越大。现引入会话分段(seq):新建会话切一个干净的 transcript
段,任务的图谱/资产/目标共享保留,旧段以只读历史随时切回查看。

- db: 新增 main_sessions 表与 activity.main_seg 列(ALTER ADD COLUMN
  IF NOT EXISTS 兼容旧库);ActivitySessionFilter 按段过滤(段 0 兼容
  旧 NULL 行);新增 CurrentMainSeg/ListMainSessions/NewMainSession;
  per-session token 统计按 main:<seg> 分段
- agent: MainAgent.Chat 增加 mainSeg,transcript SessionID 分段
  (段 0 保留旧名 exp<id>-main 兼容既有对话)
- server: parseActivitySession 支持 main:<seq>;chat handler 每轮
  stamp 当前段;新增 GET main-sessions / POST main-session/new
- web: 主 Agent 分组渲染多段会话,支持新建(轻确认)、切换、旧段只读
2026-09-10 12:56:59 -04:00
ji ruo f62f1cc1a4 Merge origin/main into codex/btw-side-question
合入 main 的 norma v0.3.7、会话分组与流量证据更新,保留 /btw 快照和输入布局。同步自动放行、关闭流量绑定的测试断言,并等待归档测试夹具的旁路后台退出。
2026-09-11 00:54:51 +08:00
ji ruo 6378791cc2 fix(btw): 修复长对话上下文预算与旁路输入布局 2026-09-11 00:41:01 +08:00
Autumn-27 028d9bd25c Merge PR #102: feat: 支持漏洞关联多条流量证据
冲突解决:
- db/schema.sql: 复测表(#103)与流量证据表(#102)均为追加,两者保留
- server/server.go: 启动序列同时保留 seedFindingRetester 与 evidence GC
- web/src/lib/types.ts: InterceptAudit/InterceptDetail(#101) 与流量证据类型(#102)并存
- findings/detail/page.tsx: 去掉 #103 带入的重复 import,仅保留 FindingRetestPanel
- chat/page.tsx: 保留 #103 的 Spinner import
2026-09-10 10:04:53 -04:00
Autumn-27 e237d3c9f3 Merge PR #103: feat: 支持会话级漏洞复测与运行状态展示 2026-09-10 10:02:39 -04:00
ji ruo aacc303706 feat(agent): 支持持久化 /btw 旁路提问 2026-09-10 21:42:25 +08:00
ji ruo 1c77195e79 feat: 支持会话级漏洞复测与运行状态展示 2026-09-10 16:06:08 +08:00
ji ruo 009799e160 feat(intercept): 增加审批记录详情与执行审计 2026-09-10 15:10:50 +08:00
ji ruo e6f8195500 feat: 支持漏洞关联多条流量证据 2026-09-10 13:03:20 +08:00
Autumn-27 76bdf4c3d2 feat(llm): 五层重试的次数与间隔可配
一次模型调用的失败由内到外经过五层重试:建连(SDK)、空响应(SDK)、同 provider
安全窗口、轮询熔断、意图重跑。此前五层的次数与间隔全是写死的常量,遇到脾气各异
的中转站只能改代码。现在都能在 LLM 页配置,不配就是原来的默认值。

配置语义五层统一(db.RetryRule):次数 0=内置默认 / -1=关掉这层 / >0=该值;
间隔 0=原本的指数退避 / >0=改用固定间隔。0 被"未配置"占用,故关闭用 -1。
写库前夹紧到 次数 [-1,20]、间隔 [0,1h],DB 另有 CHECK 兜底。

粒度按层的性质分:前三层跟着端点走,可在每个模型配置里逐字段覆盖全局默认(只钉
间隔的仍继承全局次数);熔断与意图重跑是进程级语义,只有全局一份。

生效路径:①②是 provider 构建参数,保存后重建缓存;③随每次 hooks.current()
解析,故障转移换配置时重试节奏跟着换;④在失败路径上每次都要读,由 applyRetryPolicy
推给 llmpool.Registry;⑤每条意图收尾读一次。全部热生效,无需重启。

连接测试刻意不带这些参数——它有 30s 硬超时,叠上用户配的重试只会把本来能用的
端点测成超时失败。

norma 升 v0.3.5(llm.Config 新增 RetryInterval / EmptyResponseRetries /
EmptyResponseInterval)。旧库升级后行为逐字节不变:新列默认 0,settings 键不
存在即全默认。设计见 docs/LLM重试设计.md。
2026-09-09 13:35:04 -04:00
Autumn-27 2faee6ed99 feat(digest): 探索图冷节点压缩(cold-digest)
把老且长期不活跃的意图/事实折叠成 digest 节点进 graph_overview 展示,
原始节点永久保留、按 id 可完整还原(存储无损、只压呈现、折叠可逆)。

- 数据模型:KindDigest 节点 + RelCovers 边(真相源);新列 content_version、
  cold_since_round(节点级)、round_no(任务级 planner 轮次);均带旧库迁移。
- 纯算法(coldgraph.go):冷热判定(逆向可达 + 任一活分支即热) + R=6 轮防抖 +
  连通分量分组(§3) + §3.1 同父兄弟归组(救"热 hub+扁平死叶") + §5.3 签名。
- 后台压缩(compaction.go):minor 折未覆盖冷块(tiered)、major 回源重压合并碎片;
  签名幂等复用 body;提交时活跃度复核(绝不覆盖已复活节点);60s 冷却 + 互斥。
- 概览折叠(§6):recent_* 剔除已折成员 + 渲染时复活检查;cold_digests + 按资产
  索引 cold_index(§6.2);expand_digest/expand_index 还原;covered_members 反查
  悬空血缘 id;60 兜底截断均衡保 live。
- planner 每轮 OnPlannerRound 维护戳记并触发后台压缩,绝不上热路径。

纯算法单测覆盖 §附/§3.1/§2.3/§5.3;已对 437 节点真实图验证:热 86、冷候选 324、
§3.1 分组 47 块 + 14 孤立冷点(对齐设计文档实测校验)。
2026-09-09 11:52:15 -04:00
Autumn-27 12b63ab671 feat(llm): 每个 LLM 配置支持自定义会话头
新增 session_header_key:非空时每次 LLM 请求带上该 HTTP 头,头值为当前
运行的 session id(chat 会话=conv-<id>,worker=exp<x>-worker-i<intent>
等)。用于某些按 session-id 头做提示缓存/粘性路由的网关。

实现:在已有的 quotaAwareTransport 里从请求 context 读取 session id
(transcript.WithSessionID 挂上的)并注入,无需改 norma;同一共享 provider
也能按会话发出不同头值。DB 加 session_header_key 列(带旧库迁移),前端
LLM 配置弹窗新增输入框。
2026-09-07 11:21:50 -04:00
Autumn-27 a0c17701c2 feat(llm): 模型配置新增输出上限,支持 max_completion_tokens 字段名
llm_profiles 加两列:max_tokens(单次回复输出上限,0=不发送该字段,由服务端
默认值决定)与 max_tokens_field(上限发哪个键)。后者仅对 format=openai 生效:
空(默认)= max_tokens;'max_completion_tokens' = 新键。OpenAI 推理模型
(o 系列/GPT-5)拒收 max_tokens、只认新键,而多数兼容网关反过来只认旧键,
故做成显式开关而非自动推断。anthropic 与 openai-responses 自带字段名,
存下来只会误导,保存时一并清空。

顺带补上一处从未接通的链路:planner/worker/chat/mainagent/goals 五个
agentcore.Options 都没设过 MaxTokens,恒为 0——openai 系因 omitempty 压根
不发上限,anthropic 走 norma 的 8192 兜底。光加配置项而不接这条线,用户在
后台填了数字也不会生效,故本次一并接通,取值与 streaming 一样按 profile
每轮解析,故障转移切换后下一轮即生效。

task_llm.go 里 nonStreaming() 的来源优先级(agent 绑定 → 任务链激活 profile
→ 全局)抽成共用的 activeCfg(),避免 maxTokens() 再抄一份同样的三级回退。

norma 升到 v0.3.3(该版本提供 Config.MaxTokensField)。go.mod 只动 norma 这
一行:仓库此前未 tidy 过,跑 go mod tidy 会顺带升 x/crypto、x/net 等一批无关
依赖,不混进本次改动。

迁移:新列走 ADD COLUMN IF NOT EXISTS + 默认 0/'',旧库升级后行为完全不变;
CHECK 约束限定取值,每次启动先删后建保持幂等。
2026-08-31 12:20:31 -04:00
Autumn-27 1f90ba13e7 feat(findings): 发现页新增「按资产」视图
左侧资产树 + 右侧该子树下的发现,与另外两个视图共用同一张表和同一批筛选。

后端
- db/finding_assets.go:按当前筛选构建资产树。只收有发现的资产,祖先链逐层
  补齐(endpoint→service→subdomain/ip→root_domain),父子优先级与
  BuildCoverageGraph 同源;计数沿祖先链去重累加,一条发现挂多个资产不会让
  父节点重复计数
- FindingFilter 加 AssetScope:选中节点=选中整棵子树,解析成资产 id 集合后
  下推 SQL,列表 / 分组 / 导出三个入口共用,导出「当前筛选」自动带上
- 「未关联资产」同时收 asset_ids 为空与指向已删资产的发现,与树上桶的口径
  一致(只判空数组会让桶上的数字大于点开后的条数)
- findingFilterFromQuery 统一四处的筛选解析;新增 GIN 索引
  idx_findings_asset_ids,按资产反查发现不再全表扫

前端
- 树默认展开一层,支持过滤(匹配项自动展开)、面包屑下钻、手动刷新
- 每层只显示相对上一层的增量:子域名去根域名后缀、服务显示 `https :443`、
  接口只留路径。完整值在 title 与面包屑,搜索仍按完整值匹配
- 不轮询:进入视图 / 改筛选 / 页内改动发现 / 点刷新时才查
- 节点超上限时整层丢弃接口/服务并提示,计数已在上层不会丢
2026-08-31 10:58:32 -04:00
Autumn-27 9e8b9e2247 fix: restore changes dropped by the upstream sync merge
The manual v0.3.6 sync (3675f7e) was incomplete, and the follow-up merge
(fa7b3cd) resolved conflicts in favour of that copy, silently reverting three
shipped changes. main is already an ancestor of this branch, so re-merging
cannot bring them back; restore them explicitly.

- schema.sql: re-add the idempotent llm_profiles_format_check migration.
  CREATE TABLE IF NOT EXISTS leaves an existing table alone, so databases
  created before openai-responses kept the old two-value CHECK and rejected
  every openai-responses profile.
- orchestration.go: re-add the reseedPlannerPrompt/reseedWorkerPrompt calls,
  dropped by a comment-alignment hunk while the functions stayed behind as
  dead code. Without them existing installs never receive the quantified
  acceptance check or the negative-conclusion evidence threshold.
- CHANGELOG.md: restore the 0.3.6 entries, the @neouks credit and the correct
  norma version range.

Also close three recovery gaps in the new cold-archive path:

- traffic: write the staging journal even when no legacy host directory
  exists. Modern installs keep traffic in SQLite only, so a crash between the
  PostgreSQL commit and the SQLite commit left the archived task's exclusive
  traffic in the hot store with nothing for recovery to act on.
- recoverTaskArchiveStages: treat a missing journal.json as a discardable
  directory instead of an error, which used to set recoveryFailed and stop the
  archive worker from starting for the rest of the process lifetime.
- stageTaskArchiveFiles: replay the journal's renames when resuming an
  existing staging root. The journal is written before the first rename, so
  its presence did not guarantee a complete payload, and a resumed round could
  package an archive missing transcripts or workspace files.
2026-08-29 01:37:46 -04:00
pphua 7df4406d44 fix: prevent OOM when archiving large tasks 2026-08-28 15:25:20 +08:00
pphua fa7b3cd4ec merge: sync official main into PR branch 2026-08-28 14:14:41 +08:00
pphua 3675f7e530 chore: sync upstream v0.3.6 changes 2026-08-28 14:03:07 +08:00
pphua 809e94f6e4 归档 2026-08-28 12:56:29 +08:00
pphua 028a2e9a6c 修复BUG: 1、当存在较多任务时,系统会发生卡顿,加载对话或者加载界面时候存在明显卡顿,优化性能找出问题 2026-08-28 08:00:36 +08:00