Skip to content

ADR-0081: DoYour.Art Phase A — operator-usable Art Studio

  • Status: Accepted
  • Date: 2026-05-23
  • Driver: discuss_do_your_art_feature_2026_05_10 locked 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-offensive RuntimeClass (runsc) → syscall-level isolation
  • Non-root user (UID 1000), no ServiceAccount token mounted
  • NetworkPolicy egress restricted to DUT INSIDE subnets only
  • python3 -I isolated mode → no PYTHONPATH / PYTHONHOME / user site-packages leakage
  • Subprocess timeout enforced 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 /craft endpoint (DOYOUR-1, v3.7.0) and the free-form /exec-python endpoint (DY-A2) both live on the same pod. The structured endpoint will eventually become a thin convenience layer that POSTs to /exec-python with 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 because eval() 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