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.
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.
Per-level token/turn budgets: task -> intent (worker run) -> planner round
(counted inside the task). A child's effective budget is
min(own budget, parent's remaining above the wrap-up reserve). When the task
can no longer afford one worker, workers stop claiming intents and the task
settles through one forced wrap-up planner round (the existing settle
sequence, budget words instead of the timeout words).
Config: ARTEX_TASK_BUDGET_TOKENS, ARTEX_TASK_BUDGET_TURNS,
ARTEX_INTENT_BUDGET_TOKENS, ARTEX_INTENT_BUDGET_TURNS. 0 or unset = unlimited;
with all four unset nothing changes.
When ARTEX_TOOL_RAG is enabled, AugmentTools keeps the core tools plus the
top-k (ARTEX_TOOL_RAG_TOPK, default 12) BM25-ranked bound tools for the
worker intent / planner goal. Runs after ToolResolve, so it only removes;
falls back to the full list on empty query, catalog under 20, or scorer error.
ARTEX_UNTRUSTED_BOUNDARY unset or empty now enables the boundary.
0, false, off and no (case-insensitive, trimmed) disable it; any other
value keeps it on. Docs state the default, the opt-out and the measured
cost (about 154 bytes per tool result plus one system note).
The planner and worker user messages now carry worker output, finding
details, recent_facts, finding_list, cold_digests, related_tasks and hints
not written by the operator inside a code-owned data envelope whose nonce
comes from crypto/rand (16 bytes, hex) once per message. Embedded marker
stems are neutralised with a fixed-string replacement. The task goal, goal
nodes, constraints and the operator's own hints stay outside, labelled
operator. Overview fact summaries are capped at factSummaryMax (160 runes),
the cap list_facts already uses.
record_fact takes a source field from a closed enum (target_response,
tool_output, third_party_intel, operator_input, inference). An empty value
is stored as inference; any other value is refused with the allowed list.
The source is stored in the fact payload.
The writer of a node is already recorded by the server in the origin
column. compactNode now surfaces origin and source, so recent_facts in the
overview, list_facts, expand_digest members and related-task facts carry
them. The cold-digest compressor input carries source and origin next to
confidence; the digest body is unchanged model text.
prove_goal refuses a fact whose source is target_response, in addition to
the existing confidence=observed rule. The goal stays open; findings are
still accepted.
The worker, pentest and planner default prompts ask for the source, and the
planner treats target_response facts as non-authoritative for goals.
Refs #124 (F4).
Audit findings F5, F6 and F11 for issue #124.
F5, goal guards:
- goal_met refuses while any goal node of the task is open, and when the
goals cannot be read. A task without goal nodes keeps the old behavior.
- prove_goal accepts a fact as evidence only when its confidence is
"observed"; otherwise it refuses and the goal stays open. Findings are
still accepted.
F6, hint origin:
- add_hint stores the calling ToolSet's worker as the hint origin instead
of "human", and passes it to the planner trigger (TriggerEvent.HintOrigin).
- renderTriggers names the writer: the human via the main agent, a
cross-task orchestration, or an agent, with a neutral fallback.
F11, built-in rules for the planner and the main agent:
- Interceptor.MatchBuiltin evaluates the enabled "[built-in]" deny rules on
the intercept-enabled tools.
- The target-only guard applies them before the target gate, whether the
gate is on or off. User rules and the judge stay out of it.
- SetRulesForTest is a test seam for the store-less guard tests; Log is a
no-op without a store.
Phase B, part 1 of the agentic attack-class audit (issue #124).
- Planner and cold-digest compactor: a code-owned untrusted-data tail
after the rendered body. Worker summaries, facts, digests, traces and
tool outputs are data; they cannot add goals, targets or scope, end the
task, or be relayed through steer_work as instructions.
- The #49 untrusted-content block moves out of the editable section [A]
of the worker, mainagent, auto and pentest seeds into a code-owned tail,
also applied to the reporter. A body that already carries the block
verbatim (rows seeded before this change) does not get it twice.
- Planner user message: a code-owned goal anchor after the situational
block restates the task goal and the operation constraints and asks each
new intent to name the goal id it serves. The message builder is
factored into plannerRoundInput for pure tests.
- bbot-recon skill: every event field is untrusted; only scope_distance
== 0 reaches insert_assets; scope_distance > 0 stays an intel fact with
source=third_party_intel, never auto-scoped; FINDING text is a lead.
Tests: prompt_injection_seed_test.go checks that a PromptOverride without
the block still renders the tail for every role, planner and compactor
included, that the block is not duplicated, and that the planner input
ends with the goal anchor. shipped_skills_test.go checks the bbot markers.
In a gated scope, the two HTTP clients the platform owns connect only to
addresses that a pin or a stored exemption covers at the moment of the
connection, on every hop.
- intercept.PinnedDialer(tool, scoped): the DialContext of both clients.
It reads the target scope from the context net/http passes to the dial.
Gate off, or no scope in a client built without one: plain dial, as
today. A client built for a scope whose scope is missing at the dial
fails closed with TARGET_GATE_UNAVAILABLE.
- dialTarget runs the gate decision on the dialled host:port with a fresh
lookup (block rules first, exemptions, pins, port precedence) and
connects only to the addresses it compared, never to the name. Derived
exemptions and name-only pins dial the address as received. An address
no pin covers finds or opens the request (new_ip / no_pin, wait 0) and
fails at once with TARGET_APPROVAL_PENDING. Redirect hops are dialled
against the hop's own (host, port).
- WebFetch (withWebFetch, now given the run context): DialContext set on
every agent kind; DisableDirectRetry when the run's scope is gated.
- Custom http tool: a direct call in a gated scope gets a DefaultTransport
clone with the pinned dialer, no environment proxy and keep-alive off.
Proxied calls are unchanged (the recording proxy is step 2.2).
- Name-only: request reason says the platform does not bind the
connection; the first-use pin_match row records the effective state.
- Derived exemptions: a proxy URL without a port is listed with the port
net/http dials (1080 for socks5) (OPEN-9).
- go.mod: Norma v0.4.8 (DialContext, DisableDirectRetry).
WebFetch and the custom http tool followed redirects with net/http's
default policy, so a pinned host could send the client to a target nobody
approved. RedirectCheck is now the CheckRedirect policy of both clients.
- Gate off, or no scope (operator test endpoint): only the 10-hop limit,
same error text; nothing is parsed, resolved, read or written.
- Gate on: each hop runs through the call's decision code (URL rules on
the hop URL, block rules before any lookup, exemptions, resolution,
pins). A hop no pin covers is not followed and never waits: its request
row is found or created and the call fails with TARGET_APPROVAL_PENDING.
http -> https asks again for port 443 unless a (host, 0) pin exists. An
unreadable hop is blocked with TARGET_GATE_UNAVAILABLE.
- Rows raised by a hop record redirect_from in the audit targets.
- Norma v0.4.7 has WebFetchConfig.CheckRedirect but no agentcore option,
so the four agent kinds build WebFetch themselves (same proxy, CA and
position in the tool list) instead of EnableWebFetch. Norma carries the
policy to the direct retry client.
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.
Norma v0.4.6 frames tool output between nonce-tagged markers, so the model
reads it as data. ARTEX passes that option from the environment: 1, true or
on enable it (case and surrounding spaces ignored); unset or anything else
keeps it off, which is the default. Nothing changes unless the variable is set.
The option is wired into the four agentcore.Options builders that set
ToolOutputDir: worker, planner, chat and the main agent. .env.example
documents the variable, commented out, with a note to test it on a local
target before enabling it in production.
Not included: docker-compose.yml does not pass the variable to the artex
service, so it has no effect under compose until it is listed there.
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).
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