Skip to main content
Pairs with: Governance CLI (the CLI is a thin shell over this API) and Governance MCP server (the MCP tools are a thin shell over this API). All three surfaces dispatch through the same service-layer functions; the dashboard tRPC procedures call into the same services. There is exactly one place each governance verb is implemented.
Service-layer-shared, repository-pattern-backed. Per specs/ai-gateway/governance/governance-api-cli-mcp-coverage.feature, every Hono route delegates to a shared service-layer function (IngestionTemplateService.updateOttlRules, IngestionKeyService.install, etc.). Services use repositories for persistence; services never import prisma directly. The umbrella spec also locks the no-bypass invariant, no UI page, route handler, CLI command, or MCP tool may call prisma.<governanceModel>.* directly. The governance repositories hold that boundary: see enterprise/modules/governance/process/src/repositories/prisma/prisma.ingestion-template.repository.ts and modules/audit-log/process/src/repositories/prisma/prisma.audit-log.repository.ts (the latter wraps every AuditLog write so audit emission also goes through the repository boundary). Repo methods accept Prisma.TransactionClient | PrismaClient so they work inside service $transaction blocks and against the top-level client transparently.

Why a separate REST API alongside tRPC

LangWatch’s dashboard already uses tRPC, type-safe, but coupled to the Vite React app that renders it. The Hono-mounted REST API exists for the surfaces outside the dashboard:
  • Agentic workflows: Claude Code, Codex, Cursor running langwatch governance … commands need a stable, OpenAPI-described surface they can call directly.
  • CI, scripting: the same OpenAPI spec is consumed by the TypeScript and Python SDKs (auto-regen on build), so any pipeline language with a generated client gets the full governance feature set.
  • Future integrations: partner platforms (SIEMs, ticketing, ops automations) want a documented API contract, not a tRPC client.
Both paths call into the same service-layer functions, there is no business-logic duplication. The umbrella coverage spec locks that invariant.

Where it lives

Resource × verb matrix

Governance resources expose the full CRUD set, list, get, create, update, delete, over Hono, the CLI, and the MCP server, except where the table below names a narrower verb set (audit-log is read-only, ingestion keys mint through their own route). The umbrella spec lists the resource set; each resource gets its own OpenAPI path group. Anomaly rules are not in the table: they have no REST surface and no authoring page, see Anomaly rules. audit-log is intentionally read-only (no create, update, delete), audit rows are emitted by other state-changing routes and are immutable by design.

Verb shape: worked example

The first surface to ship is ingestion-templates (Lane B-1 commit). The verb set: A few resource-shape notes that generalise across the namespace:
  • End-user vs admin shape: list endpoints return a redacted shape by default and a separate /admin sub-route for the canonical shape. Avoids accidentally leaking the OTTL source to non-admins via list iteration.
  • Resource-specific verbs: update-ottl-rules (instead of generic update) and clone-from-platform (instead of generic create) reflect the domain, admins don’t generically PATCH every field; they specifically replace OTTL or clone a platform row. Other resources will introduce their own resource-specific verbs (rotate on ingestion sources, assign-to-user on role bindings, revoke on sessions) the same way.
  • Error mapping: 403 PlatformTemplateImmutable (admin tried to PATCH a platform-published row), 404 TemplateNotFound (cross-org probe), 400 InvalidSourceType, validation. All map to a common errorSchema with { type, code, message }.
The second resource to ship is ingestion-keys. An ingestion key is just an ApiKey (prefix ik-lw-) scoped to one project with an ingest-only role, there is no separate binding model. The CLI mints one through a dedicated auth route; the dashboard manages the rest through the ingestionKey tRPC router (list / install / rotate / revoke): Three consequences:
  • Ingest-only role, single project: the minted ApiKey carries an ingest-only role scoped to exactly one project, so the token can write traces into that project but cannot read or mutate any other governance resource. The full ik-lw-… secret is shown exactly once at mint/install/rotate; thereafter only the prefix is returned.
  • source_type selects the shaping: the mint body’s source_type records which upstream tool the key ingests for, so the gateway can apply the matching ingestion-template OTTL transform when traces arrive on that key. A key minted for an org-authored template carries that template’s id.
  • A personal CLI key follows its session: a personal key the CLI mints stores the session’s login key as parentApiKeyId. Logout, a devices-tab revoke, a re-login from the same device and session expiry all revoke that login key, and every key parented to it goes with it. A mint from a revoked session answers 401.
  • Each side records its own cause: the login key carries why it died (user for a logout or a devices-tab revoke, rotation for a re-login, expired for a session that ran out, offboarded for a membership that ended). The keys under it carry the same cause, except user, which reaches them as session. A re-login therefore leaves rotation on the keys, which is what tells the CLI it may re-mint under the session that replaced theirs.
  • A project CLI key does not: a key minted into a named team project carries no parent and outlives the session that asked for it.
  • Only the CLI mints for a tool it wraps: install and the MCP mint accept only source types a published template names, and a wrapped tool answers ingestion_key_source_not_allowed.
