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.
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
2 changes: 2 additions & 0 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 @@ -147,6 +148,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
37 changes: 20 additions & 17 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 Down Expand Up @@ -127,6 +128,7 @@ func (s *Server) resolveReview(w http.ResponseWriter, r *http.Request, status st
EventId: row.EventID,
SessionId: row.SessionID,
MadCode: row.MADCode,
Status: pb.VerdictStatus_VERDICT_STATUS_OK,
Policy: s.policySnapshotProto(pol),
Hitl: &pb.HitlResponse{ContinueExecution: continueExec},
}},
Expand All @@ -148,14 +150,15 @@ 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")
Expand Down
54 changes: 53 additions & 1 deletion backend/internal/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,15 @@ func TestPolicyGetAndUpdate(t *testing.T) {
if body["data"].(map[string]any)["mode"] != "alert" {
t.Errorf("default mode = %v, want alert", body["data"].(map[string]any)["mode"])
}
if body["data"].(map[string]any)["fail_closed_on_classifier_error"] != false {
t.Errorf("default fail_closed_on_classifier_error = %v, want false",
body["data"].(map[string]any)["fail_closed_on_classifier_error"])
}

// PUT
resp = doJSON(t, srv, cookie, http.MethodPut, "/api/settings/policy", map[string]any{
"mode": "hitl",
"mode": "hitl",
"fail_closed_on_classifier_error": true,
})
if resp.StatusCode != http.StatusOK {
t.Fatalf("PUT status = %d, want 200", resp.StatusCode)
Expand All @@ -301,6 +306,10 @@ func TestPolicyGetAndUpdate(t *testing.T) {
if body["data"].(map[string]any)["mode"] != "hitl" {
t.Errorf("post-PUT mode = %v, want hitl", body["data"].(map[string]any)["mode"])
}
if body["data"].(map[string]any)["fail_closed_on_classifier_error"] != true {
t.Errorf("post-PUT fail_closed_on_classifier_error = %v, want true",
body["data"].(map[string]any)["fail_closed_on_classifier_error"])
}
}

func TestPolicyInvalidMode(t *testing.T) {
Expand Down Expand Up @@ -478,6 +487,44 @@ func TestStatsActivityEmpty(t *testing.T) {
}
}

// -----------------------------------------------------------------
// Verdicts
// -----------------------------------------------------------------

func TestListVerdictsIncludesStatusAndFiltersError(t *testing.T) {
srv, db, _, cookie := newTestServerLoggedIn(t)

eventID := uuid.NewString()
if _, err := db.Exec(
`INSERT INTO events (id, session_id, agent_id, event_type, run_id, payload)
VALUES (?, 'sess-verdicts', 'agent-v', 'tool', 'r1', '{}')`,
eventID,
); err != nil {
t.Fatalf("seed event: %v", err)
}
if _, err := db.Exec(
`INSERT INTO verdicts (id, event_id, session_id, mad_code, classification, verdict_status, reasoning)
VALUES (?, ?, 'sess-verdicts', '', 'error', 'error', 'classifier failed')`,
uuid.NewString(), eventID,
); err != nil {
t.Fatalf("seed verdict: %v", err)
}

resp := getReq(t, srv, cookie, "/api/verdicts?classification=error&verdict_status=error")
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
data := decodeBody(t, resp)["data"].(map[string]any)
if int(data["total"].(float64)) != 1 {
t.Fatalf("total = %v, want 1", data["total"])
}
verdicts := data["verdicts"].([]any)
row := verdicts[0].(map[string]any)
if row["classification"] != "error" || row["verdict_status"] != "error" {
t.Errorf("verdict row = %v, want classification/status error", row)
}
}

// -----------------------------------------------------------------
// Reviews / HITL
// -----------------------------------------------------------------
Expand Down Expand Up @@ -915,6 +962,9 @@ func TestSessionTimeline(t *testing.T) {
if verdict["mad_code"] != "M3" {
t.Errorf("verdict.mad_code = %v, want M3", verdict["mad_code"])
}
if verdict["verdict_status"] != "ok" {
t.Errorf("verdict.verdict_status = %v, want ok", verdict["verdict_status"])
}
}

// -----------------------------------------------------------------
Expand Down Expand Up @@ -1291,6 +1341,7 @@ CREATE TABLE policies (
policy_m2 INTEGER NOT NULL DEFAULT 0,
policy_m3 INTEGER NOT NULL DEFAULT 1,
policy_m4 INTEGER NOT NULL DEFAULT 1,
fail_closed_on_classifier_error INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
INSERT INTO policies (id) VALUES (1);
Expand All @@ -1312,6 +1363,7 @@ CREATE TABLE verdicts (
agent_profile_id TEXT,
mad_code TEXT NOT NULL,
classification TEXT NOT NULL,
verdict_status TEXT NOT NULL DEFAULT 'ok',
reasoning TEXT,
latency_ms INTEGER,
tokens_used INTEGER NOT NULL DEFAULT 0,
Expand Down
27 changes: 27 additions & 0 deletions backend/internal/api/handlers_verdicts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type verdictResponse struct {
SessionID string `json:"session_id"`
MADCode string `json:"mad_code"`
Classification string `json:"classification"`
VerdictStatus string `json:"verdict_status"`
LatencyMS *int64 `json:"latency_ms,omitempty"`
TokensUsed int32 `json:"tokens_used"`
CreatedAt string `json:"created_at"`
Expand All @@ -38,10 +39,19 @@ func (s *Server) handleListVerdicts(w http.ResponseWriter, r *http.Request) {
since = t
}
}
if c := q.Get("classification"); c != "" && !validVerdictClassification(c) {
writeError(w, http.StatusBadRequest, "invalid classification")
return
}
if status := q.Get("verdict_status"); status != "" && !validVerdictStatus(status) {
writeError(w, http.StatusBadRequest, "invalid verdict_status")
return
}
filters := store.VerdictFilters{
Since: since,
Classification: q.Get("classification"),
MADCode: q.Get("mad_code"),
VerdictStatus: q.Get("verdict_status"),
}
rows, total, err := s.store.ListVerdicts(r.Context(), filters, pg.PerPage, pg.Offset)
if err != nil {
Expand All @@ -67,8 +77,25 @@ func verdictRowToResponse(r *store.VerdictListRow) verdictResponse {
SessionID: r.SessionID,
MADCode: r.MADCode,
Classification: r.Classification,
VerdictStatus: r.VerdictStatus,
LatencyMS: r.LatencyMS,
TokensUsed: r.TokensUsed,
CreatedAt: r.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
}
}

func validVerdictClassification(c string) bool {
switch c {
case "benign", "notify", "block", "error":
return true
}
return false
}

func validVerdictStatus(s string) bool {
switch s {
case "ok", "error":
return true
}
return false
}
9 changes: 5 additions & 4 deletions backend/internal/db/db.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 SecureAgentics

// Package db opens the SQLite database, applies idempotent migrations,
// and exposes the *sql.DB handle to the rest of the backend.
// Package db opens the SQLite database, applies pending ledger-tracked
// migrations, and exposes the *sql.DB handle to the rest of the backend.
package db

import (
Expand All @@ -16,8 +16,9 @@ import (
)

// Open opens the SQLite database at path, applies the WAL / FK
// pragmas, and runs every embedded migration in lexical order.
// Migrations are idempotent so re-running on each startup is safe.
// pragmas, and runs each pending embedded migration in lexical order.
// Applied migrations are recorded in schema_migrations so startup can
// safely skip files that already ran.
func Open(path string) (*sql.DB, error) {
conn, err := sql.Open("sqlite", path)
if err != nil {
Expand Down
Loading