@nullsquare/agent-authority 0.4.2 → 0.4.3
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 +33 -12
- package/ROADMAP.md +6 -3
- package/docs/authority-extractor-conformance.md +95 -0
- package/docs/evidence.md +74 -25
- package/docs/npm-release.md +3 -3
- package/examples/live-github-derived-mutation.js +82 -65
- package/package.json +1 -1
- package/src/providers/github.js +148 -18
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
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.2 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, 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
|
|
|
@@ -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,7 @@ 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 stricter v0.4.2 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).
|
|
156
162
|
|
|
157
163
|
## Minimal developer API
|
|
158
164
|
|
|
@@ -160,6 +166,7 @@ The repository also includes a real Gmail → Calendar validation path and a reu
|
|
|
160
166
|
import { AuthorityRuntime } from '@nullsquare/agent-authority';
|
|
161
167
|
import { createTaskLease } from '@nullsquare/agent-authority/task-lease';
|
|
162
168
|
import { createTaskLeaseGuard } from '@nullsquare/agent-authority/guard';
|
|
169
|
+
import { gmailThreadSenderAuthorityExtractor } from '@nullsquare/agent-authority/providers/google';
|
|
163
170
|
|
|
164
171
|
const lease = createTaskLease({
|
|
165
172
|
mission,
|
|
@@ -171,7 +178,7 @@ const lease = createTaskLease({
|
|
|
171
178
|
{
|
|
172
179
|
service: 'calendar',
|
|
173
180
|
action: 'event.create',
|
|
174
|
-
context_field: '
|
|
181
|
+
context_field: 'attendee_email',
|
|
175
182
|
fact_id: 'fact:sender-email'
|
|
176
183
|
}
|
|
177
184
|
]
|
|
@@ -185,25 +192,30 @@ const guard = createTaskLeaseGuard({
|
|
|
185
192
|
const read = await guard.run({
|
|
186
193
|
service: 'gmail',
|
|
187
194
|
action: 'thread.read',
|
|
188
|
-
context: {
|
|
195
|
+
context: { thread_id: 'thread:demo-91' }
|
|
189
196
|
}, () => gmail.readThread('thread:demo-91'));
|
|
190
197
|
|
|
191
|
-
lease.
|
|
198
|
+
const senderFact = lease.deriveFromEvidence({
|
|
192
199
|
fact_id: 'fact:sender-email',
|
|
193
200
|
kind: 'email.address',
|
|
194
|
-
value: read.output.sender,
|
|
195
201
|
from: ['fact:thread'],
|
|
196
202
|
receipt: read.receipt,
|
|
197
|
-
|
|
203
|
+
evidence: read.evidence,
|
|
204
|
+
output: read.output,
|
|
205
|
+
extractor: gmailThreadSenderAuthorityExtractor
|
|
198
206
|
});
|
|
199
207
|
|
|
200
208
|
await guard.run({
|
|
201
209
|
service: 'calendar',
|
|
202
210
|
action: 'event.create',
|
|
203
|
-
context: {
|
|
204
|
-
}, () => calendar.createEvent({ attendee:
|
|
211
|
+
context: { attendee_email: senderFact.value }
|
|
212
|
+
}, () => calendar.createEvent({ attendee: senderFact.value }));
|
|
205
213
|
```
|
|
206
214
|
|
|
215
|
+
`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.
|
|
216
|
+
|
|
217
|
+
The older `derive()` API remains available as the explicitly **host-trusted compatibility path**.
|
|
218
|
+
|
|
207
219
|
The host keeps its existing SDK, connector and authentication. Agent Authority controls whether the effect may happen.
|
|
208
220
|
|
|
209
221
|
## Three integration modes, one authority model
|
|
@@ -249,7 +261,11 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
249
261
|
- Task Lease prototype
|
|
250
262
|
- explicit authority roots
|
|
251
263
|
- same-lease provenance-bound derived facts
|
|
252
|
-
-
|
|
264
|
+
- execution evidence binding an allowed receipt, request and exact output hash
|
|
265
|
+
- strict `deriveFromEvidence()` path where the caller cannot provide the authority value
|
|
266
|
+
- reviewed Gmail sender authority extractor bound to `gmail:thread.read`
|
|
267
|
+
- legacy host-trusted `derive()` compatibility path
|
|
268
|
+
- required parent lineage and extraction selector
|
|
253
269
|
- exact context-field bindings
|
|
254
270
|
- authority-delta step-up signal
|
|
255
271
|
- immediate task completion/expiry enforcement
|
|
@@ -259,6 +275,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
259
275
|
|
|
260
276
|
- protocol-neutral `guard.run()` wrapper
|
|
261
277
|
- blocked side effects never invoke their callback
|
|
278
|
+
- successful guarded effects return separate execution evidence
|
|
262
279
|
- one-time human approvals bound to exact request
|
|
263
280
|
- mutation idempotency
|
|
264
281
|
- conservative uncertain-state handling
|
|
@@ -278,10 +295,12 @@ The long-term validation target is the **same Task Lease and authority lineage a
|
|
|
278
295
|
### Engineering quality
|
|
279
296
|
|
|
280
297
|
- adversarial authorization tests
|
|
298
|
+
- execution-evidence substitution, tampering, replay, cross-lease and selector tests
|
|
281
299
|
- Node 20 and Node 22 CI
|
|
282
300
|
- coverage run
|
|
283
301
|
- package checks
|
|
284
302
|
- clean-consumer npm registry verification
|
|
303
|
+
- live GitHub read and mutation proofs
|
|
285
304
|
- CodeQL
|
|
286
305
|
|
|
287
306
|
## What is different from OAuth, IAM and MCP authorization?
|
|
@@ -337,7 +356,7 @@ Allow a natural workflow across mail, calendar, CRM and internal systems without
|
|
|
337
356
|
1. **Task before credential.** A provider token is not task authority.
|
|
338
357
|
2. **Mission is the ceiling.** Task Leases cannot override explicit denies.
|
|
339
358
|
3. **No side effect before authorization.** Denied and step-up actions never execute.
|
|
340
|
-
4. **Authority lineage matters.**
|
|
359
|
+
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
360
|
5. **No silent resource expansion.** A different concrete resource becomes an authority delta.
|
|
342
361
|
6. **Task authority ends with the task.** Completion and expiry are independent from provider credential lifetime.
|
|
343
362
|
7. **Authority may shrink, never silently grow.** Delegation and transport changes must preserve non-amplification.
|
|
@@ -352,7 +371,9 @@ See [SECURITY.md](SECURITY.md).
|
|
|
352
371
|
This is still a validation implementation.
|
|
353
372
|
|
|
354
373
|
- Task Lease state is currently process-local.
|
|
355
|
-
-
|
|
374
|
+
- `deriveFromEvidence()` proves consistency with the exact output returned through the trusted Agent Authority guard, but the output is not cryptographically attested by Gmail, GitHub, or another remote provider.
|
|
375
|
+
- The legacy `derive()` API remains host-trusted for compatibility; audit provenance distinguishes it from `execution-evidence-v1` derivation.
|
|
376
|
+
- Source-data changes do not yet automatically invalidate already-derived authority facts.
|
|
356
377
|
- Bindings currently target top-level request context fields.
|
|
357
378
|
- Approved authority deltas are surfaced but not automatically applied back into a live lease.
|
|
358
379
|
- 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,14 +85,17 @@ 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
100
|
## M4 — Same task, multiple transports
|
|
98
101
|
|
|
@@ -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,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.2
|
|
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.2 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` export plus the Google provider and Gmail authority-extractor exports.
|
|
22
22
|
|
|
23
23
|
## npm vs GitHub release surfaces
|
|
24
24
|
|
|
25
25
|
Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
|
|
26
26
|
|
|
27
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`.
|
|
28
|
-
- **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.2`. A release must be created explicitly or by release automation.
|
|
29
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.
|
|
30
30
|
|
|
31
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.
|
|
@@ -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/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
|
}
|