Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
8 changes: 6 additions & 2 deletions docs/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ Denied calls always have a null `upstream_response` since no upstream request wa

The dashboard provides two views for audit data:

- **Feed tab** — A real-time stream of tool calls as they happen, powered by Server-Sent Events. Each action card shows the tool name, policy decision, timing, and matched rule.
- **Audit tab** — A searchable, filterable, paginated log of all recorded actions. Click any record to see full details including tool arguments, upstream response, and evidence chain.
- **Feed tab** — A real-time stream of tool calls as they happen, powered by Server-Sent Events. Each action card shows the tool name, policy decision, timing, and matched rule. An **Origin** badge (MCP or the adapter slug) identifies where the call originated. For non-`tool_call` records, a **record-kind chip** (Install Scan, Drift, or Expired) appears alongside the badge. Adapter-origin cards also surface context such as `channel_id` and `sender_id` from the record's metadata.
- **Audit tab** — A searchable, filterable, paginated log of all recorded actions. An **Origin** column shows the enforcement origin (MCP or adapter) with a record-kind chip for non-`tool_call` entries. **Channel ID** and **Sender ID** columns show adapter metadata and are visible at wider viewports. Click any record to see full details including tool arguments, upstream response, and evidence chain.

![Dashboard Audit](./images/dashboard-audit.png)

Expand All @@ -105,6 +105,10 @@ The dashboard provides two views for audit data:
- Upstream HTTP status range (`upstream_status_min` / `upstream_status_max`)
- Destructive flag
- Dry-run flag
- Origin (`mcp`, `openclaw`, or any adapter slug)
- Record kind (`tool_call`, `drift_event`, `install_scan`, `evaluation_expired`)
- Channel ID (`metadata.channel_id`)
- Sender ID (`metadata.sender_id`)

## CLI Export

Expand Down
90 changes: 49 additions & 41 deletions docs/sideband-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,24 +306,28 @@ Searchable, filtered, paginated audit log.

**Query parameters:**

| Parameter | Default | Range | Description |
| --------------------- | ------- | -------------- | --------------------------------------------- |
| `limit` | `50` | `[1, 1000]` | Page size. |
| `offset` | `0` | `[0, ∞)` | Number of records to skip. |
| `tool` | — | — | Tool name substring filter (`LIKE %tool%`). |
| `decision` | — | — | Filter by `policy_decision`. |
| `reason` | — | — | Filter by `block_reason`. |
| `blocked` | — | `true`/`false` | Filter by whether `block_reason` is non-null. |
| `session` | — | — | Filter by session ID. |
| `agent` | — | — | Filter by agent ID. |
| `from` | — | ISO 8601 | Lower bound on `created_at` (inclusive). |
| `to` | — | ISO 8601 | Upper bound on `created_at` (inclusive). |
| `upstream_status_min` | — | integer | Minimum upstream HTTP status (inclusive). |
| `upstream_status_max` | — | integer | Maximum upstream HTTP status (inclusive). |
| `destructive` | — | `true`/`false` | Filter by the `flagged_destructive` column. |
| `dry_run` | — | `true`/`false` | Filter by the `dry_run` column. |