The Zod schemas backing each request and response body are the same schemas the corresponding service-layer function consumes. The OpenAPI generator (describeRoute + hono-openapi) reads them directly, so the spec and the runtime are guaranteed in sync.

OpenAPI spec generation + SDK regen

Governance routes register through the same describeRoute + hono-openapi pipeline as the rest of the public REST API. The OpenAPI document is generated from the route declarations and is not committed. The api builds it at boot and serves it at /api/openapi.json. The SDK clients and the copy published alongside these docs, docs/api-reference/openapiLangWatch.json, are generated from it and committed, and CI fails when they no longer match the routes.
The TS SDK exposes langwatch.governance.<resource>.<verb>(...), the Python SDK exposes langwatch.governance.<resource>.<verb>(...). Type signatures match the Zod-derived schemas exactly.

Audit emission

State-changing calls always emit an audit row from the service layer (the same row a dashboard tRPC mutation would emit, there is one service-layer audit emitter, not three). Every audit row carries a surface attribution tag stamped into AuditLog.metadata.surface: The shared type lives at @ee/governance/services/auditSurface.ts (GovernanceCallSurface); every mutating service method on the governance services takes an optional surface parameter (default "trpc") which is stamped into metadata at audit emission. Forensic readers query metadata->>'surface' to filter by surface.
All four rows have identical payload shapes apart from the metadata.surface field, same event kind, same target, same actor. Surface attribution helps incident response (which automation made this change?) without changing the audit-row event-kind taxonomy. The mutating verbs across the two shipped resources (createOrgTemplate, updateOttlRules, archiveOrgTemplate, cloneFromPlatform on templates; install, rotate, revoke on ingestion keys) all carry surface attribution. Minting an ingestion key emits an ingestionKey.mint audit row, rotating a source emits ingestionKey.rotate with the count of keys it revoked, and revoking one emits ingestionKey.revoke.

Verifying the contract

The wire shape, request bodies, response envelopes, status-code mapping, and surface attribution, is locked by the governance transport and application suites, which drive the real Hono pipeline and the real application over stubbed services: Ingestion-templates wire shape: enterprise/modules/governance/process/src/app/__tests__/governance.transport.unit.test.ts. 20 cases covering the full ingestion-templates verb set, the 401, 400, 403 and 404 envelopes in this family’s nested { error: { type, code, message } } body, and the audit-uniform assertion that a state-changing call is attributed with surface: "hono". It is the successor to the monolith’s governance-rest-api.integration.test.ts: what that suite proved about the DOOR moved here, and what it proved about the DOMAIN moved to the service tests beside it. Surface-spoof rejection: same file. Fires Hono POSTs with X-LangWatch-Surface set to cli and to in-process values, and asserts only cli is honoured while everything else falls back to "hono". resolveSurfaceFromRequest in enterprise/modules/governance/process/src/transport/governance.rest.ts filters the header against the GovernanceCallSurface vocabulary, which is what stops an external HTTP caller forging an in-process surface tag: cli is the only value the Hono surface accepts on inbound, and trpc and mcp are reachable only in-process. Cross-surface audit uniformity: enterprise/modules/governance/process/src/app/__tests__/governance.app.unit.test.ts, scenario “audit rows are identical apart from metadata.surface”. Creates the same organization template over all four surfaces, tRPC, Hono REST, CLI and MCP, and asserts the four writes are field-for-field identical apart from surface. Surface attribution therefore cannot drift into a second opinion about what a write means, and there is no separate MCP-transport regression because the MCP tool reaches this same application call. No-bypass invariant (CI-enforced): the prisma-containment policy in packages/architecture-enforcer/src/prisma-boundaries.ts, run by pnpm lint:architecture. It rejects any file importing generated Prisma outside a repository under server/src/repositories/prisma/ or the Postgres composition adapter server/src/adapters/postgres.<subject>.adapter.ts. That is the umbrella spec’s @no-bypass invariant enforced repo-wide rather than for governance alone, so the repository-pattern boundary cannot silently drift. Ingestion-key flows: enterprise/modules/governance/process/src/services/__tests__/ingestion-key.service.unit.test.ts pins the service-level ordering, that a replacement key is minted only after the prior one is hard-revoked, and that an additive issue leaves another machine’s key alone. The wire-level guarantees below are contract rather than covered ground, they carry no test of their own today:
  • the mint route answers 201 with an ik-lw-… token, its prefix and the OTLP endpoint, and a second mint for the same source returns a different token rather than replacing the first
  • a mint from a CLI session stores that session’s login key as parentApiKeyId and the session’s device label, and a mint from a session whose login key was revoked answers 401
  • the describe route reports a key’s status and its revocation_cause, which is what the CLI reads before it re-mints
