Core0 / Nio
Core0
GitHub v2.4.4
Telemetry · Logs

# Logs — audit-log entries, dual-written

Audit entries are dual-written: OTEL Logs export to `<endpoint>/v1/logs` AND a local JSONL file at `collector.logs.path` (default `~/.nio/audit.jsonl`). The JSONL line is the entry verbatim; the OTEL LogRecord uses `body = JSON.stringify(entry)` plus a flat attribute set for indexing.

## Entry types

Discriminated by the `event` field. Five shapes — four named entry types plus the catch-all "hook event" record.

event: "guard" Guard decision per PreToolUse / PostToolUse

Field | Type | Notes
--- | --- | ---
`timestamp` | string | ISO-8601
`platform` | string | `claude-code` / `hermes` / `openclaw`
`agent_name` | string? | User-configured `agent_name`; omitted when unset
`session_id` | string? | host session id
`cwd` | string? | working dir
`tool_name` | string | host tool name
`action_type` | string? | `exec_command` / `write_file` / `network_request` / `read_file`
`tool_input_summary` | string | redacted ≤200-char summary of tool input
`decision` | string | `allow` / `deny` / `ask`
`risk_level` | string | `low` / `medium` / `high` / `critical`
`max_finding_severity` | string | highest finding severity
`risk_score` | number | 0–1 final score
`risk_tags` | string[] | rule IDs hit (deduped)
`phase_stopped` | number | null | which Phase 0–6 produced the decision
`scores` | Record<string, number> | per-phase score (`runtime`, `static`, `behavioural`, `llm`, `external`, `final`)
`phases` | AuditPhaseMap? | per-phase `{score, finding_count, duration_ms}`
`top_findings` | AuditFindingSummary[] | up to 5: `{rule_id, severity, category, title, confidence}`
`explanation` | string? | human-readable reason
`initiating_skill` | string? | which skill scope the action originated from
`event_type` | "pre" | "post"? | which hook side fired

event: "session_scan" Skill scan (on-demand or session-start)

Field | Type | Notes
--- | --- | ---
`timestamp` | string | ISO-8601
`platform` | string | host
`agent_name` | string? | User-configured `agent_name`; omitted when unset
`session_id` | string? | host session id
`skill_name` | string | scanned skill / dir
`risk_level` | string | aggregated severity
`risk_tags` | string[] | rule IDs hit
`finding_count` | number? | total findings

event: "lifecycle" Subagent / agent / session lifecycle

Field | Type | Notes
--- | --- | ---
`timestamp` | string | ISO-8601
`platform` | string | host
`agent_name` | string? | User-configured `agent_name`; omitted when unset
`session_id` | string? | host session id
`lifecycle_type` | string | `subagent_spawning` / `subagent_ended` / `agent_end` / `session_start` / `session_end`
`details` | Record<string, unknown>? | platform-specific (e.g. OpenClaw: `{subagent_id, run_id}`)

event: "config_error" Config load failure

Field | Type | Notes
--- | --- | ---
`timestamp` | string | ISO-8601
`config_path` | string | path that failed to load
`error_message` | string | parser / IO error

event: <hook event> Collector hook record (one entry per dispatched hook event)

Discriminator is the canonical hook event name itself: `UserPromptSubmit` · `PreToolUse` · `PostToolUse` · `TaskCreated` · `TaskCompleted` · `Stop` · `SubagentStop` · `SessionStart` · `SessionEnd`.

Field | Type | Notes
--- | --- | ---
`timestamp` | string | ISO-8601
`platform` | string | host
`agent_name` | string? | User-configured `agent_name`; omitted when unset
`session_id` | string? | host session id
`cwd` | string | null | working dir
`transcript_path` | string? | Claude Code-only — path to session transcript JSONL
`tool_name` | string? | for PreToolUse / PostToolUse
`tool_use_id` | string? | for PreToolUse / PostToolUse
`tool_summary` | string? | for PreToolUse / PostToolUse
`task_id` | string? | for TaskCreated / TaskCompleted
`task_summary` | string? | for TaskCreated

## OTEL LogRecord projection

The flat attribute set used for OTEL Logs indexing. Same key names as the matching trace span attributes wherever a concept overlaps (tool name, conversation id, guard decision, …) — same query keys work across logs and traces.

- `body` = JSON-stringified entry (full content of the JSONL line)
- `severityNumber` / `severityText` derived from `risk_level`: `low`→INFO, `medium`→WARN, `high`→ERROR, `critical`→FATAL; INFO when no `risk_level`

Attribute | Description | Captured at | Platforms
--- | --- | --- | ---
`gen_ai.tool.name` | Host tool name; same key as the tool-span attribute | PreToolUse · PostToolUse · guard decision | all
`gen_ai.tool.call.id` | Host tool-call id; same key as the tool-span attribute | PreToolUse · PostToolUse | all
`gen_ai.conversation.id` | Host session id; same key as the turn-span attribute | every audit entry with a session | all
`session.id` | Mirror of `gen_ai.conversation.id` for OTel base-spec consumers | every audit entry with a session | all
`nio.guard.decision` | Guard verdict — `allow` / `deny` / `ask` | guard decision | all
`nio.guard.risk_level` | Guard risk level — `low` / `medium` / `high` / `critical` | guard decision | all
`nio.guard.risk_score` | Guard risk score, 0–1 | guard decision | all
`nio.guard.risk_tags` | Comma-joined rule IDs that fired | guard decision | all
`nio.tool_summary` | One-line summary derived from tool input | PreToolUse · PostToolUse | all
`nio.task_id` | Task id from the dispatch event | TaskCreated · TaskCompleted | Claude Code + OpenClaw
`nio.task_summary` | Derived from task input | TaskCreated | Claude Code + OpenClaw
`nio.platform` | Source platform — `claude-code` / `hermes` / `openclaw` | every audit entry | all
`gen_ai.agent.name` | User-configured `agent_name`; only emitted when set | every audit entry (when configured) | all
`nio.cwd` | Working dir at hook fire | every audit entry with cwd | all
`nio.event` | Discriminator — hook event name vs guard / lifecycle / scan / config_error | every audit entry | all
`nio.event_type` | `pre` / `post` for guard entries | guard decision | all
`nio.action_type` | `exec_command` / `write_file` / `network_request` / `read_file` | guard decision | all
`nio.max_finding_severity` | Highest finding severity surfaced this run | guard decision | all
`nio.phase_stopped` | Which Phase 0–6 produced the decision | guard decision | all
`nio.explanation` | Human-readable reason for the verdict | guard decision | all
`nio.transcript_path` | Claude Code-only — path to session transcript JSONL | hook events with transcript | Claude Code only
`nio.phases.{name}.score` | Per-phase score (Phase 0–6) | guard decision | all
`nio.phases.{name}.finding_count` | Per-phase finding count | guard decision | all
`nio.phases.{name}.duration_ms` | Per-phase wall-clock cost (ms) | guard decision | all

Local JSONL path: `collector.logs.path` (default `~/.nio/audit.jsonl`). Rotation kicks in at `collector.logs.max_size_mb` (default 100 MB) — the live file is renamed to `<path>.1`.