@nullsquare/agent-authority 0.4.6 → 0.4.7

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,223 @@
1
+ import { AuthorityApprovalRequiredError, AuthorityDeniedError } from '../src/guard.js';
2
+ import { createTask } from '../src/task.js';
3
+
4
+ const ticketId = 'ticket:4821';
5
+
6
+ function operationExtractor({ service, action, selector, extractorId, validate }) {
7
+ return ({ receipt, output } = {}) => {
8
+ if (receipt?.service !== service || receipt?.action !== action) {
9
+ const error = new Error(`${extractorId} received the wrong operation`);
10
+ error.code = 'trusted_extractor_operation_mismatch';
11
+ throw error;
12
+ }
13
+ const value = selector.split('.').slice(1).reduce((current, key) => current?.[key], output);
14
+ if (!validate(value)) {
15
+ const error = new Error(`${extractorId} received invalid normalized output`);
16
+ error.code = 'trusted_extractor_output_invalid';
17
+ throw error;
18
+ }
19
+ return { extractor_id: extractorId, selector };
20
+ };
21
+ }
22
+
23
+ const orderIdExtractor = operationExtractor({
24
+ service: 'helpdesk',
25
+ action: 'ticket.read',
26
+ selector: 'output.order_id',
27
+ extractorId: 'demo.helpdesk.ticket.order-id.v1',
28
+ validate: (value) => typeof value === 'string' && value.startsWith('order:')
29
+ });
30
+
31
+ const paymentIdExtractor = operationExtractor({
32
+ service: 'commerce',
33
+ action: 'order.read',
34
+ selector: 'output.payment_id',
35
+ extractorId: 'demo.commerce.order.payment-id.v1',
36
+ validate: (value) => typeof value === 'string' && value.startsWith('payment:')
37
+ });
38
+
39
+ const paymentAmountExtractor = operationExtractor({
40
+ service: 'payments',
41
+ action: 'payment.read',
42
+ selector: 'output.amount_minor',
43
+ extractorId: 'demo.payments.payment.amount-minor.v1',
44
+ validate: (value) => Number.isSafeInteger(value) && value >= 0
45
+ });
46
+
47
+ const paymentCurrencyExtractor = operationExtractor({
48
+ service: 'payments',
49
+ action: 'payment.read',
50
+ selector: 'output.currency',
51
+ extractorId: 'demo.payments.payment.currency.v1',
52
+ validate: (value) => typeof value === 'string' && /^[A-Z]{3}$/.test(value)
53
+ });
54
+
55
+ const task = createTask({
56
+ principal: 'user:finance-demo',
57
+ agent: 'agent:finance-demo',
58
+ request: 'Resolve this support ticket by refunding only its affected payment',
59
+ permissions: {
60
+ helpdesk: {
61
+ allow: ['ticket.read'],
62
+ constraints: { ticket_id: [ticketId] }
63
+ },
64
+ commerce: {
65
+ allow: ['order.read'],
66
+ constraints: {}
67
+ },
68
+ payments: {
69
+ allow: ['payment.read', 'refund.create'],
70
+ deny: ['payment.capture', 'refund.delete'],
71
+ constraints: {}
72
+ }
73
+ },
74
+ authority: {
75
+ ticket: { kind: 'helpdesk.ticket', value: ticketId }
76
+ },
77
+ bindings: [
78
+ { service: 'helpdesk', action: 'ticket.read', field: 'ticket_id', authority: 'ticket' },
79
+ { service: 'commerce', action: 'order.read', field: 'order_id', authority: 'order' },
80
+ { service: 'payments', action: 'payment.read', field: 'payment_id', authority: 'payment' },
81
+ { service: 'payments', action: 'refund.create', field: 'payment_id', authority: 'payment' },
82
+ { service: 'payments', action: 'refund.create', field: 'amount_minor', authority: 'paymentAmount' },
83
+ { service: 'payments', action: 'refund.create', field: 'currency', authority: 'paymentCurrency' }
84
+ ]
85
+ });
86
+
87
+ let ticketReads = 0;
88
+ let orderReads = 0;
89
+ let paymentReads = 0;
90
+ let refunds = 0;
91
+
92
+ console.log('Task: Resolve one support ticket by refunding only the payment discovered through that ticket');
93
+
94
+ console.log('1. Read the exact authorized ticket');
95
+ const ticket = await task.run({
96
+ service: 'helpdesk',
97
+ action: 'ticket.read',
98
+ context: { ticket_id: ticketId }
99
+ }, async () => {
100
+ ticketReads += 1;
101
+ return { ticket_id: ticketId, customer_id: 'customer:77', order_id: 'order:991' };
102
+ });
103
+
104
+ const order = task.authorityFrom(ticket, {
105
+ name: 'order',
106
+ kind: 'commerce.order',
107
+ from: 'ticket',
108
+ extractor: orderIdExtractor
109
+ });
110
+ console.log(` authority -> ${order.value}`);
111
+
112
+ console.log('2. Read only the order established by the ticket');
113
+ const orderRead = await task.run({
114
+ service: 'commerce',
115
+ action: 'order.read',
116
+ context: { order_id: order.value }
117
+ }, async () => {
118
+ orderReads += 1;
119
+ return { order_id: order.value, payment_id: 'payment:abc123' };
120
+ });
121
+
122
+ const payment = task.authorityFrom(orderRead, {
123
+ name: 'payment',
124
+ kind: 'payments.payment',
125
+ from: 'order',
126
+ extractor: paymentIdExtractor
127
+ });
128
+ console.log(` authority -> ${payment.value}`);
129
+
130
+ console.log('3. Read only the payment established by the order');
131
+ const paymentRead = await task.run({
132
+ service: 'payments',
133
+ action: 'payment.read',
134
+ context: { payment_id: payment.value }
135
+ }, async () => {
136
+ paymentReads += 1;
137
+ return { payment_id: payment.value, amount_minor: 12500, currency: 'USD' };
138
+ });
139
+
140
+ const amount = task.authorityFrom(paymentRead, {
141
+ name: 'paymentAmount',
142
+ kind: 'money.minor-units',
143
+ from: 'payment',
144
+ extractor: paymentAmountExtractor
145
+ });
146
+ const currency = task.authorityFrom(paymentRead, {
147
+ name: 'paymentCurrency',
148
+ kind: 'money.currency',
149
+ from: 'payment',
150
+ extractor: paymentCurrencyExtractor
151
+ });
152
+ console.log(` authority -> ${amount.value} minor units ${currency.value}`);
153
+
154
+ console.log('4. Refund exactly the payment established by the authorized chain');
155
+ await task.run({
156
+ service: 'payments',
157
+ action: 'refund.create',
158
+ context: {
159
+ payment_id: payment.value,
160
+ amount_minor: amount.value,
161
+ currency: currency.value
162
+ }
163
+ }, async () => {
164
+ refunds += 1;
165
+ return { refund_id: 'refund:full-1', status: 'succeeded' };
166
+ });
167
+ console.log(' ALLOW -> exact full refund executed');
168
+
169
+ async function proveRefundBlocked(label, context) {
170
+ try {
171
+ await task.run({ service: 'payments', action: 'refund.create', context }, async () => {
172
+ refunds += 1;
173
+ return { refund_id: 'must-not-exist' };
174
+ });
175
+ throw new Error(`${label} unexpectedly executed`);
176
+ } catch (error) {
177
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
178
+ throw error;
179
+ }
180
+ console.log(` STEP-UP -> ${label} blocked before refund callback`);
181
+ console.log(` ${task.explain(error).summary}`);
182
+ }
183
+ }
184
+
185
+ console.log('5. Prove unrelated payment and amount changes do not inherit the task authority');
186
+ await proveRefundBlocked('unrelated payment', {
187
+ payment_id: 'payment:other',
188
+ amount_minor: amount.value,
189
+ currency: currency.value
190
+ });
191
+ await proveRefundBlocked('over-refund', {
192
+ payment_id: payment.value,
193
+ amount_minor: 15000,
194
+ currency: currency.value
195
+ });
196
+ await proveRefundBlocked('partial refund under the current exact-binding model', {
197
+ payment_id: payment.value,
198
+ amount_minor: 5000,
199
+ currency: currency.value
200
+ });
201
+
202
+ if (refunds !== 1) throw new Error(`expected exactly one refund callback, got ${refunds}`);
203
+
204
+ console.log('6. Complete the task and remove the remaining refund authority');
205
+ task.complete('ticket refund completed');
206
+ try {
207
+ await task.run({
208
+ service: 'payments',
209
+ action: 'refund.create',
210
+ context: { payment_id: payment.value, amount_minor: amount.value, currency: currency.value }
211
+ }, async () => {
212
+ refunds += 1;
213
+ return { refund_id: 'must-not-run-after-completion' };
214
+ });
215
+ throw new Error('post-completion refund unexpectedly executed');
216
+ } catch (error) {
217
+ if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') throw error;
218
+ console.log(' DENY -> completed task cannot refund again');
219
+ }
220
+
221
+ console.log(`Provider-shaped callbacks: tickets=${ticketReads}, orders=${orderReads}, payments=${paymentReads}, refunds=${refunds}`);
222
+ console.log('PASS -> ticket -> order -> payment -> exact refund authority stayed on one evidence-derived lineage');
223
+ console.log('PRODUCT GAP -> partial refunds currently step up because Task Lease bindings are exact equality; derived numeric <= is intentionally not implemented yet');
@@ -0,0 +1,127 @@
1
+ import { AuthorityApprovalRequiredError, AuthorityDeniedError } from '../src/guard.js';
2
+ import { gmailThreadSenderAuthorityExtractor } from '../src/providers/google.js';
3
+ import { createTask } from '../src/task.js';
4
+
5
+ const threadId = 'thread:demo-91';
6
+ const calendarId = 'primary';
7
+
8
+ const task = createTask({
9
+ principal: 'user:demo',
10
+ agent: 'agent:support-demo',
11
+ request: 'Handle the customer request in this email thread and schedule the requested meeting',
12
+ permissions: {
13
+ gmail: {
14
+ allow: ['thread.read'],
15
+ deny: ['email.delete'],
16
+ constraints: { thread_id: [threadId] }
17
+ },
18
+ calendar: {
19
+ allow: ['event.create'],
20
+ deny: ['event.delete'],
21
+ constraints: { calendar_id: [calendarId] }
22
+ }
23
+ },
24
+ authority: {
25
+ originThread: { kind: 'gmail.thread', value: threadId },
26
+ calendar: { kind: 'calendar.id', value: calendarId }
27
+ },
28
+ bindings: [
29
+ { service: 'gmail', action: 'thread.read', field: 'thread_id', authority: 'originThread' },
30
+ { service: 'calendar', action: 'event.create', field: 'calendar_id', authority: 'calendar' }
31
+ ]
32
+ });
33
+
34
+ let gmailReads = 0;
35
+ let calendarMutations = 0;
36
+
37
+ console.log('Task: Handle one customer email and schedule only the meeting justified by that thread');
38
+ console.log('1. Read the exact task-authorized Gmail thread');
39
+ const read = await task.run({
40
+ service: 'gmail',
41
+ action: 'thread.read',
42
+ context: { thread_id: threadId }
43
+ }, async () => {
44
+ gmailReads += 1;
45
+ // Replace with the existing Gmail adapter/SDK call. This shape matches the
46
+ // normalized output produced by createGoogleProviderAdapter().
47
+ return {
48
+ provider: 'gmail',
49
+ sender_email: 'customer@example.com',
50
+ thread_id: threadId,
51
+ message_count: 1
52
+ };
53
+ });
54
+ console.log(` ALLOW -> sender discovered: ${read.output.sender_email}`);
55
+
56
+ const customer = task.authorityFrom(read, {
57
+ name: 'customerEmail',
58
+ kind: 'email.address',
59
+ from: 'originThread',
60
+ extractor: gmailThreadSenderAuthorityExtractor
61
+ });
62
+
63
+ task.bind({
64
+ service: 'calendar',
65
+ action: 'event.create',
66
+ field: 'attendee_email',
67
+ authority: 'customerEmail'
68
+ });
69
+ console.log(`2. Authority follows the guarded Gmail result -> ${customer.value}`);
70
+
71
+ const meetingContext = {
72
+ calendar_id: calendarId,
73
+ attendee_email: customer.value,
74
+ start_time: '2030-01-15T10:00:00Z',
75
+ end_time: '2030-01-15T10:30:00Z'
76
+ };
77
+
78
+ const meeting = await task.run({
79
+ service: 'calendar',
80
+ action: 'event.create',
81
+ context: meetingContext
82
+ }, async () => {
83
+ calendarMutations += 1;
84
+ return { provider: 'calendar', event_id: 'event:customer-demo' };
85
+ });
86
+ console.log(`3. ALLOW -> Calendar event ${meeting.output.event_id} for ${customer.value}`);
87
+
88
+ try {
89
+ await task.run({
90
+ service: 'calendar',
91
+ action: 'event.create',
92
+ context: { ...meetingContext, attendee_email: 'other@example.com' }
93
+ }, async () => {
94
+ calendarMutations += 1;
95
+ return { event_id: 'must-not-exist' };
96
+ });
97
+ throw new Error('unrelated attendee unexpectedly executed');
98
+ } catch (error) {
99
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
100
+ throw error;
101
+ }
102
+ console.log('4. STEP-UP -> unrelated attendee blocked before Calendar mutation');
103
+ console.log(` ${task.explain(error).summary}`);
104
+ }
105
+
106
+ if (calendarMutations !== 1) {
107
+ throw new Error(`expected exactly one Calendar mutation, got ${calendarMutations}`);
108
+ }
109
+
110
+ task.complete('customer request handled');
111
+ try {
112
+ await task.run({
113
+ service: 'calendar',
114
+ action: 'event.create',
115
+ context: meetingContext
116
+ }, async () => {
117
+ calendarMutations += 1;
118
+ return { event_id: 'must-not-run-after-completion' };
119
+ });
120
+ throw new Error('post-completion Calendar mutation unexpectedly executed');
121
+ } catch (error) {
122
+ if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') throw error;
123
+ console.log('5. DENY -> task completion removed the meeting authority');
124
+ }
125
+
126
+ console.log(`Provider-shaped callbacks: gmail_reads=${gmailReads}, calendar_mutations=${calendarMutations}`);
127
+ console.log('PASS -> one authorized email established exactly one downstream Calendar attendee');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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",
@@ -39,11 +39,12 @@
39
39
  "demo:live-google": "node examples/live-google-cross-provider.js",
