Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ models/*
!models/.gitkeep

# Go
.tools/
*.exe
*.exe~
*.dll
Expand Down
1 change: 1 addition & 0 deletions CLA.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ To accept this Agreement, open a pull request that adds an entry to the table be
| _example placeholder_ | _@example_ | _2026-01-01_ |
| Dhrit Timinkumar Patel | @d180 | 2026-05-20 |
| Adarsh Tiwari | @adarsh9977 | 2026-05-22 |
| Muhammad usman | @Muhammad-usman92 | 2026-06-11 |

Once a CLA-bot (cla-assistant.io or equivalent) is wired up, this manual table will be replaced by the bot's status check on each pull request. Existing signatures in this table remain valid; the bot reads from a separate signers list.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ Adrian supports entirely offline, data sovereign deployments using just a handfu

Use the same `adrian.init` snippet as in the [Quickstart](#quickstart) above. The SDK defaults to `ws://localhost:8080/ws`, so a self-hosted setup needs nothing more than the API key - drop the `ws_url=` line.

### Classifier error policy

Adrian records classifier outages, malformed classifier responses, and unparseable classifier output as `verdict_status=error` with `mad_code=""`. These are operational classifier errors, not benign `M0` findings and not synthetic malicious activity.

The default policy remains availability-first: classifier errors fail open. In **Settings -> Policy**, enable **Fail closed on classifier error** to make BLOCK-mode tool calls return blocked responses when the classifier cannot produce a verdict. In HITL mode, actionable classifier errors are sent to the review queue and held until an operator approves or rejects them.

Fail-closed classifier-error enforcement requires the Python SDK version shipped with this repository update. Older SDKs ignore the additive protobuf `status` and policy fields, see an empty MAD code, and continue fail-open even when the dashboard toggle is enabled.

To [reset the admin password](https://docs.adrian.secureagentics.ai/reference/backend#reset-the-admin-password), [change the model](https://docs.adrian.secureagentics.ai/reference/backend#switch-the-local-gguf) and much more check out the dedicated [Docs site](https://docs.adrian.secureagentics.ai/).

## Why Adrian is different
Expand Down
4 changes: 2 additions & 2 deletions backend/cmd/adrian/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

// Adrian backend entrypoint.
//
// Loads config, opens the SQLite database (running idempotent
// migrations), constructs the API server with the LLM-backed
// Loads config, opens the SQLite database (running pending
// ledger-tracked migrations), constructs the API server with the LLM-backed
// classifier, and listens on ADRIAN_BACKEND_PORT until SIGTERM.
package main

Expand Down
17 changes: 12 additions & 5 deletions backend/internal/api/handlers_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type timelineVerdict struct {
ID string `json:"id"`
MADCode string `json:"mad_code"`
Classification string `json:"classification"`
VerdictStatus string `json:"verdict_status"`
}

type timelineEntry struct {
Expand Down Expand Up @@ -71,13 +72,18 @@ func (s *Server) handleListEvents(w http.ResponseWriter, r *http.Request) {
since = t
}
}
if status := q.Get("verdict_status"); status != "" && !validVerdictStatus(status) {
writeError(w, http.StatusBadRequest, "invalid verdict_status")
return
}

filters := store.EventFilters{
Since: since,
AgentID: q.Get("agent_id"),
SessionID: q.Get("session_id"),
EventType: q.Get("event_type"),
MinMAD: q.Get("min_mad"),
Since: since,
AgentID: q.Get("agent_id"),
SessionID: q.Get("session_id"),
EventType: q.Get("event_type"),
MinMAD: q.Get("min_mad"),
VerdictStatus: q.Get("verdict_status"),
}

rows, total, err := s.store.ListEvents(r.Context(), filters, pg.PerPage, pg.Offset)
Expand Down Expand Up @@ -147,6 +153,7 @@ func (s *Server) handleSessionTimeline(w http.ResponseWriter, r *http.Request) {
ID: row.VerdictID,
MADCode: row.MADCode,
Classification: row.Classification,
VerdictStatus: row.VerdictStatus,
}
}
resp.Entries = append(resp.Entries, entry)
Expand Down
51 changes: 29 additions & 22 deletions backend/internal/api/handlers_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import (
)

type policyResponse struct {
Mode string `json:"mode"`
PolicyM0 bool `json:"policy_m0"`
PolicyM2 bool `json:"policy_m2"`
PolicyM3 bool `json:"policy_m3"`
PolicyM4 bool `json:"policy_m4"`
UpdatedAt string `json:"updated_at"`
Mode string `json:"mode"`
PolicyM0 bool `json:"policy_m0"`
PolicyM2 bool `json:"policy_m2"`
PolicyM3 bool `json:"policy_m3"`
PolicyM4 bool `json:"policy_m4"`
FailClosedOnClassifierError bool `json:"fail_closed_on_classifier_error"`
UpdatedAt string `json:"updated_at"`
}

type policyPatchRequest struct {
Mode *string `json:"mode"`
PolicyM0 *bool `json:"policy_m0"`
PolicyM2 *bool `json:"policy_m2"`
PolicyM3 *bool `json:"policy_m3"`
PolicyM4 *bool `json:"policy_m4"`
Mode *string `json:"mode"`
PolicyM0 *bool `json:"policy_m0"`
PolicyM2 *bool `json:"policy_m2"`
PolicyM3 *bool `json:"policy_m3"`
PolicyM4 *bool `json:"policy_m4"`
FailClosedOnClassifierError *bool `json:"fail_closed_on_classifier_error"`
}

func (s *Server) handleGetPolicy(w http.ResponseWriter, r *http.Request) {
Expand All @@ -47,11 +49,12 @@ func (s *Server) handleUpdatePolicy(w http.ResponseWriter, r *http.Request) {
}

patch := &store.PolicyPatch{
Mode: req.Mode,
PolicyM0: req.PolicyM0,
PolicyM2: req.PolicyM2,
PolicyM3: req.PolicyM3,
PolicyM4: req.PolicyM4,
Mode: req.Mode,
PolicyM0: req.PolicyM0,
PolicyM2: req.PolicyM2,
PolicyM3: req.PolicyM3,
PolicyM4: req.PolicyM4,
FailClosedOnClassifierError: req.FailClosedOnClassifierError,
}
if err := s.store.UpdatePolicy(r.Context(), patch); err != nil {
writeError(w, http.StatusInternalServerError, "update failed")
Expand Down Expand Up @@ -80,19 +83,23 @@ func (s *Server) handleUpdatePolicy(w http.ResponseWriter, r *http.Request) {
if req.PolicyM4 != nil {
details["policy_m4"] = *req.PolicyM4
}
if req.FailClosedOnClassifierError != nil {
details["fail_closed_on_classifier_error"] = *req.FailClosedOnClassifierError
}
writeAuditLog(r.Context(), s.store, userID(r), "policy_updated", "policies", details)

writeJSON(w, http.StatusOK, policyResponseFromStore(pol))
}

func policyResponseFromStore(p *store.Policy) policyResponse {
return policyResponse{
Mode: p.Mode,
PolicyM0: p.PolicyM0,
PolicyM2: p.PolicyM2,
PolicyM3: p.PolicyM3,
PolicyM4: p.PolicyM4,
UpdatedAt: p.UpdatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
Mode: p.Mode,
PolicyM0: p.PolicyM0,
PolicyM2: p.PolicyM2,
PolicyM3: p.PolicyM3,
PolicyM4: p.PolicyM4,
FailClosedOnClassifierError: p.FailClosedOnClassifierError,
UpdatedAt: p.UpdatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
}
}

Expand Down
60 changes: 41 additions & 19 deletions backend/internal/api/handlers_reviews.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import (
)

type reviewSummary struct {
ID string `json:"id"`
EventID string `json:"event_id"`
VerdictID string `json:"verdict_id"`
SessionID string `json:"session_id"`
MADCode string `json:"mad_code"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ReviewedBy string `json:"reviewed_by,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty"`
ID string `json:"id"`
EventID string `json:"event_id"`
VerdictID string `json:"verdict_id"`
SessionID string `json:"session_id"`
MADCode string `json:"mad_code"`
VerdictStatus string `json:"verdict_status"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ReviewedBy string `json:"reviewed_by,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty"`
}

type reviewListResponse struct {
Expand All @@ -38,6 +39,7 @@ type reviewDetail struct {
reviewSummary
EventPayload json.RawMessage `json:"event_payload,omitempty"`
Classification string `json:"classification,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
}

type reviewResolveResponse struct {
Expand All @@ -48,8 +50,14 @@ type reviewResolveResponse struct {

func (s *Server) handleListReviews(w http.ResponseWriter, r *http.Request) {
pg := parsePagination(r)
status := r.URL.Query().Get("status")
rows, total, err := s.store.ListHitlQueue(r.Context(), status, pg.PerPage, pg.Offset)
q := r.URL.Query()
status := q.Get("status")
verdictStatus := q.Get("verdict_status")
if verdictStatus != "" && !validVerdictStatus(verdictStatus) {
writeError(w, http.StatusBadRequest, "invalid verdict_status")
return
}
rows, total, err := s.store.ListHitlQueue(r.Context(), status, verdictStatus, pg.PerPage, pg.Offset)
if err != nil {
writeError(w, http.StatusInternalServerError, "query failed")
return
Expand Down Expand Up @@ -80,6 +88,7 @@ func (s *Server) handleGetReview(w http.ResponseWriter, r *http.Request) {
resp := reviewDetail{
reviewSummary: reviewToSummary(&row.HitlReview),
Classification: row.Classification,
Reasoning: row.Reasoning,
}
if row.EventPayloadJSON != "" {
resp.EventPayload = json.RawMessage(row.EventPayloadJSON)
Expand Down Expand Up @@ -127,6 +136,7 @@ func (s *Server) resolveReview(w http.ResponseWriter, r *http.Request, status st
EventId: row.EventID,
SessionId: row.SessionID,
MadCode: row.MADCode,
Status: reviewVerdictStatusProto(row.VerdictStatus),
Policy: s.policySnapshotProto(pol),
Hitl: &pb.HitlResponse{ContinueExecution: continueExec},
}},
Expand All @@ -148,17 +158,29 @@ func (s *Server) resolveReview(w http.ResponseWriter, r *http.Request, status st

func reviewToSummary(r *store.HitlReview) reviewSummary {
out := reviewSummary{
ID: r.ID,
EventID: r.EventID,
VerdictID: r.VerdictID,
SessionID: r.SessionID,
MADCode: r.MADCode,
Status: r.Status,
CreatedAt: r.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
ReviewedBy: r.ReviewedBy,
ID: r.ID,
EventID: r.EventID,
VerdictID: r.VerdictID,
SessionID: r.SessionID,
MADCode: r.MADCode,
VerdictStatus: r.VerdictStatus,
Status: r.Status,
CreatedAt: r.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
ReviewedBy: r.ReviewedBy,
}
if !r.ReviewedAt.IsZero() {
out.ReviewedAt = r.ReviewedAt.UTC().Format("2006-01-02T15:04:05.000Z")
}
return out
}

func reviewVerdictStatusProto(status string) pb.VerdictStatus {
switch status {
case "error":
return pb.VerdictStatus_VERDICT_STATUS_ERROR
case "ok":
return pb.VerdictStatus_VERDICT_STATUS_OK
default:
return pb.VerdictStatus_VERDICT_STATUS_UNSPECIFIED
}
}
26 changes: 14 additions & 12 deletions backend/internal/api/handlers_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ package api
import "net/http"

type overviewResponse struct {
TotalEvents int `json:"total_events"`
FlaggedVerdicts int `json:"flagged_verdicts"`
PendingReviews int `json:"pending_reviews"`
ActiveAgents int `json:"active_agents"`
VerdictsByMAD map[string]int `json:"verdicts_by_mad"`
Window string `json:"window"`
TotalEvents int `json:"total_events"`
FlaggedVerdicts int `json:"flagged_verdicts"`
ClassifierErrors int `json:"classifier_errors"`
PendingReviews int `json:"pending_reviews"`
ActiveAgents int `json:"active_agents"`
VerdictsByMAD map[string]int `json:"verdicts_by_mad"`
Window string `json:"window"`
}

type activityBucketEntry struct {
Expand All @@ -31,12 +32,13 @@ func (s *Server) handleStatsOverview(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusOK, overviewResponse{
TotalEvents: o.TotalEvents,
FlaggedVerdicts: o.FlaggedVerdicts,
PendingReviews: o.PendingReviews,
ActiveAgents: o.ActiveAgents,
VerdictsByMAD: o.VerdictsByMAD,
Window: "24h",
TotalEvents: o.TotalEvents,
FlaggedVerdicts: o.FlaggedVerdicts,
ClassifierErrors: o.ClassifierErrors,
PendingReviews: o.PendingReviews,
ActiveAgents: o.ActiveAgents,
VerdictsByMAD: o.VerdictsByMAD,
Window: "24h",
})
}

Expand Down
Loading