124 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 e59db80173 Opt-in update banner, off by default
ARTEX_UPDATE_CHECK=1 lets the backend check the public GitHub releases API
for the latest tag at most once per 24 h (in-memory cache; failures are not
retried in the window) and report it in GET /api/health when it is newer
than the build version. The web UI shows a dismissible strip linking to the
Releases page. Unset (the default), no update request is made.
2026-11-12 12:54:25 +01:00
BaaRai f07e84ac7a feat(intercept): spawned tasks and triggered conversations inherit the target gate
spawn_task now reads the target-gate scope of the calling session from the
tool context. When the caller is gated, the child task is created with the
gate on (D23) and with a copy of the calling task's block rules (D24). Pins
and allow rules are never copied, parent_ref is not consulted, and spawn_task
gets no new parameter. A gate-off caller, or a call with no scope, creates the
child exactly as before.

A conversation started by a trigger of a gated task is created with the gate
on (D25). A merged run is gated if any of its fires came from a gated task.
Pins are never copied. Fires of a gate-off task and fires with no task are
unchanged.

TestSpawnedTaskStartsWithGateOff becomes TestSpawnedTaskInheritsGateFromCaller.
New: TestSpawnTaskSeesCallerScopeThroughHarness and
TestTriggeredConversationInheritsGate.
2026-11-11 15:47:21 +01:00
BaaRai 78f92f44fc feat(intercept): target extraction, resolver, exemptions and the gate in the hook (S3b 1.5-1.7)
The approval gate for asset targets now decides. It is opt-in per task and
per conversation (target_gate, default off). With the flag off nothing is
extracted, resolved, read or written, and every branch of the hook behaves as
before. No UI.

Extraction and normalisation (1.5)
- intercept.NormalizeTarget: lower case, trailing dot, userinfo, IPv6
  brackets, IPv4-mapped IPv6, decimal / hex / octal / short IPv4, percent-
  encoded host, punycode; port explicit, else the scheme default, else 0.
- intercept.ExtractTargets: WebFetch url exactly; Bash, shell_open, Monitor
  (command) and shell_send (text) with the command-string rules (URL with a
  scheme, IP, IP:port, host:port, CIDR; a bare name is not a target); every
  string of the input for MCP tools, custom tools and any tool the gate does
  not know. ExecuteExtraTool is unwrapped to the inner tool. The platform's
  domain tools, the file tools and web_search have no target.
- The fields are decoded as the tools decode them (key case, last duplicate),
  and a URL authority that reads differently quoted and unquoted yields both
  readings.

Resolver, exemptions, gate decision (1.6)
- Resolver interface, system implementation (A and AAAA), 3 s per lookup,
  positive answers cached 10 s per host with collapsed lookups, failures and
  empty answers never cached, always fresh when a request is raised.
- Exemptions are exact host + port. Derived from the configuration (LLM
  gateways, search backend in use, platform proxies): computed on every call,
  never resolved, never voided. Stored (settings key
  target_gate_exempt_hosts, seeded once with the package sources): resolved,
  and void when any address is private, loopback, link-local, unspecified,
  unique-local, multicast or a metadata address. An entry in both origins
  counts as derived. A stored IP literal that can never be exempt does not
  apply.
- GET / PUT /api/target-gate/exemptions and GET .../history. A change is
  validated (ValidateExemptionHost on the normalised host) and appended to
  target_gate_exemption_log in the transaction that stores it.
- Interceptor.CheckTargets: asset block rules (global and of the task) on the
  name and its addresses, exemptions, resolution, pin lookup. A (host, 0) pin
  covers every port; a (host, 443) pin covers 443 only and never a target
  without a declared port; two pins are never merged. At most 5 unpinned
  targets per call. Codes: TARGET_RULE_BLOCKED, TARGET_RESOLVE_FAILED,
  TARGET_TOO_MANY, TARGET_GATE_UNAVAILABLE; none opens a request.
- With the upstream egress proxy set every pin is name-only at call time,
  whatever name_only_effective says in its row.
- An approval never pins zero addresses: the pin writer refuses a request
  that displays none (db.ErrTargetPinEmpty, 422 on the decide endpoint, the
  row stays pending) and RequestTargets does not open one.
