Skip to content

Data Dictionary

What data SCP and its collectors actually hold — every table backing the dashboard, plus the three activity-collector schemas that feed the Agent Activity tab. Written by hand against the current schema/model files on 2026-07-26; it is a snapshot, not a generated artifact, so it can drift if the schema changes without this page being updated. See API Endpoints for how each of these gets read or written.

Scope for this pass: control-plane Postgres tables that back the dashboard's Registry/Agent Activity/Compliance tabs, plus the collector SQLite schema. Supporting tables that exist but aren't surfaced in the current dashboard (rules engine, telemetry billing, deployments/usage/ adherence) are listed briefly at the end for completeness, not documented field-by-field.


Agent Registry

Source: control-plane/database/schemas/agent_registry.sql, control-plane/shared/models/agents.py.

agents

Field Type Notes
agent_id text, PK e.g. agent:prior-auth-prod
name text Human-readable name
description text, nullable
role_id text Reference to a role in the context graph
allowed_intents jsonb Array of {task_type, scope?} — what task types this agent may request context for
overrides jsonb SCD IDs always included in this agent's context
restrictions jsonb SCD IDs never included in this agent's context
status text active | suspended | revoked
owner_name text, nullable HITL accountability field (migration 005) — who's accountable for this agent's operation: a Propel workflow owner, or whoever provisioned it. Distinct from created_by below.
owner_verified boolean, default false true only once owner_name is confirmed against a real identity provider (SSO). false = manually asserted — must be disclosed as such in the UI, never presented as system-verified. This is the field blocked on Dan's SSO/SCIM work.
created_by text Who registered this agent (context/bundle provenance — not the same concept as owner_name)
created_at / updated_at timestamptz
organization_id text, nullable Reserved for future multi-tenancy

agent_api_keys

Field Type Notes
key_id text, PK
agent_id text, FK → agents
key_prefix text First 8 chars, for lookup
key_hash text bcrypt/argon2 hash — plaintext key is never stored
name text, nullable e.g. "Production Key"
scopes jsonb, nullable Subset of agent's allowed_intents, or null = full permissions
is_active boolean Legacy flag, kept for backward compatibility
status text active | revoked | expired — authoritative field
created_at, expires_at, last_used_at, revoked_at timestamptz, nullable
revoked_by text, nullable Actor who revoked the key

agent_context_requests — the audit trail

Append-only, hash-chained, and enforced immutable at the database level (trigger blocks UPDATE/ DELETE for all roles including superusers; row-level security has no UPDATE/DELETE policy at all). This is the record an auditor would actually be shown.

Field Type Notes
request_id text, PK
agent_id text, FK → agents
task_type text What was requested
request_params jsonb, nullable
intent_valid boolean Whether the request was allowed
validation_errors jsonb, nullable Why, if not valid
context_bundle_ids / context_scd_ids jsonb, nullable What was actually returned
processing_time_ms integer, nullable
requested_at timestamptz
chain_sequence bigserial Strict insertion order for chain traversal
previous_hash text SHA256 of the prior record (or genesis hash for the first)
record_hash text SHA256(chain_sequence \|\| request_id \|\| agent_id \|\| task_type \|\| intent_valid \|\| requested_at \|\| previous_hash)

GET /api/audit/verify walks this chain and recomputes every hash to detect tampering — see API Endpoints.


MCP Server Registry

Source: control-plane/database/schemas/mcp_servers.sql.

mcp_servers

