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

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 21, 2026 1:53pm

Request Review

@github-actions github-actions Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

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