@nullsquare/agent-authority 0.4.3 → 0.4.5

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.
@@ -0,0 +1,153 @@
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 transport 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
+ ## AI SDK harness proof
101
+
102
+ `test/integrations/ai-sdk.integration.mjs` drives the current Vercel AI SDK `ToolLoopAgent` with the output of `protectAiSdkTools()` as its executable tool set.
103
+
104
+ The positive path asks the model to use the task-bound GitHub issue. The protected tool executes exactly once.
105
+
106
+ The same real agent loop then exercises three adversarial paths:
107
+
108
+ ```text
109
+ unrelated issue
110
+ -> AuthorityApprovalRequiredError
111
+ -> authority_delta_required
112
+ -> underlying effect count remains 0
113
+
114
+ executable tool with no Agent Authority mapping
115
+ -> UnmappedAiSdkToolError
116
+ -> ai_sdk_tool_unmapped
117
+ -> underlying effect count remains 0
118
+
119
+ completed Task Lease
120
+ -> AuthorityDeniedError
121
+ -> task_lease_completed
122
+ -> underlying effect count remains 0
123
+ ```
124
+
125
+ The AI SDK represents tool execution failures in the generated result as `tool-error` content parts rather than requiring `agent.generate()` itself to reject. The proof therefore checks both sides of the boundary: the harness records the exact Agent Authority error and the protected underlying side effect never runs.
126
+
127
+ This matters because the model is not calling the wrapped function directly in these cases. The real `ToolLoopAgent` selects and invokes the tool through its normal tool loop, and the Agent Authority wrapper remains the executable boundary.
128
+
129
+ ## What this proves
130
+
131
+ - Task-Lease narrowing is not specific to the direct SDK guard;
132
+ - MCP cannot silently fall back to Mission-only authority when explicitly configured with a Task Lease;
133
+ - brokered provider execution enforces the same Task Lease before credential-backed execution;
134
+ - one evidence-derived fact can constrain direct SDK, MCP and brokered execution;
135
+ - task completion invalidates the same authority across those execution paths;
136
+ - broker credentials may remain connected after task authority disappears;
137
+ - a configured Vercel AI SDK `ToolLoopAgent` whose executable tool set is passed through `protectAiSdkTools()` cannot use its normal tool path to bypass Task-Lease narrowing;
138
+ - executable AI SDK tools without an Agent Authority request mapping fail closed before their underlying effect executes.
139
+
140
+ ## Boundary of the claim
141
+
142
+ This is an execution-boundary guarantee, not hostile-host containment.
143
+
144
+ It does **not** prove that a malicious application host cannot deliberately give the model another unwrapped tool, direct provider credential, shell, network client or other execution channel outside Agent Authority.
145
+
146
+ It also does not yet prove that:
147
+
148
+ - Task Lease state survives process restart;
149
+ - the same lease can be serialized and safely recovered across separate processes or hosts;
150
+ - an approved authority delta is durably applied back into a running lease;
151
+ - provider outputs are cryptographically attested by providers.
152
+
153
+ M4 is complete for configured Agent Authority execution boundaries: direct SDK, MCP, brokered execution and the Vercel AI SDK `ToolLoopAgent` protected-tool path now preserve task authority without silent expansion. Durability, cross-process recovery and hostile-host containment are separate problems.
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.5",
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",
@@ -41,7 +41,7 @@
41
41
  "test": "node --test test/*.test.js",
42
42
  "test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
43
43
  "test:coverage": "node --experimental-test-coverage --test test/*.test.js",
44
- "check:syntax": "node --check src/index.js && node --check src/authority-evidence.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
+ "check:syntax": "node --check src/index.js && node --check src/authority-evidence.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/durable-task-lease.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",
45
45
  "check:package": "npm pack --dry-run",
46
46
  "check": "npm run check:syntax && npm test && npm run demo:task-lease && npm run check:package"
47
47
  },
@@ -62,6 +62,7 @@
62
62
  "./approvals": "./src/approvals.js",
63
63
  "./authority-evidence": "./src/authority-evidence.js",
64
64
  "./connections": "./src/connections.js",
65
+ "./durable-task-lease": "./src/durable-task-lease.js",
65
66
  "./execution": "./src/execution.js",
66
67
  "./guard": "./src/guard.js",
67
68
  "./harness-bridge": "./src/harness-bridge.js",