`tool` uses substring matching (`LIKE %value%`). `decision`, `reason`, `session`, and `agent` use exact equality matching.
| Parameter | Default | Range | Description |
| --------------------- | ------- | -------------- | --------------------------------------------------------------------------------------------- |
| `limit` | `50` | `[1, 1000]` | Page size. |
| `offset` | `0` | `[0, ∞)` | Number of records to skip. |
| `tool` | — | — | Tool name substring filter (`LIKE %tool%`). |
| `decision` | — | — | Filter by `policy_decision`. |
| `reason` | — | — | Filter by `block_reason`. |
| `blocked` | — | `true`/`false` | Filter by whether `block_reason` is non-null. |
| `session` | — | — | Filter by session ID. |
| `agent` | — | — | Filter by agent ID. |
| `from` | — | ISO 8601 | Lower bound on `created_at` (inclusive). |
| `to` | — | ISO 8601 | Upper bound on `created_at` (inclusive). |
| `upstream_status_min` | — | integer | Minimum upstream HTTP status (inclusive). |
| `upstream_status_max` | — | integer | Maximum upstream HTTP status (inclusive). |
| `destructive` | — | `true`/`false` | Filter by the `flagged_destructive` column. |
| `dry_run` | — | `true`/`false` | Filter by the `dry_run` column. |
| `origin` | — | — | Filter by enforcement origin (`mcp`, or an adapter slug like `openclaw`). |
| `record_kind` | — | — | Filter by record category (`tool_call`, `drift_event`, `install_scan`, `evaluation_expired`). |
| `channel_id` | — | — | Filter by `metadata.channel_id` (adapter-supplied). |
| `sender_id` | — | — | Filter by `metadata.sender_id` (adapter-supplied). |

`tool`, `origin`, `channel_id`, and `sender_id` use substring matching (`LIKE %value%`). `decision`, `reason`, `session`, `agent`, and `record_kind` use exact equality matching.

**Response (200):**

Expand Down Expand Up @@ -366,21 +370,25 @@ Bulk export of audit records as a downloadable attachment. **Not a JSON envelope

**Query parameters:**

| Parameter | Default | Description |
| --------------------- | ------- | --------------------------------------------- |
| `format` | `json` | `json` or `csv`. |
| `limit` | `10000` | Maximum records. Capped at 10k. |
| `tool` | — | Filter by tool name substring. |
| `decision` | — | Filter by policy decision. |
| `reason` | — | Filter by block reason. |
| `blocked` | — | Filter by whether `block_reason` is non-null. |
| `dry_run` | — | Filter by dry-run records (`true`/`false`). |
| `session` | — | Filter by session ID. |
| `agent` | — | Filter by agent ID. |
| `from` | — | Start time (ISO 8601). |
| `to` | — | End time (ISO 8601). |
| `upstream_status_min` | — | Minimum upstream HTTP status (inclusive). |
| `upstream_status_max` | — | Maximum upstream HTTP status (inclusive). |
| Parameter | Default | Description |
| --------------------- | ------- | --------------------------------------------------------------------------------------------- |
| `format` | `json` | `json` or `csv`. |
| `limit` | `10000` | Maximum records. Capped at 10k. |
| `tool` | — | Filter by tool name substring. |
| `decision` | — | Filter by policy decision. |
| `reason` | — | Filter by block reason. |
| `blocked` | — | Filter by whether `block_reason` is non-null. |
| `dry_run` | — | Filter by dry-run records (`true`/`false`). |
| `session` | — | Filter by session ID. |
| `agent` | — | Filter by agent ID. |
| `from` | — | Start time (ISO 8601). |
| `to` | — | End time (ISO 8601). |
| `upstream_status_min` | — | Minimum upstream HTTP status (inclusive). |
| `upstream_status_max` | — | Maximum upstream HTTP status (inclusive). |
| `origin` | — | Filter by enforcement origin (`mcp`, or an adapter slug like `openclaw`). |
| `record_kind` | — | Filter by record category (`tool_call`, `drift_event`, `install_scan`, `evaluation_expired`). |
| `channel_id` | — | Filter by `metadata.channel_id` (adapter-supplied). |
| `sender_id` | — | Filter by `metadata.sender_id` (adapter-supplied). |

