Skip to content

feat(daemon): explicit classify/abstain signal on /v1/infer (#131)#36

Merged
thebenignhacker merged 1 commit into
mainfrom
fix/daemon-abstain-contract-131-stage1
Jun 22, 2026
Merged

feat(daemon): explicit classify/abstain signal on /v1/infer (#131)#36
thebenignhacker merged 1 commit into
mainfrom
fix/daemon-abstain-contract-131-stage1

Conversation

@thebenignhacker

Copy link
Copy Markdown
Contributor

Summary

/v1/infer now emits an explicit classification field ("classified" | "abstain") alongside attackClass, so a consumer can distinguish a confident benign from "the model could not produce a usable verdict". Until now both carried attackClass: "" and were indistinguishable, so the downstream FGA Step 5 intent check failed silently open (issue #131).

What changed

  • New InferResponse.classification: "classified" | "abstain".
    • "classified": the model produced a usable verdict (benign OR an attack class) at or above ABSTAIN_CONFIDENCE_FLOOR. attackClass is authoritative ("" is a confident benign).
    • "abstain": the model could not produce a usable verdict — inference threw, the engine reported no usable confidence (missing/NaN), or the predicted-class confidence was below the floor. attackClass is forced to "" so a low-confidence guess is never actionable; the raw guess is preserved in evidence. A non-classifying engine abstains rather than masquerading as a confident benign.
  • New exported ABSTAIN_CONFIDENCE_FLOOR = 0.5 — a conservative heuristic, not a calibrated value. The v0.5.0 classifier saturates confidence near 1.0, so this rarely trips today; the dominant fix is the explicit field. Calibration is a separate follow-up.
  • The HTTP 500 error body also carries classification: "abstain" for consumers that read the body without checking the status code.

Compatibility

Additive and forward/backward compatible. Older consumers ignore the field and still get a deterministic attackClass ("" on abstain); an older daemon that omits the field lets the consumer fall back to its legacy attackClass==""?abstain:classified heuristic. No lockstep deploy required. No model retrain.

Tests

21/21 daemon tests pass (was 18; +3): a no-confidence engine result abstains; sub-floor confidence abstains and blanks attackClass; a verdict exactly at the inclusive floor classifies through. Build clean; HMA static scan 98/100 (one pre-existing LOW: missing .gitignore in the package subdir). Bumps @nanomind/daemon 0.3.0 → 0.4.0.

The matching consumer change lands in agent-identity-management (FGA Step 5 reads classification + checks the HTTP status code).

/v1/infer now emits a `classification` field ("classified" | "abstain")
alongside attackClass, so a consumer can distinguish a confident benign
from "the model could not produce a usable verdict". Until now both
carried attackClass:"" and were indistinguishable, so FGA Step 5 failed
silently open.

- Abstain when inference throws or the predicted-class confidence is below
  ABSTAIN_CONFIDENCE_FLOOR (0.5, a conservative heuristic pending
  selective-risk calibration). On abstain, attackClass is forced to "" so a
  low-confidence guess is never actionable; the raw guess is kept in evidence.
- The HTTP 500 error body also carries classification:"abstain".
- Additive and backward/forward compatible: older consumers ignore the
  field; an older daemon lets the consumer fall back to its legacy
  attackClass==""?abstain:classified heuristic. No lockstep deploy required.
- 20/20 daemon tests pass (+2). Bump @nanomind/daemon 0.3.0 -> 0.4.0.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Security Review: PR #36 — Explicit classify/abstain signal on /v1/infer

VERDICT: APPROVE

SUMMARY:

This PR introduces an explicit classification field ("classified" | "abstain") to the /v1/infer response contract, addressing issue #131 where confident benign verdicts were indistinguishable from "model could not answer" cases (both produced attackClass: ""). The change is additive and backward-compatible: older consumers continue to work with the existing attackClass behavior, while new consumers can read the explicit signal. The implementation includes a conservative ABSTAIN_CONFIDENCE_FLOOR = 0.5 threshold, below which verdicts are marked as abstain with attackClass forced to "" to prevent low-confidence guesses from being actionable. Three new tests verify the abstain logic (no-confidence engine, sub-floor confidence, at-floor boundary). The change is contract/fallback only with no model retrain required. All mitigations are properly implemented: the confidence floor check includes NaN handling, the HTTP 500 error path carries the abstain signal, and the raw low-confidence guess is preserved in evidence for audit. No security, correctness, or publish-surface regressions detected.

FINDINGS:

None. All potential concerns were verified and found to have adequate mitigations in place:

  • ✅ The hasConfidence check correctly handles both missing and NaN values (line 306-307 in server.ts)
  • ✅ The confidence floor comparison is inclusive (>=) as intended, with a boundary test confirming the behavior
  • ✅ The HTTP 500 error path includes classification: "abstain" (line 343 in server.ts)
  • ✅ Low-confidence verdicts preserve the raw guess in evidence for audit (line 324 in server.ts)
  • ✅ The change is additive; no breaking changes to the wire contract

Reviewed 7 files changed (28985 bytes)

@thebenignhacker thebenignhacker merged commit 567469a into main Jun 22, 2026
3 checks passed
@thebenignhacker thebenignhacker deleted the fix/daemon-abstain-contract-131-stage1 branch June 22, 2026 19:45
thebenignhacker added a commit that referenced this pull request Jun 22, 2026
#36) (#37)

/v1/infer now emits a `classification` field ("classified" | "abstain")
alongside attackClass, so a consumer can distinguish a confident benign
from "the model could not produce a usable verdict". Until now both
carried attackClass:"" and were indistinguishable, so FGA Step 5 failed
silently open.

- Abstain when inference throws or the predicted-class confidence is below
  ABSTAIN_CONFIDENCE_FLOOR (0.5, a conservative heuristic pending
  selective-risk calibration). On abstain, attackClass is forced to "" so a
  low-confidence guess is never actionable; the raw guess is kept in evidence.
- The HTTP 500 error body also carries classification:"abstain".
- Additive and backward/forward compatible: older consumers ignore the
  field; an older daemon lets the consumer fall back to its legacy
  attackClass==""?abstain:classified heuristic. No lockstep deploy required.
- 20/20 daemon tests pass (+2). Bump @nanomind/daemon 0.3.0 -> 0.4.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant