Documentation
¶
Overview ¶
Package reasoners ports src/sec_af/reasoners/{__init__,recon,hunt,prove}.py — the thin adapter layer between the reasoner surface the pipeline `.call`s and the agent functions under internal/agents.
Python builds that surface with a module-level router:
router = AgentRouter(tags=["security", "audit", "red-team"]) # __init__.py
@router.reasoner()
async def run_architecture_mapper(repo_path: str) -> dict[str, Any]:
router.note("Architecture mapper starting", tags=["recon", "architecture"])
result = await _run_architecture_mapper(router, repo_path)
return result.model_dump()
Every adapter does the same four things, and this package reproduces them one for one:
- emit the reasoner's "X starting" note with its exact tags (a few carry none — see NameRunLogicBugsHunter and NameRunCWEExpansion);
- materialize the untyped request body into the pydantic models the agent function expects (afx.Bind over a typed input struct whose json tags and defaults are the Python signature, plus the required-field checks pydantic performs — validate.go);
- call the agent function;
- return `result.model_dump()` as a map (afx.ToMap).
Input validation ¶
Step 2 is not the first thing a request meets. The Python SDK runs `Agent._validate_handler_input` over the body BEFORE the decorated function is entered, answering 422 for a null on a required parameter and COERCING scalars (`"50"` -> 50, `5` -> "5", `"yes"` -> true). handler_input.go ports that layer and RegisterAll wraps every handler in it; internal/secaf/audit runs the same layer over the audit entry's own request. A refusal is an *InputError carrying 422.
Registration ¶
RegisterAll puts the 33 router reasoners into a Registry in the canonical DESIGN.md §3 order and returns its own ordered bookkeeping, which the parity test compares with the registry's. The sec-af node mounted the same handlers on an SDK router served to the AgentField control plane; inside codeaf the registry is read by the in-process Caller (internal/secaf/audit), and the router's tags, which only labelled the node for the control plane, are gone.
Input schemas ¶
Python publishes a JSON Schema per reasoner, derived from the decorated function's signature. The exact schemas the Python node registered are committed as testdata/python_input_schemas.json and replayed verbatim through InputSchema — including Python's own derivation quirks — and the registry holds each reasoner's schema beside its handler. input_schemas.go documents them and how the capture was made.
A SECOND capture, testdata/python_input_types.json, holds the raw `(annotation, default)` pairs the same signatures produce — the input `_validate_handler_input` runs on. The two are derived by different SDK code paths from the same source, and handler_input_test.go asserts they agree.
What is NOT here ¶
The `*_phase` bodies (the four reasoners that were the control-plane DAG) live in internal/phases; this package only binds their inputs and threads the node id. `run_cwe_expansion` likewise delegates to phases.RunCWEExpansion. Keeping the split means a phase can be tested without the registration layer and vice versa.
Index ¶
- Constants
- Variables
- func CoerceVerifierFinding(finding map[string]any) (schemas.RawFinding, error)
- func HandlerInputStatusError(err error) error
- func HuntPhase(ctx context.Context, app appx.App, nodeID string, in HuntPhaseInput) (map[string]any, error)
- func InputSchema(name string) json.RawMessage
- func InputSchemaNames() []string
- func ProvePhase(ctx context.Context, app appx.App, nodeID string, in ProvePhaseInput) (map[string]any, error)
- func ReconPhase(ctx context.Context, app appx.App, nodeID string, in ReconPhaseInput) (map[string]any, error)
- func RegisterAll(reg *Registry, app appx.App) []string
- func RemediationPhase(ctx context.Context, app appx.App, nodeID string, in RemediationPhaseInput) (map[string]any, error)
- func RunAPISecurityHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunArchitectureMapper(ctx context.Context, app appx.App, in RepoPathInput) (map[string]any, error)
- func RunAuthHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunBusinessLogicHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunCWEExpansion(ctx context.Context, app appx.App, in CWEExpansionInput) (map[string]any, error)
- func RunConfigScanner(ctx context.Context, app appx.App, in RepoPathInput) (map[string]any, error)
- func RunConfigSecretsHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunCrossServiceAnalyzer(ctx context.Context, app appx.App, in CrossServiceInput) (map[string]any, error)
- func RunCryptoHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunDastVerifier(ctx context.Context, app appx.App, in DastVerifierInput) (map[string]any, error)
- func RunDataExposureHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunDataFlowMapper(ctx context.Context, app appx.App, in ArchitectureInput) (map[string]any, error)
- func RunDeduplicator(ctx context.Context, app appx.App, in DeduplicatorInput) (map[string]any, error)
- func RunDepReachability(ctx context.Context, app appx.App, in FindingDepthInput) (map[string]any, error)
- func RunDependencyAuditor(ctx context.Context, app appx.App, in RepoPathInput) (map[string]any, error)
- func RunDosHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunExploitHypothesizer(ctx context.Context, app appx.App, in ExploitInput) (map[string]any, error)
- func RunInjectionHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunLogicBugsHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunRemediation(ctx context.Context, app appx.App, in RemediationInput) (map[string]any, error)
- func RunRemediationAgent(ctx context.Context, app appx.App, in RemediationAgentInput) (map[string]any, error)
- func RunSSRFHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunSanitizationAnalyzer(ctx context.Context, app appx.App, in SanitizationInput) (map[string]any, error)
- func RunSecurityContextProfiler(ctx context.Context, app appx.App, in ArchitectureInput) (map[string]any, error)
- func RunSupplyChainHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func RunTracer(ctx context.Context, app appx.App, in FindingDepthInput) (map[string]any, error)
- func RunVerdictAgent(ctx context.Context, app appx.App, in VerdictInput) (map[string]any, error)
- func RunVerifier(ctx context.Context, app appx.App, in FindingDepthInput) (map[string]any, error)
- func RunXSSHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
- func ValidateHandlerInput(name string, data map[string]any) (map[string]any, error)
- type ArchitectureInput
- type CWEExpansionInput
- type CrossServiceInput
- type DastVerifierInput
- type DeduplicatorInput
- type ExploitInput
- type FindingDepthInput
- type Handler
- type HandlerInputError
- type HuntPhaseInput
- type HunterInput
- type InputError
- type ProvePhaseInput
- type ReconPhaseInput
- type Registry
- type RemediationAgentInput
- type RemediationInput
- type RemediationPhaseInput
- type RepoPathInput
- type SanitizationInput
- type VerdictInput
Constants ¶
const ( NameRunArchitectureMapper = "run_architecture_mapper" NameRunDependencyAuditor = "run_dependency_auditor" NameRunConfigScanner = "run_config_scanner" NameRunDataFlowMapper = "run_data_flow_mapper" NameRunSecurityContextProfiler = "run_security_context_profiler" NameRunInjectionHunter = "run_injection_hunter" NameRunDosHunter = "run_dos_hunter" NameRunSSRFHunter = "run_ssrf_hunter" NameRunAuthHunter = "run_auth_hunter" NameRunXSSHunter = "run_xss_hunter" NameRunCryptoHunter = "run_crypto_hunter" NameRunBusinessLogicHunter = "run_business_logic_hunter" // NameRunLogicBugsHunter is an ALIAS reasoner: its Python body forwards to // run_business_logic_hunter (the reasoner, not the agent function), so it // emits no note of its own and its DAG child is indistinguishable from a // direct run_business_logic_hunter call. NameRunLogicBugsHunter = "run_logic_bugs_hunter" NameRunDataExposureHunter = "run_data_exposure_hunter" NameRunSupplyChainHunter = "run_supply_chain_hunter" NameRunConfigSecretsHunter = "run_config_secrets_hunter" NameRunAPISecurityHunter = "run_api_security_hunter" NameRunDeduplicator = "run_deduplicator" NameRunDepReachability = "run_dep_reachability" NameRunVerifier = "run_verifier" NameRunTracer = "run_tracer" NameRunSanitizationAnalyzer = "run_sanitization_analyzer" NameRunExploitHypothesizer = "run_exploit_hypothesizer" NameRunVerdictAgent = "run_verdict_agent" NameRunRemediation = "run_remediation" NameRunRemediationAgent = "run_remediation_agent" NameRunDastVerifier = "run_dast_verifier" NameRunCrossServiceAnalyzer = "run_cross_service_analyzer" // NameRunCWEExpansion is REGISTERED but never `.call`ed: hunt_phase invokes // expand_cwes_for_hunt in process (an `.ai()` call), so this reasoner // contributes no DAG node. See DESIGN.md §3. NameRunCWEExpansion = "run_cwe_expansion" NameReconPhase = "recon_phase" NameHuntPhase = "hunt_phase" NameProvePhase = "prove_phase" NameRemediationPhase = "remediation_phase" )
The 33 router reasoner names, in DESIGN.md §3 order. The order is the Python import order of reasoners/__init__.py (recon -> hunt -> prove -> phases) and, within each module, the order the `@router.reasoner()` decorators appear.
const DefaultMaxFilesWithoutSignal = 30
DefaultMaxFilesWithoutSignal ports the `max_files_without_signal: int = 30` default every hunter reasoner declares.
const NameAudit = "audit"
NameAudit is the ONE externally driven reasoner. Python registered it with `@app.reasoner()` on the Agent, not on the AgentRouter, so it is not part of the registry; internal/secaf/audit owns the entry, and the name is still how its input specification is looked up (ValidateHandlerInput).
Variables ¶
var Names = []string{ NameRunArchitectureMapper, NameRunDependencyAuditor, NameRunConfigScanner, NameRunDataFlowMapper, NameRunSecurityContextProfiler, NameRunInjectionHunter, NameRunDosHunter, NameRunSSRFHunter, NameRunAuthHunter, NameRunXSSHunter, NameRunCryptoHunter, NameRunBusinessLogicHunter, NameRunLogicBugsHunter, NameRunDataExposureHunter, NameRunSupplyChainHunter, NameRunConfigSecretsHunter, NameRunAPISecurityHunter, NameRunDeduplicator, NameRunDepReachability, NameRunVerifier, NameRunTracer, NameRunSanitizationAnalyzer, NameRunExploitHypothesizer, NameRunVerdictAgent, NameRunRemediation, NameRunRemediationAgent, NameRunDastVerifier, NameRunCrossServiceAnalyzer, NameRunCWEExpansion, NameReconPhase, NameHuntPhase, NameProvePhase, NameRemediationPhase, }
Names is the canonical ordered list of the 33 router reasoner names — DESIGN.md §3's list verbatim. RegisterAll registers exactly these, in exactly this order, and returns the same slice content as its bookkeeping.
Functions ¶
func CoerceVerifierFinding ¶
func CoerceVerifierFinding(finding map[string]any) (schemas.RawFinding, error)
CoerceVerifierFinding ports `_coerce_verifier_finding(finding)` (reasoners/prove.py:28):
try:
return RawFinding.model_validate(finding)
except Exception:
view = FindingForVerifier.model_validate(finding)
return RawFinding(
id=view.id,
hunter_strategy="phase_boundary_projection",
title=view.title,
description=view.data_flow_summary or view.title,
finding_type=FindingType.SAST,
cwe_id=view.cwe_id,
cwe_name=view.cwe_id,
file_path=view.file_path,
start_line=view.start_line,
end_line=view.end_line,
function_name=view.function_name,
code_snippet=view.code_snippet,
estimated_severity=apply_cwe_severity_floor(view.cwe_id, Severity.MEDIUM),
confidence=Confidence.MEDIUM,
fingerprint=view.id,
)
This is the reasoner boundary that makes prove_phase work: prove_phase sends `finding.for_verifier().model_dump()`, a ten-field FindingForVerifier projection, which cannot satisfy RawFinding's twelve required fields — so the fallback branch is the LIVE path, not an edge case. See validate.go for why the required-field check has to be real.
Python parity in the fallback:
- `cwe_name` is set to the CWE **id**, not a name. Reproduced.
- `description` uses `view.data_flow_summary or view.title` — Python truthiness, so an empty summary falls back to the title.
- the reconstructed RawFinding's `fingerprint` is the view's id, and its `id` is the view's id too, so the two match (a fresh RawFinding would have minted two different uuid4s).
- `related_files` is not passed and keeps `default_factory=list` -> []; `data_flow` keeps None. NewRawFinding() seeds exactly that (and then the explicit ID/Fingerprint assignments overwrite its minted uuid4s).
- the `except Exception` is BROAD: any failure to build a RawFinding — not just a missing field — takes the fallback. A payload that is neither model still fails, on the FindingForVerifier validation.
func HandlerInputStatusError ¶
HandlerInputStatusError maps a *HandlerInputError onto the *InputError that carries 422, the status Python's endpoint returned.
func HuntPhase ¶
func HuntPhase(ctx context.Context, app appx.App, nodeID string, in HuntPhaseInput) (map[string]any, error)
HuntPhase ports the `hunt_phase` reasoner (reasoners/phases.py:239).
Python parity: `ai_gate` is always None on the live path (app.py's `.call` omits it), so phases.NewJSONAIGate yields nil there. A direct caller that supplies one takes the `ai_gate is not None` branch and gets the "AI gate failed: ..." note — see HuntPhaseInput.
func InputSchema ¶
func InputSchema(name string) json.RawMessage
InputSchema returns the input schema the Python node publishes for the reasoner called name, as the registry stores it beside the handler.
It PANICS when the fixture has no entry for name. That is the point: every registration goes through here, so adding a reasoner to the Go port without regenerating the capture (or renaming one out of sync with Python) crashes at registration — loudly, at boot and in every test — instead of silently publishing the SDK's `{"type":"object","additionalProperties":true}` placeholder and letting the two nodes drift apart in discovery.
The returned slice is a copy, so a caller cannot mutate the shared fixture.
func InputSchemaNames ¶
func InputSchemaNames() []string
InputSchemaNames returns every reasoner id the fixture carries, sorted. It is the "what Python publishes" side of the registration parity assertions.
func ProvePhase ¶
func ProvePhase(ctx context.Context, app appx.App, nodeID string, in ProvePhaseInput) (map[string]any, error)
ProvePhase ports the `prove_phase` reasoner (reasoners/phases.py:381).
func ReconPhase ¶
func ReconPhase(ctx context.Context, app appx.App, nodeID string, in ReconPhaseInput) (map[string]any, error)
ReconPhase ports the `recon_phase` reasoner (reasoners/phases.py:152).
func RegisterAll ¶
RegisterAll registers every router reasoner on reg, in DESIGN.md §3 order, and returns the ordered names it registered.
app is the App every handler closes over — and therefore the App whose Call the four `*_phase` handlers make their nested `.call`s through. To keep those nested calls in process too, pass the same local-calling App the registry is answering for (audit.WithLocalCalls does exactly that).
nodeID is read ONCE, here, from phases.NodeID(): Python's module-level `NODE_ID = os.getenv("NODE_ID", "sec-af")`, now a constant. Every `*_phase` handler closes over that value, so the `.call` targets are fixed.
func RemediationPhase ¶
func RemediationPhase(ctx context.Context, app appx.App, nodeID string, in RemediationPhaseInput) (map[string]any, error)
RemediationPhase ports the `remediation_phase` reasoner (reasoners/phases.py:487).
func RunAPISecurityHunter ¶
func RunAPISecurityHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
RunAPISecurityHunter ports `run_api_security_hunter` (reasoners/hunt.py:256). Depth-less hunter — see runHunter.
func RunArchitectureMapper ¶
func RunArchitectureMapper(ctx context.Context, app appx.App, in RepoPathInput) (map[string]any, error)
RunArchitectureMapper ports `run_architecture_mapper(repo_path)` (reasoners/recon.py:16).
func RunAuthHunter ¶
RunAuthHunter ports `run_auth_hunter` (reasoners/hunt.py:123).
func RunBusinessLogicHunter ¶
func RunBusinessLogicHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
RunBusinessLogicHunter ports `run_business_logic_hunter` (reasoners/hunt.py:174).
Python parity: the agent function's sixth parameter `depth_prompt: str = ""` is never supplied by _run_hunter, so it keeps its default.
func RunCWEExpansion ¶
func RunCWEExpansion(ctx context.Context, app appx.App, in CWEExpansionInput) (map[string]any, error)
RunCWEExpansion ports `run_cwe_expansion(recon_summary, strategies)` (reasoners/phases.py:139):
additional = await expand_cwes_for_hunt(recon_summary, strategies)
return {"additional_cwes": additional}
Python parity: no note, and no error path — expand_cwes_for_hunt swallows every failure and returns []. It is registered but never `.call`ed (hunt_phase runs expand_cwes_for_hunt in process), so it draws no DAG node.
func RunConfigScanner ¶
RunConfigScanner ports `run_config_scanner(repo_path)` (reasoners/recon.py:32).
func RunConfigSecretsHunter ¶
func RunConfigSecretsHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
RunConfigSecretsHunter ports `run_config_secrets_hunter` (reasoners/hunt.py:239). Depth-less hunter — see runHunter.
func RunCrossServiceAnalyzer ¶
func RunCrossServiceAnalyzer(ctx context.Context, app appx.App, in CrossServiceInput) (map[string]any, error)
RunCrossServiceAnalyzer ports `run_cross_service_analyzer(repo_path, services, findings_summary, depth)` (reasoners/prove.py:169).
func RunCryptoHunter ¶
RunCryptoHunter ports `run_crypto_hunter` (reasoners/hunt.py:157).
Python parity: run_crypto_hunter's signature has no `depth`, so _run_hunter reaches it through the third cascade shape — depth is DROPPED and max_files_without_signal falls back to 30. See runHunter.
func RunDastVerifier ¶
func RunDastVerifier(ctx context.Context, app appx.App, in DastVerifierInput) (map[string]any, error)
RunDastVerifier ports `run_dast_verifier(repo_path, finding, exploit_payload, depth)` (reasoners/prove.py:158).
func RunDataExposureHunter ¶
func RunDataExposureHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
RunDataExposureHunter ports `run_data_exposure_hunter` (reasoners/hunt.py:205). Depth-less hunter — see runHunter.
func RunDataFlowMapper ¶
func RunDataFlowMapper(ctx context.Context, app appx.App, in ArchitectureInput) (map[string]any, error)
RunDataFlowMapper ports `run_data_flow_mapper(repo_path, architecture)` (reasoners/recon.py:40):
architecture_model = ArchitectureMap(**architecture)
ArchitectureMap has no required field of its OWN, so an empty (or absent) `architecture` binds to the pydantic defaults rather than raising — but each of its five lists holds a model that DOES have required fields (Module name/path/language, EntryPoint kind/identifier/file_path/line, APIEndpoint method/path/handler/file_path/line, ...), and pydantic validates those. `ArchitectureMap(**{"modules": [{"name": "x"}]})` raises 2 errors on the pinned interpreter, so this is bindArchitectureMap, not a bare afx.Bind: the reasoner is registered on the router and a control-plane caller can send that payload directly.
func RunDeduplicator ¶
func RunDeduplicator(ctx context.Context, app appx.App, in DeduplicatorInput) (map[string]any, error)
RunDeduplicator ports `run_deduplicator(findings, recon_context, repo_path)` (reasoners/hunt.py:273):
raw_findings = [RawFinding(**f) for f in findings] recon = ReconResult(**recon_context) result = await _deduplicate_and_correlate(raw_findings, recon, router, repo_path) return result.model_dump()
Python parity: `ReconResult(**recon_context)` here is the RAW constructor — it does NOT go through _recon_model, so an incomplete recon_context fails validation instead of being seeded. hunt_phase always forwards the caller's full recon dump to this reasoner (only the HUNTERS get the pruned projection), so the strict bind is the correct one.
func RunDepReachability ¶
func RunDepReachability(ctx context.Context, app appx.App, in FindingDepthInput) (map[string]any, error)
RunDepReachability ports `run_dep_reachability(repo_path, finding, depth)` (reasoners/prove.py:52).
Python parity: `finding` is forwarded to the agent as the RAW DICT — this is the one prove reasoner that does not build a model first, because agents/prove/dep_reachability.py renders the dict into its prompt directly.
func RunDependencyAuditor ¶
func RunDependencyAuditor(ctx context.Context, app appx.App, in RepoPathInput) (map[string]any, error)
RunDependencyAuditor ports `run_dependency_auditor(repo_path)` (reasoners/recon.py:24).
func RunDosHunter ¶
RunDosHunter ports `run_dos_hunter` (reasoners/hunt.py:89).
func RunExploitHypothesizer ¶
func RunExploitHypothesizer(ctx context.Context, app appx.App, in ExploitInput) (map[string]any, error)
RunExploitHypothesizer ports `run_exploit_hypothesizer(repo_path, finding, data_flow, sanitization, depth)` (reasoners/prove.py:91).
func RunInjectionHunter ¶
RunInjectionHunter ports `run_injection_hunter` (reasoners/hunt.py:72).
func RunLogicBugsHunter ¶
RunLogicBugsHunter ports `run_logic_bugs_hunter` (reasoners/hunt.py:191):
return await run_business_logic_hunter(repo_path=..., recon_context=...,
depth=..., max_files_without_signal=...)
Python parity, two points that are easy to miss:
- it emits NO note of its own. The note the execution shows is the "Business logic hunter starting" one the delegate emits.
- it delegates to the REASONER, not to agents/hunt/logic.py's run_logic_hunter. reasoners/hunt.py imports run_logic_hunter and never uses it (a dead import in the Python source), so this reasoner is behaviorally indistinguishable from run_business_logic_hunter.
func RunRemediation ¶
RunRemediation ports `run_remediation(repo_path, finding)` (reasoners/prove.py:131) — the reasoner remediation_phase fans out over.
finding_model = VerifiedFinding(**finding) result = await generate_remediation(router, repo_path, finding_model) return RemediationSuggestion.model_validate(result).model_dump()
Python parity: the note is "Remediation agent starting" — the same string run_remediation_agent uses, tags included.
The trailing `RemediationSuggestion.model_validate(result)` re-validates a value that generate_remediation already returned as a RemediationSuggestion. It is a no-op round trip; GenerateRemediation returns the typed value here, so ToMap is the whole of it.
func RunRemediationAgent ¶
func RunRemediationAgent(ctx context.Context, app appx.App, in RemediationAgentInput) (map[string]any, error)
RunRemediationAgent ports `run_remediation_agent(repo_path, finding, verdict, rationale)` (reasoners/prove.py:145). `finding` is a RawFinding here, unlike run_remediation's VerifiedFinding.
func RunSSRFHunter ¶
RunSSRFHunter ports `run_ssrf_hunter` (reasoners/hunt.py:106).
func RunSanitizationAnalyzer ¶
func RunSanitizationAnalyzer(ctx context.Context, app appx.App, in SanitizationInput) (map[string]any, error)
RunSanitizationAnalyzer ports `run_sanitization_analyzer(repo_path, finding, data_flow, depth)` (reasoners/prove.py:77).
func RunSecurityContextProfiler ¶
func RunSecurityContextProfiler(ctx context.Context, app appx.App, in ArchitectureInput) (map[string]any, error)
RunSecurityContextProfiler ports `run_security_context_profiler(repo_path, architecture)` (reasoners/recon.py:49). Same `ArchitectureMap(**architecture)` bind, same nested validation — see RunDataFlowMapper.
func RunSupplyChainHunter ¶
func RunSupplyChainHunter(ctx context.Context, app appx.App, in HunterInput) (map[string]any, error)
RunSupplyChainHunter ports `run_supply_chain_hunter` (reasoners/hunt.py:222). Depth-less hunter — see runHunter.
func RunTracer ¶
RunTracer ports `run_tracer(repo_path, finding, depth)` (reasoners/prove.py:69).
Python parity: `RawFinding(**finding)` here is the STRICT constructor, not _coerce_verifier_finding — a FindingForVerifier projection sent to this reasoner raises rather than being adapted.
func RunVerdictAgent ¶
RunVerdictAgent ports `run_verdict_agent(finding, data_flow, sanitization, exploit)` (reasoners/prove.py:109).
Python parity: the reasoner has no repo_path parameter and passes the literal "." to the agent, which never reads it.
func RunVerifier ¶
RunVerifier ports `run_verifier(repo_path, finding, depth)` (reasoners/prove.py:60) — the reasoner prove_phase fans out over.
func RunXSSHunter ¶
RunXSSHunter ports `run_xss_hunter` (reasoners/hunt.py:140).
func ValidateHandlerInput ¶
ValidateHandlerInput is `_validate_handler_input(data, input_types)` for the reasoner called name: it returns the validated, coerced keyword map to bind, or a *HandlerInputError.
Like Python's, the returned map contains ONLY declared parameters — an undeclared key in the body is dropped rather than forwarded.
It PANICS on a name the capture does not know, for the same reason InputSchema does: every registration goes through here, so adding a reasoner without regenerating the capture fails at boot and in every test instead of silently shipping an unvalidated handler.
Types ¶
type ArchitectureInput ¶
type ArchitectureInput struct {
RepoPath string `json:"repo_path"`
Architecture map[string]any `json:"architecture"`
}
ArchitectureInput ports the two deep-recon reasoner signatures:
run_data_flow_mapper(repo_path: str, architecture: dict[str, Any]) run_security_context_profiler(repo_path: str, architecture: dict[str, Any])
`architecture` becomes `ArchitectureMap(**architecture)`. ArchitectureMap has no required field of its own, but its five lists hold models that do, so the bind goes through phases.BindArchitectureMap rather than a bare afx.Bind.
type CWEExpansionInput ¶
type CWEExpansionInput struct {
ReconSummary string `json:"recon_summary"`
Strategies []string `json:"strategies"`
}
CWEExpansionInput ports:
run_cwe_expansion(recon_summary: str, strategies: list[str])
type CrossServiceInput ¶
type CrossServiceInput struct {
RepoPath string `json:"repo_path"`
Services []string `json:"services"`
FindingsSummary string `json:"findings_summary"`
Depth string `json:"depth"`
}
CrossServiceInput ports:
run_cross_service_analyzer(repo_path: str, services: list[str],
findings_summary: str, depth: str)
type DastVerifierInput ¶
type DastVerifierInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
ExploitPayload string `json:"exploit_payload"`
Depth string `json:"depth"`
}
DastVerifierInput ports:
run_dast_verifier(repo_path: str, finding: dict[str, Any], exploit_payload: str, depth: str)
type DeduplicatorInput ¶
type DeduplicatorInput struct {
Findings []map[string]any `json:"findings"`
ReconContext map[string]any `json:"recon_context"`
RepoPath string `json:"repo_path"`
}
DeduplicatorInput ports:
run_deduplicator(findings: list[dict[str, Any]], recon_context: dict[str, Any], repo_path: str)
Field ORDER follows the Python signature (findings first), which is also the order the JSON keys are documented in; it has no effect on binding.
type ExploitInput ¶
type ExploitInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
DataFlow map[string]any `json:"data_flow"`
Sanitization map[string]any `json:"sanitization"`
Depth string `json:"depth"`
}
ExploitInput ports:
run_exploit_hypothesizer(repo_path: str, finding: dict[str, Any],
data_flow: dict[str, Any],
sanitization: dict[str, Any], depth: str)
type FindingDepthInput ¶
type FindingDepthInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
Depth string `json:"depth"`
}
FindingDepthInput ports the three signatures shaped `(repo_path, finding, depth)`:
run_dep_reachability(repo_path: str, finding: dict[str, Any], depth: str) run_verifier(repo_path: str, finding: dict[str, Any], depth: str) run_tracer(repo_path: str, finding: dict[str, Any], depth: str)
type Handler ¶
Handler is one reasoner: the untyped keyword map a `.call` carries in, the reasoner's result out. It is the shape the SDK's agent.HandlerFunc had, kept so every handler below moved across unchanged.
func ValidateHandler ¶
ValidateHandler wraps h with `_validate_handler_input` for the reasoner called name — the check the Python SDK ran on every request body BEFORE the handler function was entered (agent.py:3120-3134).
Exported because internal/secaf/audit runs the same layer over the audit entry's own request.
Python answered a failure with `JSONResponse(422, {"detail": safe_message})` from the endpoint, so the handler never ran. Here the check runs as the first thing inside the handler and reports the same status through an *InputError.
type HandlerInputError ¶
type HandlerInputError struct{ Message string }
HandlerInputError is the Go shape of `_HandlerInputError`, the ValueError subclass `_validate_handler_input` raises. Its message is the SDK-constructed `safe_message` Python puts in the 422 body verbatim.
func (*HandlerInputError) Error ¶
func (e *HandlerInputError) Error() string
type HuntPhaseInput ¶
type HuntPhaseInput struct {
RepoPath string `json:"repo_path"`
ReconContext map[string]any `json:"recon_context"`
Depth string `json:"depth"`
AIGate json.RawMessage `json:"ai_gate"`
MaxConcurrentHunters int `json:"max_concurrent_hunters"`
EarlyStopFileThreshold int `json:"early_stop_file_threshold"`
}
HuntPhaseInput ports:
hunt_phase(repo_path: str, recon_context: dict[str, Any], depth: str = "standard",
ai_gate: Any | None = None, max_concurrent_hunters: int = 4,
early_stop_file_threshold: int = 30)
`ai_gate` is kept as a RAW JSON value. It is an object parameter that app.py's `.call` into hunt_phase never passes, but hunt_phase is a REGISTERED reasoner, so a control-plane caller can send one — and Python binds it (the SDK passes an `Any`-hinted parameter through untouched), takes the `ai_gate is not None` branch, raises AttributeError on `ai_gate.select_strategy(...)` and emits
AI gate failed: 'dict' object has no attribute 'select_strategy', using default strategies
with tags ["hunt","ai_gate","error"] before falling back to the default strategies. phases.NewJSONAIGate turns the raw value into exactly that: nil (Python's None) for an absent key or `null`, and otherwise a gate that fails with the CPython message for that JSON type. Raw bytes rather than `any` because encoding/json decodes every number to float64, which would report "float" where CPython reports "int".
func NewHuntPhaseInput ¶
func NewHuntPhaseInput() HuntPhaseInput
NewHuntPhaseInput returns the Python keyword defaults.
func (*HuntPhaseInput) UnmarshalJSON ¶
func (h *HuntPhaseInput) UnmarshalJSON(b []byte) error
UnmarshalJSON seeds hunt_phase's three keyword defaults before decoding.
type HunterInput ¶
type HunterInput struct {
RepoPath string `json:"repo_path"`
ReconContext map[string]any `json:"recon_context"`
Depth string `json:"depth"`
MaxFilesWithoutSignal int `json:"max_files_without_signal"`
}
HunterInput is the signature all twelve hunter reasoners share:
run_<strategy>_hunter(repo_path: str, recon_context: dict[str, Any],
depth: str, max_files_without_signal: int = 30)
`depth` has NO default here (it is positional-or-keyword without one), so a request that OMITS it binds the empty string — which every downstream _normalize_depth turns into "standard".
This is the ONE rule of handler_input.go's port of the Python SDK's `_validate_handler_input` that is deliberately not reproduced: Python answers `422 Missing required field: depth` before the handler body runs (agent.py:1169-1171), because it can see that the key was absent. The Go input structs cannot — an absent scalar is indistinguishable from a zero one — so the request proceeds. Everything else that layer does (the null-on-required rejection, the int/float/str/bool coercions, the dict/list shape checks) IS reproduced; see handler_input.go.
func NewHunterInput ¶
func NewHunterInput() HunterInput
NewHunterInput returns the Python keyword defaults.
func (*HunterInput) UnmarshalJSON ¶
func (h *HunterInput) UnmarshalJSON(b []byte) error
UnmarshalJSON seeds max_files_without_signal=30 before decoding.
type InputError ¶
InputError is a request the reasoner refused before running: the input did not bind. StatusCode keeps the HTTP status the node answered with — 422 for the SDK-level validation Python's endpoint performs, 400 for the audit entry's own bad-input family — because that number is how a caller told "your input was wrong" from "the audit failed", and the codeaf side may want to keep drawing the same line.
func (*InputError) Error ¶
func (e *InputError) Error() string
type ProvePhaseInput ¶
type ProvePhaseInput struct {
RepoPath string `json:"repo_path"`
HuntResult map[string]any `json:"hunt_result"`
Depth string `json:"depth"`
MaxProvers *int `json:"max_provers"`
MaxConcurrentProvers int `json:"max_concurrent_provers"`
}
ProvePhaseInput ports:
prove_phase(repo_path: str, hunt_result: dict[str, Any], depth: str = "standard",
max_provers: int | None = None, max_concurrent_provers: int = 3)
max_provers is a pointer: nil is Python's None (use the depth's cap), and a present 0 caps the prover fan-out at zero.
func NewProvePhaseInput ¶
func NewProvePhaseInput() ProvePhaseInput
NewProvePhaseInput returns the Python keyword defaults.
func (*ProvePhaseInput) UnmarshalJSON ¶
func (p *ProvePhaseInput) UnmarshalJSON(b []byte) error
UnmarshalJSON seeds prove_phase's keyword defaults before decoding.
type ReconPhaseInput ¶
ReconPhaseInput ports:
recon_phase(repo_path: str, depth: str = "standard")
func NewReconPhaseInput ¶
func NewReconPhaseInput() ReconPhaseInput
NewReconPhaseInput returns the Python keyword defaults.
func (*ReconPhaseInput) UnmarshalJSON ¶
func (r *ReconPhaseInput) UnmarshalJSON(b []byte) error
UnmarshalJSON seeds depth="standard" before decoding.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the reasoner table: name -> handler, plus the registration order and the input schema each name was registered with.
It is built once by RegisterAll and only read afterwards, so it needs no lock; a caller that registers concurrently with lookups is misusing it.
func (*Registry) InputSchema ¶
func (r *Registry) InputSchema(name string) (json.RawMessage, bool)
InputSchema returns a copy of the input schema name was registered with, and false for a name the registry does not carry.
func (*Registry) Register ¶
func (r *Registry) Register(name string, h Handler, schema json.RawMessage)
Register adds one handler under name.
It PANICS on a name registered twice: the SDK's reasoner table was a map where the second registration silently won, and a collision is a wiring bug that must fail at construction, not at the first `.call` that reaches the wrong handler.
type RemediationAgentInput ¶
type RemediationAgentInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
Verdict string `json:"verdict"`
Rationale string `json:"rationale"`
}
RemediationAgentInput ports:
run_remediation_agent(repo_path: str, finding: dict[str, Any], verdict: str, rationale: str)
`finding` is a RawFinding here. Nothing in the pipeline calls this reasoner; it is part of the registered surface.
type RemediationInput ¶
type RemediationInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
}
RemediationInput ports:
run_remediation(repo_path: str, finding: dict[str, Any])
`finding` is a VerifiedFinding here (not a RawFinding) — this is the reasoner remediation_phase calls.
type RemediationPhaseInput ¶
type RemediationPhaseInput struct {
RepoPath string `json:"repo_path"`
VerifiedFindings []map[string]any `json:"verified_findings"`
MaxConcurrentRemediations int `json:"max_concurrent_remediations"`
}
RemediationPhaseInput ports:
remediation_phase(repo_path: str, verified_findings: list[dict[str, Any]],
max_concurrent_remediations: int = 3)
func NewRemediationPhaseInput ¶
func NewRemediationPhaseInput() RemediationPhaseInput
NewRemediationPhaseInput returns the Python keyword defaults.
func (*RemediationPhaseInput) UnmarshalJSON ¶
func (r *RemediationPhaseInput) UnmarshalJSON(b []byte) error
UnmarshalJSON seeds max_concurrent_remediations=3 before decoding.
type RepoPathInput ¶
type RepoPathInput struct {
RepoPath string `json:"repo_path"`
}
RepoPathInput is the signature shared by the three recon reasoners that take only a repository path:
run_architecture_mapper(repo_path: str) run_dependency_auditor(repo_path: str) run_config_scanner(repo_path: str)
type SanitizationInput ¶
type SanitizationInput struct {
RepoPath string `json:"repo_path"`
Finding map[string]any `json:"finding"`
DataFlow map[string]any `json:"data_flow"`
Depth string `json:"depth"`
}
SanitizationInput ports:
run_sanitization_analyzer(repo_path: str, finding: dict[str, Any],
data_flow: dict[str, Any], depth: str)
type VerdictInput ¶
type VerdictInput struct {
Finding map[string]any `json:"finding"`
DataFlow map[string]any `json:"data_flow"`
Sanitization map[string]any `json:"sanitization"`
Exploit map[string]any `json:"exploit"`
}
VerdictInput ports:
run_verdict_agent(finding: dict[str, Any], data_flow: dict[str, Any],
sanitization: dict[str, Any], exploit: dict[str, Any])
There is no repo_path parameter: the adapter passes the literal "." to run_verdict_agent, which never reads it.