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. Sergey’s Lane B-5 commit at 8fffad4ad locked the invariant for the governance resources shipped in v1: see langwatch/ee/governance/repositories/ingestionTemplate.repository.ts and governanceAudit.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 Next.js, React render path. The Hono-mounted REST API exists for the surfaces that aren’t 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.
Where it lives
Resource × verb matrix
Every governance resource exposes the full CRUD triple,list, get, create, update, delete, over Hono, the CLI, and the MCP server. The umbrella spec lists the resource set; each resource gets its own OpenAPI path group.
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 isingestion-templates (Sergey’s 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
/adminsub-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 genericupdate) andclone-from-platform(instead of genericcreate) 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 (rotateon ingestion sources,assign-to-useron role bindings,revokeon 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 commonerrorSchemawith{ type, code, message }.
ingestion-keys (Sergey’s Lane B-2 commit at 5275e7e11). An ingestion key is just an ApiKey (prefix sk-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):
Two consequences worth noting:
- Ingest-only role, single project: the minted
ApiKeycarries 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 fullsk-lw-…secret is shown exactly once at mint/install/rotate; thereafter only the prefix is returned. source_typeselects the shaping: the mint body’ssource_typerecords 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.
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 samedescribeRoute + hono-openapi pipeline as the rest of the public REST API. The canonical OpenAPI spec lives at langwatch/openapiLangWatch.json, and both SDKs regenerate from it on the standard build target.
TBD-IMPL, exact script names: Sergey’s first route SHA at
0bb951160 mounts ingestion-templates into api-router + generateOpenAPISpec so the OpenAPI shape lands in openapiLangWatch.json on the next regen. The exact package.json script names fold in once the SDK regen target lands.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 intoAuditLog.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.
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 on ingestion keys) all carry surface attribution as of Sergey’s Lane B-3 commit at fc6d54100. Minting or rotating an ingestion key emits a gateway.ingestion_key.minted audit row, and revoking one emits gateway.ingestion_key.revoked.
Verifying the contract
The wire shape, request bodies, response envelopes, status-code mapping, and surface attribution, is locked by two integration tests that run against real Postgres + the real Hono pipeline (no service mocks): Ingestion-templates wire shape:langwatch/src/app/api/governance/__tests__/governance-rest-api.integration.test.ts. 14 scenarios (Lane B-4 at 1839d9f54) covering the full ingestion-templates verb set, 401, 400, 403, 404 envelopes, and a wire-level audit-uniform assertion that metadata.surface === "hono" for state-changing calls.
Ingestion-key wire shape: langwatch/src/app/api/governance/__tests__/governance-ingestion-keys.integration.test.ts. 9 scenarios (Lane B-6 at 60f769498) covering the ingestion-key flows. Specifically locks:
- the mint route returns a one-time
sk-lw-…token and thereafter exposes only the prefix installandrotateaudit rows both stampmetadata.surface === "hono"end-to-end (cross-validates the B-3 surface threading atfc6d54100)rotateemitsgateway.ingestion_key.mintedand revoking a key emitsgateway.ingestion_key.revokedat the wire level, previously unit-only
aiTools:manage caller) is the canonical reference for any downstream lane (CLI, MCP) that needs an ingestion-key test path.
No-bypass invariant (CI-enforced): langwatch/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: langwatch/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: langwatch/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; asserts all three audit rows fall back to metadata.surface === "hono". Locks Alexis’s GovernanceCallSurface enum filter at resolveSurfaceFromRequest as the defense against external HTTP callers forging in-process surface tags, the only legal value the Hono surface accepts on inbound is cli; everything else falls through to the route’s default "hono". Together with the cross-surface uniformity test this exhaustively pins the umbrella spec’s @audit-uniform invariant for v1.
To run locally:
Cross-references
- Governance CLI:
langwatch governance <resource> <verb>thin shell - Governance MCP server: same surface as MCP tools for agent use
- Roles and permissions: 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,
langwatch/src/app/api/governance/__tests__/governance-rest-api.integration.test.ts