- Every block, and the first use per process of each exemption and of each
  pin version, is a decided row (decided_by system, decision_source target).

The gate in the hook (1.7)
- guard.applyIntercept: a deny by a rule or by the judge blocks first, before
  any target request exists; then CheckTargets; an allow is logged as before
  and does not satisfy the gate; an ask and an unpinned target share one
  request, and the classic ask is raised only when it was not merged. The
  call is checked again after the decisions, at most three passes.
- The gate applies to every tool of a gated session, enabled for rules or
  not, and to the target-only guard of the planner and the main agent, which
  still records blocks only.
- Target blocks have their own model-facing text, built from the code and the
  reason of the result.

Not in this change: rendered targets of custom tools (1.8) and redirect
targets (1.9). Subprocess paths stay advisory; the TestBypass_* tests document
what the gate does not see.
2026-11-11 00:34:55 +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 135b083b9a feat(guard): planner and main-agent hooks, target-gate scope in every session (S3b 1.1)
Step 1.1 of the S3b phase 1 plan. Wiring only: the approval gate for asset
targets is not enforced yet and no behaviour changes.

- Planner.Plan and MainAgent.Chat take a hooks parameter, like Worker.Execute.
  Their sessions had no hooks, so nothing saw their tool calls before execution.
- guard.NewTargetOnly: the guard for those two agent kinds. It runs the
  target-gate seam only: no intercept rule, no fallback judge, and no entry in
  the audit ring (Guard.record is untouched and not called for these calls).
- Task.TargetGuard, built beside Task.Guard.
- intercept.WithTargetScope / TargetScopeFromContext: task id or conversation
  id, an Enabled func and a wait bound, set at the five session entry points
  (planner round, final planner round with wait bound 0, main-agent turn,
  worker intent, conversation turn).

The seam allows every call and reads and writes nothing. No task or
conversation has an opt-in flag yet, so Enabled is nil (off) everywhere.
2026-11-10 22:14:24 +01:00
BaaRai 809436e111 notify: make the summary and asset limits configurable
The IM renderers cut the summary of a single push at 120 characters and
listed at most 3 assets, as fixed constants. Both are now global settings
in the settings table, read by the delivery engine when it renders:

  notify_summary_max_runes   0-4000, default 120
  notify_assets_max_shown    0-50,   default 3

0 means no truncation and an unset key keeps 120 / 3. PUT /api/settings
answers 400 for a value outside the bounds. The limits travel on
notify.Message, so the notify package still does not touch the database.

With no truncation, nothing bounded a single item in a Feishu card, and
Feishu rejects an oversized request as a whole. feishuCard now truncates
an item's text as the final backstop, the role TruncateBytes plays in
markdownBody for DingTalk and WeCom. feishuMaxCardBytes goes from 24000
to 16000: Feishu custom bots cap the request body at 20 KB, which the old
value exceeded.

Refs #168.
2026-11-10 16:05:31 +01:00
BaaRai 4acdf3bb26 fix(llm): validate custom User-Agent in the connection-test path
testLLM set cfg.UserAgent from the request (or the stored-profile
fallback) with no validation, while pgSaveProfile runs
agent.ValidateUserAgent and returns 400 on a bad value. A UA with a
control character could therefore reach the wire from the test button
though it is rejected on save. Call ValidateUserAgent on the final value
(after the stored fallback) and return 400 with the same message style,
mirroring pgSaveProfile; the fallback itself is unchanged.

ValidateUserAgent rejects every control character (r < 0x20 or r ==
0x7f), TAB included, so the header value stays a single printable token.
Extend the agent tests to cover NUL, US, DEL and TAB, and that a
printable slash-token value with spaces survives trimming unchanged. Add
a Postgres-free server test driving testLLM through an httptest endpoint:
a control-character UA is a 400 before any request leaves, a printable UA
reaches the wire verbatim.
2026-11-08 18:15:33 +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
BaaRai d0d153b768 fix(auth): trusted proxies for the login rate limit, 12 characters in reset-password.sh
- ARTEX_TRUSTED_PROXIES (comma-separated CIDRs, empty by default). When
  the TCP peer is listed, the login limit reads X-Forwarded-For from the
  right and keys on the first hop that is not a trusted proxy; otherwise
  the header is ignored. Without it every client behind a reverse proxy
  shares one bucket and an attacker can block the admin. Passed through
  by docker-compose.yml and listed in .env.example.
