Skip to content
Merged
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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ community mirror, **Enterprise** changes are EE-only.

---

## [9.3.0] - 2026-07-02 (audit visibility: per-developer + per-session identity, audit read/report/export API, portal Log Explorer)

**Additive minor.** v9.3.0 makes governed activity visible end to end: per-developer and per-session identity now flows through to the canonical audit row, a read/report/export API exposes the audit trail, and the customer portal gains a filterable, expandable Log Explorer. Claude Code traffic is brought into the same governed view via a Grafana dashboard and OpenTelemetry ingest. One additive migration (`core/129`); the new endpoints are backward-compatible and the remaining changes are behavior-preserving fixes.

### Added

- **Per-developer and per-session identity through to the canonical audit row.** *(Community)* Requests carrying `X-User-Email` and `X-Session-Id` now propagate that identity into `audit_logs`, so every governed decision can be attributed to the developer and session that produced it. Migration `core/129` adds the nullable `audit_logs.session_id` column; the user email lands on the existing identity columns.
- **Audit read / report / export API.** *(Community)* New read-only endpoints expose the audit trail: `GET /api/v1/audit/{id}` returns a single decision record, `POST /api/v1/audit/report` returns per-action counts and top policies for a filter, and `POST /api/v1/audit/export` streams the filtered rows (with a truncation header when a row cap is reached). Redacted values are served as stored — there is no unmask path.
- **Portal audit-logs Log Explorer.** *(Enterprise)* The customer-portal Audit Logs page is rebuilt as a Log Explorer: combinable filters (user email, action, tenant, date range) with pagination, per-row expansion to the full decision record, a report-by-action view, and export of the active filter. Redacted content is rendered as stored and labelled.
- **Claude Code Grafana dashboard and decision origin label.** *(Community)* A new Grafana dashboard visualizes Claude Code governed traffic, and decisions now carry a bounded `origin` metric label (with obligations and blocks series) so decision volume can be sliced by call origin without unbounded cardinality.
- **Cowork / Claude Code OpenTelemetry ingest to canonical `audit_logs`.** *(Enterprise)* An authenticated `POST /v1/logs` endpoint lands Cowork and Claude Code OpenTelemetry log events as canonical `audit_logs` rows (`plane=cowork` / `plane=claude_code`), so agent activity from those hosts is a first-class audit source rather than a satellite table. The ingest is a force-redact storage plane: PII is masked before the row is stored, and it fails closed (withholds the row) when detection is unavailable.
- **Unified policy write dispatcher.** *(Enterprise)* A single `/unified-policies/*` write path now dispatches policy create/update/delete across the system and tenant policy stores, replacing the scattered per-store write handlers.

### Fixed

- **Portal Usage reflects governed Claude Code traffic.** *(Enterprise)* The portal Usage page under-counted governed activity because Claude Code MCP traffic was not attributed to the usage rollup; it now surfaces that traffic.
- **Grafana agent and orchestrator blocked/allowed panels use the real metric names.** *(Community)* The agent and orchestrator dashboards referenced defunct synthetic-exporter series, so the blocked/allowed panels were empty; they now query the real emitted metric names.
- **Policy action override on the static / system path.** *(Community)* The policy action override now applies on the static (system) policy path: it authenticates through the proxy, reads back via the agent `GET`, and keys on `policy.id`, with an allow-flip guard. The redundant dynamic-override path is removed — action override is a system/static-only capability (dynamic policies are edited or deleted directly).
- **Cross-tenant static-policy read/write isolation.** *(Community)* Static-policy reads and writes are now scoped to the caller's tenant, closing a path where one tenant could read or write another tenant's static policies.
- **Executions and Approvals no longer 500 on NULL columns.** *(Community)* The executions list and the approvals list / approve / reject paths failed to scan rows with legitimately-NULL columns (for example a MAP-plan row with a NULL `source`, or workflow-step rows with NULL identity or step fields); those scans now tolerate NULL.
- **SSO setup flow.** *(Enterprise)* The portal SSO page treated a "not configured" backend response as already-configured and never showed the provider selector; the setup flow now renders correctly.
- **SCIM and SSO modal z-index.** *(Enterprise)* The SCIM token and SSO confirmation modals sat beneath their own backdrop, leaving their buttons unclickable; the modal panels are now layered above the backdrop.
- **Compliance evidence export no longer drops audit rows.** *(Enterprise)* The compliance evidence export selected columns that do not exist on the audit row, dropping every `audit_logs` entry from the export; it now derives the blocked flag from the policy decision and the risk score from the stored decision details, and includes the rows.

### Migration

- **`core/129`** *(Community)* — adds the nullable `audit_logs.session_id` column (additive; existing rows and existing callers are unaffected).

## [9.2.2] - 2026-07-01 (patch on 9.2.1)

