Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7b8d53a
docs(specs): Spec 101 US2 tasks — seed corpus and eval gate
Dumbris Oct 20, 2026
6b89152
feat(scanner): validate bundle signatures[] and export a pure bundle …
Dumbris Oct 20, 2026
c41d27a
docs(spec-101): amend contracts and spec docs for the in-repo TPA cor…
Dumbris Oct 20, 2026
1ed00c8
feat(security): deterministic TPA corpus builder and six migrated sig…
Dumbris Oct 20, 2026
96e4a31
feat(scan-eval): score the TPA signature bundle in the detect gate
Dumbris Oct 20, 2026
79a0521
feat(security): final TPA class vocabulary and the verified corpus as…
Dumbris Oct 20, 2026
1cf931e
feat(security): 44-signature TPA corpus across 16 classes, bundle v0.1.1
Dumbris Oct 20, 2026
97c6faa
test(scan-eval): eval pairs for every TPA signature, validated agains…
Dumbris Oct 20, 2026
d97328f
feat(scan-eval): gate every TPA signature class through the bundle check
Dumbris Oct 20, 2026
647f230
test(scan-eval): prove the TPA bundle gate is not vacuous with a remo…
Dumbris Oct 20, 2026
6981840
docs(security): TPA signature contributor guide and bundle gate docs
Dumbris Oct 20, 2026
b0cdb38
fix(security): pin the TPA bundle and its sources to LF
Dumbris Oct 20, 2026
d367692
fix(security): narrow TPA rules flagged by review and correct provenance
Dumbris Oct 20, 2026
c4587ff
docs: Spec 101 US2 Recent Changes line and task ticks
Dumbris Oct 20, 2026
d1cc978
fix(security): address cross-model review round 1 (TPA rule precision…
Dumbris Oct 20, 2026
0610dec
chore(roadmap): tpa-db-corpus in review (#1702)
Dumbris Oct 20, 2026
998059c
fix(security): address cross-model review round 2 (TPA rule precision…
Dumbris Oct 20, 2026
9c9da3d
Merge remote-tracking branch 'origin/main' into claude/101-tpa-db-cor…
Dumbris Oct 20, 2026
945b667
fix(security): address cross-model review round 3 (TPA rule precision…
Dumbris Oct 20, 2026
fcfaecb
fix(security): address cross-model review round 4 and sweep every TPA…
Dumbris Oct 20, 2026
8d468b2
fix(security): address cross-model review round 5 (recall on cited pa…
Dumbris Oct 20, 2026
8b55e90
fix(security): address cross-model review round 6 (negation guard acr…
Dumbris Oct 20, 2026
e43d724
fix(security): address cross-model review round 7 (negations inside r…
Dumbris Oct 20, 2026
f0369a7
fix(security): address cross-model review round 8 (secret-value vs ac…
Dumbris Oct 20, 2026
b423df4
fix(security): address cross-model review round 9 (negated objects an…
Dumbris Oct 20, 2026
e63eb46
Merge remote-tracking branch 'origin/main' into claude/101-tpa-db-cor…
Dumbris Oct 20, 2026
4087e32
Merge remote-tracking branch 'origin/main' into claude/101-tpa-db-cor…
Dumbris Oct 20, 2026
610a108
test(scan-eval): compare the quoted bundle path so the assertion hold…
Dumbris Oct 20, 2026
cf172f7
Merge remote-tracking branch 'origin/main' into claude/101-tpa-db-cor…
Dumbris Oct 22, 2026
bd33838
fix(security): close the two round-10 recall gaps (0011 multiline cla…
Dumbris Oct 22, 2026
f98611e
test(server): build the operator-signed fixture on the seed corpus
Dumbris Oct 22, 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
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ internal/httpapi/testdata/sc006_activity_recordsets.json text eol=lf
cmd/mcpproxy/testdata/cli108/* text eol=lf
internal/server/testdata/profiles_v3/*.json text eol=lf

# Spec 101 US2: the embedded TPA bundle is byte-compared against a rebuild of
# its signature sources (TestEmbeddedCorpus_RebuildIsByteIdentical, `tpa-corpus
# verify`), and go:embed embeds whatever bytes the checkout has — a CRLF
# checkout on Windows would report spurious drift.
internal/security/scanner/bundled/scanner-bundle.json text eol=lf
internal/security/tpacorpus/signatures/*.json text eol=lf
internal/security/tpacorpus/testdata/*.json text eol=lf

# Spec 101 sidecar conformance fixtures are byte-exact (signatures cover exact
# bytes, the 4096/4097-byte ceilings are measured on the file, and one fixture
# deliberately ends in CRLF). Never convert line endings on checkout.
Expand Down
22 changes: 21 additions & 1 deletion .github/workflows/eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ name: Eval (Spec 065 regression gate)
# masks or blocks the deterministic D2 (security) gate.
#
# security-d2 — Go + Python only, no live upstreams. HARD gate (blocking).
# The detect-engine gate scores the EMBEDDED signature bundle
# (scanner-bundle.json, Spec 101 FR-016a), and a negative control
# proves that scoring is not vacuous (FR-016b).
# retrieval-d1 — needs a live mcpproxy serving 7 reference servers; network
# dependent. Report-only on PRs, blocking on the nightly
# schedule (promote to PR-blocking after a green soak — see the
Expand All @@ -29,6 +32,7 @@ on:
- "cmd/mcpproxy/**"
# Eval harness + frozen datasets + this workflow
- "cmd/scan-eval/**"
- "cmd/tpa-corpus/**"
- "specs/065-evaluation-foundation/datasets/**"
- "scripts/eval-ci-smoke.sh"
- ".github/workflows/eval.yml"
Expand Down Expand Up @@ -62,13 +66,29 @@ jobs:
# labeled detect corpus. It runs the production detect.Engine and fails the
# build if malicious recall drops below 0.90 or the hard-negative
# false-positive rate climbs above 0.05. No mcp-eval/Python needed — runs
# first so a detector regression fails fast.
# first so a detector regression fails fast. With no --bundle flag the gate
# scores the signature bundle embedded in this build (the production TPA
# corpus, FR-016a), so a corpus regression fails here, not only a built-in one.
- name: Run Spec-076 detect-engine gate (offline, blocking)
run: |
go run ./cmd/scan-eval \
--corpus specs/065-evaluation-foundation/datasets/detect_corpus_v1.json \
--gate --min-recall 0.90 --max-fp 0.05

# Spec 101 US2 (FR-016a/b): the corpus must be reproducible from its sources
# (verify), pass the policy against the eval dataset (validate), and the gate
# must be provably non-vacuous: TestBundleControl removes the control
# signature TPA-2026-0044 and requires the gate to breach (exit 6), while
# the other tests pin the class coverage and the SC-004 production-path check.
- name: Verify TPA corpus and gate non-vacuity (blocking)
run: |
go run ./cmd/tpa-corpus verify
go run ./cmd/tpa-corpus validate \
--bundle internal/security/scanner/bundled/scanner-bundle.json \
--dataset specs/065-evaluation-foundation/datasets/detect_corpus_v1.json
go test -count=1 -run 'TestBundleControl|TestCategoryCheck|TestSC004|TestEmbeddedCorpus' \
./cmd/scan-eval/ ./internal/security/scanner/ ./internal/security/tpacorpus/

- name: Checkout mcp-eval (public, pinned)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ tail -f ~/Library/Logs/mcpproxy/main.log # main log (macOS; Linux: ~/.mcpproxy/
- 101-tpa-db US3 + 087-tpa-daily-refresh (tpa-bundle-refresh): Go 1.26, Vue 3 Web UI — a refresh cycle (`internal/security/scanner/tpa_refresh.go`: file leg then optional fetch leg, jittered `tpa_refresh_interval` default 24h, single-flight joins, `Nudge` on `config.reloaded`, joined in `Runtime.Close`) feeds the same source-neutral `evaluateCandidate` pipeline; the opt-in fetch (`tpa_fetch_enabled` default off, `tpa_fetch_url` no compiled-in default, env `MCPPROXY_TPA_FETCH_ENABLED`; Releases-API `latest` or direct `<url>` + `<url>.sig`, signed-only via `evalEnv.MustSign`, https/loopback-only transport with dial-time egress checks, host-only logs, outcomes kept apart from `last_rejection`) works today only with operator-signed bundles (publisher key set still empty); the activation self-check (`tpa_selfcheck.go`, parity-tested port of the `cmd/scan-eval` gate, embedded `bundled/selfcheck-corpus.json`) refuses a candidate as `gate_failure`; fetched bundles survive reload/restart (`externalConfigured`) and disabling fetch keeps them serving; activation observers drive the cached-only re-protection sweep (`internal/server/scan_bundle_reprotect.go`, held-evidence leg + `Service.StartScanFromCachedContracts` gated by `auto_baseline_scan`, marker `storage.BundleReprotectState`, RT-02: never approves, unquarantines or revokes); `BundleInfo` gains `last_checked_at`, `last_activated_at`, `stale`, `refresh`, `reprotect`; surfaces `POST /api/v1/security/bundle/refresh`, `mcpproxy security bundle refresh`, `BundleDoctorWarnings`, telemetry schema 16 refresh counters. Docs: `docs/features/tpa-bundle-signing.md` "Daily refresh and optional fetch", `docs/features/security-quarantine.md` "Re-protection after a bundle activates". **No new dependencies.**
- 101-tpa-db US1 (tpa-db-format): Go 1.26, Vue 3 Web UI — external TPA bundles are signed artifacts: `<bundle>.sig` four-key Ed25519 sidecar (`internal/security/bundlesig`, pure, contract `specs/101-tpa-db/contracts/sidecar-format.md`), verify-before-parse source-neutral `Candidate` pipeline (`scanner/tpa_candidate.go`, FR-008 order, ceilings 8 MiB / 2000 rules / 4 KiB / 5 s via `candidate_open_{unix,windows}.go` no-follow descriptor opens), per-authority (publisher/operator) epoch-scoped watermark + ratchet + pin/deny-list in BBolt `tpa_bundle_state` (one tx with the history row) and content-addressed LKG files `<datadir>/tpa-lkg/`, full-digest rollback, `coverageOK` + async scan-mode admission degrade on the four FR-007/009a/009b conditions. Config: `require_signed_bundle` (default off), `tpa_active_root`, `tpa_operator_signers`, `tpa_operator_baseline`, `tpa_operator_epoch`. Surfaces: `GET/POST /api/v1/security/bundle{,/history,/rollback,/unpin,/reload}`, `mcpproxy security bundle …`, `cmd/tpa-db keygen|sign|verify`, telemetry schema 15 bundle keys. Publisher key set ships EMPTY. Docs: `docs/features/tpa-bundle-signing.md`. **No new dependencies.**
- 118-auto-rescan-on-change: Go 1.26, small Web UI change — after a tool definition changes (or a tool is added after the baseline) in `manual`/`scan` trust mode the in-process TPA scan of its current definition is recorded as hold evidence (`held_reason: awaiting_review` + `held_verdict: clean` for a clean verdict; findings/coverage as before) and a debounced informational server rescan (`scan_informational.go`, fed by `Runtime.AddToolDefinitionsChangedObserver`) brings the review scan line back to "current". Switch: `security.auto_rescan_on_change` (default on, env `MCPPROXY_AUTO_RESCAN_ON_CHANGE`) AND `auto_baseline_scan`. RT-02 untouched: a clean verdict never approves a change. Docs: `docs/features/security-quarantine.md` "Automatic informational baseline scan". **No new dependencies.**
- 101-tpa-db US2 (tpa-db-corpus): Go 1.26, backend + CI + docs — embedded TPA bundle grows to 44 signatures / 16 `tpa_*` classes (42 gating, 95 runnable rules, v0.1.1) with an additive `signatures[]` section (provenance, SPDX licence, `rule_ids` `<TPA-id>/<detector>`; load-time referential integrity in `internal/security/scanner/tpa_bundle_signatures.go`, rejected whole), built deterministically from `internal/security/tpacorpus/signatures/` by `cmd/tpa-corpus build|verify|validate` (policy layer: licence allowlist, eval pairs); `scan-eval --gate` scores the bundle via `scanner.LoadBundleCheck` (`--bundle`, default embedded) with `categoryCheck` per class and a deterministic negative control (TPA-2026-0044, `cmd/scan-eval/testdata/bundle_control_corpus.json`); eval pairs extend `detect_corpus_v1.json` in place. Docs: `docs/features/tpa-signatures.md`. **No new dependencies.**
- quarantine-approval-pilot: Go 1.26, tests + CI only — independent policy model `internal/qapilot` (no production imports), deterministic scenario inventory S01–S26 + 896-cell dispatch table + REST authz table + counting-upstream dispatch check, check ledger (`QAPILOT_REPORT_DIR`) gated by `go run ./cmd/qapilot report`, versioned mutation set `tests/qapilot/mutations.json` run by `qapilot mutate` in disposable worktrees; workflow `.github/workflows/quarantine-pilot.yml` (3 PR checks + scheduled rapid exploration). Docs: `docs/development/quarantine-approval-pilot.md`. **No new dependencies.**
- 117-review-bound-approvals PR-3 (#1565): Go 1.26, backend-only — every upstream `tools/list` carries a `managed.InventoryCapture{Seq, Epoch, Client}` (process-global sequence stamped by the flight leader; `ListToolsCaptured`/`ListToolsFreshCaptured`; `Client.Retire`/`CaptureIsCurrent`; the manager retires replaced/removed clients; `Manager.DiscoverToolsReportCaptured` gives the sweep one capture per server). The runtime admits each inventory under the server's approval lock (`admitInventoryHeld`, per-server `applied`/`definitions` in `toolApprovalServerLock`): an older, retired or reconnected capture writes nothing (records, index, last-good snapshot + its capture, StateView); a lenient empty result is an observation fence; the approval reindex/sweep fallback re-apply the snapshot only while its capture is current, else re-list/skip. Docs: `docs/features/security-quarantine.md` "Tool lists are applied in the order they were captured". **No new dependencies.**
- 116-config-commit-consistency PR2: Go 1.26 + a small Web UI change — removal purges the whole name-keyed purge set in the commit's bbolt transaction (`internal/storage/purge.go`, exact OAuth key match), in-memory purge, incarnation-bound approval writers (`internal/runtime/approval_incarnation.go`, REST `expected_incarnation` → 409 `server_replaced`), incarnation-conditional scan/identity/history writes. **No new dependencies.**
Expand Down
10 changes: 4 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,13 @@ graph LR
tpa_db_format --> tpa_db_refresh

classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff;
classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff;
class tpa_db_format,tpa_db_refresh done;
class tpa_db_corpus todo;
class tpa_db_format,tpa_db_corpus,tpa_db_refresh done;
```

| Task | Status | Refs |
| --- | --- | --- |
| Signature DB format + loader (versioned, signed, bundled default) | 🟢 Done | #1701 |
| Seed corpus: catalog known public TPA campaigns/patterns into the DB | ⚪ Todo | — |
| Seed corpus: catalog known public TPA campaigns/patterns into the DB | 🟢 Done | #1702 |
| Out-of-band refresh (offline-friendly: manual file drop + optional fetch), eval-gated | 🟢 Done | #1720 |

</details>
Expand Down Expand Up @@ -1008,7 +1006,7 @@ graph LR
| Web UI + macOS app UX audit | In progress | P0 | — | | |
| Release qualification gate (auto-QA matrix blocks the tag) | In progress | P0 | 4/4 (100%) | [081-release-qa-gate](./specs/081-release-qa-gate/) | |
| Action log / transparency — info at a glance | In progress | P1 | — | | |
| tpa-db: versioned TPA signature database for the offline scanner | In progress | P1 | 81/81 (100%) | [101-tpa-db](./specs/101-tpa-db/) | |
| tpa-db: versioned TPA signature database for the offline scanner | In progress | P1 | 117/117 (100%) | [101-tpa-db](./specs/101-tpa-db/) | |
| Token-efficiency benchmark: measured savings, published results | In progress | P1 | 62/64 (97%) | [103-token-bench](./specs/103-token-bench/) | |
| Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | P1 | — | | |
| Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | |
Expand Down Expand Up @@ -1158,7 +1156,7 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [098-tools-preflight](./specs/098-tools-preflight/) | `in-flight` | 28/33 (85%) |
| [099-describe-check-mode](./specs/099-describe-check-mode/) | `in-flight` | 9/10 (90%) |
| [100-prompt-rugpull-baseline](./specs/100-prompt-rugpull-baseline/) | — | — |
| [101-tpa-db](./specs/101-tpa-db/) | `shipped` | 81/81 (100%) |
| [101-tpa-db](./specs/101-tpa-db/) | `shipped` | 117/117 (100%) |
| [102-schema-deferred](./specs/102-schema-deferred/) | `shipped` | 89/89 (100%) |
| [103-token-bench](./specs/103-token-bench/) | `shipped` | 62/64 (97%) |
| [104-auto-routing-mode](./specs/104-auto-routing-mode/) | — | — |
Expand Down
178 changes: 178 additions & 0 deletions cmd/scan-eval/bundle_control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/smart-mcp-proxy/mcpproxy-go/internal/security/detect"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/security/scanner"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/security/tpacorpus"
)

// controlID is the D9 negative control (Spec 101 D-US2-7, FR-016b): the one
// signature whose sample no built-in hard check flags, so removing it from the
// bundle must breach the gate.
const controlID = "TPA-2026-0044"

const (
controlCorpusPath = "testdata/bundle_control_corpus.json"
controlSignaturesFS = "../../internal/security/tpacorpus/signatures"
)

// scanEntry scans one entry (plus its peers) with the given checks and returns
// the finding for the entry's own tool, if any.
func scanEntry(checkList []detect.Check, e gateEntry) (detect.Finding, bool) {
engine := detect.NewEngine(detect.Options{Checks: checkList})
views := []detect.ToolView{toGateView(e.Server, e.Tool)}
for _, p := range e.Peers {
views = append(views, toGateView(p.Server, p.Tool))
}
res := engine.Scan(detect.NewRegistryView(views))
want := e.Server + ":" + e.Tool.Name
for _, f := range res.Findings {
if f.Location == want {
return f, true
}
}
return detect.Finding{}, false
}

func tpaSignalIDs(f detect.Finding) []string {
var ids []string
for _, s := range f.Signals {
if strings.HasPrefix(s, "tpa.") {
ids = append(ids, s)
}
}
return ids
}

// TestBundleControl_NoOverlap proves the control is a real control: the gate
// built-ins alone detect nothing on its sample, and with the full embedded
// bundle the only tpa. signal is the control's own, with no hard negative
// flagged.
func TestBundleControl_NoOverlap(t *testing.T) {
gc, err := loadGateCorpus(controlCorpusPath)
if err != nil {
t.Fatal(err)
}

m := evaluateGateCorpus(gc, builtinGateChecks())
// Without the bundle no tpa_* category is gated, but Detected still counts
// what the engine flagged.
for _, c := range m.Categories {
if c.Category == "tpa_persistence_implant" && c.Detected != 0 {
t.Errorf("built-ins alone detected %d control sample(s); the control must be invisible to them", c.Detected)
}
}

bundle, err := loadGateBundle("")
if err != nil {
t.Fatal(err)
}
full := gateChecks(bundle.check)

var sawControl bool
for _, e := range gc.Entries {
if e.Label == "malicious" {
if _, hit := scanEntry(builtinGateChecks(), e); hit {
t.Errorf("%s: a built-in check produced a finding; the control must not overlap them", e.ID)
}
f, ok := scanEntry(full, e)
if !ok || f.ThreatLevel != detect.ThreatLevelDangerous {
t.Fatalf("%s: full bundle did not hard-flag the control sample (finding=%v ok=%v)", e.ID, f, ok)
}
ids := tpaSignalIDs(f)
if len(ids) == 0 {
t.Fatalf("%s: no tpa. signal in %v", e.ID, f.Signals)
}
fams := map[string]bool{}
for _, id := range ids {
parts := strings.Split(id, ".")
if len(parts) < 3 {
t.Fatalf("unexpected signal id %q", id)
}
fams[parts[1]] = true
}
if len(fams) != 1 || !fams[controlID] {
t.Errorf("%s: tpa. id families = %v, want exactly {%s}", e.ID, fams, controlID)
}
sawControl = true
continue
}
if f, ok := scanEntry(full, e); ok && f.ThreatLevel == detect.ThreatLevelDangerous {
t.Errorf("%s: benign/hard-negative entry hard-flagged by the full bundle: %v", e.ID, f.Signals)
}
}
if !sawControl {
t.Fatal("control corpus has no malicious entry")
}
}

// recallOf parses overall_recall and gated_malicious out of the gate's metrics JSON.
func recallOf(t *testing.T, stdout []byte) (recall float64, gated int) {
t.Helper()
var m struct {
OverallRecall float64 `json:"overall_recall"`
GatedMalicious int `json:"gated_malicious"`
}
if err := json.Unmarshal(stdout, &m); err != nil {
t.Fatalf("stdout is not metrics JSON: %v\n%s", err, stdout)
}
return m.OverallRecall, m.GatedMalicious
}

// TestBundleControl_RemovalBreachesGate is the non-vacuity proof (FR-016b): the
// control corpus scores recall 1.0 against the full bundle and 0.0 against the
// bundle built without the control, and the second run exits exactly
// exitGateBreach (a recall breach), never exitConfigError (a load failure).
func TestBundleControl_RemovalBreachesGate(t *testing.T) {
dir := t.TempDir()
fullPath := filepath.Join(dir, "full.json")
if err := os.WriteFile(fullPath, scanner.EmbeddedBundle(), 0o644); err != nil {
t.Fatal(err)
}
reduced, err := tpacorpus.Build(os.DirFS(controlSignaturesFS), tpacorpus.Options{Exclude: []string{controlID}})
if err != nil {
t.Fatalf("building the reduced bundle: %v", err)
}
// The reduced bundle must still load, so exit 6 is a recall breach and not
// a load failure.
if _, _, err := scanner.LoadBundleCheck(reduced); err != nil {
t.Fatalf("reduced bundle does not load: %v", err)
}
if bytes.Contains(reduced, []byte(controlID)) {
t.Fatalf("reduced bundle still mentions %s", controlID)
}
reducedPath := filepath.Join(dir, "reduced.json")
if err := os.WriteFile(reducedPath, reduced, 0o644); err != nil {
t.Fatal(err)
}

runControl := func(bundle string) (int, float64, int, string) {
var out, errBuf bytes.Buffer
code := run([]string{"--corpus", controlCorpusPath, "--bundle", bundle, "--gate", "--min-recall", "0.90", "--max-fp", "0.05"}, &out, &errBuf)
r, g := recallOf(t, out.Bytes())
return code, r, g, errBuf.String()
}

code, recall, gated, stderr := runControl(fullPath)
if code != exitOK {
t.Fatalf("full bundle: exit=%d want %d stderr=%s", code, exitOK, stderr)
}
if recall != 1.0 || gated != 1 {
t.Errorf("full bundle: recall=%v gated_malicious=%d, want 1.0 and 1", recall, gated)
}

code, recall, gated, stderr = runControl(reducedPath)
if code != exitGateBreach {
t.Fatalf("reduced bundle: exit=%d want exactly %d (gate breach, not config error %d) stderr=%s", code, exitGateBreach, exitConfigError, stderr)
}
if recall != 0.0 || gated != 1 {
t.Errorf("reduced bundle: recall=%v gated_malicious=%d, want 0.0 and 1", recall, gated)
}
}
Loading
Loading