@nullsquare/agent-authority 0.4.0 → 0.4.1

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
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- ![Agent Authority](docs/assets/agent-authority-cover.svg)
3
+ ![Agent Authority](docs/assets/agent-authority-cover-editorial-v2.png)
4
4
 
5
5
  # Agent Authority
6
6
 
@@ -10,10 +10,18 @@
10
10
 
11
11
  [Task Leases](docs/task-leases.md) · [Validate](docs/validation.md) · [Integration contract](docs/integration-contract.md) · [CLI](docs/cli.md) · [Architecture](docs/architecture.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
12
12
 
13
- > **Status: public pre-alpha / v0.4 validation candidate.** 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 brokered execution, CI and CodeQL. It is not production-ready yet.
13
+ > **Status: public pre-alpha / v0.4.0 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 brokered execution, CI and CodeQL. It is not production-ready yet.
14
14
 
15
15
  </div>
16
16
 
17
+ ## Install
18
+
19
+ Requires Node.js 20+.
20
+
21
+ ```bash
22
+ npm install @nullsquare/agent-authority
23
+ ```
24
+
17
25
  ## The problem
18
26
 
19
27
  AI agents increasingly receive broad provider permissions so they can complete narrow human tasks.
package/ROADMAP.md CHANGED
@@ -55,10 +55,15 @@ never silently grow
55
55
  - [x] independent task-lease expiry
56
56
  - [x] task-lease ID/hash in receipts
57
57
  - [x] self-contained cross-system demo: Gmail-thread fact -> Calendar attendee
58
- - [ ] exercise the same pattern against two real provider operations
58
+ - [x] reusable Google REST adapter for Gmail thread reads and Calendar event mutations
59
+ - [x] adversarial Gmail -> Calendar test proving unrelated and post-completion effects never invoke provider callbacks
60
+ - [x] real connected-account smoke: Gmail self-test fixture -> exact attendee -> temporary Calendar event -> cleanup
61
+ - [ ] rerun the same proof as a public GitHub Actions job after repository Google OAuth secrets are configured
59
62
 
60
63
  **Success criterion:** a real multi-step agent workflow discovers a resource during an authorized read and can use exactly that resource in a later side effect, while an unrelated resource is technically blocked without requiring approval for every normal task step.
61
64
 
65
+ The core cross-provider behavior is now implemented and exercised against connected Gmail and Calendar accounts. Public Actions reproducibility remains the final M1 evidence gate because GitHub cannot reuse an interactive ChatGPT connector credential; the manual workflow is committed and expects a refresh token stored only as repository secrets.
66
+
62
67
  ## M2 — Durable task execution
63
68
 
64
69
  Build only what the real M1 workflow proves necessary.
@@ -117,7 +122,7 @@ Only after the task-bound enforcement model is validated.
117
122
  - [ ] framework integration examples
118
123
  - [ ] `good first issue` tasks based on real mappings/tests
119
124
  - [ ] independent contributor implementation of one adapter
120
- - [ ] release packaging and npm publication
125
+ - [x] release packaging and npm publication
121
126
  - [ ] documentation site only when README/docs become too large
122
127
 
123
128
  ## M7 — Standards interoperability
@@ -1,4 +1,4 @@
1
- # Announcement draft — hold until registry verification
1
+ # Announcement draft — v0.4.0 Developer Preview
2
2
 
3
3
  NullSquare is open-sourcing **Agent Authority**, an experimental runtime for task-bounded AI agent side effects.
4
4
 
@@ -8,6 +8,12 @@ A Task Lease can follow resources discovered through authorized execution withou
8
8
 
9
9
  The developer preview also includes a Vercel AI SDK integration that wraps ordinary `ToolLoopAgent` tools at their existing `execute` boundary.
10
10
 
11
+ Install the published developer preview with:
12
+
13
+ ```bash
14
+ npm install @nullsquare/agent-authority@0.4.0
15
+ ```
16
+
11
17
  **Give your agent a task, not your account.**
12
18
 
13
19
  This is a developer preview, not a claim that NullSquare invented task-scoped authorization and not a production-ready security control. The repository documents the current trust assumptions and bypass boundaries explicitly.
package/docs/evidence.md CHANGED
@@ -10,6 +10,102 @@ This page records the strongest properties the repository currently demonstrates
10
10
 
11
11
  The guarantee applies to effects that actually pass through the Agent Authority enforcement boundary. A separate unguarded provider path is outside this guarantee.
12
12
 
13
+ ## Cross-provider derived authority — Gmail → Calendar
14
+
15
+ Agent Authority now includes a real Google provider mapping, an adversarial cross-provider test, a live validation script, and an opt-in GitHub Actions workflow.
16
+
17
+ The task shape is:
18
+
19
+ ```text
20
+ Task root
21
+ one Gmail thread
22
+ |
23
+ v
24
+ ALLOW Gmail thread.read
25
+ |
26
+ v
27
+ discover sender email
28
+ |
29
+ v
30
+ derive fact: sender_email
31
+ |
32
+ v
33
+ ALLOW Calendar event.create
34
+ attendee == derived sender
35
+ |
36
+ +--> different attendee
37
+ | -> authority_delta_required
38
+ | -> Calendar mutation callback does not run
39
+ |
40
+ v
41
+ complete Task Lease
42
+ |
43
+ +--> same attendee again
44
+ -> task_lease_completed
45
+ -> Calendar mutation callback does not run
46
+ ```
47
+
48
+ ### Connected-account smoke — 2026-08-22
49
+
50
+ A controlled self-test was exercised against the connected NullSquare Gmail and Google Calendar accounts:
51
+
52
+ 1. a temporary validation message was sent from the validation account to itself;
53
+ 2. Gmail returned the self-test message with sender `null@nullsquare.net`;
54
+ 3. one private, transparent Calendar event was created with exactly `null@nullsquare.net` as attendee;
55
+ 4. the temporary Calendar event was deleted immediately after the proof;
56
+ 5. the validation email was moved to Trash after cleanup.
57
+
58
+ No unrelated external person was invited. The smoke test establishes that the concrete Gmail and Calendar operations used by the validation are available and compatible with the intended data shape.
59
+
60
+ The live connected-account smoke is not presented as a public CI proof of the repository script. The Task Lease zero-call assertions are separately executable in `test/google-cross-provider.test.js`, and `.github/workflows/live-google-validation.yml` is provided for a rerunnable provider-backed proof once repository Google OAuth secrets are configured.
61
+
62
+ ### Executable local/CI assertions
63
+
64
+ `test/google-cross-provider.test.js` proves deterministically that:
65
+
66
+ - the approved Gmail thread callback runs exactly once;
67
+ - its sender becomes a same-lease derived authority fact;
68
+ - one Calendar mutation for that sender runs exactly once;
69
+ - a different attendee produces `authority_delta_required` and the Calendar callback is not invoked;
70
+ - after lease completion, the previously valid attendee produces `task_lease_completed` and the Calendar callback is not invoked again.
71
+
72
+ `test/google-provider.test.js` separately validates the Google REST mappings, metadata-only Gmail sender extraction, Calendar attendee body, default `sendUpdates=none`, credential redaction, unsupported-action failure, and mutation classification.
73
+
74
+ ### Public workflow
75
+
76
+ The manual workflow `Live Google cross-provider validation` mints a short-lived access token from repository secrets and runs `npm run demo:live-google`.
77
+
78
+ It expects:
79
+
80
+ ```text
81
+ AA_GOOGLE_OAUTH_CLIENT_ID
82
+ AA_GOOGLE_OAUTH_CLIENT_SECRET
83
+ AA_GOOGLE_OAUTH_REFRESH_TOKEN
84
+ ```
85
+
86
+ The refresh token should be restricted to the practical Gmail read-only and Calendar event scopes documented in `docs/live-google-validation.md`.
87
+
88
+ The connected GitHub tool does not expose repository Actions-secret management, so those secrets are intentionally not installed automatically from this development session.
89
+
90
+ ### What this proves
91
+
92
+ Taken together, the deterministic Task Lease test plus the controlled provider smoke show that:
93
+
94
+ - the Gmail → Calendar derived-authority shape is implemented, not just diagrammed;
95
+ - the exact attendee binding is enforced before the Calendar mutation callback;
96
+ - provider operations exist and work against real Gmail and Calendar accounts;
97
+ - cleanup can keep the live proof reversible and self-contained.
98
+
99
+ ### What this does not prove yet
100
+
101
+ - a public GitHub Actions run has executed the full repository live script;
102
+ - host-side extraction of `sender_email` is cryptographically attested by Gmail;
103
+ - the agent cannot bypass Agent Authority if it independently holds a Google credential;
104
+ - Task Lease state is durable across process failure;
105
+ - the refresh-token setup is production credential onboarding.
106
+
107
+ See [Live Gmail → Calendar validation](live-google-validation.md).
108
+
13
109
  ## Live derived-authority mutation — GitHub
14
110
 
15
111
  Public fixture: [issue #9](https://github.com/Null-Square/agent-authority/issues/9)
@@ -129,8 +225,11 @@ Current pull requests run:
129
225
  - coverage;
130
226
  - live GitHub read validation;
131
227
  - live derived GitHub mutation validation for trusted in-repository branches;
228
+ - Google provider and cross-provider adversarial tests;
132
229
  - CodeQL.
133
230
 
231
+ 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.
232
+
134
233
  ## Evidence standard for new claims
135
234
 
136
235
  A new security claim should ideally include all four:
@@ -0,0 +1,100 @@
1
+ # Live Gmail → Calendar validation
2
+
3
+ This validation turns the README's Gmail → Calendar Task Lease example into a real cross-provider proof.
4
+
5
+ The narrow claim is:
6
+
7
+ > A sender discovered from one authorized Gmail thread can become exact Task Lease authority for one Calendar event attendee, while a different attendee and any post-completion retry are blocked before the Calendar mutation callback runs.
8
+
9
+ It is intentionally not a general Google OAuth product or a production onboarding flow.
10
+
11
+ ## Safe fixture
12
+
13
+ Use a one-message thread sent from the validation account to itself. This keeps the proof real without inviting an unrelated person.
14
+
15
+ The live script:
16
+
17
+ 1. reads the approved Gmail thread through `users.threads.get` using metadata format and only the `From` header;
18
+ 2. derives `fact:sender-email` from the same-lease `ALLOW` receipt;
19
+ 3. creates one private, transparent Calendar event whose attendee is exactly that derived sender;
20
+ 4. attempts a different attendee and requires `authority_delta_required` before the provider callback;
21
+ 5. completes the Task Lease and proves the previously authorized attendee can no longer be used;
22
+ 6. deletes the temporary event as harness cleanup outside the agent-authority proof.
23
+
24
+ The event uses `sendUpdates=none` and is deleted after validation. The cleanup call is counted separately from task-side provider mutations.
25
+
26
+ ## Required Google OAuth scopes
27
+
28
+ The validation credential needs only the Google permissions required for the two provider operations:
29
+
30
+ ```text
31
+ https://www.googleapis.com/auth/gmail.readonly
32
+ https://www.googleapis.com/auth/calendar.events
33
+ ```
34
+
35
+ Do not put an access token in the repository or workflow inputs.
36
+
37
+ ## GitHub Actions secrets
38
+
39
+ The manual workflow `.github/workflows/live-google-validation.yml` mints a short-lived access token at runtime from these repository secrets:
40
+
41
+ ```text
42
+ AA_GOOGLE_OAUTH_CLIENT_ID
43
+ AA_GOOGLE_OAUTH_CLIENT_SECRET
44
+ AA_GOOGLE_OAUTH_REFRESH_TOKEN
45
+ ```
46
+
47
+ The refresh token should have been granted the Gmail read-only and Calendar event scopes above. The access token is masked and exists only for the workflow run.
48
+
49
+ ## Run from GitHub Actions
50
+
51
+ Open **Actions → Live Google cross-provider validation → Run workflow** and provide:
52
+
53
+ - `gmail_thread_id`: the approved self-test Gmail thread ID;
54
+ - `calendar_id`: normally `primary`;
55
+ - `expected_sender`: optional, but recommended for a deterministic fixture.
56
+
57
+ A successful run should record the following shape:
58
+
59
+ ```text
60
+ ALLOW -> Gmail returned sender <fixture account>
61
+ Derived authority -> Calendar attendee <fixture account>
62
+ ALLOW -> real Calendar event mutation executed
63
+ STEP-UP -> unrelated attendee blocked before Calendar provider mutation
64
+ DENY -> post-completion Calendar mutation blocked
65
+ Provider calls observed before cleanup: gmail_reads=1, calendar_task_mutations=1
66
+ Cleanup -> deleted temporary Calendar event
67
+ ```
68
+
69
+ ## Run locally with a short-lived token
70
+
71
+ ```bash
72
+ export GOOGLE_ACCESS_TOKEN='...'
73
+ export AA_GOOGLE_GMAIL_THREAD_ID='...'
74
+ export AA_GOOGLE_CALENDAR_ID='primary'
75
+ export AA_GOOGLE_EXPECTED_SENDER='validation@example.com'
76
+ npm run demo:live-google
77
+ ```
78
+
79
+ Prefer the refresh-token GitHub Actions workflow for repeatable validation. Do not persist the short-lived access token in shell history, source files, screenshots, or issue comments.
80
+
81
+ ## What this proves
82
+
83
+ When the real provider calls are placed inside the Task Lease guard callbacks, the validation demonstrates:
84
+
85
+ - cross-provider derived authority from Gmail to Calendar;
86
+ - one exact derived attendee can be used for the real Calendar effect;
87
+ - a different attendee does not silently inherit authority;
88
+ - blocked attempts create zero additional task-side Calendar provider mutations;
89
+ - task completion removes the previously valid derived authority;
90
+ - the Google credential can remain broader than the Task Lease.
91
+
92
+ ## What this does not prove
93
+
94
+ - the current host-side extraction of `sender_email` is cryptographically attested by Gmail;
95
+ - Task Lease state survives process failure;
96
+ - an agent holding an independent Google credential cannot bypass the guard;
97
+ - the validation refresh-token setup is a production credential-onboarding design;
98
+ - `sendUpdates=none` is a general invitation-delivery strategy for user-facing calendar workflows.
99
+
100
+ The purpose of this proof is to close the real cross-provider M1 validation gap and expose the next implementation problem with evidence rather than speculation.
@@ -17,3 +17,13 @@ npm install @nullsquare/agent-authority@0.4.0
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
+
21
+ ## npm vs GitHub release surfaces
22
+
23
+ Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
24
+
25
+ - **npm registry** — `npm publish --access public` publishes `@nullsquare/agent-authority` to `registry.npmjs.org` / npmjs.com. This is the package users install with `npm install`.
26
+ - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.0`. A release must be created explicitly or by release automation.
27
+ - **GitHub Packages** — a separate package registry. It only appears when the package is published to GitHub's npm registry (`npm.pkg.github.com`); publishing to npmjs.com does not populate it.
28
+
29
+ Agent Authority currently uses npmjs.com as its public package registry. Therefore an empty GitHub **Packages** section is expected unless the project intentionally adopts dual publication. A GitHub **Release** is still useful for source-release discoverability and should track published versions, but it is independent from npm publication.
@@ -0,0 +1,256 @@
1
+ import { AuthorityRuntime } from '../src/index.js';
2
+ import {
3
+ AuthorityApprovalRequiredError,
4
+ AuthorityDeniedError,
5
+ createTaskLeaseGuard
6
+ } from '../src/guard.js';
7
+ import { gmailThreadSenderEmail } from '../src/providers/google.js';
8
+ import { createTaskLease } from '../src/task-lease.js';
9
+
10
+ const token = process.env.GOOGLE_ACCESS_TOKEN;
11
+ const threadId = process.env.AA_GOOGLE_GMAIL_THREAD_ID;
12
+ const calendarId = process.env.AA_GOOGLE_CALENDAR_ID || 'primary';
13
+ const expectedSender = process.env.AA_GOOGLE_EXPECTED_SENDER?.trim().toLowerCase() || null;
14
+
15
+ if (!token) throw new Error('GOOGLE_ACCESS_TOKEN is required for live Google validation');
16
+ if (!threadId) throw new Error('AA_GOOGLE_GMAIL_THREAD_ID is required for live Google validation');
17
+
18
+ const now = Date.now();
19
+ const startTime = new Date(now + 10 * 60_000).toISOString();
20
+ const endTime = new Date(now + 25 * 60_000).toISOString();
21
+ const marker = `Agent Authority live Google validation ${new Date(now).toISOString()}`;
22
+
23
+ const mission = {
24
+ version: '0.1',
25
+ mission_id: `mission:live-google-cross-provider:${now}`,
26
+ principal: { id: 'user:google-validation' },
27
+ agent: { id: 'agent:google-cross-provider-validation' },
28
+ objective: 'Read one authorized Gmail thread and create one Calendar event only for the sender discovered from that thread',
29
+ resources: [
30
+ {
31
+ service: 'gmail',
32
+ allow: ['thread.read'],
33
+ deny: ['message.send', 'message.delete', 'thread.delete'],
34
+ constraints: { thread_id: [threadId] }
35
+ },
36
+ {
37
+ service: 'calendar',
38
+ allow: ['event.create'],
39
+ deny: ['event.delete', 'calendar.delete'],
40
+ constraints: { calendar_id: [calendarId] }
41
+ }
42
+ ],
43
+ constraints: { expires_at: new Date(now + 15 * 60_000).toISOString() }
44
+ };
45
+
46
+ const lease = createTaskLease({
47
+ mission,
48
+ request: 'Schedule one temporary validation event with the sender in the approved Gmail thread',
49
+ roots: [
50
+ {
51
+ fact_id: 'fact:gmail-thread',
52
+ kind: 'gmail.thread',
53
+ value: threadId,
54
+ source: 'validation-task'
55
+ },
56
+ {
57
+ fact_id: 'fact:calendar',
58
+ kind: 'google.calendar',
59
+ value: calendarId,
60
+ source: 'validation-task'
61
+ }
62
+ ],
63
+ bindings: [
64
+ {
65
+ service: 'gmail',
66
+ action: 'thread.read',
67
+ context_field: 'thread_id',
68
+ fact_id: 'fact:gmail-thread'
69
+ },
70
+ {
71
+ service: 'calendar',
72
+ action: 'event.create',
73
+ context_field: 'calendar_id',
74
+ fact_id: 'fact:calendar'
75
+ },
76
+ {
77
+ service: 'calendar',
78
+ action: 'event.create',
79
+ context_field: 'attendee_email',
80
+ fact_id: 'fact:sender-email'
81
+ }
82
+ ],
83
+ expires_at: new Date(now + 10 * 60_000).toISOString()
84
+ });
85
+
86
+ const guard = createTaskLeaseGuard({ lease, runtime: new AuthorityRuntime() });
87
+ const providerHeaders = {
88
+ accept: 'application/json',
89
+ authorization: `Bearer ${token}`,
90
+ 'content-type': 'application/json',
91
+ 'user-agent': 'agent-authority-live-google-validation/0.4'
92
+ };
93
+
94
+ let providerReadCalls = 0;
95
+ let providerMutationCalls = 0;
96
+ let cleanupCalls = 0;
97
+ let createdEventId = null;
98
+
99
+ async function googleResponse(url, options = {}) {
100
+ const response = await fetch(url, {
101
+ ...options,
102
+ headers: { ...providerHeaders, ...(options.headers || {}) }
103
+ });
104
+ const text = await response.text();
105
+ let body = null;
106
+ if (text) {
107
+ try { body = JSON.parse(text); } catch { body = text; }
108
+ }
109
+ if (!response.ok) {
110
+ throw new Error(`Google API ${response.status}: ${typeof body === 'string' ? body.slice(0, 300) : JSON.stringify(body).slice(0, 300)}`);
111
+ }
112
+ return body;
113
+ }
114
+
115
+ async function readAuthorizedThread() {
116
+ return guard.run(
117
+ {
118
+ service: 'gmail',
119
+ action: 'thread.read',
120
+ context: { thread_id: threadId }
121
+ },
122
+ async () => {
123
+ providerReadCalls += 1;
124
+ const query = new URLSearchParams({ format: 'metadata' });
125
+ query.append('metadataHeaders', 'From');
126
+ const thread = await googleResponse(
127
+ `https://gmail.googleapis.com/gmail/v1/users/me/threads/${encodeURIComponent(threadId)}?${query}`
128
+ );
129
+ const sender = gmailThreadSenderEmail(thread);
130
+ return {
131
+ thread_id: thread.id,
132
+ sender_email: sender.email,
133
+ sender_raw: sender.raw,
134
+ sender_message_id: sender.message_id,
135
+ message_count: Array.isArray(thread.messages) ? thread.messages.length : 0
136
+ };
137
+ }
138
+ );
139
+ }
140
+
141
+ async function createCalendarEvent(attendeeEmail, suffix) {
142
+ const request = {
143
+ service: 'calendar',
144
+ action: 'event.create',
145
+ context: {
146
+ calendar_id: calendarId,
147
+ attendee_email: attendeeEmail,
148
+ start_time: startTime,
149
+ end_time: endTime,
150
+ summary: `${marker} — ${suffix}`,
151
+ transparency: 'transparent',
152
+ visibility: 'private',
153
+ send_updates: 'none'
154
+ }
155
+ };
156
+
157
+ return guard.run(request, async () => {
158
+ providerMutationCalls += 1;
159
+ const event = await googleResponse(
160
+ `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?sendUpdates=none`,
161
+ {
162
+ method: 'POST',
163
+ body: JSON.stringify({
164
+ summary: request.context.summary,
165
+ description: 'Temporary event created by the Agent Authority live Gmail → Calendar validation. Cleanup deletes it after the proof.',
166
+ start: { dateTime: startTime },
167
+ end: { dateTime: endTime },
168
+ attendees: [{ email: attendeeEmail }],
169
+ transparency: 'transparent',
170
+ visibility: 'private'
171
+ })
172
+ }
173
+ );
174
+ return { event_id: event.id, html_link: event.htmlLink || null };
175
+ });
176
+ }
177
+
178
+ async function cleanupEvent(eventId) {
179
+ cleanupCalls += 1;
180
+ const response = await fetch(
181
+ `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}?sendUpdates=none`,
182
+ { method: 'DELETE', headers: providerHeaders }
183
+ );
184
+ if (!response.ok && response.status !== 404) {
185
+ const body = await response.text();
186
+ throw new Error(`Google Calendar cleanup ${response.status}: ${body.slice(0, 300)}`);
187
+ }
188
+ }
189
+
190
+ try {
191
+ console.log(`Task root Gmail thread: ${threadId}`);
192
+ console.log(`Task root Calendar: ${calendarId}`);
193
+ console.log('1. Read the approved Gmail thread through the Task Lease');
194
+ const discovered = await readAuthorizedThread();
195
+ console.log(` ALLOW -> Gmail returned sender ${discovered.output.sender_email}`);
196
+
197
+ if (expectedSender && discovered.output.sender_email !== expectedSender) {
198
+ throw new Error(`expected sender ${expectedSender}, got ${discovered.output.sender_email}`);
199
+ }
200
+
201
+ lease.derive({
202
+ fact_id: 'fact:sender-email',
203
+ kind: 'email.address',
204
+ value: discovered.output.sender_email,
205
+ from: ['fact:gmail-thread'],
206
+ receipt: discovered.receipt,
207
+ selector: 'output.sender_email'
208
+ });
209
+ console.log(`2. Derived authority -> Calendar attendee ${discovered.output.sender_email}`);
210
+
211
+ const allowed = await createCalendarEvent(discovered.output.sender_email, 'authorized');
212
+ createdEventId = allowed.output.event_id;
213
+ console.log(`3. ALLOW -> real Calendar event mutation executed (${createdEventId})`);
214
+
215
+ try {
216
+ await createCalendarEvent('blocked@example.invalid', 'must-never-run');
217
+ throw new Error('unrelated attendee mutation unexpectedly executed');
218
+ } catch (error) {
219
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
220
+ throw error;
221
+ }
222
+ console.log('4. STEP-UP -> unrelated attendee blocked before Calendar provider mutation');
223
+ }
224
+
225
+ if (providerMutationCalls !== 1) {
226
+ throw new Error(`expected exactly one task-side Calendar mutation before completion, got ${providerMutationCalls}`);
227
+ }
228
+
229
+ lease.complete('live Gmail to Calendar validation complete');
230
+ try {
231
+ await createCalendarEvent(discovered.output.sender_email, 'must-not-run-after-completion');
232
+ throw new Error('post-completion Calendar mutation unexpectedly executed');
233
+ } catch (error) {
234
+ if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') {
235
+ throw error;
236
+ }
237
+ console.log('5. DENY -> post-completion Calendar mutation blocked');
238
+ }
239
+
240
+ if (providerReadCalls !== 1) {
241
+ throw new Error(`expected exactly one Gmail provider read, got ${providerReadCalls}`);
242
+ }
243
+ if (providerMutationCalls !== 1) {
244
+ throw new Error(`expected exactly one Calendar provider mutation after blocked attempts, got ${providerMutationCalls}`);
245
+ }
246
+
247
+ console.log('PASS -> a sender discovered from real Gmail became exact derived authority for one real Calendar mutation');
248
+ console.log('PASS -> unrelated and post-completion attempts caused zero additional Calendar provider mutations');
249
+ } finally {
250
+ if (createdEventId) {
251
+ await cleanupEvent(createdEventId);
252
+ console.log(`Cleanup -> deleted temporary Calendar event ${createdEventId} outside the agent authority proof`);
253
+ }
254
+ console.log(`Provider calls observed before cleanup: gmail_reads=${providerReadCalls}, calendar_task_mutations=${providerMutationCalls}`);
255
+ console.log(`Harness cleanup calls: ${cleanupCalls}`);
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",
@@ -35,12 +35,13 @@
35
35
  "demo:task-lease": "node examples/task-lease-demo.js",
36
36
  "demo:live-github": "node examples/live-github-task-lease.js",
37
37
  "demo:live-derived-github": "node examples/live-github-derived-mutation.js",
38
+ "demo:live-google": "node examples/live-google-cross-provider.js",
38
39
  "demo:mcp-upstream": "node examples/validation-mcp-upstream.js",
39
40
  "demo:guard": "node examples/direct-guard.js",
40
41
  "test": "node --test test/*.test.js",
41
42
  "test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
42
43
  "test:coverage": "node --experimental-test-coverage --test test/*.test.js",
43
- "check:syntax": "node --check src/index.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/storage.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js",
44
+ "check:syntax": "node --check src/index.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/providers/google.js && node --check src/storage.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js && node --check examples/live-google-cross-provider.js",
44
45
  "check:package": "npm pack --dry-run",
45
46
  "check": "npm run check:syntax && npm test && npm run demo:task-lease && npm run check:package"
46
47
  },