@@ -0,0 +1,185 @@
1
+ import { TaskLease } from './task-lease.js';
2
+
3
+ function sessionError(code, message, details = {}) {
4
+ const error = new Error(message);
5
+ error.code = code;
6
+ Object.assign(error, details);
7
+ return error;
8
+ }
9
+
10
+ function assertStore(store) {
11
+ if (!store || typeof store !== 'object') {
12
+ throw new Error('durable Task Lease store is required');
13
+ }
14
+ for (const method of ['save', 'load', 'transact']) {
15
+ if (typeof store[method] !== 'function') {
16
+ throw new Error(`durable Task Lease store must implement ${method}()`);
17
+ }
18
+ }
19
+ return store;
20
+ }
21
+
22
+ function assertLease(lease) {
23
+ if (!(lease instanceof TaskLease)) throw new Error('TaskLease instance is required');
24
+ return lease;
25
+ }
26
+
27
+ /**
28
+ * A small stateful facade over JsonFileTaskLeaseStore-style transactional
29
+ * persistence.
30
+ *
31
+ * The session never exposes its mutable TaskLease instance. Reads come from the
32
+ * cached recovered lease, explicit refresh() reloads authenticated state, and
33
+ * evaluate() refreshes before every security decision so another worker's
34
+ * completion or narrowing is observed before the next guarded effect.
35
+ *
36
+ * Mutations use optimistic compare-and-swap against the session's current lease
37
+ * hash. A stale session receives task_lease_state_conflict and must refresh and
38
+ * reconsider the intended authority mutation; semantic mutations are never
39
+ * silently replayed against a newer authority state.
40
+ */
41
+ export class DurableTaskLeaseSession {
42
+ constructor({ store, mission, lease_id, lease, lease_hash } = {}) {
43
+ this.store = assertStore(store);
44
+ if (!mission || typeof mission !== 'object') throw new Error('mission is required');
45
+ if (!lease_id) throw new Error('lease_id is required');
46
+ assertLease(lease);
47
+ if (lease.lease_id !== lease_id || lease.mission.mission_id !== mission.mission_id) {
48
+ throw sessionError('durable_task_lease_identity_mismatch', 'session lease identity does not match mission and lease_id');
49
+ }
50
+
51
+ this._mission = structuredClone(mission);
52
+ this.lease_id = lease_id;
53
+ this._lease = lease;
54
+ this._leaseHash = lease_hash || lease.hash();
55
+ if (this._leaseHash !== lease.hash()) {
56
+ throw sessionError('durable_task_lease_hash_mismatch', 'session lease hash does not match recovered Task Lease state');
57
+ }
58
+ }
59
+
60
+ get mission() {
61
+ return structuredClone(this._mission);
62
+ }
63
+
64
+ get status() {
65
+ return this._lease.status;
66
+ }
67
+
68
+ get expires_at() {
69
+ return this._lease.expires_at;
70
+ }
71
+
72
+ get completed_at() {
73
+ return this._lease.completed_at;
74
+ }
75
+
76
+ get completion_reason() {
77
+ return this._lease.completion_reason;
78
+ }
79
+
80
+ hash() {
81
+ return this._leaseHash;
82
+ }
83
+
84
+ snapshot() {
85
+ return structuredClone(this._lease.snapshot());
86
+ }
87
+
88
+ fact(factId) {
89
+ return this._lease.fact(factId);
90
+ }
91
+
92
+ listFacts() {
93
+ return this._lease.listFacts();
94
+ }
95
+
96
+ refresh() {
97
+ const lease = this.store.load({
98
+ mission: this._mission,
99
+ lease_id: this.lease_id
100
+ });
101
+ if (!lease) {
102
+ throw sessionError(
103
+ 'task_lease_state_missing',
104
+ `task lease ${this.lease_id} has no durable state`,
105
+ { lease_id: this.lease_id }
106
+ );
107
+ }
108
+ this._lease = lease;
109
+ this._leaseHash = lease.hash();
110
+ return this;
111
+ }
112
+
113
+ evaluate(runtime, request, now = new Date()) {
114
+ this.refresh();
115
+ return this._lease.evaluate(runtime, request, now);
116
+ }
117
+
118
+ _commit(mutator) {
119
+ const result = this.store.transact({
120
+ mission: this._mission,
121
+ lease_id: this.lease_id,
122
+ expected_lease_hash: this._leaseHash,
123
+ mutate: mutator
124
+ });
125
+ this._lease = result.lease;
126
+ this._leaseHash = result.lease_hash;
127
+ return result.value;
128
+ }
129
+
130
+ addRoot(options) {
131
+ return this._commit((lease) => lease.addRoot(options));
132
+ }
133
+
134
+ derive(options) {
135
+ return this._commit((lease) => lease.derive(options));
136
+ }
137
+
138
+ deriveFromEvidence(options) {
139
+ return this._commit((lease) => lease.deriveFromEvidence(options));
140
+ }
141
+
142
+ bind(binding) {
143
+ return this._commit((lease) => lease.bind(binding));
144
+ }
145
+
146
+ complete(reason = 'task completed') {
147
+ return this._commit((lease) => lease.complete(reason));
148
+ }
149
+ }
150
+
151
+ export function createDurableTaskLeaseSession({ store, lease } = {}) {
152
+ assertStore(store);
153
+ assertLease(lease);
154
+ const mission = structuredClone(lease.mission);
155
+ const saved = store.save(lease);
156
+ const recovered = store.load({ mission, lease_id: lease.lease_id });
157
+ if (!recovered) {
158
+ throw sessionError('task_lease_state_missing', `task lease ${lease.lease_id} was not recoverable after creation`);
159
+ }
160
+ return new DurableTaskLeaseSession({
161
+ store,
162
+ mission,
163
+ lease_id: lease.lease_id,
164
+ lease: recovered,
165
+ lease_hash: saved.lease_hash
166
+ });
167
+ }
168
+
169
+ export function openDurableTaskLeaseSession({ store, mission, lease_id } = {}) {
170
+ assertStore(store);
171
+ if (!mission || typeof mission !== 'object') throw new Error('mission is required');
172
+ if (!lease_id) throw new Error('lease_id is required');
173
+ const missionSnapshot = structuredClone(mission);
174
+ const lease = store.load({ mission: missionSnapshot, lease_id });
175
+ if (!lease) {
176
+ throw sessionError('task_lease_state_missing', `task lease ${lease_id} has no durable state`, { lease_id });
177
+ }
178
+ return new DurableTaskLeaseSession({
179
+ store,
180
+ mission: missionSnapshot,
181
+ lease_id,
182
+ lease,
183
+ lease_hash: lease.hash()
184
+ });
185
+ }
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
  }