@nullsquare/agent-authority 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  **Agent Authority turns a human-approved task into temporary execution authority, then keeps that authority bounded as the agent discovers resources, crosses tools, and performs side effects.**
10
10
 
11
- [Task Leases](docs/task-leases.md) · [Validate](docs/validation.md) · [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)
11
+ [Task Leases](docs/task-leases.md) · [Executable evidence](docs/evidence.md) · [Extractor conformance](docs/authority-extractor-conformance.md) · [Google proof](docs/live-google-validation.md) · [Integration contract](docs/integration-contract.md) · [CLI](docs/cli.md) · [Architecture](docs/architecture.md) · [Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
12
12
 
13
- > **Status: public pre-alpha / v0.4.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.
13
+ > **Status: public pre-alpha / v0.4.3 Developer Preview.** Published on npm as `@nullsquare/agent-authority`. The repository has a working policy runtime, protocol-neutral guard, Task Lease prototype, execution-bound derived facts, reviewed Google and GitHub authority extractors, two-provider conformance tests, approvals, revocation, idempotency, credential isolation, MCP v2 gateway, live GitHub proofs, CI and CodeQL. It is not production-ready yet.
14
14
 
15
15
  </div>
16
16
 
@@ -158,7 +158,9 @@ The demo performs this flow without provider credentials:
158
158
 
159
159
  The side-effect callbacks for blocked actions never run.
160
160
 
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).
161
+ The repository also includes a real Gmail → Calendar validation path and a reusable Google provider adapter. The strict path binds the derived sender to the exact guarded output before it becomes authority. See [Live Gmail → Calendar validation](docs/live-google-validation.md) and [Executable Evidence](docs/evidence.md).
162
+
163
+ v0.4.3 applies the **same primitive to GitHub**: a root-bound repository + fixture marker are used by the reviewed GitHub adapter to select one issue from a real `issue.list` response; `deriveFromEvidence()` establishes that exact issue number as downstream authority; one real comment mutation succeeds; unrelated and post-completion issue mutations never reach the provider. Google and GitHub are now exercised by the same [authority extractor conformance contract](docs/authority-extractor-conformance.md).
162
164
 
163
165
  ## Minimal developer API
164
166
 
@@ -264,6 +266,8 @@ The long-term validation target is the **same Task Lease and authority lineage a
264
266
  - execution evidence binding an allowed receipt, request and exact output hash
265
267
  - strict `deriveFromEvidence()` path where the caller cannot provide the authority value
266
268
  - reviewed Gmail sender authority extractor bound to `gmail:thread.read`
269
+ - reviewed GitHub selected-issue-number extractor bound to marker-scoped `github:issue.list`
270
+ - shared Google/GitHub authority-extractor conformance suite
267
271
  - legacy host-trusted `derive()` compatibility path
268
272
  - required parent lineage and extraction selector
269
273
  - exact context-field bindings
@@ -288,6 +292,7 @@ The long-term validation target is the **same Task Lease and authority lineage a
288
292
  - AES-256-GCM local encrypted secret store
289
293
  - safe reconnect cleanup
290
294
  - GitHub brokered execution without returning the token to the agent
295
+ - GitHub REST mappings for repository access plus evidence-derived `issue.list` / `issue.comment`
291
296
  - Google REST provider mappings for Gmail thread reads and Calendar event mutations
292
297
  - short-lived signed local agent-instance tokens
293
298
  - local CLI/daemon
@@ -296,11 +301,12 @@ The long-term validation target is the **same Task Lease and authority lineage a
296
301
 
297
302
  - adversarial authorization tests
298
303
  - execution-evidence substitution, tampering, replay, cross-lease and selector tests
304
+ - the same provider-derived-authority conformance attacks against Google and GitHub
299
305
  - Node 20 and Node 22 CI
300
306
  - coverage run
301
307
  - package checks
302
308
  - clean-consumer npm registry verification
