Skip to content

Latest commit

 

History

History
95 lines (64 loc) · 8.7 KB

File metadata and controls

95 lines (64 loc) · 8.7 KB

Shadow-mode pointer-fetch mismatches (internal-only)

Internal-only diagnostic doc. Not part of the public surface; not a normative spec; not linked from the front door. Stability classification is captured in docs/STABILITY-CONTRACT.md. Threat-coverage entries are captured in docs/THREAT_MODEL.md.

Purpose

v0.13.2 adds a shadow-mode diagnostic foundation for comparing the protocol pointer-fetch path against a workspace-private resolver composition layer on the same input pair, without changing public protocol or wire behavior.

The foundation provides:

  • A lazy-import boundary that loads the workspace-private resolver composition layer only when PEAC_INTERNAL_SHADOW_RESOLVER=1.
  • Bounded normalization shapes so two implementations with different native taxonomies can be compared like-for-like.
  • A redaction-safe in-memory ring buffer (shadow-mismatch-sink) capped to [64, 16384] entries with a ~512-byte JSON-stringified cap per entry.
  • A pure-function parity verdict computer that tags each mismatch into a small set of public classes.
  • A no-network parity smoke that drives both implementations through inputs they reject before any network I/O is attempted.

What this foundation does not ship

  • No live Hosted Verify route shadowing. The apps/api /v1/verify route receives an inline compact JWS receipt; it does not accept a pointer URL or expected digest. Pointer-fetch is caller-side today: callers dereference pointer URLs and pass the resulting JWS into /v1/verify. There is therefore no primary-path pointer-fetch result to capture for shadow comparison without introducing brand-new network behavior on the verify route.
  • No internal mismatch endpoint. A read-only viewer endpoint (GET /__internal__/shadow-mismatches) is intentionally omitted because the sink has no live producer in this version; an empty endpoint adds review surface without operational value.
  • No packages/protocol source change. No diagnostic capture hook is added; that pattern is reserved for a future change.
  • No public API, wire-format, OpenAPI, or publish-manifest change.

Why route integration is deferred

A live Hosted Verify route integration would have to either:

  1. Run protocol's pointer-fetch as a brand-new behavior on a route that has none today, then run the workspace-private resolver alongside as a "shadow." This would be a new feature, not shadow observation, and would double-fetch upstream pointer URLs. The diagnostic foundation's "no double-fetch" invariant rejects this.
  2. Wait for a primary-path pointer-fetch result to capture from a route that already does pointer-fetch. No such route exists in apps/api as of v0.13.2.

Live route shadowing is therefore deferred to a future change contingent on either:

  • A new Hosted Verify pointer-input feature that legitimately exercises pointer-fetch on the verify path (route would surface pointer_url + expected_digest in the request shape), OR
  • A protocol diagnostic capture hook (callback or telemetry channel exposed by @peac/protocol) that surfaces pointer-fetch outcomes from any caller for observation, without requiring the observer to invoke pointer-fetch a second time.

Until one of those exists, this foundation supports offline parity comparison only.

Mismatch classes

The sink records entries with the following class field. Parity classes capture cross-implementation drift detected by the verdict computer; the remaining classes are reserved for future producers.

Class Meaning
parity_class_mismatch Protocol path and the workspace-private resolver path normalize to different NormalizedPointerClass values for the same input.
parity_digest_mismatch Both implementations succeeded but reported different actualDigest values. Stop-the-line if observed in production.
parity_content_type_warning_mismatch One implementation surfaced a content-type warning class and the other did not.
parity_success_shape_mismatch Both succeeded but the public success-shape keys (actualDigest, contentType presence) differ.
output-byte-diff Reserved. Future producer for raw-output byte divergence under a future capture hook.
error-code-diff Reserved. Future producer for native-error-code divergence under a future capture hook.
timing-diff Reserved. Future producer for cross-implementation timing-bucket divergence.
resource-limit-diff Reserved. Future producer for resource-limit-class divergence.
cache-hit-diff Reserved. Future producer for JWKS cache-hit divergence.
cross-runtime-drift Reserved. Future producer for cross-runtime drift.

Sink entry shape

Each sink entry is bounded to ~512 bytes when JSON-stringified. Strict redaction: no raw URL path or query, no headers, no body bytes, no excerpts beyond the bounded excerptLegacy / excerptShadow fields (<=128 bytes each, only set for output-byte-diff). No bearer tokens, cookies, private key material, or secret-shaped values reach the sink.

ts                ISO 8601, second precision
requestHash       sha256 hex of normalized request signature (<=64 chars)
class             one of the classes above
legacySummary     bounded { ok, code? <=64, byteCount?, jwksKid? <=32, durationBucket? }
shadowSummary     same shape as legacySummary
excerptLegacy?    <=128 bytes; only for output-byte-diff (no live producer yet)
excerptShadow?    <=128 bytes; only for output-byte-diff (no live producer yet)

When an entry would exceed the ~512-byte cap, the sink degrades progressively: clamp string fields to per-field caps, then drop excerpts, then replace summary codes with the placeholder truncated, then fall back to a minimal entry (ts, class, ok flags).

Buffer behavior

The buffer is a fixed-capacity in-memory ring. Default capacity 1024 entries. PEAC_INTERNAL_SHADOW_BUFFER_SIZE is read once at first use and clamped to [64, 16384]. When the buffer is full, the oldest entry is overwritten; entries are never persisted to disk and never sent over the network.

In clustered deployments each Hosted Verify instance has its own ring buffer; no cross-instance aggregation is built or planned for this foundation.

How to read entries (after a future producer ships)

Once a real producer is wired (live route integration, protocol diagnostic hook, or both), entries can be inspected via the in-process getter getMismatches() exported from apps/api/src/lib/shadow-mismatch-sink.ts. This foundation ships no remote retrieval surface.

When investigating drift:

  1. Group recent entries by class. Any parity_digest_mismatch is a stop-the-line bug; both implementations should compute the same SHA-256 of the same UTF-8 decoded body.
  2. parity_class_mismatch typically points to a taxonomy gap in apps/api/src/lib/shadow-classify.ts. Inspect the input that produced the mismatch (via requestHash) and whether the classifiers map a new error code or reason from either implementation.
  3. parity_content_type_warning_mismatch and parity_success_shape_mismatch typically reflect implementation differences that are operationally benign but worth tracking. They may inform future doctrine clarifications.

What this is not

This document does not specify any normative protocol behavior. It does not commit to wire formats, public APIs, or operational SLOs. It is a maintainer-facing diagnostic note about the shadow-mode foundation shipped in v0.13.2.

Related documents