@nullsquare/agent-authority 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  **Agent Authority turns a human-approved task into temporary execution authority, then keeps that authority bounded as the agent discovers resources, crosses tools, and performs side effects.**
10
10
 
11
- [Task Leases](docs/task-leases.md) · [Validate](docs/validation.md) · [Google proof](docs/live-google-validation.md) · [Integration contract](docs/integration-contract.md) · [CLI](docs/cli.md) · [Architecture](docs/architecture.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
11
+ [Task Leases](docs/task-leases.md) · [Executable evidence](docs/evidence.md) · [Extractor conformance](docs/authority-extractor-conformance.md) · [Transport invariance](docs/transport-invariance.md) · [Google proof](docs/live-google-validation.md) · [Integration contract](docs/integration-contract.md) · [CLI](docs/cli.md) · [Architecture](docs/architecture.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
12
12
 
13
- > **Status: public pre-alpha / v0.4.2 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype, execution-bound derived facts, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, GitHub and Google provider integrations, CI and CodeQL. It is not production-ready yet.
13
+ > **Status: public pre-alpha / v0.4.4 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype, execution-bound derived facts, reviewed Google and GitHub authority extractors, two-provider conformance tests, Task-Lease-aware SDK/MCP/broker execution, a real Vercel AI SDK harness proof, approvals, revocation, idempotency, credential isolation, live GitHub proofs, CI and CodeQL. It is not production-ready yet.
14
14
 
15
15
  </div>
16
16
 
@@ -158,7 +158,11 @@ The demo performs this flow without provider credentials:
158
158
 
159
159
  The side-effect callbacks for blocked actions never run.
160
160
 
161
- The repository also includes a real Gmail → Calendar validation path and a reusable Google provider adapter. The stricter v0.4.2 path binds the derived sender to the exact guarded output before it becomes authority. See [Live Gmail → Calendar validation](docs/live-google-validation.md) and [Executable Evidence](docs/evidence.md).
161
+ The repository also includes a real Gmail → Calendar validation path and a reusable Google provider adapter. The strict path binds the derived sender to the exact guarded output before it becomes authority. See [Live Gmail → Calendar validation](docs/live-google-validation.md) and [Executable Evidence](docs/evidence.md).
162
+
163
+ v0.4.3 applied the **same evidence-derived authority primitive to GitHub**: a root-bound repository + fixture marker are used by the reviewed GitHub adapter to select one issue from a real `issue.list` response; `deriveFromEvidence()` establishes that exact issue number as downstream authority; one real comment mutation succeeds; unrelated and post-completion issue mutations never reach the provider. Google and GitHub are exercised by the same [authority extractor conformance contract](docs/authority-extractor-conformance.md).
164
+
165
+ v0.4.4 adds the **transport-invariance proof**: one `execution-evidence-v1` derived fact is established under one Task Lease, then that exact lease and fact constrain ordinary `guard.run()`, the MCP gateway and brokered provider execution. The same allowed resource succeeds, an unrelated resource produces `authority_delta_required` with zero blocked callbacks/provider calls, and task completion produces `task_lease_completed` across all three paths. The repository also drives a real Vercel AI SDK `ToolLoopAgent` through `protectAiSdkTools()`: unrelated resources, unmapped executable tools and completed leases are surfaced as tool errors while the underlying side-effect counters remain zero. See [Task Lease transport invariance](docs/transport-invariance.md).
162
166
 
163
167
  ## Minimal developer API
164
168
 
@@ -228,7 +232,7 @@ Agent Authority is deliberately **not tied to MCP, OAuth, or one agent framework
228
232
  agent code -> guard.run() -> existing SDK / API
229
233
  ```
230
234
 
231
- Best when the application already owns the provider connection. This is the primary v0.4 adoption path.
235
+ Best when the application already owns the provider connection.
232
236
 
233
237
  ### 2. MCP gateway
234
238
 
@@ -246,7 +250,7 @@ agent -> Agent Authority -> isolated credential -> provider
246
250
 
247
251
  Best when the agent should not receive the provider credential at all.
248
252
 
249
- The long-term validation target is the **same Task Lease and authority lineage across all three paths**.
253
+ v0.4.4 demonstrates the **same Task Lease and authority fact across all three paths in-process**, plus the configured Vercel AI SDK `ToolLoopAgent` protected-tool path. M4 is complete at this execution-boundary level. A malicious host that deliberately exposes a separate unguarded tool or credential remains outside Agent Authority's security boundary.
250
254
 
251
255
  ## What is implemented
252
256
 
@@ -264,6 +268,8 @@ The long-term validation target is the **same Task Lease and authority lineage a
264
268
  - execution evidence binding an allowed receipt, request and exact output hash
265
269
  - strict `deriveFromEvidence()` path where the caller cannot provide the authority value
266
270
  - reviewed Gmail sender authority extractor bound to `gmail:thread.read`
271
+ - reviewed GitHub selected-issue-number extractor bound to marker-scoped `github:issue.list`
272
+ - shared Google/GitHub authority-extractor conformance suite
267
273
  - legacy host-trusted `derive()` compatibility path
268
274
  - required parent lineage and extraction selector
269
275
  - exact context-field bindings
@@ -276,6 +282,12 @@ The long-term validation target is the **same Task Lease and authority lineage a
276
282
  - protocol-neutral `guard.run()` wrapper
277
283
  - blocked side effects never invoke their callback
278
284
  - successful guarded effects return separate execution evidence
285
+ - Task-Lease-aware MCP gateway/proxy evaluation
286
+ - Task-Lease-aware brokered execution via `ExecutingAuthorityRuntime.executeTaskLease()`
287
+ - brokered execution evidence bound to Task-Lease receipts
288
+ - SDK/MCP/broker transport-invariance conformance test
289
+ - real Vercel AI SDK `ToolLoopAgent` protected-tool harness proof
290
+ - unmapped executable AI SDK tools fail closed before their effect executes
279
291
  - one-time human approvals bound to exact request
280
292
  - mutation idempotency
281
293
  - conservative uncertain-state handling
@@ -288,6 +300,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
288
300
  - AES-256-GCM local encrypted secret store
289
301
  - safe reconnect cleanup
290
302
  - GitHub brokered execution without returning the token to the agent
303
+ - GitHub REST mappings for repository access plus evidence-derived `issue.list` / `issue.comment`
291
304
  - Google REST provider mappings for Gmail thread reads and Calendar event mutations
292
305
  - short-lived signed local agent-instance tokens
293
306
  - local CLI/daemon
@@ -296,11 +309,14 @@ The long-term validation target is the **same Task Lease and authority lineage a
296
309
 
297
310
  - adversarial authorization tests
298
311
  - execution-evidence substitution, tampering, replay, cross-lease and selector tests
312
+ - the same provider-derived-authority conformance attacks against Google and GitHub
313
+ - cross-transport invariance test for direct SDK, MCP and brokered execution
314
+ - real AI SDK agent-loop tests for unauthorized, unmapped and completed-lease tool calls with zero underlying effects
299
315
  - Node 20 and Node 22 CI
300
316
  - coverage run
301
317
  - package checks
302
318
  - clean-consumer npm registry verification
303
- - live GitHub read and mutation proofs
319
+ - live GitHub read and evidence-derived mutation proofs
304
320
  - CodeQL
305
321
 
306
322
  ## What is different from OAuth, IAM and MCP authorization?
@@ -371,6 +387,7 @@ See [SECURITY.md](SECURITY.md).
371
387
  This is still a validation implementation.
372
388
 
373
389
  - Task Lease state is currently process-local.
390
+ - Transport and harness proofs cover configured Agent Authority execution boundaries, not a malicious host that exposes a separate unguarded tool, credential, shell or network path.
374
391
  - `deriveFromEvidence()` proves consistency with the exact output returned through the trusted Agent Authority guard, but the output is not cryptographically attested by Gmail, GitHub, or another remote provider.
375
392
  - The legacy `derive()` API remains host-trusted for compatibility; audit provenance distinguishes it from `execution-evidence-v1` derivation.
376
393
  - Source-data changes do not yet automatically invalidate already-derived authority facts.
package/ROADMAP.md CHANGED
@@ -64,18 +64,33 @@ never silently grow
64
64
 
65
65
  The core cross-provider behavior is now implemented and exercised against connected Gmail and Calendar accounts. Public Actions reproducibility remains the final M1 evidence gate because GitHub cannot reuse an interactive ChatGPT connector credential; the manual workflow is committed and expects a refresh token stored only as repository secrets.
66
66
 
67
- ## M2 — Durable task execution
67
+ ## M2 — Durable task execution — durable session established
68
68
 
69
- Build only what the real M1 workflow proves necessary.
69
+ Build only what the real Task Lease workflows prove necessary.
70
70
 
71
- - [ ] durable Task Lease persistence/recovery
72
- - [ ] atomic fact/binding updates
71
+ - [x] authenticated local Task Lease persistence/recovery
72
+ - [x] atomic authenticated fact/binding/status transaction primitive
73
+ - [x] stale-writer compare-and-swap protection for independent recovered worker views
74
+ - [x] local per-lease transaction lock that fails closed on overlap
75
+ - [x] ordinary running Task Lease mutations can use the durable transaction boundary through `DurableTaskLeaseSession`
76
+ - [x] security-critical session evaluation refreshes current durable state before the next authority decision
73
77
  - [ ] approved authority delta can safely attenuate/update a live lease
74
- - [ ] completion state survives process restart
78
+ - [x] completion state survives process restart
75
79
  - [ ] durable lineage query: why was this exact action authorized?
76
- - [ ] concurrency tests for multiple agent workers operating under one lease
80
+ - [ ] multi-process stress/recovery tests for multiple agent workers operating under one lease
81
+ - [ ] crash-safe coupling between remote provider side effects, receipts and durable Task Lease state
77
82
 
78
- **Success criterion:** a Task Lease survives daemon/process restarts without gaining authority or losing its provenance lineage.
83
+ `JsonFileTaskLeaseStore` writes the complete lease snapshot atomically and authenticates it with a purpose-derived HMAC key from the local Agent Authority master key. Recovery verifies the envelope before authority hydration, binds the snapshot to the exact mission hash, validates status/timestamps and the authority-fact DAG, restores evidence-derived provenance hashes, and verifies the reconstructed Task Lease hash.
84
+
85
+ `JsonFileTaskLeaseStore.transact()` is the local durable mutation boundary: acquire one per-lease filesystem lock, reload authenticated current state, compare an optional expected lease hash, apply one synchronous Task Lease mutation, validate the complete authority graph, and atomically replace the authenticated snapshot. Stale writers receive `task_lease_state_conflict`; overlapping local transactions receive `task_lease_state_locked`.
86
+
87
+ `DurableTaskLeaseSession` is the normal mutation facade over that primitive. `addRoot()`, legacy `derive()`, strict `deriveFromEvidence()`, `bind()` and `complete()` commit through compare-and-swap and update the session only after durable success. The session exposes detached mission/snapshot/fact views rather than its mutable internal Task Lease. It never auto-replays a stale semantic mutation after a conflict.
88
+
89
+ The session also implements the Task Lease `evaluate(runtime, request)` shape used by guard/MCP/broker paths. Evaluation refreshes authenticated durable state first, so another worker's already-committed completion or narrowing is observed before the next decision. Tests also prove that execution evidence captured at H0 cannot be automatically converted into a derived fact after another worker commits H1; derivation fails on CAS and requires explicit reconsideration.
90
+
91
+ This does **not** yet turn a remote provider side effect and a local Task Lease transition into one distributed transaction. A different worker can still change durable state after an ALLOW decision and before an asynchronous provider effect begins. That TOCTOU/effect-coupling problem remains explicit follow-on M2 work rather than being hidden behind the local filesystem lock. See `docs/durable-task-leases.md`.
92
+
93
+ **Success criterion:** a Task Lease survives restart without gaining authority or losing provenance, cooperating local workers cannot silently overwrite newer authority, and ordinary durable mutations use the CAS boundary by default through the session API. Authenticated recovery, local transactions and the durable session are now demonstrated; approved deltas, stronger multi-process stress and remote-effect coupling remain open.
79
94
 
80
95
  ## M3 — Trustworthy derived facts — two-provider proof established
81
96
 
@@ -97,17 +112,25 @@ Do **not** build a general semantic policy language unless real integrations req
97
112
 
98
113
  **Success criterion:** provider-derived authority cannot be established through the strict path unless the exact guarded output, ALLOW receipt and reviewed extractor contract agree on the selected value. This behavior is now exercised across two provider mappings. Stronger provider attestation and source invalidation remain separate follow-on problems.
99
114
 
100
- ## M4 — Same task, multiple transports
115
+ ## M4 — Same task, multiple transports — complete
101
116
 
102
117
  Prove Agent Authority is not an MCP product or SDK wrapper.
103
118
 
104
- - [ ] same Task Lease through ordinary `guard.run()` SDK call
105
- - [ ] same Task Lease through MCP gateway
106
- - [ ] same Task Lease through brokered provider execution
107
- - [ ] at least one non-bypassable harness/tool-middleware integration
108
- - [ ] interoperability test vectors across transports
119
+ - [x] same Task Lease through ordinary `guard.run()` SDK call
120
+ - [x] same Task Lease through MCP gateway
121
+ - [x] same Task Lease through brokered provider execution
122
+ - [x] at least one harness/tool-middleware integration whose configured executable tool path cannot bypass the Task Lease
123
+ - [x] interoperability test vectors across transports
124
+
125
+ `test/transport-invariance.test.js` establishes one `execution-evidence-v1` derived fact from brokered execution, then reuses that exact Task Lease and fact through direct SDK, MCP and brokered execution. The three paths produce the same `allow`, `authority_delta_required` and `task_lease_completed` outcomes, and blocked attempts execute zero host callbacks, MCP upstream calls or brokered provider operations.
126
+
127
+ `test/integrations/ai-sdk.integration.mjs` drives the real Vercel AI SDK `ToolLoopAgent` through the protected tool set. An authorized task-bound tool executes once; an unrelated resource, an executable tool with no Agent Authority mapping, and a completed Task Lease are surfaced by the harness as `tool-error` results while the underlying side-effect counters remain zero. This proves the configured AI SDK tool execution path cannot silently bypass the Task Lease.
128
+
129
+ The harness claim is intentionally bounded: a malicious host that deliberately gives the model a separate unguarded executable channel remains outside Agent Authority's enforcement boundary.
130
+
131
+ Brokered Task Lease execution deliberately does not consume mission-level one-time approval to override a lease-level authority delta. Updating a live Task Lease after explicit approval remains separate M2 work.
109
132
 
110
- **Success criterion:** changing transport or harness does not expand the task's authority.
133
+ **Success criterion:** changing transport or the configured harness execution path does not expand the task's authority. This is now demonstrated across direct SDK, MCP, brokered execution and the Vercel AI SDK `ToolLoopAgent` protected-tool path.
111
134
 
112
135
  ## M5 — Production credential and approval UX
113
136
 
@@ -157,4 +180,4 @@ Only after operational evidence.
157
180
  2. How should an approved authority delta update a running task without opening a broader wildcard permission?
158
181
  3. How should source-data changes invalidate downstream derived authority?
159
182
  4. What provider/tool metadata is required to map operations to resource context reliably?
160
- 5. Can the same non-amplification conformance suite work across SDK, MCP, CLI and brokered execution?
183
+ 5. How should a local ALLOW decision be coupled to an asynchronous remote effect without holding a filesystem lease lock across network I/O?
@@ -0,0 +1,336 @@
1
+ # Durable Task Leases
2
+
3
+ Agent Authority v0.4.x originally kept Task Lease state only in process memory. That was useful for proving task-bounded authority, but process restart could lose task completion, expiry state, bindings and provenance lineage.
4
+
5
+ M2 now has three local-host durability layers:
6
+
7
+ 1. authenticated recovery of Task Lease authority state;
8
+ 2. transactional mutation with local serialization and stale-writer compare-and-swap protection;
9
+ 3. `DurableTaskLeaseSession`, which routes normal Task Lease mutations through that transaction boundary automatically.
10
+
11
+ The core authority model is unchanged.
12
+
13
+ ## Security goal
14
+
15
+ Restart or concurrent local workers must never increase authority by reconstructing, overwriting or racing durable state.
16
+
17
+ ```text
18
+ live Task Lease
19
+ |
20
+ v
21
+ atomic authenticated snapshot
22
+ |
23
+ process restart
24
+ |
25
+ v
26
+ authenticate + validate + recover
27
+ |
28
+ v
29
+ recovered Task Lease authority
30
+ <=
31
+ pre-restart Task Lease authority
32
+ ```
33
+
34
+ For updates:
35
+
36
+ ```text
37
+ worker view @ hash H0
38
+ |
39
+ v
40
+ acquire per-lease lock
41
+ |
42
+ reload authenticated current state
43
+ |
44
+ expected hash == current hash ?
45
+ |
46
+ yes
47
+ v
48
+ apply one synchronous lease mutation
49
+ |
50
+ validate complete authority snapshot
51
+ |
52
+ atomic authenticated replacement -> H1
53
+ ```
54
+
55
+ A stale worker that still expects `H0` after another worker committed `H1` receives `task_lease_state_conflict`. It does not overwrite `H1`.
56
+
57
+ A state file is not authority merely because it contains plausible JSON.
58
+
59
+ ## Snapshot contents
60
+
61
+ `TaskLease.snapshot()` preserves:
62
+
63
+ - lease ID;
64
+ - exact mission ID and mission hash;
65
+ - principal and agent IDs;
66
+ - original task request;
67
+ - active/completed status;
68
+ - creation, expiry and completion timestamps;
69
+ - completion reason;
70
+ - exact context bindings;
71
+ - root authority facts;
72
+ - derived facts and parent lineage;
73
+ - strict execution-evidence provenance, including receipt, request, provider-output and evidence hashes.
74
+
75
+ Snapshots do **not** contain provider credentials.
76
+
77
+ ## Recovery validation
78
+
79
+ `TaskLease.restore()` / `restoreTaskLease()` validate the recovered state before reconstructing a lease.
80
+
81
+ Recovery rejects:
82
+
83
+ - a different mission ID;
84
+ - the same mission ID paired with a different mission definition/hash;
85
+ - a different principal or agent;
86
+ - unsupported snapshot versions;
87
+ - invalid active/completed state;
88
+ - invalid timestamps;
89
+ - duplicate authority fact IDs;
90
+ - missing parent facts;
91
+ - cyclic authority lineage;
92
+ - derived facts claiming another Task Lease ID;
93
+ - derived facts missing receipt/request/selector provenance;
94
+ - `execution-evidence-v1` facts missing extractor/output/evidence hashes.
95
+
96
+ `restoreTaskLease()` validates state structure and mission identity, but it does not authenticate where arbitrary caller-supplied JSON came from. Durable applications should load persisted authority through an authenticated store.
97
+
98
+ ## Authenticated local store
99
+
100
+ `JsonFileTaskLeaseStore` is the first reference persistence backend.
101
+
102
+ ```js
103
+ import { JsonFileTaskLeaseStore } from '@nullsquare/agent-authority/storage';
104
+
105
+ const store = new JsonFileTaskLeaseStore({
106
+ dir: config.paths.task_leases,
107
+ keyPath: config.paths.master_key
108
+ });
109
+
110
+ const created = store.save(lease);
111
+
112
+ // Later, after process restart:
113
+ const recovered = store.load({
114
+ mission,
115
+ lease_id: leaseId
116
+ });
117
+ ```
118
+
119
+ The store snapshots the whole lease, records its hash, authenticates the envelope with HMAC-SHA256 using a purpose-derived local key, atomically writes it, then verifies identity/MAC/mission hash/fact graph/reconstructed lease hash before returning recovered authority.
120
+
121
+ The default local configuration reserves:
122
+
123
+ ```text
124
+ ~/.agent-authority/state/task-leases/
125
+ ```
126
+
127
+ ## Save semantics
128
+
129
+ `save()` is deliberately not last-writer-wins.
130
+
131
+ For a lease that does not yet exist, `store.save(lease)` creates the authenticated durable state.
132
+
133
+ For an existing lease:
134
+
135
+ - saving an unchanged recovered state is idempotent;
136
+ - replacing changed state requires `expected_lease_hash`;
137
+ - attempting to replace changed state without an expected hash fails with `task_lease_state_conflict`.
138
+
139
+ For normal authority mutations, prefer `transact()` or `DurableTaskLeaseSession` rather than mutating a recovered lease and then saving it.
140
+
141
+ ## Transactional mutation
142
+
143
+ `store.transact()` is the low-level durable read-modify-write boundary.
144
+
145
+ ```js
146
+ const view = store.load({ mission, lease_id });
147
+
148
+ const committed = store.transact({
149
+ mission,
150
+ lease_id,
151
+ expected_lease_hash: view.hash(),
152
+ mutate: (lease) => {
153
+ lease.addRoot({
154
+ fact_id: 'fact:region',
155
+ kind: 'demo.region',
156
+ value: 'us-east'
157
+ });
158
+
159
+ lease.bind({
160
+ service: 'demo',
161
+ action: 'item.access',
162
+ context_field: 'region',
163
+ fact_id: 'fact:region'
164
+ });
165
+ }
166
+ });
167
+ ```
168
+
169
+ A transaction acquires a per-lease local lock, reloads/authenticates current state, checks the expected hash, applies one synchronous mutation, validates the full authority graph, atomically writes a new authenticated envelope, and returns the previous/new hashes. Throwing or async callbacks do not persist mutated state.
170
+
171
+ The mutation callback should modify **Task Lease state only**. Provider calls, network requests and other external side effects do not belong inside this synchronous local transaction.
172
+
173
+ ## DurableTaskLeaseSession
174
+
175
+ Most callers should not manually orchestrate `load()` + `transact()` for ordinary Task Lease changes. `DurableTaskLeaseSession` keeps the current recovered hash and applies normal Task Lease mutations through compare-and-swap automatically.
176
+
177
+ ```js
178
+ import {
179
+ createDurableTaskLeaseSession,
180
+ openDurableTaskLeaseSession
181
+ } from '@nullsquare/agent-authority/durable-task-lease';
182
+ import { createTaskLeaseGuard } from '@nullsquare/agent-authority/guard';
183
+
184
+ const session = createDurableTaskLeaseSession({
185
+ store,
186
+ lease
187
+ });
188
+
189
+ const guard = createTaskLeaseGuard({
190
+ lease: session,
191
+ runtime
192
+ });
193
+
194
+ const read = await guard.run(
195
+ {
196
+ service: 'gmail',
197
+ action: 'thread.read',
198
+ context: { thread_id: 'thread:demo-91' }
199
+ },
200
+ () => gmail.readThread('thread:demo-91')
201
+ );
202
+
203
+ session.deriveFromEvidence({
204
+ fact_id: 'fact:sender-email',
205
+ kind: 'email.address',
206
+ from: ['fact:thread'],
207
+ receipt: read.receipt,
208
+ evidence: read.evidence,
209
+ output: read.output,
210
+ extractor: gmailThreadSenderAuthorityExtractor
211
+ });
212
+ ```
213
+
214
+ The session exposes durable equivalents of the ordinary state-changing methods:
215
+
216
+ - `addRoot()`;
217
+ - legacy `derive()`;
218
+ - `deriveFromEvidence()`;
219
+ - `bind()`;
220
+ - `complete()`.
221
+
222
+ Each mutation uses the session's current lease hash as `expected_lease_hash`. On success the session adopts the committed lease and new hash. On conflict it does **not** auto-retry or replay the mutation. The caller must `refresh()` and reconsider the intended change against the newer authority state.
223
+
224
+ The session does not expose its mutable internal `TaskLease`. `mission`, snapshots and facts are returned as detached values so mutating a caller-visible object does not mutate durable authority by reference.
225
+
226
+ ### Guard behavior
227
+
228
+ `DurableTaskLeaseSession` implements `evaluate(runtime, request)`, so it can be passed anywhere a Task Lease is accepted by the current guard/MCP/broker interfaces.
229
+
230
+ Security-critical `evaluate()` calls `refresh()` first. That means a stale worker which another worker has already completed or narrowed will observe the durable state before the next authority decision.
231
+
232
+ Example:
233
+
234
+ ```text
235
+ worker B cached active H0
236
+ worker A complete() -> H1 completed
237
+ worker B guard.run(...)
238
+ -> session.evaluate()
239
+ -> refresh H1
240
+ -> task_lease_completed
241
+ -> effect callback never runs
242
+ ```
243
+
244
+ ### Evidence race behavior
245
+
246
+ Suppose worker B receives an ALLOW receipt and execution evidence at H0, but worker A changes durable Task Lease state to H1 before B calls `deriveFromEvidence()`.
247
+
248
+ The session does not silently derive against H1:
249
+
250
+ ```text
251
+ B guarded read @ H0 -> receipt + evidence
252
+ A commits authority state H1
253
+ B deriveFromEvidence(... expected H0 ...)
254
+ -> task_lease_state_conflict
255
+ -> no derived fact persisted
256
+ ```
257
+
258
+ B must refresh and explicitly reconsider whether the old provider result still justifies a derived authority fact under H1.
259
+
260
+ ## Local worker concurrency
261
+
262
+ The local lock prevents two cooperating Agent Authority processes from entering the same per-lease durable mutation window at the same time.
263
+
264
+ If a worker observes an already-held lock, it fails closed with:
265
+
266
+ ```text
267
+ task_lease_state_locked
268
+ ```
269
+
270
+ The lock is intentionally local-filesystem scoped. A crashed process may leave a lock directory behind; this is an availability failure rather than an authority-expansion failure and should be repaired explicitly rather than silently deleting a lock that might still belong to a live worker.
271
+
272
+ The expected lease hash adds optimistic stale-view protection:
273
+
274
+ ```text
275
+ worker A loads H0
276
+ worker B loads H0
277
+ worker A transact(H0) -> H1
278
+ worker B transact(H0) -> task_lease_state_conflict
279
+ ```
280
+
281
+ Worker B must reload H1 and reconsider its intended mutation against the new authority state.
282
+
283
+ ## Properties under test
284
+
285
+ `test/task-lease-persistence.test.js` and `test/durable-task-lease-session.test.js` prove that:
286
+
287
+ - strict `execution-evidence-v1` facts survive recovery with provenance hashes unchanged;
288
+ - unrelated resources still require step-up after restart;
289
+ - completion and expiry survive restart;
290
+ - disk tampering, changed mission definitions and malformed lineage fail closed;
291
+ - fact+binding changes can commit as one authenticated snapshot;
292
+ - stale worker views cannot overwrite newer authority;
293
+ - raw changed saves cannot bypass compare-and-swap;
294
+ - overlapping local transactions fail closed;
295
+ - throwing or async transactions leave durable state unchanged;
296
+ - a durable session can perform strict evidence derivation and reopen with identical authority behavior;
297
+ - another worker's completion is observed before the stale session's next guarded effect;
298
+ - stale semantic mutations are not automatically replayed;
299
+ - evidence captured at H0 cannot be automatically derived after another worker commits H1;
300
+ - caller-visible mission/snapshot objects do not expose mutable aliases to the session's internal authority state.
301
+
302
+ ## Trust boundary
303
+
304
+ This persistence mechanism protects authority state on the **trusted local Agent Authority host** against accidental corruption, caller-controlled state-file modification without the authentication key, and stale cooperating local writers.
305
+
306
+ It is not hostile-host containment. An attacker or malicious host process that can read the Agent Authority master key can authenticate modified local state and remains outside this guarantee.
307
+
308
+ The HMAC does not make provider output cryptographically attested by the provider. The per-lease lock is local filesystem coordination, not distributed consensus.
309
+
310
+ ## Important remaining TOCTOU boundary
311
+
312
+ Refreshing before `evaluate()` closes stale-state decisions, but it does **not** make an asynchronous external provider effect and Task Lease state transition one distributed transaction.
313
+
314
+ There is still a possible sequence:
315
+
316
+ ```text
317
+ worker B refresh/evaluate -> ALLOW
318
+ worker A completes lease
319
+ worker B provider effect begins
320
+ ```
321
+
322
+ The ordinary guard correctly checks authority immediately before invoking its effect callback, but another process can change durable state after that decision. Agent Authority does not yet hold the local lease lock across a network/provider effect, and it should not do so casually.
323
+
324
+ Crash-safe/provider-side coordination, idempotency and execution receipts need a dedicated design rather than pretending a filesystem transaction covers remote side effects.
325
+
326
+ ## What is not durable yet
327
+
328
+ Still open:
329
+
330
+ - crash-safe coupling between provider side effects, execution receipts and Task Lease state changes;
331
+ - durable application of an explicitly approved authority delta;
332
+ - a durable lineage query/index across many leases;
333
+ - stronger multi-process stress tests and recovery tooling for abandoned local locks;
334
+ - remote/KMS-backed persistence and distributed coordination where required.
335
+
336
+ The next M2 work should tackle approved authority deltas and execution/effect coupling as separate explicit problems rather than broadening the persistence layer into a general database abstraction.
@@ -13,19 +13,26 @@ Before any publication:
13
13
  After publication, verify from a fresh project with:
14
14
 
15
15
  ```bash
16
- npm install @nullsquare/agent-authority@0.4.2
16
+ npm install @nullsquare/agent-authority@0.4.4
17
17
  ```
18
18
 
19
19
  Then run the same consumer smoke flow through the registry-installed package. Registry verification is part of the release gate; a successful `npm publish` command alone is not sufficient.
20
20
 
21
- The repository also includes `.github/workflows/verify-npm-registry.yml`, which verifies registry visibility and a clean consumer install. For v0.4.2 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` export plus the Google provider and Gmail authority-extractor exports.
21
+ The repository also includes `.github/workflows/verify-npm-registry.yml`, which verifies registry visibility and a clean consumer install. For v0.4.4 it verifies the public execution-evidence and two-provider authority-extractor APIs plus the Task-Lease-aware transport surfaces:
22
+
23
+ - `gmailThreadSenderAuthorityExtractor` from `@nullsquare/agent-authority/providers/google`;
24
+ - `githubIssueListSelectedNumberAuthorityExtractor` from `@nullsquare/agent-authority/providers/github`;
25
+ - `ExecutingAuthorityRuntime.executeTaskLease()` from `@nullsquare/agent-authority/execution`;
26
+ - `MissionMcpGateway` from `@nullsquare/agent-authority/mcp-gateway`.
27
+
28
+ This makes the registry artifact verification cover both the two-provider execution-evidence surface and the SDK/MCP/broker transport-invariance surface exercised by the repository conformance tests.
22
29
 
23
30
  ## npm vs GitHub release surfaces
24
31
 
25
32
  Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
26
33
 
27
34
  - **npm registry** — `npm publish --access public` publishes `@nullsquare/agent-authority` to `registry.npmjs.org` / npmjs.com. This is the package users install with `npm install`.
28
- - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.2`. A release must be created explicitly or by release automation.
35
+ - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.4`. A release must be created explicitly or by release automation.
29
36
  - **GitHub Packages** — a separate package registry. It only appears when the package is published to GitHub's npm registry (`npm.pkg.github.com`); publishing to npmjs.com does not populate it.
30
37
 
31
38
  Agent Authority currently uses npmjs.com as its public package registry. Therefore an empty GitHub **Packages** section is expected unless the project intentionally adopts dual publication. A GitHub **Release** is still useful for source-release discoverability and should track published versions, but it is independent from npm publication.