Skip to content

Prompt System

Each stage owns its prompts locally — there is no central PromptBuilder or shared prompt registry. Prompt templates are .txt files living alongside each stage's stage.py, loaded at runtime from the package data.

Prompt ownership

Stage Prompt directory Template files
Plan stages/plan/prompts/ default.txt
Threat model (codex direct) stages/threat_model/prompts/ codex_direct.txt
Threat model (bug class) stages/threat_model/prompts/ bug_class_selection.txt
Threat model (default) stages/threat_model/prompts/ default.txt
Audit (default) stages/audit/prompts/ default.txt
Audit (variant analysis) stages/audit/prompts/ variant_analysis.txt
Verify stages/verify/prompts/ default.txt, threat_model_simplify.txt
Patch — generate stages/patch/prompts/ generate.txt
Patch — minimality review stages/patch/prompts/ minimality_review.txt
Patch — vulnerability recheck stages/patch/prompts/ vulnerability_recheck.txt
Patch — fallback stages/patch/prompts/ default.txt
Plugin (Linux) plugins/builtin/linux/prompts/ plugin-specific
Plugin (Web application) plugins/builtin/web_application/ audit_prompt.txt

Plugin prompt injection

Plugins customize prompts through the build_prompt_sections hook rather than modifying stage templates directly:

class StagePlugin(Protocol):
    def build_prompt_sections(
        self,
        context: RunContext,
        stage: StageName,
        input: StageInput,
    ) -> tuple[PromptSection, ...]: ...
@dataclass(frozen=True)
class PromptSection:
    title: str
    content: str
    metadata: Mapping[str, Any] = field(default_factory=dict)

BaseStage calls this hook before execute(...) and injects the returned sections into the prompt before the agent call. The concrete stage template includes placeholder markers where plugin sections are inserted.

Example: Linux plugin sections

LinuxPlugin.build_prompt_sections(...) for StageName.AUDIT returns sections that include:

  • Linux-specific vulnerability classes to focus on (kernel memory safety, privilege escalation paths, syscall surface issues)
  • Reproduction guidance for kernel code
  • Audit scope constraints

Example: Web application plugin sections

WebApplicationPlugin.build_prompt_sections(...) for StageName.AUDIT returns sections loaded from audit_prompt.txt covering OWASP Top 10 patterns, session/auth boundaries, injection sinks, and serialization risks.

Schema enum overrides

Plugins can also narrow or extend the allowed values for structured enum fields in stage output schemas:

class StagePlugin(Protocol):
    def output_schema_overrides(
        self,
        context: RunContext,
        stage: StageName,
    ) -> tuple[SchemaEnumOverride, ...]: ...
@dataclass(frozen=True)
class SchemaEnumOverride:
    stage: StageName
    field: str                    # extension point field name
    values: tuple[str, ...]
    mode: SchemaOverrideMode = "replace"   # "replace" | "extend"
    prompt_hint: str | None = None         # appended to prompt if set

BaseStage calls apply_schema_enum_overrides(base_schema, ...) from stages/schema_overrides.py to produce the effective output schema for the agent call. Any prompt_hint strings are injected into the prompt alongside plugin sections.

Audit extension points

AuditStage exposes these schema extension points:

Field Default values Allowed modes
bug_class General security bug classes replace, extend
attacker_profile Generic attacker profiles replace, extend
required_state Generic state requirements replace, extend
required_services Generic service requirements replace, extend

LinuxPlugin overrides

Replaces bug_class with kernel-specific values: use_after_free, double_free, race_condition, integer_overflow, privilege_escalation, oob_read, oob_write, etc.

WebApplicationPlugin overrides

Replaces bug_class with web-specific values: sql_injection, xss, csrf, ssrf, broken_auth, insecure_deserialization, etc. Also overrides attacker_profile and required_services with web-relevant values.

Static threat model (plugin shortcut)

Rather than injecting a prompt, a plugin can bypass the threat-model agent call entirely:

class StagePlugin(Protocol):
    def build_static_threat_model(
        self,
        context: RunContext,
        input: StageInput,
        source_snapshot: Mapping[str, Any],
        plan: Mapping[str, Any],
    ) -> Mapping[str, Any] | None: ...

If this returns a non-None mapping, BaseThreatModelStage.execute(...) writes it directly as vega.threat_model without running an agent. DefaultPlugin and LinuxPlugin return None; WebApplicationPlugin returns a pre-built static threat model for web targets.

Prompt construction flow

1. BaseStage.run() calls plugin.before_stage(context, stage, input)
   → plugin may augment input (e.g. add data fields)

2. BaseStage calls plugin.output_schema_overrides(context, stage)
   → apply_schema_enum_overrides() produces effective schema

3. Stage.execute() builds prompt:
   a. Load stage template text
   b. Interpolate source/plan/artifact context
   c. Append plugin.build_prompt_sections() results
   d. Append schema override prompt_hints
   e. Append PR review focus block if metadata["pr_review"] present

4. AgentRunner.run(AgentTask(prompt=..., output_schema=effective_schema)) called

Agent task schema validation

Each stage passes its effective output schema to AgentRunner as AgentTask.output_schema. The Codex adapter passes this as a --json-schema flag to the Codex CLI, which enforces structured JSON output. The resulting agent output is re-validated against the same schema before further parsing.