@nullsquare/agent-authority 0.4.2 → 0.4.4
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 +40 -13
- package/ROADMAP.md +16 -9
- package/docs/authority-extractor-conformance.md +95 -0
- package/docs/evidence.md +74 -25
- package/docs/npm-release.md +8 -3
- package/docs/transport-invariance.md +117 -0
- package/examples/live-github-derived-mutation.js +82 -65
- package/package.json +1 -1
- 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/providers/github.js +148 -18
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) · [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.3 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype, execution-bound derived facts, reviewed Google and GitHub authority extractors, two-provider conformance tests, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, live GitHub proofs, CI and CodeQL. It is not production-ready yet.
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -97,6 +97,12 @@ Human-approved task
|
|
|
97
97
|
Gmail thread #91
|
|
98
98
|
|
|
|
99
99
|
authorized read
|
|
100
|
+
|
|
|
101
|
+
+--> ALLOW receipt
|
|
102
|
+
+--> exact output hash evidence
|
|
103
|
+
|
|
|
104
|
+
v
|
|
105
|
+
reviewed adapter extractor
|
|
100
106
|
|
|
|
101
107
|
v
|
|
102
108
|
derived fact
|
|
@@ -152,7 +158,9 @@ The demo performs this flow without provider credentials:
|
|
|
152
158
|
|
|
153
159
|
The side-effect callbacks for blocked actions never run.
|
|
154
160
|
|
|
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).
|
|
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 applies the **same primitive to GitHub**: a root-bound repository + fixture marker are used by the reviewed GitHub adapter to select one issue from a real `issue.list` response; `deriveFromEvidence()` establishes that exact issue number as downstream authority; one real comment mutation succeeds; unrelated and post-completion issue mutations never reach the provider. Google and GitHub are now exercised by the same [authority extractor conformance contract](docs/authority-extractor-conformance.md).
|
|
156
164
|
|
|
157
165
|
## Minimal developer API
|
|
158
166
|
|
|
@@ -160,6 +168,7 @@ The repository also includes a real Gmail → Calendar validation path and a reu
|
|
|
160
168
|
import { AuthorityRuntime } from '@nullsquare/agent-authority';
|
|
161
169
|
import { createTaskLease } from '@nullsquare/agent-authority/task-lease';
|
|
162
170
|
import { createTaskLeaseGuard } from '@nullsquare/agent-authority/guard';
|
|
171
|
+
import { gmailThreadSenderAuthorityExtractor } from '@nullsquare/agent-authority/providers/google';
|
|
163
172
|
|
|
164
173
|
const lease = createTaskLease({
|
|
165
174
|
mission,
|
|
@@ -171,7 +180,7 @@ const lease = createTaskLease({
|
|
|
171
180
|
{
|
|
172
181
|
service: 'calendar',
|
|
173
182
|
action: 'event.create',
|
|
174
|
-
context_field: '
|
|
183
|
+
context_field: 'attendee_email',
|
|
175
184
|
fact_id: 'fact:sender-email'
|
|
176
185
|
}
|
|
177
186
|
]
|
|
@@ -185,25 +194,30 @@ const guard = createTaskLeaseGuard({
|
|
|
185
194
|
const read = await guard.run({
|
|
186
195
|
service: 'gmail',
|
|
187
196
|
action: 'thread.read',
|
|
188
|
-
context: {
|
|
197
|
+
context: { thread_id: 'thread:demo-91' }
|
|
189
198
|
}, () => gmail.readThread('thread:demo-91'));
|
|
190
199
|
|
|
191
|
-
lease.
|
|
200
|
+
const senderFact = lease.deriveFromEvidence({
|
|
192
201
|
fact_id: 'fact:sender-email',
|
|
193
202
|
kind: 'email.address',
|
|
194
|
-
value: read.output.sender,
|
|
195
203
|
from: ['fact:thread'],
|
|
196
204
|
receipt: read.receipt,
|
|
197
|
-
|
|
205
|
+
evidence: read.evidence,
|
|
206
|
+
output: read.output,
|
|
207
|
+
extractor: gmailThreadSenderAuthorityExtractor
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
await guard.run({
|
|
201
211
|
service: 'calendar',
|
|
202
212
|
action: 'event.create',
|
|
203
|
-
context: {
|
|
204
|
-
}, () => calendar.createEvent({ attendee:
|
|
213
|
+
context: { attendee_email: senderFact.value }
|
|
214
|
+
}, () => calendar.createEvent({ attendee: senderFact.value }));
|
|
205
215
|
```
|
|
206
216
|
|
|
217
|
+
`deriveFromEvidence()` does not accept the authority value. The reviewed extractor selects a normalized output field, and Task Lease resolves that value only after verifying that the output still matches the exact allowed execution evidence.
|
|
218
|
+
|
|
219
|
+
The older `derive()` API remains available as the explicitly **host-trusted compatibility path**.
|
|
220
|
+
|
|
207
221
|
The host keeps its existing SDK, connector and authentication. Agent Authority controls whether the effect may happen.
|
|
208
222
|
|
|
209
223
|
## Three integration modes, one authority model
|
|
@@ -249,7 +263,13 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
249
263
|
- Task Lease prototype
|
|
250
264
|
- explicit authority roots
|
|
251
265
|
- same-lease provenance-bound derived facts
|
|
252
|
-
-
|
|
266
|
+
- execution evidence binding an allowed receipt, request and exact output hash
|
|
267
|
+
- strict `deriveFromEvidence()` path where the caller cannot provide the authority value
|
|
268
|
+
- reviewed Gmail sender authority extractor bound to `gmail:thread.read`
|
|
269
|
+
- reviewed GitHub selected-issue-number extractor bound to marker-scoped `github:issue.list`
|
|
270
|
+
- shared Google/GitHub authority-extractor conformance suite
|
|
271
|
+
- legacy host-trusted `derive()` compatibility path
|
|
272
|
+
- required parent lineage and extraction selector
|
|
253
273
|
- exact context-field bindings
|
|
254
274
|
- authority-delta step-up signal
|
|
255
275
|
- immediate task completion/expiry enforcement
|
|
@@ -259,6 +279,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
259
279
|
|
|
260
280
|
- protocol-neutral `guard.run()` wrapper
|
|
261
281
|
- blocked side effects never invoke their callback
|
|
282
|
+
- successful guarded effects return separate execution evidence
|
|
262
283
|
- one-time human approvals bound to exact request
|
|
263
284
|
- mutation idempotency
|
|
264
285
|
- conservative uncertain-state handling
|
|
@@ -271,6 +292,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
271
292
|
- AES-256-GCM local encrypted secret store
|
|
272
293
|
- safe reconnect cleanup
|
|
273
294
|
- GitHub brokered execution without returning the token to the agent
|
|
295
|
+
- GitHub REST mappings for repository access plus evidence-derived `issue.list` / `issue.comment`
|
|
274
296
|
- Google REST provider mappings for Gmail thread reads and Calendar event mutations
|
|
275
297
|
- short-lived signed local agent-instance tokens
|
|
276
298
|
- local CLI/daemon
|
|
@@ -278,10 +300,13 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
278
300
|
### Engineering quality
|
|
279
301
|
|
|
280
302
|
- adversarial authorization tests
|
|
303
|
+
- execution-evidence substitution, tampering, replay, cross-lease and selector tests
|
|
304
|
+
- the same provider-derived-authority conformance attacks against Google and GitHub
|
|
281
305
|
- Node 20 and Node 22 CI
|
|
282
306
|
- coverage run
|
|
283
307
|
- package checks
|
|
284
308
|
- clean-consumer npm registry verification
|
|
309
|
+
- live GitHub read and evidence-derived mutation proofs
|
|
285
310
|
- CodeQL
|
|
286
311
|
|
|
287
312
|
## What is different from OAuth, IAM and MCP authorization?
|
|
@@ -337,7 +362,7 @@ Allow a natural workflow across mail, calendar, CRM and internal systems without
|
|
|
337
362
|
1. **Task before credential.** A provider token is not task authority.
|
|
338
363
|
2. **Mission is the ceiling.** Task Leases cannot override explicit denies.
|
|
339
364
|
3. **No side effect before authorization.** Denied and step-up actions never execute.
|
|
340
|
-
4. **Authority lineage matters.**
|
|
365
|
+
4. **Authority lineage matters.** Provider-derived authority should bind the exact allowed receipt and guarded output to a reviewed extractor; legacy host-trusted derivation remains identifiable in provenance.
|
|
341
366
|
5. **No silent resource expansion.** A different concrete resource becomes an authority delta.
|
|
342
367
|
6. **Task authority ends with the task.** Completion and expiry are independent from provider credential lifetime.
|
|
343
368
|
7. **Authority may shrink, never silently grow.** Delegation and transport changes must preserve non-amplification.
|
|
@@ -352,7 +377,9 @@ See [SECURITY.md](SECURITY.md).
|
|
|
352
377
|
This is still a validation implementation.
|
|
353
378
|
|
|
354
379
|
- Task Lease state is currently process-local.
|
|
355
|
-
-
|
|
380
|
+
- `deriveFromEvidence()` proves consistency with the exact output returned through the trusted Agent Authority guard, but the output is not cryptographically attested by Gmail, GitHub, or another remote provider.
|
|
381
|
+
- The legacy `derive()` API remains host-trusted for compatibility; audit provenance distinguishes it from `execution-evidence-v1` derivation.
|
|
382
|
+
- Source-data changes do not yet automatically invalidate already-derived authority facts.
|
|
356
383
|
- Bindings currently target top-level request context fields.
|
|
357
384
|
- Approved authority deltas are surfaced but not automatically applied back into a live lease.
|
|
358
385
|
- GitHub token-stdin is a developer bridge, not final browser OAuth onboarding.
|
package/ROADMAP.md
CHANGED
|
@@ -77,7 +77,7 @@ 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 — two-provider proof established
|
|
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
|
|
|
@@ -85,26 +85,33 @@ The first real Gmail -> Calendar integration showed that recording a host-suppli
|
|
|
85
85
|
- [x] bind successful guarded outputs to the exact ALLOW receipt, request and output hash
|
|
86
86
|
- [x] add `TaskLease.deriveFromEvidence()` so the caller cannot provide the authority value
|
|
87
87
|
- [x] migrate Gmail sender -> Calendar attendee derivation to the evidence-verified path
|
|
88
|
+
- [x] migrate the real GitHub issue discovery -> comment mutation proof to the same evidence-verified path
|
|
88
89
|
- [x] adversarial tests for value substitution, output/evidence tampering, receipt replay, cross-lease reuse, wrong-operation extraction and dangerous selectors
|
|
90
|
+
- [x] shared conformance fixtures for reviewed operation -> authority-field mappings across Google and GitHub
|
|
89
91
|
- [ ] 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
92
|
- [ ] define freshness/invalidation rules when a source resource changes
|
|
92
93
|
|
|
94
|
+
The shared contract is documented in `docs/authority-extractor-conformance.md`. Google and GitHub now use the same `guard.run()` -> execution evidence -> reviewed extractor -> `deriveFromEvidence()` primitive, and the same conformance suite attacks both mappings.
|
|
95
|
+
|
|
93
96
|
Do **not** build a general semantic policy language unless real integrations require it.
|
|
94
97
|
|
|
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.
|
|
98
|
+
**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.
|
|
96
99
|
|
|
97
|
-
## M4 — Same task, multiple transports
|
|
100
|
+
## M4 — Same task, multiple transports — first proof established
|
|
98
101
|
|
|
99
102
|
Prove Agent Authority is not an MCP product or SDK wrapper.
|
|
100
103
|
|
|
101
|
-
- [
|
|
102
|
-
- [
|
|
103
|
-
- [
|
|
104
|
+
- [x] same Task Lease through ordinary `guard.run()` SDK call
|
|
105
|
+
- [x] same Task Lease through MCP gateway
|
|
106
|
+
- [x] same Task Lease through brokered provider execution
|
|
104
107
|
- [ ] at least one non-bypassable harness/tool-middleware integration
|
|
105
|
-
- [
|
|
108
|
+
- [x] interoperability test vectors across transports
|
|
109
|
+
|
|
110
|
+
`test/transport-invariance.test.js` establishes one `execution-evidence-v1` derived fact from brokered execution, then reuses that exact Task Lease and fact through direct SDK, MCP and brokered execution. The three paths produce the same `allow`, `authority_delta_required` and `task_lease_completed` outcomes, and blocked attempts execute zero host callbacks, MCP upstream calls or brokered provider operations.
|
|
111
|
+
|
|
112
|
+
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.
|
|
106
113
|
|
|
107
|
-
**Success criterion:** changing transport or harness does not expand the task's authority.
|
|
114
|
+
**Success criterion:** changing transport or harness does not expand the task's authority. The SDK/MCP/broker portion is now demonstrated in-process; an external non-bypassable harness/tool-middleware integration remains the final M4 proof.
|
|
108
115
|
|
|
109
116
|
## M5 — Production credential and approval UX
|
|
110
117
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Authority extractor conformance
|
|
2
|
+
|
|
3
|
+
Agent Authority treats provider-derived authority as a small adapter contract, not a general semantic policy language.
|
|
4
|
+
|
|
5
|
+
The strict path is:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
authorized request
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
reviewed provider adapter
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
normalized provider output
|
|
15
|
+
|
|
|
16
|
+
+--> ALLOW receipt
|
|
17
|
+
+--> execution output hash
|
|
18
|
+
|
|
|
19
|
+
v
|
|
20
|
+
adapter.authorityExtractor(request, factKind)
|
|
21
|
+
|
|
|
22
|
+
v
|
|
23
|
+
{ extractor_id, selector }
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
TaskLease.deriveFromEvidence()
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
derived authority fact
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Adapter requirements
|
|
33
|
+
|
|
34
|
+
A provider mapping intended to establish downstream authority SHOULD satisfy all of these:
|
|
35
|
+
|
|
36
|
+
1. **Reviewed operation mapping.** The adapter owns the mapping from Agent Authority `service` + `action` to the external provider operation.
|
|
37
|
+
2. **Normalized authority field.** Provider data is normalized into a small output shape before authority extraction. Credentials and unrelated raw payloads should not be copied into the authority result.
|
|
38
|
+
3. **Fail-closed extractor advertisement.** `adapter.authorityExtractor(request, factKind)` returns an extractor only for an explicitly supported operation/fact-kind pair. Unsupported mappings return `null`.
|
|
39
|
+
4. **Selector, never value.** The extractor returns `{ extractor_id, selector }`. It must not return the derived authority value itself.
|
|
40
|
+
5. **Operation binding.** The extractor rejects receipts from another provider action.
|
|
41
|
+
6. **Canonical output.** The extractor rejects malformed, ambiguous, or non-canonical normalized output.
|
|
42
|
+
7. **Task lineage.** `TaskLease.deriveFromEvidence()` requires an ALLOW receipt from the same mission and Task Lease plus at least one existing parent fact.
|
|
43
|
+
8. **Exact-output integrity.** The output passed to derivation must still hash to the output bound into the execution evidence.
|
|
44
|
+
|
|
45
|
+
This is an integrity contract inside the trusted Agent Authority host/adapter boundary. It is not provider-signed remote attestation.
|
|
46
|
+
|
|
47
|
+
## Current fixtures
|
|
48
|
+
|
|
49
|
+
### Google Gmail sender
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
gmail:thread.read
|
|
53
|
+
-> normalized sender_email
|
|
54
|
+
-> google.gmail.thread.sender-email.v1
|
|
55
|
+
-> email.address
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The Gmail extractor accepts only canonical normalized `sender_email` output and selects `output.sender_email`.
|
|
59
|
+
|
|
60
|
+
### GitHub selected issue
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
github:issue.list
|
|
64
|
+
+ root-bound repository
|
|
65
|
+
+ root-bound fixture_marker
|
|
66
|
+
-> exactly one normalized marker match
|
|
67
|
+
-> github.issue.list.selected-number.v1
|
|
68
|
+
-> github.issue.number
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The GitHub adapter performs marker matching against the provider response, does not expose issue bodies in the normalized output, and the extractor selects only `output.selected_issue_number` when exactly one non-pull-request issue matched.
|
|
72
|
+
|
|
73
|
+
## Shared adversarial conformance suite
|
|
74
|
+
|
|
75
|
+
`test/provider-authority-conformance.test.js` applies the same strict-path checks to both provider fixtures:
|
|
76
|
+
|
|
77
|
+
- positive derivation obtains the value from the evidence-bound output rather than caller input;
|
|
78
|
+
- modified output under unchanged evidence is rejected;
|
|
79
|
+
- execution evidence cannot be replayed under a second ALLOW receipt;
|
|
80
|
+
- receipt/evidence from one Task Lease cannot establish authority in another lease;
|
|
81
|
+
- an extractor cannot be reused with evidence from another provider operation.
|
|
82
|
+
|
|
83
|
+
Provider-specific tests additionally verify canonical normalization, exact REST mappings, extractor advertisement, and ambiguity failure.
|
|
84
|
+
|
|
85
|
+
## What conformance does not prove
|
|
86
|
+
|
|
87
|
+
Passing this contract does not prove that:
|
|
88
|
+
|
|
89
|
+
- a provider cryptographically signed the normalized result;
|
|
90
|
+
- the trusted host itself is non-malicious;
|
|
91
|
+
- a source resource has not changed since the read;
|
|
92
|
+
- a derived fact is automatically invalidated when provider data changes;
|
|
93
|
+
- an agent cannot bypass Agent Authority through a separate credential or unguarded provider path.
|
|
94
|
+
|
|
95
|
+
Those are separate trust, freshness, and deployment-boundary problems and should not be hidden inside the extractor API.
|
package/docs/evidence.md
CHANGED
|
@@ -12,7 +12,7 @@ The guarantee applies to effects that actually pass through the Agent Authority
|
|
|
12
12
|
|
|
13
13
|
## Execution-bound derived authority
|
|
14
14
|
|
|
15
|
-
The strict derived-authority path
|
|
15
|
+
The strict derived-authority path binds provider-derived authority to the exact output returned by an authorized `guard.run()` effect.
|
|
16
16
|
|
|
17
17
|
A successful guarded effect returns three relevant records:
|
|
18
18
|
|
|
@@ -56,6 +56,36 @@ Execution evidence is an integrity mechanism inside the trusted host/runtime bou
|
|
|
56
56
|
|
|
57
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
58
|
|
|
59
|
+
## Two-provider authority-extractor conformance
|
|
60
|
+
|
|
61
|
+
The same strict primitive is now exercised by two independent provider mappings:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Google Gmail
|
|
65
|
+
thread.read
|
|
66
|
+
-> sender_email
|
|
67
|
+
-> reviewed extractor
|
|
68
|
+
-> email.address authority
|
|
69
|
+
|
|
70
|
+
GitHub
|
|
71
|
+
issue.list
|
|
72
|
+
-> selected_issue_number
|
|
73
|
+
-> reviewed extractor
|
|
74
|
+
-> github.issue.number authority
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`test/provider-authority-conformance.test.js` applies the same contract to both mappings:
|
|
78
|
+
|
|
79
|
+
- positive derivation gets its value from the evidence-bound output rather than caller input;
|
|
80
|
+
- modifying the selected output under unchanged evidence is rejected;
|
|
81
|
+
- evidence replay under another ALLOW receipt is rejected;
|
|
82
|
+
- cross-Task-Lease receipt/evidence reuse is rejected;
|
|
83
|
+
- an extractor rejects evidence from another operation.
|
|
84
|
+
|
|
85
|
+
Provider-specific tests additionally verify canonical normalization and fail-closed extractor advertisement. The contract is documented in [Authority extractor conformance](authority-extractor-conformance.md).
|
|
86
|
+
|
|
87
|
+
This is the current evidence that execution-bound derived authority is a reusable provider primitive rather than a Gmail-only special case.
|
|
88
|
+
|
|
59
89
|
## Cross-provider derived authority — Gmail → Calendar
|
|
60
90
|
|
|
61
91
|
Agent Authority includes a real Google provider mapping, an adversarial cross-provider test, a live validation script, and an opt-in GitHub Actions workflow.
|
|
@@ -158,13 +188,13 @@ Taken together, the deterministic Task Lease tests plus the controlled provider
|
|
|
158
188
|
|
|
159
189
|
See [Live Gmail → Calendar validation](live-google-validation.md).
|
|
160
190
|
|
|
161
|
-
## Live derived
|
|
191
|
+
## Live evidence-derived mutation — GitHub
|
|
162
192
|
|
|
163
193
|
Public fixture: [issue #9](https://github.com/Null-Square/agent-authority/issues/9)
|
|
164
194
|
|
|
165
195
|
Validation workflow: CI job `live-derived-github-mutation`
|
|
166
196
|
|
|
167
|
-
Passing run: [CI run
|
|
197
|
+
Passing evidence-derived run: [CI run 262](https://github.com/Null-Square/agent-authority/actions/runs/32600963479)
|
|
168
198
|
|
|
169
199
|
The job uses a GitHub Actions token with:
|
|
170
200
|
|
|
@@ -173,25 +203,40 @@ contents: read
|
|
|
173
203
|
issues: write
|
|
174
204
|
```
|
|
175
205
|
|
|
176
|
-
The Task Lease
|
|
206
|
+
The Task Lease begins with two explicit authority roots:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
repository = Null-Square/agent-authority
|
|
210
|
+
fixture_marker = agent-authority-live-fixture-v1
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Both values are bound to the `issue.list` request. The brokered GitHub provider adapter owns the external issue-list operation and normalizes the provider response. Issue bodies are used internally for marker matching but are not copied into the normalized authority output.
|
|
177
214
|
|
|
178
215
|
### Executed path
|
|
179
216
|
|
|
180
217
|
```text
|
|
181
|
-
Task
|
|
182
|
-
|
|
218
|
+
Task roots
|
|
219
|
+
repository + fixture marker
|
|
183
220
|
|
|
|
184
221
|
v
|
|
185
|
-
ALLOW
|
|
222
|
+
ALLOW github:issue.list
|
|
223
|
+
through reviewed provider adapter
|
|
224
|
+
|
|
|
225
|
+
+--> exact output-bound execution evidence
|
|
226
|
+
|
|
|
227
|
+
v
|
|
228
|
+
adapter selects exactly one marker match
|
|
229
|
+
selected_issue_number = 9
|
|
186
230
|
|
|
|
187
231
|
v
|
|
188
|
-
|
|
232
|
+
reviewed GitHub issue-number extractor
|
|
189
233
|
|
|
|
190
234
|
v
|
|
191
|
-
|
|
235
|
+
TaskLease.deriveFromEvidence()
|
|
236
|
+
issue_number = 9
|
|
192
237
|
|
|
|
193
238
|
v
|
|
194
|
-
ALLOW one real comment
|
|
239
|
+
ALLOW one real github:issue.comment on #9
|
|
195
240
|
|
|
|
196
241
|
+--> attempt comment on #1
|
|
197
242
|
| -> authority_delta_required
|
|
@@ -208,38 +253,41 @@ complete Task Lease
|
|
|
208
253
|
The passing job recorded:
|
|
209
254
|
|
|
210
255
|
```text
|
|
211
|
-
ALLOW ->
|
|
212
|
-
|
|
256
|
+
ALLOW -> selected issue #9: Agent Authority live validation fixture — do not close
|
|
257
|
+
Evidence-verified authority -> issue #9
|
|
213
258
|
ALLOW -> real GitHub comment mutation executed
|
|
214
259
|
STEP-UP -> unrelated issue #1 blocked before provider mutation
|
|
215
260
|
DENY -> post-completion mutation blocked for issue #9
|
|
261
|
+
PASS -> GitHub provider output became downstream authority only through execution evidence and a reviewed extractor
|
|
216
262
|
Provider calls observed before cleanup: reads=1, task_mutations=1
|
|
217
263
|
```
|
|
218
264
|
|
|
219
|
-
The temporary validation comment
|
|
265
|
+
The temporary validation comment was deleted by harness cleanup after the proof. Cleanup remains intentionally outside the Task Lease authority path and is counted separately.
|
|
220
266
|
|
|
221
267
|
### What this proves
|
|
222
268
|
|
|
223
|
-
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
- a
|
|
228
|
-
-
|
|
229
|
-
- the
|
|
269
|
+
- the same execution-evidence + reviewed-extractor primitive used for Gmail works against a second real provider;
|
|
270
|
+
- the brokered GitHub adapter, rather than arbitrary host extraction code, owns provider response normalization;
|
|
271
|
+
- the caller does not provide the issue number to `deriveFromEvidence()`;
|
|
272
|
+
- repository and discovery marker are explicit Task Lease roots;
|
|
273
|
+
- a real provider mutation is limited to the issue selected from the authorized provider result;
|
|
274
|
+
- another issue causes zero additional task-side provider mutation calls;
|
|
275
|
+
- completing the Task Lease prevents reuse of the previously authorized issue;
|
|
276
|
+
- the provider credential can remain valid after task authority disappears.
|
|
230
277
|
|
|
231
278
|
### What this does not prove
|
|
232
279
|
|
|
233
|
-
-
|
|
280
|
+
- GitHub cryptographically attests the normalized Agent Authority output;
|
|
281
|
+
- source issue changes automatically invalidate a derived fact;
|
|
234
282
|
- an agent cannot bypass Agent Authority if it independently possesses the provider credential or another unguarded provider path;
|
|
235
283
|
- Task Lease state is durable across process failure;
|
|
236
284
|
- the current prototype is ready for adversarial production use.
|
|
237
285
|
|
|
238
286
|
## Live provider read boundary — GitHub
|
|
239
287
|
|
|
240
|
-
CI also runs `demo:live-github` against the
|
|
288
|
+
CI also runs `demo:live-github` against the GitHub API.
|
|
241
289
|
|
|
242
|
-
It proves that one repository permitted by the Task Lease causes one live
|
|
290
|
+
It proves that one repository permitted by the Task Lease causes one live provider read while another repository produces `authority_delta_required` before a second provider request occurs.
|
|
243
291
|
|
|
244
292
|
## Network-boundary integration test
|
|
245
293
|
|
|
@@ -263,6 +311,7 @@ The test suite also covers:
|
|
|
263
311
|
- parent lineage is required;
|
|
264
312
|
- legacy host-trusted derivation records its extraction selector;
|
|
265
313
|
- strict execution-evidence derivation rejects substitution/replay/tampering cases;
|
|
314
|
+
- the shared provider conformance suite applies the same attacks to Google and GitHub mappings;
|
|
266
315
|
- explicit mission deny rules remain the ceiling;
|
|
267
316
|
- lease expiry and mission expiry are enforced against a consistent evaluation clock.
|
|
268
317
|
|
|
@@ -277,8 +326,8 @@ Current pull requests run:
|
|
|
277
326
|
- package checks;
|
|
278
327
|
- coverage;
|
|
279
328
|
- live GitHub read validation;
|
|
280
|
-
- live derived GitHub mutation validation for trusted in-repository branches;
|
|
281
|
-
- Google
|
|
329
|
+
- live evidence-derived GitHub mutation validation for trusted in-repository branches;
|
|
330
|
+
- Google and GitHub provider/extractor conformance and execution-evidence adversarial tests;
|
|
282
331
|
- CodeQL.
|
|
283
332
|
|
|
284
333
|
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,19 +13,24 @@ 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.3
|
|
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.3 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` API plus both reviewed provider authority extractors:
|
|
22
|
+
|
|
23
|
+
- `gmailThreadSenderAuthorityExtractor` from `@nullsquare/agent-authority/providers/google`;
|
|
24
|
+
- `githubIssueListSelectedNumberAuthorityExtractor` from `@nullsquare/agent-authority/providers/github`.
|
|
25
|
+
|
|
26
|
+
This makes the registry artifact verification cover the same two-provider execution-evidence surface exercised by the repository conformance suite.
|
|
22
27
|
|
|
23
28
|
## npm vs GitHub release surfaces
|
|
24
29
|
|
|
25
30
|
Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
|
|
26
31
|
|
|
27
32
|
- **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.
|
|
33
|
+
- **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.3`. A release must be created explicitly or by release automation.
|
|
29
34
|
- **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
35
|
|
|
31
36
|
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,117 @@
|
|
|
1
|
+
# Task Lease transport invariance
|
|
2
|
+
|
|
3
|
+
Agent Authority treats SDK calls, MCP calls and brokered provider execution as execution paths, not separate authority models.
|
|
4
|
+
|
|
5
|
+
The property under test is:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
same Task Lease + same established authority fact
|
|
9
|
+
|
|
|
10
|
+
+----------+----------+
|
|
11
|
+
| | |
|
|
12
|
+
direct MCP brokered
|
|
13
|
+
guard.run() gateway provider
|
|
14
|
+
| | |
|
|
15
|
+
+----------+----------+
|
|
16
|
+
|
|
|
17
|
+
same authority
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Changing the transport must not broaden task authority.
|
|
21
|
+
|
|
22
|
+
## Executable proof
|
|
23
|
+
|
|
24
|
+
`test/transport-invariance.test.js` creates one Task Lease and one derived authority fact.
|
|
25
|
+
|
|
26
|
+
The fact is first established from a brokered provider result using the strict evidence path:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Task Lease root
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
brokered item.discover
|
|
33
|
+
|
|
|
34
|
+
+--> Task-Lease ALLOW receipt
|
|
35
|
+
+--> exact output hash evidence
|
|
36
|
+
|
|
|
37
|
+
v
|
|
38
|
+
reviewed test extractor
|
|
39
|
+
|
|
|
40
|
+
v
|
|
41
|
+
deriveFromEvidence()
|
|
42
|
+
|
|
|
43
|
+
v
|
|
44
|
+
fact:selected-item = alpha
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That exact Task Lease and fact are then used through three execution paths:
|
|
48
|
+
|
|
49
|
+
1. ordinary in-process `guard.run()`;
|
|
50
|
+
2. `MissionMcpGateway` configured with the Task Lease;
|
|
51
|
+
3. `ExecutingAuthorityRuntime.executeTaskLease()` with a brokered provider adapter.
|
|
52
|
+
|
|
53
|
+
For `item = alpha`, every path allows execution.
|
|
54
|
+
|
|
55
|
+
For `item = beta`, every path returns the same task-level authority delta:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
authority_delta_required
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The blocked direct callback, MCP upstream call and brokered provider operation all remain unexecuted.
|
|
62
|
+
|
|
63
|
+
After the Task Lease is completed, all three paths return:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
task_lease_completed
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Again, no blocked execution reaches the host callback or provider boundary.
|
|
70
|
+
|
|
71
|
+
## Broker behavior
|
|
72
|
+
|
|
73
|
+
`ExecutingAuthorityRuntime.executeTaskLease()` evaluates the Task Lease before adapter readiness or provider execution.
|
|
74
|
+
|
|
75
|
+
A lease-level `require_approval` result is returned as-is. Brokered execution does not consume a mission-level one-time approval to bypass the narrower Task Lease. Applying an explicitly approved authority delta back into a live Task Lease is separate roadmap work.
|
|
76
|
+
|
|
77
|
+
Successful brokered Task Lease execution returns execution evidence bound to the Task-Lease receipt and exact provider output, so strict derived authority can originate from brokered execution as well as from `guard.run()`.
|
|
78
|
+
|
|
79
|
+
## MCP behavior
|
|
80
|
+
|
|
81
|
+
`MissionMcpGateway` remains backward-compatible with Mission-only use, but now accepts exactly one authority source:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
mission OR lease
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When configured with a Task Lease, each tool call is evaluated through that lease before `upstream.callTool()` can run.
|
|
88
|
+
|
|
89
|
+
MCP result metadata includes:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
io.nullsquare.agent-authority/decision
|
|
93
|
+
io.nullsquare.agent-authority/code
|
|
94
|
+
io.nullsquare.agent-authority/receipt_hash
|
|
95
|
+
io.nullsquare.agent-authority/task_lease_id
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The remote MCP handler and loopback proxy can pass the same Task Lease into the gateway.
|
|
99
|
+
|
|
100
|
+
## What this proves
|
|
101
|
+
|
|
102
|
+
- Task-Lease narrowing is no longer specific to the direct SDK guard;
|
|
103
|
+
- MCP cannot silently fall back to Mission-only authority when explicitly configured with a Task Lease;
|
|
104
|
+
- brokered provider execution can enforce the same Task Lease before credential-backed execution;
|
|
105
|
+
- one derived fact can constrain all three execution paths;
|
|
106
|
+
- task completion invalidates the same authority across all three paths;
|
|
107
|
+
- broker credentials may remain connected after task authority disappears.
|
|
108
|
+
|
|
109
|
+
## What this does not prove yet
|
|
110
|
+
|
|
111
|
+
- a hostile harness cannot bypass Agent Authority through an entirely separate unguarded tool path;
|
|
112
|
+
- Task Lease state survives process restart;
|
|
113
|
+
- the same lease is serialized and recovered across separate processes or hosts;
|
|
114
|
+
- an approved authority delta is durably applied back into a running lease;
|
|
115
|
+
- provider outputs are cryptographically attested by providers.
|
|
116
|
+
|
|
117
|
+
The remaining M4 target is at least one real harness/tool-middleware integration where executable tool calls cannot bypass the Task Lease boundary.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { CredentialBroker } from '../src/connections.js';
|
|
1
2
|
import { AuthorityRuntime } from '../src/index.js';
|
|
2
3
|
import {
|
|
3
4
|
AuthorityApprovalRequiredError,
|
|
4
5
|
AuthorityDeniedError,
|
|
5
6
|
createTaskLeaseGuard
|
|
6
7
|
} from '../src/guard.js';
|
|
8
|
+
import { createGitHubProviderAdapter } from '../src/providers/github.js';
|
|
7
9
|
import { createTaskLease } from '../src/task-lease.js';
|
|
8
10
|
|
|
9
11
|
const repository = process.env.AA_VALIDATION_REPOSITORY || 'Null-Square/agent-authority';
|
|
@@ -43,6 +45,12 @@ const lease = createTaskLease({
|
|
|
43
45
|
kind: 'github.repository',
|
|
44
46
|
value: repository,
|
|
45
47
|
source: 'validation-task'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
fact_id: 'fact:fixture-marker',
|
|
51
|
+
kind: 'github.issue.marker',
|
|
52
|
+
value: marker,
|
|
53
|
+
source: 'validation-task'
|
|
46
54
|
}
|
|
47
55
|
],
|
|
48
56
|
bindings: [
|
|
@@ -52,6 +60,12 @@ const lease = createTaskLease({
|
|
|
52
60
|
context_field: 'repository',
|
|
53
61
|
fact_id: 'fact:repository'
|
|
54
62
|
},
|
|
63
|
+
{
|
|
64
|
+
service: 'github',
|
|
65
|
+
action: 'issue.list',
|
|
66
|
+
context_field: 'fixture_marker',
|
|
67
|
+
fact_id: 'fact:fixture-marker'
|
|
68
|
+
},
|
|
55
69
|
{
|
|
56
70
|
service: 'github',
|
|
57
71
|
action: 'issue.comment',
|
|
@@ -68,10 +82,20 @@ const lease = createTaskLease({
|
|
|
68
82
|
});
|
|
69
83
|
|
|
70
84
|
const guard = createTaskLeaseGuard({ lease, runtime: new AuthorityRuntime() });
|
|
71
|
-
const
|
|
85
|
+
const broker = new CredentialBroker();
|
|
86
|
+
broker.connect({
|
|
87
|
+
principal_id: mission.principal.id,
|
|
88
|
+
service: 'github',
|
|
89
|
+
auth_kind: 'github-actions-token',
|
|
90
|
+
credential: { access_token: token },
|
|
91
|
+
scopes: ['contents:read', 'issues:write']
|
|
92
|
+
});
|
|
93
|
+
const adapter = createGitHubProviderAdapter({ broker });
|
|
94
|
+
|
|
95
|
+
const cleanupHeaders = {
|
|
72
96
|
accept: 'application/vnd.github+json',
|
|
73
97
|
authorization: `Bearer ${token}`,
|
|
74
|
-
'user-agent': 'agent-authority-derived-mutation-validation',
|
|
98
|
+
'user-agent': 'agent-authority-derived-mutation-validation-cleanup',
|
|
75
99
|
'x-github-api-version': '2022-11-28'
|
|
76
100
|
};
|
|
77
101
|
|
|
@@ -80,90 +104,83 @@ let providerMutationCalls = 0;
|
|
|
80
104
|
let cleanupCalls = 0;
|
|
81
105
|
let createdCommentId = null;
|
|
82
106
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
function discoveryRequest() {
|
|
108
|
+
return {
|
|
109
|
+
service: 'github',
|
|
110
|
+
action: 'issue.list',
|
|
111
|
+
context: {
|
|
112
|
+
repository,
|
|
113
|
+
fixture_marker: marker,
|
|
114
|
+
state: 'open',
|
|
115
|
+
per_page: 100
|
|
116
|
+
}
|
|
117
|
+
};
|
|
94
118
|
}
|
|
95
119
|
|
|
96
120
|
async function discoverFixtureIssue() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
async () => {
|
|
104
|
-
providerReadCalls += 1;
|
|
105
|
-
const issues = await githubJson(
|
|
106
|
-
`https://api.github.com/repos/${owner}/${repo}/issues?state=open&per_page=100`
|
|
107
|
-
);
|
|
108
|
-
const fixture = issues.find((issue) => !issue.pull_request && issue.body?.includes(marker));
|
|
109
|
-
if (!fixture) throw new Error(`validation fixture with marker ${marker} was not found`);
|
|
110
|
-
return { number: fixture.number, title: fixture.title };
|
|
111
|
-
}
|
|
112
|
-
);
|
|
121
|
+
const request = discoveryRequest();
|
|
122
|
+
return guard.run(request, async () => {
|
|
123
|
+
providerReadCalls += 1;
|
|
124
|
+
return adapter.execute({ mission, request });
|
|
125
|
+
});
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
async function commentOnIssue(issueNumber, body) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
method: 'POST',
|
|
128
|
-
headers: { 'content-type': 'application/json' },
|
|
129
|
-
body: JSON.stringify({ body })
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
return { id: comment.id, html_url: comment.html_url };
|
|
133
|
-
}
|
|
134
|
-
);
|
|
129
|
+
const request = {
|
|
130
|
+
service: 'github',
|
|
131
|
+
action: 'issue.comment',
|
|
132
|
+
context: { repository, issue_number: issueNumber, body }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return guard.run(request, async () => {
|
|
136
|
+
providerMutationCalls += 1;
|
|
137
|
+
return adapter.execute({ mission, request });
|
|
138
|
+
});
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
async function cleanupComment(commentId) {
|
|
138
142
|
cleanupCalls += 1;
|
|
139
|
-
await
|
|
143
|
+
const response = await fetch(
|
|
140
144
|
`https://api.github.com/repos/${owner}/${repo}/issues/comments/${commentId}`,
|
|
141
|
-
{ method: 'DELETE' }
|
|
145
|
+
{ method: 'DELETE', headers: cleanupHeaders }
|
|
142
146
|
);
|
|
147
|
+
if (!response.ok && response.status !== 404) {
|
|
148
|
+
const body = await response.text();
|
|
149
|
+
throw new Error(`GitHub cleanup ${response.status}: ${body.slice(0, 300)}`);
|
|
150
|
+
}
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
try {
|
|
146
154
|
console.log(`Task root repository: ${repository}`);
|
|
147
|
-
console.log(
|
|
155
|
+
console.log(`Task root fixture marker: ${marker}`);
|
|
156
|
+
console.log('1. Discover fixture through the reviewed GitHub provider adapter');
|
|
148
157
|
const discovered = await discoverFixtureIssue();
|
|
149
|
-
console.log(` ALLOW -> discovered issue #${discovered.output.number}: ${discovered.output.title}`);
|
|
150
158
|
|
|
151
|
-
|
|
159
|
+
if (discovered.output.selected_issue_match_count !== 1) {
|
|
160
|
+
throw new Error(`expected exactly one fixture marker match, got ${discovered.output.selected_issue_match_count}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(` ALLOW -> selected issue #${discovered.output.selected_issue_number}: ${discovered.output.selected_issue_title}`);
|
|
163
|
+
|
|
164
|
+
const extractor = adapter.authorityExtractor(discoveryRequest(), 'github.issue.number');
|
|
165
|
+
if (!extractor) throw new Error('GitHub provider did not advertise the issue-number authority extractor');
|
|
166
|
+
|
|
167
|
+
const issueFact = lease.deriveFromEvidence({
|
|
152
168
|
fact_id: 'fact:discovered-issue-number',
|
|
153
169
|
kind: 'github.issue.number',
|
|
154
|
-
|
|
155
|
-
from: ['fact:repository'],
|
|
170
|
+
from: ['fact:repository', 'fact:fixture-marker'],
|
|
156
171
|
receipt: discovered.receipt,
|
|
157
|
-
|
|
172
|
+
evidence: discovered.evidence,
|
|
173
|
+
output: discovered.output,
|
|
174
|
+
extractor
|
|
158
175
|
});
|
|
159
|
-
console.log(`2.
|
|
176
|
+
console.log(`2. Evidence-verified authority -> issue #${issueFact.value}`);
|
|
160
177
|
|
|
161
|
-
const validationBody = `Agent Authority live derived
|
|
162
|
-
const allowedMutation = await commentOnIssue(
|
|
163
|
-
createdCommentId = allowedMutation.output.
|
|
178
|
+
const validationBody = `Agent Authority live evidence-derived authorization validation (${new Date().toISOString()}). Temporary comment; CI removes it after the proof.`;
|
|
179
|
+
const allowedMutation = await commentOnIssue(issueFact.value, validationBody);
|
|
180
|
+
createdCommentId = allowedMutation.output.comment_id;
|
|
164
181
|
console.log(`3. ALLOW -> real GitHub comment mutation executed (comment ${createdCommentId})`);
|
|
165
182
|
|
|
166
|
-
const unrelatedIssue =
|
|
183
|
+
const unrelatedIssue = issueFact.value === 1 ? 2 : 1;
|
|
167
184
|
try {
|
|
168
185
|
await commentOnIssue(unrelatedIssue, 'THIS MUST NEVER REACH GITHUB');
|
|
169
186
|
throw new Error('unrelated issue mutation unexpectedly executed');
|
|
@@ -178,15 +195,15 @@ try {
|
|
|
178
195
|
throw new Error(`expected exactly one task-side provider mutation before completion, got ${providerMutationCalls}`);
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
lease.complete('live derived
|
|
198
|
+
lease.complete('live evidence-derived mutation validation complete');
|
|
182
199
|
try {
|
|
183
|
-
await commentOnIssue(
|
|
200
|
+
await commentOnIssue(issueFact.value, 'THIS MUST NOT RUN AFTER TASK COMPLETION');
|
|
184
201
|
throw new Error('post-completion mutation unexpectedly executed');
|
|
185
202
|
} catch (error) {
|
|
186
203
|
if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') {
|
|
187
204
|
throw error;
|
|
188
205
|
}
|
|
189
|
-
console.log(`5. DENY -> post-completion mutation blocked for issue #${
|
|
206
|
+
console.log(`5. DENY -> post-completion mutation blocked for issue #${issueFact.value}`);
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
if (providerReadCalls !== 1) {
|
|
@@ -196,7 +213,7 @@ try {
|
|
|
196
213
|
throw new Error(`expected exactly one provider mutation after blocked attempts, got ${providerMutationCalls}`);
|
|
197
214
|
}
|
|
198
215
|
|
|
199
|
-
console.log('PASS ->
|
|
216
|
+
console.log('PASS -> GitHub provider output became downstream authority only through execution evidence and a reviewed extractor');
|
|
200
217
|
console.log('PASS -> unrelated and post-completion mutations produced zero additional provider mutation calls');
|
|
201
218
|
} finally {
|
|
202
219
|
if (createdCommentId) {
|
package/package.json
CHANGED
package/src/execution.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthorityRuntime, createReceipt } from './index.js';
|
|
2
|
+
import { createExecutionEvidence } from './authority-evidence.js';
|
|
2
3
|
|
|
3
4
|
function executionFailure(mission, request, code, reason, extra = {}) {
|
|
4
5
|
const result = { decision: 'deny', code, reason, ...extra };
|
|
@@ -115,10 +116,39 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Execute through the broker while preserving Task Lease narrowing.
|
|
121
|
+
*
|
|
122
|
+
* A lease-level REQUIRE_APPROVAL is returned before adapter readiness or
|
|
123
|
+
* provider execution. It is not converted into a mission-level one-time
|
|
124
|
+
* approval because applying an authority delta back into a live lease is a
|
|
125
|
+
* separate, not-yet-implemented capability.
|
|
126
|
+
*/
|
|
127
|
+
async executeTaskLease(lease, request) {
|
|
128
|
+
if (!lease || typeof lease.evaluate !== 'function' || !lease.mission) {
|
|
129
|
+
throw new Error('task lease with mission and evaluate() is required');
|
|
130
|
+
}
|
|
131
|
+
return this.execute(lease.mission, request, { lease });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async execute(missionInput, request, { lease = null } = {}) {
|
|
135
|
+
if (lease && lease.mission?.mission_id !== missionInput?.mission_id) {
|
|
136
|
+
throw new Error('task lease mission does not match execution mission');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let evaluation = lease
|
|
140
|
+
? lease.evaluate(this, request)
|
|
141
|
+
: this.evaluate(missionInput, request);
|
|
142
|
+
|
|
120
143
|
if (evaluation.result.decision === 'deny') return { ...evaluation, output: null };
|
|
121
144
|
|
|
145
|
+
// A Task Lease is the narrowest authority object. Do not let brokered
|
|
146
|
+
// execution reinterpret a lease-level authority delta as a broader mission
|
|
147
|
+
// approval. This keeps broker behavior aligned with guard.run() and MCP.
|
|
148
|
+
if (lease && evaluation.result.decision !== 'allow') {
|
|
149
|
+
return { ...evaluation, output: null };
|
|
150
|
+
}
|
|
151
|
+
|
|
122
152
|
const adapter = this.adapters.resolve(request.service);
|
|
123
153
|
if (!adapter) {
|
|
124
154
|
return executionFailure(missionInput, request, 'adapter_unavailable', `no adapter is registered for ${request.service}`);
|
|
@@ -148,6 +178,7 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
|
|
|
148
178
|
|
|
149
179
|
try {
|
|
150
180
|
const output = await adapter.execute({ mission: missionInput, request });
|
|
181
|
+
const evidence = createExecutionEvidence({ receipt: evaluation.receipt, output });
|
|
151
182
|
let usage = null;
|
|
152
183
|
if (budgetCheck && !budgetCheck.result) {
|
|
153
184
|
const spent = this.usage.record(missionInput.mission_id, budgetCheck.currency, budgetCheck.amount);
|
|
@@ -160,7 +191,7 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
|
|
|
160
191
|
if (executionRecord && this.executions) {
|
|
161
192
|
this.executions.complete({ mission: missionInput, request, receipt_id: evaluation.receipt?.receipt_id || null });
|
|
162
193
|
}
|
|
163
|
-
return { ...evaluation, output, usage, execution: executionRecord || null };
|
|
194
|
+
return { ...evaluation, output, evidence, usage, execution: executionRecord || null };
|
|
164
195
|
} catch (error) {
|
|
165
196
|
if (executionRecord && this.executions) {
|
|
166
197
|
this.executions.uncertain({ mission: missionInput, request, error_code: error.code || 'provider_error' });
|
package/src/mcp-gateway.js
CHANGED
|
@@ -31,6 +31,15 @@ export function isDeclaredReadOnlyTool(tool) {
|
|
|
31
31
|
return tool?.annotations?.readOnlyHint === true;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function authorityMeta(evaluation = {}) {
|
|
35
|
+
return {
|
|
36
|
+
'io.nullsquare.agent-authority/decision': evaluation.result?.decision || 'deny',
|
|
37
|
+
'io.nullsquare.agent-authority/code': evaluation.result?.code || null,
|
|
38
|
+
'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null,
|
|
39
|
+
'io.nullsquare.agent-authority/task_lease_id': evaluation.receipt?.task_lease_id || null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function deniedToolResult(result, extra = {}) {
|
|
35
44
|
return {
|
|
36
45
|
content: [{
|
|
@@ -47,32 +56,43 @@ function deniedToolResult(result, extra = {}) {
|
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
50
|
-
* Small
|
|
59
|
+
* Small policy gateway for MCP tools.
|
|
51
60
|
*
|
|
52
61
|
* The upstream object only needs two methods:
|
|
53
62
|
* listTools(params?) -> { tools: [...] }
|
|
54
63
|
* callTool(params) -> MCP CallToolResult
|
|
55
64
|
*
|
|
56
|
-
*
|
|
65
|
+
* The gateway accepts either a Mission or a Task Lease. When a Task Lease is
|
|
66
|
+
* supplied, every MCP tool call is evaluated through that exact lease before
|
|
67
|
+
* the upstream callback can run. This keeps transport changes from bypassing
|
|
68
|
+
* task-level narrowing.
|
|
69
|
+
*
|
|
70
|
+
* The gateway still defaults to read-only enforcement. A tool is considered
|
|
57
71
|
* read-only only when its MCP annotations explicitly set readOnlyHint=true.
|
|
58
|
-
* Write support
|
|
59
|
-
* is intentionally not inferred from tool names.
|
|
72
|
+
* Write support must be enabled deliberately; it is never inferred from names.
|
|
60
73
|
*/
|
|
61
74
|
export class MissionMcpGateway {
|
|
62
75
|
constructor({
|
|
63
76
|
mission,
|
|
77
|
+
lease,
|
|
64
78
|
runtime,
|
|
65
79
|
upstream,
|
|
66
80
|
service = 'mcp:upstream',
|
|
67
81
|
readOnly = true,
|
|
68
82
|
contextMapper = contextFromToolArguments
|
|
69
83
|
} = {}) {
|
|
70
|
-
if (
|
|
84
|
+
if ((mission && lease) || (!mission && !lease)) {
|
|
85
|
+
throw new Error('provide exactly one of mission or lease');
|
|
86
|
+
}
|
|
87
|
+
if (lease && (typeof lease.evaluate !== 'function' || !lease.mission)) {
|
|
88
|
+
throw new Error('lease must provide mission and evaluate(runtime, request)');
|
|
89
|
+
}
|
|
71
90
|
if (!runtime || typeof runtime.evaluate !== 'function') throw new Error('authority runtime is required');
|
|
72
91
|
if (!upstream || typeof upstream.listTools !== 'function' || typeof upstream.callTool !== 'function') {
|
|
73
92
|
throw new Error('upstream MCP client must implement listTools() and callTool()');
|
|
74
93
|
}
|
|
75
|
-
this.mission = mission;
|
|
94
|
+
this.mission = mission || lease.mission;
|
|
95
|
+
this.lease = lease || null;
|
|
76
96
|
this.runtime = runtime;
|
|
77
97
|
this.upstream = upstream;
|
|
78
98
|
this.service = service;
|
|
@@ -81,6 +101,12 @@ export class MissionMcpGateway {
|
|
|
81
101
|
this.tools = new Map();
|
|
82
102
|
}
|
|
83
103
|
|
|
104
|
+
evaluate(request) {
|
|
105
|
+
return this.lease
|
|
106
|
+
? this.lease.evaluate(this.runtime, request)
|
|
107
|
+
: this.runtime.evaluate(this.mission, request);
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
async refreshTools(params = undefined) {
|
|
85
111
|
const listed = await this.upstream.listTools(params);
|
|
86
112
|
for (const tool of listed.tools || []) this.tools.set(tool.name, tool);
|
|
@@ -122,11 +148,9 @@ export class MissionMcpGateway {
|
|
|
122
148
|
action: mcpToolAction(toolName),
|
|
123
149
|
context
|
|
124
150
|
};
|
|
125
|
-
const evaluation = this.
|
|
151
|
+
const evaluation = this.evaluate(request);
|
|
126
152
|
if (evaluation.result.decision !== 'allow') {
|
|
127
|
-
return deniedToolResult(evaluation.result,
|
|
128
|
-
'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null
|
|
129
|
-
});
|
|
153
|
+
return deniedToolResult(evaluation.result, authorityMeta(evaluation));
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
const output = await this.upstream.callTool(params);
|
|
@@ -134,8 +158,7 @@ export class MissionMcpGateway {
|
|
|
134
158
|
...output,
|
|
135
159
|
_meta: {
|
|
136
160
|
...(output?._meta || {}),
|
|
137
|
-
|
|
138
|
-
'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null
|
|
161
|
+
...authorityMeta(evaluation)
|
|
139
162
|
}
|
|
140
163
|
};
|
|
141
164
|
}
|
package/src/mcp-remote.js
CHANGED
|
@@ -54,6 +54,7 @@ export class RemoteMcpUpstream {
|
|
|
54
54
|
|
|
55
55
|
export function createMcpGatewayHandler({
|
|
56
56
|
mission,
|
|
57
|
+
lease,
|
|
57
58
|
runtime,
|
|
58
59
|
upstream,
|
|
59
60
|
upstreamUrl,
|
|
@@ -63,6 +64,7 @@ export function createMcpGatewayHandler({
|
|
|
63
64
|
const resolvedUpstream = upstream || new RemoteMcpUpstream({ url: upstreamUrl });
|
|
64
65
|
const gateway = new MissionMcpGateway({
|
|
65
66
|
mission,
|
|
67
|
+
lease,
|
|
66
68
|
runtime,
|
|
67
69
|
upstream: resolvedUpstream,
|
|
68
70
|
service,
|
|
@@ -74,7 +76,9 @@ export function createMcpGatewayHandler({
|
|
|
74
76
|
{
|
|
75
77
|
name: 'agent-authority-gateway',
|
|
76
78
|
version: '0.3.0',
|
|
77
|
-
description:
|
|
79
|
+
description: lease
|
|
80
|
+
? 'Task-Lease-aware policy gateway for MCP tools'
|
|
81
|
+
: 'Mission-aware policy gateway for MCP tools'
|
|
78
82
|
},
|
|
79
83
|
{ capabilities: { tools: {} } }
|
|
80
84
|
);
|
package/src/mcp-server.js
CHANGED
|
@@ -30,6 +30,7 @@ function sendJson(res, status, value) {
|
|
|
30
30
|
*/
|
|
31
31
|
export function createMcpProxyServer({
|
|
32
32
|
mission,
|
|
33
|
+
lease,
|
|
33
34
|
runtime,
|
|
34
35
|
upstream,
|
|
35
36
|
upstreamUrl,
|
|
@@ -43,6 +44,7 @@ export function createMcpProxyServer({
|
|
|
43
44
|
|
|
44
45
|
const gateway = createMcpGatewayHandler({
|
|
45
46
|
mission,
|
|
47
|
+
lease,
|
|
46
48
|
runtime,
|
|
47
49
|
upstream,
|
|
48
50
|
upstreamUrl,
|
|
@@ -64,6 +66,7 @@ export function createMcpProxyServer({
|
|
|
64
66
|
ok: true,
|
|
65
67
|
service: 'agent-authority-mcp-gateway',
|
|
66
68
|
mode: 'read-only',
|
|
69
|
+
authority: lease ? 'task-lease' : 'mission',
|
|
67
70
|
upstream: upstreamUrl || 'injected'
|
|
68
71
|
});
|
|
69
72
|
}
|
package/src/providers/github.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { brokeredProviderAdapter } from '../connections.js';
|
|
2
2
|
|
|
3
|
-
const MUTATING_ACTIONS = new Set(['issue.create', 'pull_request.create', 'repo.contents.write']);
|
|
3
|
+
const MUTATING_ACTIONS = new Set(['issue.create', 'issue.comment', 'pull_request.create', 'repo.contents.write']);
|
|
4
|
+
const ISSUE_STATES = new Set(['open', 'closed', 'all']);
|
|
4
5
|
|
|
5
6
|
function required(value, name) {
|
|
6
7
|
if (value === undefined || value === null || value === '') throw new Error(`${name} is required`);
|
|
7
8
|
return value;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
function providerError(code, message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
function repoParts(context = {}) {
|
|
11
18
|
const repository = required(context.repository, 'context.repository');
|
|
12
19
|
const [owner, repo, ...extra] = String(repository).split('/');
|
|
@@ -22,6 +29,26 @@ function encodedPath(path) {
|
|
|
22
29
|
.join('/');
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function issueNumber(value) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
35
|
+
throw providerError('invalid_issue_number', 'context.issue_number must be a positive integer');
|
|
36
|
+
}
|
|
37
|
+
return number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function issueListQuery(context = {}) {
|
|
41
|
+
const state = context.state || 'open';
|
|
42
|
+
if (!ISSUE_STATES.has(state)) {
|
|
43
|
+
throw providerError('invalid_issue_state', 'context.state must be open, closed, or all');
|
|
44
|
+
}
|
|
45
|
+
const perPage = context.per_page === undefined ? 100 : Number(context.per_page);
|
|
46
|
+
if (!Number.isSafeInteger(perPage) || perPage < 1 || perPage > 100) {
|
|
47
|
+
throw providerError('invalid_issue_page_size', 'context.per_page must be an integer between 1 and 100');
|
|
48
|
+
}
|
|
49
|
+
return new URLSearchParams({ state, per_page: String(perPage) }).toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
function buildOperation(request) {
|
|
26
53
|
const context = request.context || {};
|
|
27
54
|
const { owner, repo } = repoParts(context);
|
|
@@ -37,6 +64,9 @@ function buildOperation(request) {
|
|
|
37
64
|
return { method: 'GET', path: `${root}/contents/${encodedPath(path)}${query}` };
|
|
38
65
|
}
|
|
39
66
|
|
|
67
|
+
case 'issue.list':
|
|
68
|
+
return { method: 'GET', path: `${root}/issues?${issueListQuery(context)}` };
|
|
69
|
+
|
|
40
70
|
case 'issue.create':
|
|
41
71
|
return {
|
|
42
72
|
method: 'POST',
|
|
@@ -49,6 +79,13 @@ function buildOperation(request) {
|
|
|
49
79
|
}
|
|
50
80
|
};
|
|
51
81
|
|
|
82
|
+
case 'issue.comment':
|
|
83
|
+
return {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
path: `${root}/issues/${issueNumber(context.issue_number)}/comments`,
|
|
86
|
+
body: { body: required(context.body, 'context.body') }
|
|
87
|
+
};
|
|
88
|
+
|
|
52
89
|
case 'pull_request.create':
|
|
53
90
|
return {
|
|
54
91
|
method: 'POST',
|
|
@@ -77,11 +114,8 @@ function buildOperation(request) {
|
|
|
77
114
|
};
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
default:
|
|
81
|
-
|
|
82
|
-
error.code = 'unsupported_action';
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
117
|
+
default:
|
|
118
|
+
throw providerError('unsupported_action', `GitHub action ${request.action} has no provider operation mapping`);
|
|
85
119
|
}
|
|
86
120
|
}
|
|
87
121
|
|
|
@@ -94,6 +128,91 @@ function sanitizeBody(body) {
|
|
|
94
128
|
return clone;
|
|
95
129
|
}
|
|
96
130
|
|
|
131
|
+
function normalizeIssueList(request, body) {
|
|
132
|
+
if (!Array.isArray(body)) {
|
|
133
|
+
throw providerError('github_issue_list_invalid', 'GitHub issue.list response must be an array');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const issues = body.map((issue) => ({
|
|
137
|
+
number: issueNumber(issue?.number),
|
|
138
|
+
title: typeof issue?.title === 'string' ? issue.title : null,
|
|
139
|
+
is_pull_request: Boolean(issue?.pull_request)
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const marker = request.context?.fixture_marker;
|
|
143
|
+
if (typeof marker !== 'string' || marker.trim() === '') {
|
|
144
|
+
return { issues, selected_issue_number: null, selected_issue_title: null, selected_issue_match_count: 0, selected_issue_marker: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const matches = body
|
|
148
|
+
.map((issue, index) => ({ issue, index }))
|
|
149
|
+
.filter(({ issue }) => !issue?.pull_request && typeof issue?.body === 'string' && issue.body.includes(marker));
|
|
150
|
+
|
|
151
|
+
const selected = matches.length === 1 ? issues[matches[0].index] : null;
|
|
152
|
+
return {
|
|
153
|
+
issues,
|
|
154
|
+
selected_issue_number: selected?.number || null,
|
|
155
|
+
selected_issue_title: selected?.title || null,
|
|
156
|
+
selected_issue_match_count: matches.length,
|
|
157
|
+
selected_issue_marker: marker
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizedOutput(request, response, body) {
|
|
162
|
+
const common = {
|
|
163
|
+
provider: 'github',
|
|
164
|
+
status: response.status,
|
|
165
|
+
ok: response.ok,
|
|
166
|
+
request_id: response.headers?.get?.('x-github-request-id') || null
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (request.action === 'issue.list') {
|
|
170
|
+
return { ...common, ...normalizeIssueList(request, body) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (request.action === 'issue.comment') {
|
|
174
|
+
return {
|
|
175
|
+
...common,
|
|
176
|
+
comment_id: body?.id || null,
|
|
177
|
+
html_url: body?.html_url || null,
|
|
178
|
+
issue_number: issueNumber(request.context?.issue_number)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { ...common, body: sanitizeBody(body) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reviewed authority extractor for an issue selected by the normalized
|
|
187
|
+
* issue.list mapping using the request's root-bound fixture_marker.
|
|
188
|
+
*
|
|
189
|
+
* The extractor returns a selector only. TaskLease resolves the issue number
|
|
190
|
+
* from the evidence-bound output after verifying the ALLOW receipt.
|
|
191
|
+
*/
|
|
192
|
+
export function githubIssueListSelectedNumberAuthorityExtractor({ receipt, output } = {}) {
|
|
193
|
+
if (receipt?.service !== 'github' || receipt?.action !== 'issue.list') {
|
|
194
|
+
throw providerError(
|
|
195
|
+
'trusted_extractor_operation_mismatch',
|
|
196
|
+
'GitHub issue-number authority extractor only accepts github:issue.list receipts'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (output?.provider !== 'github' || output?.selected_issue_match_count !== 1) {
|
|
200
|
+
throw providerError(
|
|
201
|
+
'trusted_extractor_output_invalid',
|
|
202
|
+
'normalized GitHub output must contain exactly one selected issue'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
issueNumber(output.selected_issue_number);
|
|
206
|
+
if (typeof output.selected_issue_marker !== 'string' || output.selected_issue_marker.trim() === '') {
|
|
207
|
+
throw providerError('trusted_extractor_output_invalid', 'normalized GitHub output is missing the selection marker');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
extractor_id: 'github.issue.list.selected-number.v1',
|
|
212
|
+
selector: 'output.selected_issue_number'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
97
216
|
export function createGitHubProviderAdapter({ broker, fetchImpl = globalThis.fetch, baseUrl = 'https://api.github.com' } = {}) {
|
|
98
217
|
if (!broker) throw new Error('credential broker is required');
|
|
99
218
|
if (typeof fetchImpl !== 'function') throw new Error('fetch implementation is required');
|
|
@@ -114,7 +233,7 @@ export function createGitHubProviderAdapter({ broker, fetchImpl = globalThis.fet
|
|
|
114
233
|
authorization: `Bearer ${token}`,
|
|
115
234
|
'content-type': 'application/json',
|
|
116
235
|
'x-github-api-version': '2022-11-28',
|
|
117
|
-
'user-agent': 'nullsquare-agent-authority/0.
|
|
236
|
+
'user-agent': 'nullsquare-agent-authority/0.4'
|
|
118
237
|
},
|
|
119
238
|
body: operation.body ? JSON.stringify(operation.body) : undefined
|
|
120
239
|
});
|
|
@@ -125,25 +244,36 @@ export function createGitHubProviderAdapter({ broker, fetchImpl = globalThis.fet
|
|
|
125
244
|
try { body = JSON.parse(text); } catch { /* preserve text */ }
|
|
126
245
|
}
|
|
127
246
|
|
|
128
|
-
const output = {
|
|
129
|
-
provider: 'github',
|
|
130
|
-
status: response.status,
|
|
131
|
-
ok: response.ok,
|
|
132
|
-
body: sanitizeBody(body),
|
|
133
|
-
request_id: response.headers?.get?.('x-github-request-id') || null
|
|
134
|
-
};
|
|
135
|
-
|
|
136
247
|
if (!response.ok) {
|
|
137
|
-
const
|
|
138
|
-
|
|
248
|
+
const output = {
|
|
249
|
+
provider: 'github',
|
|
250
|
+
status: response.status,
|
|
251
|
+
ok: false,
|
|
252
|
+
body: sanitizeBody(body),
|
|
253
|
+
request_id: response.headers?.get?.('x-github-request-id') || null
|
|
254
|
+
};
|
|
255
|
+
const error = providerError('provider_error', `GitHub API ${response.status}`);
|
|
139
256
|
error.provider_output = output;
|
|
140
257
|
throw error;
|
|
141
258
|
}
|
|
142
259
|
|
|
143
|
-
return
|
|
260
|
+
return normalizedOutput(request, response, body);
|
|
144
261
|
}
|
|
145
262
|
});
|
|
146
263
|
|
|
264
|
+
adapter.validateRequest = (request) => buildOperation(request);
|
|
147
265
|
adapter.isMutation = (request) => MUTATING_ACTIONS.has(request?.action);
|
|
266
|
+
adapter.authorityExtractor = (request, kind = 'opaque') => {
|
|
267
|
+
if (
|
|
268
|
+
request?.service === 'github' &&
|
|
269
|
+
request?.action === 'issue.list' &&
|
|
270
|
+
kind === 'github.issue.number' &&
|
|
271
|
+
typeof request?.context?.fixture_marker === 'string' &&
|
|
272
|
+
request.context.fixture_marker.trim() !== ''
|
|
273
|
+
) {
|
|
274
|
+
return githubIssueListSelectedNumberAuthorityExtractor;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
};
|
|
148
278
|
return adapter;
|
|
149
279
|
}
|