Skip to content

fix(server-timing): emit admin-gated per-request Server-Timing on the standard server (#3361) - #3384

Merged
os-zhuang merged 1 commit into
mainfrom
claude/server-timing-admin-gated-f8kj2z
Jul 21, 2026
Merged

fix(server-timing): emit admin-gated per-request Server-Timing on the standard server (#3361)#3384
os-zhuang merged 1 commit into
mainfrom
claude/server-timing-admin-gated-f8kj2z

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#3361.

Problem

The per-request, admin-gated Server-Timing path (#2408) — an admin sends X-OS-Debug-Timing: 1 (or json) and sees auth/db/hooks/serialize spans while an ordinary user sees nothing — never emitted on the shipped os serve / os dev / os start server. Only global mode (OS_SERVER_TIMING=true), which discloses to every caller and so is not admin-gated, worked.

Root cause

The disclosure gate the Hono middleware opens per-request is only ever flipped open by the runtime dispatcher's timedResolveExecutionContext (http-dispatcher.ts). But on os serve/dev the data (/api/v1/data/*) and metadata (/api/v1/meta/*) routes are served by @objectstack/rest's RestServer, which registers its routes in start() and therefore shadows the Hono plugin's own CRUD (registered later on kernel:ready). RestServer.resolveExecCtx resolves the principal but never called allowPerfDisclosure(), so the gate stayed closed and the header was always withheld.

Note: this refines the issue's premise. The issue pointed at the Hono plugin's self-contained resolveCtx; that path is real but only used by the standalone Hono surface (no @objectstack/rest). The routes in the issue's repro are actually served by RestServer, so the primary fix is there. Both are fixed.

Fix

  • @objectstack/observabilityisPerfDisclosurePrincipal(ec) now lives here, in the package that owns the disclosure gate: the single shared definition of "who may pull per-request timings" (isSystem / principalKind service·system / posture PLATFORM_ADMIN·TENANT_ADMIN). Prevents any admin-serving path from hand-rolling its own rule and drifting.
  • @objectstack/runtime — re-exports the predicate for back-compat (its timedResolveExecutionContext + existing tests are unchanged).
  • @objectstack/restRestServer.resolveExecCtx opens the gate for an admin/service principal via the carried posture rung. This is the fix that makes os serve/dev emit.
  • @objectstack/plugin-hono-server — the standalone CRUD surface's self-contained resolveCtx opens the gate too. Because that resolver derives no posture rung, it derives one from the resolved permission-set grants for the gate decision only, on a throw-away object — it never writes posture onto the returned context, since ctx.posture is an enforcement input for Layer 0 cross-tenant tier adjudication (ADR-0099 D1, security-plugin.ts) and only the authoritative resolver may set it.

Tests

  • packages/rest/src/rest-server-timing.test.ts — drives the real resolution pipeline (computeExecCtxresolveAuthzContextderivePosture) inside an ambient disclosure gate: a PLATFORM_ADMIN opens it, a MEMBER and an anonymous caller do not, and it is a safe no-op when no gate is active.
  • packages/plugins/plugin-hono-server/src/server-timing-e2e.test.ts — boots the Hono app with the real /api/v1/data/:object handler and asserts an admin sending X-OS-Debug-Timing gets a Server-Timing header while a member (200, no header) and an anonymous caller (401, no header) do not, and that an admin gets nothing without the opt-in header. This is the end-to-end coverage the issue calls out as the CI gap.
  • packages/observability/src/perf-timing.test.ts — unit coverage for the predicate in its new home.

Verification

  • Build (type-check + .d.ts): ✅ turbo run build for rest / plugin-hono-server / runtime — 24 tasks, all green.
  • Tests: ✅ turbo run test for observability / rest / plugin-hono-server / runtime — 27 tasks; new suites: observability 41, rest 4, hono e2e + existing 28, runtime 577.

A changeset (patch across the four packages, which version together in the fixed group) is included.


Generated by Claude Code

… standard server (#3361)
The per-request, admin-gated `Server-Timing` path (#2408) — an admin sends
`X-OS-Debug-Timing: 1`/`json` and sees auth/db/hooks/serialize spans while an
ordinary user sees nothing — never emitted on the shipped `os serve`/`dev`
server. The disclosure gate the Hono middleware opens is only ever flipped by
the runtime dispatcher's `timedResolveExecutionContext`, but the data
(`/api/v1/data/*`) and metadata (`/api/v1/meta/*`) routes there are served by
`@objectstack/rest`'s `RestServer` (which shadows the Hono plugin's own CRUD),
and its identity resolver never opened the gate. Only global mode
(`OS_SERVER_TIMING=true`) — which discloses to every caller, not just admins —
worked, so the documented admin-gated path was unavailable on the OSS server.
- observability: home `isPerfDisclosurePrincipal(ec)` here (the gate's package),
the ONE shared definition of "who may pull per-request timings" used by every
HTTP entry point. `@objectstack/runtime` re-exports it for back-compat.
- rest: `RestServer.resolveExecCtx` opens the gate for an admin/service principal
via the carried `posture` rung — the fix that makes `os serve`/`dev` emit.
- plugin-hono-server: the standalone CRUD surface's self-contained `resolveCtx`
opens the gate too, deriving the rung for the gate decision ONLY (never writing
an imprecise `posture` onto the returned context — `ctx.posture` is an
enforcement input for Layer 0 tier adjudication, ADR-0099 D1).
Tests: a rest integration test driving the real `resolveAuthzContext` →
`derivePosture` pipeline (admin opens the gate; member/anon do not) and a Hono
e2e test that boots the app and asserts an admin gets `Server-Timing` while a
member/anonymous caller does not — the end-to-end coverage the issue calls out
as the CI gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHeWEHnvQHiN9f9P1t47Sj
@vercel

vercelBot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 21, 2026 1:53pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/m labels Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/observability, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime.

23 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx(via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx(via @objectstack/rest)
  • content/docs/api/index.mdx(via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-hono-server, @objectstack/rest)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx(via @objectstack/observability, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx(via @objectstack/rest)
  • content/docs/releases/v16.mdx(via @objectstack/plugin-hono-server)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 21, 2026 14:06
@os-zhuang
os-zhuang merged commit 818e6a3 into mainJul 21, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/server-timing-admin-gated-f8kj2z branch July 21, 2026 14:06
os-zhuang pushed a commit that referenced this pull request Jul 21, 2026
… discovery (#3369)
Rebased onto main after the #3361 Server-Timing fix landed independently
(#3384) and the notifications mark-read e2e landed (#3388). This keeps only the
net-new route-parity work; the redundant #3361 fix is dropped in favour of
main's canonical version.
- plugin-hono-server: make the STANDALONE discovery honest. The static
`registerDiscoveryAndCrudEndpoints` list advertised metadata/packages/
analytics/workflow/automation/ai/notifications/i18n/storage/ui — none of
which HonoServerPlugin mounts on its own — so a truly standalone deployment
advertised a dozen routes that 404 (the literal `declared !== enforced` gap
#3369 cites). It now advertises only what it serves (`/data` + `/auth/me/*`).
Shadowed by the dispatcher/REST service-aware discovery under `os serve`, so
real deployments are unaffected.
- runtime: add the route-parity e2e gate. Boots the real hono app the way
`os serve` mounts it (hono server + dispatcher plugin, services provisioned),
reads `/api/v1/discovery`, and asserts every advertised/dispatcher route is
reachable (never 404/405/501) for anonymous AND admin principals, and that
discovery is service-aware in both directions (no dead advertisement). Closes
the class of "works in the dispatcher, dead on os serve" bugs (#3361/#3362/
MCP 501) with one boot-the-real-server test. Server-Timing and notifications
mark-read specifics are covered by #3384 / #3388; this gate does not duplicate
them.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang pushed a commit that referenced this pull request Jul 21, 2026
…3369)
Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto
main after the #3361 Server-Timing fix (#3384) and the notifications mark-read
e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work.
- runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the
real hono app the way `os serve` mounts it (hono server + dispatcher plugin,
services provisioned), reads `/api/v1/discovery`, and asserts every advertised
/ dispatcher-registered route is reachable (never 404/405/501) for an anonymous
AND an admin principal, and that discovery is service-aware in both directions
(no dead advertisement). Closes the "works in the dispatcher, dead on os serve"
class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing
(#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui`
(@objectstack/client) coverage.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Test + comment only; no runtime code changes (no-release changeset).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang added a commit that referenced this pull request Jul 21, 2026
…3369) (#3386)
Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto
main after the #3361 Server-Timing fix (#3384) and the notifications mark-read
e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work.
- runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the
real hono app the way `os serve` mounts it (hono server + dispatcher plugin,
services provisioned), reads `/api/v1/discovery`, and asserts every advertised
/ dispatcher-registered route is reachable (never 404/405/501) for an anonymous
AND an admin principal, and that discovery is service-aware in both directions
(no dead advertisement). Closes the "works in the dispatcher, dead on os serve"
class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing
(#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui`
(@objectstack/client) coverage.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Test + comment only; no runtime code changes (no-release changeset).
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v16.0: per-request admin-gated Server-Timing never emits on the hono server (os serve/dev)

2 participants

@os-zhuang@claude