From 3ef98f6ab77cd6708a74f5fd8e5916b23511e4b3 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:23:06 -0400 Subject: [PATCH 1/3] 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 Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --- docs/design/storage.md | 25 ++- go/adapter/agent/invoke.go | 4 +- go/adapter/agent/invoke_test.go | 26 +++ go/context/run_lease.go | 182 ++++++++++++++----- go/context/run_lease_batch_test.go | 192 ++++++++++++++++++++ go/context/run_lease_test.go | 9 +- go/context/run_store_conformance.go | 39 ++-- go/context/run_writer.go | 6 +- packages/harness/scripts/mutation-rows.json | 112 +++++++++++- packages/harness/storage/conformance.ts | 12 +- packages/harness/storage/lease.test.ts | 67 ++++++- packages/harness/storage/lease.ts | 95 +++++++--- 12 files changed, 659 insertions(+), 110 deletions(-) create mode 100644 go/context/run_lease_batch_test.go diff --git a/docs/design/storage.md b/docs/design/storage.md index 455ba014..c977621b 100644 --- a/docs/design/storage.md +++ b/docs/design/storage.md @@ -340,13 +340,21 @@ replays it; its own host reconciles it. Fencing is not an exactly-once claim acr systems. **Certifying an effect host.** `effectHostConformance(authority => host)` (Go -`CheckEffectHostConformance`) hands the factory a lease authority the kit controls and requires the -host to admit a current fence once and to refuse, without performing, an identity it admitted -before (also under a takeover's fence), another owner, a stale or forged epoch, a fence for another -run than the operation's, a key that is not its operation's, an expired lease and a released one. -`certifyEffectHost(create, authority)` (Go `CertifyEffectHost`) runs it and returns the host built -over the real authority. Unattended admission accepts only that host or the package's -`ReferenceEffectHost`: a declared `effectFencing: 'enforced'` is not evidence. +`CheckEffectHostConformance`) hands the factory a lease authority the kit controls. The host must +refuse, without performing, while that authority is unavailable (unavailable ownership evidence +closes dispatch); admit a current fence once; and refuse, without performing, an identity it +admitted before (also under a takeover's fence), another owner, a stale or forged epoch, a fence +for another run than the operation's, a key that is not its operation's, an expired lease and a +released one. `certifyEffectHost(create, authority)` (Go `CertifyEffectHost`) calls the factory +once, over an authority that answers from the kit until the checks pass and from `authority` for +good after, and admits that exact instance: TypeScript returns a frozen wrapper that refuses once the +instance's `dispatch` is not the one the kit checked; Go returns a `*CertifiedEffectHost` whose +identity, not its value, is the evidence. A `ReferenceEffectHost` is evidence when the package made +it (its `dispatch` is sealed on the instance; a subclass or a type embedding it is not). Unattended +admission accepts only these, and only when the authority they enforce against is the run's own +store: a host over a no-op authority, over another store, or certified over another store is +refused. A declared `effectFencing: 'enforced'` is never evidence. Nothing keeps an admitted host +alive: there is no registry of hosts. **Truthful kinds and unattended admission.** `leaseLifetime` is `durable` only when rows survive the store's process. `MemoryRunStore` and `MemoryChatStore` implement the transitions within their @@ -364,5 +372,6 @@ renewal from the store clock, equal-time expiry, no revival, a strictly greater and after release, and atomic competing acquisitions. Every port the writer and the effect host rely on (`checkFence`, `renewLease`, `releaseLease`, `commitFenced` and, with history, `commitHistoryFenced` and `commitContextSnapshotFenced`) must refuse a stale epoch (including the -same owner's earlier epoch), another owner, a released lease and a mismatched resource. It certifies the port; the store's +same owner's earlier epoch), another owner, a released lease and a mismatched resource; the history +ports also refuse an expired fence, at equal time and after. It certifies the port; the store's durability across its own restarts and backups is the host's to prove. diff --git a/go/adapter/agent/invoke.go b/go/adapter/agent/invoke.go index 692dc163..fc8ca11b 100644 --- a/go/adapter/agent/invoke.go +++ b/go/adapter/agent/invoke.go @@ -790,8 +790,8 @@ func (r *runState) answer(ctx context.Context, call adapter.AgentToolCall, held r.held.Unlock() return textResult(turn.ProposalReceived, false), false, nil } - if !runcontext.ReadingTool(host) { - // A side effect: the checkpoint is acknowledged before it, or it is not made and the run + if !runcontext.DurableReadingTool(host) { + // A side effect (by its effect, not its answer's framing; #253): the checkpoint is acknowledged before it, or it is not made and the run // ends with the storage failure. if err := r.save(ctx); err != nil { if !failure.IsCancelled(err) { diff --git a/go/adapter/agent/invoke_test.go b/go/adapter/agent/invoke_test.go index 66d48cfd..850c8bf4 100644 --- a/go/adapter/agent/invoke_test.go +++ b/go/adapter/agent/invoke_test.go @@ -226,3 +226,29 @@ func TestPlanDriftStopsProviderInvocation(t *testing.T) { t.Fatalf("approvals %d paused %v callback %v effects %d", approvals, paused, afterPause, effects.Load()) } } + +// #253 (N2): a source act is a side effect by its effect class, whatever the framing of its answer: the +// run's checkpoint is acknowledged before the act runs, as for any other effect. +func TestASourceActIsSavedBeforeItRuns(t *testing.T) { + var saves, before, atExecute atomic.Int32 + note := runcontext.HostTool{Name: "write_note", Taxonomy: &runcontext.ToolTaxonomy{Operation: "act", Effect: "act"}, EffectClass: "act", Approval: "required", Source: writeNoteSource(), + Approve: func(runcontext.ApprovalRequest) string { return "allow" }, Description: "Writes a note.", InputSchema: json.RawMessage(`{"type":"object"}`), + Execute: func(context.Context, json.RawMessage) (runcontext.ToolResult, error) { + atExecute.Store(saves.Load()) + return runcontext.ResultOf([]runcontext.Content{runcontext.TextContent("written")}, nil), nil + }} + run, _ := oneTool(t, note, func(ctx context.Context, request adapter.AgentRequest, callbacks adapter.AgentCallbacks) error { + before.Store(saves.Load()) + if _, err := callbacks.Tool(ctx, adapter.AgentToolCall{ID: "c1", Name: aliasOf(request, "notes__write_note"), Arguments: json.RawMessage(`{}`)}); err != nil { + return err + } + return callbacks.Event(ctx, adapter.AgentEvent{Type: "message", ID: "m", Text: "Done.", Phase: "final"}) + }) + run.Context.Checkpoint = func(context.Context, contract.Checkpoint) error { saves.Add(1); return nil } + if err := agent.New().Run(context.Background(), run); err != nil { + t.Fatal(err) + } + if atExecute.Load() <= before.Load() { + t.Fatalf("the act ran before its checkpoint was acknowledged: %d saves before the call, %d when it ran", before.Load(), atExecute.Load()) + } +} diff --git a/go/context/run_lease.go b/go/context/run_lease.go index 0fe5a411..2edd760c 100644 --- a/go/context/run_lease.go +++ b/go/context/run_lease.go @@ -16,6 +16,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" "github.com/Judgment-Pack/judgment-pack-agent/go/contract" @@ -553,34 +554,51 @@ type ReferenceEffectHost struct { } func NewReferenceEffectHost(authority LeaseAuthority) *ReferenceEffectHost { - host := &ReferenceEffectHost{authority: authority, admitted: map[string]bool{}} - provenHosts.Store(host, true) - return host + return &ReferenceEffectHost{authority: authority, admitted: map[string]bool{}} } -// provenHosts are the effect hosts unattended admission accepts: the package's reference host and hosts -// certified by CertifyEffectHost. A declaration of EffectFencing alone is not evidence. -var provenHosts sync.Map +// CheckEffectHostConformance (#223 review, #253) is the effect-host conformance. The kit owns the lease +// authority it hands the factory and moves its clock. The host must refuse, without performing, while the +// authority is unavailable; admit a current fence once; and refuse, without performing, an identity it +// admitted before (also under a takeover's fence), another owner, a stale or forged epoch, a fence for +// another run than the operation's, a key that is not its operation's, an expired lease and a released one. +// The TypeScript twin is effectHostConformance. +func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) EffectHost) error { + kit := newKitAuthority() + return exerciseEffectHost(ctx, create(kit), kit) +} -func provenHost(host EffectHost) bool { - if host == nil || !reflect.TypeOf(host).Comparable() { - return false +// kitAuthority is the conformance kit's lease authority: its own table, clock and failure switch. +type kitAuthority struct { + mu sync.Mutex + at time.Time + failing bool + table *LeaseTable +} + +func newKitAuthority() *kitAuthority { + kit := &kitAuthority{at: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)} + kit.table = NewLeaseTable(func() time.Time { return kit.at }) + return kit +} +func (k *kitAuthority) CheckFence(_ context.Context, fence LeaseFence) error { + k.mu.Lock() + defer k.mu.Unlock() + if k.failing { + return errors.New("the lease authority is unavailable") } - _, ok := provenHosts.Load(host) - return ok + _, err := k.table.Check(fence) + return err +} +func (k *kitAuthority) do(run func()) { + k.mu.Lock() + defer k.mu.Unlock() + run() } -// CheckEffectHostConformance (#223 review) is the effect-host conformance. The kit owns the lease authority -// it hands the factory and moves its clock. The host must admit a current fence once and refuse, without -// performing, an identity it admitted before (also under a takeover's fence), another owner, a stale or -// forged epoch, a fence for another run than the operation's, a key that is not its operation's, an expired -// lease and a released one. The TypeScript twin is effectHostConformance. -func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) EffectHost) error { +// exerciseEffectHost runs the checks on the exact host instance given, against the kit's authority. +func exerciseEffectHost(ctx context.Context, host EffectHost, kit *kitAuthority) error { fail := func(property string) error { return &StorageConformanceFailure{Property: property} } - at := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) - table := NewLeaseTable(func() time.Time { return at }) - var mu sync.Mutex - host := create(tableAuthority{table: table, mu: &mu}) if host == nil || host.EffectFencing() != EffectFencingEnforced { return fail("host.declared") } @@ -597,18 +615,30 @@ func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) before := performed var refused *LeaseRefused err := host.Dispatch(ctx, held, perform) + if reason == "" { + return err != nil && performed == before + } return errors.As(err, &refused) && refused.Reason == reason && performed == before } - acquire := func(owner string) (LeaseFence, error) { - mu.Lock() - defer mu.Unlock() - lease, err := table.Acquire(LeaseAcquire{Resource: "host-run", Owner: owner, DurationMs: 1000}, "") - return FenceOf(lease), err + acquire := func(owner string) (fence LeaseFence, err error) { + kit.do(func() { + var lease RunLease + lease, err = kit.table.Acquire(LeaseAcquire{Resource: "host-run", Owner: owner, DurationMs: 1000}, "") + fence = FenceOf(lease) + }) + return fence, err } a, err := acquire("worker-a") if err != nil { return fail("host.operation") } + // Unavailable ownership evidence closes dispatch: an authority that errors is never a pass. + kit.do(func() { kit.failing = true }) + unavailable := refuses(admission(a, op("op-0", "host-run")), "") + kit.do(func() { kit.failing = false }) + if !unavailable { + return fail("host.authority-unavailable") + } if err := host.Dispatch(ctx, admission(a, op("op-1", "host-run")), perform); err != nil || performed != 1 { return fail("host.admit.current") } @@ -633,7 +663,7 @@ func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) if !refuses(mismatched, "invalid") { return fail("host.invalid") } - at = at.Add(time.Second) + kit.do(func() { kit.at = kit.at.Add(time.Second) }) if !refuses(admission(a, op("op-7", "host-run")), "expired") { return fail("host.expired") } @@ -647,9 +677,7 @@ func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) if !refuses(admission(b, op("op-1", "host-run")), "already-admitted") { return fail("host.takeover.already-admitted") } - mu.Lock() - _, err = table.Release(b) - mu.Unlock() + kit.do(func() { _, err = kit.table.Release(b) }) if err != nil || !refuses(admission(b, op("op-9", "host-run")), "released") { return fail("host.released") } @@ -659,31 +687,85 @@ func CheckEffectHostConformance(ctx context.Context, create func(LeaseAuthority) return nil } -type tableAuthority struct { - table *LeaseTable - mu *sync.Mutex +// boundAuthority answers from the kit until certification passes, then from the real authority for good. +type boundAuthority struct { + live atomic.Bool + kit *kitAuthority + authority LeaseAuthority } -func (a tableAuthority) CheckFence(_ context.Context, fence LeaseFence) error { - a.mu.Lock() - defer a.mu.Unlock() - _, err := a.table.Check(fence) - return err +func (b *boundAuthority) CheckFence(ctx context.Context, fence LeaseFence) error { + if b.live.Load() { + return b.authority.CheckFence(ctx, fence) + } + return b.kit.CheckFence(ctx, fence) } -// CertifyEffectHost certifies an effect host for unattended admission: the factory's hosts must pass the -// conformance, and the host it then builds over the real authority is the one admitted. -func CertifyEffectHost(ctx context.Context, create func(LeaseAuthority) EffectHost, authority LeaseAuthority) (EffectHost, error) { - if err := CheckEffectHostConformance(ctx, create); err != nil { - return nil, err +// CertifiedEffectHost is the evidence unattended admission accepts for a host other than the reference +// host: the exact instance the kit exercised and the authority it was certified for. Only CertifyEffectHost +// makes one; it holds no registry entry, so a released host is collected like any other value. +type CertifiedEffectHost struct { + self *CertifiedEffectHost // identity: a copy of the value is not the evidence + host EffectHost + authority LeaseAuthority +} + +func (c *CertifiedEffectHost) EffectFencing() string { return EffectFencingEnforced } +func (c *CertifiedEffectHost) Dispatch(ctx context.Context, admission EffectAdmission, perform func(context.Context) error) error { + if c == nil || c.self != c || c.host == nil { + return &LeaseCapabilityRefused{Reason: "effect-host-unfenced"} } - host := create(authority) - if host == nil || host.EffectFencing() != EffectFencingEnforced || !reflect.TypeOf(host).Comparable() { + return c.host.Dispatch(ctx, admission, perform) +} + +// CertifyEffectHost (#253) certifies an effect host over authority (for unattended admission, the run's own +// store). The factory is called once, over an authority the kit controls until the checks pass and that +// then answers from authority for good; the exact instance the kit exercised is the one admitted. +func CertifyEffectHost(ctx context.Context, create func(LeaseAuthority) EffectHost, authority LeaseAuthority) (*CertifiedEffectHost, error) { + if authority == nil { return nil, &LeaseCapabilityRefused{Reason: "effect-host-unfenced"} } - provenHosts.Store(host, true) - return host, nil + bound := &boundAuthority{kit: newKitAuthority(), authority: authority} + host := create(bound) + if err := exerciseEffectHost(ctx, host, bound.kit); err != nil { + return nil, err + } + bound.live.Store(true) + certified := &CertifiedEffectHost{host: host, authority: authority} + certified.self = certified + return certified, nil +} + +// sameAuthority is identity: the same pointer, or the same comparable value of the same type. +func sameAuthority(authority LeaseAuthority, store RunStore) bool { + if authority == nil || store == nil { + return false + } + a, s := reflect.ValueOf(authority), reflect.ValueOf(store) + if a.Type() != s.Type() { + return false + } + if a.Kind() == reflect.Pointer { + return a.Pointer() == s.Pointer() + } + return a.Type().Comparable() && any(authority) == any(store) } + +// provenAuthority is the authority a host is proven to enforce against, or nil. +func provenAuthority(host EffectHost) LeaseAuthority { + switch h := host.(type) { + case *ReferenceEffectHost: + if h != nil && h.admitted != nil { + return h.authority + } + case *CertifiedEffectHost: + if h != nil && h.self == h && h.host != nil { + return h.authority + } + } + return nil +} + func (h *ReferenceEffectHost) EffectFencing() string { return EffectFencingEnforced } // Refusals is every refusal this host gave, as its versioned record, in order. @@ -788,7 +870,8 @@ func FencedRunStore(store RunStore, fence LeaseFence, host EffectHost) (RunStore } // AdmitUnattended admits only a store whose leases outlive its process and an effect host proven to -// enforce the fence (the reference host, or one certified by CertifyEffectHost). Anything else refuses by name before a dispatch; in particular the memory and +// enforce the fence against that same store: a reference host built over it, or a host CertifyEffectHost +// certified over it. Anything else refuses by name before a dispatch; in particular the memory and // callback kinds never qualify. func AdmitUnattended(store RunStore, host EffectHost) error { lease, ok := store.(LeaseRunStore) @@ -798,7 +881,8 @@ func AdmitUnattended(store RunStore, host EffectHost) error { if lease.LeaseLifetime() != LeaseLifetimeDurable { return &LeaseCapabilityRefused{Reason: "store-lifetime"} } - if host == nil || host.EffectFencing() != EffectFencingEnforced || !provenHost(host) { + // #253: the host's evidence must be its own, and its authority the run's own store. + if host == nil || host.EffectFencing() != EffectFencingEnforced || !sameAuthority(provenAuthority(host), store) { return &LeaseCapabilityRefused{Reason: "effect-host-unfenced"} } return nil diff --git a/go/context/run_lease_batch_test.go b/go/context/run_lease_batch_test.go new file mode 100644 index 00000000..c13ec81b --- /dev/null +++ b/go/context/run_lease_batch_test.go @@ -0,0 +1,192 @@ +package runcontext + +import ( + "context" + "errors" + "runtime" + "strings" + "sync/atomic" + "testing" + "time" +) + +// #253, the Go twin of the #253 cases in packages/harness/storage/lease.test.ts: the kits test expiry +// on every fenced commit path, certification is bound to the exact instance it checked and to the run's +// own store, and nothing keeps a released host alive. + +// expiryBlindStore checks owner, epoch and release on one fenced commit path, but not expiry. +type expiryBlindStore struct { + *MemoryRunStore + path string +} + +func (s expiryBlindStore) current(ctx context.Context, fence LeaseFence) bool { + row, _ := s.ReadLease(ctx, fence.Resource) + return row != nil && row.State == "held" && row.Epoch == fence.Epoch && row.Owner == fence.Owner +} +func (s expiryBlindStore) CommitHistoryFenced(ctx context.Context, input RunHistoryCommit, fence LeaseFence) (RunHistoryReceipt, error) { + if s.path == "history" && s.current(ctx, fence) { + return s.CommitHistory(ctx, input) + } + return s.MemoryRunStore.CommitHistoryFenced(ctx, input, fence) +} +func (s expiryBlindStore) CommitContextSnapshotFenced(ctx context.Context, input ContextSnapshotUpdate, commitID string, fence LeaseFence) (ContextSnapshotHead, error) { + if s.path == "snapshot" && s.current(ctx, fence) { + return s.CommitContextSnapshot(ctx, input, commitID) + } + return s.MemoryRunStore.CommitContextSnapshotFenced(ctx, input, commitID, fence) +} + +func TestLeaseKitsTestExpiryOnEveryFencedCommitPath(t *testing.T) { + for _, path := range []string{"history", "snapshot"} { + err := CheckRunStoreLeaseConformance(context.Background(), func(now func() time.Time) LeaseRunStore { + return expiryBlindStore{MemoryRunStore: NewMemoryRunStore(MemoryRunStoreOptions{Now: now}), path: path} + }) + var failed *StorageConformanceFailure + if !errors.As(err, &failed) || failed.Property != "lease."+path+".expired.equal-time" { + t.Fatalf("the lease conformance passed a store whose %s path skips expiry: %v", path, err) + } + } +} + +type failOpenHost struct{ inner *ReferenceEffectHost } + +func (failOpenHost) EffectFencing() string { return EffectFencingEnforced } +func (h failOpenHost) Dispatch(ctx context.Context, admission EffectAdmission, perform func(context.Context) error) error { + err := h.inner.Dispatch(ctx, admission, perform) + var refused *LeaseRefused + if err != nil && !errors.As(err, &refused) { + return perform(ctx) + } + return err +} + +type valueHost struct{ name string } + +func (valueHost) EffectFencing() string { return EffectFencingEnforced } +func (valueHost) Dispatch(ctx context.Context, _ EffectAdmission, perform func(context.Context) error) error { + return perform(ctx) +} + +type noopAuthority struct{} + +func (noopAuthority) CheckFence(context.Context, LeaseFence) error { return nil } + +type embeddedReference struct{ *ReferenceEffectHost } + +func TestLeaseCertificationIsBoundToTheInstanceItChecked(t *testing.T) { + ctx := context.Background() + var failed *StorageConformanceFailure + durable := durableLeaseFixture{NewMemoryRunStore(MemoryRunStoreOptions{})} + other := durableLeaseFixture{NewMemoryRunStore(MemoryRunStoreOptions{})} + performs := func(a LeaseAuthority) EffectHost { return wrappedReferenceHost{NewReferenceEffectHost(a)} } + t.Run("authority-unavailable", func(t *testing.T) { + failOpen := func(a LeaseAuthority) EffectHost { return failOpenHost{NewReferenceEffectHost(a)} } + if err := CheckEffectHostConformance(ctx, failOpen); !errors.As(err, &failed) || failed.Property != "host.authority-unavailable" { + t.Fatalf("a host that performs while the authority is unavailable passed: %v", err) + } + if _, err := CertifyEffectHost(ctx, failOpen, durable); !errors.As(err, &failed) { + t.Fatal("certified a host that fails open") + } + }) + t.Run("factory-called-once", func(t *testing.T) { + products := []EffectHost{} + create := func(a LeaseAuthority) EffectHost { + host := performs(a) + if len(products) > 0 { + host = valueHost{"second"} + } + products = append(products, host) + return host + } + certified, err := CertifyEffectHost(ctx, create, durable) + if err != nil || len(products) != 1 { + t.Fatalf("certification called the factory %d times: %v", len(products), err) + } + if err := AdmitUnattended(durable, certified); err != nil { + t.Fatal(err) + } + if err := AdmitUnattended(durable, products[0]); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("the exercised instance itself was admitted without its evidence: %v", err) + } + // The certified host now answers from the real store: a fence it never issued is refused. + admission, _ := NewEffectAdmission(LeaseFence{Version: 1, Resource: "r", Owner: "o", Epoch: "1"}, EffectOperation{Run: "r", Operation: "op", Source: "host", Tool: "t", RequestDigest: "sha256:" + strings.Repeat("d", 64)}) + performed := false + if err := certified.Dispatch(ctx, admission, func(context.Context) error { performed = true; return nil }); leaseReason(err) != "no-lease" || performed { + t.Fatalf("the certified host did not answer from the store: %v", err) + } + }) + t.Run("identity-not-equality", func(t *testing.T) { + certified, err := CertifyEffectHost(ctx, func(LeaseAuthority) EffectHost { return valueHost{"equal"} }, durable) + if err == nil { + t.Fatal("certified a host that performs without checking") + } + certified, err = CertifyEffectHost(ctx, performs, durable) + if err != nil { + t.Fatal(err) + } + copied := *certified + if err := AdmitUnattended(durable, &copied); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a copy of the evidence was admitted: %v", err) + } + if err := AdmitUnattended(durable, valueHost{"equal"}); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("an equal value was admitted: %v", err) + } + }) + t.Run("authority-is-the-store", func(t *testing.T) { + if err := AdmitUnattended(durable, NewReferenceEffectHost(noopAuthority{})); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a reference host over a no-op authority was admitted: %v", err) + } + if err := AdmitUnattended(durable, NewReferenceEffectHost(other)); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a reference host over another store was admitted: %v", err) + } + foreign, err := CertifyEffectHost(ctx, performs, other) + if err != nil { + t.Fatal(err) + } + if err := AdmitUnattended(durable, foreign); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a host certified over another store was admitted: %v", err) + } + if err := AdmitUnattended(durable, embeddedReference{NewReferenceEffectHost(durable)}); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a type embedding the reference host was admitted: %v", err) + } + if err := AdmitUnattended(durable, &ReferenceEffectHost{authority: durable}); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a reference host not made by NewReferenceEffectHost was admitted: %v", err) + } + if err := AdmitUnattended(durable, NewReferenceEffectHost(durable)); err != nil { + t.Fatal(err) + } + }) +} + +func TestLeaseAdmittedHostsAreCollectable(t *testing.T) { + ctx := context.Background() + durable := durableLeaseFixture{NewMemoryRunStore(MemoryRunStoreOptions{})} + var collected atomic.Int32 + func() { + for range 50 { + reference := NewReferenceEffectHost(durable) + if err := AdmitUnattended(durable, reference); err != nil { + t.Fatal(err) + } + runtime.AddCleanup(reference, func(*atomic.Int32) { collected.Add(1) }, &collected) + } + for range 50 { + certified, err := CertifyEffectHost(ctx, func(a LeaseAuthority) EffectHost { return wrappedReferenceHost{NewReferenceEffectHost(a)} }, durable) + if err != nil { + t.Fatal(err) + } + if err := AdmitUnattended(durable, certified); err != nil { + t.Fatal(err) + } + runtime.AddCleanup(certified, func(*atomic.Int32) { collected.Add(1) }, &collected) + } + }() + for attempt := 0; attempt < 50 && collected.Load() < 100; attempt++ { + runtime.GC() + time.Sleep(10 * time.Millisecond) + } + if collected.Load() != 100 { + t.Fatalf("admitted hosts were kept alive: %d of 100 collected", collected.Load()) + } +} diff --git a/go/context/run_lease_test.go b/go/context/run_lease_test.go index 20142c32..8e308c2f 100644 --- a/go/context/run_lease_test.go +++ b/go/context/run_lease_test.go @@ -560,10 +560,15 @@ func TestLeaseUnattendedAdmissionRefusesByName(t *testing.T) { t.Fatalf("%s: %v", c.name, c.err) } } - if err := AdmitUnattended(durableLeaseFixture{memory}, host); err != nil { + durable := durableLeaseFixture{memory} + own := NewReferenceEffectHost(durable) + if err := AdmitUnattended(durable, own); err != nil { t.Fatal(err) } - if view, err := UnattendedRunStore(durableLeaseFixture{memory}, fence, host); err != nil || view.Durability() != RunStoreMemory { + if err := AdmitUnattended(durable, host); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a reference host over another authority was admitted: %v", err) + } + if view, err := UnattendedRunStore(durable, fence, own); err != nil || view.Durability() != RunStoreMemory { t.Fatalf("unattended view: %v", err) } if _, err := UnattendedRunStore(memory, fence, host); leaseReason(err) != "store-lifetime" { diff --git a/go/context/run_store_conformance.go b/go/context/run_store_conformance.go index 86993bc9..a53ed43a 100644 --- a/go/context/run_store_conformance.go +++ b/go/context/run_store_conformance.go @@ -229,6 +229,18 @@ func CheckRunStoreLeaseConformance(ctx context.Context, create func(now func() t if _, err = store.RenewLease(ctx, forged, 1000); !refuses(err, "other-owner") { return fail("lease.renew.other-owner") } + history, withHistory := store.(LeaseHistoryRunStore) + if plain, ok := store.(HistoryRunStore); ok && plain.HistoryVersion() == 1 && !withHistory { + return fail("lease.history.declared") + } + withHistory = withHistory && history.HistoryVersion() == 1 + historyCommit := func(commitID, expected string, fence LeaseFence, runID string) (RunHistoryReceipt, error) { + return history.CommitHistoryFenced(ctx, RunHistoryCommit{Run: RunCommit{RunID: runID, ExpectedRevision: expected, CommitID: commitID, Checkpoint: []byte(`{"commit":"` + commitID + `"}`)}, Observations: []HistoryObjectRef{}}, fence) + } + snapshot := func(fence LeaseFence) error { + _, err := history.CommitContextSnapshotFenced(ctx, ContextSnapshotUpdate{ChatID: "lease-chat", Manifest: HistoryObjectRef{Digest: "sha256:" + strings.Repeat("a", 64), Bytes: 2}}, "lease-snapshot", fence) + return err + } at = time.Date(2026, 1, 1, 0, 0, 1, 500_000_000, time.UTC) if err = store.CheckFence(ctx, a); !refuses(err, "expired") { return fail("lease.expiry.equal-time") @@ -239,6 +251,21 @@ func CheckRunStoreLeaseConformance(ctx context.Context, create func(now func() t if _, err = commit("lease-a-late", current.Revision, a); !refuses(err, "expired") { return fail("lease.commit.expired") } + // #253: the writer commits only through the history ports; each refuses an expired fence, at equal + // time and after. + if withHistory { + for _, when := range []string{"equal-time", "after"} { + if when == "after" { + at = at.Add(time.Millisecond) + } + if _, err = historyCommit("lease-a-late-history-"+when, current.Revision, a, run); !refuses(err, "expired") { + return fail("lease.history.expired." + when) + } + if err = snapshot(a); !refuses(err, "expired") { + return fail("lease.snapshot.expired." + when) + } + } + } if head, _ := store.Load(ctx, run); head == nil || head.Revision != current.Revision { return fail("lease.commit.expired-unapplied") } @@ -274,18 +301,6 @@ func CheckRunStoreLeaseConformance(ctx context.Context, create func(now func() t return fail("lease.release." + c.name) } } - history, withHistory := store.(LeaseHistoryRunStore) - if plain, ok := store.(HistoryRunStore); ok && plain.HistoryVersion() == 1 && !withHistory { - return fail("lease.history.declared") - } - withHistory = withHistory && history.HistoryVersion() == 1 - historyCommit := func(commitID, expected string, fence LeaseFence, runID string) (RunHistoryReceipt, error) { - return history.CommitHistoryFenced(ctx, RunHistoryCommit{Run: RunCommit{RunID: runID, ExpectedRevision: expected, CommitID: commitID, Checkpoint: []byte(`{"commit":"` + commitID + `"}`)}, Observations: []HistoryObjectRef{}}, fence) - } - snapshot := func(fence LeaseFence) error { - _, err := history.CommitContextSnapshotFenced(ctx, ContextSnapshotUpdate{ChatID: "lease-chat", Manifest: HistoryObjectRef{Digest: "sha256:" + strings.Repeat("a", 64), Bytes: 2}}, "lease-snapshot", fence) - return err - } if withHistory { mismatched := b mismatched.Resource = "lease-other" diff --git a/go/context/run_writer.go b/go/context/run_writer.go index 63b9929c..2f46e9b5 100644 --- a/go/context/run_writer.go +++ b/go/context/run_writer.go @@ -1833,7 +1833,11 @@ func (c *SessionContext) ExecuteDurableHost(ctx context.Context, tool HostTool, // source is a read only when its effect class is read, and an owner-declared act or propose effect is // never a read. Every other call is an effect: its outcome is recorded before dependent work, an unknown // outcome is never repeated, and under a fenced store the effect host admits it. -func durableReadingTool(tool *HostTool) bool { +func durableReadingTool(tool *HostTool) bool { return DurableReadingTool(tool) } + +// DurableReadingTool is durableReadingTool for the adapters: whether a host tool's call is a read for +// the durable record (#253), as distinct from ReadingTool, which frames its answer. +func DurableReadingTool(tool *HostTool) bool { if tool.Taxonomy != nil && (tool.Taxonomy.Effect == "act" || tool.Taxonomy.Effect == "propose") { return false } diff --git a/packages/harness/scripts/mutation-rows.json b/packages/harness/scripts/mutation-rows.json index d4df3c97..34205449 100644 --- a/packages/harness/scripts/mutation-rows.json +++ b/packages/harness/scripts/mutation-rows.json @@ -8030,14 +8030,14 @@ { "name": "lease fence L17 unattended admission trusts a host's declaration", "file": "storage/lease.ts", - "find": "||!provenHost(input.effectHost))throw", + "find": "||provenAuthority(input.effectHost)!==(input.store as object))throw", "replace": ")throw", "tests": [ "storage/lease.test.ts" ], "status": "caught", "origin": "agent#223", - "note": "The review's finding 3: a host that declares enforced and performs without checking is admitted. Caught at lease.test.ts > the kits refuse what does not enforce the fence (every non-enforcing host, and the uncertified wrapper). Go twin (by hand, dropping provenHost from AdmitUnattended): caught at TestLeaseKitsRefuseWhatDoesNotEnforceTheFence." + "note": "The review's finding 3: a host that declares enforced and performs without checking is admitted. Caught at lease.test.ts > the kits refuse what does not enforce the fence (every non-enforcing host, and the uncertified wrapper). Go twin (by hand, dropping provenHost from AdmitUnattended): caught at TestLeaseKitsRefuseWhatDoesNotEnforceTheFence. Needle re-anchored for #253: the evidence check is now provenAuthority against the run's store." }, { "name": "lease fence L18 an impossible expiry throws an untyped error", @@ -8050,6 +8050,114 @@ "status": "caught", "origin": "agent#223", "note": "The review's finding 5: month 13 or second 60 throws RangeError instead of LeaseRefused invalid. Caught at lease.test.ts > both readers accept every lease… (the impossible-expiry vectors). No Go twin: Go's reader refuses them by parsing." +}, +{ + "name": "lease batch B1 the kit does not test expiry on the fenced history commit", + "file": "storage/conformance.ts", + "find": " await refusesLease(`lease.history.expired.${when}`,()=>historyCommit(`lease-a-late-history-${when}`,current.revision,a),'expired')\n", + "replace": "", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R1. A store whose commitHistoryFenced checks owner, epoch and release but not expiry passes the kit. Caught at lease.test.ts > the lease conformance fails a store whose commitHistoryFenced skips the expiry check. Go twin (by hand, dropping the lease.history.expired check of CheckRunStoreLeaseConformance): caught at TestLeaseKitsTestExpiryOnEveryFencedCommitPath." +}, +{ + "name": "lease batch B2 the kit does not test expiry on the fenced snapshot commit", + "file": "storage/conformance.ts", + "find": " await refusesLease(`lease.snapshot.expired.${when}`,()=>snapshot(a),'expired')\n", + "replace": "", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R1. Caught at lease.test.ts > the lease conformance fails a store whose commitContextSnapshotFenced skips the expiry check. Go twin (by hand, dropping the lease.snapshot.expired check): caught at TestLeaseKitsTestExpiryOnEveryFencedCommitPath." +}, +{ + "name": "lease batch B3 the host kit never makes the authority fail", + "file": "storage/lease.ts", + "find": " await refuses('host.authority-unavailable',effectAdmission(a,op('op-0')),'any')\n", + "replace": "", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(a). A host that performs while its authority errors would be certified. Caught at lease.test.ts > a host that performs while the authority is unavailable fails the kit and certification. Go twin (by hand, dropping the host.authority-unavailable check of exerciseEffectHost): caught at TestLeaseCertificationIsBoundToTheInstanceItChecked/authority-unavailable." +}, +{ + "name": "lease batch B4 certification checks one product and admits another", + "file": "storage/lease.ts", + "find": "await exerciseEffectHost(host,kit)\n if(host.dispatch!==dispatch", + "replace": "await effectHostConformance(create)\n if(host.dispatch!==dispatch", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(b). Caught at lease.test.ts > the factory is called once: the instance the kit exercised is the one admitted. Go twin (by hand, CertifyEffectHost running the kit on a second product of the factory): caught at TestLeaseCertificationIsBoundToTheInstanceItChecked/factory-called-once." +}, +{ + "name": "lease batch B5 admission does not see a dispatch swapped after certification", + "file": "storage/lease.ts", + "find": "return record&&record.host.dispatch===record.dispatch&&record.host.effectFencing==='enforced'?record.authority:undefined", + "replace": "return record?record.authority:undefined", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(c), TypeScript only (a Go method cannot be replaced). Caught at lease.test.ts > a dispatch swapped after certification is refused." +}, +{ + "name": "lease batch B6 the certified wrapper calls a swapped dispatch", + "file": "storage/lease.ts", + "find": " if(host.dispatch!==dispatch)return Promise.reject(new LeaseCapabilityRefused('effect-host-unfenced'))\n", + "replace": "", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(c), TypeScript only. Caught at lease.test.ts > a dispatch swapped after certification is refused (the wrapper's dispatch must refuse, performing nothing)." +}, +{ + "name": "lease batch B7 the reference host's dispatch is not sealed", + "file": "storage/lease.ts", + "find": " Object.defineProperty(this,'dispatch',{value:ReferenceEffectHost.prototype.dispatch,writable:false,configurable:false,enumerable:false})\n", + "replace": "", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(c), TypeScript only. Caught at lease.test.ts > a dispatch swapped after certification is refused (the reference host's dispatch can be reassigned)." +}, +{ + "name": "lease batch B8 admission does not tie the host's authority to the run's store", + "file": "storage/lease.ts", + "find": "provenAuthority(input.effectHost)!==(input.store as object))throw", + "replace": "provenAuthority(input.effectHost)===undefined)throw", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2(e). A reference host over a no-op authority or another store, or a host certified over another store, would be admitted. Caught at lease.test.ts > a host over a no-op or another store's authority is refused. Go twin (by hand, provenAuthority(host) == nil in place of sameAuthority in AdmitUnattended): caught at TestLeaseCertificationIsBoundToTheInstanceItChecked/authority-is-the-store and TestLeaseUnattendedAdmissionRefusesByName." +}, +{ + "name": "lease batch B9 a subclass of the reference host carries its evidence", + "file": "storage/lease.ts", + "find": "if(Object.getPrototypeOf(this)===ReferenceEffectHost.prototype)references.set(this,authority)", + "replace": "references.set(this,authority)", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#253 R2. Caught at lease.test.ts > … evidence does not pass to another instance or a subclass. Go twin (by hand, accepting any type that embeds *ReferenceEffectHost): caught at TestLeaseCertificationIsBoundToTheInstanceItChecked/authority-is-the-store." } ] } diff --git a/packages/harness/storage/conformance.ts b/packages/harness/storage/conformance.ts index 66a34522..09451d06 100644 --- a/packages/harness/storage/conformance.ts +++ b/packages/harness/storage/conformance.ts @@ -118,9 +118,18 @@ async function leaseProperties(create:(now:()=>Date)=>LeaseRunStore|Promisestore.renewLease({...a,owner:'worker-b'},1000),'other-owner') at=Date.parse(renewed.expiry) + const history=store as Partial&LeaseRunStore + const historyCommit=(commitId:string,expectedRevision:string|null,fence:LeaseFence,runId=run)=>history.commitHistoryFenced!({run:{runId,expectedRevision,commitId,checkpoint:bytes(`{"commit":"${commitId}"}`),events:[]},observations:[]},fence) + const snapshot=(fence:LeaseFence)=>history.commitContextSnapshotFenced!({chatId:'lease-chat',expectedRevision:null,manifest:{digest:'sha256:'+'a'.repeat(64),bytes:2}},'lease-snapshot',fence) await refusesLease('lease.expiry.equal-time',()=>store.checkFence(a),'expired') await refusesLease('lease.renew.no-revival',()=>store.renewLease(a,1000),'expired') await refusesLease('lease.commit.expired',()=>commit('lease-a-late',current.revision,a),'expired') + // #253: the writer commits only through the history ports; each refuses an expired fence, at equal time and after. + if(history.historyVersion===1)for(const [when,step] of [['equal-time',0],['after',1]] as [string,number][]){ + at+=step + await refusesLease(`lease.history.expired.${when}`,()=>historyCommit(`lease-a-late-history-${when}`,current.revision,a),'expired') + await refusesLease(`lease.snapshot.expired.${when}`,()=>snapshot(a),'expired') + } check('lease.commit.expired-unapplied',(await store.load(run))?.revision===current.revision) const second=await store.acquireLease({resource:run,owner:'worker-b',expectedRevision:current.revision,durationMs:1000}) check('lease.takeover.greater-epoch',second.epoch==='2'&&second.owner==='worker-b') @@ -134,9 +143,6 @@ async function leaseProperties(create:(now:()=>Date)=>LeaseRunStore|Promisestore.renewLease(fence,1000),reason) await refusesLease(`lease.release.${name}`,()=>store.releaseLease(fence),reason) } - const history=store as Partial&LeaseRunStore - const historyCommit=(commitId:string,expectedRevision:string|null,fence:LeaseFence,runId=run)=>history.commitHistoryFenced!({run:{runId,expectedRevision,commitId,checkpoint:bytes(`{"commit":"${commitId}"}`),events:[]},observations:[]},fence) - const snapshot=(fence:LeaseFence)=>history.commitContextSnapshotFenced!({chatId:'lease-chat',expectedRevision:null,manifest:{digest:'sha256:'+'a'.repeat(64),bytes:2}},'lease-snapshot',fence) if(history.historyVersion===1){ check('lease.history.declared',typeof history.commitHistoryFenced==='function'&&typeof history.commitContextSnapshotFenced==='function') for(const [name,fence,reason] of [...stale.slice(0,2),['resource',{...b,resource:'lease-other'},'resource-mismatch']] as [string,LeaseFence,LeaseRefusalReason][]){ diff --git a/packages/harness/storage/lease.test.ts b/packages/harness/storage/lease.test.ts index a3e25b72..5844f31d 100644 --- a/packages/harness/storage/lease.test.ts +++ b/packages/harness/storage/lease.test.ts @@ -180,13 +180,14 @@ describe('truthful adapters and unattended admission (#223)',()=>{ expect(()=>admitUnattended({store:memory,effectHost:host})).toThrow(capability('store-lifetime')) expect(()=>admitUnattended({store:callback,effectHost:host})).toThrow(capability('store-unsupported')) expect(()=>admitUnattended({store:plain,effectHost:host})).toThrow(capability('store-unsupported')) - const durable=new DurableLeaseFixture() + const durable=new DurableLeaseFixture(),own=new ReferenceEffectHost(durable) expect(()=>admitUnattended({store:durable})).toThrow(capability('effect-host-unfenced')) expect(()=>admitUnattended({store:durable,effectHost:unfenced})).toThrow(capability('effect-host-unfenced')) - expect(()=>admitUnattended({store:durable,effectHost:host,chatStore:new MemoryChatStore()})).toThrow(capability('store-lifetime')) - expect(()=>admitUnattended({store:durable,effectHost:host})).not.toThrow() + expect(()=>admitUnattended({store:durable,effectHost:own,chatStore:new MemoryChatStore()})).toThrow(capability('store-lifetime')) + expect(()=>admitUnattended({store:durable,effectHost:own})).not.toThrow() + expect(()=>admitUnattended({store:durable,effectHost:host})).toThrow(capability('effect-host-unfenced')) const fence:LeaseFence={version:1,resource:'r',owner:'o',epoch:'1'} - expect(unattendedRunStore({store:durable,fence,effectHost:host}).durability).toBe('memory') + expect(unattendedRunStore({store:durable,fence,effectHost:own}).durability).toBe('memory') expect(()=>unattendedRunStore({store:memory,fence,effectHost:host})).toThrow(capability('store-lifetime')) expect(()=>fencedRunStore(callback,fence)).toThrow(capability('store-unsupported')) expect(()=>fencedRunStore(memory,fence,unfenced)).toThrow(capability('effect-host-unfenced')) @@ -348,3 +349,61 @@ describe('pinned safeguards (#223 review 4)',()=>{ expect(perform).not.toHaveBeenCalled();expect(host.refusals).toEqual([{version:1,reason:'resource-mismatch'}]) }) }) + +describe('the kits test expiry on every fenced commit path (#253 R1)',()=>{ + const skipsExpiry=(store:MemoryRunStore,fence:LeaseFence)=>store.readLease(fence.resource).then(row=>!!row&&row.state==='held'&&row.epoch===fence.epoch&&row.owner===fence.owner) + const breaks:Recordvoid>={ + 'commitHistoryFenced skips the expiry check':store=>{const fenced=store.commitHistoryFenced.bind(store);store.commitHistoryFenced=async(input,fence,signal)=>await skipsExpiry(store,fence)?store.commitHistory(input,signal):fenced(input,fence,signal)}, + 'commitContextSnapshotFenced skips the expiry check':store=>{const fenced=store.commitContextSnapshotFenced.bind(store);store.commitContextSnapshotFenced=async(input,commitId,fence,signal)=>await skipsExpiry(store,fence)?store.commitContextSnapshot(input,commitId,signal):fenced(input,commitId,fence,signal)}, + } + for(const [name,broken] of Object.entries(breaks))it(`the lease conformance fails a store whose ${name}`,async()=>{ + await expect(runStoreLeaseConformance(now=>{const store=new MemoryRunStore({now});broken(store);return store as LeaseRunStore})).rejects.toMatchObject({property:expect.stringMatching(/^lease\.(history|snapshot)\.expired\.equal-time$/)}) + }) +}) + +describe('certification is bound to the instance it checked (#253 R2)',()=>{ + const performs=(authority:import('./lease').LeaseAuthority):EffectHost=>{const inner=new ReferenceEffectHost(authority);return {effectFencing:'enforced',dispatch:(admission,perform,signal)=>inner.dispatch(admission,perform,signal)}} + it('a host that performs while the authority is unavailable fails the kit and certification',async()=>{ + const failOpen=(authority:import('./lease').LeaseAuthority):EffectHost=>{const inner=new ReferenceEffectHost(authority);return {effectFencing:'enforced',dispatch:async(admission,perform,signal)=>{try{return await inner.dispatch(admission,perform,signal)}catch(error){if(error instanceof LeaseRefused)throw error;return perform(signal)}}}} + await expect(effectHostConformance(failOpen)).rejects.toMatchObject({property:'host.authority-unavailable'}) + await expect(certifyEffectHost(failOpen,new DurableLeaseStoreFixture())).rejects.toMatchObject({property:'host.authority-unavailable'}) + }) + it('the factory is called once: the instance the kit exercised is the one admitted',async()=>{ + const store=new DurableLeaseStoreFixture(),products:EffectHost[]=[] + const create=(authority:import('./lease').LeaseAuthority):EffectHost=>{const host=products.length?{effectFencing:'enforced' as const,dispatch:(_admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise)=>perform()}:performs(authority);products.push(host);return host} + const certified=await certifyEffectHost(create,store) + expect(products).toHaveLength(1) + expect(()=>admitUnattended({store,effectHost:certified})).not.toThrow() + expect(()=>admitUnattended({store,effectHost:products[0]!})).toThrow(capability('effect-host-unfenced')) + // The certified host now answers from the real store: a fence the store never issued is refused, nothing performed. + const perform=vi.fn(async()=>'performed') + await expect(certified.dispatch(effectAdmission({version:1,resource:'r',owner:'o',epoch:'1'},{run:'r',operation:'op',source:'host',tool:'t',requestDigest:'sha256:'+'d'.repeat(64)}),perform)).rejects.toEqual(refusal('no-lease')) + expect(perform).not.toHaveBeenCalled() + }) + it('a dispatch swapped after certification is refused',async()=>{ + const store=new DurableLeaseStoreFixture(),inner:EffectHost[]=[] + const certified=await certifyEffectHost(authority=>{const host=performs(authority);inner.push(host);return host},store) + ;(inner[0] as {dispatch:EffectHost['dispatch']}).dispatch=(_admission,perform)=>perform() + expect(()=>admitUnattended({store,effectHost:certified})).toThrow(capability('effect-host-unfenced')) + const perform=vi.fn(async()=>'performed') + await expect(certified.dispatch(effectAdmission({version:1,resource:'r',owner:'o',epoch:'1'},{run:'r',operation:'op',source:'host',tool:'t',requestDigest:'sha256:'+'d'.repeat(64)}),perform)).rejects.toEqual(capability('effect-host-unfenced')) + expect(perform).not.toHaveBeenCalled() + expect(()=>{(certified as {dispatch:unknown}).dispatch=()=>undefined}).toThrow(TypeError) + const reference=new ReferenceEffectHost(store) + expect(()=>{(reference as {dispatch:unknown}).dispatch=()=>undefined}).toThrow(TypeError) + expect(()=>Object.defineProperty(reference,'dispatch',{value:()=>undefined})).toThrow(TypeError) + expect(()=>{(reference as {effectFencing:unknown}).effectFencing='enforced'}).toThrow(TypeError) + expect(()=>admitUnattended({store,effectHost:reference})).not.toThrow() + }) + it('a host over a no-op or another store\'s authority is refused, and evidence does not pass to another instance or a subclass',async()=>{ + const store=new DurableLeaseStoreFixture(),other=new DurableLeaseStoreFixture() + expect(()=>admitUnattended({store,effectHost:new ReferenceEffectHost({checkFence:async()=>{}})})).toThrow(capability('effect-host-unfenced')) + expect(()=>admitUnattended({store,effectHost:new ReferenceEffectHost(other)})).toThrow(capability('effect-host-unfenced')) + const foreign=await certifyEffectHost(performs,other) + expect(()=>admitUnattended({store,effectHost:foreign})).toThrow(capability('effect-host-unfenced')) + class Sub extends ReferenceEffectHost {} + expect(()=>admitUnattended({store,effectHost:new Sub(store)})).toThrow(capability('effect-host-unfenced')) + const certified=await certifyEffectHost(performs,store) + expect(()=>admitUnattended({store,effectHost:{effectFencing:'enforced',dispatch:certified.dispatch}})).toThrow(capability('effect-host-unfenced')) + }) +}) diff --git a/packages/harness/storage/lease.ts b/packages/harness/storage/lease.ts index 433e3310..c33daa92 100644 --- a/packages/harness/storage/lease.ts +++ b/packages/harness/storage/lease.ts @@ -164,7 +164,12 @@ export class ReferenceEffectHost implements EffectHost { private tail:Promise=Promise.resolve() /** Every refusal this host gave, as its versioned record, in order. */ readonly refusals:{version:1;reason:LeaseRefusalReason}[]=[] - constructor(private readonly authority:LeaseAuthority){if(Object.getPrototypeOf(this)===ReferenceEffectHost.prototype)proven.add(this)} + constructor(private readonly authority:LeaseAuthority){ + // #253: what admission relies on is sealed on the instance: its dispatch and declaration cannot be swapped afterwards. + Object.defineProperty(this,'dispatch',{value:ReferenceEffectHost.prototype.dispatch,writable:false,configurable:false,enumerable:false}) + Object.defineProperty(this,'effectFencing',{value:'enforced',writable:false,configurable:false,enumerable:true}) + if(Object.getPrototypeOf(this)===ReferenceEffectHost.prototype)references.set(this,authority) + } dispatch(admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise,signal?:AbortSignal):Promise{ const admitted=this.tail.then(async()=>{ try{ @@ -181,52 +186,87 @@ export class ReferenceEffectHost implements EffectHost { } } -/** Effect hosts unattended admission accepts: the package's own reference host, and hosts certified by the conformance - * below. A declaration of effectFencing alone is not evidence. */ -const proven=new WeakSet() -const provenHost=(host:EffectHost)=>proven.has(host)&&(!(host instanceof ReferenceEffectHost)||host.dispatch===ReferenceEffectHost.prototype.dispatch) -/** #223 review: the effect-host conformance. The kit owns the lease authority it hands the factory and moves its clock; - * the host must admit a current fence once, and refuse, without performing, an identity it admitted before (also under a - * takeover's fence), another owner, a stale or forged epoch, a fence for another run than the operation's, a key that is - * not its operation's, an expired lease and a released one. */ -export async function effectHostConformance(create:(authority:LeaseAuthority)=>EffectHost|Promise):Promise{ +/** #253: the evidence unattended admission accepts, bound to exact instances. A reference host is admitted with the + * authority it was built over; a certified host is the package's sealed wrapper of the exact instance the kit exercised, + * with the dispatch the kit checked and the authority it was certified for. A declaration of effectFencing alone is not + * evidence, and evidence on one instance is not evidence on another or on a subclass. */ +const references=new WeakMap() +const certified=new WeakMap() +function provenAuthority(host:EffectHost):LeaseAuthority|undefined{ + if(host instanceof ReferenceEffectHost){const authority=references.get(host);return authority&&host.dispatch===ReferenceEffectHost.prototype.dispatch&&host.effectFencing==='enforced'?authority:undefined} + const record=certified.get(host) + return record&&record.host.dispatch===record.dispatch&&record.host.effectFencing==='enforced'?record.authority:undefined +} +interface KitAuthority extends LeaseAuthority {table:LeaseTable;failing:boolean;at:number} +function kitAuthority():KitAuthority{ + const kit:KitAuthority={at:Date.parse('2026-01-01T00:00:00.000Z'),failing:false,table:undefined as never,checkFence:async fence=>{if(kit.failing)throw new Error('The lease authority is unavailable.');kit.table.check(fence)}} + kit.table=new LeaseTable(()=>new Date(kit.at));return kit +} +/** The checks, on the exact host instance given, against the kit's authority. */ +async function exerciseEffectHost(host:EffectHost,kit:KitAuthority):Promise{ const fail=(property:string):never=>{throw new StorageConformanceFailure(property)} try{ - let at=Date.parse('2026-01-01T00:00:00.000Z') - const table=new LeaseTable(()=>new Date(at)),host=await create({checkFence:async fence=>{table.check(fence)}}) if(host.effectFencing!=='enforced')fail('host.declared') const op=(operation:string,run='host-run'):EffectOperation=>({run,operation,source:'host',tool:'effect',requestDigest:'sha256:'+'b'.repeat(64)}) let performed=0 const perform=async()=>{performed++;return 'performed'} - const refuses=async(property:string,admission:EffectAdmission,reason:LeaseRefusalReason)=>{ + const refuses=async(property:string,admission:EffectAdmission,reason:LeaseRefusalReason|'any')=>{ const before=performed - try{await host.dispatch(admission,perform)}catch(error){if(!(error instanceof LeaseRefused&&error.reason===reason)||performed!==before)fail(property);return} + try{await host.dispatch(admission,perform)}catch(error){if(reason!=='any'&&!(error instanceof LeaseRefused&&error.reason===reason)||performed!==before)fail(property);return} fail(property) } - const a=fenceOf(table.acquire({resource:'host-run',owner:'worker-a',expectedRevision:null,durationMs:1000},null)) + const a=fenceOf(kit.table.acquire({resource:'host-run',owner:'worker-a',expectedRevision:null,durationMs:1000},null)) + // Unavailable ownership evidence closes dispatch: an authority that errors is never a pass. + kit.failing=true + await refuses('host.authority-unavailable',effectAdmission(a,op('op-0')),'any') + kit.failing=false if(await host.dispatch(effectAdmission(a,op('op-1')),perform)!=='performed'||performed!==1)fail('host.admit.current') await refuses('host.already-admitted',effectAdmission(a,op('op-1')),'already-admitted') await refuses('host.other-owner',effectAdmission({...a,owner:'worker-x'},op('op-2')),'other-owner') await refuses('host.forged-epoch',effectAdmission({...a,epoch:'2'},op('op-3')),'stale-epoch') await refuses('host.resource-mismatch',effectAdmission(a,op('op-4','other-run')),'resource-mismatch') await refuses('host.invalid',{...effectAdmission(a,op('op-5')),idempotencyKey:effectAdmission(a,op('op-6')).idempotencyKey},'invalid') - at+=1000 + kit.at+=1000 await refuses('host.expired',effectAdmission(a,op('op-7')),'expired') - const b=fenceOf(table.acquire({resource:'host-run',owner:'worker-b',expectedRevision:null,durationMs:1000},null)) + const b=fenceOf(kit.table.acquire({resource:'host-run',owner:'worker-b',expectedRevision:null,durationMs:1000},null)) await refuses('host.stale-epoch',effectAdmission(a,op('op-8')),'stale-epoch') await refuses('host.takeover.already-admitted',effectAdmission(b,op('op-1')),'already-admitted') - table.release(b) + kit.table.release(b) await refuses('host.released',effectAdmission(b,op('op-9')),'released') if(performed!==1)fail('host.performed-once') }catch(error){if(error instanceof StorageConformanceFailure)throw error;throw new StorageConformanceFailure('host.operation')} } -/** Certify an effect host for unattended admission: the factory's hosts must pass the conformance, and the host it then - * builds over the real authority is the one admitted. */ -export async function certifyEffectHost(create:(authority:LeaseAuthority)=>T|Promise,authority:LeaseAuthority):Promise{ - await effectHostConformance(create) - const host=await create(authority) - if(host.effectFencing!=='enforced')throw new LeaseCapabilityRefused('effect-host-unfenced') - proven.add(host);return host +/** #223 review, #253: the effect-host conformance. The kit owns the lease authority it hands the factory and moves its + * clock; the host must refuse, without performing, while the authority is unavailable; admit a current fence once; and + * refuse, without performing, an identity it admitted before (also under a takeover's fence), another owner, a stale or + * forged epoch, a fence for another run than the operation's, a key that is not its operation's, an expired lease and a + * released one. */ +export async function effectHostConformance(create:(authority:LeaseAuthority)=>EffectHost|Promise):Promise{ + const kit=kitAuthority() + let host:EffectHost + try{host=await create(kit)}catch{throw new StorageConformanceFailure('host.operation')} + await exerciseEffectHost(host,kit) +} +/** Certify an effect host for unattended admission over `authority` (for unattended admission, the run's own store). + * The factory is called once, over an authority the kit controls until the checks pass and that then answers from + * `authority` for good; the exact instance the kit exercised is the one admitted, through a sealed wrapper that refuses + * once the instance's dispatch is not the one the kit checked. */ +export async function certifyEffectHost(create:(authority:LeaseAuthority)=>EffectHost|Promise,authority:LeaseAuthority):Promise{ + const kit=kitAuthority() + let live=false + const bound:LeaseAuthority={checkFence:(fence,signal)=>live?authority.checkFence(fence,signal):kit.checkFence(fence,signal)} + let host:EffectHost + try{host=await create(bound)}catch{throw new StorageConformanceFailure('host.operation')} + const dispatch=host.dispatch + await exerciseEffectHost(host,kit) + if(host.dispatch!==dispatch||host.effectFencing!=='enforced')throw new LeaseCapabilityRefused('effect-host-unfenced') + live=true + const wrapper:EffectHost=Object.freeze({effectFencing:'enforced' as const,dispatch(admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise,signal?:AbortSignal):Promise{ + if(host.dispatch!==dispatch)return Promise.reject(new LeaseCapabilityRefused('effect-host-unfenced')) + return dispatch.call(host,admission,perform,signal) as Promise + }}) + certified.set(wrapper,{host,dispatch,authority}) + return wrapper } const leased=new WeakMap() const bind=(store:object,name:string)=>{const method=(store as Record)[name];return typeof method==='function'?method.bind(store):undefined} @@ -253,7 +293,7 @@ export function fencedChatStore(store:ChatStore,fence:LeaseFence):ChatStore{ return Object.freeze({consistency:store.consistency,get:store.get.bind(store),list:store.list.bind(store),update:(id:string,patch:ChatPatch,expectedRevision:StorageRevision,signal?:AbortSignal)=>lease.updateFenced!(id,patch,expectedRevision,held,signal)}) as ChatStore } /** Unattended admission: only a store whose leases outlive its process and an effect host proven to enforce the fence - * (the reference host, or one certified by certifyEffectHost). + * against that same store: a reference host built over it, or a host certifyEffectHost certified over it. * Anything else refuses by name before a dispatch; in particular the memory and callback kinds never qualify. */ export function admitUnattended(input:{store:RunStore;effectHost?:EffectHost;chatStore?:ChatStore}):void{ for(const store of [input.store,input.chatStore]){ @@ -262,7 +302,8 @@ export function admitUnattended(input:{store:RunStore;effectHost?:EffectHost;cha if(lease.leaseVersion!==1||(store===input.store?typeof lease.commitFenced!=='function'||(store as RunStore).durability==='single-writer-callback':typeof lease.updateFenced!=='function'||(store as ChatStore).consistency==='single-writer-legacy'))throw new LeaseCapabilityRefused('store-unsupported') if(lease.leaseLifetime!=='durable')throw new LeaseCapabilityRefused('store-lifetime') } - if(!input.effectHost||input.effectHost.effectFencing!=='enforced'||!provenHost(input.effectHost))throw new LeaseCapabilityRefused('effect-host-unfenced') + // #253: the host's evidence must be its own, and its authority the run's own store. + if(!input.effectHost||input.effectHost.effectFencing!=='enforced'||provenAuthority(input.effectHost)!==(input.store as object))throw new LeaseCapabilityRefused('effect-host-unfenced') } /** admitUnattended, then the fenced view the unattended run uses as its RunStore. */ export function unattendedRunStore(input:{store:RunStore;fence:LeaseFence;effectHost:EffectHost}):RunStore{admitUnattended(input);return fencedRunStore(input.store,input.fence,input.effectHost)} From 75f884c67905296dfb350ca36ef1864ce34186e3 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:26:51 -0400 Subject: [PATCH 2/3] Regenerate the reference pages for the moved lines (#253) Co-Authored-By: Claude Opus 5.5 Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --- docs/reference/events.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/reference/events.md b/docs/reference/events.md index 68087ff7..f663d431 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -68,16 +68,16 @@ Consumer entries identify explicit kind reads, not delivery guarantees. Generic | Kind | TypeScript emitters | Go emitters | TypeScript consumers | Go consumers | | --- | --- | --- | --- | --- | -| `execution_checkpoint` | [`packages/harness/context/managed.ts:126`](../../packages/harness/context/managed.ts#L126), [`packages/harness/context/managed.ts:141`](../../packages/harness/context/managed.ts#L141), [`packages/harness/context/managed.ts:255`](../../packages/harness/context/managed.ts#L255), [`packages/harness/context/managed.ts:262`](../../packages/harness/context/managed.ts#L262), [`packages/harness/delegation.ts:498`](../../packages/harness/delegation.ts#L498), [`packages/harness/storage/writer.ts:248`](../../packages/harness/storage/writer.ts#L248), [`packages/harness/storage/writer.ts:277`](../../packages/harness/storage/writer.ts#L277), [`packages/harness/storage/writer.ts:465`](../../packages/harness/storage/writer.ts#L465), [`packages/harness/storage/writer.ts:748`](../../packages/harness/storage/writer.ts#L748), [`packages/harness/storage/writer.ts:777`](../../packages/harness/storage/writer.ts#L777) | [`go/adapter/delegate/agent.go:371`](../../go/adapter/delegate/agent.go#L371), [`go/adapter/delegate/calls.go:118`](../../go/adapter/delegate/calls.go#L118), [`go/context/managed.go:138`](../../go/context/managed.go#L138), [`go/context/managed.go:240`](../../go/context/managed.go#L240), [`go/context/managed.go:426`](../../go/context/managed.go#L426), [`go/context/native_plan_pause.go:128`](../../go/context/native_plan_pause.go#L128), [`go/context/native_plan_pause.go:159`](../../go/context/native_plan_pause.go#L159), [`go/context/run_writer.go:2492`](../../go/context/run_writer.go#L2492), [`go/context/run_writer.go:2604`](../../go/context/run_writer.go#L2604), [`go/context/run_writer.go:464`](../../go/context/run_writer.go#L464), [`go/context/session.go:2019`](../../go/context/session.go#L2019) | [`packages/harness/corpus.ts:167`](../../packages/harness/corpus.ts#L167), [`packages/harness/delegation.ts:1033`](../../packages/harness/delegation.ts#L1033), [`packages/harness/delegation.ts:1034`](../../packages/harness/delegation.ts#L1034), [`packages/harness/delegation.ts:721`](../../packages/harness/delegation.ts#L721), [`packages/harness/history/capture.ts:74`](../../packages/harness/history/capture.ts#L74), [`packages/harness/history/view.ts:93`](../../packages/harness/history/view.ts#L93), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/runReport.ts:79`](../../packages/harness/runReport.ts#L79), [`packages/harness/runReport.ts:84`](../../packages/harness/runReport.ts#L84), [`packages/harness/storage/nativePlanPause.ts:64`](../../packages/harness/storage/nativePlanPause.ts#L64), [`packages/harness/storage/writer.ts:530`](../../packages/harness/storage/writer.ts#L530), [`packages/harness/storage/writer.ts:532`](../../packages/harness/storage/writer.ts#L532), [`packages/harness/storage/writer.ts:666`](../../packages/harness/storage/writer.ts#L666) | [`go/adapter/delegate/batch.go:314`](../../go/adapter/delegate/batch.go#L314), [`go/adapter/delegate/batch.go:318`](../../go/adapter/delegate/batch.go#L318), [`go/adapter/delegate/batch.go:319`](../../go/adapter/delegate/batch.go#L319), [`go/adapter/delegate/coordinator.go:545`](../../go/adapter/delegate/coordinator.go#L545), [`go/adapter/delegate/coordinator.go:546`](../../go/adapter/delegate/coordinator.go#L546), [`go/context/history_view.go:313`](../../go/context/history_view.go#L313), [`go/context/native_plan_pause.go:434`](../../go/context/native_plan_pause.go#L434), [`go/context/run_report.go:197`](../../go/context/run_report.go#L197), [`go/context/run_report.go:198`](../../go/context/run_report.go#L198), [`go/context/run_writer.go:1077`](../../go/context/run_writer.go#L1077), [`go/context/run_writer.go:1078`](../../go/context/run_writer.go#L1078), [`go/context/run_writer.go:711`](../../go/context/run_writer.go#L711), [`go/context/run_writer.go:712`](../../go/context/run_writer.go#L712), [`go/context/run_writer.go:726`](../../go/context/run_writer.go#L726), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `execution_checkpoint` | [`packages/harness/context/managed.ts:126`](../../packages/harness/context/managed.ts#L126), [`packages/harness/context/managed.ts:141`](../../packages/harness/context/managed.ts#L141), [`packages/harness/context/managed.ts:255`](../../packages/harness/context/managed.ts#L255), [`packages/harness/context/managed.ts:262`](../../packages/harness/context/managed.ts#L262), [`packages/harness/delegation.ts:498`](../../packages/harness/delegation.ts#L498), [`packages/harness/storage/writer.ts:248`](../../packages/harness/storage/writer.ts#L248), [`packages/harness/storage/writer.ts:277`](../../packages/harness/storage/writer.ts#L277), [`packages/harness/storage/writer.ts:465`](../../packages/harness/storage/writer.ts#L465), [`packages/harness/storage/writer.ts:748`](../../packages/harness/storage/writer.ts#L748), [`packages/harness/storage/writer.ts:777`](../../packages/harness/storage/writer.ts#L777) | [`go/adapter/delegate/agent.go:371`](../../go/adapter/delegate/agent.go#L371), [`go/adapter/delegate/calls.go:118`](../../go/adapter/delegate/calls.go#L118), [`go/context/managed.go:138`](../../go/context/managed.go#L138), [`go/context/managed.go:240`](../../go/context/managed.go#L240), [`go/context/managed.go:426`](../../go/context/managed.go#L426), [`go/context/native_plan_pause.go:128`](../../go/context/native_plan_pause.go#L128), [`go/context/native_plan_pause.go:159`](../../go/context/native_plan_pause.go#L159), [`go/context/run_writer.go:2496`](../../go/context/run_writer.go#L2496), [`go/context/run_writer.go:2608`](../../go/context/run_writer.go#L2608), [`go/context/run_writer.go:464`](../../go/context/run_writer.go#L464), [`go/context/session.go:2019`](../../go/context/session.go#L2019) | [`packages/harness/corpus.ts:167`](../../packages/harness/corpus.ts#L167), [`packages/harness/delegation.ts:1033`](../../packages/harness/delegation.ts#L1033), [`packages/harness/delegation.ts:1034`](../../packages/harness/delegation.ts#L1034), [`packages/harness/delegation.ts:721`](../../packages/harness/delegation.ts#L721), [`packages/harness/history/capture.ts:74`](../../packages/harness/history/capture.ts#L74), [`packages/harness/history/view.ts:93`](../../packages/harness/history/view.ts#L93), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/runReport.ts:79`](../../packages/harness/runReport.ts#L79), [`packages/harness/runReport.ts:84`](../../packages/harness/runReport.ts#L84), [`packages/harness/storage/nativePlanPause.ts:64`](../../packages/harness/storage/nativePlanPause.ts#L64), [`packages/harness/storage/writer.ts:530`](../../packages/harness/storage/writer.ts#L530), [`packages/harness/storage/writer.ts:532`](../../packages/harness/storage/writer.ts#L532), [`packages/harness/storage/writer.ts:666`](../../packages/harness/storage/writer.ts#L666) | [`go/adapter/delegate/batch.go:314`](../../go/adapter/delegate/batch.go#L314), [`go/adapter/delegate/batch.go:318`](../../go/adapter/delegate/batch.go#L318), [`go/adapter/delegate/batch.go:319`](../../go/adapter/delegate/batch.go#L319), [`go/adapter/delegate/coordinator.go:545`](../../go/adapter/delegate/coordinator.go#L545), [`go/adapter/delegate/coordinator.go:546`](../../go/adapter/delegate/coordinator.go#L546), [`go/context/history_view.go:313`](../../go/context/history_view.go#L313), [`go/context/native_plan_pause.go:434`](../../go/context/native_plan_pause.go#L434), [`go/context/run_report.go:197`](../../go/context/run_report.go#L197), [`go/context/run_report.go:198`](../../go/context/run_report.go#L198), [`go/context/run_writer.go:1077`](../../go/context/run_writer.go#L1077), [`go/context/run_writer.go:1078`](../../go/context/run_writer.go#L1078), [`go/context/run_writer.go:711`](../../go/context/run_writer.go#L711), [`go/context/run_writer.go:712`](../../go/context/run_writer.go#L712), [`go/context/run_writer.go:726`](../../go/context/run_writer.go#L726), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | | `budget_paused` | [`packages/harness/context/managed.ts:126`](../../packages/harness/context/managed.ts#L126), [`packages/harness/engines/deepagents/loop.ts:1122`](../../packages/harness/engines/deepagents/loop.ts#L1122), [`packages/harness/engines/vercel/loop.ts:1840`](../../packages/harness/engines/vercel/loop.ts#L1840) | [`go/adapter/agent/agent.go:185`](../../go/adapter/agent/agent.go#L185), [`go/adapter/eino/run.go:209`](../../go/adapter/eino/run.go#L209), [`go/context/managed.go:287`](../../go/context/managed.go#L287), [`go/context/managed.go:297`](../../go/context/managed.go#L297) | [`packages/harness/context/managed.ts:184`](../../packages/harness/context/managed.ts#L184), [`packages/harness/context/managed.ts:209`](../../packages/harness/context/managed.ts#L209), [`packages/harness/context/managed.ts:223`](../../packages/harness/context/managed.ts#L223), [`packages/harness/delegation.ts:1033`](../../packages/harness/delegation.ts#L1033), [`packages/harness/delegation.ts:728`](../../packages/harness/delegation.ts#L728), [`packages/harness/history/view.ts:93`](../../packages/harness/history/view.ts#L93), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/runReport.ts:79`](../../packages/harness/runReport.ts#L79), [`packages/harness/runReport.ts:84`](../../packages/harness/runReport.ts#L84), [`packages/harness/storage/writer.ts:530`](../../packages/harness/storage/writer.ts#L530), [`packages/harness/storage/writer.ts:532`](../../packages/harness/storage/writer.ts#L532), [`packages/harness/storage/writer.ts:666`](../../packages/harness/storage/writer.ts#L666) | [`go/adapter/delegate/batch.go:352`](../../go/adapter/delegate/batch.go#L352), [`go/adapter/delegate/coordinator.go:562`](../../go/adapter/delegate/coordinator.go#L562), [`go/adapter/delegate/coordinator.go:563`](../../go/adapter/delegate/coordinator.go#L563), [`go/context/history_view.go:313`](../../go/context/history_view.go#L313), [`go/context/managed.go:183`](../../go/context/managed.go#L183), [`go/context/managed.go:230`](../../go/context/managed.go#L230), [`go/context/managed.go:272`](../../go/context/managed.go#L272), [`go/context/run_report.go:199`](../../go/context/run_report.go#L199), [`go/context/run_report.go:200`](../../go/context/run_report.go#L200), [`go/context/run_writer.go:1080`](../../go/context/run_writer.go#L1080), [`go/context/run_writer.go:1081`](../../go/context/run_writer.go#L1081), [`go/context/run_writer.go:720`](../../go/context/run_writer.go#L720), [`go/context/run_writer.go:721`](../../go/context/run_writer.go#L721), [`go/context/run_writer.go:726`](../../go/context/run_writer.go#L726), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | -| `approval_requested` | [`packages/harness/context/session.ts:531`](../../packages/harness/context/session.ts#L531), [`packages/harness/execution/controller.ts:368`](../../packages/harness/execution/controller.ts#L368), [`packages/harness/execution/controller.ts:370`](../../packages/harness/execution/controller.ts#L370) | [`go/context/plan_actions.go:377`](../../go/context/plan_actions.go#L377), [`go/context/session.go:1344`](../../go/context/session.go#L1344) | [`packages/harness/context/managed.ts:127`](../../packages/harness/context/managed.ts#L127), [`packages/harness/context/managed.ts:148`](../../packages/harness/context/managed.ts#L148), [`packages/harness/context/managed.ts:183`](../../packages/harness/context/managed.ts#L183), [`packages/harness/context/managed.ts:191`](../../packages/harness/context/managed.ts#L191), [`packages/harness/context/managed.ts:220`](../../packages/harness/context/managed.ts#L220), [`packages/harness/context/managed.ts:255`](../../packages/harness/context/managed.ts#L255), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529), [`packages/harness/storage/writer.ts:653`](../../packages/harness/storage/writer.ts#L653) | [`go/context/managed.go:192`](../../go/context/managed.go#L192), [`go/context/managed.go:193`](../../go/context/managed.go#L193), [`go/context/managed.go:232`](../../go/context/managed.go#L232), [`go/context/run_writer.go:1985`](../../go/context/run_writer.go#L1985), [`go/context/run_writer.go:1986`](../../go/context/run_writer.go#L1986), [`go/context/run_writer.go:2003`](../../go/context/run_writer.go#L2003), [`go/context/run_writer.go:2004`](../../go/context/run_writer.go#L2004), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `approval_requested` | [`packages/harness/context/session.ts:531`](../../packages/harness/context/session.ts#L531), [`packages/harness/execution/controller.ts:368`](../../packages/harness/execution/controller.ts#L368), [`packages/harness/execution/controller.ts:370`](../../packages/harness/execution/controller.ts#L370) | [`go/context/plan_actions.go:377`](../../go/context/plan_actions.go#L377), [`go/context/session.go:1344`](../../go/context/session.go#L1344) | [`packages/harness/context/managed.ts:127`](../../packages/harness/context/managed.ts#L127), [`packages/harness/context/managed.ts:148`](../../packages/harness/context/managed.ts#L148), [`packages/harness/context/managed.ts:183`](../../packages/harness/context/managed.ts#L183), [`packages/harness/context/managed.ts:191`](../../packages/harness/context/managed.ts#L191), [`packages/harness/context/managed.ts:220`](../../packages/harness/context/managed.ts#L220), [`packages/harness/context/managed.ts:255`](../../packages/harness/context/managed.ts#L255), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529), [`packages/harness/storage/writer.ts:653`](../../packages/harness/storage/writer.ts#L653) | [`go/context/managed.go:192`](../../go/context/managed.go#L192), [`go/context/managed.go:193`](../../go/context/managed.go#L193), [`go/context/managed.go:232`](../../go/context/managed.go#L232), [`go/context/run_writer.go:1989`](../../go/context/run_writer.go#L1989), [`go/context/run_writer.go:1990`](../../go/context/run_writer.go#L1990), [`go/context/run_writer.go:2007`](../../go/context/run_writer.go#L2007), [`go/context/run_writer.go:2008`](../../go/context/run_writer.go#L2008), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | | `compacted` | [`packages/harness/context/session.ts:808`](../../packages/harness/context/session.ts#L808) | [`go/context/session.go:1150`](../../go/context/session.go#L1150) | — | — | | `skill_loaded` | [`packages/harness/skills.ts:493`](../../packages/harness/skills.ts#L493) | [`go/context/skills.go:651`](../../go/context/skills.go#L651) | — | — | | `skill_unloaded` | [`packages/harness/skills.ts:505`](../../packages/harness/skills.ts#L505) | [`go/context/skills.go:686`](../../go/context/skills.go#L686) | — | — | | `retry` | [`packages/harness/engines/codex/index.ts:126`](../../packages/harness/engines/codex/index.ts#L126), [`packages/harness/engines/deepagents/loop.ts:597`](../../packages/harness/engines/deepagents/loop.ts#L597), [`packages/harness/engines/vercel/loop.ts:1086`](../../packages/harness/engines/vercel/loop.ts#L1086) | [`go/adapter/agent/invoke.go:515`](../../go/adapter/agent/invoke.go#L515), [`go/adapter/eino/run.go:400`](../../go/adapter/eino/run.go#L400) | — | — | | `response_recovery` | [`packages/harness/engines/codex/index.ts:321`](../../packages/harness/engines/codex/index.ts#L321), [`packages/harness/engines/deepagents/loop.ts:711`](../../packages/harness/engines/deepagents/loop.ts#L711), [`packages/harness/engines/deepagents/loop.ts:714`](../../packages/harness/engines/deepagents/loop.ts#L714), [`packages/harness/engines/vercel/loop.ts:1192`](../../packages/harness/engines/vercel/loop.ts#L1192), [`packages/harness/engines/vercel/loop.ts:1195`](../../packages/harness/engines/vercel/loop.ts#L1195) | [`go/adapter/agent/agent.go:410`](../../go/adapter/agent/agent.go#L410), [`go/adapter/eino/loopphase.go:43`](../../go/adapter/eino/loopphase.go#L43), [`go/adapter/eino/loopphase.go:55`](../../go/adapter/eino/loopphase.go#L55) | — | — | | `delegation_requested` | [`packages/harness/engines/codex/index.ts:183`](../../packages/harness/engines/codex/index.ts#L183) | [`go/adapter/agent/invoke.go:766`](../../go/adapter/agent/invoke.go#L766) | [`packages/harness/context/managed.ts:167`](../../packages/harness/context/managed.ts#L167), [`packages/harness/delegation.ts:1035`](../../packages/harness/delegation.ts#L1035), [`packages/harness/delegation.ts:734`](../../packages/harness/delegation.ts#L734), [`packages/harness/storage/writer.ts:666`](../../packages/harness/storage/writer.ts#L666) | [`go/adapter/delegate/coordinator.go:534`](../../go/adapter/delegate/coordinator.go#L534), [`go/adapter/delegate/coordinator.go:536`](../../go/adapter/delegate/coordinator.go#L536), [`go/context/output_boundary.go:75`](../../go/context/output_boundary.go#L75), [`go/context/output_boundary.go:76`](../../go/context/output_boundary.go#L76), [`go/context/output_boundary.go:80`](../../go/context/output_boundary.go#L80), [`go/context/run_writer.go:1069`](../../go/context/run_writer.go#L1069), [`go/context/run_writer.go:1070`](../../go/context/run_writer.go#L1070) | -| `delegated_task` | [`packages/harness/delegation.ts:464`](../../packages/harness/delegation.ts#L464) | [`go/adapter/delegate/coordinator.go:368`](../../go/adapter/delegate/coordinator.go#L368) | [`packages/harness/storage/writer.ts:320`](../../packages/harness/storage/writer.ts#L320), [`packages/harness/storage/writer.ts:536`](../../packages/harness/storage/writer.ts#L536) | [`go/context/run_writer.go:2660`](../../go/context/run_writer.go#L2660), [`go/context/run_writer.go:2665`](../../go/context/run_writer.go#L2665), [`go/context/run_writer.go:2676`](../../go/context/run_writer.go#L2676), [`go/context/run_writer.go:2836`](../../go/context/run_writer.go#L2836), [`go/context/run_writer.go:2839`](../../go/context/run_writer.go#L2839), [`go/context/run_writer.go:2840`](../../go/context/run_writer.go#L2840), [`go/context/run_writer.go:730`](../../go/context/run_writer.go#L730), [`go/context/run_writer.go:733`](../../go/context/run_writer.go#L733) | +| `delegated_task` | [`packages/harness/delegation.ts:464`](../../packages/harness/delegation.ts#L464) | [`go/adapter/delegate/coordinator.go:368`](../../go/adapter/delegate/coordinator.go#L368) | [`packages/harness/storage/writer.ts:320`](../../packages/harness/storage/writer.ts#L320), [`packages/harness/storage/writer.ts:536`](../../packages/harness/storage/writer.ts#L536) | [`go/context/run_writer.go:2664`](../../go/context/run_writer.go#L2664), [`go/context/run_writer.go:2669`](../../go/context/run_writer.go#L2669), [`go/context/run_writer.go:2680`](../../go/context/run_writer.go#L2680), [`go/context/run_writer.go:2840`](../../go/context/run_writer.go#L2840), [`go/context/run_writer.go:2843`](../../go/context/run_writer.go#L2843), [`go/context/run_writer.go:2844`](../../go/context/run_writer.go#L2844), [`go/context/run_writer.go:730`](../../go/context/run_writer.go#L730), [`go/context/run_writer.go:733`](../../go/context/run_writer.go#L733) | | `user_question` | [`packages/harness/engines/codex/index.ts:191`](../../packages/harness/engines/codex/index.ts#L191), [`packages/harness/engines/deepagents/loop.ts:516`](../../packages/harness/engines/deepagents/loop.ts#L516), [`packages/harness/engines/vercel/loop.ts:976`](../../packages/harness/engines/vercel/loop.ts#L976) | [`go/adapter/agent/invoke.go:777`](../../go/adapter/agent/invoke.go#L777), [`go/adapter/eino/calls.go:318`](../../go/adapter/eino/calls.go#L318) | [`packages/harness/context/managed.ts:166`](../../packages/harness/context/managed.ts#L166), [`packages/harness/context/managed.ts:184`](../../packages/harness/context/managed.ts#L184), [`packages/harness/context/managed.ts:229`](../../packages/harness/context/managed.ts#L229), [`packages/harness/context/managed.ts:230`](../../packages/harness/context/managed.ts#L230), [`packages/harness/context/managed.ts:231`](../../packages/harness/context/managed.ts#L231), [`packages/harness/delegation.ts:732`](../../packages/harness/delegation.ts#L732) | [`go/adapter/delegate/batch.go:358`](../../go/adapter/delegate/batch.go#L358), [`go/adapter/delegate/batch.go:359`](../../go/adapter/delegate/batch.go#L359), [`go/context/managed.go:183`](../../go/context/managed.go#L183), [`go/context/managed.go:305`](../../go/context/managed.go#L305), [`go/context/managed.go:313`](../../go/context/managed.go#L313), [`go/context/output_boundary.go:67`](../../go/context/output_boundary.go#L67), [`go/context/output_boundary.go:68`](../../go/context/output_boundary.go#L68), [`go/context/output_boundary.go:72`](../../go/context/output_boundary.go#L72) | | `reasoning` | [`packages/harness/engines/deepagents/loop.ts:616`](../../packages/harness/engines/deepagents/loop.ts#L616), [`packages/harness/engines/deepagents/loop.ts:619`](../../packages/harness/engines/deepagents/loop.ts#L619), [`packages/harness/engines/vercel/loop.ts:1304`](../../packages/harness/engines/vercel/loop.ts#L1304), [`packages/harness/engines/vercel/loop.ts:1310`](../../packages/harness/engines/vercel/loop.ts#L1310), [`packages/harness/engines/vercel/loop.ts:1618`](../../packages/harness/engines/vercel/loop.ts#L1618), [`packages/harness/engines/vercel/loop.ts:1622`](../../packages/harness/engines/vercel/loop.ts#L1622) | [`go/adapter/eino/step.go:36`](../../go/adapter/eino/step.go#L36), [`go/adapter/eino/step.go:42`](../../go/adapter/eino/step.go#L42) | [`packages/harness/context/managed.ts:161`](../../packages/harness/context/managed.ts#L161), [`packages/harness/context/managed.ts:162`](../../packages/harness/context/managed.ts#L162) | [`go/context/managed.go:154`](../../go/context/managed.go#L154), [`go/context/output_boundary.go:49`](../../go/context/output_boundary.go#L49), [`go/context/output_boundary.go:50`](../../go/context/output_boundary.go#L50) | | `message` | [`packages/harness/engines/codex/index.ts:137`](../../packages/harness/engines/codex/index.ts#L137), [`packages/harness/engines/codex/index.ts:297`](../../packages/harness/engines/codex/index.ts#L297), [`packages/harness/engines/codex/index.ts:318`](../../packages/harness/engines/codex/index.ts#L318), [`packages/harness/engines/codex/index.ts:336`](../../packages/harness/engines/codex/index.ts#L336), [`packages/harness/engines/codex/index.ts:347`](../../packages/harness/engines/codex/index.ts#L347), [`packages/harness/engines/deepagents/loop.ts:680`](../../packages/harness/engines/deepagents/loop.ts#L680), [`packages/harness/engines/deepagents/loop.ts:889`](../../packages/harness/engines/deepagents/loop.ts#L889), [`packages/harness/engines/deepagents/loop.ts:895`](../../packages/harness/engines/deepagents/loop.ts#L895), [`packages/harness/engines/deepagents/loop.ts:900`](../../packages/harness/engines/deepagents/loop.ts#L900), [`packages/harness/engines/vercel/loop.ts:1163`](../../packages/harness/engines/vercel/loop.ts#L1163), [`packages/harness/engines/vercel/loop.ts:1445`](../../packages/harness/engines/vercel/loop.ts#L1445), [`packages/harness/engines/vercel/loop.ts:1451`](../../packages/harness/engines/vercel/loop.ts#L1451), [`packages/harness/engines/vercel/loop.ts:1458`](../../packages/harness/engines/vercel/loop.ts#L1458) | [`go/adapter/agent/agent.go:627`](../../go/adapter/agent/agent.go#L627), [`go/adapter/eino/loop.go:174`](../../go/adapter/eino/loop.go#L174), [`go/adapter/eino/loop.go:350`](../../go/adapter/eino/loop.go#L350) | [`packages/harness/context/managed.ts:163`](../../packages/harness/context/managed.ts#L163), [`packages/harness/delegation.ts:724`](../../packages/harness/delegation.ts#L724), [`packages/harness/history/feedback.ts:29`](../../packages/harness/history/feedback.ts#L29), [`packages/harness/storage/history.ts:129`](../../packages/harness/storage/history.ts#L129) | [`go/adapter/delegate/batch.go:332`](../../go/adapter/delegate/batch.go#L332), [`go/adapter/delegate/batch.go:336`](../../go/adapter/delegate/batch.go#L336), [`go/context/history_metadata_store.go:130`](../../go/context/history_metadata_store.go#L130), [`go/context/output_boundary.go:52`](../../go/context/output_boundary.go#L52), [`go/context/output_boundary.go:53`](../../go/context/output_boundary.go#L53) | @@ -102,13 +102,13 @@ Consumer entries identify explicit kind reads, not delivery guarantees. Generic | `connection_switched` | [`packages/harness/context/session.ts:1122`](../../packages/harness/context/session.ts#L1122), [`packages/harness/context/session.ts:380`](../../packages/harness/context/session.ts#L380), [`packages/harness/engines/deepagents/loop.ts:604`](../../packages/harness/engines/deepagents/loop.ts#L604), [`packages/harness/engines/vercel/loop.ts:1094`](../../packages/harness/engines/vercel/loop.ts#L1094) | [`go/adapter/agent/invoke.go:299`](../../go/adapter/agent/invoke.go#L299), [`go/adapter/eino/run.go:440`](../../go/adapter/eino/run.go#L440) | — | — | | `usage` | [`packages/harness/context/managed.ts:215`](../../packages/harness/context/managed.ts#L215), [`packages/harness/context/managed.ts:228`](../../packages/harness/context/managed.ts#L228), [`packages/harness/context/managed.ts:240`](../../packages/harness/context/managed.ts#L240), [`packages/harness/context/managed.ts:246`](../../packages/harness/context/managed.ts#L246), [`packages/harness/context/managed.ts:257`](../../packages/harness/context/managed.ts#L257), [`packages/harness/context/managed.ts:268`](../../packages/harness/context/managed.ts#L268) | [`go/context/managed.go:144`](../../go/context/managed.go#L144), [`go/context/managed.go:242`](../../go/context/managed.go#L242), [`go/context/managed.go:299`](../../go/context/managed.go#L299), [`go/context/managed.go:409`](../../go/context/managed.go#L409) | [`packages/harness/delegation.ts:720`](../../packages/harness/delegation.ts#L720) | [`go/adapter/delegate/batch.go:305`](../../go/adapter/delegate/batch.go#L305), [`go/adapter/delegate/batch.go:311`](../../go/adapter/delegate/batch.go#L311), [`go/context/managed.go:173`](../../go/context/managed.go#L173) | | `recorded` | [`packages/harness/corpus.ts:62`](../../packages/harness/corpus.ts#L62) | [`go/context/corpus.go:236`](../../go/context/corpus.go#L236) | — | — | -| `plan_revised` | [`packages/harness/execution/controller.ts:361`](../../packages/harness/execution/controller.ts#L361), [`packages/harness/execution/controller.ts:402`](../../packages/harness/execution/controller.ts#L402) | [`go/context/controller.go:1363`](../../go/context/controller.go#L1363), [`go/context/plan_actions.go:432`](../../go/context/plan_actions.go#L432) | [`packages/harness/history/query.ts:22`](../../packages/harness/history/query.ts#L22), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1973`](../../go/context/run_writer.go#L1973), [`go/context/run_writer.go:1974`](../../go/context/run_writer.go#L1974), [`go/context/run_writer.go:1991`](../../go/context/run_writer.go#L1991), [`go/context/run_writer.go:1992`](../../go/context/run_writer.go#L1992) | -| `plan_departed` | [`packages/harness/execution/controller.ts:387`](../../packages/harness/execution/controller.ts#L387) | [`go/context/plan_actions.go:365`](../../go/context/plan_actions.go#L365) | [`packages/harness/history/query.ts:23`](../../packages/harness/history/query.ts#L23), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1976`](../../go/context/run_writer.go#L1976), [`go/context/run_writer.go:1977`](../../go/context/run_writer.go#L1977), [`go/context/run_writer.go:1994`](../../go/context/run_writer.go#L1994), [`go/context/run_writer.go:1995`](../../go/context/run_writer.go#L1995) | -| `plan_completion_refused` | [`packages/harness/execution/controller.ts:397`](../../packages/harness/execution/controller.ts#L397) | [`go/context/plan_actions.go:418`](../../go/context/plan_actions.go#L418) | [`packages/harness/history/query.ts:24`](../../packages/harness/history/query.ts#L24), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1979`](../../go/context/run_writer.go#L1979), [`go/context/run_writer.go:1980`](../../go/context/run_writer.go#L1980), [`go/context/run_writer.go:1997`](../../go/context/run_writer.go#L1997), [`go/context/run_writer.go:1998`](../../go/context/run_writer.go#L1998) | -| `plan_drift_decided` | [`packages/harness/execution/controller.ts:104`](../../packages/harness/execution/controller.ts#L104) | [`go/context/plan_validation.go:274`](../../go/context/plan_validation.go#L274) | [`packages/harness/context/session.ts:1025`](../../packages/harness/context/session.ts#L1025), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/nativePlanPause.ts:78`](../../packages/harness/storage/nativePlanPause.ts#L78), [`packages/harness/storage/writer.ts:528`](../../packages/harness/storage/writer.ts#L528), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529), [`packages/harness/storage/writer.ts:655`](../../packages/harness/storage/writer.ts#L655) | [`go/context/native_plan_pause.go:188`](../../go/context/native_plan_pause.go#L188), [`go/context/native_plan_pause.go:237`](../../go/context/native_plan_pause.go#L237), [`go/context/native_plan_pause.go:464`](../../go/context/native_plan_pause.go#L464), [`go/context/native_plan_pause.go:473`](../../go/context/native_plan_pause.go#L473), [`go/context/run_writer.go:1982`](../../go/context/run_writer.go#L1982), [`go/context/run_writer.go:1983`](../../go/context/run_writer.go#L1983), [`go/context/run_writer.go:2000`](../../go/context/run_writer.go#L2000), [`go/context/run_writer.go:2001`](../../go/context/run_writer.go#L2001), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | -| `route_decided` | [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/turn_route_owner.go:230`](../../go/context/turn_route_owner.go#L230) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/run_writer.go:1967`](../../go/context/run_writer.go#L1967), [`go/context/run_writer.go:1968`](../../go/context/run_writer.go#L1968), [`go/context/run_writer.go:942`](../../go/context/run_writer.go#L942), [`go/context/run_writer.go:943`](../../go/context/run_writer.go#L943), [`go/context/turn_route_owner.go:230`](../../go/context/turn_route_owner.go#L230), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | -| `route_widened` | [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/turn_route_owner.go:232`](../../go/context/turn_route_owner.go#L232) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71), [`packages/harness/storage/writer.ts:600`](../../packages/harness/storage/writer.ts#L600), [`packages/harness/storage/writer.ts:617`](../../packages/harness/storage/writer.ts#L617) | [`go/context/run_writer.go:1970`](../../go/context/run_writer.go#L1970), [`go/context/run_writer.go:1971`](../../go/context/run_writer.go#L1971), [`go/context/run_writer.go:909`](../../go/context/run_writer.go#L909), [`go/context/run_writer.go:910`](../../go/context/run_writer.go#L910), [`go/context/run_writer.go:915`](../../go/context/run_writer.go#L915), [`go/context/run_writer.go:928`](../../go/context/run_writer.go#L928), [`go/context/run_writer.go:945`](../../go/context/run_writer.go#L945), [`go/context/run_writer.go:946`](../../go/context/run_writer.go#L946), [`go/context/turn_route_owner.go:232`](../../go/context/turn_route_owner.go#L232), [`go/context/turn_route_owner.go:246`](../../go/context/turn_route_owner.go#L246), [`go/context/turn_route_owner.go:247`](../../go/context/turn_route_owner.go#L247), [`go/context/turn_route_owner.go:251`](../../go/context/turn_route_owner.go#L251), [`go/context/turn_route_owner.go:753`](../../go/context/turn_route_owner.go#L753), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | -| `repeat_detected` | [`packages/harness/repeatTools.ts:36`](../../packages/harness/repeatTools.ts#L36) | [`go/context/repeat_tools.go:99`](../../go/context/repeat_tools.go#L99) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102) | [`go/context/run_writer.go:1964`](../../go/context/run_writer.go#L1964), [`go/context/run_writer.go:1965`](../../go/context/run_writer.go#L1965), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `plan_revised` | [`packages/harness/execution/controller.ts:361`](../../packages/harness/execution/controller.ts#L361), [`packages/harness/execution/controller.ts:402`](../../packages/harness/execution/controller.ts#L402) | [`go/context/controller.go:1363`](../../go/context/controller.go#L1363), [`go/context/plan_actions.go:432`](../../go/context/plan_actions.go#L432) | [`packages/harness/history/query.ts:22`](../../packages/harness/history/query.ts#L22), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1977`](../../go/context/run_writer.go#L1977), [`go/context/run_writer.go:1978`](../../go/context/run_writer.go#L1978), [`go/context/run_writer.go:1995`](../../go/context/run_writer.go#L1995), [`go/context/run_writer.go:1996`](../../go/context/run_writer.go#L1996) | +| `plan_departed` | [`packages/harness/execution/controller.ts:387`](../../packages/harness/execution/controller.ts#L387) | [`go/context/plan_actions.go:365`](../../go/context/plan_actions.go#L365) | [`packages/harness/history/query.ts:23`](../../packages/harness/history/query.ts#L23), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1980`](../../go/context/run_writer.go#L1980), [`go/context/run_writer.go:1981`](../../go/context/run_writer.go#L1981), [`go/context/run_writer.go:1998`](../../go/context/run_writer.go#L1998), [`go/context/run_writer.go:1999`](../../go/context/run_writer.go#L1999) | +| `plan_completion_refused` | [`packages/harness/execution/controller.ts:397`](../../packages/harness/execution/controller.ts#L397) | [`go/context/plan_actions.go:418`](../../go/context/plan_actions.go#L418) | [`packages/harness/history/query.ts:24`](../../packages/harness/history/query.ts#L24), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529) | [`go/context/run_writer.go:1983`](../../go/context/run_writer.go#L1983), [`go/context/run_writer.go:1984`](../../go/context/run_writer.go#L1984), [`go/context/run_writer.go:2001`](../../go/context/run_writer.go#L2001), [`go/context/run_writer.go:2002`](../../go/context/run_writer.go#L2002) | +| `plan_drift_decided` | [`packages/harness/execution/controller.ts:104`](../../packages/harness/execution/controller.ts#L104) | [`go/context/plan_validation.go:274`](../../go/context/plan_validation.go#L274) | [`packages/harness/context/session.ts:1025`](../../packages/harness/context/session.ts#L1025), [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/nativePlanPause.ts:78`](../../packages/harness/storage/nativePlanPause.ts#L78), [`packages/harness/storage/writer.ts:528`](../../packages/harness/storage/writer.ts#L528), [`packages/harness/storage/writer.ts:529`](../../packages/harness/storage/writer.ts#L529), [`packages/harness/storage/writer.ts:655`](../../packages/harness/storage/writer.ts#L655) | [`go/context/native_plan_pause.go:188`](../../go/context/native_plan_pause.go#L188), [`go/context/native_plan_pause.go:237`](../../go/context/native_plan_pause.go#L237), [`go/context/native_plan_pause.go:464`](../../go/context/native_plan_pause.go#L464), [`go/context/native_plan_pause.go:473`](../../go/context/native_plan_pause.go#L473), [`go/context/run_writer.go:1986`](../../go/context/run_writer.go#L1986), [`go/context/run_writer.go:1987`](../../go/context/run_writer.go#L1987), [`go/context/run_writer.go:2004`](../../go/context/run_writer.go#L2004), [`go/context/run_writer.go:2005`](../../go/context/run_writer.go#L2005), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `route_decided` | [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/turn_route_owner.go:230`](../../go/context/turn_route_owner.go#L230) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/run_writer.go:1971`](../../go/context/run_writer.go#L1971), [`go/context/run_writer.go:1972`](../../go/context/run_writer.go#L1972), [`go/context/run_writer.go:942`](../../go/context/run_writer.go#L942), [`go/context/run_writer.go:943`](../../go/context/run_writer.go#L943), [`go/context/turn_route_owner.go:230`](../../go/context/turn_route_owner.go#L230), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `route_widened` | [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71) | [`go/context/turn_route_owner.go:232`](../../go/context/turn_route_owner.go#L232) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102), [`packages/harness/storage/turnRouteOwner.ts:71`](../../packages/harness/storage/turnRouteOwner.ts#L71), [`packages/harness/storage/writer.ts:600`](../../packages/harness/storage/writer.ts#L600), [`packages/harness/storage/writer.ts:617`](../../packages/harness/storage/writer.ts#L617) | [`go/context/run_writer.go:1974`](../../go/context/run_writer.go#L1974), [`go/context/run_writer.go:1975`](../../go/context/run_writer.go#L1975), [`go/context/run_writer.go:909`](../../go/context/run_writer.go#L909), [`go/context/run_writer.go:910`](../../go/context/run_writer.go#L910), [`go/context/run_writer.go:915`](../../go/context/run_writer.go#L915), [`go/context/run_writer.go:928`](../../go/context/run_writer.go#L928), [`go/context/run_writer.go:945`](../../go/context/run_writer.go#L945), [`go/context/run_writer.go:946`](../../go/context/run_writer.go#L946), [`go/context/turn_route_owner.go:232`](../../go/context/turn_route_owner.go#L232), [`go/context/turn_route_owner.go:246`](../../go/context/turn_route_owner.go#L246), [`go/context/turn_route_owner.go:247`](../../go/context/turn_route_owner.go#L247), [`go/context/turn_route_owner.go:251`](../../go/context/turn_route_owner.go#L251), [`go/context/turn_route_owner.go:753`](../../go/context/turn_route_owner.go#L753), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | +| `repeat_detected` | [`packages/harness/repeatTools.ts:36`](../../packages/harness/repeatTools.ts#L36) | [`go/context/repeat_tools.go:99`](../../go/context/repeat_tools.go#L99) | [`packages/harness/recordReader.ts:102`](../../packages/harness/recordReader.ts#L102) | [`go/context/run_writer.go:1968`](../../go/context/run_writer.go#L1968), [`go/context/run_writer.go:1969`](../../go/context/run_writer.go#L1969), [`go/contract/records.go:583`](../../go/contract/records.go#L583) | ## Every kind with its members From f9bd581ac8f6dd854c9c5166812d4c5d535f5a1c Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:24:42 -0400 Subject: [PATCH 3/3] 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 Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --- docs/design/storage.md | 9 +++-- go/context/run_lease.go | 7 +++- go/context/run_lease_batch_test.go | 37 +++++++++++++++++ packages/harness/scripts/mutation-rows.json | 42 +++++++++++++++++-- packages/harness/storage/lease.test.ts | 45 +++++++++++++++++++++ packages/harness/storage/lease.ts | 24 ++++++----- 6 files changed, 147 insertions(+), 17 deletions(-) diff --git a/docs/design/storage.md b/docs/design/storage.md index c977621b..efa0261a 100644 --- a/docs/design/storage.md +++ b/docs/design/storage.md @@ -347,10 +347,13 @@ admitted before (also under a takeover's fence), another owner, a stale or forge for another run than the operation's, a key that is not its operation's, an expired lease and a released one. `certifyEffectHost(create, authority)` (Go `CertifyEffectHost`) calls the factory once, over an authority that answers from the kit until the checks pass and from `authority` for -good after, and admits that exact instance: TypeScript returns a frozen wrapper that refuses once the -instance's `dispatch` is not the one the kit checked; Go returns a `*CertifiedEffectHost` whose +good after, and admits that exact instance. TypeScript reads its `dispatch` once, runs the kit +through exactly that function and admits only that function, through a frozen wrapper that also +refuses once the instance's `dispatch` is not it; Go returns a `*CertifiedEffectHost` whose identity, not its value, is the evidence. A `ReferenceEffectHost` is evidence when the package made -it (its `dispatch` is sealed on the instance; a subclass or a type embedding it is not). Unattended +it: in TypeScript its `dispatch` is sealed on the instance and its authority and admitted +identities are private fields; in Go only the pointer `NewReferenceEffectHost` returned qualifies, +not a copy of it. A subclass, or a type embedding it, is not evidence. Unattended admission accepts only these, and only when the authority they enforce against is the run's own store: a host over a no-op authority, over another store, or certified over another store is refused. A declared `effectFencing: 'enforced'` is never evidence. Nothing keeps an admitted host diff --git a/go/context/run_lease.go b/go/context/run_lease.go index 2edd760c..6f43c40b 100644 --- a/go/context/run_lease.go +++ b/go/context/run_lease.go @@ -547,6 +547,7 @@ type LeaseAuthority interface { // refused as already-admitted whatever the epoch: its outcome is unknown until its own host reconciles // it, and a takeover never replays it. type ReferenceEffectHost struct { + self *ReferenceEffectHost // identity: a copy shares the key map without the lock, and is not evidence mu sync.Mutex authority LeaseAuthority admitted map[string]bool @@ -554,7 +555,9 @@ type ReferenceEffectHost struct { } func NewReferenceEffectHost(authority LeaseAuthority) *ReferenceEffectHost { - return &ReferenceEffectHost{authority: authority, admitted: map[string]bool{}} + host := &ReferenceEffectHost{authority: authority, admitted: map[string]bool{}} + host.self = host + return host } // CheckEffectHostConformance (#223 review, #253) is the effect-host conformance. The kit owns the lease @@ -755,7 +758,7 @@ func sameAuthority(authority LeaseAuthority, store RunStore) bool { func provenAuthority(host EffectHost) LeaseAuthority { switch h := host.(type) { case *ReferenceEffectHost: - if h != nil && h.admitted != nil { + if h != nil && h.self == h && h.admitted != nil { return h.authority } case *CertifiedEffectHost: diff --git a/go/context/run_lease_batch_test.go b/go/context/run_lease_batch_test.go index c13ec81b..b02a0dc6 100644 --- a/go/context/run_lease_batch_test.go +++ b/go/context/run_lease_batch_test.go @@ -3,6 +3,7 @@ package runcontext import ( "context" "errors" + "reflect" "runtime" "strings" "sync/atomic" @@ -190,3 +191,39 @@ func TestLeaseAdmittedHostsAreCollectable(t *testing.T) { t.Fatalf("admitted hosts were kept alive: %d of 100 collected", collected.Load()) } } + +// #257 re-check: only the pointer NewReferenceEffectHost made is evidence. A shallow copy shares the +// original's admitted-key map without its lock; it is refused before any dispatch could race. +func TestLeaseACopiedReferenceHostIsNotEvidence(t *testing.T) { + ctx := context.Background() + durable := durableLeaseFixture{NewMemoryRunStore(MemoryRunStoreOptions{})} + original := NewReferenceEffectHost(durable) + copied := new(ReferenceEffectHost) + reflect.ValueOf(copied).Elem().Set(reflect.ValueOf(original).Elem()) // a shallow copy, as a host could make one + if err := AdmitUnattended(durable, copied); leaseReason(err) != "effect-host-unfenced" { + t.Fatalf("a copy of the reference host was admitted: %v", err) + } + if err := AdmitUnattended(durable, original); err != nil { + t.Fatal(err) + } + // The original keeps dispatching alone, concurrently, without a race. + lease, err := durable.AcquireLease(ctx, LeaseAcquire{Resource: "copied", Owner: "worker-a", DurationMs: leaseMinute}) + if err != nil { + t.Fatal(err) + } + var performed atomic.Int32 + done := make(chan struct{}) + for i := range 4 { + go func() { + defer func() { done <- struct{}{} }() + admission, _ := NewEffectAdmission(FenceOf(lease), EffectOperation{Run: "copied", Operation: "op-" + strings.Repeat("x", i+1), Source: "host", Tool: "t", RequestDigest: "sha256:" + strings.Repeat("a", 64)}) + _ = original.Dispatch(ctx, admission, func(context.Context) error { performed.Add(1); return nil }) + }() + } + for range 4 { + <-done + } + if performed.Load() != 4 { + t.Fatalf("performed %d of 4 distinct operations", performed.Load()) + } +} diff --git a/packages/harness/scripts/mutation-rows.json b/packages/harness/scripts/mutation-rows.json index 34205449..4d4d80af 100644 --- a/packages/harness/scripts/mutation-rows.json +++ b/packages/harness/scripts/mutation-rows.json @@ -7874,7 +7874,7 @@ { "name": "lease fence L4 the effect host admits without checking the fence", "file": "storage/lease.ts", - "find": " await this.authority.checkFence(held.fence,signal)\n", + "find": " await this.#authority.checkFence(held.fence,signal)\n", "replace": "", "tests": [ "storage/lease.test.ts" @@ -7934,7 +7934,7 @@ { "name": "lease fence L9 the effect host admits an identity it admitted before", "file": "storage/lease.ts", - "find": " if(this.admitted.has(held.idempotencyKey))throw new LeaseRefused('already-admitted')\n", + "find": " if(this.#admitted.has(held.idempotencyKey))throw new LeaseRefused('already-admitted')\n", "replace": "", "tests": [ "storage/lease.test.ts" @@ -8090,7 +8090,7 @@ { "name": "lease batch B4 certification checks one product and admits another", "file": "storage/lease.ts", - "find": "await exerciseEffectHost(host,kit)\n if(host.dispatch!==dispatch", + "find": "await exerciseEffectHost(captured,kit)\n if(host.dispatch!==dispatch", "replace": "await effectHostConformance(create)\n if(host.dispatch!==dispatch", "tests": [ "storage/lease.test.ts" @@ -8158,6 +8158,42 @@ "status": "caught", "origin": "agent#253", "note": "#253 R2. Caught at lease.test.ts > … evidence does not pass to another instance or a subclass. Go twin (by hand, accepting any type that embeds *ReferenceEffectHost): caught at TestLeaseCertificationIsBoundToTheInstanceItChecked/authority-is-the-store." +}, +{ + "name": "lease batch C1 certification runs the kit through fresh reads of dispatch", + "file": "storage/lease.ts", + "find": "await exerciseEffectHost(captured,kit)", + "replace": "await exerciseEffectHost(host,kit)", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#257 re-check finding 1(a). A getter or Proxy trap shows the kit an enforcing dispatch and the capture a non-enforcing one, which is then certified, admitted and performs a fence the store never issued. Caught at lease.test.ts > a dispatch getter that changes between the kit and admission…, and a Proxy whose get trap changes dispatch…. TypeScript only." +}, +{ + "name": "lease batch C2 the reference host reads a replaceable authority", + "file": "storage/lease.ts", + "find": " await this.#authority.checkFence(held.fence,signal)", + "replace": " await ((this as unknown as Record).authority??this.#authority).checkFence(held.fence,signal)", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#257 re-check finding 1(b). Caught at lease.test.ts > replacing the reference host's authority or admitted set after admission changes nothing it enforces (the no-op authority admits a fence the store never issued). TypeScript only: Go fields are unexported." +}, +{ + "name": "lease batch C3 the reference host reads a replaceable admitted set", + "file": "storage/lease.ts", + "find": " if(this.#admitted.has(held.idempotencyKey))throw", + "replace": " if(((this as unknown as Record|undefined>).admitted??this.#admitted).has(held.idempotencyKey))throw", + "tests": [ + "storage/lease.test.ts" + ], + "status": "caught", + "origin": "agent#253", + "note": "#257 re-check finding 1(b). Caught at lease.test.ts > replacing the reference host's authority or admitted set… (an empty set admits an identity again). TypeScript only." } ] } diff --git a/packages/harness/storage/lease.test.ts b/packages/harness/storage/lease.test.ts index 5844f31d..767b14fc 100644 --- a/packages/harness/storage/lease.test.ts +++ b/packages/harness/storage/lease.test.ts @@ -407,3 +407,48 @@ describe('certification is bound to the instance it checked (#253 R2)',()=>{ expect(()=>admitUnattended({store,effectHost:{effectFencing:'enforced',dispatch:certified.dispatch}})).toThrow(capability('effect-host-unfenced')) }) }) + +describe('certification admits exactly the function it ran, and the reference host keeps its own state (#253 re-check)',()=>{ + const stale=effectAdmission({version:1,resource:'r',owner:'o',epoch:'1'},{run:'r',operation:'op',source:'host',tool:'t',requestDigest:'sha256:'+'e'.repeat(64)}) + const performsAll:EffectHost['dispatch']=(_admission,perform)=>perform() + // The first read of dispatch sees a host that performs anything; the next reads (the kit's, if it reads afresh) see an + // enforcing one, until the kit's last admission; every later read sees the first again. + const shifting=(enforcing:EffectHost)=>{ + let reads=0,after=false + const good:EffectHost['dispatch']=(admission,perform,signal)=>{if(admission.operation.operation==='op-9')after=true;return enforcing.dispatch(admission,perform,signal)} + return ()=>{reads++;return reads===1||after?performsAll:good} + } + async function outcome(create:(authority:import('./lease').LeaseAuthority)=>EffectHost,store:DurableLeaseStoreFixture){ + const perform=vi.fn(async()=>'performed') + let certified:EffectHost + try{certified=await certifyEffectHost(create,store)}catch{return {certified:false,performed:false}} + let admitted=true;try{admitUnattended({store,effectHost:certified})}catch{admitted=false} + await certified.dispatch(stale,perform).catch(()=>undefined) + return {certified:true,admitted,performed:perform.mock.calls.length>0} + } + it('a dispatch getter that changes between the kit and admission never performs a fence the store did not issue',async()=>{ + const store=new DurableLeaseStoreFixture() + const result=await outcome(authority=>{const read=shifting(new ReferenceEffectHost(authority));return Object.defineProperty({effectFencing:'enforced'},'dispatch',{get:read}) as EffectHost},store) + expect(result.performed).toBe(false) + expect(result.certified&&result.admitted).toBe(false) + }) + it('a Proxy whose get trap changes dispatch never performs a fence the store did not issue',async()=>{ + const store=new DurableLeaseStoreFixture() + const result=await outcome(authority=>{const inner=new ReferenceEffectHost(authority),read=shifting(inner);return new Proxy({effectFencing:'enforced' as const,dispatch:performsAll},{get:(target,key)=>key==='dispatch'?read():Reflect.get(target,key)})},store) + expect(result.performed).toBe(false) + expect(result.certified&&result.admitted).toBe(false) + }) + it('replacing the reference host\'s authority or admitted set after admission changes nothing it enforces',async()=>{ + const store=new DurableLeaseStoreFixture(),host=new ReferenceEffectHost(store) + expect(()=>admitUnattended({store,effectHost:host})).not.toThrow() + const a=await store.acquireLease({resource:'kept',owner:'worker-a',expectedRevision:null,durationMs:MINUTE}),perform=vi.fn(async()=>'performed') + const once=effectAdmission(fenceOf(a),{run:'kept',operation:'op-1',source:'host',tool:'t',requestDigest:'sha256:'+'f'.repeat(64)}) + await host.dispatch(once,perform);expect(perform).toHaveBeenCalledOnce() + const fields=host as unknown as Record + fields.authority={checkFence:async()=>{}};fields.admitted=new Set() + expect(()=>admitUnattended({store,effectHost:host})).not.toThrow() + await expect(host.dispatch(once,perform)).rejects.toEqual(refusal('already-admitted')) + await expect(host.dispatch(stale,perform)).rejects.toEqual(refusal('no-lease')) + expect(perform).toHaveBeenCalledOnce() + }) +}) diff --git a/packages/harness/storage/lease.ts b/packages/harness/storage/lease.ts index c33daa92..13b2416a 100644 --- a/packages/harness/storage/lease.ts +++ b/packages/harness/storage/lease.ts @@ -160,28 +160,31 @@ export interface LeaseAuthority {checkFence(fence:LeaseFence,signal?:AbortSignal * whatever the epoch: its outcome is unknown until its own host reconciles it, and a takeover never replays it. */ export class ReferenceEffectHost implements EffectHost { readonly effectFencing='enforced' as const - private readonly admitted=new Set() - private tail:Promise=Promise.resolve() + // #253: true private fields; replacing a same-named property after admission changes nothing the host enforces. + readonly #authority:LeaseAuthority + readonly #admitted=new Set() + #tail:Promise=Promise.resolve() /** Every refusal this host gave, as its versioned record, in order. */ readonly refusals:{version:1;reason:LeaseRefusalReason}[]=[] - constructor(private readonly authority:LeaseAuthority){ + constructor(authority:LeaseAuthority){ + this.#authority=authority // #253: what admission relies on is sealed on the instance: its dispatch and declaration cannot be swapped afterwards. Object.defineProperty(this,'dispatch',{value:ReferenceEffectHost.prototype.dispatch,writable:false,configurable:false,enumerable:false}) Object.defineProperty(this,'effectFencing',{value:'enforced',writable:false,configurable:false,enumerable:true}) if(Object.getPrototypeOf(this)===ReferenceEffectHost.prototype)references.set(this,authority) } dispatch(admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise,signal?:AbortSignal):Promise{ - const admitted=this.tail.then(async()=>{ + const admitted=this.#tail.then(async()=>{ try{ signal?.throwIfAborted() const held=readEffectAdmission(admission) if(held.fence.resource!==held.operation.run)throw new LeaseRefused('resource-mismatch') - await this.authority.checkFence(held.fence,signal) - if(this.admitted.has(held.idempotencyKey))throw new LeaseRefused('already-admitted') - this.admitted.add(held.idempotencyKey) + await this.#authority.checkFence(held.fence,signal) + if(this.#admitted.has(held.idempotencyKey))throw new LeaseRefused('already-admitted') + this.#admitted.add(held.idempotencyKey) }catch(error){if(error instanceof LeaseRefused)this.refusals.push(error.record());throw error} }) - this.tail=admitted.catch(()=>undefined) + this.#tail=admitted.catch(()=>undefined) return admitted.then(()=>perform(signal)) } } @@ -257,8 +260,11 @@ export async function certifyEffectHost(create:(authority:LeaseAuthority)=>Effec const bound:LeaseAuthority={checkFence:(fence,signal)=>live?authority.checkFence(fence,signal):kit.checkFence(fence,signal)} let host:EffectHost try{host=await create(bound)}catch{throw new StorageConformanceFailure('host.operation')} + // #253: the dispatch is read once; the kit runs through exactly that function, and only that function is admitted. const dispatch=host.dispatch - await exerciseEffectHost(host,kit) + if(typeof dispatch!=='function')throw new LeaseCapabilityRefused('effect-host-unfenced') + const captured:EffectHost={effectFencing:host.effectFencing,dispatch:(admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise,signal?:AbortSignal)=>dispatch.call(host,admission,perform,signal) as Promise} + await exerciseEffectHost(captured,kit) if(host.dispatch!==dispatch||host.effectFencing!=='enforced')throw new LeaseCapabilityRefused('effect-host-unfenced') live=true const wrapper:EffectHost=Object.freeze({effectFencing:'enforced' as const,dispatch(admission:EffectAdmission,perform:(signal?:AbortSignal)=>Promise,signal?:AbortSignal):Promise{