- README: nginx example sends X-Forwarded-For; document the variable;
  the binary's default listen address is 127.0.0.1:8787.
- reset-password.sh refuses passwords under 12 characters, counted in
  characters like the server.
2026-11-05 16:57:48 +01:00
BaaRai 2419038884 fix(auth): rate-limit login per IP, 12-character passwords, loopback default
- POST /api/auth/login: 10 attempts per minute per TCP peer IP
  (X-Forwarded-For ignored), then 429 with Retry-After. The block starts
  at 1 minute and doubles on each attempt while blocked and on each new
  block, capped at 15 minutes. A successful login clears the IP. No
  per-account lockout. In-memory, idle entries swept after 15 minutes,
  at most 10,000 IPs.
- New passwords (init, change-password, /setup page) need 12 characters.
  Login does not check length, so existing 8-character passwords work.
- The binary's -addr default is 127.0.0.1:8787. The Docker image passes
  -addr :8787 explicitly and is unchanged.
- GET /api/health returns {"ok":true} without a valid token; with one it
  still returns service and version for the web UI top bar.
2026-11-05 16:48:27 +01:00
BaaRai 65bdc5d73c fix(auth): keep the JWT signing key in a dedicated volume
The server wrote jwt.key to the executable's directory, /app in the
image, so it lived in the container's writable layer and every container
rebuild generated a new key and signed all users out.

- ARTEX_JWT_KEY_DIR selects the key directory (default: config.BaseDir(),
  unchanged for non-Docker installs). docker-compose.yml sets it to
  /app/keys and bind-mounts ./keys there; the Dockerfile declares the
  volume. The key stays out of data/, which the file manager browses.
- loadOrCreateJWTKey creates the directory (0700). When keyDir/jwt.key is
  missing it migrates dataDir/jwt.key, then BaseDir()/jwt.key (mode 0600,
  old file removed), otherwise generates a new key. An existing key is
  never overwritten; a short one is now a startup error.
- .gitignore and .dockerignore exclude keys/.
- README and CHANGELOG updated; the 0.5.0 known issue is removed.
2026-11-04 09:18:11 +01:00
BaaRai 90e7b3ef55 Merge pull request #146 from BaaRai/fix/apply-new-options
fix(security): apply New options so the secret scrub actually runs
2026-11-03 23:52:28 +01:00
BaaRai 69831f5cdf fix(security): apply New options so the secret scrub actually runs
New accepted opts ...Option but never called them, so scrubSecrets stayed
false in the binary and ScrubSecretEnv was unreachable: the protection merged
in #136 never engaged. The unit test passed because it called the scrub
function directly instead of going through New.

Apply the options in the constructor and add a test that builds a Server
through New with WithSecretScrub and asserts the secrets are gone. Verified to
fail without the loop.
2026-11-03 23:47:49 +01:00
GRAB Racem 334f6fdddc fix(llm): send a saved API key only to the endpoint it was saved for
POST /api/llm/test and POST /api/llm/models take provider and base_url from
the form but fell back to the stored profile key (and, for the test, the
global key) when the form had none. Any base_url typed into the form, or
supplied by a crafted request, received the saved key.

The saved key is now used only when the request's provider and base URL
normalize to the ones it was saved with (sameLLMEndpoint, built on
agent.ConfigFrom). Otherwise the key must be entered in the form, and the
error says why.
2026-11-03 23:25:47 +01:00
GRAB Racem 2aab3e8210 fix(auth): require a setup token for /api/auth/init and bind 8787 to localhost
POST /api/auth/init accepted any caller while auth.password_hash was empty,
so whoever reached port 8787 first could set the admin password and take the
instance. When no password is set at startup, the server now generates a
random one-time setup token and writes it to the server log. authInit
compares it in constant time, rejects a missing or wrong token with 403, and
clears it once the password is stored.