**Patch.** Three fixes on top of [9.2.1]: the MCP `check_policy` advisory tool now redacts PII on its allow path, the Java example dependencies clear three Jackson CVEs, and an internal license-generation doc is corrected. No new behavior and no migrations; see the 9.2.0 entry below for the feature release.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9.2.2
9.3.0
65 changes: 65 additions & 0 deletions docker-compose.ws12.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# WS-12 isolation overlay (#2778).
#
# Gives every service a unique container_name (ws12-*) and a remapped host port
# so this full-portal E2E stack coexists with other worktrees' default-named
# stacks (e.g. ws11 binds 8080/8081/8082/5432/6379/3000/9000/9090). Internal
# service-to-service DNS is unaffected: compose still exposes each service under
# its *service* name (postgres, redis, axonflow-agent, axonflow-orchestrator,
# axonflow-customer-portal) as a network alias regardless of container_name.
#
# Driven by scripts/ws12-portal-e2e-up.sh. Use with:
# docker compose -p ws12 -f docker-compose.yml -f docker-compose.enterprise.yml \
# -f docker-compose.portal-ui.yml -f docker-compose.ws12.yml up -d
#
# `ports: !override` REPLACES the inherited port list (a plain merge would append
# and re-bind the default host port, colliding with the ws11 stack).

services:
postgres:
container_name: ws12-axonflow-postgres
ports: !override
- "5442:5432"

redis:
container_name: ws12-axonflow-redis
ports: !override
- "6389:6379"

axonflow-agent:
container_name: ws12-axonflow-agent
ports: !override
- "8090:8080"

axonflow-orchestrator:
container_name: ws12-axonflow-orchestrator
ports: !override
- "8091:8081"

axonflow-customer-portal:
container_name: ws12-axonflow-customer-portal
ports: !override
- "8092:8080"

axonflow-customer-portal-ui:
container_name: ws12-axonflow-customer-portal-ui
ports: !override
- "3200:3000"

prometheus:
container_name: ws12-axonflow-prometheus
ports: !override
- "9100:9090"

grafana:
container_name: ws12-axonflow-grafana
ports: !override
- "3010:3000"

minio:
container_name: ws12-axonflow-minio
ports: !override
- "9010:9000"
- "9011:9001"

minio-init:
container_name: ws12-axonflow-minio-init
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ services:
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
AXONFLOW_INTEGRATIONS: ${AXONFLOW_INTEGRATIONS:-}
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.2.2}"
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.3.0}"

# Anonymous platform startup telemetry — inherit the operator's
# opt-out lever. CI / dev shells should export AXONFLOW_TELEMETRY=off
Expand Down Expand Up @@ -265,7 +265,7 @@ services:
PORT: 8081
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.2.2}"
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.3.0}"

