Overview
Part of the security epic #36 — continues #38 (added --strict, closed by PR #161). The opt-in --strict gate promotes HIGH Layer 1 findings to blocking, but there is no way to waive a finding an operator cannot fix. This adds a declarative, fleet-side suppression mechanism so operators can enable --strict without being permanently blocked by findings in artifacts they don't author.
Problem Statement
Surfaced while manually testing the v0.2.5 release (PR #164): deploy --strict rshade/gh-aw-fleet (dry-run) blocks with 21 HIGH findings, all actionlint:* on gh-aw compiler-generated *.lock.yml files (# This file was automatically generated by gh-aw … DO NOT EDIT). These appear on every compiled workflow in every fleet, regardless of source. The only carve-out today is the hardcoded promptinj: prefix in BlockingSecurityFindings (internal/fleet/security_gate.go:68). Net result: --strict ships correct but un-enableable on a real agentics-based fleet.
Proposed Solution
A declarative security.suppress block in fleet.json (overlay-able via fleet.local.json), matched by rule + file glob, applied at the blocking computation only — suppressed findings still surface as advisory, preserving the audit trail.
Config shape (pkg/fleet/config.go)
New wire-contract types: SecurityConfig + SuppressionRule{Rule, File, Reason string}, hung off a new optional Config.Security *SecurityConfig (json:"security,omitempty") — mirrors the RepoSpec.CompileStrict *bool optional-pointer precedent.
Matching
rule matches Finding.RuleID exactly or with a trailing * (e.g. actionlint:*) via stdlib path/filepath.Match — no new dependency (constitution: stdlib-first).
file matches Finding.File (slash form, e.g. .github/workflows/ci-doctor.lock.yml) via filepath.Match; single-level * covers the .lock.yml case (avoids a **/doublestar dep).
- Empty
rule or file = wildcard for that dimension; a finding is suppressed iff both dimensions match.
Gate integration
Add a third continue guard inside BlockingSecurityFindings (internal/fleet/security_gate.go:68), mirroring the existing promptinj: carve-out. That function takes only []Finding today, so thread the compiled rules in via a new parameter or a Suppressions field on fleet.SecurityOpts, populated from cfg.Security at the internal/fleet/deploy.go:276 / :375 call sites (the config is already in scope there). Suppression affects only the blocking set; emitSecurityFindingWarnings and appendFindingDiagnostics still print every finding.
Files likely affected
pkg/fleet/config.go — SecurityConfig/SuppressionRule types + Config.Security field.
internal/fleet/load.go — mergeConfigs (~line 184) clause so security overlays from fleet.local.json.
internal/fleet/security_gate.go — suppression guard; thread through EvaluateStrictSecurityGate.
cmd/deploy.go / cmd/sync.go / cmd/upgrade.go — pass cfg.Security into the gate.
- docs + godoc (additive — no
cmd/fleet.SchemaVersion bump).
Acceptance Criteria
Out of Scope
- Per-repo (
RepoSpec.Security) overrides — follow-on.
- A
suppressed: true / reason annotation on the JSON envelope — deferred (see Resolved decisions).
- Demoting actionlint severity — that is the companion "option 3" issue.
- Any change to
gh aw compile --strict.
Resolved decisions
- Suppressed findings stay visible as advisory (printed on stderr + JSON
warnings[]); suppression removes them from the --strict blocking set only. A suppressed: true / reason envelope annotation is deferred (later enhancement), keeping advisory output byte-identical for now.
Related
Overview
Part of the security epic #36 — continues #38 (added
--strict, closed by PR #161). The opt-in--strictgate promotes HIGH Layer 1 findings to blocking, but there is no way to waive a finding an operator cannot fix. This adds a declarative, fleet-side suppression mechanism so operators can enable--strictwithout being permanently blocked by findings in artifacts they don't author.Problem Statement
Surfaced while manually testing the v0.2.5 release (PR #164):
deploy --strict rshade/gh-aw-fleet(dry-run) blocks with 21 HIGH findings, allactionlint:*on gh-aw compiler-generated*.lock.ymlfiles (# This file was automatically generated by gh-aw … DO NOT EDIT). These appear on every compiled workflow in every fleet, regardless of source. The only carve-out today is the hardcodedpromptinj:prefix inBlockingSecurityFindings(internal/fleet/security_gate.go:68). Net result:--strictships correct but un-enableable on a real agentics-based fleet.Proposed Solution
A declarative
security.suppressblock infleet.json(overlay-able viafleet.local.json), matched by rule + file glob, applied at the blocking computation only — suppressed findings still surface as advisory, preserving the audit trail.Config shape (
pkg/fleet/config.go){ "version": 1, "security": { "suppress": [ { "rule": "actionlint:syntax-check", "file": ".github/workflows/*.lock.yml", "reason": "gh-aw 'queue' concurrency extension; actionlint schema false positive" } ] } }New wire-contract types:
SecurityConfig+SuppressionRule{Rule, File, Reason string}, hung off a new optionalConfig.Security *SecurityConfig(json:"security,omitempty") — mirrors theRepoSpec.CompileStrict *booloptional-pointer precedent.Matching
rulematchesFinding.RuleIDexactly or with a trailing*(e.g.actionlint:*) via stdlibpath/filepath.Match— no new dependency (constitution: stdlib-first).filematchesFinding.File(slash form, e.g..github/workflows/ci-doctor.lock.yml) viafilepath.Match; single-level*covers the.lock.ymlcase (avoids a**/doublestar dep).ruleorfile= wildcard for that dimension; a finding is suppressed iff both dimensions match.Gate integration
Add a third
continueguard insideBlockingSecurityFindings(internal/fleet/security_gate.go:68), mirroring the existingpromptinj:carve-out. That function takes only[]Findingtoday, so thread the compiled rules in via a new parameter or aSuppressionsfield onfleet.SecurityOpts, populated fromcfg.Securityat theinternal/fleet/deploy.go:276/:375call sites (the config is already in scope there). Suppression affects only the blocking set;emitSecurityFindingWarningsandappendFindingDiagnosticsstill print every finding.Files likely affected
pkg/fleet/config.go—SecurityConfig/SuppressionRuletypes +Config.Securityfield.internal/fleet/load.go—mergeConfigs(~line 184) clause sosecurityoverlays fromfleet.local.json.internal/fleet/security_gate.go— suppression guard; thread throughEvaluateStrictSecurityGate.cmd/deploy.go/cmd/sync.go/cmd/upgrade.go— passcfg.Securityinto the gate.cmd/fleet.SchemaVersionbump).Acceptance Criteria
security.suppressparses fromfleet.jsonand overlays fromfleet.local.json.--strictblocking set;deploy --stricton the otherwise-blocked repo exits 0.warnings[](advisory, not hidden).cmd.SchemaVersion/fleet.SchemaVersionbump.--strict/--yesremain un-persisted tofleet.json.internal/fleet/security_gate_test.go) + a load/merge test.Out of Scope
RepoSpec.Security) overrides — follow-on.suppressed: true/reasonannotation on the JSON envelope — deferred (see Resolved decisions).gh aw compile --strict.Resolved decisions
warnings[]); suppression removes them from the--strictblocking set only. Asuppressed: true/reasonenvelope annotation is deferred (later enhancement), keeping advisory output byte-identical for now.Related
--strict) and Defense-in-depth UX: stderr + interactive prompt + PR body Security Findings section #40 (PR feat(security): add interactive findings confirmation and --yes flag #177, findings UX).