Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f23afb4
Add verified fix preparation engine
yoni-at-strix Sep 25, 2026
49eca20
Harden fix preparation against review findings
yoni-at-strix Sep 25, 2026
2b413da
Keep prepared candidates and staleness consistent across revisions
yoni-at-strix Sep 25, 2026
be1c2e4
Withhold automatic fixes when repairs exceed the recorded draft
yoni-at-strix Sep 25, 2026
9d525ad
Add bounded fix verification feedback loop
yoni-at-strix Sep 25, 2026
d02b74c
Preserve explicit blocked repair outcomes
yoni-at-strix Sep 25, 2026
a41000d
Retry unchanged repairs when verification changes
yoni-at-strix Sep 25, 2026
c510f58
fix: retry transient verifier inconclusive results
yoni-at-strix Sep 28, 2026
df313c1
refactor fix preparation gates
yoni-at-strix Sep 28, 2026
00fcb4c
fix: classify distinct check failures correctly
yoni-at-strix Sep 28, 2026
f213a7d
Require functional fix evidence and preserve partial preparation work
Sep 29, 2026
198a254
Make preparation history factory explicit for strict type checking
Sep 29, 2026
791ef91
fix: let independent evidence resolve repair timeout
yoni-at-strix Sep 29, 2026
85b3030
Preserve partial fixes and require consistent execution evidence
Sep 29, 2026
f03fd72
Simplify fix preparation around native tests and independent review
Sep 29, 2026
0faa7b7
Make fix handoffs actionable and require customer unit tests
Sep 29, 2026
43391eb
Let repair and review agents own the fix workflow
Sep 29, 2026
d184142
Let fix reviewer own validation and completion
Sep 29, 2026
348fbf2
Support native fix-agent assignments and final reviewed patches
Sep 30, 2026
5badb2d
Remove retired fix execution and proof machinery
Sep 30, 2026
d35197b
Move complete fix workflow into OSS and add strix fix CLI
Sep 30, 2026
acf262f
Keep fix outputs private and outside the source checkout
Sep 30, 2026
1c1a899
Focus fix agents and preserve completion evidence
Sep 30, 2026
77bd5da
Require an explicit fix handoff for source-backed findings
yoni-at-strix Sep 30, 2026
868ba53
Scope fix validation and warn on repeated commands
yoni-at-strix Sep 30, 2026
e4f1fe6
Merge remote-tracking branch 'origin/main' into devin/1790308365-veri…
yoni-at-strix Sep 30, 2026
ba6bbaf
Include repair follow-ups in the readable review
yoni-at-strix Sep 30, 2026
1789400
Keep an approved fix when only the PR text changes
yoni-at-strix Sep 30, 2026
8317665
Run confirmed finding fixes as native agents in the scan sandbox
yoni-at-strix Sep 30, 2026
b71ed13
Use native child delegation for finding fixes and strengthen completi…
yoni-at-strix Sep 30, 2026
1f8295c
Launch native fixes after persistence and bound completion failures
yoni-at-strix Sep 30, 2026
f75fb5f
fix: harden fix dispatch and verification (STR-815)
yoni-at-strix Oct 1, 2026
bba4aa2
fix: require reviewed current patches and enforce fix network isolation
Oct 1, 2026
60d4ce1
feat: allow scans to skip automatic fixes
yoni-at-strix Oct 1, 2026
3763a67
fix: finalize cancelled fix agents
yoni-at-strix Oct 1, 2026
1fa7d21
feat: publish verified fixes as local branches
yoni-at-strix Oct 1, 2026
f144685
fix: complete interactive autofix scans before cleanup
yoni-at-strix Oct 1, 2026
098fa36
fix: guard resumed assessments and ignore withdrawn fix records
yoni-at-strix Oct 1, 2026
9edb2ae
fix: disable automatic fix agents for PR review scans
yoni-at-strix Oct 1, 2026
8bd7f4c
refactor: use one auto-fix setting and infer fix delivery
yoni-at-strix Oct 2, 2026
818d583
Accept stray characters in validation status and hide auto-fix guidan…
yoni-at-strix Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions strix/agents/prompts/system_prompt.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ WHITE-BOX TESTING (code provided):
- Local execution, unit/integration testing, patch verification, and HTTP requests against locally started in-scope services are normal authorized white-box validation
- If dynamically running the code proves impossible after exhaustive attempts, pivot to comprehensive static analysis.
- Try to infer how to run the code based on its structure and content.
- Derive the code fix as PART OF reporting, not as a separate later pass: create_vulnerability_report already requires the concrete patch inline (`code_locations` with verbatim `fix_before`/`fix_after` and `fix_pr_body`), so the reporting agent that analyzes the root cause is the one that produces the fix. Do NOT spawn a downstream agent afterwards to re-derive/re-apply the same patch.
- If you also apply and verify the patch in the repo (edit the file, re-test that the vulnerability is gone), do it in the same agent/turn while the analysis is fresh — right before or as part of filing the report — never as a second re-analysis pass.
- Draft the initial fix candidate when you file the report. Use `code_locations` with verbatim `fix_before` and `fix_after`, plus `fix_pr_body`.
- Treat the inline changes as a candidate, not as a completed fix. A later preparation stage can inspect and modify any required repository file.
- Record checks you ran in `fix_verification`. Do not describe reasoned checks as executed checks.

