@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 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.1 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype with provenance-bound derived facts, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, GitHub and Google provider integrations, CI and CodeQL. It is not production-ready yet.
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: 'attendee',
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: { thread: 'thread:demo-91' }
195
+ context: { thread_id: 'thread:demo-91' }
189
196
  }, () => gmail.readThread('thread:demo-91'));
190
197
 
191
- lease.derive({
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
- selector: 'output.sender'
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: { attendee: read.output.sender }
204
- }, () => calendar.createEvent({ attendee: read.output.sender }));
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
- - required parent lineage and trusted extraction selector
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.** Derived facts require an authorized receipt from the same Task Lease, at least one existing parent fact, and a recorded extraction selector.
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
- - Derived-value extraction is trusted to the host/adapter; v0.4 records the source receipt and selector but does not yet cryptographically prove extraction from the provider response.
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 — first slice in progress
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 now binds provider-derived authority to the exact output returned by an authorized `guard.run()` effect.
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-authority mutation — GitHub
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 136](https://github.com/Null-Square/agent-authority/actions/runs/32517381668)
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 itself starts with only the repository as an authority root.
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 root
182
- Null-Square/agent-authority
218
+ Task roots
219
+ repository + fixture marker
183
220
  |
184
221
  v
185
- ALLOW live issue-list request
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
- discover issue #9 from GitHub response
232
+ reviewed GitHub issue-number extractor
189
233
  |
190
234
  v
191
- derive fact: issue_number = 9
235
+ TaskLease.deriveFromEvidence()
236
+ issue_number = 9
192
237
  |
193
238
  v
194
- ALLOW one real comment mutation on #9
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 -> discovered issue #9
212
- Derived authority -> issue #9
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 is deleted by test-harness cleanup after the proof. Cleanup is intentionally outside the agent authority path and counted separately.
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
- - a concrete resource can be discovered from a real provider response during authorized execution;
224
- - that resource can become same-lease derived authority;
225
- - a real provider mutation can be limited to the derived resource;
226
- - asking for another resource does not silently inherit the same authority;
227
- - a blocked resource causes zero additional task-side provider mutation calls;
228
- - completing the Task Lease prevents reuse of the previously authorized resource;
229
- - the provider credential can still exist after task authority disappears.
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
- - the current GitHub example has migrated to a reviewed evidence extractor contract;
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 public GitHub API.
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 `fetch()` while another repository produces `authority_delta_required` before a second fetch occurs.
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 provider, extractor, cross-provider and execution-evidence adversarial tests;
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.
@@ -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.1
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.1 it additionally verifies the public `@nullsquare/agent-authority/providers/google` export.
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.1`. A release must be created explicitly or by release automation.
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 headers = {
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
- async function githubJson(url, options = {}) {
84
- const response = await fetch(url, {
85
- ...options,
86
- headers: { ...headers, ...(options.headers || {}) }
87
- });
88
- if (!response.ok) {
89
- const body = await response.text();
90
- throw new Error(`GitHub ${response.status}: ${body.slice(0, 300)}`);
91
- }
92
- if (response.status === 204) return null;
93
- return response.json();
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
- return guard.run(
98
- {
99
- service: 'github',
100
- action: 'issue.list',
101
- context: { repository }
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
- return guard.run(
117
- {
118
- service: 'github',
119
- action: 'issue.comment',
120
- context: { repository, issue_number: issueNumber }
121
- },
122
- async () => {
123
- providerMutationCalls += 1;
124
- const comment = await githubJson(
125
- `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
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 githubJson(
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('1. Discover fixture through an authorized live GitHub issue-list call');
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
- lease.derive({
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
- value: discovered.output.number,
155
- from: ['fact:repository'],
170
+ from: ['fact:repository', 'fact:fixture-marker'],
156
171
  receipt: discovered.receipt,
157
- selector: 'output.number'
172
+ evidence: discovered.evidence,
173
+ output: discovered.output,
174
+ extractor
158
175
  });
159
- console.log(`2. Derived authority -> issue #${discovered.output.number}`);
176
+ console.log(`2. Evidence-verified authority -> issue #${issueFact.value}`);
160
177
 
161
- const validationBody = `Agent Authority live derived-authority validation (${new Date().toISOString()}). Temporary comment; CI removes it after the proof.`;
162
- const allowedMutation = await commentOnIssue(discovered.output.number, validationBody);
163
- createdCommentId = allowedMutation.output.id;
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 = discovered.output.number === 1 ? 2 : 1;
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-mutation validation complete');
198
+ lease.complete('live evidence-derived mutation validation complete');
182
199
  try {
183
- await commentOnIssue(discovered.output.number, 'THIS MUST NOT RUN AFTER TASK COMPLETION');
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 #${discovered.output.number}`);
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 -> dynamic resource was discovered from GitHub, derived into the Task Lease, and mutated exactly once');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Task-bounded authority runtime for AI agents: give agents tasks, not standing account permissions.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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
- const error = new Error(`GitHub action ${request.action} has no provider operation mapping`);
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.3'
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 error = new Error(`GitHub API ${response.status}`);
138
- error.code = 'provider_error';
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 output;
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
  }