{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://cognitive-delivery.github.io/contract/1.x/audit-event.schema.json",
  "title": "CDF governance audit event",
  "description": "One line of the append-only governance journal. Rotated files carry the same shape. Deliberately holds a request hash rather than the request, and a sanitised summary rather than content: this file is retained indefinitely, so anything that reaches it is effectively permanent. CORRECTED against real artefacts: event_id and session_id are NOT required. A writer active between 2026-05-12 and 2026-05-15 emitted 61 codex.health.audit_probe records without them. The journal is append-only and retained indefinitely, so those records are permanent and a conformant reader will meet them. Rejecting real evidence would be worse than a slightly weaker schema. The current writer emits both.",
  "type": "object",
  "required": [
    "schema_version",
    "timestamp",
    "event_type"
  ],
  "properties": {
    "schema_version": {
      "type": "string"
    },
    "timestamp": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$",
      "description": "ISO 8601 with a timezone."
    },
    "event_id": {
      "type": "string",
      "description": "Optional for historical reasons only — see the schema description. Current writers emit it."
    },
    "event_type": {
      "type": "string",
      "description": "OPEN, unlike the CDI signal vocabulary. The journal records what happened; a product may record its own kinds of event."
    },
    "session_id": {
      "type": "string",
      "description": "Optional for historical reasons only — see the schema description. Current writers emit it."
    },
    "request_id": {
      "type": "string"
    },
    "spec_slug": {
      "type": "string"
    },
    "task_id": {
      "type": "string"
    },
    "phase": {
      "type": "string",
      "description": "An OPEN string. This field previously carried a closed six-value software lifecycle union, which meant a product with a different lifecycle could not record its own phase in the shared journal without lying. A reader must tolerate an unrecognised value: not throw, not coerce it to a known one, not drop it."
    },
    "actor": {
      "type": "object",
      "description": "Who or what acted. Records the KIND of actor and the model, never a person's name, email or git identity.",
      "required": [
        "kind"
      ],
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "human",
            "agent",
            "system"
          ]
        },
        "runtime": {
          "type": "string"
        },
        "model_vendor": {
          "type": "string"
        },
        "model_family": {
          "type": "string"
        },
        "model_version": {
          "type": "string"
        }
      },
      "additionalProperties": true
    },
    "summary": {
      "type": "string",
      "maxLength": 300,
      "description": "A sanitised one-line summary, at most 300 characters: the reference writer's own cap. Never prompt text."
    },
    "reasoning": {
      "type": "string",
      "maxLength": 500,
      "description": "Why the agent chose this action, at most 500 characters: the reference writer's own cap. Never prompt text."
    },
    "request_hash": {
      "type": "string",
      "description": "SHA-256 of the request, lower-case hex. NEVER the request itself; the pattern refuses anything that is not a 64-character digest.",
      "pattern": "^[0-9a-f]{64}$"
    },
    "outcome": {
      "type": "string",
      "enum": [
        "success",
        "failure",
        "partial"
      ]
    },
    "details": {
      "type": "object",
      "description": "Flat scalars only, and keys are filtered by name: a key whose segment is authorization, content, file, password, path, payload, prompt, request, secret or token is refused by the schema, mirroring the reference writer, which drops it before write. Do not defeat this by renaming a sensitive field.",
      "additionalProperties": {
        "type": [
          "string",
          "number",
          "boolean"
        ]
      },
      "propertyNames": {
        "description": "Mirrors the reference writer's rule, which splits a key on non-alphanumerics and camelCase boundaries and drops it when any segment is one of eleven words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.",
        "allOf": [
          {
            "not": {
              "pattern": "(^|[^A-Za-z0-9])(authorization|content|file|password|path|payload|prompt|request|secret|token)([^A-Za-z0-9]|$)"
            }
          },
          {
            "not": {
              "pattern": "[a-z0-9](Authorization|Content|File|Password|Path|Payload|Prompt|Request|Secret|Token)([^a-z]|$)"
            }
          },
          {
            "not": {
              "pattern": "^(authorization|content|file|password|path|payload|prompt|request|secret|token)[A-Z]"
            }
          }
        ]
      }
    }
  },
  "additionalProperties": true
}