Surface attribution is not asserted there. metadata.surface on the ingestion-key audit rows is locked by platform/app/src/mcp/__tests__/governance-tools.audit-uniform.integration.test.ts and platform/app/ee/governance/services/__tests__/auditSurface.crossSurface.integration.test.ts. The organization:view caller plus a project-scoped ingest-only RoleBinding is the setup pattern any downstream lane (CLI, MCP) needs for an ingestion-key test path. The cascade itself is locked by auth-cli-logout.integration.test.ts and auth-cli-refresh-expiry.integration.test.ts, and the service rules by platform/app/ee/governance/services/__tests__/ingestionKey.lifecycle.unit.test.ts. No-bypass invariant (CI-enforced): platform/app/ee/governance/repositories/__tests__/no-bypass.unit.test.ts. 3 tests (Lane B-7 at 94c219035) statically reject any future PR that adds a direct prisma.ingestionTemplate.* call outside the allowlist. Locks the umbrella spec’s @no-bypass invariant in CI so the repository-pattern boundary won’t silently drift. MCP audit-uniform regression: platform/app/src/mcp/__tests__/governance-tools.audit-uniform.integration.test.ts. 3 cases (Lane B-MCP audit at 66fd35162) prove the Path B contract end-to-end: governance_ingestion_templates_create and governance_ingestion_keys_mint both stamp metadata.surface === "mcp" against real Postgres, and the AUTH_REQUIRED: negative case stays closed (write tool with no callerUserId fails before any audit row is written). Cross-surface audit-uniformity regression: platform/app/ee/governance/services/__tests__/auditSurface.crossSurface.integration.test.ts. Invokes createOrgTemplate via all four surfaces, tRPC service-direct, Hono REST, CLI REST (with X-LangWatch-Surface: cli), and MCP service-direct, in one test (Lane B-8 at d96cd4300, extended to 4 surfaces at cb4c8224c); asserts the four audit rows have identical payload shape, same action, same targetKind, same organizationId, same metadata-key-set, with only metadata.surface varying ("trpc", "hono", "cli", "mcp"). Slug regex format identical, no default-fallback leakage. Surface-spoof rejection regression: same file as above (Lane B-8 extension at cb4c8224c). Fires three Hono POSTs with X-LangWatch-Surface set to trpc, mcp, and evil, and asserts all three audit rows fall back to metadata.surface === "hono". That locks the GovernanceCallSurface enum filter at resolveSurfaceFromRequest as the defense against external HTTP callers forging in-process surface tags. The only value the Hono surface accepts on inbound is cli; everything else falls through to the route’s default "hono". With the cross-surface uniformity test, this pins the umbrella spec’s @audit-uniform invariant for v1. cli itself is a hint from an already-authenticated caller rather than a claim the route can verify, since the CLI presents the same credentials any other HTTP client does. Read it as “which client said it did this”, never as a permission. To run locally:
Use these files as the authoritative reference when implementing a CLI command or MCP tool against a governance route, the request/response shape and audit-stamping invariants that live there are exactly what each route accepts, returns, and emits, and the architecture lint will reject any direct-prisma shortcut at PR time.

Cross-references

  • Governance CLI: langwatch governance <resource> <verb> thin shell
  • Governance MCP server: same surface as MCP tools for agent use
  • Access Control (RBAC): RBAC scopes that gate each verb
  • Audit log: where state-change rows land
  • Compliance architecture: how the OCSF v1.1 export consumes the same audit stream
  • Spec, specs/ai-gateway/governance/governance-api-cli-mcp-coverage.feature
  • v1 scope-fence audit, specs/ai-gateway/governance/agentic-first-parity-v1-status.md (12-resource × 4-surface matrix on shipped SHAs; scoring 2/12 full parity, 8/12 tRPC-only deferred to follow-on)
  • Wire-shape lock, enterprise/modules/governance/process/src/app/__tests__/governance.transport.unit.test.ts
Last modified on October 12, 2026