Skip to content

threat-detection: max-ai-credits is not propagated to the external detector's AWF config #50654

Description

@davidslater

Created by GitHub Ace · View Session

Summary

safe-outputs.threat-detection.max-ai-credits (default 400, or vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS) is parsed in threat_detection_config.go:138 and applied on the inline path:

// pkg/workflow/threat_detection_inline_engine.go:78-79
if data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil && data.SafeOutputs.ThreatDetection.MaxAICredits != 0 {
    detectionEngineConfig.MaxAICredits = data.SafeOutputs.ThreatDetection.MaxAICredits
}

buildExternalDetectorWorkflowData (pkg/workflow/threat_detection_helpers.go:148-162) never sets it. It builds d.EngineConfig from ThreatDetection.EngineConfig — a different field — so ThreatDetection.MaxAICredits is dropped.

Why this matters

AWFConfig.apiProxy.maxAiCredits is populated from WorkflowData.EngineConfig.MaxAICredits (pkg/workflow/awf_config.go:544-545, 571). With it unset on the external path, the detection job's AWF config falls back to the generic runtime expression rather than the detection-specific budget and its vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS default.

So a workflow that explicitly configures safe-outputs.threat-detection.max-ai-credits gets that cap enforced on the inline path and not on the external path. This compounds with the detector's --retries default of 1 (up to two full engine passes), so a pathological external run can cost more than the inline path ever would.

The enforcement mechanism itself is fine — upstream's investigation on #698 confirmed the AWF API proxy enforces the cumulative cap across retries, so nothing needs to be passed to the binary. The bug is purely that the wrong number reaches the proxy.

Proposed change

Set MaxAICredits in buildExternalDetectorWorkflowData the same way the inline path does:

if data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil && data.SafeOutputs.ThreatDetection.MaxAICredits != 0 {
    d.EngineConfig.MaxAICredits = data.SafeOutputs.ThreatDetection.MaxAICredits
}

Mirror the existing inline-path tests: TestBuildDetectionEngineExecutionStepMaxAICredits and TestBuildDetectionEngineExecutionStepMaxAICreditsNotInheritedFromMainAgent (pkg/workflow/threat_detection_test.go:766-832) already encode the exact semantics — including that the detection budget must not be inherited from the main agent's budget. Add external-path equivalents.

Companion: aic smoke assertion

Upstream item 10 on github/gh-aw-threat-detection#745. parse_token_usage.cjs sources aic from the API proxy's …/api-proxy-logs/token-usage.jsonlnot from threat-detect's stdout — so engine-flag drift cannot break it. But nothing currently asserts it is non-empty on the external path.

aic is a detection job output (needs.detection.outputs.aicGH_AW_THREAT_DETECTION_AIC), which the agentic smoke step in the detector repo cannot observe. The assertion has to live here: either a step in a job with needs: detection asserting the output is non-empty, or a compiler-level test over the generated lock.

Companion: scope decision for non-proxy invocation

Both maxAiCredits enforcement and aic derivation depend on apiProxy.enabled + enableTokenSteering: true. A plain threat-detect invocation without the proxy has no credit cap and produces no token-usage.jsonl. Today the shipping path is always AWF + proxy — confirm and document that as the only supported configuration, or decide that the detector must emit its own agent_usage.json from engine usage records. This is a scoping decision, not a code change, but it should be written down rather than left implicit.

Acceptance criteria

  • MaxAICredits propagated in buildExternalDetectorWorkflowData.
  • Compiled detection AWF config contains the detection-specific apiProxy.maxAiCredits.
  • Tests mirroring the two inline-path MaxAICredits tests, including the not-inherited-from-main-agent case.
  • Non-empty aic asserted on the external detection path.
  • Non-proxy invocation documented as out of scope (or an alternative decided).
  • make recompile.

References

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions