Skip to content

fix(conformance): the delegation-lifetime case proves the lifetime without racing the clock - #65

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/conformance-delegation-lifetime-not-timing-bound
Oct 30, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/conformance-delegation-lifetime-not-timing-bound

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator

The two ejections

module-saas-starter's required check Host interface conformance (authority main) failed twice today on merge-group runs, with green PR runs on the same heads:

  • run 41085221872: conformance.go:950: a standing one-second delegation does not issue: rpc error: code = Unauthenticated desc = work context: invalid: parent session expired at 2026-10-30T15:59:19Z; it can open no further session
  • run 41133302655: the same, parent session expired at 2026-10-30T21:49:12Z (subtest took 0.18 s)

The race

The case asked for a one-second delegation and issued from it at once. The shared core (authority-core delegation.go) starts the root until d.ExpiresAt, and core's workcontext derive reads the parent's end as time.Unix(expires_at_unix, 0), which is whole seconds. A delegation asked at hh:mm:11.85 for 1 s ends at 12.85 but is signed to end at 12, leaving 0.15 s. So a one-second delegation could be issued for anywhere from a few milliseconds to one second. Whenever Delegate and Issue straddled that truncated second, the issue was refused. This is a whole-second race in the case, not a host bug. Runner load only makes it more likely.

The fix

Only the case changes, in libraries/conformance/go/conformance.go:

  • The enforcement delegation now has a lifetime of 10 s. The truncation costs at most 1 s, which leaves at least 9 s for one Delegate→Issue round trip; the case measures 11.3 s end to end.
  • The suite still cannot move a host's clock (the fixture has no clock hook), so it still waits. It now measures the wait on the host's own clock: issued_at_unix of the first issue, then sleep(at + 1 − now) + 250 ms. That is past the host's precise end, whatever offset the suite's clock has.
  • The carried end must lie in [now + 5, now + 10] of the host's now: within the lifetime asked and not much sooner.
  • Each lifetime in the 60 / 3600 / 0 loop must also end no later than issued_at + ttl. A host may clamp a lifetime shorter, never longer.

What stays proven

Mutation runs against the local authority (core edited temporarily, not committed). Each one fails the case:

Host mutation Fails at
ignores the lifetime (no end) a delegation asked for 60 s ends at 0
carries an end 5 s longer than asked … ends at …, past the host's now … plus its lifetime
reports the end but never enforces it its module reads an expired delegation as standing

Unchanged: an issued capability never outlives its delegation, a negative lifetime is InvalidArgument, no lifetime carries no end, the module reads the same end, and after the end the host refuses to issue (PermissionDenied) and reads the delegation as not standing.

Evidence

  • go vet ./... in libraries/conformance/go: clean.
  • Local authority, TestTheLocalAuthorityPassesTheSuiteEveryImplementationMustPass: full suite ok (43.5 s).
  • Local authority, the case with -count=20: 20/20 PASS (11.25 s each, 225 s total).
  • accounts (module-saas-starter main a75fae35), with -modfile replacing only the conformance library with this branch and nothing in that checkout changed: full TestAccountsPassesTheHostInterfaceConformanceSuite ok (72 s, 294 PASS lines). The case with -count=20: 20/20 PASS (11.30 s each).

Cost: the case now waits about 11 s instead of 1.5 s.

🤖 Generated with Claude Code

…thout racing the clock

A host carries a delegation's end in whole seconds and signs the issued
root until that second, so a one-second delegation was issuable for
anywhere between a few milliseconds and one second. The case asked for one
and issued at once, and failed whenever the end landed before the issue.

The case now asks for ten seconds, checks the carried end lies within the
lifetime of the host's own now (issued_at of the first issue), and waits
from that carried end on the host's clock before asserting the host
refuses to issue and reads the delegation as not standing. Each lifetime
in the loop is also checked to end no later than asked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit dea37af into main Oct 30, 2026
17 checks passed
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