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.jsonl — not 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.aic → GH_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
References
Summary
safe-outputs.threat-detection.max-ai-credits(default 400, orvars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS) is parsed inthreat_detection_config.go:138and applied on the inline path:buildExternalDetectorWorkflowData(pkg/workflow/threat_detection_helpers.go:148-162) never sets it. It buildsd.EngineConfigfromThreatDetection.EngineConfig— a different field — soThreatDetection.MaxAICreditsis dropped.Why this matters
AWFConfig.apiProxy.maxAiCreditsis populated fromWorkflowData.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 itsvars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITSdefault.So a workflow that explicitly configures
safe-outputs.threat-detection.max-ai-creditsgets that cap enforced on the inline path and not on the external path. This compounds with the detector's--retriesdefault 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
MaxAICreditsinbuildExternalDetectorWorkflowDatathe same way the inline path does:Mirror the existing inline-path tests:
TestBuildDetectionEngineExecutionStepMaxAICreditsandTestBuildDetectionEngineExecutionStepMaxAICreditsNotInheritedFromMainAgent(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:
aicsmoke assertionUpstream item 10 on github/gh-aw-threat-detection#745.
parse_token_usage.cjssourcesaicfrom the API proxy's…/api-proxy-logs/token-usage.jsonl— not fromthreat-detect's stdout — so engine-flag drift cannot break it. But nothing currently asserts it is non-empty on the external path.aicis a detection job output (needs.detection.outputs.aic→GH_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 withneeds: detectionasserting the output is non-empty, or a compiler-level test over the generated lock.Companion: scope decision for non-proxy invocation
Both
maxAiCreditsenforcement andaicderivation depend onapiProxy.enabled+enableTokenSteering: true. A plainthreat-detectinvocation without the proxy has no credit cap and produces notoken-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 ownagent_usage.jsonfrom engine usage records. This is a scoping decision, not a code change, but it should be written down rather than left implicit.Acceptance criteria
MaxAICreditspropagated inbuildExternalDetectorWorkflowData.apiProxy.maxAiCredits.MaxAICreditstests, including the not-inherited-from-main-agent case.aicasserted on the external detection path.make recompile.References
aic) unverified andmax-ai-creditsbudget not enforced on the external detector path gh-aw-threat-detection#698