syntax = "proto3";

package assembly.audit.v1;

import "common.proto";

// AuditService handles path ③ (runtime → gateway) — the async audit trail.
// Non-critical path; runtime buffers events and flushes every few seconds.
//
// Consumed by: aa-runtime/src/gateway_client.rs (client side)
//              aa-gateway/src/audit_store.rs    (server side)
service AuditService {
  // ReportEvents is the primary upload path — batched, called every few seconds.
  rpc ReportEvents(ReportEventsRequest) returns (ReportEventsResponse);

  // StreamEvents is for high-frequency scenarios where per-event latency matters.
  // The client streams AuditEvent messages; the server acknowledges on completion.
  rpc StreamEvents(stream AuditEvent) returns (StreamEventsResponse);
}

// ── Core event ────────────────────────────────────────────────────────────────

// AuditEvent records a single governance-relevant occurrence with full context.
// Maps to aa-core/src/audit.rs AuditEvent struct.
message AuditEvent {
  // Unique identifier for this audit record (UUID v7 for sortability).
  string                      event_id       = 1;
  // Composite identity of the agent that produced this event.
  assembly.common.v1.AgentId  agent_id       = 2;
  // Wall-clock time when the event occurred, in Unix milliseconds.
  assembly.common.v1.Timestamp occurred_at   = 3;
  // High-level action category — drives storage partitioning and alert rules.
  assembly.common.v1.ActionType action_type  = 4;
  // Policy engine verdict for this action.
  assembly.common.v1.Decision  decision      = 5;
  // Distributed tracing: run-level trace ID.
  string                      trace_id       = 6;
  // Distributed tracing: action-level span ID.
  string                      span_id        = 7;
  // Distributed tracing: parent span ID (links to the calling action).
  string                      parent_span_id = 8;

  // Action-type-specific detail payload. Exactly one field must be set.
  oneof detail {
    LLMCallDetail     llm_call  = 10;
    ToolCallDetail    tool_call = 11;
    FileOpDetail      file_op   = 12;
    NetworkCallDetail network   = 13;
    ProcessExecDetail process   = 14;
    PolicyViolation   violation = 15;
    ApprovalEvent     approval  = 16;
  }

  // Arbitrary labels attached at event creation for downstream query filtering.
  map<string, string> labels = 20;

  // ── Lineage fields (AAASM-934) ──────────────────────────────────────────
  // All optional. Populated when the emitting agent registered with lineage
  // context. Absent (default empty) for legacy events and root agents.

  // Root of the delegation chain; equals agent_id for root agents.
  string root_agent_id    = 21;
  // The agent that directly spawned this one; empty for root agents.
  string parent_agent_id  = 22;
  // Team this agent belongs to; empty if not set.
  string team_id          = 23;
  // Session identifier for this agent run.
  string session_id       = 24;
  // Human-readable reason the parent delegated to this agent.
  string delegation_reason = 25;
  // Tool or framework that triggered the spawn (e.g. "langgraph.subgraph").
  string spawned_by_tool  = 26;
  // Delegation depth — 0 for root agents, +1 per level.
  uint32 depth            = 27;

  // ── Call stack (AAASM-1419) ─────────────────────────────────────────────
  // Hierarchical record of LLM/tool/result steps that led to this event.
  // Renders inline beneath an expanded Live Ops row in the dashboard.
  // Empty (default) when the producer did not record a stack.
  repeated CallStackNode call_stack = 28;

  // ── Sandbox / observe-mode shadow event (AAASM-1553) ────────────────────
  // Populated only when the emitting agent ran under EnforcementMode::OBSERVE
  // and the policy engine would have produced a non-Allow decision. The event
  // is emitted INSTEAD OF blocking — the agent proceeds, the audit log records
  // what would have happened.

  // True when this event represents a shadow / would-have decision rather
  // than a live enforcement outcome. Default false leaves enforce-mode
  // events unchanged.
  bool   dry_run         = 30;
  // The decision the policy engine would have applied if enforcement were
  // active. One of "deny" / "redact" / "pending" / "budget_block". Empty
  // when dry_run = false.
  string shadow_decision = 31;
  // Identifier of the policy rule that produced shadow_decision. Empty
  // when dry_run = false or when no specific rule matched.
  string matched_rule_id = 32;
}

// ── Call stack ────────────────────────────────────────────────────────────────

// CallStackNode is one node in the hierarchical call stack attached to an
// AuditEvent. Mirrors the dashboard `LiveOperation.callStack` TS type so a
// proto event can be rendered without a structural transform.
message CallStackNode {
  // Stable identifier for this node within the call stack.
  string id        = 1;
  // Node category — one of "llm", "tool", or "result". String-typed (not
  // an enum) to keep this open-ended for downstream renderers.
  string kind      = 2;
  // Human-readable label rendered in the dashboard (e.g. a tool name or
  // a model identifier).
  string label     = 3;
  // Step-local latency in milliseconds. Optional; `0` (default) means
  // the producer did not record a duration for this node.
  int64  latency_ms = 4;
  // Recursive descent — nested calls produced by this step.
  repeated CallStackNode children = 5;
}

// ── Detail messages ───────────────────────────────────────────────────────────

// LLMCallDetail records the outcome of a large-language-model invocation.
message LLMCallDetail {
  // Model identifier (e.g. "gpt-4o", "claude-3-5-sonnet", "gemini-1.5-pro").
  string model             = 1;
  // Token count in the prompt (pre-execution estimate from the SDK layer).
  int32  prompt_tokens     = 2;
  // Token count in the completion (post-execution, from the model response).
  int32  completion_tokens = 3;
  // End-to-end call latency in milliseconds.
  int64  latency_ms        = 4;
  // Whether the SDK-layer PII scan detected sensitive data in the prompt.
  bool   pii_detected      = 5;
  // Whether PII was redacted by the policy engine before the call proceeded.
  bool   pii_redacted      = 6;
  // Inference provider name (e.g. "openai", "anthropic", "gemini").
  string provider          = 7;
}

