@nullsquare/agent-authority 0.4.6 → 0.4.8

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,84 @@
1
+ import { createTask } from '@nullsquare/agent-authority/task';
2
+ import { AuthorityApprovalRequiredError } from '@nullsquare/agent-authority/guard';
3
+
4
+ const allowedRepository = process.argv[2] || 'Null-Square/agent-authority';
5
+ const blockedRepository = process.argv[3] || 'octocat/Hello-World';
6
+
7
+ const task = createTask({
8
+ principal: 'user:quickstart',
9
+ agent: 'agent:quickstart',
10
+ request: `Inspect only ${allowedRepository}`,
11
+ permissions: {
12
+ github: {
13
+ // Model the standing account/app capability as broader than this task.
14
+ // The Task authority root below narrows repo.read to one repository.
15
+ allow: ['repo.read'],
16
+ deny: ['repo.write', 'repo.delete'],
17
+ constraints: {}
18
+ }
19
+ },
20
+ authority: {
21
+ repository: { kind: 'github.repository', value: allowedRepository }
22
+ },
23
+ bindings: [
24
+ { service: 'github', action: 'repo.read', field: 'repository', authority: 'repository' }
25
+ ]
26
+ });
27
+
28
+ let outboundCalls = 0;
29
+
30
+ async function readRepository(repository) {
31
+ return task.run({
32
+ service: 'github',
33
+ action: 'repo.read',
34
+ context: { repository }
35
+ }, async () => {
36
+ outboundCalls += 1;
37
+
38
+ const headers = {
39
+ accept: 'application/vnd.github+json',
40
+ 'user-agent': 'agent-authority-live-quickstart',
41
+ 'x-github-api-version': '2022-11-28'
42
+ };
43
+ if (process.env.GITHUB_TOKEN) {
44
+ headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
45
+ }
46
+
47
+ const response = await fetch(`https://api.github.com/repos/${repository}`, { headers });
48
+ if (!response.ok) {
49
+ throw new Error(`GitHub returned ${response.status} for ${repository}`);
50
+ }
51
+
52
+ const body = await response.json();
53
+ return {
54
+ full_name: body.full_name,
55
+ private: body.private,
56
+ html_url: body.html_url
57
+ };
58
+ });
59
+ }
60
+
61
+ console.log(`Standing GitHub permission -> repo.read`);
62
+ console.log(`Task authority -> ${allowedRepository}`);
63
+ console.log(`GitHub mode -> ${process.env.GITHUB_TOKEN ? 'authenticated token' : 'public API; no credential required'}`);
64
+
65
+ const allowed = await readRepository(allowedRepository);
66
+ console.log(`ALLOW -> real GitHub returned ${allowed.output.full_name}`);
67
+
68
+ try {
69
+ await readRepository(blockedRepository);
70
+ throw new Error('unrelated repository unexpectedly reached GitHub');
71
+ } catch (error) {
72
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
73
+ throw error;
74
+ }
75
+
76
+ console.log(`STEP-UP -> ${task.explain(error).summary}`);
77
+ }
78
+
79
+ if (outboundCalls !== 1) {
80
+ throw new Error(`expected exactly one outbound GitHub call, observed ${outboundCalls}`);
81
+ }
82
+
83
+ task.complete('live GitHub quickstart complete');
84
+ console.log('PASS -> broader standing repo.read permission could not reach an unrelated repository for this task');
@@ -0,0 +1,101 @@
1
+ import { createTask } from '@nullsquare/agent-authority/task';
2
+ import { AuthorityApprovalRequiredError } from '@nullsquare/agent-authority/guard';
3
+ import { githubIssueListSelectedNumberAuthorityExtractor } from '@nullsquare/agent-authority/providers/github';
4
+
5
+ const repository = 'acme/app';
6
+ const marker = 'quickstart-selected-issue';
7
+
8
+ const task = createTask({
9
+ principal: 'user:quickstart',
10
+ agent: 'agent:quickstart',
11
+ request: 'Handle the issue selected for this task and comment only on that issue',
12
+ permissions: {
13
+ github: {
14
+ allow: ['issue.list', 'issue.comment'],
15
+ deny: ['repo.delete'],
16
+ constraints: { repository: [repository] }
17
+ }
18
+ },
19
+ authority: {
20
+ repository: { kind: 'github.repository', value: repository },
21
+ marker: { kind: 'github.issue.marker', value: marker }
22
+ },
23
+ bindings: [
24
+ { service: 'github', action: 'issue.list', field: 'repository', authority: 'repository' },
25
+ { service: 'github', action: 'issue.list', field: 'fixture_marker', authority: 'marker' },
26
+ { service: 'github', action: 'issue.comment', field: 'repository', authority: 'repository' }
27
+ ]
28
+ });
29
+
30
+ let effects = 0;
31
+
32
+ const discovery = await task.run({
33
+ service: 'github',
34
+ action: 'issue.list',
35
+ context: { repository, fixture_marker: marker }
36
+ }, async () => {
37
+ effects += 1;
38
+
39
+ // This is provider-shaped fixture output so the quickstart needs no account.
40
+ // In a real app, replace only this callback with the SDK/provider call you already use.
41
+ return {
42
+ provider: 'github',
43
+ status: 200,
44
+ ok: true,
45
+ selected_issue_number: 42,
46
+ selected_issue_title: 'Quickstart issue',
47
+ selected_issue_match_count: 1,
48
+ selected_issue_marker: marker
49
+ };
50
+ });
51
+
52
+ const issue = task.authorityFrom(discovery, {
53
+ name: 'issue',
54
+ kind: 'github.issue.number',
55
+ from: ['repository', 'marker'],
56
+ extractor: githubIssueListSelectedNumberAuthorityExtractor
57
+ });
58
+
59
+ task.bind({
60
+ service: 'github',
61
+ action: 'issue.comment',
62
+ field: 'issue_number',
63
+ authority: 'issue'
64
+ });
65
+
66
+ await task.run({
67
+ service: 'github',
68
+ action: 'issue.comment',
69
+ context: { repository, issue_number: issue.value, body: 'Handled.' }
70
+ }, async () => {
71
+ effects += 1;
72
+ return { comment_id: 1001 };
73
+ });
74
+
75
+ console.log(`ALLOW -> task discovered issue #${issue.value} and the exact comment effect ran`);
76
+
77
+ try {
78
+ await task.run({
79
+ service: 'github',
80
+ action: 'issue.comment',
81
+ context: { repository, issue_number: 7, body: 'This must not run.' }
82
+ }, async () => {
83
+ effects += 1;
84
+ return { comment_id: 1002 };
85
+ });
86
+
87
+ throw new Error('unrelated issue unexpectedly executed');
88
+ } catch (error) {
89
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
90
+ throw error;
91
+ }
92
+
93
+ console.log(`STEP-UP -> ${task.explain(error).summary}`);
94
+ }
95
+
96
+ if (effects !== 2) {
97
+ throw new Error(`expected exactly 2 provider-shaped effects, observed ${effects}`);
98
+ }
99
+
100
+ task.complete('quickstart complete');
101
+ console.log('PASS -> useful task work ran; unrelated standing permission did not become task authority');
@@ -0,0 +1,238 @@
1
+ import { AuthorityApprovalRequiredError, AuthorityDeniedError } from '../src/guard.js';
2
+ import {
3
+ githubGitRefShaAuthorityExtractor,
4
+ githubIssueListSelectedNumberAuthorityExtractor,
5
+ githubPullRequestCreateNumberAuthorityExtractor
6
+ } from '../src/providers/github.js';
7
+ import {
8
+ githubContentsWritePathAuthorityExtractor,
9
+ githubGitRefCreateBranchAuthorityExtractor
10
+ } from '../src/providers/github-coding.js';
11
+ import { createTask } from '../src/task.js';
12
+
13
+ const repository = 'acme/app';
14
+ const marker = 'coding-fixture-42';
15
+ const baseBranch = 'main';
16
+ const plannedBranch = 'agent/issue-42';
17
+ const targetPath = 'src/auth.js';
18
+ const baseSha = 'a'.repeat(40);
19
+
20
+ const task = createTask({
21
+ principal: 'user:demo',
22
+ agent: 'agent:coder',
23
+ request: 'Fix the selected issue on one task branch, change only src/auth.js, and open a draft PR. Do not merge or deploy.',
24
+ permissions: {
25
+ github: {
26
+ allow: ['issue.list', 'git.ref.read', 'git.ref.create', 'repo.contents.write', 'pull_request.create'],
27
+ deny: ['pull_request.merge', 'repo.delete'],
28
+ constraints: {}
29
+ }
30
+ },
31
+ authority: {
32
+ repository: { kind: 'github.repository', value: repository },
33
+ fixture_marker: { kind: 'github.issue.marker', value: marker },
34
+ base_branch: { kind: 'github.git.branch', value: baseBranch },
35
+ planned_branch: { kind: 'github.git.branch.intent', value: plannedBranch },
36
+ target_path: { kind: 'github.repository.path.intent', value: targetPath }
37
+ },
38
+ bindings: [
39
+ { service: 'github', action: 'issue.list', field: 'repository', authority: 'repository' },
40
+ { service: 'github', action: 'issue.list', field: 'fixture_marker', authority: 'fixture_marker' },
41
+ { service: 'github', action: 'git.ref.read', field: 'repository', authority: 'repository' },
42
+ { service: 'github', action: 'git.ref.read', field: 'branch', authority: 'base_branch' },
43
+ { service: 'github', action: 'git.ref.create', field: 'repository', authority: 'repository' },
44
+ { service: 'github', action: 'git.ref.create', field: 'branch', authority: 'planned_branch' },
45
+ { service: 'github', action: 'git.ref.create', field: 'sha', authority: 'base_sha' },
46
+ { service: 'github', action: 'git.ref.create', field: 'issue_number', authority: 'issue' },
47
+ { service: 'github', action: 'repo.contents.write', field: 'repository', authority: 'repository' },
48
+ { service: 'github', action: 'repo.contents.write', field: 'branch', authority: 'task_branch' },
49
+ { service: 'github', action: 'repo.contents.write', field: 'path', authority: 'target_path' },
50
+ { service: 'github', action: 'repo.contents.write', field: 'issue_number', authority: 'issue' },
51
+ { service: 'github', action: 'pull_request.create', field: 'repository', authority: 'repository' },
52
+ { service: 'github', action: 'pull_request.create', field: 'head', authority: 'task_branch' },
53
+ { service: 'github', action: 'pull_request.create', field: 'base', authority: 'base_branch' },
54
+ { service: 'github', action: 'pull_request.create', field: 'issue_number', authority: 'issue' },
55
+ { service: 'github', action: 'pull_request.create', field: 'changed_path', authority: 'changed_file' }
56
+ ]
57
+ });
58
+
59
+ let reads = 0;
60
+ let mutations = 0;
61
+
62
+ const discovery = await task.run({
63
+ service: 'github',
64
+ action: 'issue.list',
65
+ context: { repository, fixture_marker: marker, state: 'open' }
66
+ }, async () => {
67
+ reads += 1;
68
+ return {
69
+ provider: 'github',
70
+ selected_issue_number: 42,
71
+ selected_issue_title: 'Fix auth edge case',
72
+ selected_issue_match_count: 1,
73
+ selected_issue_marker: marker
74
+ };
75
+ });
76
+ const issue = task.authorityFrom(discovery, {
77
+ name: 'issue',
78
+ kind: 'github.issue.number',
79
+ from: ['repository', 'fixture_marker'],
80
+ extractor: githubIssueListSelectedNumberAuthorityExtractor
81
+ });
82
+ console.log(`DISCOVER -> issue #${issue.value} became task authority`);
83
+
84
+ const base = await task.run({
85
+ service: 'github',
86
+ action: 'git.ref.read',
87
+ context: { repository, branch: baseBranch }
88
+ }, async () => {
89
+ reads += 1;
90
+ return { provider: 'github', branch: baseBranch, ref: `refs/heads/${baseBranch}`, sha: baseSha };
91
+ });
92
+ const baseAuthority = task.authorityFrom(base, {
93
+ name: 'base_sha',
94
+ kind: 'github.git.sha',
95
+ from: ['repository', 'base_branch'],
96
+ extractor: githubGitRefShaAuthorityExtractor
97
+ });
98
+
99
+ const branch = await task.run({
100
+ service: 'github',
101
+ action: 'git.ref.create',
102
+ context: { repository, branch: plannedBranch, sha: baseAuthority.value, issue_number: issue.value }
103
+ }, async () => {
104
+ mutations += 1;
105
+ return { provider: 'github', branch: plannedBranch, ref: `refs/heads/${plannedBranch}`, sha: baseSha };
106
+ });
107
+ const taskBranch = task.authorityFrom(branch, {
108
+ name: 'task_branch',
109
+ kind: 'github.git.branch',
110
+ from: ['planned_branch', 'issue', 'base_sha'],
111
+ extractor: githubGitRefCreateBranchAuthorityExtractor
112
+ });
113
+ console.log(`ALLOW -> created only task branch ${taskBranch.value}`);
114
+
115
+ async function expectStepUp(request, label) {
116
+ const before = mutations;
117
+ try {
118
+ await task.run(request, async () => { mutations += 1; });
119
+ throw new Error(`${label} unexpectedly executed`);
120
+ } catch (error) {
121
+ if (!(error instanceof AuthorityApprovalRequiredError)) throw error;
122
+ if (mutations !== before) throw new Error(`${label} reached the provider callback`);
123
+ console.log(`STEP-UP -> ${label}: ${task.explain(error).summary}`);
124
+ }
125
+ }
126
+
127
+ await expectStepUp({
128
+ service: 'github',
129
+ action: 'repo.contents.write',
130
+ context: {
131
+ repository,
132
+ branch: baseBranch,
133
+ path: targetPath,
134
+ issue_number: issue.value,
135
+ message: 'wrong branch',
136
+ content_base64: 'd3Jvbmc='
137
+ }
138
+ }, 'write directly to main');
139
+
140
+ await expectStepUp({
141
+ service: 'github',
142
+ action: 'repo.contents.write',
143
+ context: {
144
+ repository,
145
+ branch: taskBranch.value,
146
+ path: 'src/admin.js',
147
+ issue_number: issue.value,
148
+ message: 'wrong file',
149
+ content_base64: 'd3Jvbmc='
150
+ }
151
+ }, 'change an unrelated file');
152
+
153
+ const write = await task.run({
154
+ service: 'github',
155
+ action: 'repo.contents.write',
156
+ context: {
157
+ repository,
158
+ branch: taskBranch.value,
159
+ path: targetPath,
160
+ issue_number: issue.value,
161
+ message: 'Fix issue #42',
162
+ content_base64: Buffer.from('export const fixed = true;\n').toString('base64')
163
+ }
164
+ }, async () => {
165
+ mutations += 1;
166
+ return {
167
+ provider: 'github',
168
+ body: { content: { path: targetPath, sha: 'b'.repeat(40) }, commit: { sha: 'c'.repeat(40) } }
169
+ };
170
+ });
171
+ const changedFile = task.authorityFrom(write, {
172
+ name: 'changed_file',
173
+ kind: 'github.repository.path',
174
+ from: ['task_branch', 'issue', 'target_path'],
175
+ extractor: githubContentsWritePathAuthorityExtractor
176
+ });
177
+ console.log(`ALLOW -> changed only ${changedFile.value} on ${taskBranch.value}`);
178
+
179
+ await expectStepUp({
180
+ service: 'github',
181
+ action: 'pull_request.create',
182
+ context: {
183
+ repository,
184
+ head: 'agent/unrelated',
185
+ base: baseBranch,
186
+ issue_number: issue.value,
187
+ changed_path: changedFile.value,
188
+ title: 'Wrong PR head',
189
+ draft: true
190
+ }
191
+ }, 'open PR from another branch');
192
+
193
+ const pr = await task.run({
194
+ service: 'github',
195
+ action: 'pull_request.create',
196
+ context: {
197
+ repository,
198
+ head: taskBranch.value,
199
+ base: baseBranch,
200
+ issue_number: issue.value,
201
+ changed_path: changedFile.value,
202
+ title: 'Fix issue #42',
203
+ draft: true
204
+ }
205
+ }, async () => {
206
+ mutations += 1;
207
+ return {
208
+ provider: 'github',
209
+ pull_request_number: 77,
210
+ head: taskBranch.value,
211
+ base: baseBranch,
212
+ draft: true
213
+ };
214
+ });
215
+ const pullRequest = task.authorityFrom(pr, {
216
+ name: 'pull_request',
217
+ kind: 'github.pull_request.number',
218
+ from: ['issue', 'task_branch', 'changed_file'],
219
+ extractor: githubPullRequestCreateNumberAuthorityExtractor
220
+ });
221
+ console.log(`ALLOW -> opened draft PR #${pullRequest.value}`);
222
+
223
+ const beforeMerge = mutations;
224
+ try {
225
+ await task.run({
226
+ service: 'github',
227
+ action: 'pull_request.merge',
228
+ context: { repository, pull_request_number: pullRequest.value }
229
+ }, async () => { mutations += 1; });
230
+ throw new Error('merge unexpectedly executed');
231
+ } catch (error) {
232
+ if (!(error instanceof AuthorityDeniedError)) throw error;
233
+ if (mutations !== beforeMerge) throw new Error('merge reached provider callback');
234
+ console.log('DENY -> merge remains outside task authority');
235
+ }
236
+
237
+ task.complete('draft PR opened');
238
+ console.log(`PASS -> reads=${reads}, authorized mutations=${mutations}; unrelated writes/PRs and merge executed zero callbacks`);
@@ -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');