@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 +16 -5
- package/ROADMAP.md +30 -11
- package/docs/durable-task-leases.md +336 -0
- package/docs/npm-release.md +7 -5
- package/docs/transport-invariance.md +46 -10
- package/package.json +3 -2
- package/src/durable-task-lease.js +185 -0
- package/src/storage.js +298 -9
- package/src/task-lease.js +243 -1
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
69
|
+
Build only what the real Task Lease workflows prove necessary.
|
|
70
70
|
|
|
71
|
-
- [
|
|
72
|
-
- [
|
|
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
|
-
- [
|
|
78
|
+
- [x] completion state survives process restart
|
|
75
79
|
- [ ] durable lineage query: why was this exact action authorized?
|
|
76
|
-
- [ ]
|
|
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
|
-
|
|
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 —
|
|
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
|
-
- [
|
|
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.
|
|
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.
|
|
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.
|
package/docs/npm-release.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
105
|
-
- one derived fact can constrain
|
|
106
|
-
- task completion invalidates the same authority across
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|