// ToolCallDetail records the outcome of a tool or MCP server invocation.
message ToolCallDetail {
  // Registered tool name (e.g. "web_search", "run_python").
  string tool_name     = 1;
  // Source system: "mcp", "langchain", or "function".
  string tool_source   = 2;
  // End-to-end tool call latency in milliseconds.
  int64  latency_ms    = 3;
  // Whether the tool call completed without error.
  bool   succeeded     = 4;
  // Populated when succeeded = false; contains the error message.
  string error_message = 5;
  // JSON-encoded arguments the agent passed to the tool, copied from the
  // request-side `ToolCallContext.args_json`. The producer (e.g. aa-proxy
  // for MCP) MUST run the credential scanner against this payload before
  // populating; secrets are replaced with `[REDACTED:<Kind>]` markers so
  // the audit chain never carries raw credentials. Empty when the
  // producer has no args context (legacy LLM intercepts).
  bytes  args_json     = 6;
}

// FileOpDetail records the outcome of a filesystem operation.
message FileOpDetail {
  // Operation type: "read", "write", "delete", or "create".
  string operation  = 1;
  // Absolute path of the file that was accessed.
  string path       = 2;
  // Number of bytes read or written (0 for delete/create).
  int64  bytes      = 3;
  // Detection source: "sdk_hook" (interceptor) or "ebpf" (kernel-level).
  string source     = 4;
  // End-to-end syscall latency in milliseconds. Currently `0` (default)
  // until the eBPF probes are extended to record enter/exit timestamps —
  // tracked under AAASM-1425. Schema lives here so the wire path is
  // ready when the kernel-side instrumentation lands.
  int64  latency_ms = 5;
}

// NetworkCallDetail records the outcome of an outbound network request.
message NetworkCallDetail {
  // Target hostname.
  string host        = 1;
  // Target port.
  int32  port        = 2;
  // Protocol: "https", "grpc", "ws", "tcp".
  string protocol    = 3;
  // End-to-end request latency in milliseconds.
  int64  latency_ms  = 4;
  // HTTP/gRPC status code (0 if not applicable).
  int32  status_code = 5;
  // Whether the request completed successfully.
  bool   succeeded   = 6;
}

// ProcessExecDetail records the outcome of a subprocess execution.
message ProcessExecDetail {
  // Command that was executed.
  string          command     = 1;
  // Arguments passed to the command.
  repeated string args        = 2;
  // Exit code returned by the process (0 = success by convention).
  int32           exit_code   = 3;
  // Elapsed time from process start to exit, in milliseconds.
  int64           duration_ms = 4;
  // Whether the process exited with code 0.
  bool            succeeded   = 5;
}

// PolicyViolation records an action that was blocked or flagged by the policy engine.
message PolicyViolation {
  // ID of the policy rule that triggered the violation.
  string policy_rule    = 1;
  // Human-readable summary of the blocked action.
  string blocked_action = 2;
  // Human-readable reason for the block (copied from CheckActionResponse).
  string reason         = 3;
  // Policy-engine evaluation latency in milliseconds. Currently `0`
  // (default) — no production code in this repo constructs
  // `Detail::Violation` today; the gateway tracks the verdict as a
  // string in its audit reader. Schema lives here so when the gateway
  // is wired to emit structured `PolicyViolation` events with timing
  // (tracked under AAASM-1426), the wire path is ready.
  int64  latency_ms     = 4;
}

// ApprovalEvent records the resolution of a human-in-the-loop approval request.
message ApprovalEvent {
  // Unique identifier of the approval queue item.
  string approval_id  = 1;
  // Account ID of the reviewer who made the decision.
  string approver_id  = 2;
  // true = approved; false = rejected.
  bool   approved     = 3;
  // Optional reason provided by the reviewer.
  string reason       = 4;
  // Elapsed time between approval request creation and reviewer decision, in ms.
  int64  wait_time_ms = 5;
}

// ── Layer degradation ────────────────────────────────────────────────────────

// LayerDegradationEvent records that an interception layer became unavailable.
// Emitted at startup when a layer fails detection, or at runtime when a layer
// degrades (e.g. proxy process exits).
message LayerDegradationEvent {
  // Agent identity string.
  string agent_id         = 1;
  // Session identifier (runtime instance).
  string session_id       = 2;
  // Nanosecond Unix timestamp when degradation was detected.
  int64  timestamp_ns     = 3;
  // Name of the degraded layer: "ebpf", "proxy", or "sdk".
  string layer            = 4;
  // Human-readable reason for the degradation.
  string reason           = 5;
  // Remaining active layers after degradation (e.g. ["proxy", "sdk"]).
  repeated string remaining_layers = 6;
}

// ── Batch upload ──────────────────────────────────────────────────────────────

// ReportEventsRequest submits a batch of audit events to the gateway.
message ReportEventsRequest {
  // Batch of events to record. Order is preserved in the response.
  repeated AuditEvent events = 1;
}

// ReportEventsResponse acknowledges a batch upload, returning one ID per event.
message ReportEventsResponse {
  // Event IDs in the same order as the request batch.
  repeated string event_ids = 1;
}

// ── Streaming upload ──────────────────────────────────────────────────────────

// StreamEventsResponse is returned after the client closes the stream.
message StreamEventsResponse {
  // Total number of events received by the server during the stream.
  int64 events_received = 1;
}
