6
Engine Workflow DSL v2
Engine workflow Guardian memisahkan kompilasi dari eksekusi. core/workflow_schema.py adalah compiler sekaligus DSL, sementara eksekusi hidup di core/workflow.py (core/workflow_schema.py:141). Dokumen YAML divalidasi dan diubah menjadi CompiledWorkflow sebelum satu tool pun dijalankan, lalu engine menelusuri levels hasil kompilasi generation demi generation (core/workflow_schema.py:1-252).
Bentuk dokumen: WorkflowStep
Setiap step v2 wajib punya id. Field itu menjadi kunci namespace untuk depends_on dan untuk template {{ <id>.parsed.<key> }} (core/workflow_schema.py:1-252). Validator hanya ketat pada dua field: id harus cocok dengan ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$, dan type harus salah satu dari tool, analysis, report setelah di-lowercase (core/workflow_schema.py:39-543, core/workflow_schema.py:56-744).
| Field | Tipe | Batas / nilai | Peran |
|---|---|---|---|
id | str | ≤64 karakter, regex ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$ | kunci step_results, target depends_on |
type | str | tool / analysis / report | default tool |
tool | str opsional | nama tool | dipakai step bertipe tool |
agent | str opsional | bebas, tanpa validator enum | diteruskan ke _execute_step |
objective | str | ≤500 karakter | label tujuan step |
parameters | dict | nilai str boleh template | dirender sebelum eksekusi |
depends_on | list[str] | id yang harus ada | menentukan generation |
when | str opsional | ≤400 karakter | ekspresi Jinja2, falsy berarti skip |
agent tidak punya validator, jadi nilai seperti debate atau visual baru bermakna saat _execute_step memilih cabang analysis (core/workflow_schema.py:42-545). Pembagian peran antar agen dijelaskan di Sistem Multi-Agen.
compile_workflow: validasi, migrasi, dan level
graph LR
A["YAML dict"] --> B{"mode autonomous atau tanpa steps?"}
B -->|ya| C["CompiledWorkflow kosong"]
B -->|tidak| D{"ada step yang punya id?"}
D -->|tidak| E["_migrate_v1_steps"]
D -->|ya| F["WorkflowDoc.model_validate"]
E --> F
F --> G{"id duplikat atau depends_on asing?"}
G -->|ya| H["WorkflowCompileError"]
G -->|tidak| I["_topological_levels"]
I --> J{"semua node terurut?"}
J -->|tidak| K["Cycle detected"]
J -->|ya| L["CompiledWorkflow.levels"]
style A fill:#f59e0b,color:#000
style C fill:#dc2626,color:#fff
style H fill:#dc2626,color:#fff
style K fill:#dc2626,color:#fff
style L fill:#2d7d46,color:#fff
%% fig: Tahap compile_workflow
%% desc: Dokumen YAML melewati short-circuit autonomous, migrasi v1, validasi pydantic, lalu Kahn sampai menghasilkan levels atau WorkflowCompileError.compile_workflow menyalin dict pemanggil lebih dulu, lalu memeriksa bentuknya: argumen yang bukan mapping langsung ditolak dengan WorkflowCompileError (core/workflow_schema.py:128-1316). Dokumen mode: autonomous — atau dokumen tanpa steps — tidak dijadwalkan sama sekali; compiler mengembalikan CompiledWorkflow kosong dengan steps={} dan levels=[] (core/workflow_schema.py:132-1427).
Deteksi versi tidak bergantung pada field version. Compiler menganggap dokumen sebagai v2 hanya jika ada minimal satu step yang memuat id berupa string; selain itu langkah-langkahnya dimigrasi dulu, lalu version dipaksa menjadi 2 (core/workflow_schema.py:144-1578). Karena itu dokumen bertanda version: 2.0.0 yang masih memakai skema v1 tetap compile (core/workflow_schema.py:144-1489).
Migrasi v1 mengambil name tiap step, menyanitasinya menjadi id lewat _sanitize_id, dan merangkai depends_on ke id sebelumnya sehingga urutan linear lama terjaga (core/workflow_schema.py:220-23910, core/workflow_schema.py:242-24611). Setelah validasi pydantic, compiler menolak id duplikat, depends_on ke id yang tidak dikenal, dan dependensi ke diri sendiri (core/workflow_schema.py:164-17912).
DAG hasil kompilasi dan level paralel
_topological_levels menjalankan algoritma Kahn. Setiap elemen levels adalah satu generation: seluruh node di dalamnya sudah terpenuhi dependensinya dan saling independen, sedangkan urutan antar generation ketat (core/workflow_schema.py:249-25413). Node yang siap diurutkan secara alfabetis sebelum masuk level, jadi hasilnya deterministik (core/workflow_schema.py:262-27414). Jika jumlah node yang berhasil diurutkan tidak sama dengan jumlah step, sisa node dilaporkan sebagai siklus (core/workflow_schema.py:276-28115).
graph TD
subgraph G1["Generation 1"]
probe["probe — httpx"]
end
subgraph G2["Generation 2"]
js_crawl["js_crawl — cariddi"]
spec_fuzz["spec_fuzz — schemathesis"]
auth_check["auth_check — nuclei"]
end
subgraph G3["Generation 3"]
stateful_fuzz["stateful_fuzz — restler, bersyarat when"]
end
subgraph G4["Generation 4"]
correlate["correlate — analyst"]
end
subgraph G5["Generation 5"]
report["report"]
end
probe --> js_crawl
probe --> spec_fuzz
probe --> auth_check
spec_fuzz --> stateful_fuzz
js_crawl --> correlate
spec_fuzz --> correlate
stateful_fuzz --> correlate
auth_check --> correlate
correlate --> report
style probe fill:#2d7d46,color:#fff
style js_crawl fill:#2d7d46,color:#fff
style spec_fuzz fill:#2d7d46,color:#fff
style auth_check fill:#2d7d46,color:#fff
style stateful_fuzz fill:#f59e0b,color:#000
style correlate fill:#8b5cf6,color:#fff
style report fill:#336791,color:#fff
%% fig: DAG dan generation hasil kompilasi api_pentest_v2
%% desc: Tiga step berbagi satu pendahulu di generation 2, stateful_fuzz tertahan oleh klausa when, lalu correlate dan report menutup DAG.probe menjadi satu-satunya node berderajat nol. js_crawl, spec_fuzz, dan auth_check sama-sama hanya bergantung pada probe, jadi ketiganya berada di generation 2 (workflows/api_pentest_v2.yaml:6-4716). stateful_fuzz menambah satu tingkat karena bergantung pada spec_fuzz (workflows/api_pentest_v2.yaml:30-3817), dan correlate menunggu empat pendahulu sekaligus sebelum report menutup DAG (workflows/api_pentest_v2.yaml:49-5318).
Eksekusi per generation
sequenceDiagram
autonumber
participant CLI as cli/commands/workflow.py
participant Engine as WorkflowEngine
participant Schema as workflow_schema
participant Mem as PentestMemory
participant Disk as session JSON
CLI->>Engine: resume_session lalu run_workflow
Engine->>Schema: compile_workflow(doc)
Schema-->>Engine: CompiledWorkflow.levels
loop tiap generation
Engine->>Mem: cek completed_actions
Engine->>Schema: evaluate_when(step.when, prior)
alt when falsy
Engine->>Mem: mark_action_complete(step.id)
Engine->>Disk: _save_session
else when truthy
Engine->>Schema: render_parameters(step.parameters, prior)
Engine->>Engine: _execute_step(v1_step)
Engine->>Disk: _save_session
end
end
%% fig: Siklus satu step di dalam satu generation
%% desc: Engine memeriksa completed_actions, mengevaluasi when, merender parameter, menjalankan step, lalu menulis checkpoint atomik.run_workflow memuat dokumen, mengompilasinya, lalu membuat satu asyncio.Semaphore dari config["pentest"]["max_parallel_tools"] yang di-floor minimal 1 (core/workflow.py:97-10219). Loop berjalan sebagai for gen_idx, level in enumerate(compiled.levels); di dalam satu generation semua step dijalankan lewat asyncio.gather, sehingga paralelisme dibatasi semaphore, bukan urutan penulisan YAML (core/workflow.py:112-13220).
⚠️
settings.max_parallel_toolsdi YAML tidak dipakai untuk concurrency. Engine membacapentest.max_parallel_toolsdari config (core/workflow.py:97-10219), sementaraworkflows/web_pentest.yamlmenulismax_parallel_tools: 3di bloksettings(workflows/web_pentest.yaml:40-4321). Untuk mengubah batas paralel, ubah config.
_execute_compiled_step menjembatani DSL v2 ke eksekutor v1: step dirakit ulang menjadi dict {name, type, objective, parameters} lalu diteruskan ke _execute_step (core/workflow.py:711-72722). Hasil yang dikembalikan berbentuk id, success, command, exit_code, parsed, dan raw_output, dengan raw_output dipotong 8000 karakter (core/workflow.py:736-74323). Bentuk inilah yang menjadi konteks template untuk step berikutnya (core/workflow.py:108-11024).
success dihitung dari exit_code == 0 eksekusi tool terakhir, bukan dari isi output; bila tidak ada eksekusi baru, exit_code bernilai -1 (core/workflow.py:726-74125). Pemilihan tool dan streaming output-nya dibahas di Integrasi Tool dan Pemilihan Tool.
Template Jinja2 tersandbox
_JINJA_ENV memakai SandboxedEnvironment dengan undefined=StrictUndefined dan autoescape=False (core/workflow_schema.py:105-11026). Sandbox mematikan akses filesystem dan network; StrictUndefined membuat variabel yang salah ketik melempar error alih-alih menghasilkan string kosong (core/workflow_schema.py:105-10927). render_parameters menelusuri dict dan list secara rekursif, melewati nilai non-string, dan hanya merender string yang memuat {{ atau {% (core/workflow_schema.py:192-20228, core/workflow_schema.py:284-29629).
| Ekspresi di YAML | Konteks yang dibutuhkan | Nilai |
|---|---|---|
{{ discovery.parsed.host }} | step_results["discovery"]["parsed"]["host"] | string hasil render |
spec_fuzz.parsed.failed | int > 0 | hasil step spec_fuzz | bool untuk when |
hosts | length > 0 | variabel hosts di context | bool untuk when |
Bentuk key parsed berasal dari dict hasil step (core/workflow.py:736-74323). Rujukan ke variabel yang tidak ada menggagalkan render, dan _execute_compiled_step menangkapnya sebagai step yang di-skip, bukan crash (core/workflow.py:704-70930).
Klausa when
evaluate_when mengembalikan True bila when bernilai None, lalu merender ekspresi di dalam {{ }} pada sandbox yang sama (core/workflow_schema.py:205-21231). Hasil render dianggap falsy bila string-nya, setelah di-strip dan di-lowercase, termasuk "", "false", "none", atau "0" (core/workflow_schema.py:213-21432). Kegagalan template dipetakan ke WorkflowCompileError (core/workflow_schema.py:210-21233).
Di engine, step dengan when falsy ditandai selesai lewat mark_action_complete dan menghasilkan {"success": False, "skipped": True, "reason": "when-false"} (core/workflow.py:699-70234, core/memory.py:210-21335). Berbeda dari itu, kegagalan evaluasi mengembalikan {"success": False, "skipped": True, "error": ...} tanpa menandai selesai (core/workflow.py:692-69736).
⚠️ Hasil step yang di-skip tetap masuk
step_results, tetapi tidak punya kunciparsed. Step berikutnya yang menulis{{ <id>.parsed.<key> }}ke arah step terlewat akan gagal render karenaStrictUndefined, lalu ikut ter-skip (core/workflow.py:736-74323, core/workflow_schema.py:105-11026).
Checkpoint atomik dan --resume
_save_session menulis state ke NamedTemporaryFile di direktori output yang sama, lalu menukarnya dengan os.replace agar session.json tidak pernah setengah tertulis (core/workflow.py:846-86237). Checkpoint dipanggil setelah setiap step selesai di dalam _run_one, dan sekali lagi di blok except saat workflow gagal (core/workflow.py:126-13038, core/workflow.py:146-14939).
| Artefak | Lokasi | Isi utama |
|---|---|---|
session_<id>.json | <output.save_path>/session_<session_id>.json, default ./reports | completed_actions, findings, tool_executions, token_ledger, threat_model |
| berkas temp | .session_<id>_*.tmp di direktori yang sama | state sebelum os.replace |
Isi JSON berasal dari PentestMemory.save_state (core/memory.py:329-35240). resume_session memuat berkas itu, mengembalikan False bila berkas tidak ada atau tidak terbaca, dan menyetel current_step = len(completed_actions) (core/workflow.py:872-89241). Saat resume, _run_one melewati step yang id-nya sudah ada di completed_actions (core/workflow.py:119-12542).
⚠️ Kunci skip adalah
step.idsaja, tanpa hash parameter atau versi dokumen. Mengubahparameterssebuah step lalu menjalankannya ulang dengan--resumetetap melewati step tersebut (core/workflow.py:119-12542, core/memory.py:210-21335).
Di CLI, --resume/-r menerima session ID, dan --target boleh kosong hanya ketika --resume dipakai (cli/commands/workflow.py:46-5143, cli/commands/workflow.py:71-7844). Alurnya: engine.resume_session(resume) lebih dulu, baru run_autonomous untuk nama autonomous atau run_workflow untuk nama lain (cli/commands/workflow.py:154-16345). Detail seluruh opsi ada di Referensi Perintah CLI.
Katalog YAML di workflows/
Berkas di workflows/ memuat campuran skema. Empat contoh yang mewakili:
| Berkas | Skema | Langkah | Ciri |
|---|---|---|---|
workflows/web_pentest.yaml | v1, tanpa id | 5 | dirangkai berurutan oleh migrasi, name menjadi id |
workflows/web_pentest_with_debate.yaml | v2 | 6 | rantai linier ber-id, diakhiri agent: debate |
workflows/api_pentest_v2.yaml | v2 | 7 | punya when, lima generation |
workflows/autonomous.yaml | mode: autonomous | 0 | tanpa steps, max_steps: 25 |
workflows/web_pentest.yaml menulis name pada setiap step tanpa id, jadi seluruhnya melewati _migrate_v1_steps (workflows/web_pentest.yaml:7-3846). workflows/web_pentest_with_debate.yaml sudah memakai id dan depends_on eksplisit, dengan max_parallel_tools: 2 di settings (workflows/web_pentest_with_debate.yaml:5-4047). workflows/autonomous.yaml tidak punya steps sama sekali; ia hanya mendeklarasikan mode: autonomous, max_steps: 25, dan daftar agen, sehingga compile menghasilkan rencana kosong dan CLI mengalihkannya ke run_autonomous (workflows/autonomous.yaml:1-39, core/workflow_schema.py:132-142, cli/commands/workflow.py:160-163).
_load_workflow_doc mencari <nama>.yaml lebih dulu, lalu jatuh ke pencocokan substring pada stem berkas, dan bila tetap tidak ketemu memakai fallback tiga step: subdomain_discovery lalu port_scanning lalu analysis (core/workflow.py:643-66850).
⚠️
docs/WORKFLOW_GUIDE.mdmasih menyatakan conditional step "not natively supported" dan menyarankan memecah workflow menjadi beberapa berkas (docs/WORKFLOW_GUIDE.md:166-19651). Sejak DSL v2,whensudah menjadi field resmi diWorkflowStep(core/workflow_schema.py:5452). Panduan itu tertinggal dari kode.
Kelayakan setiap YAML di katalog dijaga oleh test parametrize yang mengompilasi seluruh berkas dan memastikan setiap step muncul di levels (tests/test_shipped_workflows.py:16-3953). Cakupan pengujiannya dibahas di Pengujian dan Evaluasi.