@nullsquare/agent-authority 0.4.1 → 0.4.2
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 +6 -2
- package/ROADMAP.md +12 -9
- package/docs/evidence.md +63 -10
- package/docs/npm-release.md +4 -2
- package/docs/task-leases.md +95 -28
- package/examples/live-google-cross-provider.js +13 -8
- package/package.json +3 -2
- package/src/authority-evidence.js +146 -0
- package/src/guard.js +8 -1
- package/src/providers/google.js +37 -0
- package/src/task-lease.js +77 -0
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
**Agent Authority turns a human-approved task into temporary execution authority, then keeps that authority bounded as the agent discovers resources, crosses tools, and performs side effects.**
|
|
10
10
|
|
|
11
|
-
[Task Leases](docs/task-leases.md) · [Validate](docs/validation.md) · [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) · [Validate](docs/validation.md) · [Google proof](docs/live-google-validation.md) · [Integration contract](docs/integration-contract.md) · [CLI](docs/cli.md) · [Architecture](docs/architecture.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
|
|
12
12
|
|
|
13
|
-
> **Status: public pre-alpha / v0.4.
|
|
13
|
+
> **Status: public pre-alpha / v0.4.1 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype with provenance-bound derived facts, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, GitHub and Google provider integrations, CI and CodeQL. It is not production-ready yet.
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -152,6 +152,8 @@ The demo performs this flow without provider credentials:
|
|
|
152
152
|
|
|
153
153
|
The side-effect callbacks for blocked actions never run.
|
|
154
154
|
|
|
155
|
+
The repository also includes a real Gmail → Calendar validation path and a reusable Google provider adapter. See [Live Gmail → Calendar validation](docs/live-google-validation.md).
|
|
156
|
+
|
|
155
157
|
## Minimal developer API
|
|
156
158
|
|
|
157
159
|
```js
|
|
@@ -269,6 +271,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
269
271
|
- AES-256-GCM local encrypted secret store
|
|
270
272
|
- safe reconnect cleanup
|
|
271
273
|
- GitHub brokered execution without returning the token to the agent
|
|
274
|
+
- Google REST provider mappings for Gmail thread reads and Calendar event mutations
|
|
272
275
|
- short-lived signed local agent-instance tokens
|
|
273
276
|
- local CLI/daemon
|
|
274
277
|
|
|
@@ -278,6 +281,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
278
281
|
- Node 20 and Node 22 CI
|
|
279
282
|
- coverage run
|
|
280
283
|
- package checks
|
|
284
|
+
- clean-consumer npm registry verification
|
|
281
285
|
- CodeQL
|
|
282
286
|
|
|
283
287
|
## What is different from OAuth, IAM and MCP authorization?
|
package/ROADMAP.md
CHANGED
|
@@ -77,19 +77,22 @@ Build only what the real M1 workflow proves necessary.
|
|
|
77
77
|
|
|
78
78
|
**Success criterion:** a Task Lease survives daemon/process restarts without gaining authority or losing its provenance lineage.
|
|
79
79
|
|
|
80
|
-
## M3 — Trustworthy derived facts
|
|
80
|
+
## M3 — Trustworthy derived facts — first slice in progress
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
The first real Gmail -> Calendar integration showed that recording a host-supplied value plus selector was too weak for the strongest derived-authority claim. The compatibility `derive()` path remains host-trusted; new provider work should prefer execution-bound evidence and reviewed adapter extractors.
|
|
83
83
|
|
|
84
|
-
- [
|
|
85
|
-
- [
|
|
86
|
-
- [
|
|
87
|
-
- [
|
|
88
|
-
- [
|
|
84
|
+
- [x] define a small trusted-adapter extractor contract for authority-relevant normalized provider fields
|
|
85
|
+
- [x] bind successful guarded outputs to the exact ALLOW receipt, request and output hash
|
|
86
|
+
- [x] add `TaskLease.deriveFromEvidence()` so the caller cannot provide the authority value
|
|
87
|
+
- [x] migrate Gmail sender -> Calendar attendee derivation to the evidence-verified path
|
|
88
|
+
- [x] adversarial tests for value substitution, output/evidence tampering, receipt replay, cross-lease reuse, wrong-operation extraction and dangerous selectors
|
|
89
|
+
- [ ] define provider/result attestation stronger than a trusted host output hash where practical
|
|
90
|
+
- [ ] conformance fixtures for reviewed operation -> authority-field mappings across multiple providers
|
|
91
|
+
- [ ] define freshness/invalidation rules when a source resource changes
|
|
89
92
|
|
|
90
93
|
Do **not** build a general semantic policy language unless real integrations require it.
|
|
91
94
|
|
|
92
|
-
**Success criterion:**
|
|
95
|
+
**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. Stronger provider attestation and source invalidation remain separate follow-on problems.
|
|
93
96
|
|
|
94
97
|
## M4 — Same task, multiple transports
|
|
95
98
|
|
|
@@ -147,7 +150,7 @@ Only after operational evidence.
|
|
|
147
150
|
|
|
148
151
|
## Research questions
|
|
149
152
|
|
|
150
|
-
1. What
|
|
153
|
+
1. What provider-side or transport-side evidence can strengthen output integrity without turning Agent Authority into an attestation protocol?
|
|
151
154
|
2. How should an approved authority delta update a running task without opening a broader wildcard permission?
|
|
152
155
|
3. How should source-data changes invalidate downstream derived authority?
|
|
153
156
|
4. What provider/tool metadata is required to map operations to resource context reliably?
|
package/docs/evidence.md
CHANGED
|
@@ -10,9 +10,55 @@ This page records the strongest properties the repository currently demonstrates
|
|
|
10
10
|
|
|
11
11
|
The guarantee applies to effects that actually pass through the Agent Authority enforcement boundary. A separate unguarded provider path is outside this guarantee.
|
|
12
12
|
|
|
13
|
+
## Execution-bound derived authority
|
|
14
|
+
|
|
15
|
+
The strict derived-authority path now binds provider-derived authority to the exact output returned by an authorized `guard.run()` effect.
|
|
16
|
+
|
|
17
|
+
A successful guarded effect returns three relevant records:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
ALLOW receipt
|
|
21
|
+
+
|
|
22
|
+
exact effect output
|
|
23
|
+
|
|
|
24
|
+
v
|
|
25
|
+
execution evidence
|
|
26
|
+
(receipt + request + output hash)
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
reviewed adapter extractor
|
|
30
|
+
(selector only, no value)
|
|
31
|
+
|
|
|
32
|
+
v
|
|
33
|
+
TaskLease.deriveFromEvidence()
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
derived authority fact
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`deriveFromEvidence()` does not accept the derived authority value. The trusted adapter extractor chooses a reviewed selector, and Task Lease resolves that selector itself only after verifying the execution evidence.
|
|
40
|
+
|
|
41
|
+
`test/authority-evidence.test.js` attacks this boundary directly and proves that:
|
|
42
|
+
|
|
43
|
+
- caller-supplied value substitution does not control the resulting fact;
|
|
44
|
+
- modified provider output is rejected with `evidence_output_mismatch`;
|
|
45
|
+
- modified execution-evidence contents are rejected;
|
|
46
|
+
- evidence cannot be replayed under a second ALLOW receipt;
|
|
47
|
+
- a receipt/evidence chain from another Task Lease is rejected;
|
|
48
|
+
- the Gmail sender extractor rejects the wrong provider operation;
|
|
49
|
+
- dangerous selector paths such as `__proto__` fail closed.
|
|
50
|
+
|
|
51
|
+
The legacy `TaskLease.derive()` API remains for compatibility and records `derivation_mode: host-trusted`. New provider-derived authority should prefer `deriveFromEvidence()`, which records `derivation_mode: execution-evidence-v1`, extractor ID, source output hash, and execution-evidence hash.
|
|
52
|
+
|
|
53
|
+
### Boundary of this claim
|
|
54
|
+
|
|
55
|
+
Execution evidence is an integrity mechanism inside the trusted host/runtime boundary. It proves that the value selected for strict derivation came from the exact output object bound to that Agent Authority receipt.
|
|
56
|
+
|
|
57
|
+
It does **not** prove that Gmail, GitHub, or another provider cryptographically signed that output, and it does not protect a malicious host that bypasses or replaces the Agent Authority enforcement path. Stronger provider/transport attestation remains open M3 work.
|
|
58
|
+
|
|
13
59
|
## Cross-provider derived authority — Gmail → Calendar
|
|
14
60
|
|
|
15
|
-
Agent Authority
|
|
61
|
+
Agent Authority includes a real Google provider mapping, an adversarial cross-provider test, a live validation script, and an opt-in GitHub Actions workflow.
|
|
16
62
|
|
|
17
63
|
The task shape is:
|
|
18
64
|
|
|
@@ -22,9 +68,11 @@ one Gmail thread
|
|
|
22
68
|
|
|
|
23
69
|
v
|
|
24
70
|
ALLOW Gmail thread.read
|
|
71
|
+
|
|
|
72
|
+
+--> output-bound execution evidence
|
|
25
73
|
|
|
|
26
74
|
v
|
|
27
|
-
|
|
75
|
+
trusted Gmail sender extractor
|
|
28
76
|
|
|
|
29
77
|
v
|
|
30
78
|
derive fact: sender_email
|
|
@@ -57,19 +105,21 @@ A controlled self-test was exercised against the connected NullSquare Gmail and
|
|
|
57
105
|
|
|
58
106
|
No unrelated external person was invited. The smoke test establishes that the concrete Gmail and Calendar operations used by the validation are available and compatible with the intended data shape.
|
|
59
107
|
|
|
60
|
-
The live connected-account smoke is not presented as a public CI proof of the repository script. The Task Lease zero-call assertions are separately executable in `test/google-cross-provider.test.js`, and `.github/workflows/live-google-validation.yml` is provided for a rerunnable provider-backed proof once repository Google OAuth secrets are configured.
|
|
108
|
+
The live connected-account smoke is not presented as a public CI proof of the repository script. The Task Lease zero-call and execution-evidence assertions are separately executable in `test/google-cross-provider.test.js` and `test/authority-evidence.test.js`, and `.github/workflows/live-google-validation.yml` is provided for a rerunnable provider-backed proof once repository Google OAuth secrets are configured.
|
|
61
109
|
|
|
62
110
|
### Executable local/CI assertions
|
|
63
111
|
|
|
64
112
|
`test/google-cross-provider.test.js` proves deterministically that:
|
|
65
113
|
|
|
66
114
|
- the approved Gmail thread callback runs exactly once;
|
|
67
|
-
- its sender becomes a same-lease
|
|
115
|
+
- its sender becomes a same-lease `execution-evidence-v1` authority fact;
|
|
68
116
|
- one Calendar mutation for that sender runs exactly once;
|
|
69
117
|
- a different attendee produces `authority_delta_required` and the Calendar callback is not invoked;
|
|
70
118
|
- after lease completion, the previously valid attendee produces `task_lease_completed` and the Calendar callback is not invoked again.
|
|
71
119
|
|
|
72
|
-
`test/google-provider.test.js`
|
|
120
|
+
`test/google-provider.test.js` validates the Google REST mappings, metadata-only Gmail sender extraction, Calendar attendee body, default `sendUpdates=none`, credential redaction, unsupported-action failure, and mutation classification.
|
|
121
|
+
|
|
122
|
+
`test/google-authority-extractor.test.js` separately verifies that the reviewed Gmail extractor only accepts canonical `gmail:thread.read` sender output and that the Google adapter advertises it only for the supported authority mapping.
|
|
73
123
|
|
|
74
124
|
### Public workflow
|
|
75
125
|
|
|
@@ -89,9 +139,10 @@ The connected GitHub tool does not expose repository Actions-secret management,
|
|
|
89
139
|
|
|
90
140
|
### What this proves
|
|
91
141
|
|
|
92
|
-
Taken together, the deterministic Task Lease
|
|
142
|
+
Taken together, the deterministic Task Lease tests plus the controlled provider smoke show that:
|
|
93
143
|
|
|
94
144
|
- the Gmail → Calendar derived-authority shape is implemented, not just diagrammed;
|
|
145
|
+
- strict derivation binds the sender to the exact guarded Gmail output before it becomes authority;
|
|
95
146
|
- the exact attendee binding is enforced before the Calendar mutation callback;
|
|
96
147
|
- provider operations exist and work against real Gmail and Calendar accounts;
|
|
97
148
|
- cleanup can keep the live proof reversible and self-contained.
|
|
@@ -99,9 +150,10 @@ Taken together, the deterministic Task Lease test plus the controlled provider s
|
|
|
99
150
|
### What this does not prove yet
|
|
100
151
|
|
|
101
152
|
- a public GitHub Actions run has executed the full repository live script;
|
|
102
|
-
-
|
|
153
|
+
- `sender_email` is cryptographically attested by Gmail rather than integrity-bound to the trusted host's guarded output;
|
|
103
154
|
- the agent cannot bypass Agent Authority if it independently holds a Google credential;
|
|
104
155
|
- Task Lease state is durable across process failure;
|
|
156
|
+
- source-data changes automatically invalidate downstream facts;
|
|
105
157
|
- the refresh-token setup is production credential onboarding.
|
|
106
158
|
|
|
107
159
|
See [Live Gmail → Calendar validation](live-google-validation.md).
|
|
@@ -178,7 +230,7 @@ The temporary validation comment is deleted by test-harness cleanup after the pr
|
|
|
178
230
|
|
|
179
231
|
### What this does not prove
|
|
180
232
|
|
|
181
|
-
- the
|
|
233
|
+
- the current GitHub example has migrated to a reviewed evidence extractor contract;
|
|
182
234
|
- an agent cannot bypass Agent Authority if it independently possesses the provider credential or another unguarded provider path;
|
|
183
235
|
- Task Lease state is durable across process failure;
|
|
184
236
|
- the current prototype is ready for adversarial production use.
|
|
@@ -209,7 +261,8 @@ The test suite also covers:
|
|
|
209
261
|
- receipts from another mission are rejected;
|
|
210
262
|
- receipts from another Task Lease are rejected;
|
|
211
263
|
- parent lineage is required;
|
|
212
|
-
-
|
|
264
|
+
- legacy host-trusted derivation records its extraction selector;
|
|
265
|
+
- strict execution-evidence derivation rejects substitution/replay/tampering cases;
|
|
213
266
|
- explicit mission deny rules remain the ceiling;
|
|
214
267
|
- lease expiry and mission expiry are enforced against a consistent evaluation clock.
|
|
215
268
|
|
|
@@ -225,7 +278,7 @@ Current pull requests run:
|
|
|
225
278
|
- coverage;
|
|
226
279
|
- live GitHub read validation;
|
|
227
280
|
- live derived GitHub mutation validation for trusted in-repository branches;
|
|
228
|
-
- Google provider
|
|
281
|
+
- Google provider, extractor, cross-provider and execution-evidence adversarial tests;
|
|
229
282
|
- CodeQL.
|
|
230
283
|
|
|
231
284
|
The live Google provider mutation workflow is manual because it requires repository-owned Google OAuth secrets. It should be added to the public evidence list after its first successful run.
|
package/docs/npm-release.md
CHANGED
|
@@ -13,17 +13,19 @@ 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.1
|
|
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.1 it additionally verifies the public `@nullsquare/agent-authority/providers/google` export.
|
|
22
|
+
|
|
21
23
|
## npm vs GitHub release surfaces
|
|
22
24
|
|
|
23
25
|
Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
|
|
24
26
|
|
|
25
27
|
- **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`.
|
|
26
|
-
- **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.
|
|
28
|
+
- **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.1`. A release must be created explicitly or by release automation.
|
|
27
29
|
- **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.
|
|
28
30
|
|
|
29
31
|
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.
|
package/docs/task-leases.md
CHANGED
|
@@ -32,6 +32,12 @@ Human-approved task
|
|
|
32
32
|
authority roots
|
|
33
33
|
|
|
|
34
34
|
authorized execution
|
|
35
|
+
|
|
|
36
|
+
+--> ALLOW receipt
|
|
37
|
+
+--> exact output hash evidence
|
|
38
|
+
|
|
|
39
|
+
v
|
|
40
|
+
trusted adapter extractor
|
|
35
41
|
|
|
|
36
42
|
v
|
|
37
43
|
derived facts
|
|
@@ -64,7 +70,60 @@ Examples:
|
|
|
64
70
|
- customer ID discovered from an authorized support-ticket lookup;
|
|
65
71
|
- order ID discovered from an authorized customer lookup.
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
A derived fact must reference an `ALLOW` receipt from the same Task Lease and at least one parent authority fact.
|
|
74
|
+
|
|
75
|
+
Agent Authority now exposes two derivation modes:
|
|
76
|
+
|
|
77
|
+
- `derive()` — compatibility path where the trusted host supplies both `value` and `selector`;
|
|
78
|
+
- `deriveFromEvidence()` — stricter path where the host does **not** supply the authority value.
|
|
79
|
+
|
|
80
|
+
For provider-derived authority, prefer `deriveFromEvidence()`.
|
|
81
|
+
|
|
82
|
+
### Execution evidence
|
|
83
|
+
|
|
84
|
+
After an allowed `guard.run()` effect succeeds, the guard returns:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
{
|
|
88
|
+
output,
|
|
89
|
+
receipt,
|
|
90
|
+
evidence
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The execution-evidence record binds:
|
|
95
|
+
|
|
96
|
+
- the receipt ID and receipt hash;
|
|
97
|
+
- mission and Task Lease identity;
|
|
98
|
+
- service and action;
|
|
99
|
+
- request hash;
|
|
100
|
+
- a hash of the exact returned output.
|
|
101
|
+
|
|
102
|
+
If downstream code changes the output and tries to reuse the original evidence, derivation fails with `evidence_output_mismatch`.
|
|
103
|
+
|
|
104
|
+
This is an integrity mechanism inside the trusted host/runtime boundary. It is **not** provider-signed remote attestation.
|
|
105
|
+
|
|
106
|
+
### Trusted adapter extractor
|
|
107
|
+
|
|
108
|
+
The adapter extractor identifies which normalized provider-output field may become authority.
|
|
109
|
+
|
|
110
|
+
For Gmail, the current extractor accepts only a `gmail:thread.read` receipt and selects:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
output.sender_email
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The extractor returns only an ID and selector. It does not return the authority value.
|
|
117
|
+
|
|
118
|
+
Task Lease resolves the selector itself after checking the execution evidence. This prevents ordinary host code from doing this:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
Gmail returned customer@example.com
|
|
122
|
+
host claims attacker@example.com
|
|
123
|
+
while reusing the original Gmail receipt
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The stricter path rejects output/evidence substitution rather than recording the host's claimed value.
|
|
68
127
|
|
|
69
128
|
### Binding
|
|
70
129
|
|
|
@@ -74,7 +133,7 @@ A binding narrows an otherwise permitted action to the exact value held by an au
|
|
|
74
133
|
{
|
|
75
134
|
service: 'calendar',
|
|
76
135
|
action: 'event.create',
|
|
77
|
-
context_field: '
|
|
136
|
+
context_field: 'attendee_email',
|
|
78
137
|
fact_id: 'fact:requester-email'
|
|
79
138
|
}
|
|
80
139
|
```
|
|
@@ -87,7 +146,7 @@ If it has value `customer@example.com`, this request can proceed:
|
|
|
87
146
|
{
|
|
88
147
|
service: 'calendar',
|
|
89
148
|
action: 'event.create',
|
|
90
|
-
context: {
|
|
149
|
+
context: { attendee_email: 'customer@example.com' }
|
|
91
150
|
}
|
|
92
151
|
```
|
|
93
152
|
|
|
@@ -97,7 +156,7 @@ This request does not proceed automatically:
|
|
|
97
156
|
{
|
|
98
157
|
service: 'calendar',
|
|
99
158
|
action: 'event.create',
|
|
100
|
-
context: {
|
|
159
|
+
context: { attendee_email: 'other@example.com' }
|
|
101
160
|
}
|
|
102
161
|
```
|
|
103
162
|
|
|
@@ -126,12 +185,13 @@ lease authority <= mission authority
|
|
|
126
185
|
|
|
127
186
|
Authority may stay the same or shrink. It must never grow silently.
|
|
128
187
|
|
|
129
|
-
##
|
|
188
|
+
## Evidence-verified Gmail -> Calendar example
|
|
130
189
|
|
|
131
190
|
```js
|
|
132
191
|
import { AuthorityRuntime } from '@nullsquare/agent-authority';
|
|
133
192
|
import { createTaskLease } from '@nullsquare/agent-authority/task-lease';
|
|
134
193
|
import { createTaskLeaseGuard } from '@nullsquare/agent-authority/guard';
|
|
194
|
+
import { gmailThreadSenderAuthorityExtractor } from '@nullsquare/agent-authority/providers/google';
|
|
135
195
|
|
|
136
196
|
const lease = createTaskLease({
|
|
137
197
|
mission,
|
|
@@ -143,7 +203,7 @@ const lease = createTaskLease({
|
|
|
143
203
|
{
|
|
144
204
|
service: 'calendar',
|
|
145
205
|
action: 'event.create',
|
|
146
|
-
context_field: '
|
|
206
|
+
context_field: 'attendee_email',
|
|
147
207
|
fact_id: 'fact:sender-email'
|
|
148
208
|
}
|
|
149
209
|
]
|
|
@@ -157,30 +217,29 @@ const guard = createTaskLeaseGuard({
|
|
|
157
217
|
const read = await guard.run({
|
|
158
218
|
service: 'gmail',
|
|
159
219
|
action: 'thread.read',
|
|
160
|
-
context: {
|
|
220
|
+
context: { thread_id: 'thread:demo-91' }
|
|
161
221
|
}, () => gmail.readThread('thread:demo-91'));
|
|
162
222
|
|
|
163
|
-
lease.
|
|
223
|
+
const senderFact = lease.deriveFromEvidence({
|
|
164
224
|
fact_id: 'fact:sender-email',
|
|
165
225
|
kind: 'email.address',
|
|
166
|
-
value: read.output.sender,
|
|
167
226
|
from: ['fact:thread'],
|
|
168
227
|
receipt: read.receipt,
|
|
169
|
-
|
|
228
|
+
evidence: read.evidence,
|
|
229
|
+
output: read.output,
|
|
230
|
+
extractor: gmailThreadSenderAuthorityExtractor
|
|
170
231
|
});
|
|
171
232
|
|
|
172
233
|
await guard.run({
|
|
173
234
|
service: 'calendar',
|
|
174
235
|
action: 'event.create',
|
|
175
|
-
context: {
|
|
176
|
-
}, () => calendar.createEvent({ attendee:
|
|
236
|
+
context: { attendee_email: senderFact.value }
|
|
237
|
+
}, () => calendar.createEvent({ attendee: senderFact.value }));
|
|
177
238
|
```
|
|
178
239
|
|
|
179
|
-
|
|
240
|
+
Notice that `deriveFromEvidence()` has no `value` argument. The fact value comes from the exact output already bound to the authorized read.
|
|
180
241
|
|
|
181
|
-
|
|
182
|
-
npm run demo:task-lease
|
|
183
|
-
```
|
|
242
|
+
The older `derive()` API remains available for integrations that have not adopted the evidence contract yet. Facts created through that API record `derivation_mode: host-trusted` so audit code can distinguish the weaker path.
|
|
184
243
|
|
|
185
244
|
## Task completion
|
|
186
245
|
|
|
@@ -207,36 +266,44 @@ requested new resource
|
|
|
207
266
|
authority_delta_required
|
|
208
267
|
```
|
|
209
268
|
|
|
210
|
-
The existing approval store can handle the human decision. Automatically applying approved deltas to a live Task Lease is a later milestone;
|
|
269
|
+
The existing approval store can handle the human decision. Automatically applying approved deltas to a live Task Lease is a later milestone; the current implementation deliberately stops at the safe enforcement signal.
|
|
211
270
|
|
|
212
271
|
## Current security properties
|
|
213
272
|
|
|
214
|
-
The
|
|
273
|
+
The implementation tests that:
|
|
215
274
|
|
|
216
275
|
- a bound action cannot run before its fact exists;
|
|
217
|
-
- derived facts require an `ALLOW` receipt;
|
|
218
|
-
- the receipt must belong to the same mission;
|
|
276
|
+
- derived facts require an `ALLOW` receipt from the same mission and Task Lease;
|
|
219
277
|
- explicit mission denies cannot be overridden by lease bindings;
|
|
220
278
|
- an exact derived resource can execute;
|
|
221
279
|
- a different resource becomes an authority delta and the effect does not run;
|
|
222
280
|
- completed and expired leases stop execution;
|
|
223
|
-
- Task Lease receipts include the lease ID and lease hash
|
|
281
|
+
- Task Lease receipts include the lease ID and lease hash;
|
|
282
|
+
- successful guarded effects produce output-bound execution evidence;
|
|
283
|
+
- `deriveFromEvidence()` ignores any caller-supplied `value` and resolves the trusted selector itself;
|
|
284
|
+
- modified provider output is rejected;
|
|
285
|
+
- modified execution evidence is rejected;
|
|
286
|
+
- execution evidence cannot be replayed under another receipt or Task Lease;
|
|
287
|
+
- the Gmail extractor rejects the wrong service/action;
|
|
288
|
+
- dangerous selector paths such as `__proto__` fail closed.
|
|
224
289
|
|
|
225
290
|
## Current limitations
|
|
226
291
|
|
|
227
292
|
This is still a validation implementation.
|
|
228
293
|
|
|
229
|
-
1. **
|
|
230
|
-
2. **
|
|
231
|
-
3. **
|
|
232
|
-
4. **
|
|
233
|
-
5. **
|
|
294
|
+
1. **Trusted host/adapter boundary:** execution evidence is produced by Agent Authority around the host effect, not signed by Gmail, Calendar, or another provider. A malicious host that can bypass or replace Agent Authority remains outside the guarantee.
|
|
295
|
+
2. **Provider attestation:** output hashes prove consistency with what the guarded effect returned; they do not cryptographically prove what the remote provider emitted on the wire.
|
|
296
|
+
3. **Source invalidation:** a source resource changing later does not yet invalidate already-derived facts automatically.
|
|
297
|
+
4. **In-memory lease state:** TaskLease instances are currently process-local. Durable lease persistence/recovery is not implemented yet.
|
|
298
|
+
5. **Top-level binding fields:** bindings target top-level request context fields. A general nested policy language is intentionally deferred.
|
|
299
|
+
6. **Step-up application:** authority deltas are surfaced but approved deltas are not yet automatically applied back into the lease.
|
|
300
|
+
7. **Adapter semantics:** each provider still needs a reviewed operation -> authority-field mapping. The Google sender extractor is the first concrete contract.
|
|
234
301
|
|
|
235
|
-
These constraints are deliberate. The
|
|
302
|
+
These constraints are deliberate. The project should improve the evidence contract from real provider cases rather than build a universal semantic policy language.
|
|
236
303
|
|
|
237
304
|
## Validation target
|
|
238
305
|
|
|
239
|
-
The product thesis is validated when the same Task Lease can safely govern a real multi-step workflow across more than one execution transport, for example:
|
|
306
|
+
The longer-term product thesis is validated when the same Task Lease can safely govern a real multi-step workflow across more than one execution transport, for example:
|
|
240
307
|
|
|
241
308
|
```text
|
|
242
309
|
one human task
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
AuthorityDeniedError,
|
|
5
5
|
createTaskLeaseGuard
|
|
6
6
|
} from '../src/guard.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
gmailThreadSenderAuthorityExtractor,
|
|
9
|
+
gmailThreadSenderEmail
|
|
10
|
+
} from '../src/providers/google.js';
|
|
8
11
|
import { createTaskLease } from '../src/task-lease.js';
|
|
9
12
|
|
|
10
13
|
const token = process.env.GOOGLE_ACCESS_TOKEN;
|
|
@@ -128,6 +131,7 @@ async function readAuthorizedThread() {
|
|
|
128
131
|
);
|
|
129
132
|
const sender = gmailThreadSenderEmail(thread);
|
|
130
133
|
return {
|
|
134
|
+
provider: 'gmail',
|
|
131
135
|
thread_id: thread.id,
|
|
132
136
|
sender_email: sender.email,
|
|
133
137
|
sender_raw: sender.raw,
|
|
@@ -198,17 +202,18 @@ try {
|
|
|
198
202
|
throw new Error(`expected sender ${expectedSender}, got ${discovered.output.sender_email}`);
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
lease.
|
|
205
|
+
const senderFact = lease.deriveFromEvidence({
|
|
202
206
|
fact_id: 'fact:sender-email',
|
|
203
207
|
kind: 'email.address',
|
|
204
|
-
value: discovered.output.sender_email,
|
|
205
208
|
from: ['fact:gmail-thread'],
|
|
206
209
|
receipt: discovered.receipt,
|
|
207
|
-
|
|
210
|
+
evidence: discovered.evidence,
|
|
211
|
+
output: discovered.output,
|
|
212
|
+
extractor: gmailThreadSenderAuthorityExtractor
|
|
208
213
|
});
|
|
209
|
-
console.log(`2.
|
|
214
|
+
console.log(`2. Evidence-verified authority -> Calendar attendee ${senderFact.value}`);
|
|
210
215
|
|
|
211
|
-
const allowed = await createCalendarEvent(
|
|
216
|
+
const allowed = await createCalendarEvent(senderFact.value, 'authorized');
|
|
212
217
|
createdEventId = allowed.output.event_id;
|
|
213
218
|
console.log(`3. ALLOW -> real Calendar event mutation executed (${createdEventId})`);
|
|
214
219
|
|
|
@@ -228,7 +233,7 @@ try {
|
|
|
228
233
|
|
|
229
234
|
lease.complete('live Gmail to Calendar validation complete');
|
|
230
235
|
try {
|
|
231
|
-
await createCalendarEvent(
|
|
236
|
+
await createCalendarEvent(senderFact.value, 'must-not-run-after-completion');
|
|
232
237
|
throw new Error('post-completion Calendar mutation unexpectedly executed');
|
|
233
238
|
} catch (error) {
|
|
234
239
|
if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') {
|
|
@@ -244,7 +249,7 @@ try {
|
|
|
244
249
|
throw new Error(`expected exactly one Calendar provider mutation after blocked attempts, got ${providerMutationCalls}`);
|
|
245
250
|
}
|
|
246
251
|
|
|
247
|
-
console.log('PASS -> a sender discovered from real Gmail became exact
|
|
252
|
+
console.log('PASS -> a sender discovered from real Gmail became evidence-verified exact authority for one real Calendar mutation');
|
|
248
253
|
console.log('PASS -> unrelated and post-completion attempts caused zero additional Calendar provider mutations');
|
|
249
254
|
} finally {
|
|
250
255
|
if (createdEventId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullsquare/agent-authority",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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/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/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
|
},
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
".": "./src/index.js",
|
|
61
61
|
"./agent-auth": "./src/agent-auth.js",
|
|
62
62
|
"./approvals": "./src/approvals.js",
|
|
63
|
+
"./authority-evidence": "./src/authority-evidence.js",
|
|
63
64
|
"./connections": "./src/connections.js",
|
|
64
65
|
"./execution": "./src/execution.js",
|
|
65
66
|
"./guard": "./src/guard.js",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { hashObject } from './index.js';
|
|
2
|
+
|
|
3
|
+
const FORBIDDEN_SELECTOR_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
4
|
+
|
|
5
|
+
function evidenceError(code, message) {
|
|
6
|
+
const error = new Error(message);
|
|
7
|
+
error.code = code;
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function requireReceipt(receipt) {
|
|
12
|
+
if (!receipt?.receipt_id) throw evidenceError('evidence_receipt_required', 'execution evidence requires a receipt');
|
|
13
|
+
if (!receipt?.receipt_hash) throw evidenceError('evidence_receipt_hash_required', 'execution evidence requires a receipt hash');
|
|
14
|
+
if (receipt.decision !== 'allow') {
|
|
15
|
+
throw evidenceError('evidence_receipt_not_authorized', 'execution evidence can only be created for an ALLOW receipt');
|
|
16
|
+
}
|
|
17
|
+
return receipt;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unsignedEvidence(evidence = {}) {
|
|
21
|
+
const { evidence_hash: _evidenceHash, ...unsigned } = evidence;
|
|
22
|
+
return unsigned;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hashExecutionOutput(output) {
|
|
26
|
+
return hashObject({
|
|
27
|
+
output_type: output === null ? 'null' : typeof output,
|
|
28
|
+
output
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bind the exact output returned by an authorized effect to its decision receipt.
|
|
34
|
+
*
|
|
35
|
+
* This is integrity evidence, not remote attestation. It prevents downstream code
|
|
36
|
+
* from silently swapping the output object while still claiming the original
|
|
37
|
+
* Agent Authority receipt as provenance.
|
|
38
|
+
*/
|
|
39
|
+
export function createExecutionEvidence({ receipt, output } = {}) {
|
|
40
|
+
requireReceipt(receipt);
|
|
41
|
+
const evidence = {
|
|
42
|
+
version: '0.1',
|
|
43
|
+
type: 'execution-output',
|
|
44
|
+
receipt_id: receipt.receipt_id,
|
|
45
|
+
receipt_hash: receipt.receipt_hash,
|
|
46
|
+
mission_id: receipt.mission_id,
|
|
47
|
+
task_lease_id: receipt.task_lease_id || null,
|
|
48
|
+
service: receipt.service,
|
|
49
|
+
action: receipt.action,
|
|
50
|
+
request_hash: receipt.request_hash,
|
|
51
|
+
output_hash: hashExecutionOutput(output)
|
|
52
|
+
};
|
|
53
|
+
return { ...evidence, evidence_hash: hashObject(evidence) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function verifyExecutionEvidence({ receipt, output, evidence } = {}) {
|
|
57
|
+
requireReceipt(receipt);
|
|
58
|
+
if (!evidence || typeof evidence !== 'object') {
|
|
59
|
+
throw evidenceError('execution_evidence_required', 'derived authority requires execution evidence');
|
|
60
|
+
}
|
|
61
|
+
if (evidence.type !== 'execution-output' || evidence.version !== '0.1') {
|
|
62
|
+
throw evidenceError('execution_evidence_invalid', 'unsupported execution evidence format');
|
|
63
|
+
}
|
|
64
|
+
if (hashObject(unsignedEvidence(evidence)) !== evidence.evidence_hash) {
|
|
65
|
+
throw evidenceError('execution_evidence_tampered', 'execution evidence hash does not match its contents');
|
|
66
|
+
}
|
|
67
|
+
if (evidence.receipt_id !== receipt.receipt_id || evidence.receipt_hash !== receipt.receipt_hash) {
|
|
68
|
+
throw evidenceError('evidence_receipt_mismatch', 'execution evidence belongs to another receipt');
|
|
69
|
+
}
|
|
70
|
+
if (evidence.mission_id !== receipt.mission_id) {
|
|
71
|
+
throw evidenceError('evidence_mission_mismatch', 'execution evidence belongs to another mission');
|
|
72
|
+
}
|
|
73
|
+
if ((evidence.task_lease_id || null) !== (receipt.task_lease_id || null)) {
|
|
74
|
+
throw evidenceError('evidence_lease_mismatch', 'execution evidence belongs to another task lease');
|
|
75
|
+
}
|
|
76
|
+
if (evidence.service !== receipt.service || evidence.action !== receipt.action) {
|
|
77
|
+
throw evidenceError('evidence_operation_mismatch', 'execution evidence operation does not match its receipt');
|
|
78
|
+
}
|
|
79
|
+
if (evidence.request_hash !== receipt.request_hash) {
|
|
80
|
+
throw evidenceError('evidence_request_mismatch', 'execution evidence request does not match its receipt');
|
|
81
|
+
}
|
|
82
|
+
if (hashExecutionOutput(output) !== evidence.output_hash) {
|
|
83
|
+
throw evidenceError('evidence_output_mismatch', 'provider output no longer matches the authorized execution evidence');
|
|
84
|
+
}
|
|
85
|
+
return evidence;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveEvidenceSelector(output, selector) {
|
|
89
|
+
if (typeof selector !== 'string' || selector.trim() === '') {
|
|
90
|
+
throw evidenceError('selector_required', 'trusted extractor must provide a selector');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let normalized = selector.trim();
|
|
94
|
+
if (normalized === 'output') return output;
|
|
95
|
+
if (normalized.startsWith('output.')) normalized = normalized.slice('output.'.length);
|
|
96
|
+
|
|
97
|
+
const segments = normalized.split('.');
|
|
98
|
+
if (segments.length === 0 || segments.some((segment) => !segment || FORBIDDEN_SELECTOR_SEGMENTS.has(segment))) {
|
|
99
|
+
throw evidenceError('selector_invalid', 'trusted extractor selector contains an invalid path segment');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let current = output;
|
|
103
|
+
for (const segment of segments) {
|
|
104
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
105
|
+
throw evidenceError('selector_unresolved', `trusted extractor selector ${selector} does not resolve against provider output`);
|
|
106
|
+
}
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
108
|
+
throw evidenceError('selector_unresolved', `trusted extractor selector ${selector} does not resolve against provider output`);
|
|
109
|
+
}
|
|
110
|
+
current = current[segment];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (current === undefined) {
|
|
114
|
+
throw evidenceError('selector_unresolved', `trusted extractor selector ${selector} resolved to undefined`);
|
|
115
|
+
}
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Execute the small trusted-adapter extraction contract.
|
|
121
|
+
*
|
|
122
|
+
* Extractors choose which already-normalized output field is authority-relevant;
|
|
123
|
+
* they do not supply the value. TaskLease resolves the selector itself so the
|
|
124
|
+
* caller cannot substitute a different value while keeping the same evidence.
|
|
125
|
+
*/
|
|
126
|
+
export function runAuthorityExtractor({ extractor, receipt, output } = {}) {
|
|
127
|
+
if (typeof extractor !== 'function') {
|
|
128
|
+
throw evidenceError('trusted_extractor_required', 'deriveFromEvidence requires a trusted adapter extractor');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const descriptor = extractor({ receipt, output: structuredClone(output) });
|
|
132
|
+
if (!descriptor || typeof descriptor !== 'object') {
|
|
133
|
+
throw evidenceError('trusted_extractor_invalid', 'trusted adapter extractor must return a descriptor');
|
|
134
|
+
}
|
|
135
|
+
if (typeof descriptor.extractor_id !== 'string' || descriptor.extractor_id.trim() === '') {
|
|
136
|
+
throw evidenceError('trusted_extractor_id_required', 'trusted adapter extractor must provide extractor_id');
|
|
137
|
+
}
|
|
138
|
+
if (typeof descriptor.selector !== 'string' || descriptor.selector.trim() === '') {
|
|
139
|
+
throw evidenceError('selector_required', 'trusted adapter extractor must provide selector');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
extractor_id: descriptor.extractor_id.trim(),
|
|
144
|
+
selector: descriptor.selector.trim()
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/guard.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createExecutionEvidence } from './authority-evidence.js';
|
|
2
|
+
|
|
1
3
|
export class AuthorityDeniedError extends Error {
|
|
2
4
|
constructor({ result, receipt }) {
|
|
3
5
|
super(result?.reason || 'action denied by Agent Authority');
|
|
@@ -25,6 +27,10 @@ export class AuthorityApprovalRequiredError extends Error {
|
|
|
25
27
|
* authority boundary returns ALLOW. A guard can use either a static mission or
|
|
26
28
|
* a TaskLease. Task leases add provenance-bound restrictions without changing
|
|
27
29
|
* the host application's credential ownership.
|
|
30
|
+
*
|
|
31
|
+
* Successful effects also return execution evidence binding the exact output
|
|
32
|
+
* hash to the ALLOW receipt. TaskLease.deriveFromEvidence() can use that record
|
|
33
|
+
* with a trusted adapter extractor so callers do not provide derived values.
|
|
28
34
|
*/
|
|
29
35
|
export class AuthorityGuard {
|
|
30
36
|
constructor({ mission, lease, runtime, onDecision } = {}) {
|
|
@@ -66,7 +72,8 @@ export class AuthorityGuard {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const output = await effect();
|
|
69
|
-
|
|
75
|
+
const evidence = createExecutionEvidence({ receipt: evaluation.receipt, output });
|
|
76
|
+
return { output, result: evaluation.result, receipt: evaluation.receipt, evidence };
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
package/src/providers/google.js
CHANGED
|
@@ -42,6 +42,37 @@ export function gmailThreadSenderEmail(thread) {
|
|
|
42
42
|
throw providerError('gmail_sender_missing', 'Gmail thread does not contain a usable From header');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Trusted authority-extraction contract for the normalized Gmail thread output.
|
|
47
|
+
*
|
|
48
|
+
* The extractor chooses the authority-relevant selector only. It never returns
|
|
49
|
+
* the value itself; TaskLease resolves output.sender_email after verifying the
|
|
50
|
+
* guard's execution evidence. This prevents host code from substituting another
|
|
51
|
+
* email while retaining the original Gmail receipt/evidence chain.
|
|
52
|
+
*/
|
|
53
|
+
export function gmailThreadSenderAuthorityExtractor({ receipt, output } = {}) {
|
|
54
|
+
if (receipt?.service !== 'gmail' || receipt?.action !== 'thread.read') {
|
|
55
|
+
throw providerError(
|
|
56
|
+
'trusted_extractor_operation_mismatch',
|
|
57
|
+
'Gmail sender authority extractor only accepts gmail:thread.read receipts'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = typeof output?.sender_email === 'string' ? output.sender_email.trim() : '';
|
|
62
|
+
const normalized = extractEmailAddress(raw);
|
|
63
|
+
if (!normalized || normalized !== raw) {
|
|
64
|
+
throw providerError(
|
|
65
|
+
'trusted_extractor_output_invalid',
|
|
66
|
+
'normalized Gmail output does not contain a canonical sender_email'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
extractor_id: 'google.gmail.thread.sender-email.v1',
|
|
72
|
+
selector: 'output.sender_email'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
function validateSendUpdates(value) {
|
|
46
77
|
const normalized = value || 'none';
|
|
47
78
|
if (!SEND_UPDATES.has(normalized)) {
|
|
@@ -197,5 +228,11 @@ export function createGoogleProviderAdapter({
|
|
|
197
228
|
|
|
198
229
|
adapter.validateRequest = (request) => operationFor(request);
|
|
199
230
|
adapter.isMutation = (request) => MUTATING_ACTIONS.has(request?.action);
|
|
231
|
+
adapter.authorityExtractor = (request, kind = 'email.address') => {
|
|
232
|
+
if (request?.service === 'gmail' && request?.action === 'thread.read' && kind === 'email.address') {
|
|
233
|
+
return gmailThreadSenderAuthorityExtractor;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|
|
200
237
|
return adapter;
|
|
201
238
|
}
|
package/src/task-lease.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assertMission, createReceipt, hashObject, matchPattern } from './index.js';
|
|
3
|
+
import {
|
|
4
|
+
resolveEvidenceSelector,
|
|
5
|
+
runAuthorityExtractor,
|
|
6
|
+
verifyExecutionEvidence
|
|
7
|
+
} from './authority-evidence.js';
|
|
3
8
|
|
|
4
9
|
function authorityError(code, message) {
|
|
5
10
|
const error = new Error(message);
|
|
@@ -95,6 +100,12 @@ export class TaskLease {
|
|
|
95
100
|
return structuredClone(fact);
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Legacy host-trusted derivation path.
|
|
105
|
+
*
|
|
106
|
+
* The caller supplies both value and selector. Keep this API for compatibility,
|
|
107
|
+
* but prefer deriveFromEvidence() for authority-relevant provider outputs.
|
|
108
|
+
*/
|
|
98
109
|
derive({ fact_id, kind = 'opaque', value, from = [], receipt, selector } = {}) {
|
|
99
110
|
if (!fact_id) throw new Error('derived fact_id is required');
|
|
100
111
|
if (value === undefined) throw new Error('derived value is required');
|
|
@@ -125,6 +136,7 @@ export class TaskLease {
|
|
|
125
136
|
value,
|
|
126
137
|
provenance: {
|
|
127
138
|
type: 'derived',
|
|
139
|
+
derivation_mode: 'host-trusted',
|
|
128
140
|
from: parents,
|
|
129
141
|
task_lease_id: this.lease_id,
|
|
130
142
|
receipt_id: receipt.receipt_id,
|
|
@@ -140,6 +152,71 @@ export class TaskLease {
|
|
|
140
152
|
return structuredClone(fact);
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Strict derivation path for provider data.
|
|
157
|
+
*
|
|
158
|
+
* The caller cannot supply the authority value. A trusted adapter extractor
|
|
159
|
+
* identifies one normalized output selector, TaskLease resolves that selector
|
|
160
|
+
* itself, and execution evidence proves the output still matches the exact
|
|
161
|
+
* ALLOW receipt returned by guard.run().
|
|
162
|
+
*/
|
|
163
|
+
deriveFromEvidence({
|
|
164
|
+
fact_id,
|
|
165
|
+
kind = 'opaque',
|
|
166
|
+
from = [],
|
|
167
|
+
receipt,
|
|
168
|
+
evidence,
|
|
169
|
+
output,
|
|
170
|
+
extractor
|
|
171
|
+
} = {}) {
|
|
172
|
+
if (!fact_id) throw new Error('derived fact_id is required');
|
|
173
|
+
if (this.facts.has(fact_id)) throw authorityError('fact_exists', `authority fact ${fact_id} already exists`);
|
|
174
|
+
if (!receipt) throw authorityError('receipt_required', 'derived authority requires an authorized source receipt');
|
|
175
|
+
if (receipt.decision !== 'allow') throw authorityError('receipt_not_authorized', 'derived authority requires an ALLOW receipt');
|
|
176
|
+
if (receipt.mission_id !== this.mission.mission_id) {
|
|
177
|
+
throw authorityError('receipt_mission_mismatch', 'source receipt belongs to another mission');
|
|
178
|
+
}
|
|
179
|
+
if (receipt.task_lease_id !== this.lease_id) {
|
|
180
|
+
throw authorityError('receipt_lease_mismatch', 'source receipt belongs to another task lease');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parents = [...new Set(from)];
|
|
184
|
+
if (parents.length === 0) {
|
|
185
|
+
throw authorityError('parent_fact_required', 'derived authority must descend from at least one existing task authority fact');
|
|
186
|
+
}
|
|
187
|
+
for (const parentId of parents) {
|
|
188
|
+
if (!this.facts.has(parentId)) throw authorityError('parent_fact_missing', `authority fact ${parentId} does not exist`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
verifyExecutionEvidence({ receipt, output, evidence });
|
|
192
|
+
const extraction = runAuthorityExtractor({ extractor, receipt, output });
|
|
193
|
+
const value = resolveEvidenceSelector(output, extraction.selector);
|
|
194
|
+
|
|
195
|
+
const fact = {
|
|
196
|
+
fact_id,
|
|
197
|
+
kind,
|
|
198
|
+
value,
|
|
199
|
+
provenance: {
|
|
200
|
+
type: 'derived',
|
|
201
|
+
derivation_mode: 'execution-evidence-v1',
|
|
202
|
+
from: parents,
|
|
203
|
+
task_lease_id: this.lease_id,
|
|
204
|
+
receipt_id: receipt.receipt_id,
|
|
205
|
+
receipt_hash: receipt.receipt_hash,
|
|
206
|
+
source_service: receipt.service,
|
|
207
|
+
source_action: receipt.action,
|
|
208
|
+
source_request_hash: receipt.request_hash,
|
|
209
|
+
selector: extraction.selector,
|
|
210
|
+
extractor_id: extraction.extractor_id,
|
|
211
|
+
source_output_hash: evidence.output_hash,
|
|
212
|
+
execution_evidence_hash: evidence.evidence_hash
|
|
213
|
+
},
|
|
214
|
+
created_at: new Date().toISOString()
|
|
215
|
+
};
|
|
216
|
+
this.facts.set(fact_id, fact);
|
|
217
|
+
return structuredClone(fact);
|
|
218
|
+
}
|
|
219
|
+
|
|
143
220
|
bind(binding) {
|
|
144
221
|
const normalized = validateBinding(binding);
|
|
145
222
|
this.bindings.push(normalized);
|