303
- - live GitHub read and mutation proofs
309
+ - live GitHub read and evidence-derived mutation proofs
304
310
  - CodeQL
305
311
 
306
312
  ## What is different from OAuth, IAM and MCP authorization?
package/ROADMAP.md CHANGED
@@ -97,17 +97,21 @@ Do **not** build a general semantic policy language unless real integrations req
97
97
 
98
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.
99
99
 
100
- ## M4 — Same task, multiple transports
100
+ ## M4 — Same task, multiple transports — first proof established
101
101
 
102
102
  Prove Agent Authority is not an MCP product or SDK wrapper.
103
103
 
104
- - [ ] same Task Lease through ordinary `guard.run()` SDK call
105
- - [ ] same Task Lease through MCP gateway
106
- - [ ] same Task Lease through brokered provider execution
104
+ - [x] same Task Lease through ordinary `guard.run()` SDK call
105
+ - [x] same Task Lease through MCP gateway
106
+ - [x] same Task Lease through brokered provider execution
107
107
  - [ ] at least one non-bypassable harness/tool-middleware integration
108
- - [ ] interoperability test vectors across transports
108
+ - [x] interoperability test vectors across transports
109
109
 
110
- **Success criterion:** changing transport or harness does not expand the task's authority.
110
+ `test/transport-invariance.test.js` establishes one `execution-evidence-v1` derived fact from brokered execution, then reuses that exact Task Lease and fact through direct SDK, MCP and brokered execution. The three paths produce the same `allow`, `authority_delta_required` and `task_lease_completed` outcomes, and blocked attempts execute zero host callbacks, MCP upstream calls or brokered provider operations.
111
+
112
+ Brokered Task Lease execution deliberately does not consume mission-level one-time approval to override a lease-level authority delta. Updating a live Task Lease after explicit approval remains separate M2 work.
113
+
114
+ **Success criterion:** changing transport or harness does not expand the task's authority. The SDK/MCP/broker portion is now demonstrated in-process; an external non-bypassable harness/tool-middleware integration remains the final M4 proof.
111
115
 
112
116
  ## M5 — Production credential and approval UX
113
117
 
@@ -13,19 +13,24 @@ Before any publication:
13
13
  After publication, verify from a fresh project with:
14
14
 
15
15
  ```bash
16
- npm install @nullsquare/agent-authority@0.4.2
16
+ npm install @nullsquare/agent-authority@0.4.3
17
17
  ```
18
18
 
19
19
  Then run the same consumer smoke flow through the registry-installed package. Registry verification is part of the release gate; a successful `npm publish` command alone is not sufficient.
20
20
 
21
- The repository also includes `.github/workflows/verify-npm-registry.yml`, which verifies registry visibility and a clean consumer install. For v0.4.2 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` export plus the Google provider and Gmail authority-extractor exports.
21
+ The repository also includes `.github/workflows/verify-npm-registry.yml`, which verifies registry visibility and a clean consumer install. For v0.4.3 it additionally verifies the public `@nullsquare/agent-authority/authority-evidence` API plus both reviewed provider authority extractors:
22
+
23
+ - `gmailThreadSenderAuthorityExtractor` from `@nullsquare/agent-authority/providers/google`;
24
+ - `githubIssueListSelectedNumberAuthorityExtractor` from `@nullsquare/agent-authority/providers/github`.
25
+
26
+ This makes the registry artifact verification cover the same two-provider execution-evidence surface exercised by the repository conformance suite.
22
27
 
23
28
  ## npm vs GitHub release surfaces
24
29
 
25
30
  Publishing to the public npm registry does not automatically create either a GitHub Release or a GitHub Packages entry.
26
31
 
27
32
  - **npm registry** — `npm publish --access public` publishes `@nullsquare/agent-authority` to `registry.npmjs.org` / npmjs.com. This is the package users install with `npm install`.
28
- - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.2`. A release must be created explicitly or by release automation.
33
+ - **GitHub Releases** — a separate GitHub object, normally backed by a Git tag such as `v0.4.3`. A release must be created explicitly or by release automation.
29
34
  - **GitHub Packages** — a separate package registry. It only appears when the package is published to GitHub's npm registry (`npm.pkg.github.com`); publishing to npmjs.com does not populate it.
