@nullsquare/agent-authority 0.4.4 → 0.4.6
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 +245 -289
- package/ROADMAP.md +137 -95
- package/benchmarks/task-utility.mjs +130 -0
- package/docs/durable-task-leases.md +336 -0
- package/docs/npm-release.md +13 -7
- package/docs/product-proof.md +185 -0
- package/docs/transport-invariance.md +46 -10
- package/examples/task-first-github.js +102 -0
- package/package.json +8 -4
- package/src/durable-task-lease.js +185 -0
- package/src/storage.js +298 -9
- package/src/task-lease.js +243 -1
- package/src/task.js +238 -0
|
@@ -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
|
@@ -6,31 +6,37 @@ Before any publication:
|
|
|
6
6
|
|
|
7
7
|
1. the release commit must pass CI, CodeQL, live GitHub validation, current AI SDK integration validation, and packed-consumer validation;
|
|
8
8
|
2. `npm pack` must contain the documented public exports;
|
|
9
|
-
3. a fresh Node.js 20 consumer must install the tarball and run the
|
|
9
|
+
3. a fresh Node.js 20 consumer must install the tarball and run the current public behavior smoke test;
|
|
10
10
|
4. the optional AI SDK integration must import without making `ai` a production dependency;
|
|
11
11
|
5. the registry package must be public and its repository metadata must point to `https://github.com/Null-Square/agent-authority`.
|
|
12
12
|
|
|
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.5
|
|
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
|
|
21
|
+
The repository includes `.github/workflows/verify-npm-registry.yml`, which verifies registry visibility, a clean Node.js 20 install, and the current public behavior from the registry artifact. For v0.4.5 the consumer exercises:
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
- `
|
|
23
|
+
- execution evidence and the reviewed Google/GitHub authority extractors;
|
|
24
|
+
- `ExecutingAuthorityRuntime.executeTaskLease()` and `MissionMcpGateway` transport surfaces;
|
|
25
|
+
- `JsonFileTaskLeaseStore` from `@nullsquare/agent-authority/storage`;
|
|
26
|
+
- `DurableTaskLeaseSession` and `createDurableTaskLeaseSession()` from `@nullsquare/agent-authority/durable-task-lease`;
|
|
27
|
+
- durable allow / `authority_delta_required` / completion behavior;
|
|
28
|
+
- the requirement that the optional `ai` package is not installed as a production dependency.
|
|
25
29
|
|
|
26
|
-
This makes the registry artifact verification cover the same
|
|
30
|
+
This makes the registry artifact verification cover the same public durability, evidence and transport surfaces exercised by the repository tests rather than checking export names alone.
|
|
31
|
+
|
|
32
|
+
The v0.4.5 independent registry verification passed after publication: npm visibility succeeded and the fresh registry-installed consumer completed the durable Task Lease smoke.
|
|
27
33
|
|
|
28
34
|
## npm vs GitHub release surfaces
|
|
29
35
|
|
|
30
36
|
Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
|
|
31
37
|
|
|
32
38
|
- **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.
|
|
39
|
+
- **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.5`. A release must be created explicitly or by release automation.
|
|
34
40
|
- **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
41
|
|
|
36
42
|
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.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Product proof gate
|
|
2
|
+
|
|
3
|
+
Agent Authority has enough security machinery to validate its core thesis. The next risk is no longer "can we make the invariant stronger?" It is "will an agent developer actually install and keep this layer?"
|
|
4
|
+
|
|
5
|
+
The product thesis is:
|
|
6
|
+
|
|
7
|
+
> **Your agent may use the permissions it already has only for the task the user actually gave it.**
|
|
8
|
+
|
|
9
|
+
The differentiated mechanism is narrower:
|
|
10
|
+
|
|
11
|
+
> **Authority may follow exact resources discovered through already-authorized execution, without turning those resources into standing account permissions.**
|
|
12
|
+
|
|
13
|
+
Everything else in the repository exists to make those two statements true.
|
|
14
|
+
|
|
15
|
+
## Developer mental model
|
|
16
|
+
|
|
17
|
+
The preferred public experience should stay close to three concepts:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Task -> Effect -> Authority
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A developer should not need to understand Mission internals, Task Lease hashing, execution evidence envelopes, CAS persistence or transport adapters before getting value.
|
|
24
|
+
|
|
25
|
+
Those primitives remain available for advanced integrations and audits.
|
|
26
|
+
|
|
27
|
+
## Product-facing API
|
|
28
|
+
|
|
29
|
+
The task-first facade intentionally composes the existing primitives instead of replacing them:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { createTask } from '@nullsquare/agent-authority/task';
|
|
33
|
+
|
|
34
|
+
const task = createTask({
|
|
35
|
+
principal: 'user:me',
|
|
36
|
+
agent: 'agent:assistant',
|
|
37
|
+
request: 'Handle issue #42',
|
|
38
|
+
permissions: {
|
|
39
|
+
github: {
|
|
40
|
+
allow: ['issue.list', 'issue.comment'],
|
|
41
|
+
deny: ['repo.delete'],
|
|
42
|
+
constraints: { repository: ['acme/app'] }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
authority: {
|
|
46
|
+
repository: { kind: 'github.repository', value: 'acme/app' }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const discovery = await task.run(request, () => github.listIssues());
|
|
51
|
+
const issue = task.authorityFrom(discovery, {
|
|
52
|
+
name: 'issue',
|
|
53
|
+
kind: 'github.issue.number',
|
|
54
|
+
from: 'repository',
|
|
55
|
+
extractor
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
task.bind({
|
|
59
|
+
service: 'github',
|
|
60
|
+
action: 'issue.comment',
|
|
61
|
+
field: 'issue_number',
|
|
62
|
+
authority: 'issue'
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The low-level Mission and Task Lease APIs remain the source of truth. The facade must never add authority that those lower layers would reject.
|
|
67
|
+
|
|
68
|
+
## Adoption gate
|
|
69
|
+
|
|
70
|
+
Do not prioritize another deep authorization subsystem until the following are demonstrated:
|
|
71
|
+
|
|
72
|
+
- [ ] a new developer can run a meaningful task-first example in under 10 minutes;
|
|
73
|
+
- [ ] at least three real workflow examples exist: coding, support/communications, and operations/finance;
|
|
74
|
+
- [ ] the same task-first API works in-memory and with durable local state;
|
|
75
|
+
- [ ] useful-task completion stays high under the product benchmark;
|
|
76
|
+
- [ ] normal task actions do not trigger unnecessary approvals;
|
|
77
|
+
- [ ] unrelated-resource effects execute zero provider callbacks;
|
|
78
|
+
- [ ] approval/step-up output explains the established authority and requested delta clearly;
|
|
79
|
+
- [ ] at least one external developer uses the package without project-author assistance.
|
|
80
|
+
|
|
81
|
+
## Utility metrics
|
|
82
|
+
|
|
83
|
+
Security tests remain required, but product work should additionally track:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
normal task completion rate
|
|
87
|
+
false approval rate
|
|
88
|
+
true authority-delta step-up rate
|
|
89
|
+
unauthorized effect rate
|
|
90
|
+
provider effects per completed task
|
|
91
|
+
integration lines required for a representative workflow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`npm run benchmark:task` is the first deterministic fixture for these metrics. It is not a real-world benchmark and must not be marketed as one. Its purpose is to make utility regressions visible alongside security regressions.
|
|
95
|
+
|
|
96
|
+
The current fixture target is:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
normal task completion rate = 100%
|
|
100
|
+
false approval rate = 0%
|
|
101
|
+
true authority-delta step-up rate = 100%
|
|
102
|
+
unauthorized effect rate = 0%
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Real provider/harness benchmarks should replace or supplement the fixture as the product matures.
|
|
106
|
+
|
|
107
|
+
## Three product proofs
|
|
108
|
+
|
|
109
|
+
### Coding agent
|
|
110
|
+
|
|
111
|
+
Task:
|
|
112
|
+
|
|
113
|
+
> Fix issue #42 and open a PR. Do not merge or deploy.
|
|
114
|
+
|
|
115
|
+
Desired authority lineage:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
repository -> issue -> task branch -> changed files -> pull request
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Unrelated repositories, issues, merge and deploy remain outside the task.
|
|
122
|
+
|
|
123
|
+
### Support / communications agent
|
|
124
|
+
|
|
125
|
+
Task:
|
|
126
|
+
|
|
127
|
+
> Handle this customer email.
|
|
128
|
+
|
|
129
|
+
Desired authority lineage:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
email thread -> customer -> meeting / CRM record / reply target
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The underlying connected account may have broad access; task authority follows only the customer/resource discovered through the authorized thread.
|
|
136
|
+
|
|
137
|
+
### Operations / finance agent
|
|
138
|
+
|
|
139
|
+
Task:
|
|
140
|
+
|
|
141
|
+
> Resolve this ticket and refund the affected order.
|
|
142
|
+
|
|
143
|
+
Desired authority lineage:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
ticket -> customer -> order -> payment -> refund <= original payment
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is the strongest long-term proof because it combines resource lineage with an amount ceiling.
|
|
150
|
+
|
|
151
|
+
## Freeze list
|
|
152
|
+
|
|
153
|
+
Until the adoption gate moves, the following remain research backlog unless a real workflow proves they are blocking adoption or safety:
|
|
154
|
+
|
|
155
|
+
- distributed Task Lease databases;
|
|
156
|
+
- generic storage abstraction layers;
|
|
157
|
+
- provider-signed attestation protocols;
|
|
158
|
+
- another token or identity format;
|
|
159
|
+
- a general delegation standard;
|
|
160
|
+
- a proprietary policy DSL;
|
|
161
|
+
- broad OAuth/OIDC platform work;
|
|
162
|
+
- another MCP control plane;
|
|
163
|
+
- A2A protocol implementation;
|
|
164
|
+
- large connector-count expansion;
|
|
165
|
+
- full distributed transaction semantics across arbitrary remote providers.
|
|
166
|
+
|
|
167
|
+
The existing durability, evidence, transport and credential primitives should be reused rather than deepened by default.
|
|
168
|
+
|
|
169
|
+
## Boundary discipline
|
|
170
|
+
|
|
171
|
+
Agent Authority should integrate with identity providers, OAuth systems, MCP gateways, policy engines and agent frameworks rather than compete with all of them.
|
|
172
|
+
|
|
173
|
+
The intended position is:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
agent reasoning
|
|
177
|
+
|
|
|
178
|
+
v
|
|
179
|
+
Agent Authority
|
|
180
|
+
|
|
|
181
|
+
v
|
|
182
|
+
existing SDK / MCP / gateway / OAuth / provider
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The product wins if that middle layer is small to adopt, preserves useful autonomy, and technically prevents the same standing account permission from becoming unrelated task authority.
|
|
@@ -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.
|