2026-05-10-fix-issues-272-273-274

Fix Issues #272 / #273 / #274 — XDR doc/OpenAPI/RBAC alignment

Date: 2026-05-10 Status: shipped Slug: issues-272-273-274-xdr-doc-fixes Branch: feature/issues-272-273-274-xdr-doc-fixes Closes: #272, #273, #274

Summary

Fixes the three issues filed by Perplexity's audit of PR #271. The runtime code is correct — every fix landed in this PR is on the documentation / OpenAPI / code-comment side of the contract.

Issue #272 (HIGH) — risk-clear route shape

Bug I introduced: docs/openapi.yaml documented POST /v1/admin/xdr/risk-clear as a separate path. That route does not exist.

Actual handler: DELETE /v1/admin/xdr/risk-push with the tenant_id + agent_id in the request body. SDKs (pushXdrRisk, clearXdrRisk in both TS and Python) already use _deleteWithBody("/v1/admin/xdr/risk-push", ...) — only the OpenAPI + docs prose had the wrong shape.

Fix: Replaced the fictional risk-clear operation with a delete entry under the existing /v1/admin/xdr/risk-push path. Updated the curl example in xdr-pipeline.mdx and observability.mdx to use curl -X DELETE with the body.

Issue #273 (HIGH) — schema drift on push/clear

Bugs:

  • source was optional in OpenAPI; it is required in XdrRiskPushSchema

(z.string().min(1).max(128)).

  • tenant_id and agent_id had no UUID validation (Zod uses .uuid()).
  • ttl_seconds had no max constraint (Zod uses .max(2_592_000) = 30d).
  • I invented an indicator field that doesn't exist.
  • Response shapes did not match handler c.json(...) returns.

Fix:

  • source marked required; minLength: 1, maxLength: 128.
  • tenant_id and agent_id set to format: uuid.
  • ttl_seconds capped at 2592000 (no default, since the handler uses

?? 86400 rather than a Zod default — documented explicitly).

  • Removed the fictional indicator field.
  • Push response: { ok, agent_id, tenant_id, risk_score, source, ttl_seconds, set_at?, dry_run?, would_set? }

matching c.json({ ok: true, agent_id, tenant_id, risk_score, ... }).

  • Clear response: { ok, agent_id, tenant_id, dry_run?, would_delete? }

matching c.json({ ok: true, agent_id, tenant_id }).

  • Added 403 feature_disabled response on both operations (handler returns

this when XDR_BIDIRECTIONAL_ENABLED != "1").

  • Added ?dry_run=true query parameter on both.

Issue #274 (MEDIUM) — destination + RBAC + feature-flag drift

Bugs:

  • OpenAPI create/update bodies omitted event_types and batch, both

accepted by CreateDestinationSchema / UpdateDestinationSchema.

  • XdrDestination response schema omitted six fields: event_types,

batch, and the four stats.* fields (last_delivered_at, delivery_count, error_count, last_error) — see formatDestination in src/api/routes/xdr.ts.

  • XDR_BIDIRECTIONAL_ENABLED env-var feature flag undocumented.
  • ?dry_run=true query parameter undocumented.
  • Doc prose claimed permissions xdr.risk.push / xdr.risk.clear exist;

RBAC actually requires platform.admin (per src/api/middleware/rbac.ts:379-380).

Fix:

  • Added event_types and batch to create + update OpenAPI bodies.
  • Expanded XdrDestination schema with the six missing fields and the

new DestinationBatchConfig component.

  • Documented the feature flag in xdr-pipeline.mdx, observability.mdx,

and the OpenAPI descriptions for risk-push / risk-clear.

  • Documented ?dry_run=true semantics in all three places.
  • Replaced xdr.risk.push / xdr.risk.clear permission references with

platform.admin everywhere.

Cortex auth — code/comment correction

Perplexity flagged that xdr-pipeline.mdx said SHA256(api_key + nonce + ts) while the prior cortex.ts file header comment claimed "HMAC-SHA256". The implementation in cortex.ts:56 uses createHash("sha256") — a plain SHA256 hash, not an HMAC.

The docs were already accurate (matching the implementation). The code's file-header comment was wrong. Fixed:

  • src/observability/xdr/adapters/cortex.ts:7 — comment now reads "per-request

SHA256 hash signature ... NOT an HMAC."

  • docs/concepts/xdr-pipeline.mdx — adapter table column reads "Per-request

SHA256 hash" instead of "HMAC-SHA256"; the explanatory paragraph below now says "per-request hashing scheme" and explicitly notes "not an HMAC."

Files

Modified

  • docs/openapi.yamlrisk-push POST schema (source required, UUID, TTL cap, dry_run, 403); new risk-push DELETE entry; deleted risk-clear operation; XdrDestination + create/update bodies expanded; new DestinationBatchConfig component
  • docs/concepts/xdr-pipeline.mdx — risk-push/clear curl examples + auth/feature-flag/dry-run notes; Cortex adapter row + paragraph corrected
  • docs/api-reference/observability.mdx — same risk-push/clear corrections + auth/feature-flag/dry-run notes
  • src/observability/xdr/adapters/cortex.ts — file header comment corrected (SHA256, not HMAC-SHA256)
  • site/public/openapi.yaml — synced from docs/openapi.yaml
  • site/public/llms-full.txt + site/public/routes.json — auto-regenerated by pnpm build
  • src/api/static-assets.generated.ts — auto-regenerated (runtime /openapi.json now serves the corrected spec)

New

  • docs/ship-log/2026-05-10-fix-issues-272-273-274.md — this file

Verification

  • python3 -c "import yaml; yaml.safe_load(open('docs/openapi.yaml'))" — parses
  • Programmatic checks against the parsed YAML:
  • risk-push has [post, delete] methods ✅
  • risk-push POST body required = [tenant_id, agent_id, risk_score, source]
  • XdrDestination has stats, event_types, batch properties ✅
  • risk-clear path no longer present ✅
  • DestinationBatchConfig component present ✅
  • pnpm tsgo — exit 0
  • pnpm exec oxfmt --check / oxlint — clean
  • pnpm test:fast — 7755/0 (no logic change, only doc + comment edits)

Open follow-up — automation target

The user's audit closing point: generate or parity-test OpenAPI from Hono route registrations + Zod schemas. This PR fixes the symptoms; the structural fix is a CI gate that compares the two surfaces. Sketch:

  1. Walk src/api/routes/*/.ts, extract every app.{get,post,put,delete}(path, ...).
  2. For each, find the zBody(SomeSchema) middleware and reflect the schema.
  3. Compare against docs/openapi.yaml:
  • Missing handler for an OpenAPI path → fail
  • Missing OpenAPI entry for a registered handler → fail
  • Required-field set mismatch (Zod vs OpenAPI) → fail
  • Method set mismatch → fail
  • RBAC permission referenced in docs but not in RBAC_RULES → fail

This is filed as a task for a follow-up PR (not in scope here).

Lockstep

  • TS SDK / Python SDK — no surface change (already matched code in #271)
  • MCP tools — no surface change
  • Ship log — this file