Goal
A visual, drag-and-drop way to assemble an attack (Target, Dataset/Goals, Attack technique + params, optional Guardrails, optional chained fallback steps) from inside the Local Dashboard, run it, and see results in the same dashboard — without dropping to the CLI first.
Current state
The building blocks already exist and are well-factored on the CLI side — this should be a UI over them, not a new execution engine:
- Attack catalog:
cli/commands/attack/catalog.py lists all 14 supported attack_types (AdvPrefix, PAIR, TAP, AutoDAN-Turbo, BoN, CipherChat, h4rm3l, PAP, MML, FC/tFC-Attack, FlipAttack, Static Template, Baseline) with label + description — this is directly usable as the palette of draggable "attack" blocks.
- Shared config shape:
_build_attack_config in cli/commands/attack/config.py builds a plain dict — {"attack_type": ..., "goals": [...] | "dataset": {...}, ...} — that already separates the Dataset/Goals block from the technique block.
- Target + guardrail blocks:
cli/commands/attack/options.py defines the common fields every attack needs: target (agent-name/agent-type/endpoint), and optional before/after guardrails (identifier/agent_type/endpoint each, via _build_guardrail_config).
- Dataset providers:
hackagent/datasets/providers/ — huggingface.py, url_json.py, file.py, plus presets.py/intents.py — these are the concrete "Dataset" block variants.
- Chaining already has fallback-ladder semantics —
hackagent eval chain (cli/commands/attack/chain.py) and HackAgent.hack_chain() (agent.py:327) run an ordered list of attack_configs against a shared goal pool: a goal that succeeds is dropped, a goal that's mitigated escalates to the next attack in the list. This is exactly the semantic a canvas's node connections should express — connecting attack-block A → attack-block B on the canvas is "B is A's fallback," not a generic graph. The board doesn't need to invent new orchestration; it needs to produce the attacks: [...] list hack_chain() already consumes.
- Progress streaming already exists, just not wired to the dashboard:
HackAgent.hack()/hack_chain() take a _tui_event_bus parameter, threaded through attacks/orchestrator.py and every technique's base.py/generation.py, currently consumed only by cli/tui/views/attacks/executor.py. Live progress in the new dashboard panel should plug into this same bus rather than poll.
- What's missing today: the dashboard (
server/dashboard/) is currently read-only — its mixins (_runs_mixin.py, _reports_mixin.py, etc.) only load and render runs/results that already exist in self.backend. There is no code path anywhere under server/dashboard/ that constructs a HackAgent or calls .hack()/.hack_chain(). This issue is the first time the dashboard becomes a place attacks are launched, not just reviewed.
Proposed design
- New
DashboardAttackBuilderMixin (server/dashboard/_attack_builder_mixin.py), following the existing mixin-per-concern pattern wired into DashboardPage in _page.py, with its own nav entry alongside dashboard/runs/history/reports.
- Canvas nodes map 1:1 to existing config sections, not to something new:
- Target block →
agent-name/agent-type/endpoint
- Dataset/Goals block →
goals list or dataset section (provider + params)
- Attack block(s) → one per
ATTACK_CATALOG entry, palette generated from that dict so a new CLI-supported technique automatically appears on the canvas with no dashboard code change
- Guardrail block(s) → optional before/after, same three fields as the CLI
- Connecting two attack blocks in sequence → append to an
attacks: [...] chain list (fallback-ladder order), matching --config-file's documented chain shape in chain.py
- Submit → serialize to the exact same dict shape
_build_attack_config/chain already build, then call HackAgent(...).hack(attack_config) or .hack_chain(attacks=[...]) in-process from the NiceGUI server (this is what "local mode" already means for hackagent web — see cli/commands/web.py), on a background task so the event loop isn't blocked, streaming progress into the panel via _tui_event_bus.
- Draft persistence: canvas layouts (node positions + the underlying config, before it's run) need to be saved/reopened. Reuse the active
StorageBackend (server/storage/local.py for SQLite, server/storage/remote.py for the hosted API — see below) rather than inventing a separate file format, so drafts round-trip the same way runs do.
- Results: once a run is submitted this way, it's written through the same
StorageBackend the read-only mixins already query — no new results-rendering code needed, the existing Runs/History/Reports panels pick it up automatically.
Open question to resolve before implementation: what does "local vs remote mode" mean here
The issue text says results should save "accordingly" to local or remote mode and still be visible in the local dashboard — but today those two things are coupled differently than that implies:
server/storage/ already has both a local.py (SQLite) and remote.py (hosted API) StorageBackend, so a HackAgent instance genuinely can persist either way.
- But
hackagent web itself (cli/commands/web.py) currently treats "remote mode" (API key configured) as "don't run the local NiceGUI dashboard at all — open app.hackagent.dev instead." So there is currently no scenario where the local dashboard is running and writing through the remote backend.
Before building the canvas, decide: (a) should hackagent web gain a mode where it still serves the local UI but a configured API key routes attack runs through the remote StorageBackend (diverging from today's all-or-nothing redirect), or (b) is "remote mode" out of scope for this issue and the builder only ever targets the local SQLite backend, with the cloud dashboard (a separate codebase, hackagent-webapp) getting its own equivalent builder later. This changes where the "run" button's write path goes.
Acceptance criteria
Goal
A visual, drag-and-drop way to assemble an attack (Target, Dataset/Goals, Attack technique + params, optional Guardrails, optional chained fallback steps) from inside the Local Dashboard, run it, and see results in the same dashboard — without dropping to the CLI first.
Current state
The building blocks already exist and are well-factored on the CLI side — this should be a UI over them, not a new execution engine:
cli/commands/attack/catalog.pylists all 14 supportedattack_types (AdvPrefix, PAIR, TAP, AutoDAN-Turbo, BoN, CipherChat, h4rm3l, PAP, MML, FC/tFC-Attack, FlipAttack, Static Template, Baseline) with label + description — this is directly usable as the palette of draggable "attack" blocks._build_attack_configincli/commands/attack/config.pybuilds a plain dict —{"attack_type": ..., "goals": [...] | "dataset": {...}, ...}— that already separates the Dataset/Goals block from the technique block.cli/commands/attack/options.pydefines the common fields every attack needs: target (agent-name/agent-type/endpoint), and optional before/after guardrails (identifier/agent_type/endpointeach, via_build_guardrail_config).hackagent/datasets/providers/—huggingface.py,url_json.py,file.py, pluspresets.py/intents.py— these are the concrete "Dataset" block variants.hackagent eval chain(cli/commands/attack/chain.py) andHackAgent.hack_chain()(agent.py:327) run an ordered list of attack_configs against a shared goal pool: a goal that succeeds is dropped, a goal that's mitigated escalates to the next attack in the list. This is exactly the semantic a canvas's node connections should express — connecting attack-block A → attack-block B on the canvas is "B is A's fallback," not a generic graph. The board doesn't need to invent new orchestration; it needs to produce theattacks: [...]listhack_chain()already consumes.HackAgent.hack()/hack_chain()take a_tui_event_busparameter, threaded throughattacks/orchestrator.pyand every technique'sbase.py/generation.py, currently consumed only bycli/tui/views/attacks/executor.py. Live progress in the new dashboard panel should plug into this same bus rather than poll.server/dashboard/) is currently read-only — its mixins (_runs_mixin.py,_reports_mixin.py, etc.) only load and render runs/results that already exist inself.backend. There is no code path anywhere underserver/dashboard/that constructs aHackAgentor calls.hack()/.hack_chain(). This issue is the first time the dashboard becomes a place attacks are launched, not just reviewed.Proposed design
DashboardAttackBuilderMixin(server/dashboard/_attack_builder_mixin.py), following the existing mixin-per-concern pattern wired intoDashboardPagein_page.py, with its own nav entry alongsidedashboard/runs/history/reports.agent-name/agent-type/endpointgoalslist ordatasetsection (provider + params)ATTACK_CATALOGentry, palette generated from that dict so a new CLI-supported technique automatically appears on the canvas with no dashboard code changeattacks: [...]chain list (fallback-ladder order), matching--config-file's documented chain shape inchain.py_build_attack_config/chainalready build, then callHackAgent(...).hack(attack_config)or.hack_chain(attacks=[...])in-process from the NiceGUI server (this is what "local mode" already means forhackagent web— seecli/commands/web.py), on a background task so the event loop isn't blocked, streaming progress into the panel via_tui_event_bus.StorageBackend(server/storage/local.pyfor SQLite,server/storage/remote.pyfor the hosted API — see below) rather than inventing a separate file format, so drafts round-trip the same way runs do.StorageBackendthe read-only mixins already query — no new results-rendering code needed, the existing Runs/History/Reports panels pick it up automatically.Open question to resolve before implementation: what does "local vs remote mode" mean here
The issue text says results should save "accordingly" to local or remote mode and still be visible in the local dashboard — but today those two things are coupled differently than that implies:
server/storage/already has both alocal.py(SQLite) andremote.py(hosted API)StorageBackend, so aHackAgentinstance genuinely can persist either way.hackagent webitself (cli/commands/web.py) currently treats "remote mode" (API key configured) as "don't run the local NiceGUI dashboard at all — openapp.hackagent.devinstead." So there is currently no scenario where the local dashboard is running and writing through the remote backend.Before building the canvas, decide: (a) should
hackagent webgain a mode where it still serves the local UI but a configured API key routes attack runs through the remoteStorageBackend(diverging from today's all-or-nothing redirect), or (b) is "remote mode" out of scope for this issue and the builder only ever targets the local SQLite backend, with the cloud dashboard (a separate codebase,hackagent-webapp) getting its own equivalent builder later. This changes where the "run" button's write path goes.Acceptance criteria
ATTACK_CATALOG(no hardcoded technique list to keep in sync)hack_chain()-compatibleattackslist; single block produces a plainhack()callStorageBackendwithout blocking the NiceGUI event loop, with live progress via_tui_event_bus