@nullsquare/agent-authority 0.4.4 → 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) · [Executable evidence](docs/evidence.md) · [Extractor conformance](docs/authority-extractor-conformance.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.3 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, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, live GitHub proofs, 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
 
@@ -160,7 +160,9 @@ The side-effect callbacks for blocked actions never run.
160
160
 
161
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
162
 
163
- v0.4.3 applies the **same 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 now exercised by the same [authority extractor conformance contract](docs/authority-extractor-conformance.md).
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).
164
166
 
165
167
  ## Minimal developer API
166
168
 
@@ -230,7 +232,7 @@ Agent Authority is deliberately **not tied to MCP, OAuth, or one agent framework
230
232
  agent code -> guard.run() -> existing SDK / API
231
233
  ```
232
234
 
233
- 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.
234
236
 
235
237
  ### 2. MCP gateway
236
238
 
@@ -248,7 +250,7 @@ agent -> Agent Authority -> isolated credential -> provider
248
250
 
249
251
  Best when the agent should not receive the provider credential at all.
250
252
 
251
- 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.
252
254
 
253
255
  ## What is implemented
254
256
 
@@ -280,6 +282,12 @@ The long-term validation target is the **same Task Lease and authority lineage a
280
282
  - protocol-neutral `guard.run()` wrapper
281
283
  - blocked side effects never invoke their callback
282
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
283
291
  - one-time human approvals bound to exact request
284
292
  - mutation idempotency
285
293
  - conservative uncertain-state handling
@@ -302,6 +310,8 @@ The long-term validation target is the **same Task Lease and authority lineage a
302
310
  - adversarial authorization tests
303
311
  - execution-evidence substitution, tampering, replay, cross-lease and selector tests
304
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
305
315
  - Node 20 and Node 22 CI
306
316
  - coverage run
307
317
  - package checks
@@ -377,6 +387,7 @@ See [SECURITY.md](SECURITY.md).
377
387
  This is still a validation implementation.
378
388
 
379
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.
380
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.
381
392
  - The legacy `derive()` API remains host-trusted for compatibility; audit provenance distinguishes it from `execution-evidence-v1` derivation.
382
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,21 +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 — first proof established
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
119
  - [x] same Task Lease through ordinary `guard.run()` SDK call
105
120
  - [x] same Task Lease through MCP gateway
106
121
  - [x] same Task Lease through brokered provider execution
107
- - [ ] at least one non-bypassable harness/tool-middleware integration
122
+ - [x] at least one harness/tool-middleware integration whose configured executable tool path cannot bypass the Task Lease
108
123
  - [x] interoperability test vectors across transports
109
124
 
110
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.
111
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
+
112
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.
113
132
 
114
- **Success criterion:** changing transport or harness does not expand the task's authority. The SDK/MCP/broker portion is now demonstrated in-process; an external non-bypassable harness/tool-middleware integration remains the final M4 proof.
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.
115
134
 
116
135
  ## M5 — Production credential and approval UX
117
136
 
@@ -161,4 +180,4 @@ Only after operational evidence.
161
180
  2. How should an approved authority delta update a running task without opening a broader wildcard permission?
162
181
  3. How should source-data changes invalidate downstream derived authority?
163
182
  4. What provider/tool metadata is required to map operations to resource context reliably?
164
- 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,24 +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.3
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.3 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` API plus both reviewed provider authority extractors:
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
22
 
23
23
  - `gmailThreadSenderAuthorityExtractor` from `@nullsquare/agent-authority/providers/google`;
24
- - `githubIssueListSelectedNumberAuthorityExtractor` from `@nullsquare/agent-authority/providers/github`.
24
+ - `githubIssueListSelectedNumberAuthorityExtractor` from `@nullsquare/agent-authority/providers/github`;
25
+ - `ExecutingAuthorityRuntime.executeTaskLease()` from `@nullsquare/agent-authority/execution`;
26
+ - `MissionMcpGateway` from `@nullsquare/agent-authority/mcp-gateway`.
25
27
 
26
- This makes the registry artifact verification cover the same two-provider execution-evidence surface exercised by the repository conformance suite.
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.
27
29
 
28
30
  ## npm vs GitHub release surfaces
29
31
 
30
32
  Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
31
33
 
32
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`.
33
- - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.3`. 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.
34
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.
35
37
 
36
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.
@@ -19,7 +19,7 @@ same Task Lease + same established authority fact
19
19
 
20
20
  Changing the transport must not broaden task authority.
21
21
 
22
- ## Executable proof
22
+ ## Executable transport proof
23
23
 
24
24
  `test/transport-invariance.test.js` creates one Task Lease and one derived authority fact.
25
25
 
@@ -97,21 +97,57 @@ io.nullsquare.agent-authority/task_lease_id
97
97
 
98
98
  The remote MCP handler and loopback proxy can pass the same Task Lease into the gateway.
99
99
 
