Skip to content

feat(zdr): give every provider ZDR attestation an explicit expiry - #1916

Open
seonghobae wants to merge 5 commits into
mainfrom
lane-jan/zdr-attestation-expiry
Open

feat(zdr): give every provider ZDR attestation an explicit expiry#1916
seonghobae wants to merge 5 commits into
mainfrom
lane-jan/zdr-attestation-expiry

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Lane: jan | session:session_01BV96rXhqoR3tYZ9AeAVur4

Why

The five entries in PROVIDER_ZDR_SCOPE each carry an as_of date and nothing else. Nothing anywhere ever compares that date to anything, so a frozen citation ages silently: it keeps reading as authoritative long after the last time anyone actually opened the source.

That matters because these are not measurements, they are readings of living documents. scripts/ci/zdr_policy.py:99-111 spends thirteen lines explaining that NVIDIA's Section 3.3(iv) training carve-out is what makes nvidia_nim affirmatively not-ZDR, and instructs the reader not to reclassify it "without a superseding, dated NVIDIA document". If NVIDIA published exactly that superseding document tomorrow, this table would not notice, and neither would any agent reading it — the as_of would still look reassuringly recent to anyone who did not do the subtraction. The same is true in the dangerous direction for openrouter, the one entry that grants ZDR.

These are the only dated citations in this repository with no expiry of any kind.

What this changes

  • valid_until is now a required field on ProviderZdrScope — no default, so a future entry cannot be added without one. All five entries are set to as_of + 90 days, recorded as ATTESTATION_REVIEW_WINDOW_DAYS. Ninety days is a quarterly re-read cadence for a published ToS or data-policy page; deliberately longer than the monthly window used for measured cost evidence, because these track legal text that changes on the provider's schedule rather than a price that drifts continuously.
  • attestation_is_current(provider_name, today) and expired_provider_names(today) — pure reporting functions. Both take the date as an argument rather than reading the clock, so callers and tests stay deterministic.
  • is_zdr_model(..., today=None) — when today is supplied, an attestation past its valid_until grants nothing. That is not a new policy: the module docstring already states that any provider whose guarantee "cannot be attested from a machine-readable, dated source is treated as NOT ZDR", and a citation nobody has re-read for a quarter is precisely such a source.

What this deliberately does not do

It does not become a dated time bomb in the org-wide review path. today defaults to None, which preserves the current decision exactly, so no existing caller changes behaviour when these dates lapse in November. Wiring a caller to pass today flips a switch that can withdraw ZDR routes from the whole review pipeline at a date certain, and a change with that blast radius should be reviewed on its own terms rather than riding in on a data-model change. Until then the expiry is fully observable via expired_provider_names().

This is the shape the same problem should have taken elsewhere: contextual_orchestrator/nim_benchmark.py enforces its window unconditionally, and the resulting wall-clock coupling had to be repaired in ContextualWisdomLab/contextual-orchestrator#1073 by pinning the test dates. Hence the corresponding discipline below.

Verification

Every new test pins fixed dates — there is no date.today() anywhere in them — so the tests cannot age into failure. test_expired_provider_names_reports_only_the_lapsed_entries asserts against 2026-11-26 and 2999-12-31; test_attestation_is_current_on_and_after_the_expiry_boundary derives its dates from the entry it is testing rather than hard-coding them, so it stays correct when the dates are next refreshed.

test_every_attestation_carries_a_valid_until_after_its_as_of is the contract that prevents the original defect from returning: it fails if any entry is added without valid_until, or with one that does not equal as_of + ATTESTATION_REVIEW_WINDOW_DAYS.

test_is_zdr_model_expiry_cannot_promote_a_non_zdr_provider pins the direction of the fail-closed path — expiry may only ever remove a grant.

Test plan

  • python3 -m pytest tests/test_zdr_policy.py -q33 passed (25 existing + 8 new)
  • coverage run -m pytest tests -q2891 passed, 1 skipped, 21 subtests passed
  • coverage report100%; scripts/ci/zdr_policy.py 45 statements / 8 branches, 0 missing
  • interrogatePASSED (100.0%)
  • The four other suites that import this module — test_contextual_orchestrator_review_policy, test_contextual_orchestrator_free_credential_admission, test_contextual_orchestrator_review_sidecar_contract, test_agent_review_runtime_quality_consolidation — pass unchanged; no other file constructs ProviderZdrScope, so the new required field breaks no caller
  • git diff --check clean
  • Authored in a fresh worktree branched from origin/main, not a stale checkout

