Skip to content

ADR-0003: AI Interaction Logging

Proposed Issue Tracker

Date: 2026-02-11 | Last Revised: 2026-03-08

Compliance: ISO 27001 (A.8.15 Logging, A.15.1 External Provider Relationships, A.8.6 Capacity Management)

Context

The Issue Tracker integrates AI across three distinct channels:

  1. Outbound AI calls — The application calls Claude's API to generate user stories and drive multi-turn feature planning conversations. These cross the system boundary to an external data processor.
  2. Inbound MCP tool invocations — External AI actors (Claude Code, IDE integrations) call the application's MCP server via OAuth tokens. 20 tools expose operations on issues, comments, time logs, epics, sprints, and branches.
  3. Inbound AI run webhooks — GitHub Actions AI runs report completion with cost, duration, and outcome data.

An ISO 27001 compliance review identified that none of these channels produce an auditable trail. An auditor cannot answer:

  • How many AI-generated stories were created, by whom, and when
  • Which MCP tools were invoked, by which token holder, and with what outcome
  • Whether AI run cost data has been tampered with after the fact

Decision

Three-Channel Logging Architecture

Each AI interaction channel gets a dedicated append-only, hash-chained log table. All three follow the patterns established in Audit Logging: per-table hash chains, explicit Action-level logging, and RequestContext for forensic metadata.

Channel 1: Outbound AI Calls (ai_outbound_logs)

Logs every call from the application to an external AI provider.

Trigger points:

  • GenerateStoryAction::execute() — structured story generation
  • AgentGenerateStoryAction::execute() — agentic story generation with tools
  • FeaturePlannerChatAction::execute() — multi-turn feature planning conversation

Schema

sql
-- ai_outbound_logs
id               BIGINT UNSIGNED   -- Auto-increment primary key
actor_type       TINYINT UNSIGNED  -- ActorType enum (consistent with ADR-0001)
user_id          BIGINT UNSIGNED   -- FK to users (nullable for system actors)
user_name        VARCHAR           -- Point-in-time snapshot
user_email       VARCHAR           -- Point-in-time snapshot
user_role        VARCHAR           -- Point-in-time snapshot
feature          VARCHAR NOT NULL  -- Feature identifier (e.g., 'story_generation')
provider         VARCHAR NOT NULL  -- AI provider (e.g., 'anthropic')
model            VARCHAR NOT NULL  -- Model used (e.g., 'claude-sonnet-4-5-20250929')
project_id       BIGINT UNSIGNED   -- Project context (nullable)
input_tokens     INT UNSIGNED      -- Token count sent (nullable)
output_tokens    INT UNSIGNED      -- Token count received (nullable)
response_time_ms INT UNSIGNED      -- Round-trip time in milliseconds (nullable)
status           VARCHAR NOT NULL  -- 'success' / 'error' / 'timeout'
error_message    VARCHAR           -- Error details (nullable)
ip_address       VARCHAR           -- Request origin
user_agent       VARCHAR           -- Client identification
url              VARCHAR           -- Request method + path
hash             CHAR(64) NOT NULL -- SHA-256 hash for tamper detection
created_at       TIMESTAMP NOT NULL -- Immutable (no updated_at)

What is NOT stored: Actual user input content (may contain sensitive business information), AI response content (large, not needed for compliance), or system prompts (stored separately, traceable via feature + project_id).

AiService prerequisite: AiService must be modified to return a response DTO that includes both the content and token usage metadata. All four methods (generate(), generateStructured(), generateWithTools(), generateConversational()) must return the DTO.

Channel 2: MCP Tool Invocations (ai_mcp_logs)

Logs every MCP tool call made by an external AI actor against the application.

Trigger point: Each MCP tool invocation — explicit per-tool logging, consistent with doctrine.

Schema

sql
-- ai_mcp_logs
id               BIGINT UNSIGNED   -- Auto-increment primary key
user_id          BIGINT UNSIGNED   -- FK to users (NOT NULL — MCP always has token owner)
user_name        VARCHAR NOT NULL  -- Point-in-time snapshot
user_email       VARCHAR NOT NULL  -- Point-in-time snapshot
user_role        VARCHAR NOT NULL  -- Point-in-time snapshot
token_id         BIGINT UNSIGNED   -- OAuth token used for authentication
tool_name        VARCHAR NOT NULL  -- MCP tool invoked (e.g., 'CreateIssueTool')
project_id       BIGINT UNSIGNED   -- Project context (nullable)
status           VARCHAR NOT NULL  -- 'success' / 'error'
error_message    VARCHAR           -- Error details (nullable)
ip_address       VARCHAR           -- Request origin
user_agent       VARCHAR           -- Client identification
url              VARCHAR           -- Request method + path
hash             CHAR(64) NOT NULL -- SHA-256 hash for tamper detection
created_at       TIMESTAMP NOT NULL -- Immutable (no updated_at)

Design notes:

  • user_id is NOT NULL — MCP calls always have an authenticated token owner
  • token_id enables "show me everything this token did" queries for security investigations
  • Tool parameters are NOT stored — they may contain sensitive data. The tool name + project context is sufficient for compliance; detailed parameters can be reconstructed from entity audit logs
  • MCP resource reads are NOT logged — only tool invocations (mutations + searches) are in scope
  • The 9 agent tools used by the Feature Planner are NOT in scope — they are internal tools covered by Channel 1's logging of the parent Action