COMBINED MODE (code + deployed target present):
- Treat this as static analysis plus dynamic testing simultaneously
Expand Down Expand Up @@ -240,7 +241,9 @@ VALIDATION REQUIREMENTS:
- THREAT MODEL: before you start testing, call `get_threat_model` on the target you were pointed at — it is the scan's shared answer to who the attacker is, where the trust boundaries sit, and what counts as critical here. It is scoped to this scan and nothing carries over from an earlier run, so `found: false` means no agent on this run has derived one yet. Read it instead of re-deriving trust boundaries yourself; where your testing disproves it — a boundary it calls trusted turns out to be attacker-reachable, a role it did not know about, a host or endpoint it never listed — record that with `amend_threat_model` so the agents after you inherit the correction. Amending is not optional politeness: a model nobody corrects turns the first agent's guesses into everyone's assumptions.
- Before filing any report, run the counterevidence pass: argue the strongest case AGAINST the finding, record what you found in the `counterevidence` field, set `confidence` honestly (a static-only trace you couldn't execute is at best `medium`), and state what evidence would change the severity. See the counterevidence and severity-calibration knowledge above.
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
- Reporting and fixing are ONE step, not two: when source is available, the reporting agent derives the concrete fix and files it INLINE via create_vulnerability_report (`code_locations` with `fix_before`/`fix_after` + `fix_pr_body`) — the report is not complete without it. Do NOT report first and then spawn a separate downstream agent to re-derive and re-apply the same patch; that just re-does the analysis and wastes tokens. (Do not silently patch a finding WITHOUT filing a report — the report, with its embedded fix, is the deliverable.)
- When source is available, the reporting agent files an initial fix candidate with the report. The candidate uses `code_locations` with `fix_before` and `fix_after`, plus `fix_pr_body`.
- Do not treat the candidate as a prepared fix. The fix preparation stage applies, repairs, tests, and independently verifies it after finding discovery closes.
- Do not silently patch a finding without filing a report.
- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent. If your evidence proves more than the finding it matched (a working exploit where that one had only a static trace, a chain that raises the impact), revise that finding with update_vulnerability_report using the duplicate_of id — never re-file it.
- HTTP EVIDENCE: a finding you validated through the proxy is not fully filed until `http_exchange_ids` carries the proxy request ids of the exchanges that prove it — the request that triggers the vulnerability plus the baseline/control request it differs from (an unauthenticated success next to the authenticated one, the payload response next to the benign one). Copy the ids exactly as `list_requests`/`view_request` show them, never invent or guess one, and never omit the field to bypass validation. Leave it out only when there is no captured HTTP exchange at all (static-only code findings, dependency CVEs). If you filed before the proving exchanges existed, attach them afterwards with update_vulnerability_report. Without the ids, the finding ships as prose nobody can replay.
- REVISING A FINDING: use update_vulnerability_report (report id + the fields you want to replace + update_reason) when you learn something a finding already on file does not carry — you built the PoC after filing it, a chain raised its impact, further testing weakened it, or its counterevidence/remediation/code locations were wrong. Editing a finding needs no duplicate verdict, and it is always better than filing a second report for the same issue. Read the finding first with get_report, and pass only the fields that change.
Expand Down Expand Up @@ -360,7 +363,7 @@ ROOT AGENT ROLE:

1. **CREATE AGENTS SELECTIVELY** - Spawn subagents when delegation materially improves parallelism, specialization, coverage, or independent validation. Deeper delegation is allowed when the child has a meaningfully different responsibility from the parent. Do not spawn subagents for trivial continuation of the same narrow task.
2. **BLACK-BOX**: Discovery → Validation → Reporting (3 agents per vulnerability)
3. **WHITE-BOX**: Discovery → Validation → Reporting-with-fix (3 agents per vulnerability — the reporting agent derives and files the fix inline; do NOT add a separate fixing agent that re-derives the same patch)
3. **WHITE-BOX**: Discovery → Validation → Reporting with an initial fix candidate. The later preparation phase repairs and verifies the candidate.
4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain
5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces
6. **ONE JOB PER AGENT** - Each agent has ONE specific task only
Expand All @@ -379,7 +382,7 @@ BLACK-BOX (domain/URL only):
WHITE-BOX (source code provided):
- Found authentication code issues? → Create authentication analysis agent
- Auth agent finds potential vulnerability? → Create "Auth Validation Agent"
- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" that files the report AND its inline fix (`code_locations` + `fix_pr_body`) in one shot — no separate fixing agent
- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" that files the report and an initial fix candidate (`code_locations` + `fix_pr_body`)

VULNERABILITY WORKFLOW (MANDATORY FOR EVERY FINDING):

Expand All @@ -401,10 +404,10 @@ Authentication Code Agent finds weak password validation
Spawns "Auth Validation Agent" (proves it's exploitable)
↓
If valid → Spawns "Auth Reporting Agent" (creates the vulnerability report
WITH the fix inline: code_locations fix_before/fix_after + fix_pr_body,
applying/verifying the patch in the same turn if desired)
with the initial fix candidate: code_locations fix_before/fix_after
+ fix_pr_body)
↓
STOP - no separate fixing agent; the fix was derived once, at report time
STOP - the scan preparation phase handles repository-wide repair and verification
```

CRITICAL RULES:
Expand Down Expand Up @@ -440,7 +443,7 @@ FOCUS PRINCIPLES:
REALISTIC TESTING OUTCOMES:
- **No Findings**: Agent completes testing but finds no vulnerabilities
- **Validation Failed**: Initial finding was false positive, validation agent confirms it's not exploitable
- **Valid Vulnerability**: Validation succeeds, spawns a reporting agent that files the report with the fix inline (white-box) — no separate fixing agent
- **Valid Vulnerability**: Validation succeeds. The reporting agent files the report and an initial fix candidate for white-box findings.

PERSISTENCE IS MANDATORY:
- Real vulnerabilities take TIME - expect to need 2000+ steps minimum
Expand Down
52 changes: 52 additions & 0 deletions strix/fix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Verified fix preparation contracts and runtime."""

from strix.fix.contracts import (
CandidateLocation,
CheckResult,
CheckStatus,
CommandSpec,
FileManifestEntry,
FixCandidateV1,
FixEdit,
FixPreparationRequestV1,
FixPreparationResultV1,
PreparationState,
ReproductionSpec,
SourceIdentity,
SourceIdentityKind,
VerificationDecision,
VerifierResult,
candidate_from_legacy_report,
)
from strix.fix.prepare import (
PreparationCancelledError,
PreparationContext,
PreparationPolicy,
build_git_manifest,
prepare_fix,
)


__all__ = [
"CandidateLocation",
"CheckResult",
"CheckStatus",
"CommandSpec",
"FileManifestEntry",
"FixCandidateV1",
"FixEdit",
"FixPreparationRequestV1",
"FixPreparationResultV1",
"PreparationCancelledError",
"PreparationContext",
"PreparationPolicy",
"PreparationState",
"ReproductionSpec",
"SourceIdentity",
"SourceIdentityKind",
"VerificationDecision",
"VerifierResult",
"build_git_manifest",
"candidate_from_legacy_report",
"prepare_fix",
]
279 changes: 279 additions & 0 deletions strix/fix/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
"""Versioned contracts for verified fix preparation."""

from __future__ import annotations

import hashlib
import json
from enum import StrEnum
from pathlib import PurePosixPath
from typing import TYPE_CHECKING, Literal, cast

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator


if TYPE_CHECKING:
from collections.abc import Mapping


class ContractModel(BaseModel):
model_config = ConfigDict(extra="forbid")


class SourceIdentityKind(StrEnum):
COMMIT = "commit"
ARCHIVE = "archive"


class PreparationState(StrEnum):
PREPARING = "preparing"
READY = "ready"
READY_WITH_GAPS = "ready_with_gaps"
NEEDS_REVIEW = "needs_review"
BLOCKED = "blocked"
FAILED = "failed"
STALE = "stale"


class CheckStatus(StrEnum):
PASSED = "passed"
FAILED = "failed"
UNAVAILABLE = "unavailable"
CANCELLED = "cancelled"


class VerificationDecision(StrEnum):
VERIFIED = "verified"
REJECTED = "rejected"
INCONCLUSIVE = "inconclusive"


class SourceIdentity(ContractModel):
kind: SourceIdentityKind
value: str = Field(min_length=1)
repository: str | None = None

@field_validator("value")
@classmethod
def validate_value(cls, value: str) -> str:
normalized = value.strip().lower()
if not normalized or any(char not in "0123456789abcdef" for char in normalized):
raise ValueError("source identity must be a hexadecimal digest")
return normalized

@model_validator(mode="after")
def validate_digest_length(self) -> SourceIdentity:
valid_lengths = {40, 64} if self.kind is SourceIdentityKind.COMMIT else {64}
if len(self.value) not in valid_lengths:
expected = "40 or 64" if self.kind is SourceIdentityKind.COMMIT else "64"
raise ValueError(f"{self.kind} identity must contain {expected} hexadecimal characters")
return self


def _validate_relative_path(value: str) -> str:
path = value.strip()
parsed = PurePosixPath(path)
if not path or parsed.is_absolute() or ".." in parsed.parts or path.startswith("./"):
raise ValueError("path must be relative to the repository root")
return path


class CandidateLocation(ContractModel):
file: str
start_line: int = Field(ge=1)
end_line: int = Field(ge=1)
snippet: str | None = None
label: str | None = None

_relative_file = field_validator("file")(_validate_relative_path)

@model_validator(mode="after")
def validate_range(self) -> CandidateLocation:
if self.end_line < self.start_line:
raise ValueError("end_line must be greater than or equal to start_line")
return self


class FixEdit(CandidateLocation):
before: str = Field(min_length=1)
after: str
original_sha256: str | None = None


class CommandSpec(ContractModel):
name: str = Field(min_length=1, max_length=120)
argv: list[str] = Field(min_length=1)
required: bool = True
timeout_seconds: int = Field(default=300, ge=1, le=3600)
cwd: str = "."

_relative_cwd = field_validator("cwd")(_validate_relative_path)

@field_validator("argv")
@classmethod
def validate_argv(cls, value: list[str]) -> list[str]:
if any(not argument or "\x00" in argument for argument in value):
raise ValueError("command arguments must be non-empty and cannot contain NUL")
return value


class ReproductionSpec(ContractModel):
instructions: str = Field(min_length=1)
command: CommandSpec | None = None


class ReportedCheck(ContractModel):
name: str
result: str
executed: bool = False


class FixCandidateV1(ContractModel):
version: Literal["1"] = "1"
source_identity: SourceIdentity | None = None
security_invariant: str = Field(min_length=1)
finding_locations: list[CandidateLocation] = []
draft_edits: list[FixEdit] = []
reproduction: ReproductionSpec | None = None
reported_checks: list[ReportedCheck] = []
known_gaps: list[str] = []

def digest(self) -> str:
payload = json.dumps(
self.model_dump(mode="json"),
sort_keys=True,
separators=(",", ":"),
).encode()
return hashlib.sha256(payload).hexdigest()


class FixPreparationRequestV1(ContractModel):
version: Literal["1"] = "1"
organization_id: str | None = None
scan_id: str
finding_id: str
repository_id: str | None = None
candidate: FixCandidateV1
checks: list[CommandSpec] = []
max_repair_attempts: int = Field(default=2, ge=1, le=5)
timeout_seconds: int = Field(default=1800, ge=30, le=14400)
network_allowed: bool = False
credentials_allowed: list[str] = []
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved


class CheckResult(ContractModel):
name: str
argv: list[str]
status: CheckStatus
exit_code: int | None = None
duration_seconds: float = Field(ge=0)
output: str = ""
required: bool = True


class VerifierResult(ContractModel):
decision: VerificationDecision
summary: str
security_invariant_closed: bool = False
reproduction_executed: bool = False
reproduction_summary: str | None = None
sibling_paths_reviewed: list[str] = []
preserved_behaviors: list[str] = []
gaps: list[str] = []


class FileManifestEntry(ContractModel):
path: str
operation: Literal["add", "modify", "delete"]
original_sha256: str | None = None
resulting_sha256: str | None = None
artifact_ref: str | None = None

_relative_path = field_validator("path")(_validate_relative_path)


class FixPreparationResultV1(ContractModel):
version: Literal["1"] = "1"
state: PreparationState
stop_reason: str
source_identity: SourceIdentity | None
candidate: FixCandidateV1
candidate_digest: str
final_file_manifest: list[FileManifestEntry] = []
artifact_ref: str | None = None
changed_files: list[str] = []
diff_summary: str = ""
checks: list[CheckResult] = []
security_reproduction: CheckResult | None = None
verifier: VerifierResult | None = None
gaps: list[str] = []
attempts: int = Field(default=0, ge=0)
elapsed_seconds: float = Field(default=0, ge=0)
cost_usd: float | None = Field(default=None, ge=0)


def candidate_from_legacy_report(
report: Mapping[str, object],
*,
source_identity: SourceIdentity | None = None,
) -> FixCandidateV1 | None:
raw_locations = report.get("code_locations")
if not isinstance(raw_locations, list):
return None
location_values = cast("list[object]", raw_locations)

locations: list[CandidateLocation] = []
edits: list[FixEdit] = []
for raw_value in location_values:
if not isinstance(raw_value, dict):
continue
raw = cast("dict[str, object]", raw_value)
file_path = raw.get("file")
start_line = raw.get("start_line")
end_line = raw.get("end_line")
if not isinstance(file_path, str) or type(start_line) is not int:
continue
if type(end_line) is not int:
end_line = start_line
common = {
"file": file_path,
"start_line": start_line,
"end_line": end_line,
"snippet": raw.get("snippet") if isinstance(raw.get("snippet"), str) else None,
"label": raw.get("label") if isinstance(raw.get("label"), str) else None,
}
try:
locations.append(CandidateLocation.model_validate(common))
before = raw.get("fix_before")
after = raw.get("fix_after")
if isinstance(before, str) and isinstance(after, str):
edits.append(FixEdit.model_validate({**common, "before": before, "after": after}))
except ValueError:
continue

if not locations:
return None

invariant = str(
report.get("remediation_steps") or report.get("technical_analysis") or ""
).strip()
if not invariant:
invariant = "Resolve the reported security finding without changing legitimate behavior."

reported_checks: list[ReportedCheck] = []
verification = str(report.get("fix_verification") or "").strip()
if verification:
reported_checks.append(
ReportedCheck(name="reporting-agent verification", result=verification, executed=False)
)

return FixCandidateV1(
source_identity=source_identity,
security_invariant=invariant,
finding_locations=locations,
draft_edits=edits,
reproduction=ReproductionSpec(
instructions=str(report.get("poc_description") or report.get("evidence") or invariant)
),
reported_checks=reported_checks,
known_gaps=["The reporting-agent verification is not independent."],
)
Loading