Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions docs/design/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,24 @@ 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 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: 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
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
Expand All @@ -364,5 +375,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.
20 changes: 10 additions & 10 deletions docs/reference/events.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions go/adapter/agent/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions go/adapter/agent/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
181 changes: 134 additions & 47 deletions go/context/run_lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/Judgment-Pack/judgment-pack-agent/go/contract"
Expand Down Expand Up @@ -546,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
Expand All @@ -554,33 +556,52 @@ type ReferenceEffectHost struct {

func NewReferenceEffectHost(authority LeaseAuthority) *ReferenceEffectHost {
host := &ReferenceEffectHost{authority: authority, admitted: map[string]bool{}}
provenHosts.Store(host, true)
host.self = host
return host
}

// 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")
}
Expand All @@ -597,18 +618,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")
}
Expand All @@ -633,7 +666,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")
}
Expand All @@ -647,9 +680,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")
}
Expand All @@ -659,31 +690,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.self == h && 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.
Expand Down Expand Up @@ -788,7 +873,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)
Expand All @@ -798,7 +884,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
Expand Down
Loading
Loading