Developer experience

An agent reading the table can now tell at a glance whether a citation is still inside its review window, and expired_provider_names(today) answers "what needs re-reading" in one call instead of five manual date subtractions. Adding a provider without an expiry now fails a test rather than passing silently.

User experience

No runtime behaviour changes in this PR. The path it opens is that a stale ZDR citation can be made to withdraw a routing grant automatically instead of continuing to authorize private-target traffic on an unread policy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4


Generated by Claude Code

The five entries in PROVIDER_ZDR_SCOPE each carried an as_of date and
nothing else, so a frozen citation aged silently: it kept reading as
authoritative long after anyone last opened the source. A provider data
policy is a living document — NVIDIA can repeal the Section 3.3(iv)
training carve-out and OpenRouter can change what its ZDR feed covers,
and neither event edits this file.

Add a required valid_until to ProviderZdrScope, set on all five entries
to as_of plus a 90-day quarterly re-read window, so no future entry can
be added without one. Expose the staleness as data through
attestation_is_current() and expired_provider_names(), both taking the
date as an argument rather than reading the clock.

is_zdr_model() gains an optional today. Supplied, an expired attestation
grants nothing — the same conservative stance the module already takes
toward a policy it cannot ascertain. Omitted, the decision is unchanged,
so no existing caller changes behaviour and this cannot become a dated
time bomb in the org-wide review path. Wiring a caller to pass today is
deliberately left to its own reviewed change.

Every new test pins fixed dates instead of date.today(), so the tests
themselves cannot age into failure — the mistake tests/test_nim_benchmark.py
had to be repaired for in #1073.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 36 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 0d01c0c3-b1a3-4514-89b2-30f82dfefdff

📥 Commits

Reviewing files that changed from the base of the PR and between 4302463 and bf6e047.

📒 Files selected for processing (2)
  • scripts/ci/zdr_policy.py
  • tests/test_zdr_policy.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae marked this pull request as ready for review September 5, 2026 16:13

Copy link
Copy Markdown
Contributor Author

CI status note — standing down on the two red checks on head ae918d114.

CodeQL compatibility analysis (python) and (actions) ended with VERDICT_STATE=pending: the compat job's designed fail-fast after dispatching a CodeQL PR scan whose terminal verdict has not been published yet. The dispatched scan (run 33977186000, re-dispatched 16:13Z by the ready-for-review event) is queued behind the organization job ceiling — of 70 CodeQL PR runs in .github since 13:00Z, 44 are queued, 20 cancelled, 2 failed, 0 succeeded. Nothing in this PR's diff is involved, and the dispatch workflow reruns this exact job once the verdict lands.

Not re-running it manually: a re-run dispatches again and enlarges the queue that is causing the failure (docs/doctoring/ci-failure-signature-triage.md §9). Capacity relief is in #1938.


Generated by Claude Code

Rebinds workflow_sha so the required review runs pick up #1939's
round-robin catalog fill; the previous head's Strix and Noema runs failed
on the single-upstream gateway stall that #1939 removes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