100
+ ## AI SDK harness proof
101
+
102
+ `test/integrations/ai-sdk.integration.mjs` drives the current Vercel AI SDK `ToolLoopAgent` with the output of `protectAiSdkTools()` as its executable tool set.
103
+
104
+ The positive path asks the model to use the task-bound GitHub issue. The protected tool executes exactly once.
105
+
106
+ The same real agent loop then exercises three adversarial paths:
107
+
108
+ ```text
109
+ unrelated issue
110
+ -> AuthorityApprovalRequiredError
111
+ -> authority_delta_required
112
+ -> underlying effect count remains 0
113
+
114
+ executable tool with no Agent Authority mapping
115
+ -> UnmappedAiSdkToolError
116
+ -> ai_sdk_tool_unmapped
117
+ -> underlying effect count remains 0
118
+
119
+ completed Task Lease
120
+ -> AuthorityDeniedError
121
+ -> task_lease_completed
122
+ -> underlying effect count remains 0
123
+ ```
124
+
125
+ The AI SDK represents tool execution failures in the generated result as `tool-error` content parts rather than requiring `agent.generate()` itself to reject. The proof therefore checks both sides of the boundary: the harness records the exact Agent Authority error and the protected underlying side effect never runs.
126
+
127
+ This matters because the model is not calling the wrapped function directly in these cases. The real `ToolLoopAgent` selects and invokes the tool through its normal tool loop, and the Agent Authority wrapper remains the executable boundary.
128
+
100
129
  ## What this proves
101
130
 
102
- - Task-Lease narrowing is no longer specific to the direct SDK guard;
131
+ - Task-Lease narrowing is not specific to the direct SDK guard;
103
132
  - MCP cannot silently fall back to Mission-only authority when explicitly configured with a Task Lease;
104
- - brokered provider execution can enforce the same Task Lease before credential-backed execution;
105
- - one derived fact can constrain all three execution paths;
106
- - task completion invalidates the same authority across all three paths;
107
- - broker credentials may remain connected after task authority disappears.
133
+ - brokered provider execution enforces the same Task Lease before credential-backed execution;
134
+ - one evidence-derived fact can constrain direct SDK, MCP and brokered execution;
135
+ - task completion invalidates the same authority across those execution paths;
136
+ - broker credentials may remain connected after task authority disappears;
137
+ - a configured Vercel AI SDK `ToolLoopAgent` whose executable tool set is passed through `protectAiSdkTools()` cannot use its normal tool path to bypass Task-Lease narrowing;
138
+ - executable AI SDK tools without an Agent Authority request mapping fail closed before their underlying effect executes.
139
+
140
+ ## Boundary of the claim
141
+
142
+ This is an execution-boundary guarantee, not hostile-host containment.
143
+
144
+ It does **not** prove that a malicious application host cannot deliberately give the model another unwrapped tool, direct provider credential, shell, network client or other execution channel outside Agent Authority.
108
145
 
109
- ## What this does not prove yet
146
+ It also does not yet prove that:
110
147
 
111
- - a hostile harness cannot bypass Agent Authority through an entirely separate unguarded tool path;
112
148
  - Task Lease state survives process restart;
113
- - the same lease is serialized and recovered across separate processes or hosts;
149
+ - the same lease can be serialized and safely recovered across separate processes or hosts;
114
150
  - an approved authority delta is durably applied back into a running lease;
115
151
  - provider outputs are cryptographically attested by providers.
116
152
 
117
- The remaining M4 target is at least one real harness/tool-middleware integration where executable tool calls cannot bypass the Task Lease boundary.
153
+ M4 is complete for configured Agent Authority execution boundaries: direct SDK, MCP, brokered execution and the Vercel AI SDK `ToolLoopAgent` protected-tool path now preserve task authority without silent expansion. Durability, cross-process recovery and hostile-host containment are separate problems.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Task-bounded authority runtime for AI agents: give agents tasks, not standing account permissions.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "test": "node --test test/*.test.js",
42
42
  "test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
43
43
  "test:coverage": "node --experimental-test-coverage --test test/*.test.js",
44
- "check:syntax": "node --check src/index.js && node --check src/authority-evidence.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/providers/google.js && node --check src/storage.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js && node --check examples/live-google-cross-provider.js",
44
+ "check:syntax": "node --check src/index.js && node --check src/authority-evidence.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/providers/google.js && node --check src/storage.js && node --check src/durable-task-lease.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js && node --check examples/live-google-cross-provider.js",
45
45
  "check:package": "npm pack --dry-run",
46
46
  "check": "npm run check:syntax && npm test && npm run demo:task-lease && npm run check:package"
47
47
  },
@@ -62,6 +62,7 @@
62
62
  "./approvals": "./src/approvals.js",
63
63
  "./authority-evidence": "./src/authority-evidence.js",
64
64
  "./connections": "./src/connections.js",
65
+ "./durable-task-lease": "./src/durable-task-lease.js",
65
66
  "./execution": "./src/execution.js",
66
67
  "./guard": "./src/guard.js",
67
68
  "./harness-bridge": "./src/harness-bridge.js",