ADR-0081: DoYour.Art Phase A — operator-usable Art Studio¶
- Status: Accepted
- Date: 2026-05-23
- Driver:
discuss_do_your_art_feature_2026_05_10locked Phase A as "operator can write Scapy/Go/PCAP pieces from the dashboard and run them inside a gVisor sandbox". The v3.7.0 scaffold (PR #678) shipped the pod + K8s manifests + TypeScript schemas, but the operator surface was a placeholder — the locked decision was incomplete. - Related: PRs #992 (DY-A1), #993 (DY-A2), #994 (DY-A3), #995 (DY-A4), #996 (DY-A5), #997 (DY-A6), this PR (DY-A7).
Context¶
DoYour.Art is the third member of the offensive suite (alongside
KALI.Art and the structured /craft mode of the Scapy pod). The
suite serves audiences orthogonal to the guided Test Plan wizard:
pen testers, vendor QA, vulnerability researchers, advanced cert
students. None of Spirent / Keysight / Ixia offers this level of
free-form crafting — the locked memo positioned it as a moat feature.
The pod (doyour-art), the gVisor RuntimeClass, the VLAN 2901 NAD,
the persona-bound NetworkPolicy, and the TypeScript schemas (modes,
pcap-replay, token-escrow, art-studio) shipped in v3.7.0
(PR #678) as scaffolds — useful as design documents, not usable
as features. The route /art-studio did not exist; the pod's HTTP
endpoint accepted only structured spec JSON, not free-form Python;
there was no upload path for PCAP files; no tier admission gate; no
escrow accounting.
The dashboard-only operator-access invariant (dashboard_only_operator_access_2026_05_13) treats any feature without UI as incomplete. Phase A closes that gap end-to-end.
Decision¶
Ship DoYour.Art Phase A in 7 sub-PRs, each one independently useful + admin-mergeable. The 5 open questions from the discuss memo are answered as follows:
| # | Question | Decision | Sub-PR |
|---|---|---|---|
| 1 | Monaco LSP | defer — DY-A5 ships Monaco with built-in keyword highlighting; full Scapy LSP is a v5.1 follow-up | DY-A5 |
| 2 | PCAP storage | in-memory LRU now + MinIO on-prem in DY-A4b — durable backing waits for the bucket-and-quota setup | DY-A4 |
| 3 | Go embed compilation | sidecar build pod (doyour-art-go-builder), not in-pod toolchain — keeps the Scapy image ~200 MB instead of ~700 MB |
DY-A3 |
| 4 | Audit log granularity | summary only v1 (PPS / duration / src-IP histogram); per-packet defers to v5.1+ | DY-A2 |
| 5 | Templates library | hybrid — TLSStress.Art curated seed + community PRs accepted; gallery in v5.5 | DY-A1 |
Phase A sub-PRs¶
| Sub-PR | Scope | Status |
|---|---|---|
| DY-A1 | Dashboard route /art-studio/* with 3 tabs (Scapy / Go embed / PCAP replay), shared layout, textarea editor, REST API stub, in-memory simulator store, E2E baseline |
✅ |
| DY-A2 | Pod-side POST /exec-python (free-form Python under python3 -I, hard subprocess timeout, summary-only audit), dashboard bridge gated on DOYOUR_ART_POD_URL |
✅ |
| DY-A3 | New module doyour-art-go-builder/ (Go server, POST /build-and-run, sidecar K8s deployment with toolchain pre-warm); dashboard bridge gated on DOYOUR_ART_GO_BUILDER_URL |
✅ |
| DY-A4 | Pod-side POST /replay-pcap (tcpreplay), dashboard POST /api/doyour/pcaps upload + in-memory store, file picker UI auto-fills SHA-256 |
✅ |
| DY-A5 | Monaco editor via @monaco-editor/react, lazy-loaded via next/dynamic, textarea kept as no-JS fallback |
✅ |
| DY-A6 | tiers.ts (free / indie / team / enterprise limits), escrow.ts (in-memory hold/settle/refund/overage), createArtPiece() gate, 429 + Retry-After on admission denial, escrow row in RunPanel |
✅ |
| DY-A7 | Extended Playwright e2e (PCAP upload + tier 429 + escrow row), updated module docs in 3 languages, this ADR | ✅ |
Security envelope (shared across all sub-PRs)¶
gvisor-offensiveRuntimeClass (runsc) → syscall-level isolation- Non-root user (UID 1000), no ServiceAccount token mounted
NetworkPolicyegress restricted to DUT INSIDE subnets onlypython3 -Iisolated mode → no PYTHONPATH / PYTHONHOME / user site-packages leakage- Subprocess
timeoutenforced per request, capped at the pod-level free-tier 5 min ceiling - Tier B per ZTP-prem tier policy → no proprietary code leaked to operator script context
- Summary-only audit (no source body, no per-packet) per locked question 4
Consequences¶
Positive
- The dashboard-only invariant is satisfied for the offensive-suite third member.
- The 7-sub-PR cadence keeps each merge small + reviewable + revertable (the admission-deny path can be rolled back without touching Monaco or PCAP wiring).
- The dual-mode pattern (env var enables real backend, fallback is the simulator) keeps local dev + e2e tests working without an attached cluster.
- The locked decisions are documented in ONE place — future operator questions ("why no Monaco LSP?", "why a separate Go builder pod?") have a single, canonical answer.
Negative / deferred
- DY-A4b still owes durable MinIO bucket-and-quota wire + BPF filter input + MAC / IP rewrite options.
- DY-A6b still owes Postgres-backed escrow rows + the MGMT-plane UTXO vault integration.
- The structured
/craftendpoint (DOYOUR-1, v3.7.0) and the free-form/exec-pythonendpoint (DY-A2) both live on the same pod. The structured endpoint will eventually become a thin convenience layer that POSTs to/exec-pythonwith a templated source. Not done yet — too much risk while operators still depend on the structured shape. - Community gallery + cross-MÓDULO orchestration + templated art pieces (Phases B, C, D, E of the discuss memo roadmap) are explicitly out of scope for Phase A.
Alternatives considered¶
- Skip DY-A4 (PCAP replay) for Phase A — would have left one of the three locked modes broken. Rejected: the three modes are the operator-facing identity of the feature; shipping two of three ships a half-feature.
- Use
eval()for Python free-form execution instead of subprocess — rejected becauseeval()shares state with the HTTP server (one bad script wedges the server) and gives the attacker a much larger surface area than a subprocess with a fresh environment. - Single image carrying both Python and Go toolchains — rejected because the Scapy image stays ~200 MB and the Go image is ~500 MB; bundling them would slow down both rollouts and would couple Python security CVEs to Go build paths.
- Real Postgres escrow rows in DY-A6 — rejected because that requires a Drizzle migration + MGMT-plane vault wire that is out of scope for the operator-facing Phase A. The in-memory shim validates the API + UI shapes; the durable backing is a drop-in replacement.
Compliance¶
Per
CLAUDE.md:
"Dashboard is the ONLY operator interface." Phase A ships every
operator-facing primitive (craft, run, observe, escrow display) via
/art-studio/*; no kubectl edit, curl admin/, or pod exec
required.
Per the locked ZTP-prem posture: Tier B obfuscation is preserved
(both pods stay on the ztp-prem.tlsstress.art/tier: B label).
gosec W0 baseline stays at 0 findings post-merge (the new modules
ship with inline-annotated suppressions that map 1:1 to the rationale
in PR #991).
References¶
- discuss_do_your_art_feature_2026_05_10
- ADR-0025 — offensive-suite parent ADR
- project_dashboard_only_operator_access_2026_05_13
- PR #678 — v3.7.0 scaffold
- PRs #992, #993, #994, #995, #996, #997 — DY-A1 through DY-A6