# Anonymous platform startup telemetry — inherit the operator's
# opt-out lever. See agent service above for the full rationale.
Expand Down
24 changes: 24 additions & 0 deletions docs/enterprise/customer-portal-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,30 @@ Note: This goes through the Agent Gateway on port 443/8080 (which proxies to Por
curl "https://{domain}:8090/api/v1/auth/sso/availability?org_id=acme-corp"
```

## Reading the Audit Logs

The **Audit Logs** page (`/audit`) is the tenant's compliance trail. It has two views:

- **Log Explorer** — a filterable, paginated table of every governed request. Columns
include timestamp, verdict (Allowed / Blocked / Redacted / Needs Approval / Error),
**user email**, tenant, request type, matched policy, and latency.
- **Filters combine.** Narrow by user email (partial match), verdict, and date range
at the same time; the table and the export both respect the active filters.
- **Expand any row** (the ▸ chevron, or click the query) to see the full record:
the query / blocked command, redacted response, matched policy and the reasons it
fired, and the correlation / decision / session IDs used to stitch a request across
planes.
- **Report by Action** — per-verdict counts for the selected range (filterable by user).
Select a verdict card to drill straight into the matching rows in the Log Explorer.

**Redaction is preserved.** Values the engine masked before storage (e.g. an Indonesian
NIK, an email) are shown exactly as stored and are clearly labelled *"Redacted before
storage"*. The portal never reconstructs the original content — there is no "unmask".

**Export.** *Export CSV* / *Export JSON* download the filtered result set (not just the
current page). Large ranges are capped at the server row limit; when that happens the
page warns that the file is partial so you can narrow the range and re-export.

## Related Resources

| Resource | Path |
Expand Down
29 changes: 29 additions & 0 deletions migrations/core/129_audit_logs_session_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Migration 129: Per-session identity on the canonical audit_logs
-- Date: 2026-07-01
-- Purpose: Add a first-class session_id column to the canonical decision row
-- (audit_logs) so a governed request can be attributed to the AI-tool
-- session that produced it (Claude Code / Claude Desktop session_id),
-- alongside the per-developer user_email that already exists. This
-- front-loads the data for session-level reporting (epic #2753 WS-7)
-- so that layer is pure read-side aggregation.
--
-- Sibling of user_email (migration 059): both are asserted identity fields the
-- plugin/proxy forward as HTTP headers (X-User-Email / X-Session-Id) and the
-- write paths (agent check_policy → writeMCPDecisionAudit; orchestrator
-- auditToolCallHandler → LogToolCallAudit) persist here.
--
-- NULLABLE with no default: every existing writer that does not forward a
-- session id writes NULL, so existing behavior is byte-identical and the
-- column is purely additive. session_id is an ASSERTED attribution label, not
-- an authentication boundary.

ALTER TABLE audit_logs
ADD COLUMN IF NOT EXISTS session_id VARCHAR(255);

-- Partial index backing session-scoped reporting/filtering
-- (`WHERE session_id IS NOT NULL`): only rows that actually carry a session are
-- indexed, so the (majority) of non-session rows add no index weight.
CREATE INDEX IF NOT EXISTS idx_audit_logs_session_id
ON audit_logs(session_id) WHERE session_id IS NOT NULL;

COMMENT ON COLUMN audit_logs.session_id IS 'AI-tool session id (Claude Code / Desktop) forwarded via X-Session-Id; asserted attribution label, not an auth boundary (#2753/#2754)';
7 changes: 7 additions & 0 deletions migrations/core/129_audit_logs_session_id_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Migration 129 Down: Remove the session_id column from audit_logs
-- Rollback script for the canonical-row per-session identity field.

DROP INDEX IF EXISTS idx_audit_logs_session_id;

ALTER TABLE audit_logs
DROP COLUMN IF EXISTS session_id;
4 changes: 2 additions & 2 deletions platform/agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ARG EDITION=community
# — the SAME source as the io.opencontainer.image.version label below, so the
# baked binary version and the image label never drift. Re-declared here in the
# builder stage (ARG scope is per-stage); the final stage declares it again.
ARG AXONFLOW_VERSION=9.2.2
ARG AXONFLOW_VERSION=9.3.0

# Install git for go modules
RUN apk add --no-cache git ca-certificates tzdata
Expand Down Expand Up @@ -138,7 +138,7 @@ RUN set -e && \
# Final stage - minimal runtime image
FROM alpine:3.24

ARG AXONFLOW_VERSION=9.2.2
ARG AXONFLOW_VERSION=9.3.0
ENV AXONFLOW_VERSION=${AXONFLOW_VERSION}

# AWS Marketplace metadata
Expand Down
10 changes: 10 additions & 0 deletions platform/agent/audit_coverage_allowlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ func auditCoverageAllowlist() map[string]string {
// (decide / gateway / mcp) records the redacted verdict.
"platform/agent/request_redaction_detector.go::textPIIDetector.Redact": "by-design: engine-backed redactor adapter (Redactor iface impl) wrapping redactInputStatement; no verdict, the invoking PEP records the redaction.",

// ---- BY-DESIGN: Cowork / Claude Code OTEL ingest plane (caller audits) ----
// coworkRedactDefault is the redact-at-collector helper: it wraps the SAME
// engine response-plane redactor (evaluateOutputPolicies) and returns a
// coworkRedactResult (masked/withheld/allowed + verdict). It writes no row
// itself; its only caller, processCoworkRecord, records the canonical
// audit_logs row via writeCoworkAuditLog AND signs it via recordSignedDecision
// on every terminal verdict (allowed/redacted/blocked/error) — the same
// split as the MCP evaluateOutputPolicies helper above. (#2760 / WS-6.)
"platform/agent/cowork_otel_ingest.go::coworkRedactDefault": "by-design: redact-at-collector helper wrapping evaluateOutputPolicies; caller processCoworkRecord audits via writeCoworkAuditLog + recordSignedDecision on every verdict (#2760).",

// ---- BY-DESIGN: orchestrator response plane (handler audits) ----
// DetectWithSharedEngine is the shared-engine detector; it returns a
// ResponseResult. The orchestrator response plane audits the outcome in
Expand Down
10 changes: 6 additions & 4 deletions platform/agent/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,19 @@ func getPluginCompatibility() PluginCompatInfo {
// security patch (2026-06-16): 2.6.6 clears a runtime protobufjs CVE
// and is republished on ClawHub/npm. claude-code/cursor/codex are
// unchanged for 9.1.1. claude-code bumped 1.6.0 -> 1.7.0 in the
// 9.2.2 release-train (ships with the v9.2.2 platform patch; the
// 1.7.0 marketplace release fires immediately after the tag); cursor
// stays 1.5.3, codex stays 1.5.2, openclaw stays 2.6.6. The other
// 9.2.2 release-train. claude-code bumped 1.7.0 -> 1.8.0 in the
// 9.3.0 release-train (ships with the v9.3.0 platform minor carrying
// the audit-visibility bundle; the 1.8.0 marketplace release fires
// immediately after the tag); cursor stays 1.5.3, codex stays 1.5.2,
// openclaw stays 2.6.6. The other
// plugin tags are live on their registries (openclaw 2.6.6 on
// npm/ClawHub, cursor 1.5.3 + codex 1.5.2; claude-code/cursor on the
// GitHub marketplace, codex on ClawHub). Plugins below the recommended
// version receive an actionable upgrade-warning header on every
// governed call; the MinPluginVersion floor stays 1.4.0 / 2.4.0.
RecommendedPluginVersion: map[string]string{
"openclaw": "2.6.6",
"claude-code": "1.7.0",
"claude-code": "1.8.0",
"cursor": "1.5.3",
"codex": "1.5.2",
},
Expand Down
Loading
Loading