40
40
  "demo:mcp-upstream": "node examples/validation-mcp-upstream.js",
41
41
  "demo:guard": "node examples/direct-guard.js",
42
+ "demo:connected-github": "node examples/quickstart-github-connected.mjs",
42
43
  "benchmark:task": "node benchmarks/task-utility.mjs",
43
44
  "test": "node --test test/*.test.js",
44
45
  "test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
45
46
  "test:coverage": "node --experimental-test-coverage --test test/*.test.js",
46
- "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/task.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-first-github.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 && node --check benchmarks/task-utility.mjs",
47
+ "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/task.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-first-github.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 && node --check examples/quickstart-github-connected.mjs && node --check benchmarks/task-utility.mjs",
47
48
  "check:package": "npm pack --dry-run",
48
49
  "check": "npm run check:syntax && npm test && npm run demo:task && npm run benchmark:task && npm run demo:task-lease && npm run check:package"
49
50
  },
@@ -73,6 +74,7 @@
73
74
  "./mcp-gateway": "./src/mcp-gateway.js",
74
75
  "./mcp-remote": "./src/mcp-remote.js",
75
76
  "./mcp-server": "./src/mcp-server.js",
77
+ "./runtime-env": "./src/runtime-env.js",
76
78
  "./sdk": "./src/sdk.js",
