Skip to content

fix: validate stored records at the public boundary (v0.1.3) - #8

Merged
bearmug merged 1 commit into
mainfrom
release/v0.1.3-record-boundary
Sep 18, 2026
Merged

bearmug merged 1 commit into
mainfrom
release/v0.1.3-record-boundary

Conversation

@bearmug

@bearmug bearmug commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What

isRecord was exported as a DecisionRecord type guard but only detected record-shaped envelopes: isRecord({ response: null }) returned true, so a library consumer could treat a malformed envelope as a validated record. Split detection from validation:

  • isRecord(input): boolean — envelope detector only.
  • readRecord(input): DecisionRecord — new validated boundary; returns a checked record or throws.
  • gate, replay, extractResponse route through readRecord. Behavior on malformed input is unchanged (same validator, same errors); only the advertised contract is honest.

Plus examples/library/record-and-gate.mts: the complete recorded workflow with public imports only — one shared SDK client, SIGINT/SIGTERM abort, elapsed-time measurement, client errors that never reach policy, a record storing hashes of state/questions plus model identity and latency (never the supplied text), then offline policy over readRecord + evaluatePolicy. Verified against the stub: says_nothing at 0.6 → deny exit 3, record carries stub:jev-latest, no claim text, replay re-emits it. README library section rebuilt around it.

Verification

  • 93 tests pass, tsc --noEmit clean.
  • New tests: detection without a validity claim, validation through readRecord, plus the existing malformed-envelope and bare-response coverage.
  • npm pack --dry-run: 93 files / 60.4 kB, example included via examples/.

Version bumped to 0.1.3. After merge: tag + release → publish workflow stages it → maintainer approves with 2FA.

…l library workflow

The package root exported isRecord as a DecisionRecord type guard, but it only
detected record-shaped envelopes: isRecord({ response: null }) returned true,
so a consumer could treat a malformed envelope as a validated record. Split the
two jobs. isRecord is now a plain detector; readRecord is the boundary that
returns a checked DecisionRecord or throws. gate, replay, and extractResponse
route through it; behavior on malformed input is unchanged (the same errors,
from the same validator), only the advertised contract is honest. Tests pin
both: detection without a validity claim, and validation through readRecord.

Add examples/library/record-and-gate.mts, the complete recorded workflow with
public imports only: one shared SDK client, SIGINT/SIGTERM abort, elapsed-time
measurement, client errors that never reach policy, a record that stores hashes
of state and questions plus model identity and latency (never the supplied
text), then offline policy over readRecord + evaluatePolicy. Verified against
the stub: says_nothing at 0.6, deny exit 3, record carries stub:jev-latest and
no claim text, replay re-emits it. README library section rebuilt around it;
examples README documents how to run it.
@bearmug

bearmug commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

@astra please review this PR (slow role). Focus areas:

  1. The isRecord → readRecord split: does any public path still let a malformed envelope reach response handling unvalidated? extractResponse routes record-shaped input through readRecord; callers in gate.ts and replay.ts use the same. Confirm the old isRecord(...) ? coerceRecord(...) call sites are all converted.
  2. The new example: does it claim anything the code does not do (e.g. about what the record stores, or policy behavior on failed requests)? It is meant to be the canonical library-integration pattern.
  3. Check README / examples README / tests stay consistent with the implementation, as before.

Blocking: any path where malformed or ambiguous input can still reach accept, or where the docs promise a guarantee the code does not enforce.

@bearmug
bearmug merged commit 75fa788 into main Sep 18, 2026
2 checks passed
@bearmug
bearmug deleted the release/v0.1.3-record-boundary branch September 18, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant