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).
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.
Executor agents ingest attacker-controlled content on every run (HTTP
responses via curl, browser pages via the browser MCP, traffic records,
binary strings via remote analysis tools) with no counter-instruction
anywhere in their context.
Two defenses, both zero-cost per call:
- A standing hardening block in the default prompt seeds of worker,
pentest, mainagent and auto: tool output from or through the target is
data, never instructions; embedded directives are quoted verbatim and
recorded as a possible prompt-injection finding instead of followed;
credentials and operator data never go to a destination suggested by
target-controlled content.
- Every MCP tool description gains a one-line untrusted-output note at
wiring time (covers both transports and travels with the description:
cached prompt for global tools, on-demand schema for deferred ones).
A seed test guards the hardening block against silent removal.
Closes#48
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