Repository navigation
Run leases and effect-host fencing for unattended runs: owner, authoritative expiry, monotonic epoch, fence at commit and admission (#223) - #251
Conversation
A lease of one store resource has an owner, an authoritative expiry
decided by the store clock and an exact epoch: a canonical base-10
string from 1 to 999999999999999999 that never travels as a JSON number.
Acquire is revision-checked, renew needs the current owner and epoch
before expiry (an expired lease is never revived), and release keeps the
epoch, so every later acquisition is strictly greater.
The fence {resource, owner, epoch} is checked by the store in the same
transaction as every checkpoint commit (and again at publication), and
by the effect host itself at every non-read host or runtime admission.
The effect's idempotency key is the SHA-256 of {run, operation, source,
tool, requestDigest} with no epoch, so a takeover cannot turn an unknown
operation into a new action. Memory and callback kinds and unfenced
effect hosts refuse unattended admission by name.
New versioned records (contract/schemas/run-lease.json) with readers in
both languages, handwritten vectors in contract/vectors/lease/, the lease
conformance kit, the ADR-0025 amendment naming the open members, the
host-facing storage section and twelve mutation rows with Go twins.
Published checkpoint and event formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Carries #250 (dialect inventory paging). mutation-rows.json keeps main's agent#243 rows, then agent#223's; record shapes, the Go contract types and the event reference are regenerated by their generators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…rcing kits (#223) The security review of a9d9e3b found one HIGH, three MEDIUM and one LOW. - HIGH: the writer took its read flag from readingTool, which frames every source's answer as data, so a gateway, MCP or custom act was recorded as a read, never reached the effect host, ran under an expired lease and ran again after a takeover. durableReadingTool decides by effect: a source tool is a read only with effect class read, and an owner-declared act or propose effect is never a read. Source reads and the receipt quarantine are unchanged. - MEDIUM: a leased run's chat-snapshot commits present its fence (commitContextSnapshotFenced), so a driver that lost its lease cannot release the turn its takeover restores. - MEDIUM: both lease kits now refuse stores whose checkFence, renew, release, fenced history commit or fenced snapshot commit accept a stale epoch (also the same owner's earlier one), another owner, a released lease or a mismatched resource. A new effect-host conformance kit; unattended admission accepts only the reference host or a host certified by certifyEffectHost, never a declaration. - MEDIUM: tests pin runtime-kind effects through the effect host and the host's resource-mismatch refusal. - LOW: impossible expiries refuse as invalid in TypeScript, with vectors. Six mutation rows (L13-L18) and five Go twins, all caught; storage docs updated, including that the fenced view is passed unwrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
|
Verdict at 67e25ed: merge once the pending Review record: agent PR #251 (issue #223), run leases and effect-host fencingCandidate and toolchains
A Claude Opus review over a Claude Opus build; not a cross-vendor claim. Clean room: I did not use the builder's workspace (I only measured its dependency folder's size, to plan disk use) and did not contact the builder. Nothing was posted, pushed or edited outside the room. Method
Probes"Plain" is a non-read host tool without a source (the PR tests'
Probe commandsTypeScript runs
R1 compares MutantsRows ran through
Findings
What was not established
Re-read at 67e25edThis is the one re-read the one-round rule allows. A Claude Opus review over a Claude Opus build; not a cross-vendor claim. Nothing was posted, pushed or edited outside the room. Candidate
Dispositions
Re-read probesThese are the same probe files with P17–P21 and G10–G11 added. At this head all 36 TypeScript probes and all Go probes pass. G11 records its results without asserting.
Re-read mutantsEvery result below was decided by the selected tests, not by the whole-suite fallback.
Findings that remain at 67e25ed
What the re-read did not establish
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
|
🎉 Thank you for your contribution to Judgment Pack! If you’d like another task, choose a path that interests you: |
Conflicts resolved: mutation-rows.json keeps main's agent#223 rows, then the agent#252 rows; both record-shapes files regenerated with the generator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…test expiry on the fenced commit paths (#253) (#257) * Run leases: bind effect-host certification to the admitted instance; expiry on the fenced commit paths (#253) The v0.12.0 pre-release batch from the security re-read of #251. - R1: both lease kits now refuse a store whose commitHistoryFenced or commitContextSnapshotFenced accepts an expired fence, at equal time and after. - R2: the effect-host kit makes its authority fail, and a host that performs then fails it. certifyEffectHost / CertifyEffectHost call the factory once, over an authority that answers from the kit until the checks pass and from the real authority after, and admit that exact instance: a frozen TypeScript wrapper that refuses once the instance's dispatch is not the one checked, a Go *CertifiedEffectHost whose identity is the evidence. The reference host's dispatch is sealed on the instance. Unattended admission accepts only these, and only when their authority is the run's own store. - N1: Go keeps no registry of hosts; admitted hosts are collectable. - N2: the Go agent adapter's pre-effect save follows DurableReadingTool. Nine mutation rows (agent#253) and ten Go twins, all caught; L17 re-anchored. Storage docs updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> * Regenerate the reference pages for the moved lines (#253) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> * Run leases: admit exactly the dispatch certification ran; refuse a copied reference host (#253) From the narrow re-check of #257. TypeScript certification reads the host's dispatch once, runs the kit through that function and admits only it, so a getter or a Proxy trap cannot show the kit one function and admission another; the reference host's authority, admitted set and queue are #private fields. In Go only the pointer NewReferenceEffectHost made is evidence (a self pointer), not a shallow copy that would share its key map without the lock. Rows C1-C3 added; L4, L9 and B4 re-anchored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --------- Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #223. Milestone v0.12.0. Implements ADR-0025 decision 1 on the package RunStore (and ChatStore) ports, in both languages.
What changed
Vocabulary (ADR-0025 and its new amendment). A lease of one store
resourcehas an owner, an authoritative expiry (the store clock plus a requested duration of 1 ms to 24 h, UTC with exact millisecond precision) and an exact epoch, plusstate(held/released; expired is decided by the store clock at each check, equal time is expired). The fence{resource, owner, epoch}is the evidence presented at every checkpoint commit and received by the effect host at every admission. ADR-0025 is already accepted; the amendment only fixes the names the decision left open (thefencerecord,expiry,state, the refusal reasons,leaseLifetime,effectFencing, the epoch encoding). Its review stays with the maintainer.Epoch encoding. A canonical base-10 string from
1to999999999999999999(18 digits: exact in a signed 64-bit integer and a SQLbigint, never a JSON number, so no reader rounds it). Acquiring past the largest epoch refusesepoch-exhausted; any sign, zero, leading zero, space, fraction, exponent, hex or numeric spelling isinvalid.Lease port.
LeaseStore(TSstorage/lease.ts, Gocontext/run_lease.go):acquireLease(checked against the resource's revision;heldwhile an unexpired lease is held; epoch + 1),renewLease(current owner and epoch before expiry; an expired lease is never revived),releaseLease(keeps the epoch),readLease,checkFence.MemoryRunStoreandMemoryChatStoreimplement it withleaseLifetime: 'process'; the callback and legacy chat adapters have no lease port.Fencing at every commit.
commitFenced/commitHistoryFencedcheck the fence in the same transaction as the revision, and again at publication.fencedRunStore(store, fence, effectHost)(GoFencedRunStore) is the view a durable run writes through, so every claim, result and checkpoint presents the fence.fencedChatStoredoes the same for chat updates (Go has no ChatStore).Fencing at effect admission. The
EffectHostport receives{version:1, fence, operation, idempotencyKey}and checks the fence itself before performing the effect. The writer routes every non-read host or runtime dispatch under a fenced view through it (storage/writer.tsinvoke; GoinvokeDurable).ReferenceEffectHostserializes admission, checks the fence against the store's authority, records the key, and refuses. With no effect host, a non-read effect refuseseffect-host-unfencedbefore it runs.Idempotency. The key is the SHA-256 of the canonical
{run, operation, source, tool, requestDigest}: the acknowledged claim's identity, with no epoch. A takeover presents the same key; the host answersalready-admitted; the resumed writer still refuses an unacknowledged claim asHostOutcomeUnknown. Nothing is replayed.Truthful adapters.
admitUnattended/unattendedRunStore(GoAdmitUnattended/UnattendedRunStore) refuse by name:store-unsupported,store-lifetime(the memory kinds),effect-host-unfenced.Records (public surface). New schema
contract/schemas/run-lease.jsonversion 1 (root lease;$defsfence, operation, effectAdmission, refusal, capabilityRefusal, epoch, expiry), regenerated record shapes and Go contract types, readers in both languages, handwritten vectorscontract/vectors/lease/{lease,fence,admission,refusal,capability}.jsoncovering every member and enum value (held by a new Python probe), the largest epoch, overflow and non-integer refusals. Checkpoint and event formats are unchanged; a fenced run's stored records have the same shape as an unfenced run's.Conformance kit.
runStoreLeaseConformance(now => store)/CheckRunStoreLeaseConformancefor a host store kind (desk #609).Docs.
docs/design/storage.md"Run leases and fencing (#223)" for the store-kind implementer and the Runner's reader; the ADR-0025 amendment; README pointers.Tests (both languages)
storage/lease.test.ts)context/run_lease_test.go)New tests are at the store/context level; no adapter-level test was added (the
./adapter/race budget).Mutation rows (
origin: agent#223, all caught; Go twins applied by hand in a scratch copy, all caught)L1 a commit accepts a stale epoch · L2 no fence check at publication · L3 the view commits history unfenced · L4 the effect host skips the fence · L5 the writer skips the effect host · L6 renewal revives an expired lease · L7 a takeover reuses the epoch · L8 release forgets the epoch (reset after restart) · L9 the host admits an identity again (replay on takeover) · L10 the idempotency key carries the epoch · L11 a process-lifetime store admits unattended runs · L12 a chat update skips the fence (TS only: no Go ChatStore).
Parity
scripts/parity.shagainst the Desk atdesk_pin: every difference named. Onlynew_fileschanged (storage/lease.ts,storage/lease.test.ts); the touched storage files were already package-new files.Self-checks
(a) boundary paths untouched. (b) every run-lease member and enum value is carried (Python omission probe). (c) both readers pass every lease vector. (d) 12 rows caught, 11 Go twins caught. (e) refusals are typed errors carrying the versioned
refusal/capabilityRefusalrecord; malformed fences (empty owner, missing epoch, oversized owner) refuseinvalid. No run event was added: the ADR adds no members to current published formats. (f) no checkpoint member added. (g) the storage section and the ADR amendment were checked against the code.Not done, and why
Checks
Candidate:
e195f18ed673b8ce0259ab73efd546de029db552(branchrun-leases-223, no uncommitted diff). Command:jpdev check --changed origin/main(every step selected: package, Go and contract paths changed), with the parity step against a shallow Desk checkout atdesk_pin.Mutation rows run in a scratch copy:
node scripts/mutation-check.mjs --row "lease fence": 12 rows chosen, 12 caught. Go twins (scratch copy,go test -run Lease ./context/): L1–L11 caught. The first full run failed onlyreference(event-reference line numbers moved instorage/writer.ts); the second commit regenerates that page. CI owns the exact-head gates and the push-to-main mutation table.🤖 Generated with Claude Code