Same HITL-ownership pattern as agents, deliberately a separate table (a connector isn't an agent).

Field Type Notes
server_id text, PK
name text
description text, nullable
status text active | inactive | deprecated
owner_name text, nullable Same accountability concept as agents.owner_name
owner_verified boolean, default false Same SSO-pending caveat as agents.owner_verified
created_by text
created_at / updated_at timestamptz

Bundle Registry & Context Service

Source: control-plane/database/schemas/index_ledger.sql, context_service.sql.

bundles

Field Type Notes
bundle_id, version text, composite PK
bundle_type text project | meta | standards | domain | concern
schema_version text
created_by text
description text, nullable
manifest jsonb Complete bundle manifest — SCDs and imports
validation_status text pending | valid | invalid
validation_errors jsonb, nullable
created_at, published_at timestamp, published_at nullable until published
is_active boolean

tags (Context-as-a-Service)

Mutable pointers to bundle versions (Docker-tag style): tag_name (PK), bundle_id, resolved_bundle_ids (flattened transitive import graph), cached context_yaml, description, timestamps.

Supporting tables not detailed here: context_service_events (append-only audit log of tag/ subscription lifecycle events), active_subscriptions (live agent↔tag subscriptions for streaming context).


Observed Activity Log (Collector Store)

Source: integrations/cowork-telemetry/store/schema.sql. This is a separate SQLite database, not part of the control-plane Postgres — deliberately standalone (see FR-005/FR-006), read via its own API on port 7002 (see Activity Ingestion API). Three independent collectors write into the same table, distinguished by source_id.

observed_activity_log

Field Type Notes
event_id text, PK
source_id text claude-enterprise-cowork | claude-code-cli | claude-enterprise-chat
observed_at text (ISO 8601) From the event's own timestamp
correlation_id text, nullable Cowork's prompt.id (UUID v4), where applicable
session_id text, nullable
actor_identity text, nullable Stable, non-PII user identifier (user.account_uuid / actor.user_id) — not the email address. This is the field SSO/SCIM would resolve to a verified NexTern identity once connected.
event_name text Raw/inferred source event name (e.g. user_prompt, tool_result, api_request)
event_type text Normalized: prompt | tool_call | mcp_call | model_call | approval | error | unknown
tool_name, mcp_server_scope text, nullable
success text ("true"/"false"), nullable tool_result events only
decision, decision_source text, nullable tool_decision (human approval) events only
model, cost_usd, input_tokens, output_tokens, duration_ms nullable api_request/api_error events only
error_message text, nullable
redaction_masked_keys, redaction_masked_count text/integer, nullable What the collector masked before this row was ever written
raw_attributes text (JSON) Full already-redacted attribute map, for drill-down
ingested_at text When the watcher/poller wrote the row

activity_sources

One row per collector: source_id (PK), description, coverage_json (the machine-readable manifest below), first_event_at, last_event_at, event_count.

What each collector actually captures — and what it redacts

This is the part that matters most for an auditor asking "what PII touches storage." All three write into the same table above; their capture/redaction posture differs significantly.

claude-enterprise-cowork (watcher.py, tails an OTel collector's file export)

  • Captures: user prompts (event inferred, value hashed by the collector before reaching this store), tool/MCP invocations (name, success, duration, MCP scope), human approval decisions, per-request model/tokens/cost/duration/errors.
  • Redacted at the collector, upstream of this store, per otel-collector-config.yaml: full prompt text, tool input/parameters, user email, workspace path — this store never sees those in plaintext.
  • Does not capture: anything from a session where the admin hasn't configured an OTLP endpoint in Cowork Admin settings; sessions already open when that config was saved; traffic dropped by network egress allowlisting.
  • Caveat: event-type classification is inferred from which attributes are present, not read from a confirmed "event name" wire field (Anthropic's docs don't specify one) — flagged unknown rather than guessed when no pattern matches.

claude-code-cli (watcher_claude_code.py, tails a second, independent OTel collector)

  • Captures: prompt/response length only (text redacted unless an admin explicitly sets OTEL_LOG_USER_PROMPTS/OTEL_LOG_ASSISTANT_RESPONSES — not recommended, and this deployment's setup instructions say not to), tool results and human accept/reject decisions, per-request model/tokens/cost/duration/errors, permission-mode changes, MCP connection status, plugin events, model refusals (category redacted unless OTEL_LOG_TOOL_DETAILS is set).
  • Redacted: user.email is always masked by this collector regardless of the above env vars — defense-in-depth on top of Claude Code's own default redaction.
  • Does not capture: prompt/response text, tool inputs, file paths, raw API bodies (redacted by Claude Code itself by default — this adapter deliberately doesn't opt back in); Claude Code's separate metrics signal (only logs/events are ingested); anything from a session predating the telemetry env vars or outside the MDM rollout.
  • Caveat: not yet validated against a real Claude Code OTel export as of last verification (2026-07-24) — event-name inference is built from documented attribute signatures, unconfirmed against real wire data.

claude-enterprise-chat (poller_compliance.py, polls Anthropic's Compliance API directly — no OTel collector in this path)

  • Captures: chat lifecycle (created/viewed/updated/deleted/access-denied/settings/snapshot/ sharing), project lifecycle, project/chat document and file events, artifact events — scoped via a deliberate allowlist of ~40 claude_chat_*/claude_project_*/claude_file_*/ claude_artifact_* activity types out of the "hundreds" the Compliance API can return (auth, admin, SCIM sync, etc. are out of scope by design — this is AI-usage governance visibility, not a general security SIEM feed).
  • Redacted: actor.email_address, actor.unauthenticated_email_address, and actor.ip_address are hashed (hmac-sha256, same key the OTel collectors use) before being written anywhere, including raw_attributes — this API would otherwise hand back plaintext PII directly. actor_identity uses actor.user_id (Anthropic's documented stable, non-PII join key), never the email.
  • Retention/backfill: Anthropic retains Activity Feed records for 6 years and supports full backfill — this poller deliberately does not auto-backfill on first run (seeds from "now" forward) unless BACKFILL_ON_FIRST_RUN=1 is set; that's an operational choice here, not an API limitation.

Each collector's manifest above is generated by the collector itself (COVERAGE_MANIFEST in its source) and served live at GET /api/activity-sources/{source_id}/coverage — so this table is a snapshot of something that's also independently machine-readable at runtime, not the only copy of this information.


Supporting tables (not currently surfaced in the dashboard)

Exist in the schema, used internally, not documented field-by-field here:

  • Rules Engine (rules.sql): rule_schemas, rule_sets, rules, rule_history — customer- defined validation rules referenced by bundles.
  • Telemetry/billing (telemetry.sql): telemetry_reports, telemetry_queue — SCP's own phone-home usage telemetry for billing. Explicitly privacy-first by design: aggregate counts only, no PII, no agent names, no SCD content, no request parameters.
  • Deployment/usage/adherence (index_ledger.sql): events, deployments, ai_services, access, usage_events, adherence_reports, validations — bundle lifecycle and adherence tracking; per api-endpoints.md's API Status table, analytics on top of these tables is still TBD.

Next steps

  • See API Endpoints for how each table is read/written.
  • See demo/docs/audit-checklist-mapping.md (nextern-2h26 repo) for how this data maps to a real external audit checklist.