77
79
  "./storage": "./src/storage.js",
78
80
  "./task": "./src/task.js",
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import { createAgentToken, parseTtl } from './agent-auth.js';
6
6
  import { createRuntimeEnvironment } from './runtime-env.js';
7
7
  import { authorityHome, ensureAuthorityHome, loadConfig, saveConfig } from './storage.js';
8
8
 
9
- const VERSION = '0.3.0';
9
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
10
10
 
11
11
  function fail(message, code = 1) {
12
12
  console.error(`error: ${message}`);
@@ -144,7 +144,7 @@ async function connectionsCommand(args) {
144
144
  }
145
145
 
146
146
  async function connectGitHub(args) {
147
- if (!has(args, '--token-stdin')) throw new Error('GitHub currently requires --token-stdin; browser OAuth/PKCE is the next provider-onboarding milestone');
147
+ if (!has(args, '--token-stdin')) throw new Error('GitHub local onboarding currently requires --token-stdin');
148
148
  const home = homeFrom(args);
149
149
  const env = createRuntimeEnvironment({ home });
150
150
  const token = await readStdinSecret();
@@ -184,7 +184,7 @@ async function disconnectGitHub(args) {
184
184
  const accountId = flag(args, '--account', 'default');
185
185
  const result = env.broker.disconnect({ principal_id: env.config.principal_id, service: 'github', account_id: accountId });
186
186
  if (!result) throw new Error(`GitHub account ${accountId} is not connected`);
187
- console.log(`✓ GitHub ${accountId} disconnected and local credential removed`);
187
+ console.log(`✓ GitHub ${result.account_id} disconnected and local credential removed`);
188
188
  }
189
189
 
190
190
  async function agentCommand(args) {
@@ -116,8 +116,19 @@ export class CredentialBroker {
116
116
  return connection;
117
117
  }
118
118
 
119
+ resolveConnection({ principal_id, service, account_id = 'default' }) {
120
+ const exact = this.connections.get({ principal_id, service, account_id });
121
+ if (exact) return exact;
122
+ if (account_id !== 'default') return null;
123
+
124
+ const candidates = this.connections.list(principal_id)
125
+ .filter((connection) => connection.service === service && connection.status === 'active');
126
+
127
+ return candidates.length === 1 ? candidates[0] : null;
128
+ }
129
+
119
130
  getConnection({ principal_id, service, account_id = 'default' }) {
120
- return this.connections.get({ principal_id, service, account_id });
131
+ return this.resolveConnection({ principal_id, service, account_id });
121
132
  }
122
133
 
123
134
  listConnections(principal_id) {
@@ -125,7 +136,7 @@ export class CredentialBroker {
125
136
  }
126
137
 
127
138
  resolveInternal({ principal_id, service, account_id = 'default' }) {
128
- const connection = this.connections.get({ principal_id, service, account_id });
139
+ const connection = this.resolveConnection({ principal_id, service, account_id });
129
140
  if (!connection || connection.status !== 'active') {
130
141
  const error = new Error(`no active ${service} connection for this principal`);
131
142
  error.code = 'connection_required';
@@ -139,10 +150,14 @@ export class CredentialBroker {
139
150
  }
140
151
 
141
152
  disconnect({ principal_id, service, account_id = 'default' }) {
142
- const current = this.connections.get({ principal_id, service, account_id });
153
+ const current = this.resolveConnection({ principal_id, service, account_id });
143
154
  if (!current) return null;
144
155
  this.secrets.delete(current.credential_ref);
145
- const connection = this.connections.disconnect({ principal_id, service, account_id });
156
+ const connection = this.connections.disconnect({
157
+ principal_id,
158
+ service,
159
+ account_id: current.account_id
160
+ });
146
161
  if (!connection) return null;
147
162
  const { credential_ref, ...safe } = connection;
148
163
  return safe;
package/src/task.js CHANGED
@@ -2,7 +2,11 @@ import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import { AuthorityRuntime } from './index.js';
4
4
  import { createTaskLease } from './task-lease.js';
5
- import { createTaskLeaseGuard } from './guard.js';
5
+ import {
6
+ AuthorityApprovalRequiredError,
7
+ AuthorityDeniedError,
8
+ createTaskLeaseGuard
9
+ } from './guard.js';
6
10
  import { createDurableTaskLeaseSession } from './durable-task-lease.js';
7
11
 
8
12
  function requiredString(value, label) {
@@ -113,13 +117,32 @@ function resultFrom(value) {
113
117
  return null;
114
118
  }
115
119
 
120
+ function assertAllowedExecution(execution) {
121
+ const decision = execution?.result?.decision;
122
+ if (decision === 'deny') throw new AuthorityDeniedError(execution);
123
+ if (decision === 'require_approval') throw new AuthorityApprovalRequiredError(execution);
124
+ if (decision !== 'allow') {
125
+ throw new AuthorityDeniedError({
126
+ ...execution,
127
+ result: {
128
+ ...(execution?.result || {}),
129
+ decision: 'deny',
130
+ code: 'unknown_decision',
131
+ reason: 'authority returned an unsupported decision'
132
+ }
133
+ });
134
+ }
135
+ return execution;
136
+ }
137
+
116
138
  /**
117
139
  * Product-facing task authority facade.
118
140
  *
119
141
  * It intentionally does not replace Mission/TaskLease. It composes those
120
142
  * primitives into the small surface most agent developers need: run an effect,
121
- * derive named authority from guarded output, bind that authority to later
122
- * effects, explain step-up decisions, and complete the task.
143
+ * execute through a connected provider, derive named authority from guarded
144
+ * output, bind that authority to later effects, explain step-up decisions, and
145
+ * complete the task.
123
146
  */
124
147
  export class AgentTask {
125
148
  constructor({ lease, runtime = new AuthorityRuntime() } = {}) {
@@ -134,13 +157,31 @@ export class AgentTask {
134
157
  get status() { return this._lease.status; }
135
158
  get mission() { return structuredClone(this._lease.mission); }
136
159
 
160
+ /**
161
+ * Guard an application-owned effect callback.
162
+ */
137
163
  run(request, effect) {
138
164
  return this.guard.run(request, effect);
139
165
  }
140
166
 
167
+ /**
168
+ * Execute through an Agent Authority connected-provider runtime.
169
+ *
170
+ * Credentials remain inside the runtime/broker. The caller receives only the
171
+ * sanitized provider output, ALLOW receipt and execution evidence. Deny and
172
+ * step-up decisions use the same public error classes as run().
173
+ */
174
+ async execute(request) {
175
+ if (typeof this.runtime.executeTaskLease !== 'function') {
176
+ throw new Error('task runtime does not support connected provider execution');
177
+ }
178
+ const execution = await this.runtime.executeTaskLease(this._lease, request);
179
+ return assertAllowedExecution(execution);
180
+ }
181
+
141
182
  authorityFrom(execution, { name, fact_id, kind = 'opaque', from = [], extractor } = {}) {
142
183
  if (!execution?.receipt || !execution?.evidence || !Object.hasOwn(execution, 'output')) {
143
- throw new Error('authorityFrom() requires the result returned by task.run()');
184
+ throw new Error('authorityFrom() requires the result returned by task.run() or task.execute()');
144
185
  }
145
186
  const parents = Array.isArray(from) ? from : [from];
146
187
  return this._lease.deriveFromEvidence({