See [Audit Trail → Dashboard API Export](./audit.md#dashboard-api-export) for full context and examples.

Expand Down Expand Up @@ -564,14 +572,14 @@ Server-Sent Events stream of dashboard events. The stream stays open indefinitel

**Event types:**

| Event | Payload fields |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `heartbeat` | empty `data`. Sent on connect and every `sseHeartbeatMs`. |
| `action` | `id`, `tool_name`, `policy_decision`, `block_reason`, `approval_status`, `session_id`, `agent_id`, `environment`, `timestamp`, `total_duration_ms`, `approval_wait_ms`, `proxy_compute_ms`, `flagged_destructive`, `dry_run`, `matched_rule`, `matched_rule_index` |
| `approval_requested` | `ticket_id`, `tool_name`, `channel`, `requested_at` |
| `approval_resolved` | `ticket_id`, `status`, `resolved_by` (optional), `resolved_at` |
| `approval_notification_failed` | `ticket_id`, `channel`, `phase` (`initial`/`escalation`), `error` |
| `limit_warning` | `key`, `type` (`rate`/`spend`), `current`, `limit`, `utilization` |
| Event | Payload fields |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `heartbeat` | empty `data`. Sent on connect and every `sseHeartbeatMs`. |
| `action` | `id`, `tool_name`, `policy_decision`, `block_reason`, `approval_status`, `session_id`, `agent_id`, `environment`, `timestamp`, `total_duration_ms`, `approval_wait_ms`, `proxy_compute_ms`, `flagged_destructive`, `dry_run`, `matched_rule`, `matched_rule_index`, `origin` (enforcement origin: `mcp` or adapter slug), `record_kind` (`tool_call` / `drift_event` / `install_scan` / `evaluation_expired`) |
| `approval_requested` | `ticket_id`, `tool_name`, `channel`, `requested_at` |
| `approval_resolved` | `ticket_id`, `status`, `resolved_by` (optional), `resolved_at` |
| `approval_notification_failed` | `ticket_id`, `channel`, `phase` (`initial`/`escalation`), `error` |
| `limit_warning` | `key`, `type` (`rate`/`spend`), `current`, `limit`, `utilization` |

For `approval_resolved`, `status` is one of `approved`, `denied`, `timeout`, `break_glass`, `client_disconnected`, or `shutdown_cancelled`.

Expand Down
15 changes: 15 additions & 0 deletions packages/dashboard/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,21 @@ describe('read endpoints', () => {
expect('page' in res).toBe(false)
})

it('fetchAudit serializes origin/record_kind/channel/sender into the query (#16)', async () => {
mockFetch.mockReturnValue(okJson({ data: [], total: 0, limit: 50, offset: 0 }))
await fetchAudit({
origin: 'openclaw',
record_kind: 'install_scan',
channel: 'C1',
sender: 'U1',
})
const url = calledUrl()
expect(url).toContain('origin=openclaw')
expect(url).toContain('record_kind=install_scan')
expect(url).toContain('channel_id=C1')
expect(url).toContain('sender_id=U1')
})

it('fetchAuditRecord encodes ID', async () => {
mockFetch.mockReturnValue(okJson({ data: { id: 'abc' } }))
await fetchAuditRecord('abc/def')
Expand Down
8 changes: 8 additions & 0 deletions packages/dashboard/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export interface AuditParams {
blocked?: boolean
session?: string
agent?: string
origin?: string
record_kind?: string
channel?: string
sender?: string
from?: string
to?: string
destructive?: boolean
Expand All @@ -141,6 +145,10 @@ export function fetchAudit(params?: AuditParams): Promise<AuditListResponse> {
blocked: params?.blocked,
session: params?.session,
agent: params?.agent,
origin: params?.origin,
record_kind: params?.record_kind,
channel_id: params?.channel,
sender_id: params?.sender,
from: params?.from,
to: params?.to,
destructive: params?.destructive,
Expand Down
5 changes: 4 additions & 1 deletion packages/dashboard/src/app.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
@import 'tailwindcss';

/* Tailwind CSS v4 removed cursor:pointer from buttons. Restore it. */
/* Tailwind CSS v4 removed cursor:pointer from buttons. Restore it.
Native <select> dropdowns also default to the arrow cursor — include them
so all interactive controls share the pointer affordance. */
button,
[role='button'],
select,
summary {
cursor: pointer;
}
Expand Down
Loading
Loading