CI note and push: head ae918d1145bf69aa6a (a base merge only; no change to the PR's own commits).

Two new red checks on the previous head, both the single-upstream gateway stall class and neither in this diff: strix (job 101354799616) ended "provider/backend was unavailable", and noema-review (job 101354039599) failed with the gateway 502 after 1274 s. #1939 removed that stall on main at 17:25Z (the sidecar catalog now round-robins across credential accounts instead of filling with two NVIDIA credentials), but a re-run cannot pick it up — workflow_sha is bound at run creation, so a re-run reuses the pre-#1939 sidecar (catalog §2). This push merges main@f2f91b806 into the branch to create the new event. The two CodeQL compatibility failures remain the designed pending state noted above.

Local gate on 5bf69aa6a: 2904 passed / 1 skipped, coverage 100% (13139 statements, 5306 branches), interrogate 100%, git diff --check clean. Cadence: 8 h 37 m since the previous push.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head 5bf69aa6a: opencode-review (run 33992902160, job 101393687476) failed in seconds at "Fail closed without a current-head OpenCode verdict" — catalog signature 1, not a review verdict; no opencode-agent review exists on this head. The job's dispatch step succeeded, but every OpenCode Review Dispatch handler run completed since 2026-09-05T17:00Z (83 of 83) was rejected in validate-pr-metadata because the dispatcher identity opencode-agent[bot] is not in OPENCODE_REPOSITORY_DISPATCH_ACTOR (still github-actions[bot]). #1932's multi-identity parser is on main; the variable is an owner action tracked in #1929.

Nothing in this diff (zdr_policy.py attestation expiry and its tests) is involved, and no push or re-run from this side changes the outcome — a re-run re-dispatches into the same rejected gate, and the dispatch workflow re-runs this exact job by itself once a verdict is published. Standing down on this check until #1929's variable is reconciled. The two CodeQL compatibility failures remain the designed pending state noted earlier.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head 5bf69aa6a, second failing check: noema-review (run 33992902161, job 101388223446) failed in "Prepare Noema model verdict" with HTTP Error 502: Bad Gateway; caller attempts=1, duration=3783.4s, phase=response_error, served_model=deepseek-ai/deepseek-v4-flash-0731. Gateway-side; nothing in this diff is on that path.

This is the longest walk of the night: 63 minutes of a held runner, 3783.4 / 548 ≈ 6.9, so every ready route waited out its per-recv timeouts before the gateway surfaced the last route's error. Same window and same free-pool exhaustion as the other four (#1872, #1938, #1930, #1913; #1930's Strix artifact showed 1 ready route of 12 at 23:48Z). The run was created at 21:23Z — post-#1939 sidecar, pre-#1944 workflow — so no noema-sidecar-evidence artifact here. Post-#1939 noema-review runs in this repository that reached the verdict step: 1 success, 5 failures; failed Noema/Strix jobs since 21:00Z have now held runners for about 290 minutes.

Action: not re-running while the pool reads like this (the catalog rule this session recorded); the single sanctioned re-run stays gated on #1938's queued attempt 2 or folds into the next base-merge push, which also binds the artifact-producing workflow. Owner-level tracking: contextual-orchestrator#1045.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head 5bf69aa6a, third failing check: strix (run 33992902189, job 101388232733) ended "provider/backend was unavailable" after 2 h 18 min in "Run Strix (quick)" (23:45–02:02Z), the longest single review job of the night. Gateway-side; nothing in this diff is on that path. From its strix-reports artifact (9980773663):

  • preflight ready 4 / 12 — all four deepseek-v4 routes on both NVIDIA keys; the four openrouter routes 429, the four gemma-3 routes 404.
  • run.json: llm_usage.requests: 29, status failed; the scan ended on Strix's stream idle timeout after the gateway answered request_failed status=500 code=internal_error 76 times and 503 service_unavailable once (sidecar stderr; pre-fix(sidecar): let the stream sanitizer pass orchestrator route and circuit events #1945 sanitizer, so no per-route lines).

That is a third shape next to the 429 and the held-open 502: the gateway itself returning 500s in bulk while every ready route is a deepseek-v4 key under contention. Standing down on this check; the single sanctioned re-run for this PR is not being spent while #1938's attempt 2 has just failed after 50 minutes on the same pool. Owner-level tracking: contextual-orchestrator#1045 and #1948.


Generated by Claude Code

