@startuml
!theme plain
title Turnstone — Intent Validation (Judge) Architecture

skinparam participant {
    BackgroundColor<<session>> #C8E6C9
    BackgroundColor<<judge>> #FFE0B2
    BackgroundColor<<storage>> #B3E5FC
    BackgroundColor<<ui>> #E8EAF6
    BackgroundColor<<fs>> #F5F5F5
}

participant "ChatSession\n(session.py)" as Session <<session>>
participant "IntentJudge\n(judge.py)" as Judge <<judge>>
participant "LLM Provider\n(provider)" as LLM <<judge>>
participant "StorageBackend\n(SQLite)" as Storage <<storage>>
participant "WebUI / SSE\n(server.py)" as UI <<ui>>
participant "Filesystem" as FS <<fs>>

== Tool Call Requires Approval ==

Session -> Session : _prepare_tool_calls()
note right
  Tool calls parsed from
  LLM response. Auto-approved
  tools dispatched immediately.
  Remaining items need approval.
end note

Session -> Session : _evaluate_intent(pending_items)

== Tier 1: Heuristic (synchronous, sub-ms) ==

Session -> Judge : evaluate(items, messages, callback)

Judge -> Judge : evaluate_heuristic()\nfor each item
note right
  **36 rules (first match wins):**
  Critical (0.90, deny): rm /, mkfs,
    dd, pipe-to-shell, chmod 777 /,
    write/edit /etc/ .ssh/,
    download-then-execute chains
  High (0.80, review): sudo, kill -9,
    destructive git, DROP TABLE,
    secrets, HTTP mutations, ssh/scp,
    browser+data-export, transitive
    install, control-plane mutation
  Medium (0.70, review): content
    ingestion, interpreter exec,
    cloud CLI mutations, pkg install,
    write_file, MCP tools, docker ops
  Low (0.85, approve): read_file,
    list_directory, search, recall,
    tool_search, read_resource,
    web_search, read-only bash
  Default: medium, 0.50, review
end note

Judge --> Session : heuristic_verdicts[]

Session -> Session : attach _heuristic_verdict\nto each pending item

Session -> UI : SSE: approve_request\n{items: [{verdict: ...}],\n judge_pending: true}
note right
  Heuristic verdict displayed
  immediately as risk badge.
  Spinner shown while LLM
  judge evaluates.
end note

Session -> Storage : create_intent_verdict()\nfor each heuristic verdict

== Tier 2: LLM Judge (daemon thread, async) ==

Judge -> Judge : spawn daemon thread\n"intent-judge"

note over Judge, LLM
  **Context preparation:**
  1. FIFO-truncate conversation history
     to max_context_ratio of context window
  2. Append tool call details as user message
  3. System prompt defines judge role + JSON schema
end note

loop up to 3 turns (timeout budget)

    Judge -> LLM : create_completion(\nmodel, judge_messages,\ntools=[read_file, list_directory])
    LLM --> Judge : CompletionResult

    alt tool_calls present (turn < 3)
        Judge -> Judge : _exec_read_only_tool()
        note right
          **Security hardening:**
          Blocked: /etc/, /root/,
          /proc/, /sys/, /dev/,
          .ssh, .gnupg, .aws,
          *.pem, *.key, *.p12
          File cap: 32KB
          Dir cap: 200 entries
        end note
        Judge -> FS : read_file / list_directory
        FS --> Judge : file contents
        Judge -> Judge : append tool result\nto judge_messages
    else text response (final verdict)
        Judge -> Judge : _parse_verdict()
        note right
          **4-stage JSON parsing:**
          1. Direct JSON.loads
          2. Markdown code block
          3. Brace-counting
          4. Regex field extraction
        end note
    end

end

== Tier 3: Arbitration ==

Judge -> Judge : compare confidence:\nLLM vs heuristic
note right
  Only deliver LLM verdict
  if confidence > heuristic.
  Otherwise heuristic stands.
end note

alt LLM confidence > heuristic confidence
    Judge -> Session : callback(llm_verdict)
    Session -> UI : SSE: intent_verdict\n{tier: "llm", ...}
    note right
      UI replaces heuristic badge
      with LLM verdict. Spinner
      resolves to final assessment.
    end note
    Session -> Storage : create_intent_verdict()\nfor LLM verdict
end

== User Decision ==

UI -> Session : resolve_approval(\napproved, feedback)

Session -> Storage : update_intent_verdict(\nverdict_id, user_decision)
note right
  All tracked verdicts
  (heuristic + LLM) updated
  with "approved" or "denied".
  Swap-and-clear avoids racing
  with daemon judge thread.
end note

== Tool Execution ==

Session -> Session : _execute_tools()
note right
  Tools execute with
  user approval.
end note

== Output Guard (synchronous, time-budgeted) ==

Session -> Session : _evaluate_output()\nfor each tool result
note right
  **Priority-ordered checks (5s budget):**
  P1: Prompt injection (role injection,
    override phrases, instruction tags)
  P2: Credential leakage (API keys,
    PEM blocks, connection strings)
  P3: Encoded payloads (data URIs,
    hex shellcode)
  P4: Adversarial URLs (cloud metadata,
    credential query params)
  P5: System info disclosure (private
    IPs, sensitive paths)

  Annotates + optionally redacts.
  Does NOT gate.
end note

alt output_warning flags detected
    Session -> UI : SSE: output_warning\n{call_id, risk_level, flags,\nfunc_name, redacted}
    note right
      Credential values replaced
      with [REDACTED:<type>] before
      output enters conversation.
      sanitized text excluded from
      SSE payload (defense in depth).
    end note
    UI -> Storage : record_output_assessment()\nfire-and-forget persistence
    note right
      Stored: flags, risk_level,
      annotations, output_length,
      redacted (bool). Raw tool
      output is never stored.
    end note
end

== Lifecycle ==

note over Session, Judge
  **Lazy initialization:**
  IntentJudge created on first approval if judge_config.enabled.
  Re-uses session's provider/client by default (self-consistency).
  Cross-model: separate provider/client from [judge] config.

  **Sub-agent exemption:**
  Plan agent and task agent skip intent validation entirely.

  **Output guard:**
  Runs when judge_config.output_guard is true (default).
  Credential redaction when judge_config.redact_secrets is true.

  **Storage:**
  intent_verdicts table (migration 012), output_assessments table
  (migration 022). Both queryable via admin API endpoints
  (requires admin.judge permission). Skills store risk_level,
  scan_report, scan_version for install-time risk assessment.
end note

@enduml