30
35
 
31
36
  Agent Authority currently uses npmjs.com as its public package registry. Therefore an empty GitHub **Packages** section is expected unless the project intentionally adopts dual publication. A GitHub **Release** is still useful for source-release discoverability and should track published versions, but it is independent from npm publication.
@@ -0,0 +1,117 @@
1
+ # Task Lease transport invariance
2
+
3
+ Agent Authority treats SDK calls, MCP calls and brokered provider execution as execution paths, not separate authority models.
4
+
5
+ The property under test is:
6
+
7
+ ```text
8
+ same Task Lease + same established authority fact
9
+ |
10
+ +----------+----------+
11
+ | | |
12
+ direct MCP brokered
13
+ guard.run() gateway provider
14
+ | | |
15
+ +----------+----------+
16
+ |
17
+ same authority
18
+ ```
19
+
20
+ Changing the transport must not broaden task authority.
21
+
22
+ ## Executable proof
23
+
24
+ `test/transport-invariance.test.js` creates one Task Lease and one derived authority fact.
25
+
26
+ The fact is first established from a brokered provider result using the strict evidence path:
27
+
28
+ ```text
29
+ Task Lease root
30
+ |
31
+ v
32
+ brokered item.discover
33
+ |
34
+ +--> Task-Lease ALLOW receipt
35
+ +--> exact output hash evidence
36
+ |
37
+ v
38
+ reviewed test extractor
39
+ |
40
+ v
41
+ deriveFromEvidence()
42
+ |
43
+ v
44
+ fact:selected-item = alpha
45
+ ```
46
+
47
+ That exact Task Lease and fact are then used through three execution paths:
48
+
49
+ 1. ordinary in-process `guard.run()`;
50
+ 2. `MissionMcpGateway` configured with the Task Lease;
51
+ 3. `ExecutingAuthorityRuntime.executeTaskLease()` with a brokered provider adapter.
52
+
53
+ For `item = alpha`, every path allows execution.
54
+
55
+ For `item = beta`, every path returns the same task-level authority delta:
56
+
57
+ ```text
58
+ authority_delta_required
59
+ ```
60
+
61
+ The blocked direct callback, MCP upstream call and brokered provider operation all remain unexecuted.
62
+
63
+ After the Task Lease is completed, all three paths return:
64
+
65
+ ```text
66
+ task_lease_completed
67
+ ```
68
+
69
+ Again, no blocked execution reaches the host callback or provider boundary.
70
+
71
+ ## Broker behavior
72
+
73
+ `ExecutingAuthorityRuntime.executeTaskLease()` evaluates the Task Lease before adapter readiness or provider execution.
74
+
75
+ A lease-level `require_approval` result is returned as-is. Brokered execution does not consume a mission-level one-time approval to bypass the narrower Task Lease. Applying an explicitly approved authority delta back into a live Task Lease is separate roadmap work.
76
+
77
+ Successful brokered Task Lease execution returns execution evidence bound to the Task-Lease receipt and exact provider output, so strict derived authority can originate from brokered execution as well as from `guard.run()`.
78
+
79
+ ## MCP behavior
80
+
81
+ `MissionMcpGateway` remains backward-compatible with Mission-only use, but now accepts exactly one authority source:
82
+
83
+ ```text
84
+ mission OR lease
85
+ ```
86
+
87
+ When configured with a Task Lease, each tool call is evaluated through that lease before `upstream.callTool()` can run.
88
+
89
+ MCP result metadata includes:
90
+
91
+ ```text
92
+ io.nullsquare.agent-authority/decision
93
+ io.nullsquare.agent-authority/code
94
+ io.nullsquare.agent-authority/receipt_hash
95
+ io.nullsquare.agent-authority/task_lease_id
96
+ ```
97
+
98
+ The remote MCP handler and loopback proxy can pass the same Task Lease into the gateway.
99
+
100
+ ## What this proves
101
+
102
+ - Task-Lease narrowing is no longer specific to the direct SDK guard;
103
+ - MCP cannot silently fall back to Mission-only authority when explicitly configured with a Task Lease;
104
+ - brokered provider execution can enforce the same Task Lease before credential-backed execution;
105
+ - one derived fact can constrain all three execution paths;
106
+ - task completion invalidates the same authority across all three paths;
107
+ - broker credentials may remain connected after task authority disappears.
108
+
109
+ ## What this does not prove yet
110
+
111
+ - a hostile harness cannot bypass Agent Authority through an entirely separate unguarded tool path;
112
+ - Task Lease state survives process restart;
113
+ - the same lease is serialized and recovered across separate processes or hosts;
114
+ - an approved authority delta is durably applied back into a running lease;
115
+ - provider outputs are cryptographically attested by providers.
116
+
117
+ The remaining M4 target is at least one real harness/tool-middleware integration where executable tool calls cannot bypass the Task Lease boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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",
package/src/execution.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AuthorityRuntime, createReceipt } from './index.js';
2
+ import { createExecutionEvidence } from './authority-evidence.js';
2
3
 
