openapi: 3.1.0
info:
  title: Witnora Control Plane API
  version: 1.0.0
  description: Project-scoped ingestion, runtime action decisions, verification, and evidence storage.
servers:
  - url: https://witnora.example.com
security:
  - bearerAuth: []
paths:
  /health:
    get:
      security: []
      summary: Check service health
      responses:
        "200": {description: Healthy control plane}
  /v1/config:
    get:
      security: []
      summary: Read public authentication configuration
      responses:
        "200": { description: Public control-plane configuration }
  /v1/webhook-test-receiver/{projectId}/{webhookId}:
    post:
      security: []
      summary: Receive a signed Witnora self-test webhook without retaining a second payload copy
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: webhookId, required: true, schema: {type: string, format: uuid}}
      responses:
        "204": {description: Signature, timestamp, and event binding verified}
        "401": {description: Signature or timestamp invalid}
  /v1/signing-keys/current:
    get:
      security: []
      summary: Read the current Ed25519 evidence verification key
      responses:
        "200": {description: Current public signing key and key ID}
        "503": {description: Server evidence signing is not configured}
  /v1/signing-keys:
    get:
      security: []
      summary: List active, retired, and revoked evidence verification keys
      responses:
        "200": {description: Durable public signing-key history}
  /v1/signing-keys/{keyId}:
    get:
      security: []
      summary: Resolve the exact public key named by an evidence attestation
      parameters:
        - {in: path, name: keyId, required: true, schema: {type: string}}
      responses:
        "200": {description: Historical public signing key}
        "404": {description: Signing key not found}
  /v1/public/assurance-reports/{publicId}:
    get:
      security: []
      summary: Verify an explicitly published, scoped assurance report and its lifecycle history
      parameters:
        - {in: path, name: publicId, required: true, schema: {type: string}}
      responses:
        "200": {description: Published assurance report with signed payload and sanitized decision history}
        "404": {description: Report is private, absent, or no longer has a published record}
  /v1/notification-destinations/verify:
    get:
      security: []
      summary: Verify ownership of an incident-alert email address
      parameters:
        - {in: query, name: token, required: true, schema: {type: string}}
      responses:
        "200": {description: Destination activated}
        "400": {description: Verification token invalid or expired}
  /v1/me/capabilities:
    get:
      summary: Read signed-in operator capabilities
      responses: {"200": {description: Platform admin and cryptographic feature flags}}
  /v1/onboarding/bootstrap:
    post:
      summary: Create the signed-in user's first organization and project
      responses:
        "200": { description: Existing or newly created workspace }
  /v1/projects:
    get:
      summary: List projects visible to the signed-in user
      responses:
        "200": { description: Project list }
    post:
      summary: Create an isolated project in the signed-in user's organization
      responses:
        "201": { description: Created project }
        "409": { description: Organization project limit reached }
  /v1/projects/{projectId}:
    patch:
      summary: Rename a project while preserving its stable slug and ID
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Renamed project }
    delete:
      summary: Move an archived project into the 30-day recovery window
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Project retained in Recently Deleted until its scheduled erasure time }
        "409": { description: Project must be archived first }
  /v1/projects/{projectId}/archive:
    post:
      summary: Stop a project, revoke active credentials, and retain its data read-only
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Archived project }
  /v1/projects/{projectId}/restore:
    post:
      summary: Restore an archived or recently deleted project without reactivating revoked credentials
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Restored project; Agents must reconnect with new credentials }
  /v1/projects/{projectId}/erase:
    post:
      summary: Permanently erase a project already in Recently Deleted
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirmation, acknowledgePermanentErasure]
              properties:
                confirmation: {type: string, description: Exact project name}
                acknowledgePermanentErasure: {type: boolean, enum: [true]}
      responses:
        "200": { description: Project erasure receipt }
        "409": { description: Project is not in Recently Deleted or a legal hold blocks erasure }
  /v1/projects/{projectId}/real-path-integrations/provider-packs:
    get:
      summary: List fixed read-only provider integration packs
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": {description: Provider contracts, allowlisted fields, credential scopes, and safe criteria}
  /v1/projects/{projectId}/real-path-integrations/composer:
    get:
      summary: Read Nora's authoritative real-path composition inputs
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": {description: Active Business Tasks, signed-discovered tools, provider packs, prior plans, and limitations}
  /v1/projects/{projectId}/real-path-integrations:
    get:
      summary: List confirmed real-path integration plans
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": {description: Project-scoped plans}
    post:
      summary: Confirm business answers and generate a bounded real-path activation plan
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": {description: Plan bound to the active Business Task and signed discovery}
        "409": {description: Agent version or signed tool binding is not established}
  /v1/projects/{projectId}/onboarding:
    get:
      summary: Compute key creation, CLI authentication, and first-evidence progress
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Three-step onboarding status with recovery guidance }
  /v1/projects/{projectId}/setup-plans:
    get:
      summary: Read the current deterministic guided setup plan and audit events
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Setup plan snapshot}}
    post:
      summary: Create a typed guided setup plan from a workflow description
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"201": {description: Setup plan created}}
  /v1/projects/{projectId}/setup-plans/{setupPlanId}/messages:
    post:
      summary: Update setup intent through deterministic or configured model interpretation
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {name: setupPlanId, in: path, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Updated setup plan snapshot}}
  /v1/projects/{projectId}/setup-plans/{setupPlanId}/authorize:
    post:
      summary: Record explicit authorization for one allowlisted setup operation
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {name: setupPlanId, in: path, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Authorized setup plan snapshot}}
  /v1/projects/{projectId}/collector-keys:
    get:
      summary: List customer-owned collector public keys and rotation state
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Active, retired, and revoked collector public keys}}
    post:
      summary: Register or rotate an Ed25519 collector public key
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": {description: Public source key registered}
        "409": {description: Key ID or rotation predecessor conflicts with stored state}
  /v1/projects/{projectId}/collector-keys/{keyId}:
    delete:
      summary: Revoke a customer-owned collector source key
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: keyId, required: true, schema: {type: string}}
      responses:
        "200": {description: Source key revoked}
        "404": {description: Source key not found}
  /v1/projects/{projectId}/trusted-runs/{runId}/records:
    post:
      summary: Append and acknowledge a source-signed hash-linked record batch
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: runId, required: true, schema: {type: string}}
        - {in: header, name: Idempotency-Key, required: true, schema: {type: string}}
      responses:
        "202": {description: Highest accepted sequence/hash ACK plus replay counts and alerts}
        "401": {description: Source key or signature is invalid}
        "409": {description: Sequence, replay, key, or hash-chain conflict}
  /v1/projects/{projectId}/trusted-runs/{runId}/reconcile:
    post:
      summary: Reconcile a source-signed run receipt and add server attestation
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: runId, required: true, schema: {type: string}}
        - {in: header, name: Idempotency-Key, required: true, schema: {type: string}}
      responses:
        "200": {description: Source receipt comparison and Witnora server attestation}
        "409": {description: Receipt does not match accepted records}
  /v1/projects/{projectId}/collector-heartbeats:
    post:
      summary: Record a source-signed collector heartbeat and pending queue depth
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "202": {description: Heartbeat verified and stored}
        "401": {description: Heartbeat source key or signature is invalid}
  /v1/projects/{projectId}/collector-status:
    get:
      summary: Read source keys, trusted runs, heartbeat freshness, and dropped-event alerts
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Remote collector operational and reconciliation state}}
  /v1/projects/{projectId}/pilot-feedback:
    get:
      summary: List bounded onboarding feedback for project owners and admins
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Pilot feedback records }
    post:
      summary: Record a project member's onboarding friction or completion signal
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": { description: Feedback recorded }
  /v1/projects/{projectId}/assurance-cases:
    get:
      summary: List scoped assurance cases visible to the project
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Assurance case list}}
    post:
      summary: Create and cryptographically lock an assurance evaluation plan
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/AssuranceCaseInput"}}}
      responses:
        "201": {description: Draft assurance case and immutable creation decision}
        "422": {description: Evaluation plan is incomplete or invalid}
  /v1/projects/{projectId}/assurance-cases/{caseId}:
    get:
      summary: Read an assurance case and its immutable decision ledger
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: caseId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Assurance case detail}, "404": {description: Case not found}}
  /v1/projects/{projectId}/assurance-cases/{caseId}/{transition}:
    post:
      summary: Apply an authorized assurance lifecycle transition
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: caseId, required: true, schema: {type: string, format: uuid}}
        - {in: path, name: transition, required: true, schema: {enum: [start, baseline, remediation, retest, submit, return, issue, suspend, revoke, expire, resume, revalidate, activate-continuous]}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/AssuranceTransitionInput"}}}
      responses:
        "200": {description: Updated case and appended decision}
        "403": {description: Human role or independent reviewer separation required}
        "409": {description: Invalid or concurrent lifecycle transition}
        "422": {description: Required project evidence is missing}
  /v1/projects/{projectId}/mutations:
    get:
      summary: List governed agent mutations and signed promotion receipts
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Mutation and receipt history}}
    post:
      summary: Register an evolution manifest and invalidate the current assured scope
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "../../schemas/witnora-evolution-manifest.schema.json"}}}
      responses:
        "201": {description: Immutable mutation awaiting human review}
        "409": {description: Parent is not the current assured artifact}
        "422": {description: Evolution manifest is invalid}
  /v1/projects/{projectId}/mutations/{mutationId}/promote:
    post:
      summary: Review capability changes and issue a signed single-use promotion grant
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: mutationId, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/PromotionReviewInput"}}}
      responses:
        "200": {description: Reviewed mutation and signed bounded grant}
        "403": {description: Project owner or administrator required}
        "409": {description: Mutation was already reviewed or has an active grant}
  /v1/projects/{projectId}/promotion-grants/{grantId}/claim:
    post:
      summary: Atomically claim a promotion grant before deployment
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/PromotionClaimInput"}}}
      responses:
        "200": {description: Claimed grant or idempotent retry}
        "409": {description: Expired, consumed, or replayed grant}
  /v1/projects/{projectId}/promotion-grants/{grantId}/exchange-deployment-credential:
    post:
      summary: Exchange one active Promotion Grant and verified GitHub OIDC identity for a short-lived deployment credential
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/DeploymentCredentialExchangeInput"}}}
      responses:
        "201": {description: Credential and one-time bearer token returned}
        "403": {description: OIDC repository, workflow, or commit binding rejected}
        "409": {description: Grant expired, replayed, or exchanged concurrently}
  /v1/projects/{projectId}/mutations/{mutationId}/rollback-credential:
    post:
      summary: Issue an owner/admin rollback credential bound to the declared parent artifact
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: mutationId, required: true, schema: {type: string, format: uuid}}
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/ExceptionalDeploymentCredentialInput"}}}}
      responses: {"201": {description: Short-lived rollback credential}, "403": {description: Owner or administrator required}}
  /v1/projects/{projectId}/mutations/{mutationId}/break-glass-credential:
    post:
      summary: Issue an owner-only break-glass credential and suspend continuous assurance
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: mutationId, required: true, schema: {type: string, format: uuid}}
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/ExceptionalDeploymentCredentialInput"}}}}
      responses: {"201": {description: Short-lived break-glass credential}, "403": {description: Project owner required}}
  /v1/projects/{projectId}/deployment-credentials/{credentialId}/revoke:
    post:
      summary: Revoke an unclaimed deployment credential
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: credentialId, required: true, schema: {type: string, format: uuid}}
      requestBody: {required: true, content: {application/json: {schema: {type: object, required: [reason], properties: {reason: {type: string, minLength: 1}}}}}}
      responses: {"200": {description: Credential revoked and signed event retained}, "409": {description: Credential already claimed, expired, or revoked}}
  /v1/projects/{projectId}/deployment-enforcement:
    get:
      summary: List signed deployment admission, denial, rollback, revocation, and break-glass events
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: query, name: mutationId, schema: {type: string, format: uuid}}
      responses: {"200": {description: Deployment enforcement timeline}}
  /v1/runtime/projects/{projectId}/deployment-credentials/{credentialId}/admit:
    post:
      summary: Fail-closed customer deployment boundary admission using the one-time bearer credential
      security: [{bearerAuth: []}]
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: credentialId, required: true, schema: {type: string, format: uuid}}
      requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/DeploymentAdmissionInput"}}}}
      responses:
        "200": {description: Exact adapter, artifact, and target admitted}
        "401": {description: Missing or invalid deployment credential}
        "409": {description: Binding mismatch, replay, expiry, or revocation}
  /v1/projects/{projectId}/promotion-grants/{grantId}/verify-deployment:
    post:
      summary: Independently verify deployed artifact bytes and issue a signed receipt
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/DeploymentObservationInput"}}}
      responses:
        "200": {description: Signed promotion or digest-mismatch receipt}
        "409": {description: Grant was not claimed or promotion was already finalized}
        "422": {description: Observer is not independent or target binding is invalid}
  /v1/projects/{projectId}/overview:
    get:
      summary: Read current assurance, protected business outcomes, the role-aware next action, operational counts, and recent activity
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": { description: Project overview }
  /v1/projects/{projectId}/nora/activation-plan:
    get:
      summary: Read the derived Agent, version, environment, and approved task scope needed by the customer-owned Gateway for continuous assurance
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": {description: Metadata-only activation plan derived from authoritative records}
  /v1/projects/{projectId}/operations:
    get:
      summary: Read Trust Operations incidents, 30/90-day SLO, multi-window burn rate, durable delivery queues, trends, and signing-key state
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "200": {description: Trust Operations v0.5 status}
  /v1/projects/{projectId}/operations/smoke-runs:
    post:
      summary: Record a production smoke health sample
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/TrustHealthSampleInput"}
      responses:
        "201": {description: Persisted health sample and incident lifecycle result}
  /v1/projects/{projectId}/operational-incidents/{incidentId}/acknowledge:
    post:
      summary: Acknowledge an open production incident and begin investigation
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: incidentId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Incident moved to investigating with transition evidence}
        "409": {description: Incident is not open}
  /v1/projects/{projectId}/operational-incidents/{incidentId}/resolve:
    post:
      summary: Resolve a recovered incident after human review
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: incidentId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Incident resolved with operator rationale}
        "409": {description: Two consecutive passing smokes have not recovered the incident}
  /v1/projects/{projectId}/notification-destinations:
    get:
      summary: List verified and pending project alert recipients
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Notification destinations without verification-token hashes}}
    post:
      summary: Send an ownership-verification email for a project alert recipient
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"201": {description: Pending notification destination}}
  /v1/projects/{projectId}/notification-destinations/{destinationId}:
    delete:
      summary: Disable an incident-alert recipient
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: destinationId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Disabled destination}}
  /v1/projects/{projectId}/notification-jobs/{jobId}/retry:
    post:
      summary: Retry a dead-letter email notification job
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: jobId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Notification job returned to the pending queue}
        "409": {description: Notification job is not in the dead-letter queue}
  /v1/projects/{projectId}/envelopes:
    post:
      summary: Ingest a framework-neutral event or action envelope
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "../../schemas/agentcert-universal-envelope.schema.json"}
      responses:
        "202": {description: Envelope mapped into a run event or policy-evaluated action}
        "409": {description: Idempotency key reused with a different body}
        "422": {description: Envelope validation failed}
  /v1/projects/{projectId}/agents:
    get:
      summary: List registered agents
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Agent list}}
    post:
      summary: Register or update an agent identity and permissions as a human owner or admin
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/AgentInput"}
      responses: {"201": {description: Registered agent}}
  /v1/projects/{projectId}/runs:
    get:
      summary: List assurance runs
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Run list}}
    post:
      summary: Start an assurance run
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/RunInput"}
      responses: {"201": {description: Started run}}
  /v1/projects/{projectId}/runs/{runId}:
    get:
      summary: Read a run and its ordered events
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/RunId"}
      responses: {"200": {description: Run detail}}
  /v1/projects/{projectId}/runs/{runId}/analysis:
    get:
      summary: Read the unified run, trace, action, approval, evidence, incident, and human-review workspace
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/RunId"}
      responses: {"200": {description: Project-scoped run analysis}}
  /v1/projects/{projectId}/observability:
    get:
      summary: Read a bounded assurance observability window
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: query, name: days, required: false, schema: {type: integer, enum: [7, 30, 90], default: 30}}
      responses:
        "200": {description: Run, fault, policy, approval, risk, and outcome aggregates with explicit truncation flags}
        "403": {description: The caller lacks runs:read or actions:read}
        "422": {description: Unsupported observation period}
  /v1/projects/{projectId}/semantics/coverage:
    get:
      summary: Read bounded observed, semantic, enforced, and verified capability coverage
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: query, name: days, required: false, schema: {type: integer, enum: [7, 30, 90], default: 30}}
      responses:
        "200": {description: Semantic coverage v0.1 with evidence strength, bypass risks, domains, and unknown capabilities}
        "403": {description: The caller lacks runs:read or actions:read}
        "422": {description: Unsupported semantic coverage period}
  /v1/projects/{projectId}/semantics/manifests:
    get:
      summary: List immutable built-in and project capability manifests
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Capability registry v0.1}}
    post:
      summary: Create or update a project capability manifest
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: "#/components/schemas/CapabilityManifest"}}}
      responses:
        "200": {description: Stored project capability manifest}
        "409": {description: Attempted replacement of a built-in capability ID}
        "422": {description: Manifest validation failed}
  /v1/projects/{projectId}/semantics/corrections:
    get:
      summary: List human-confirmed capability mappings
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Project capability correction ledger}}
  /v1/projects/{projectId}/semantics/unknown/{unknownKey}/suggest:
    post:
      summary: Request an advisory capability mapping from the optional classifier
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: unknownKey, required: true, schema: {type: string, pattern: "^[0-9a-f]{24}$"}}
      responses:
        "200": {description: Advisory suggestion that has not been persisted}
        "409": {description: Optional semantic classifier is disabled}
  /v1/projects/{projectId}/semantics/unknown/{unknownKey}/review:
    post:
      summary: Persist a human-confirmed capability mapping and reviewer rationale
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: unknownKey, required: true, schema: {type: string, pattern: "^[0-9a-f]{24}$"}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [capabilityId, rationale, confidence]
              properties:
                capabilityId: {type: string}
                rationale: {type: string, minLength: 1, maxLength: 1000}
                confidence: {type: number, minimum: 0, maximum: 1}
      responses:
        "200": {description: Attributed project capability correction}
        "404": {description: Unknown capability is absent from the current 30-day queue}
        "422": {description: Capability ID, rationale, or confidence is invalid}
  /v1/projects/{projectId}/runs/{runId}/failure-reviews:
    post:
      summary: Human-confirm or correct a generated failure taxonomy label
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/RunId"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/FailureReviewInput"}
      responses:
        "200": {description: Created or updated review for the run and pattern key}
        "403": {description: Machine API keys cannot write human reviews}
  /v1/projects/{projectId}/runs/{runId}/events:
    post:
      summary: Append one to 500 ordered events
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/RunId"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/EventBatch"}
      responses: {"202": {description: Accepted events}}
  /v1/projects/{projectId}/runs/{runId}/complete:
    post:
      summary: Complete a run and open an incident when it failed
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/RunId"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/RunCompletion"}
      responses: {"200": {description: Completed run}}
  /v1/projects/{projectId}/identity-providers:
    get:
      summary: List project-scoped OIDC and SPIFFE trust configurations
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Identity provider list without credentials}}
    post:
      summary: Register an exact issuer, audience, algorithm, and trust-domain policy
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": {description: Identity provider registered}
        "422": {description: Provider trust policy is incomplete or unsafe}
  /v1/projects/{projectId}/identity-assertions:
    get:
      summary: List sanitized verified principals retained for this project
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Verified principals containing digests but no raw credentials}}
    post:
      summary: Verify an OIDC token, SPIFFE JWT-SVID, or X.509-SVID
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": {description: Sanitized identity assertion created}
        "401": {description: Signature, issuer, audience, tenant, expiry, or trust domain rejected}
        "409": {description: Credential replay detected}
  /v1/projects/{projectId}/actions:
    get:
      summary: List runtime actions
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Action list}}
    post:
      summary: Assess a proposed action against permissions and policy
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ActionInput"}
      responses: {"201": {description: ALLOW, DENY, or REQUIRE_APPROVAL decision}}
  /v1/projects/{projectId}/actions/{actionId}:
    get:
      summary: Read current action decision and verification state
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ActionId"}
      responses: {"200": {description: Action state}}
  /v1/projects/{projectId}/actions/{actionId}/approve:
    post:
      summary: Approve a pending action with a human owner, admin, or reviewer session
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ActionId"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ReviewInput"}
      responses: {"200": {description: Approved action}, "403": {description: API keys cannot approve actions}}
  /v1/projects/{projectId}/actions/{actionId}/reject:
    post:
      summary: Reject a pending action with a human owner, admin, or reviewer session
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ActionId"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ReviewInput"}
      responses: {"200": {description: Rejected action}}
  /v1/projects/{projectId}/actions/{actionId}/verify:
    post:
      summary: Compare observed state with expected state and open an incident on mismatch
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ActionId"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/VerificationInput"}
      responses: {"200": {description: Verification result}}
  /v1/projects/{projectId}/runtime-identities:
    get:
      summary: List tenant-bound Onegent Browser runtime identities
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Runtime identity list}}
    post:
        summary: Register an Ed25519 Onegent Browser runtime identity
        parameters: [{$ref: "#/components/parameters/ProjectId"}]
        responses: {"201": {description: Immutable runtime identity registered}}
  /v1/projects/{projectId}/runtime-identities/{runtimeIdentityId}/status:
    post:
      summary: Suspend, revoke, or mark a Browser runtime identity compromised with an effective timestamp
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: runtimeIdentityId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Runtime trust status updated}, "409": {description: Invalid lifecycle transition}}
  /v1/projects/{projectId}/actions/{actionId}/execution-grant:
    post:
      summary: Issue a short-lived, one-use Browser execution grant bound to an approved action
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ActionId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      responses:
        "201": {description: Hosted-signed ExecutionGrant}
        "409": {description: Mandate, approval, runtime, adapter, or action binding is not valid}
  /v1/projects/{projectId}/execution-grants/{grantId}/revoke:
    post:
      summary: Revoke an unclaimed Browser execution grant
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Grant revoked}, "409": {description: Grant is no longer issuable}}
  /v1/runtime/projects/{projectId}/execution-grants/{grantId}/claim:
    post:
      security: []
      summary: Atomically claim a one-use grant with a signed RuntimeClaim
      description: Authentication is the tenant-registered runtime Ed25519 signature, not a normal project API key.
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      responses: {"201": {description: Grant claimed and execution session created}, "409": {description: Replay, expiry, revocation, or binding mismatch}}
  /v1/runtime/projects/{projectId}/execution-grants/{grantId}/consume:
    post:
      security: []
      summary: Mark a claimed grant consumed using the original signed RuntimeClaim
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: grantId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Grant consumed}, "409": {description: Session or grant transition mismatch}}
  /v1/runtime/projects/{projectId}/execution-attempts/{executionAttemptId}/phase:
    post:
      security: []
      summary: Persist a signed runtime execution-attempt phase transition
      description: The runtime identity signature and attempt binding authorize the transition. Unknown dispatch cannot support ENFORCED evidence.
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: executionAttemptId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Persisted execution-attempt state}
        "409": {description: Invalid, duplicate, or stale phase transition}
  /v1/runtime/projects/{projectId}/execution-attempts/{executionAttemptId}/recover:
    post:
      security: []
      summary: Claim a bounded recovery lease and fail closed from the persisted attempt phase
      description: Recovery never blindly redispatches a target request. Post-dispatch uncertainty requires independent reconciliation.
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: executionAttemptId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Recovery decision and leased execution-attempt state}
        "409": {description: Recovery is already leased or the attempt is terminal}
  /v1/runtime/projects/{projectId}/execution-sessions/{sessionId}/evidence:
    post:
      security: []
      summary: Submit the signed Browser session, event chain, outcome, and reconciliation bundle
      description: Hosted derives the enforcement level centrally; the caller cannot select ENFORCED.
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: sessionId, required: true, schema: {type: string, format: uuid}}
      responses: {"202": {description: Evidence classified and session finalized}, "409": {description: Evidence binding or trust verification failed}}
  /v1/projects/{projectId}/incidents:
    get:
      summary: List failed runs and runtime verification incidents
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Incident list}}
  /v1/projects/{projectId}/evidence:
    get:
      summary: List project evidence metadata
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Evidence list}}
    post:
      summary: Upload a bounded, allowlisted evidence artifact
      description: Accepts PNG, JPEG, WebP, JSON, JSONL, HTML, PDF, or ZIP subject to per-run and per-project storage quotas.
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: query, name: fileName, required: true, schema: {type: string}}
        - {in: query, name: kind, schema: {enum: [evidence_bundle, screenshot, trace, dom, json, report, artifact]}}
        - {in: query, name: schemaVersion, schema: {type: string}}
        - {in: query, name: runId, schema: {type: string, format: uuid}}
        - {in: query, name: actionId, schema: {type: string, format: uuid}}
        - {in: query, name: sourcePath, description: Original bundle pointer for companion-artifact correlation, schema: {type: string, maxLength: 1024}}
      requestBody:
        required: true
        content:
          application/octet-stream: {schema: {type: string, contentEncoding: binary}}
      responses:
        "201": {description: Evidence metadata with SHA-256 digest and retention metadata}
        "413": {description: Per-run or per-project evidence quota exceeded}
        "415": {description: Unsupported, mislabeled, malformed, or executable evidence artifact}
        "422": {description: Companion path, SHA-256, byte size, or kind does not match the stored bundle manifest}
  /v1/projects/{projectId}/evidence/{evidenceId}/content:
    get:
      summary: Download private evidence through the authenticated control plane
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/EvidenceId"}
      responses: {"200": {description: Original evidence bytes}}
  /v1/projects/{projectId}/api-keys:
    get:
      summary: List project API key metadata without secret hashes
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: API key metadata}}
    post:
      summary: Create a project-scoped agent credential; the secret is returned once
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ApiKeyInput"}
      responses: {"201": {description: API key and one-time secret}}
  /v1/projects/{projectId}/api-keys/{apiKeyId}:
    delete:
      summary: Revoke a project API key
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {$ref: "#/components/parameters/ApiKeyId"}
      responses: {"200": {description: Revoked API key metadata}}
  /v1/projects/{projectId}/webhooks:
    get:
      summary: List webhook registrations and recent delivery outcomes
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Webhooks and deliveries}}
    post:
      summary: Register an HTTPS HMAC-signed webhook
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, eventTypes]
              properties:
                url: {type: string, format: uri}
                eventTypes: {type: array, minItems: 1, maxItems: 20, items: {type: string}}
      responses: {"201": {description: Webhook metadata and one-time signing secret}}
  /v1/projects/{projectId}/webhooks/{webhookId}:
    delete:
      summary: Revoke a webhook
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: webhookId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Revoked webhook}}
  /v1/projects/{projectId}/webhooks/test-receiver:
    post:
      summary: Idempotently enable the Witnora signed self-test receiver
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses:
        "201": {description: Existing or newly created self-test webhook}
  /v1/projects/{projectId}/webhook-jobs/{jobId}/retry:
    post:
      summary: Requeue a dead-letter webhook job as a human owner or admin
      parameters:
        - {$ref: "#/components/parameters/ProjectId"}
        - {in: path, name: jobId, required: true, schema: {type: string, format: uuid}}
      responses:
        "200": {description: Job returned to the pending queue}
        "409": {description: Job is not in the dead-letter queue}
  /v1/projects/{projectId}/retention-report:
    get:
      summary: Export evidence retention, legal hold, and deletion-journal state
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Retention report v0.1}}
  /v1/projects/{projectId}/legal-holds:
    get:
      summary: List legal hold applications and decisions for a project
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      responses: {"200": {description: Legal hold request history}}
    post:
      summary: Apply for enterprise legal hold; retention continues until platform approval
      parameters: [{$ref: "#/components/parameters/ProjectId"}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties: {reason: {type: string, minLength: 20, maxLength: 2000}}
      responses:
        "201": {description: Legal hold application created}
        "409": {description: Project already has a requested or approved hold}
  /v1/admin/legal-hold-requests:
    get:
      summary: List pending legal hold applications (platform administrators only)
      responses: {"200": {description: Pending applications}}
  /v1/admin/pilot-report:
    get:
      summary: Aggregate platform pilot conversion, timing, and onboarding friction for a project cohort
      parameters:
        - {in: query, name: days, required: false, schema: {type: integer, enum: [7, 30, 90], default: 30}}
      responses:
        "200": {description: Witnora pilot funnel v0.2 report ending at first CURRENT}
        "403": {description: Platform administrator access required}
        "422": {description: Unsupported reporting period}
  /v1/admin/legal-hold-requests/{requestId}/{decision}:
    post:
      summary: Approve, reject, or release a legal hold (platform administrators only)
      parameters:
        - {in: path, name: requestId, required: true, schema: {type: string, format: uuid}}
        - {in: path, name: decision, required: true, schema: {enum: [approve, reject, release]}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reviewNote]
              properties: {reviewNote: {type: string, minLength: 10, maxLength: 2000}}
      responses:
        "200": {description: Updated legal hold decision}
        "403": {description: Platform administrator access required}
        "409": {description: Invalid state transition or requester attempted self-approval}
  /v1/admin/legal-hold-requests/{requestId}/report:
    get:
      summary: Export the subject project's retention report (platform administrators only)
      parameters:
        - {in: path, name: requestId, required: true, schema: {type: string, format: uuid}}
      responses: {"200": {description: Retention report v0.1}, "403": {description: Platform administrator access required}}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Supabase user access token or project-scoped ac_live API key.
  parameters:
    ProjectId: {in: path, name: projectId, required: true, schema: {type: string, format: uuid}}
    RunId: {in: path, name: runId, required: true, schema: {type: string, format: uuid}}
    ActionId: {in: path, name: actionId, required: true, schema: {type: string, format: uuid}}
    EvidenceId: {in: path, name: evidenceId, required: true, schema: {type: string, format: uuid}}
    ApiKeyId: {in: path, name: apiKeyId, required: true, schema: {type: string, format: uuid}}
    IdempotencyKey: {in: header, name: Idempotency-Key, required: false, schema: {type: string, minLength: 1, maxLength: 200}}
  schemas:
    CapabilityManifest:
      $ref: "../../schemas/agentcert-capability-manifest.schema.json"
    AssuranceCaseInput:
      type: object
      required: [name, subject, policyPackVersion, evaluationPlan]
      properties:
        name: {type: string}
        subject:
          type: object
          required: [id, name, kind]
          properties: {id: {type: string}, name: {type: string}, version: {type: string}, kind: {type: string}}
        policyPackVersion: {type: string}
        evaluationPlan:
          type: object
          required: [controls]
          properties:
            requiredEvidenceKinds: {type: array, items: {type: string}}
            limitations: {type: array, items: {type: string}}
            controls:
              type: array
              minItems: 1
              items:
                type: object
                required: [id, title, mode]
                properties: {id: {type: string}, title: {type: string}, mode: {enum: [automated, evidence_required, manual]}}
        engagement:
          type: object
          description: Optional fixed-scope 7-Day Assurance Review. Terms and due date are set by the server and the plan is immutable after creation.
          required: [customer, sandbox, workflow]
          properties:
            integrationStartedAt: {type: string, format: date-time}
            customer:
              type: object
              required: [name]
              properties: {name: {type: string}, contactEmail: {type: string, format: email}}
            sandbox:
              type: object
              required: [name, kind]
              properties: {name: {type: string}, kind: {type: string}, baseUrl: {type: string, format: uri}}
            workflow:
              type: object
              required: [name, description, highRiskAction, expectedOutcome]
              properties:
                name: {type: string}
                description: {type: string}
                highRiskAction: {enum: [SUBMIT, PAY, SEND, UPDATE]}
                expectedOutcome: {type: object, minProperties: 1}
        continuousAssurance:
          type: object
          description: Optional declared validity boundary. Issuance makes this scope CURRENT; authoritative release and nightly runs can invalidate it.
          required: [scope]
          properties:
            scope: {$ref: "../../schemas/agentcert-assurance-scope.schema.json"}
    AssuranceTransitionInput:
      type: object
      required: [reason]
      properties:
        reason: {type: string}
        evidenceIds: {type: array, uniqueItems: true, items: {type: string, format: uuid}}
        expiresAt: {type: string, format: date-time}
        publish: {type: boolean, default: false}
        verdict: {enum: [RELEASE, RELEASE_WITH_CONTROLS, BLOCK]}
        rationale: {type: string}
        firstDivergence: {type: string}
        authorizationGaps: {type: array, items: {type: string}}
        controlsRequired: {type: array, items: {type: string}}
        limitations: {type: array, items: {type: string}}
        outcome:
          type: object
          properties: {observed: {type: object}, verified: {type: boolean}}
        items:
          type: array
          description: Remediation items used only with the remediation transition.
          items: {type: object}
    PromotionReviewInput:
      type: object
      required: [rationale]
      properties:
        approvedAdditions: {type: array, uniqueItems: true, items: {type: string}}
        rationale: {type: string}
        ttlSeconds: {type: integer, minimum: 60, maximum: 3600, default: 900}
    PromotionClaimInput:
      type: object
      required: [idempotencyKey]
      properties:
        idempotencyKey: {type: string}
    DeploymentObservationInput:
      type: object
      required: [observerId, observerKind, artifactDigestSha256, deploymentUri]
      properties:
        observerId: {type: string}
        observerKind: {enum: [registry, deployment_api, customer_probe, independent_service]}
        artifactDigestSha256: {type: string, pattern: "^[a-f0-9]{64}$"}
        deploymentUri: {type: string, format: uri}
        observedAt: {type: string, format: date-time}
    DeploymentCredentialExchangeInput:
      type: object
      required: [providerId, oidcToken, adapters, rationale, idempotencyKey]
      properties:
        providerId: {type: string, format: uuid}
        oidcToken: {type: string, description: Verified in memory and never persisted}
        adapters: {type: array, minItems: 1, uniqueItems: true, items: {enum: [github_environment, kubernetes_admission, customer_gateway]}}
        rationale: {type: string, minLength: 1}
        idempotencyKey: {type: string, minLength: 1}
        ttlSeconds: {type: integer, minimum: 30, maximum: 900, default: 300}
    ExceptionalDeploymentCredentialInput:
      type: object
      required: [adapters, rationale]
      properties:
        adapters: {type: array, minItems: 1, uniqueItems: true, items: {enum: [github_environment, kubernetes_admission, customer_gateway]}}
        artifactDigestSha256: {type: string, pattern: "^[a-f0-9]{64}$", description: Required for break glass; rollback always uses the declared parent}
        rationale: {type: string, minLength: 1}
        ttlSeconds: {type: integer, minimum: 30, maximum: 900}
    DeploymentAdmissionInput:
      type: object
      required: [adapter, artifactDigestSha256, deploymentUri, idempotencyKey]
      properties:
        adapter: {enum: [github_environment, kubernetes_admission, customer_gateway]}
        artifactDigestSha256: {type: string, pattern: "^[a-f0-9]{64}$"}
        deploymentUri: {type: string, minLength: 1}
        idempotencyKey: {type: string, minLength: 1}
    TrustHealthSampleInput:
      type: object
      required: [externalId, source, status, startedAt, completedAt]
      properties:
        externalId: {type: string}
        source: {enum: [production_smoke, manual]}
        status: {enum: [passed, failed]}
        startedAt: {type: string, format: date-time}
        completedAt: {type: string, format: date-time}
        checks: {type: array, items: {type: string}}
        error: {type: string, maxLength: 2000}
        workflowRunId: {type: string}
        workflowRunUrl: {type: string, format: uri}
    AgentInput:
      type: object
      required: [externalId, name]
      properties:
        externalId: {type: string}
        name: {type: string}
        version: {type: string}
        framework: {type: string}
        allowedPermissions: {type: array, items: {type: string}}
    RunInput:
      type: object
      required: [externalId, kind]
      properties:
        externalId: {type: string}
        agentId: {type: string, format: uuid}
        agent:
          type: object
          description: Observed runtime identity. The server upserts it by externalId without granting permissions.
          required: [externalId]
          properties:
            externalId: {type: string}
            name: {type: string}
            version: {type: string}
            framework: {type: string}
        environment: {enum: [sandbox, staging, production]}
        kind: {enum: [mcpbench, tripwire, release_gate, runtime, benchmark, custom]}
        schemaVersion: {type: string}
        traceId: {type: string, pattern: "^(?!0{32}$)[0-9a-f]{32}$"}
        rootSpanId: {type: string, pattern: "^(?!0{16}$)[0-9a-f]{16}$"}
        metadata: {type: object, additionalProperties: true}
        assurance:
          type: object
          description: Bind this run to an issued continuous assurance contract. The server stores the canonical scope fingerprint and reserves its run metadata.
          required: [caseId, trigger, scope]
          properties:
            caseId: {type: string, format: uuid}
            trigger: {enum: [pull_request, release, nightly]}
            scope: {$ref: "../../schemas/agentcert-assurance-scope.schema.json"}
    EventBatch:
      type: object
      required: [events]
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 500
          items:
            type: object
            required: [sequence, type]
            properties:
              sequence: {type: integer, minimum: 0}
              type: {type: string}
              actor: {type: string}
              occurredAt: {type: string, format: date-time}
              payload: {type: object, additionalProperties: true}
              traceId: {type: string, pattern: "^(?!0{32}$)[0-9a-f]{32}$"}
              spanId: {type: string, pattern: "^(?!0{16}$)[0-9a-f]{16}$"}
              parentSpanId: {type: string, pattern: "^(?!0{16}$)[0-9a-f]{16}$"}
    RunCompletion:
      type: object
      required: [status]
      properties:
        status: {enum: [passed, failed, needs_evidence, manual_review]}
        score: {type: number}
        summary: {type: string}
        firstDivergence: {type: string}
        metadata: {type: object, additionalProperties: true}
    ReviewInput:
      type: object
      properties:
        comment: {type: string}
    FailureReviewInput:
      type: object
      required: [patternKey, type, status, taxonomyRationale]
      properties:
        patternKey: {type: string}
        suggestedType: {type: string}
        type:
          enum: [prompt_injection, wrong_click, timeout, verification_gap, silent_partial_success, network_failure, ui_drift, policy_or_approval, agent_connection, console_error, assertion_failure, unknown_failure]
        status: {enum: [confirmed, corrected]}
        confidence: {type: number, minimum: 0, maximum: 1}
        note: {type: string}
        evidenceContext:
          type: object
          properties:
            firstDivergenceSnippet: {type: string}
            screenshotPointer: {type: string}
            tracePointer: {type: string}
            stepIndex: {type: integer, minimum: 0}
        taxonomyRationale:
          type: object
          required: [primaryReason]
          properties:
            primaryReason: {type: string}
            supportingSignals: {type: array, items: {type: string}}
            contradictingSignals: {type: array, items: {type: string}}
            classifierLimitation: {type: string}
    VerificationInput:
      type: object
      required: [observedState]
      properties:
        observedState: {type: object, additionalProperties: true}
    ApiKeyInput:
      type: object
      properties:
        name: {type: string}
        scopes:
          type: array
          minItems: 1
          uniqueItems: true
          items: {enum: [agents:read, runs:read, runs:write, events:write, actions:read, actions:write, evidence:read, evidence:write, deployments:read, deployments:exchange]}
    ActionInput:
      type: object
      required: [externalId, principal, actionType, targetSystem, requestedPermissions]
      properties:
        externalId: {type: string}
        agentId: {type: string, format: uuid}
        principal: {type: object, additionalProperties: true}
        actionType: {enum: [SUBMIT, PAY, SEND, UPDATE]}
        targetSystem: {type: string}
        requestedPermissions: {type: array, items: {type: string}}
        amount: {type: number, minimum: 0}
        currency: {type: string}
        externalRecipient: {type: boolean}
        sensitive: {type: boolean}
        expectedState: {type: object, additionalProperties: true}
