@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 +23 -6
- package/ROADMAP.md +38 -15
- package/docs/durable-task-leases.md +336 -0
- package/docs/npm-release.md +10 -3
- package/docs/transport-invariance.md +153 -0
- package/package.json +3 -2
- package/src/durable-task-lease.js +185 -0
- package/src/execution.js +34 -3
- package/src/mcp-gateway.js +35 -12
- package/src/mcp-remote.js +5 -1
- package/src/mcp-server.js +3 -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) · [
|
|
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
|
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,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
|
-
- [
|
|
105
|
-
- [
|
|
106
|
-
- [
|
|
107
|
-
- [
|
|
108
|
-
- [
|
|
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.
|
|
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,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.
|
|
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
|
+
|
|
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.
|
|
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.
|