Binds the sidecar pin bump to contextual-orchestrator@414f2297
(contextual-orchestrator#1081's retry-stacking fix, .github efb8926) and the
#1943/#1944/#1945 workflows to this head's required runs; the previous head's
Noema/Strix runs failed on the stalled-route loop that fix removes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

CI note and push: head 5bf69aa6a91d972be6 (a base merge only; no change to the PR's own commits; 5 h 50 m since the previous push).

Merges main@efb892692, the owner's bypass-merged sidecar pin bump to contextual-orchestrator@414f2297 (contextual-orchestrator#1081: _invoke's retry-then-failover no longer stacks on the client's max_retries — the loop behind this head's 63-minute noema-review 502), plus #1943 / #1944 / #1945. A re-run could not pick any of that up (workflow_sha and the sidecar pin are bound at run creation), so this push is the remedy for this head's failed noema-review and strix.

Local gate on 91d972be6 with GITHUB_ACTIONS=true: 2912 passed / 1 skipped, coverage 100%, interrogate 100%, git diff --check clean. The pool condition (#1948) is unchanged by the bump, so a Noema or Strix failure on this head is still possible; if it happens, the run now ships its per-candidate trace as an artifact. opencode-review stays fail-closed until #1929's variable is set; the CodeQL shards will read designed-pending again.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head 91d972be6: noema-review (run 34008489631, job 101427228102) failed at "Provision contextual-orchestrator review sidecar" after 5 min 6 s (05:05:21–05:10:27Z) on the new pin 414f2297; "Prepare Noema model verdict" skipped. Artifact noema-sidecar-evidence (9983130621): ready 0 / rejected 12 — both NVIDIA keys' deepseek-v4-flash 429, both keys' deepseek-v4-pro TimeoutError at the 90 s probe bound, the four gemma-3 404, all four OpenRouter free routes 429, bytez discovery http_status_500 — probed 05:07Z. With zero ready routes the launcher exits before /healthz and the sidecar script fails the job: signature 3's fail-fast shape, the fifth consecutive zero-ready artifact across my heads since 04:21Z (#1913, #1661, #1938 ×2, this). Not this PR's; no re-run while the latest artifact shows ≤1 ready route; nothing to push. strix and opencode-review on this head are still queued; their outcomes get one note each only if they differ from what is already recorded here.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head 91d972be6: strix (run 34008489633, job 101427872509) failed after 2 h 16 m (05:08:23–07:24:48Z; "Run Strix (quick)" 126 min) with STRIX_PROVIDER_UNAVAILABLE on the new pin 414f2297 — the same class as #1930's scan an hour earlier, at larger scale. Artifact strix-reports (9984863327):

  • Route preflight 05:14Z: ready 4 / rejected 8 — both keys' deepseek-v4-flash and deepseek-v4-pro ready.
  • Serving 05:21–07:24Z: Strix completed 42 requests (39 with usage; 1,993,908 input / 12,457 output tokens) while the gateway logged 63 TimeoutErrors and 63 × status=500 code=internal_error; nvidia_nim deepseek-v4-flash, the first-ranked route, was attempted 114 times. 0 of the 63 timeouts is followed by a circuit_failure record; the 13 circuit records all follow fast HTTP failures. 63 × 90 s is 95 of the 126 minutes.

So this scan did more work than any earlier one on this pool and still could not finish, because every timed-out stream request re-selected the same silent first-ranked route: the tool-bearing passthrough class co#1082 fixes (bare TimeoutError500 internal_error, _record_failure unreachable), with the 30 s breaker reset against a 90 s attempt as the residual noted there. Not this PR's; nothing to push; this PR's sanctioned re-run is unspent and held for the same reason as #1930's — while the first-ranked route stalls, a re-run is a ~2-hour coin flip. Consolidated Strix outcomes for the three post-bump scans go to #1948 once #1946's ends.


Generated by Claude Code

seonghobae pushed a commit that referenced this pull request Sep 6, 2026
… timeouts, 0 circuit records, 126 min)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

CI note and push: head 91d972be6bf6e0477c (a base merge only; no change to the PR's own commits; 7 h 10 m since the previous push).

Merges main@43024633e, which carries #1947 (deferred transient routes), #1949 (lazy fill + per-account 429 skip), #1950 and #1953 on top of the efb892692 pin bump this head already had. That is worth a fresh run rather than a wait: the merged launcher measurably changes both ends of this head's failure mode. On a fully rate-limited hour it now stops after 6 probes instead of 12 and fails provisioning in 126 s instead of 300–844 s (#1913 run 34021019274, artifact 9987026530), and on a live hour it reaches routes the old fill never did — contextual-orchestrator#1044's 08:26Z run preflighted ready 6 / probed 16 / skipped 4 / deferred 2, including both keys' llama-3.2-11b, and carried a review to a served completion.

The five red checks on the previous head were all attributed and none was this diff's: strix (run 34008489633) spent 126 minutes on the tool-bearing passthrough timeout class — 63 timeouts, 63 × 500 internal_error, 114 attempts on the same first-ranked route, 0 circuit records (co#1082's scope, noted 07:24Z); noema-review the zero-ready provisioning shape; opencode-review signature 1's fail-closed wait behind #1929; both CodeQL compatibility analysis shards the designed dispatch-and-exit.

Gate on the pushed tree: 2936 passed, 1 skipped, 21 subtests; coverage 100 % (13,192 statements / 5,328 branches, 0 missed); interrogate 100 %; git diff --check clean.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI note on head bf6e0477c: three reds, all known classes, none this diff's.

Nothing to push, no re-run (0 ready routes). Worth recording as the direct before/after this head produced: the same zero-ready outcome on this PR took 555 s of provisioning + 126 min of scanning at 05:08Z on the pre-#1949 launcher, and 123 s total now. The failure is the same; its cost fell by two orders of magnitude, which is what the base merge was for.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Addition to my note 45 minutes ago: strix on this head is a different class from the noema-review zero-ready failure I described, and it is the co#1082 passthrough class again.

strix (run 34027314569, job 101477203108, 11:55–12:47Z) provisioned successfully in 751 s with preflight ready 6 / probed 16 / skipped 4 — the pool was live for this job — then "Run Strix (quick)" failed after 2356 s. The trace: 21 timeouts, 0 of them followed by a circuit_failure record, and 17 × status=500 code=internal_error. No loginAsGuest line, so not #1953's sandbox class.

That zero-of-21 adjacency is the discriminator: the tool-bearing passthrough path never records the stalled candidate, so it is re-selected on every retry (contextual-orchestrator#1082's scope) — against Noema's no-tools _invoke path on my other heads today, where 9 or 10 of every 14–15 timeouts did record and fail over. Same pool, same minute, opposite behaviour, decided by which request shape the reviewer uses.

So this head has now shown three of the four known outcomes within an hour: ready 0 provisioning failure (12:00Z, noted above), this served-but-stalled Strix scan (11:55Z), and the actor-gate fail-closed wait. Nothing this PR's; nothing to push; no re-run — the fix for the third is co#1082, not anything on this branch.


Generated by Claude Code

seonghobae pushed a commit that referenced this pull request Sep 6, 2026
…rections

Section 0 gains the CI-run counterpart of the stale-checkout rule: a
pull_request_target run executes the base branch's scripts as of the run's
created_at, not its started_at, and on 2026-09-06 those were routinely 90
minutes apart. #1916's 12:00Z preflight looked like #1957 failing 23 minutes
after it merged; the run was created at 10:24Z. Classify by created_at, or by
the report's own version marker (postponed_probed_count).

Section 3 records what #1957 changed, measured on this repository's own heads
(ready 0 -> 1, probed 6 -> 16), and three things the earlier text got wrong or
did not have:

- caller attempts=1 counts sidecar->gateway calls, not routing. One such
  attempt carried ~20 provider_attempt lines across four agents, so it is not
  evidence that failover did not happen.
- The ~90 s attempt is now measured: 90.090 s and 90.115 s TimeoutError probes
  of google/gemma-4-31b-it on both NVIDIA keys.
- A third reason the breaker cannot exclude a bad route, stronger than the 30 s
  reset: _record_success pops the circuit state entirely, so any single success
  zeroes the count and an alternating route never reaches the threshold.

The sandbox split is refined: is_caido_bootstrap_timing_error() inspects only
the last attempt, so grepping an artifact for loginAsGuest is not sufficient.
Four artifacts from one day show every combination, including #1930's run
34027404208, which carries the line but ends in gateway exhaustion. Classify on
the verdict line; the check annotation is generic by design.

Section 7 gains the occupancy half of the queue shortage: #1930's strix job held
a runner for 351.8 minutes and produced no verdict, which is execution rather
than queue wait -- its sibling jobs finished in tenths of a minute. Capping long
jobs is a different remedy from adding capacity.

Section 9 gains the DISPATCH_OUTCOME/VERDICT_STATE discriminator that separates
it from signature 1, plus the 34-of-34-queued measurement.

Gate: 2956 passed, 1 skipped, 21 subtests, coverage 100%, interrogate 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
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.

2 participants