The setup page has a field for the token and api.initPassword sends it.
docker-compose publishes 8787 on 127.0.0.1 only, like 8788. README says where
to find the token.
2026-11-03 22:18:27 +01:00
BaaRai e6f4de5ddc fix(security): scrub startup secrets before any child can spawn
The scrub ran in main after server.New returned, but New resumes every
non-terminal task before returning, and those loops start workers that run
shells. Move the scrub into New, right after both environment readers, and
start MCP discovery after it as well: both paths spawn child processes.

New takes an option rather than scrubbing unconditionally. The test suite
builds several servers in one process and still needs ARTEX_PG_DSN on later
calls, so only the binary opts in.
2026-11-03 22:14:35 +01:00
Glauber Portella 80a1ee96f6 ollama llm 2026-10-23 18:12:38 -03:00
stevenAthompson 2d5f9677f4 chore: remove automatic update check and in-app self-update
The update source is hard-coded to a repository that no longer exists and the release checksums are unsigned. Removes the selfupdate package and the /api/update/* endpoints, the top-bar check that queried GitHub on every page load, the Version & Updates card, and the exit-75 swap protocol in the start scripts.
2026-10-22 23:58:37 -04:00
BaaRai ff017216bf Merge pull request #114 from BaaRai/feat/per-intent-llm-rotation
feat(engine): per-intent LLM chain-head rotation (opt-in llm_rotate)
2026-10-21 15:13:56 +02: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 b237fbaa32 feat(engine): ouroboros takes - goal ambiguity gate, failover escalation, stagnation surfacing
Three orchestration mechanisms from the Q00/ouroboros evaluation (#99):

- Goal ambiguity gate at task creation: a goal must name a target (URL,
  domain, IPv4, localhost:port or absolute path) and reach a length floor,
  else the API returns 400 before any token is spent. Deliberately
  target-less goals opt out with the literal marker [targetless]. Guards
  against the live failure where a goal naming no target sent every worker
  to an unrelated asset.

- Consecutive-failure escalation on the task LLM chain: quota exhaustion
  already advances the chain; now 3 consecutive non-quota call failures on
  the same chain profile advance it too, with the true reason recorded.
  Escalation-only by design (no auto-downgrade: mid-task model swaps risk
  behavior drift; the operator keeps the manual chain reset).

- Stagnation surfacing in the planner overview: finished intents are
  fingerprinted (case-folded, digits collapsed); a direction dispatched 3+
  times with zero findings since its first attempt is flagged in a
  self-describing stagnation_warning payload that instructs the planner to
  diversify instead of grinding. Absent when nothing stagnates.

Closes #99
2026-10-21 14:46:56 +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 b95ece6418 Merge pull request #106 from BaaRai/rsi/skill-gaps
feat(server): capability-gap report from skill misses
2026-10-21 14:24:15 +02:00
BaaRai cbb6f66c4b feat(server): capability-gap report from skill misses
GET /api/skill-gaps aggregates skill_usage rows with found=false by
skill, agent, and task category. Hits are returned for the same key so a
miss rate needs no second query; hit-only skills are excluded. The source
rows already existed: a miss is written when the model names a skill the
platform does not provide.

This is the evidence-backed capability backlog, not an auto-fixer: it
creates no skill and changes no prompt. Motivated by the harness and
skill-evolution sections of awesome-rsi (lobehub).
2026-10-19 00:33:08 +02:00
BaaRai cf369dec4f feat(server): per-task outcome card for RSI baselines
Adds GET /api/tasks/{id}/outcome: finding disposition and severity,
retest verdicts, result-row token totals, tool calls, skill misses, and
wall time in one row. This is the measurement layer an RSI loop needs
before claiming a change helped; it alters no agent behavior.

Metric selection follows harness2 (Google Research, Apache-2.0,
arXiv 2610.14224) and the task-gain / retained-gain / improver-gain
taxonomy in the awesome-rsi list.
2026-10-19 00:28:27 +02:00
BaaRai b269d3e590 fix(server): fill task metrics on the detail endpoint
GET /api/tasks/{id} returned the metrics block zeroed (tokens, goal
progress, in_flight, findings, last_activity) because only listTasks
populated those DTO fields. Extract the fill into applyTaskMetrics and
call it from both handlers so the two endpoints report identical
numbers from the same TaskListMetricsAll query. The swallowed query
error in listTasks is now logged instead of silently zeroing the table.

Test: TestTaskDetailReturnsListMetrics seeds result-activity tokens and
goal nodes, asserts the detail endpoint reports them and matches the
list endpoint; fails on main, passes with the fix.

Fixes #91
2026-10-13 14:49:47 +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 6b1f04b1ba fix: llm/test stored profile fills endpoint fields from the row
The fallback only borrowed the stored key; provider, model, base_url, proxy, streaming and thinking settings still came from the request form. An empty form (the list-view test button) then hit ConfigFrom defaults, sending the profile key to the canonical provider endpoint and 401ing. Fields the form leaves empty now come from the stored profile; explicit form values still win.
2026-10-12 08:54:29 +02:00
BaaRai 2853f085e5 fix: llm/test stored-key fallback skips the runtime's key normalization
The connection test resolves its API key as: form input, else the stored
profile's key, else the global key. Form keys pass through ConfigFrom,
which trims surrounding whitespace; the stored-key fallback assigned
p.APIKey raw. A key pasted with a trailing space or newline therefore
behaved asymmetrically: conversations worked (the runtime path loads the
profile through ConfigFrom and trims) while the connection test sent the
malformed header and failed auth with 401. SaveProfile had the same gap
one layer down, storing the raw pasted value.

Changes:
- the llm/test fallback now trims the stored key and session header name,
  matching ConfigFrom's normalization so the test sends byte-identical
  headers to what the runtime sends for the same profile
- SaveProfile trims the key before storing (and before deriving the
  hint), so surrounding whitespace never reaches the database
- tests: an integration test saves a profile with a whitespace-padded
  key, runs the real handler against a fake anthropic endpoint, and
  asserts the endpoint received the trimmed key; a second test pins the
  ConfigFrom trim the fallback must match
2026-10-12 07:37:32 +02:00
BaaRai 07d272727d fix: emit safe skill frontmatter YAML and verify it round-trips
Descriptions written through the API were interpolated raw into the SKILL.md
frontmatter, so a value containing a colon+space made the whole frontmatter
unparseable: every field silently stayed empty, the skill loaded under its
directory name, and a skill-gated MCP server was silently skipped. The same
applied to updates through the meta rewrite, and the shipped scopesentry skill
had a malformed name line all along.

- yamlScalar: plain-safe tokens stay bare, everything else single-quoted
- fsCreateSkill/fsUpdateSkillMeta emit safe values and re-verify the written
  file with the real skill parser (name + mcps must survive), rejecting the
  write otherwise
- warnBrokenSkills at startup logs skills whose name fell back to the
  directory (the signature of broken frontmatter)
- fix skills/scopesentry/SKILL.md name line

Closes #39
2026-10-11 22:26:22 +02:00
BaaRai d8f47d4359 Traffic ingest: capture operator browsing via a browser extension
Closes #33. The MITM proxy only sees the agent's own actions, so
anything behind a human gate (signup captcha, email OTP, 2FA, SSO)
left the authenticated surface dark. The operator's browser becomes a
capture source instead:

- traffic: exported RecordExternal storing an externally captured
  exchange through the same spill/blob/FTS path as proxy traffic
  (insert logic shared with record()); rejects unparsable URLs and
  empty methods
- server: POST /api/traffic/ingest inside the authenticated API group,
  batch capped at 50 entries, bodies truncated at 256 KiB, per-entry
  ingested/failed counts
- extension/: Manifest V3 Chrome/Edge extension driving the
  chrome.debugger Network domain on the chosen tab, batching text
  exchanges to the ingest endpoint with the operator's URL + JWT in
  chrome.storage.local; binary bodies skipped; flush on batch/stop
- tests for both layers, README section
2026-10-11 14:53:08 +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 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
Autumn-27 8bc4d58c85 feat(intercept): 模型兜底审批单独计量 token 并在配置页展示
兜底审批(LLM judge)调模型判定漏网命令,其 token 消耗此前无法单独查看:
usage 行要么无 worker 归属、要么被并进复用的模型 profile 桶里。

- llmrec 增加 WithWorker 上下文覆盖,reviewer 调用前标注 worker='judge',
  让审批调用的计量行可被单独筛出(复用现有 llm_usage.worker 列,无需迁移)。
- db.JudgeUsageStats 返回累计总量 + 近 N 天每日序列(WHERE worker='judge')。
- 新增 GET /api/intercept/judge/usage。
- 配置页「模型兜底审批」开关下方新增用量卡片:调用次数 / 输入 / 输出 /
  缓存读写 五项统计 + 近 30 天每日消耗迷你柱图 + 刷新。
2026-10-04 02:43:26 -04: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 eb347d5965 feat(traffic): 流量列表新增「清空全部」,并借空库时机压实索引
一次删除全部流量记录,忽略当前筛选条件。与按 host 删除的两条路径相比多做
两件事:

- 清理索引已不再记录的历史 host 目录。按 host 删除只能从索引反查目录,
  行已消失的孤立目录会一直留着;「清空全部」既然语义是不留残余,就直接
  扫顶层目录(跳过 _index/_blobs/_ca/_delete_staging)。
- 收尾做一次全量压实(optimize + VACUUM + wal_checkpoint(TRUNCATE)),把
  索引占用的磁盘空间还给系统,并把实际释放量返回给前端提示。

VACUUM 的耗时与临时空间只和它保留的内容成正比,所以空库是做全量压实唯一
免费的时机——也正因如此,这一步顺带成为已有实例的转换途径:auto_vacuum
只能通过 VACUUM 生效,清空一次之后旧库即转为增量回收模式,此后日常按
host 删除就能自行回收空间(TestDeleteAllConvertsLegacyIndex 覆盖)。

已绑定到漏洞的流量证据保存在独立的证据库中,不受影响——绑定时已拷出独立
副本,正是为了让热流量可以随时清空而不带走证据。确认弹窗里明确写了这点。

前端把「清空全部」做成 outline 样式而非第二个 destructive 按钮:它忽略一切
筛选条件,不该看起来和「删除该目标」只差一次误点;无流量时禁用。

测试:新增 TestDeleteAllPurgesAndCompacts(清空后索引、正文、blob 引用与
孤立目录全清,空间回收到 1/8 以下,清空后仍可继续录制)与
TestDeleteAllConvertsLegacyIndex(旧库经清空后转为增量回收,随后普通删除
即可回收)。
2026-09-30 02:50:49 -04:00
Autumn-27 6d8c50a12e feat(traffic): 流量列表支持内容检索、路径/状态码/长度筛选与列排序 (#177)
- 后端 traffic.Page 改为 PageQuery:新增响应内容(FTS)、路径(url_template)、
  状态码(精确/Nxx 分桶)、响应长度范围筛选,并支持按 ts/status/resp_len 排序
  (尾随 id DESC 保证分页稳定);补 status/resp_len 两个索引(IF NOT EXISTS,旧库自动生效)
- server getTraffic 解析 body/path/status/resp_min/resp_max/sort/order 参数
- 前端流量页新增高级筛选行(响应内容/路径/状态码/长度范围)与可排序表头,
  排序偏好持久化到 localStorage;抽出公共组件 ui/sortable-head
- 仅改动前端页走的 Page,不动 Agent 工具的 query 契约
2026-09-29 21:58:17 -04:00
Autumn-27 1a10c153b0 feat(tasks): 漏洞列改为按严重度分档显示 严/高/中/低
- TaskListMetricsAll 用 COUNT(*) FILTER 按 severity 分四档统计(同一条查询)
- TaskListMetrics.Findings 及 TaskDTO.findings 由数字改为 {critical,high,medium,low}
- 列表页「漏洞」列渲染 x/x/x/x,配色沿用 severity 语义、为 0 的档位弱化
- severity 为空/白名单外的记录不计入任一档;mock 同步为对象结构
2026-09-24 13:32:45 -04:00
Autumn-27 ee33f91bc1 feat(tasks): 任务列表新增漏洞数量列,描述/目标列收窄
- 任务列表接口 TaskListMetricsAll 批量统计每任务的漏洞数(findings 表,
  按 task_id),经 TaskDTO.findings 下发;列表页新增「漏洞」列,>0 高亮
- 描述/目标列宽 max-w-xs → max-w-40,目标列补 title 悬浮全文
- mock data/handler 同步补 findings 字段
2026-09-24 13:26:22 -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
Autumn-27 0848eb6a0a feat(web): 播报板节点详情展示上游节点信息与锚定资产
上游/下游节点从只显示 id 改为悬停弹出节点名片(类型/状态/来源/时间/摘要/payload);
展开时顺带展示该节点锚定的资产(类型标签 + 可辨识文本)。

服务端 /exploration/nodes 新增 assets 字段(节点id→资产),读 exploration_anchors
并复用发现列表的资产打标逻辑;同页节点互引时用本页 items 兜底 refs 查找表。
2026-09-17 11:23:53 -04:00
Autumn-27 6c6311ff63 refactor(engine): 移除从不执行的全局 planner/worker 空壳,就绪信号改用谓词
权威解析器(SetAuthoritativeAgentResolver)落地后,引擎每轮规划/worker 都走
per-task 的 agentsForTask 路由;applyLLM 经 buildPlannerWorker 构建、UseLLM 装入
的全局 planner/worker 从不被执行,仅充当 Ready() 的'已配置'探针。这层空壳是
功能容易被错误接线的温床(冷节点压缩此前就误接在它上面)。

改动:
- 删除 buildPlannerWorker、providerForAgent(仅前者调用)、Engine.UseLLM/snapshot/
  Mode、Engine.planner/worker/mu 字段。
- Ready() 改为读 SetReadiness 注入的谓词(基于 s.llmOn);snapshotFor 去掉全局
  兜底,权威解析器返回 nil 即表示该任务不可用(语义不变)。
- applyLLM 不再构建全局 pair;NewServer 注入就绪谓词。
- 相应更新 3 处依赖旧注入方式的测试(改用权威解析器注入)。

生产行为不变(全局 pair 本就从不执行);go build/vet 通过,server 测试失败集合
与基线一致(均为需 DB 的既有用例),无新增失败。
2026-09-17 09:40:06 -04:00
Autumn-27 79cd8d6d9d chore(server): 删除已死的全局 mainAgent 与 agentsForProfile 等冗余构建
per-task 路由(agentsForTask)落地后,这些全局副本已无人使用:
- 全局 mainAgent(字段 + applyLLM 里的构造 + mainAgentRef getter):真正在跑的是
  agentsForTask(t).main,全局那份从不被读取,mainAgentRef 无调用者。
- agentsForProfile:旧的任务 pin 路径,已被 agentsForTask 取代,无调用者。
- 随之失活的 profBundle 类型与 profAgents 缓存(仅初始化/清空、再无存取)一并移除。

均为编译器可确认的死代码,不改变任何执行路径;chat 的全局兜底(chatAgent/
chatAgentForProfile)、provider 缓存(provByProfile)等仍在用,保留不动。
全局 planner/worker 空壳与 Ready()/Mode() 改造留待第二步。
2026-09-17 09:07:40 -04:00
Autumn-27 b932888b49 feat(web): 任务详情页新增探索链路播报板
按创建时间分页平铺展示探索节点,作为探索链路图的时间轴视图,
复用同一份 exploration_nodes 数据,不影响链路图。

- 后端 NodesPage/NodesByIDs/EdgesTouching:分页下推 SQL,
  支持 kind/state 过滤、payload/origin 搜索、正倒序,
  并补出当页节点的相邻边与对端节点
- 新增 GET /api/exploration/nodes(仅本任务节点,不含继承)
- 播报板:时间轴分行、按天分组、类型快筛、搜索防抖、
  自动刷新、直播位未读提示、展开看上下游与 payload
- 补 mock,NEXT_PUBLIC_MOCK=1 下可无后端预览
2026-09-17 08:42:49 -04:00
RuoJi6 48b26be980 feat(chat): 支持多类型记录 @ 引用及滚动分页 (#135) 2026-09-16 22:36:07 +08:00
Autumn-27 a6515e6f0d Merge branch 'feat/noa-experimental':noa 实验性上下文压缩 2026-09-15 12:09:24 -04:00