@@ -71,7 +72,8 @@
71
72
  "./sdk": "./src/sdk.js",
72
73
  "./storage": "./src/storage.js",
73
74
  "./task-lease": "./src/task-lease.js",
74
- "./providers/github": "./src/providers/github.js"
75
+ "./providers/github": "./src/providers/github.js",
76
+ "./providers/google": "./src/providers/google.js"
75
77
  },
76
78
  "files": ["src", "docs", "examples", "README.md", "LICENSE", "SECURITY.md", "ROADMAP.md", "CONTRIBUTING.md"],
77
79
  "repository": { "type": "git", "url": "git+https://github.com/Null-Square/agent-authority.git" },
@@ -0,0 +1,201 @@
1
+ import { brokeredProviderAdapter } from '../connections.js';
2
+
3
+ const MUTATING_ACTIONS = new Set(['event.create', 'event.delete']);
4
+ const SEND_UPDATES = new Set(['all', 'externalOnly', 'none']);
5
+
6
+ function required(value, name) {
7
+ if (value === undefined || value === null || value === '') throw new Error(`${name} is required`);
8
+ return value;
9
+ }
10
+
11
+ function providerError(code, message) {
12
+ const error = new Error(message);
13
+ error.code = code;
14
+ return error;
15
+ }
16
+
17
+ function fromHeader(message) {
18
+ const headers = message?.payload?.headers;
19
+ if (!Array.isArray(headers)) return null;
20
+ const header = headers.find((item) => String(item?.name || '').toLowerCase() === 'from');
21
+ return typeof header?.value === 'string' ? header.value : null;
22
+ }
23
+
24
+ export function extractEmailAddress(value) {
25
+ if (typeof value !== 'string') return null;
26
+ const match = value.match(/([A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9.-]+\.[A-Z]{2,})/i);
27
+ return match ? match[1].trim().toLowerCase() : null;
28
+ }
29
+
30
+ export function gmailThreadSenderEmail(thread) {
31
+ const messages = thread?.messages;
32
+ if (!Array.isArray(messages) || messages.length === 0) {
33
+ throw providerError('gmail_thread_empty', 'Gmail thread does not contain any messages');
34
+ }
35
+
36
+ for (const message of messages) {
37
+ const raw = fromHeader(message);
38
+ const email = extractEmailAddress(raw);
39
+ if (email) return { email, raw, message_id: message.id || null };
40
+ }
41
+
42
+ throw providerError('gmail_sender_missing', 'Gmail thread does not contain a usable From header');
43
+ }
44
+
45
+ function validateSendUpdates(value) {
46
+ const normalized = value || 'none';
47
+ if (!SEND_UPDATES.has(normalized)) {
48
+ throw providerError('invalid_send_updates', 'context.send_updates must be all, externalOnly, or none');
49
+ }
50
+ return normalized;
51
+ }
52
+
53
+ function buildOperation(request, {
54
+ gmailBaseUrl = 'https://gmail.googleapis.com',
55
+ calendarBaseUrl = 'https://www.googleapis.com/calendar/v3'
56
+ } = {}) {
57
+ const context = request?.context || {};
58
+
59
+ if (request?.service === 'gmail' && request?.action === 'thread.read') {
60
+ const threadId = required(context.thread_id, 'context.thread_id');
61
+ const query = new URLSearchParams({ format: 'metadata' });
62
+ query.append('metadataHeaders', 'From');
63
+ return {
64
+ method: 'GET',
65
+ url: `${gmailBaseUrl}/gmail/v1/users/me/threads/${encodeURIComponent(threadId)}?${query}`
66
+ };
67
+ }
68
+
69
+ if (request?.service === 'calendar' && request?.action === 'event.create') {
70
+ const calendarId = context.calendar_id || 'primary';
71
+ const attendeeEmail = required(context.attendee_email, 'context.attendee_email');
72
+ const startTime = required(context.start_time, 'context.start_time');
73
+ const endTime = required(context.end_time, 'context.end_time');
74
+ const sendUpdates = validateSendUpdates(context.send_updates);
75
+
76
+ return {
77
+ method: 'POST',
78
+ url: `${calendarBaseUrl}/calendars/${encodeURIComponent(calendarId)}/events?sendUpdates=${encodeURIComponent(sendUpdates)}`,
79
+ body: {
80
+ summary: context.summary || 'Agent Authority task event',
81
+ description: context.description || undefined,
82
+ start: { dateTime: startTime },
83
+ end: { dateTime: endTime },
84
+ attendees: [{ email: attendeeEmail }],
85
+ transparency: context.transparency || undefined,
86
+ visibility: context.visibility || undefined
87
+ }
88
+ };
89
+ }
90
+
91
+ if (request?.service === 'calendar' && request?.action === 'event.delete') {
92
+ const calendarId = context.calendar_id || 'primary';
93
+ const eventId = required(context.event_id, 'context.event_id');
94
+ const sendUpdates = validateSendUpdates(context.send_updates);
95
+ return {
96
+ method: 'DELETE',
97
+ url: `${calendarBaseUrl}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}?sendUpdates=${encodeURIComponent(sendUpdates)}`
98
+ };
99
+ }
100
+
101
+ throw providerError(
102
+ 'unsupported_action',
103
+ `Google action ${request?.service || 'unknown'}:${request?.action || 'unknown'} has no provider operation mapping`
104
+ );
105
+ }
106
+
107
+ function accessToken(credential) {
108
+ const token = typeof credential === 'string' ? credential : credential?.access_token;
109
+ if (!token) throw providerError('credential_invalid', 'Google credential does not contain an access token');
110
+ return token;
111
+ }
112
+
113
+ async function responseBody(response) {
114
+ const text = await response.text();
115
+ if (!text) return null;
116
+ try { return JSON.parse(text); } catch { return text; }
117
+ }
118
+
119
+ function normalizedOutput(request, response, body) {
120
+ const common = {
121
+ provider: request.service,
122
+ status: response.status,
123
+ ok: response.ok,
124
+ request_id: response.headers?.get?.('x-request-id') || null
125
+ };
126
+
127
+ if (request.service === 'gmail' && request.action === 'thread.read') {
128
+ const sender = gmailThreadSenderEmail(body);
129
+ return {
130
+ ...common,
131
+ thread_id: body?.id || request.context?.thread_id || null,
132
+ message_count: Array.isArray(body?.messages) ? body.messages.length : 0,
133
+ sender_email: sender.email,
134
+ sender_raw: sender.raw,
135
+ sender_message_id: sender.message_id
136
+ };
137
+ }
138
+
139
+ if (request.service === 'calendar' && request.action === 'event.create') {
140
+ return {
141
+ ...common,
142
+ event_id: body?.id || null,
143
+ html_link: body?.htmlLink || null,
144
+ attendees: Array.isArray(body?.attendees)
145
+ ? body.attendees.map((item) => item?.email).filter(Boolean)
146
+ : []
147
+ };
148
+ }
149
+
150
+ if (request.service === 'calendar' && request.action === 'event.delete') {
151
+ return { ...common, deleted: response.ok };
152
+ }
153
+
154
+ return { ...common, body };
155
+ }
156
+
157
+ export function createGoogleProviderAdapter({
158
+ broker,
159
+ fetchImpl = globalThis.fetch,
160
+ gmailBaseUrl = 'https://gmail.googleapis.com',
161
+ calendarBaseUrl = 'https://www.googleapis.com/calendar/v3'
162
+ } = {}) {
163
+ if (!broker) throw new Error('credential broker is required');
164
+ if (typeof fetchImpl !== 'function') throw new Error('fetch implementation is required');
165
+
166
+ const operationFor = (request) => buildOperation(request, { gmailBaseUrl, calendarBaseUrl });
167
+
168
+ const adapter = brokeredProviderAdapter({
169
+ kind: 'google-rest',
170
+ services: ['gmail', 'calendar'],
171
+ broker,
172
+ async execute({ request, credential }) {
173
+ const operation = operationFor(request);
174
+ const response = await fetchImpl(operation.url, {
175
+ method: operation.method,
176
+ headers: {
177
+ accept: 'application/json',
178
+ authorization: `Bearer ${accessToken(credential)}`,
179
+ 'content-type': 'application/json',
180
+ 'user-agent': 'nullsquare-agent-authority/0.4'
181
+ },
182
+ body: operation.body ? JSON.stringify(operation.body) : undefined
183
+ });
184
+
185
+ const body = await responseBody(response);
186
+ const output = normalizedOutput(request, response, body);
187
+
188
+ if (!response.ok) {
189
+ const error = providerError('provider_error', `${request.service} API ${response.status}`);
190
+ error.provider_output = output;
191
+ throw error;
192
+ }
193
+
194
+ return output;
195
+ }
196
+ });
197
+
198
+ adapter.validateRequest = (request) => operationFor(request);
199
+ adapter.isMutation = (request) => MUTATING_ACTIONS.has(request?.action);
200
+ return adapter;
201
+ }
@@ -1,41 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1600" height="640" viewBox="0 0 1600 640" role="img" aria-label="Agent Authority — mission scoped authorization for AI agents">
2
- <defs>
3
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
- <stop offset="0" stop-color="#07111f"/><stop offset="1" stop-color="#121a36"/>
5
- </linearGradient>
6
- <linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
7
- <stop offset="0" stop-color="#7c5cff"/><stop offset="1" stop-color="#4ecbff"/>
8
- </linearGradient>
9
- </defs>
10
- <rect width="1600" height="640" rx="36" fill="url(#bg)"/>
11
- <g font-family="Arial, Helvetica, sans-serif" fill="#fff">
12
- <text x="100" y="120" font-size="28" font-weight="700" letter-spacing="3">NULLSQUARE / OPEN SOURCE</text>
13
- <text x="100" y="220" font-size="78" font-weight="800">Agent Authority</text>
14
- <text x="100" y="285" font-size="34" fill="#b9c7e6">Mission-scoped authorization for AI agents</text>
15
- <text x="100" y="355" font-size="27" fill="#8fe1ff">One mission · Any agent · Any auth system</text>
16
- </g>
17
- <g transform="translate(930 95)" font-family="Arial, Helvetica, sans-serif">
18
- <rect x="120" y="0" width="330" height="72" rx="16" fill="#111c34" stroke="#6d85ff"/>
19
- <text x="285" y="44" text-anchor="middle" font-size="24" fill="#fff">Human-approved mission</text>
20
- <path d="M285 72 V120" stroke="#7786a8" stroke-width="3"/>
21
- <rect x="80" y="120" width="410" height="90" rx="18" fill="#171842" stroke="#8b67ff" stroke-width="2"/>
22
- <text x="285" y="158" text-anchor="middle" font-size="25" font-weight="700" fill="#fff">AUTHORITY RUNTIME</text>
23
- <text x="285" y="190" text-anchor="middle" font-size="20" fill="#b9c7e6">allow · deny · approve · revoke</text>
24
- <path d="M285 210 V260" stroke="#7786a8" stroke-width="3"/>
25
- <rect x="80" y="260" width="410" height="72" rx="16" fill="#101b30" stroke="#4ecbff"/>
26
- <text x="285" y="304" text-anchor="middle" font-size="23" fill="#fff">Adapters / connectors</text>
27
- <path d="M285 332 V382" stroke="#7786a8" stroke-width="3"/>
28
- <g fill="#101b30" stroke="#4b628c">
29
- <rect x="0" y="382" width="135" height="64" rx="14"/><rect x="145" y="382" width="135" height="64" rx="14"/>
30
- <rect x="290" y="382" width="135" height="64" rx="14"/><rect x="435" y="382" width="135" height="64" rx="14"/>
31
- </g>
32
- <g font-size="20" fill="#fff" text-anchor="middle">
33
- <text x="67" y="422">OAuth</text><text x="212" y="422">MCP</text><text x="357" y="422">API / CLI</text><text x="502" y="422">Legacy</text>
34
- </g>
35
- </g>
36
- <rect x="100" y="430" width="690" height="2" fill="url(#accent)"/>
37
- <g font-family="Arial, Helvetica, sans-serif" font-size="22" fill="#b9c7e6">
38
- <text x="100" y="485">Delegation attenuation</text><text x="330" y="485">Human approvals</text><text x="555" y="485">Action receipts</text>
39
- <text x="100" y="530">Credential isolation</text><text x="330" y="530">Legacy compatibility</text><text x="555" y="530">Protocol neutral</text>
40
- </g>
41
- </svg>