3
4
  function executionFailure(mission, request, code, reason, extra = {}) {
4
5
  const result = { decision: 'deny', code, reason, ...extra };
@@ -115,10 +116,39 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
115
116
  }
116
117
  }
117
118
 
118
- async execute(missionInput, request) {
119
- let evaluation = this.evaluate(missionInput, request);
119
+ /**
120
+ * Execute through the broker while preserving Task Lease narrowing.
121
+ *
122
+ * A lease-level REQUIRE_APPROVAL is returned before adapter readiness or
123
+ * provider execution. It is not converted into a mission-level one-time
124
+ * approval because applying an authority delta back into a live lease is a
125
+ * separate, not-yet-implemented capability.
126
+ */
127
+ async executeTaskLease(lease, request) {
128
+ if (!lease || typeof lease.evaluate !== 'function' || !lease.mission) {
129
+ throw new Error('task lease with mission and evaluate() is required');
130
+ }
131
+ return this.execute(lease.mission, request, { lease });
132
+ }
133
+
134
+ async execute(missionInput, request, { lease = null } = {}) {
135
+ if (lease && lease.mission?.mission_id !== missionInput?.mission_id) {
136
+ throw new Error('task lease mission does not match execution mission');
137
+ }
138
+
139
+ let evaluation = lease
140
+ ? lease.evaluate(this, request)
141
+ : this.evaluate(missionInput, request);
142
+
120
143
  if (evaluation.result.decision === 'deny') return { ...evaluation, output: null };
121
144
 
145
+ // A Task Lease is the narrowest authority object. Do not let brokered
146
+ // execution reinterpret a lease-level authority delta as a broader mission
147
+ // approval. This keeps broker behavior aligned with guard.run() and MCP.
148
+ if (lease && evaluation.result.decision !== 'allow') {
149
+ return { ...evaluation, output: null };
150
+ }
151
+
122
152
  const adapter = this.adapters.resolve(request.service);
123
153
  if (!adapter) {
124
154
  return executionFailure(missionInput, request, 'adapter_unavailable', `no adapter is registered for ${request.service}`);
@@ -148,6 +178,7 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
148
178
 
149
179
  try {
150
180
  const output = await adapter.execute({ mission: missionInput, request });
181
+ const evidence = createExecutionEvidence({ receipt: evaluation.receipt, output });
151
182
  let usage = null;
152
183
  if (budgetCheck && !budgetCheck.result) {
153
184
  const spent = this.usage.record(missionInput.mission_id, budgetCheck.currency, budgetCheck.amount);
@@ -160,7 +191,7 @@ export class ExecutingAuthorityRuntime extends AuthorityRuntime {
160
191
  if (executionRecord && this.executions) {
161
192
  this.executions.complete({ mission: missionInput, request, receipt_id: evaluation.receipt?.receipt_id || null });
162
193
  }
163
- return { ...evaluation, output, usage, execution: executionRecord || null };
194
+ return { ...evaluation, output, evidence, usage, execution: executionRecord || null };
164
195
  } catch (error) {
165
196
  if (executionRecord && this.executions) {
166
197
  this.executions.uncertain({ mission: missionInput, request, error_code: error.code || 'provider_error' });
@@ -31,6 +31,15 @@ export function isDeclaredReadOnlyTool(tool) {
31
31
  return tool?.annotations?.readOnlyHint === true;
32
32
  }
33
33
 
34
+ function authorityMeta(evaluation = {}) {
35
+ return {
36
+ 'io.nullsquare.agent-authority/decision': evaluation.result?.decision || 'deny',
37
+ 'io.nullsquare.agent-authority/code': evaluation.result?.code || null,
38
+ 'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null,
39
+ 'io.nullsquare.agent-authority/task_lease_id': evaluation.receipt?.task_lease_id || null
40
+ };
41
+ }
42
+
34
43
  function deniedToolResult(result, extra = {}) {
35
44
  return {
36
45
  content: [{
@@ -47,32 +56,43 @@ function deniedToolResult(result, extra = {}) {
47
56
  }
48
57
 
49
58
  /**
50
- * Small, transport-neutral policy gateway for MCP tools.
59
+ * Small policy gateway for MCP tools.
51
60
  *
52
61
  * The upstream object only needs two methods:
53
62
  * listTools(params?) -> { tools: [...] }
54
63
  * callTool(params) -> MCP CallToolResult
55
64
  *
56
- * v0.1 deliberately defaults to read-only enforcement. A tool is considered
65
+ * The gateway accepts either a Mission or a Task Lease. When a Task Lease is
66
+ * supplied, every MCP tool call is evaluated through that exact lease before
67
+ * the upstream callback can run. This keeps transport changes from bypassing
68
+ * task-level narrowing.
69
+ *
70
+ * The gateway still defaults to read-only enforcement. A tool is considered
57
71
  * read-only only when its MCP annotations explicitly set readOnlyHint=true.
58
- * Write support belongs behind the existing approval + idempotency runtime and
59
- * is intentionally not inferred from tool names.
72
+ * Write support must be enabled deliberately; it is never inferred from names.
60
73
  */
61
74
  export class MissionMcpGateway {
62
75
  constructor({
63
76
  mission,
77
+ lease,
64
78
  runtime,
65
79
  upstream,
66
80
  service = 'mcp:upstream',
67
81
  readOnly = true,
68
82
  contextMapper = contextFromToolArguments
69
83
  } = {}) {
70
- if (!mission) throw new Error('mission is required');
84
+ if ((mission && lease) || (!mission && !lease)) {
85
+ throw new Error('provide exactly one of mission or lease');
86
+ }
87
+ if (lease && (typeof lease.evaluate !== 'function' || !lease.mission)) {
88
+ throw new Error('lease must provide mission and evaluate(runtime, request)');
89
+ }
71
90
  if (!runtime || typeof runtime.evaluate !== 'function') throw new Error('authority runtime is required');
72
91
  if (!upstream || typeof upstream.listTools !== 'function' || typeof upstream.callTool !== 'function') {
73
92
  throw new Error('upstream MCP client must implement listTools() and callTool()');
74
93
  }
75
- this.mission = mission;
94
+ this.mission = mission || lease.mission;
95
+ this.lease = lease || null;
76
96
  this.runtime = runtime;
77
97
  this.upstream = upstream;
78
98
  this.service = service;
@@ -81,6 +101,12 @@ export class MissionMcpGateway {
81
101
  this.tools = new Map();
82
102
  }
83
103
 
104
+ evaluate(request) {
105
+ return this.lease
106
+ ? this.lease.evaluate(this.runtime, request)
107
+ : this.runtime.evaluate(this.mission, request);
108
+ }
109
+
84
110
  async refreshTools(params = undefined) {
85
111
  const listed = await this.upstream.listTools(params);
86
112
  for (const tool of listed.tools || []) this.tools.set(tool.name, tool);
@@ -122,11 +148,9 @@ export class MissionMcpGateway {
122
148
  action: mcpToolAction(toolName),
123
149
  context
124
150
  };
125
- const evaluation = this.runtime.evaluate(this.mission, request);
151
+ const evaluation = this.evaluate(request);
126
152
  if (evaluation.result.decision !== 'allow') {
127
- return deniedToolResult(evaluation.result, {
128
- 'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null
129
- });
153
+ return deniedToolResult(evaluation.result, authorityMeta(evaluation));
130
154
  }
131
155
 
132
156
  const output = await this.upstream.callTool(params);
@@ -134,8 +158,7 @@ export class MissionMcpGateway {
134
158
  ...output,
135
159
  _meta: {
136
160
  ...(output?._meta || {}),
137
- 'io.nullsquare.agent-authority/decision': 'allow',
138
- 'io.nullsquare.agent-authority/receipt_hash': evaluation.receipt?.receipt_hash || null
161
+ ...authorityMeta(evaluation)
139
162
  }
140
163
  };
141
164
  }
package/src/mcp-remote.js CHANGED
@@ -54,6 +54,7 @@ export class RemoteMcpUpstream {
54
54
 
55
55
  export function createMcpGatewayHandler({
56
56
  mission,
57
+ lease,
57
58
  runtime,
58
59
  upstream,
59
60
  upstreamUrl,
@@ -63,6 +64,7 @@ export function createMcpGatewayHandler({
63
64
  const resolvedUpstream = upstream || new RemoteMcpUpstream({ url: upstreamUrl });
64
65
  const gateway = new MissionMcpGateway({
65
66
  mission,
67
+ lease,
66
68
  runtime,
67
69
  upstream: resolvedUpstream,
68
70
  service,
@@ -74,7 +76,9 @@ export function createMcpGatewayHandler({
74
76
  {
75
77
  name: 'agent-authority-gateway',
76
78
  version: '0.3.0',
77
- description: 'Mission-aware policy gateway for MCP tools'
79
+ description: lease
80
+ ? 'Task-Lease-aware policy gateway for MCP tools'
81
+ : 'Mission-aware policy gateway for MCP tools'
78
82
  },
79
83
  { capabilities: { tools: {} } }
80
84
  );
package/src/mcp-server.js CHANGED
@@ -30,6 +30,7 @@ function sendJson(res, status, value) {
30
30
  */
31
31
  export function createMcpProxyServer({
32
32
  mission,
33
+ lease,
33
34
  runtime,
34
35
  upstream,
35
36
  upstreamUrl,
@@ -43,6 +44,7 @@ export function createMcpProxyServer({
43
44
 
44
45
  const gateway = createMcpGatewayHandler({
45
46
  mission,
47
+ lease,
46
48
  runtime,
47
49
  upstream,
48
50
  upstreamUrl,
@@ -64,6 +66,7 @@ export function createMcpProxyServer({
64
66
  ok: true,
65
67
  service: 'agent-authority-mcp-gateway',
66
68
  mode: 'read-only',
69
+ authority: lease ? 'task-lease' : 'mission',
67
70
  upstream: upstreamUrl || 'injected'
68
71
  });
69
72
  }