Channel 3: AI Run Completions (ai_run_logs)

Logs every AI run completion as an immutable compliance record. Complements the mutable AiRun model with a tamper-resistant audit entry.

Trigger point: CompleteAiRunAction::execute() — after the business transaction commits.

Schema

sql
-- ai_run_logs
id               BIGINT UNSIGNED   -- Auto-increment primary key
actor_type       TINYINT UNSIGNED  -- ActorType enum (GitHubWebhook for completions)
user_id          BIGINT UNSIGNED   -- FK to users (nullable)
user_name        VARCHAR           -- Point-in-time snapshot
user_email       VARCHAR           -- Point-in-time snapshot
user_role        VARCHAR           -- Point-in-time snapshot
ai_run_id        BIGINT UNSIGNED   -- The AiRun being completed (NO FK — survives deletion)
issue_id         BIGINT UNSIGNED   -- Issue context (NO FK — survives deletion)
project_id       BIGINT UNSIGNED   -- Project context (NO FK — survives deletion)
status           TINYINT UNSIGNED  -- AiRunStatusEnum (Completed/Failed)
cost_usd         DECIMAL(8,4)      -- Reported cost in USD (nullable)
duration_seconds INT UNSIGNED      -- Time from started_at to completed_at (nullable)
branch_name      VARCHAR           -- Branch created (nullable)
pr_url           VARCHAR           -- Pull request URL (nullable)
error_message    VARCHAR           -- Error details (nullable)
ip_address       VARCHAR           -- Request origin (webhook caller)
user_agent       VARCHAR           -- Client identification
url              VARCHAR           -- Request method + path
hash             CHAR(64) NOT NULL -- SHA-256 hash for tamper detection
created_at       TIMESTAMP NOT NULL -- Immutable (no updated_at)

Design notes:

  • ai_run_id, issue_id, project_id have NO FK constraints — the audit log must survive deletion of referenced entities (consistent with Audit Logging)
  • cost_usd is the immutable compliance record of what was reported. The AiRun model's cost_usd is mutable business state.

Shared Infrastructure

All three tables reuse existing infrastructure from Audit Logging:

  • AuditLogWriter — Hash computation and record writing
  • RequestContext — IP, user agent, URL capture
  • ActorTypeEnum — User, Scheduler, Cli, GitHubWebhook
  • Same configurable hash seed, same append-only rules, same CHECK constraints

Table Rules (All Three Tables)

Consistent with Audit Logging:

  • Append-only — no UPDATE or DELETE operations
  • No updated_at — records are immutable
  • No SoftDeletes — logs cannot be deleted
  • Per-table hash chains — same algorithm, sequential within each table
  • CHECK constraints — actor integrity enforcement where applicable

Architecture Test Enforcement

php
// Outbound AI Actions must inject the AI outbound logger
it('outbound AI actions log all interactions', function () {
    // Verify GenerateStoryAction, AgentGenerateStoryAction,
    // FeaturePlannerChatAction depend on logger
});

// MCP tools must inject the MCP logger
it('MCP tools log all invocations', function () {
    // Verify all 20 MCP tool classes depend on logger
});

// AI log models are append-only
it('AI log models have no update or delete methods', function () {
    // Same pattern as audit logging append-only test
});

Implementation Sequence

  1. AiService modification — Return response DTO with content + token usage
  2. Channel 1: ai_outbound_logs — Migration, model, logger, integrate into 3 Actions
  3. Channel 3: ai_run_logs — Migration, model, logger, integrate into CompleteAiRunAction
  4. Channel 2: ai_mcp_logs — Migration, model, per-tool logging across 20 MCP tools

Channel 2 is last because it touches the most files and is least urgent (MCP mutations already produce entity audit logs — the MCP log adds "who invoked via MCP" metadata).

What This ADR Does NOT Cover

These concerns are out of scope — already resolved or separate concerns:

  • Rate limiting — Already implemented (throttle:story, throttle:mcp)
  • Input/output sanitization — Implemented in story generation Actions
  • Prompt injection hardening — Implemented via prompt injection guards

Options Considered

OptionVerdictReason
Single table with nullable columns per channelRejectedSchema becomes a forest of NULLs — each channel has fundamentally different columns. Violates per-entity table pattern.
Per-channel tables with per-table hash chainsAcceptedTight schemas, no cross-channel contention, independently verifiable chains.
Reuse existing audit tablesRejectedAI interactions have different data requirements than mutation audits. Forcing AI data into old_values/new_values JSON is awkward.

Consequences

Positive

  • ISO 27001 compliance across all three AI interaction channels
  • Per-channel tables provide tight schemas with no NULL pollution
  • Token usage tracking enables capacity management without estimated cost complexity
  • MCP tool invocation audit trail enables security investigations
  • Immutable AI run records make cost data tamper-resistant
  • Reuses existing audit infrastructure

Negative

  • Three new tables, three new models, three new logger integrations
  • AI service must be modified to expose token usage metadata
  • 20 MCP tool classes each require logger injection — repetitive but doctrinally consistent

Risks

  • If the AI SDK doesn't expose token usage, those columns will be NULL until SDK support is added (columns are nullable by design)
  • Hash chain contention under high MCP volume is possible but unlikely for a project management tool

Architecture documentation for contributors and collaborators.