Skip to content

feat: expose and denormalize mission_id for delegation-chain audit queries #81

@rsharath

Description

@rsharath

Summary

Surface mission_id — a stable, opaque identifier for a delegation tree — as a first-class, queryable field: a JWT claim, a column on issued_credentials and cae_signals, a log field, and an admin API filter. Enables efficient workflow-level audit queries without walking the parent_jti chain at read time.

Implementation note: mission_id is currently populated with the JTI of the root credential in the delegation tree. Consumers MUST treat it as opaque — do not assume the value is a JTI, do not attempt to look up the underlying credential by this ID. Keeping the value opaque lets the population scheme evolve (e.g., to match a future standardized claim) without breaking consumers.

Motivation

CoSAI Agentic IAM Appendix D's "prove control on demand" query list includes "Retrieve events sharing the same correlation_id, ordered by delegation depth" — reconstruct a full workflow end-to-end. ZeroID's existing parent_jti links let you walk this chain, but every workflow query becomes an O(depth) recursive CTE.

Issue #77 (mission log) names the delegation tree as a "mission" and keys its log entries by a mission identifier. Denormalizing that identifier onto the tables that already carry per-token and per-event rows is the cheap, obvious move that makes both CoSAI-style audit queries and #77's mission log reads O(1).

Rescoped from earlier drafts. Originally proposed as a new correlation_id UUID generated at issuance. Superseded: introducing a parallel workflow UUID would duplicate identity that can be derived from the existing delegation tree. Also pivoted away from an interim root_jti name in favor of the domain-aligned mission_id — it names what the field does (groups a workflow's events) rather than what it currently happens to be (a JTI).

Proposed change

Domain / schema

  • Add nullable mission_id VARCHAR(255) column to issued_credentials with an index.
  • Add nullable mission_id VARCHAR(255) column to cae_signals with an index.
  • Migration 009_mission_id_denormalization.up.sql, reversible.

Service layer (internal/service/oauth.go, credential.go)

  • First issuance (any grant that originates a credential — client_credentials, jwt_bearer, authorization_code, api_key, external_principal_exchange, refresh_token): mission_id = <this credential's own JTI>. The root of a delegation tree starts a new mission.
  • token_exchange: derive mission_id from the subject_token:
    • If the subject_token has an explicit mission_id claim, use it.
    • Otherwise (pre-migration or flat issuance), fall back to the subject_token's own jti — the subject is a mission root by definition.
    • Write the derived value to the new credential's mission_id column.
  • Embed mission_id as a claim on every issued JWT.

Logging

  • Where request context carries a verified token, the zerolog child logger automatically includes mission_id as a structured field. Applies to token endpoint, introspection, revocation, and credential lifecycle logs.

Admin API

  • GET /credentials?mission_id=<id> — returns all credentials in the delegation tree for the given mission, ordered by delegation_depth then created_at.
  • GET /signals?mission_id=<id> — same, for CAE signals.

authjwt client library

  • Add (*Claims).MissionID() string so downstream services can log and filter by it without parsing claims manually.

Claim name: mission_id. This is a ZeroID-private claim (not IANA-registered). Collision risk is accepted within the ZeroID ↔ authjwt closed ecosystem. If a standards body (IETF WIMSE, CoSAI) later registers an equivalent claim, ZeroID can alias the registered name onto the existing field — the opaque-value contract protects consumers from the transition.

Out of scope

  • Agent-reported mission log entries and hash-chaining — that's issue Add mission log: agent-reported activity trail with hash-chained integrity #77.
  • Per-request trace IDs (OpenTelemetry style). Chi's request_id already covers single-request correlation; this issue is about workflow-spanning correlation.
  • Rich mission concepts (description, approval lifecycle, clarification dialogue) from the AAuth draft. mission_id groups events; it does not imply AAuth's richer mission semantics are implemented.

Compatibility notes

  • mission_id is nullable. Pre-migration credentials will not have it populated and will not be retrievable via the new filter; parent_jti chain walking still works for them. No backfill required — old credentials expire via TTL.
  • Additive to the JWT shape. No existing claim changed.
  • authjwt addition is a non-breaking method on Claims.
  • Opaque value contract. Consumers MUST NOT attempt to interpret mission_id (e.g., by looking up a credential by its JTI). This keeps the population scheme free to evolve.

Acceptance criteria

  • First-issuance grants write mission_id = <own jti> to the credential row and embed the mission_id claim in the JWT.
  • token_exchange correctly propagates mission_id from the subject_token to the new credential (both explicit-claim and fallback-to-subject-jti paths).
  • mission_id appears in zerolog output when the request carries a verified token.
  • Admin API supports ?mission_id=<id> on credentials and signals, returning ordered results.
  • Integration test: issue root credential → exchange twice to build a 3-node chain → GET /credentials?mission_id=<mid> returns all 3 in correct order.
  • Migration applies and reverts cleanly.
  • authjwt.Claims.MissionID() exposed and covered by a unit test.
  • Docs explicitly call out that mission_id is opaque to consumers.

Relationship to #77

Non-goals

  • Do not introduce a separate correlation_id, workflow_id, or root_jti field. The codebase has exactly one name for this identifier: mission_id.
  • Do not backfill pre-migration rows. They age out via TTL.
  • Do not expose mission_id through unauthenticated request contexts (e.g., health checks). Only emit the log field when there is a verified token.
  • Do not implement AAuth-style mission lifecycle (description, approval, clarification) in this issue. mission_id is the identifier only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions