@nullsquare/agent-authority 0.4.5 → 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.
- package/README.md +296 -295
- package/ROADMAP.md +145 -105
- package/benchmarks/task-utility.mjs +130 -0
- package/docs/connected-execution-api.md +60 -0
- package/docs/connected-github.md +140 -0
- package/docs/npm-release.md +30 -10
- package/docs/product-proof.md +311 -0
- package/docs/quickstart.md +147 -0
- package/docs/release-v0.4.7.md +29 -0
- package/examples/live-github-derived-mutation.js +47 -54
- package/examples/quickstart-github-connected.mjs +77 -0
- package/examples/quickstart-github-live.mjs +84 -0
- package/examples/quickstart.mjs +101 -0
- package/examples/task-first-finance.js +223 -0
- package/examples/task-first-github.js +102 -0
- package/examples/task-first-support.js +127 -0
- package/package.json +9 -4
- package/src/cli.js +3 -3
- package/src/connections.js +19 -4
- package/src/task.js +279 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# v0.4.7 connected-execution candidate
|
|
2
|
+
|
|
3
|
+
This candidate closes a concrete adoption gap between the task-first API and the existing credential broker/provider runtime.
|
|
4
|
+
|
|
5
|
+
## Candidate changes
|
|
6
|
+
|
|
7
|
+
- `task.execute(request)` executes through an `ExecutingAuthorityRuntime` while preserving the current Task Lease as the narrowest authority object.
|
|
8
|
+
- connected deny/step-up outcomes use the same public error classes as `task.run()`.
|
|
9
|
+
- successful connected execution returns sanitized provider output, receipt and execution evidence that can feed `task.authorityFrom()`.
|
|
10
|
+
- `@nullsquare/agent-authority/runtime-env` exposes the existing local encrypted runtime composition for developer onboarding.
|
|
11
|
+
- a sole active provider account can be resolved when requests omit `account_id`; multiple active accounts remain ambiguous and fail closed.
|
|
12
|
+
- default disconnect can remove that sole connection without requiring the caller to know an auto-detected provider account ID.
|
|
13
|
+
- the connected GitHub quickstart proves the local encrypted vault + credential broker + live provider path without copying credentials into task/model context.
|
|
14
|
+
|
|
15
|
+
## Security boundary
|
|
16
|
+
|
|
17
|
+
This does not create a GitHub token, OAuth flow, GitHub App, KMS, or new identity format. Provider-side least privilege still comes from GitHub. Agent Authority adds a task boundary underneath that connected account authority.
|
|
18
|
+
|
|
19
|
+
The local encrypted vault remains a trusted-local-host developer reference backend.
|
|
20
|
+
|
|
21
|
+
## Release gate
|
|
22
|
+
|
|
23
|
+
Before version publication:
|
|
24
|
+
|
|
25
|
+
- unit/adversarial tests must prove credential isolation and zero provider execution on task authority delta;
|
|
26
|
+
- packed consumer must import and execute the new public surfaces;
|
|
27
|
+
- live connected GitHub workflow must pass through the encrypted local runtime;
|
|
28
|
+
- Node 20/22, coverage, AI SDK, existing live GitHub proofs and CodeQL must remain green;
|
|
29
|
+
- registry verification must be performed after npm publication before the public release marker is updated.
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { CredentialBroker } from '../src/connections.js';
|
|
2
|
-
import { AuthorityRuntime } from '../src/index.js';
|
|
3
2
|
import {
|
|
4
3
|
AuthorityApprovalRequiredError,
|
|
5
|
-
AuthorityDeniedError
|
|
6
|
-
createTaskLeaseGuard
|
|
4
|
+
AuthorityDeniedError
|
|
7
5
|
} from '../src/guard.js';
|
|
8
6
|
import { createGitHubProviderAdapter } from '../src/providers/github.js';
|
|
9
|
-
import {
|
|
7
|
+
import { createTask } from '../src/task.js';
|
|
10
8
|
|
|
11
9
|
const repository = process.env.AA_VALIDATION_REPOSITORY || 'Null-Square/agent-authority';
|
|
12
10
|
const marker = process.env.AA_VALIDATION_MARKER || 'agent-authority-live-fixture-v1';
|
|
@@ -19,69 +17,55 @@ if (!token) {
|
|
|
19
17
|
const [owner, repo] = repository.split('/');
|
|
20
18
|
if (!owner || !repo) throw new Error(`invalid repository: ${repository}`);
|
|
21
19
|
|
|
22
|
-
const
|
|
23
|
-
version: '0.1',
|
|
20
|
+
const task = createTask({
|
|
24
21
|
mission_id: 'mission:live-derived-github-mutation',
|
|
25
|
-
principal:
|
|
26
|
-
agent:
|
|
22
|
+
principal: 'user:validation',
|
|
23
|
+
agent: 'agent:github-actions-validation',
|
|
24
|
+
request: 'Find the Agent Authority live validation fixture and leave one validation comment',
|
|
27
25
|
objective: 'Discover the validation issue and comment only on that issue',
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
service: 'github',
|
|
26
|
+
permissions: {
|
|
27
|
+
github: {
|
|
31
28
|
allow: ['issue.list', 'issue.comment'],
|
|
32
29
|
deny: ['issue.close', 'issue.delete', 'repo.write', 'repo.delete'],
|
|
33
30
|
constraints: { repository: [repository] }
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
constraints: { expires_at: '2099-01-01T00:00:00Z' }
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const lease = createTaskLease({
|
|
40
|
-
mission,
|
|
41
|
-
request: 'Find the Agent Authority live validation fixture and leave one validation comment',
|
|
42
|
-
roots: [
|
|
43
|
-
{
|
|
44
|
-
fact_id: 'fact:repository',
|
|
32
|
+
},
|
|
33
|
+
constraints: { expires_at: '2099-01-01T00:00:00Z' },
|
|
34
|
+
authority: {
|
|
35
|
+
repository: {
|
|
45
36
|
kind: 'github.repository',
|
|
46
37
|
value: repository,
|
|
47
38
|
source: 'validation-task'
|
|
48
39
|
},
|
|
49
|
-
{
|
|
50
|
-
fact_id: 'fact:fixture-marker',
|
|
40
|
+
fixtureMarker: {
|
|
51
41
|
kind: 'github.issue.marker',
|
|
52
42
|
value: marker,
|
|
53
43
|
source: 'validation-task'
|
|
54
44
|
}
|
|
55
|
-
|
|
45
|
+
},
|
|
56
46
|
bindings: [
|
|
57
47
|
{
|
|
58
48
|
service: 'github',
|
|
59
49
|
action: 'issue.list',
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
field: 'repository',
|
|
51
|
+
authority: 'repository'
|
|
62
52
|
},
|
|
63
53
|
{
|
|
64
54
|
service: 'github',
|
|
65
55
|
action: 'issue.list',
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
field: 'fixture_marker',
|
|
57
|
+
authority: 'fixtureMarker'
|
|
68
58
|
},
|
|
69
59
|
{
|
|
70
60
|
service: 'github',
|
|
71
61
|
action: 'issue.comment',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
service: 'github',
|
|
77
|
-
action: 'issue.comment',
|
|
78
|
-
context_field: 'issue_number',
|
|
79
|
-
fact_id: 'fact:discovered-issue-number'
|
|
62
|
+
field: 'repository',
|
|
63
|
+
authority: 'repository'
|
|
80
64
|
}
|
|
81
65
|
]
|
|
82
66
|
});
|
|
83
67
|
|
|
84
|
-
const
|
|
68
|
+
const mission = task.mission;
|
|
85
69
|
const broker = new CredentialBroker();
|
|
86
70
|
broker.connect({
|
|
87
71
|
principal_id: mission.principal.id,
|
|
@@ -95,7 +79,7 @@ const adapter = createGitHubProviderAdapter({ broker });
|
|
|
95
79
|
const cleanupHeaders = {
|
|
96
80
|
accept: 'application/vnd.github+json',
|
|
97
81
|
authorization: `Bearer ${token}`,
|
|
98
|
-
'user-agent': 'agent-authority-derived-mutation-validation-cleanup',
|
|
82
|
+
'user-agent': 'agent-authority-task-first-derived-mutation-validation-cleanup',
|
|
99
83
|
'x-github-api-version': '2022-11-28'
|
|
100
84
|
};
|
|
101
85
|
|
|
@@ -119,7 +103,7 @@ function discoveryRequest() {
|
|
|
119
103
|
|
|
120
104
|
async function discoverFixtureIssue() {
|
|
121
105
|
const request = discoveryRequest();
|
|
122
|
-
return
|
|
106
|
+
return task.run(request, async () => {
|
|
123
107
|
providerReadCalls += 1;
|
|
124
108
|
return adapter.execute({ mission, request });
|
|
125
109
|
});
|
|
@@ -132,7 +116,7 @@ async function commentOnIssue(issueNumber, body) {
|
|
|
132
116
|
context: { repository, issue_number: issueNumber, body }
|
|
133
117
|
};
|
|
134
118
|
|
|
135
|
-
return
|
|
119
|
+
return task.run(request, async () => {
|
|
136
120
|
providerMutationCalls += 1;
|
|
137
121
|
return adapter.execute({ mission, request });
|
|
138
122
|
});
|
|
@@ -151,9 +135,10 @@ async function cleanupComment(commentId) {
|
|
|
151
135
|
}
|
|
152
136
|
|
|
153
137
|
try {
|
|
154
|
-
console.log(
|
|
155
|
-
console.log(`Task root
|
|
156
|
-
console.log(
|
|
138
|
+
console.log('Task-first live workflow: discover one issue and comment only on that issue');
|
|
139
|
+
console.log(`Task root repository: ${task.authority('repository').value}`);
|
|
140
|
+
console.log(`Task root fixture marker: ${task.authority('fixtureMarker').value}`);
|
|
141
|
+
console.log('1. Discover fixture through task.run() and the reviewed GitHub provider adapter');
|
|
157
142
|
const discovered = await discoverFixtureIssue();
|
|
158
143
|
|
|
159
144
|
if (discovered.output.selected_issue_match_count !== 1) {
|
|
@@ -164,18 +149,21 @@ try {
|
|
|
164
149
|
const extractor = adapter.authorityExtractor(discoveryRequest(), 'github.issue.number');
|
|
165
150
|
if (!extractor) throw new Error('GitHub provider did not advertise the issue-number authority extractor');
|
|
166
151
|
|
|
167
|
-
const issueFact =
|
|
168
|
-
|
|
152
|
+
const issueFact = task.authorityFrom(discovered, {
|
|
153
|
+
name: 'discoveredIssue',
|
|
169
154
|
kind: 'github.issue.number',
|
|
170
|
-
from: ['
|
|
171
|
-
receipt: discovered.receipt,
|
|
172
|
-
evidence: discovered.evidence,
|
|
173
|
-
output: discovered.output,
|
|
155
|
+
from: ['repository', 'fixtureMarker'],
|
|
174
156
|
extractor
|
|
175
157
|
});
|
|
176
|
-
|
|
158
|
+
task.bind({
|
|
159
|
+
service: 'github',
|
|
160
|
+
action: 'issue.comment',
|
|
161
|
+
field: 'issue_number',
|
|
162
|
+
authority: 'discoveredIssue'
|
|
163
|
+
});
|
|
164
|
+
console.log(`2. task.authorityFrom() -> issue #${issueFact.value}`);
|
|
177
165
|
|
|
178
|
-
const validationBody = `Agent Authority live
|
|
166
|
+
const validationBody = `Agent Authority live task-first authorization validation (${new Date().toISOString()}). Temporary comment; CI removes it after the proof.`;
|
|
179
167
|
const allowedMutation = await commentOnIssue(issueFact.value, validationBody);
|
|
180
168
|
createdCommentId = allowedMutation.output.comment_id;
|
|
181
169
|
console.log(`3. ALLOW -> real GitHub comment mutation executed (comment ${createdCommentId})`);
|
|
@@ -188,14 +176,19 @@ try {
|
|
|
188
176
|
if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
|
|
189
177
|
throw error;
|
|
190
178
|
}
|
|
179
|
+
const explanation = task.explain(error);
|
|
191
180
|
console.log(`4. STEP-UP -> unrelated issue #${unrelatedIssue} blocked before provider mutation`);
|
|
181
|
+
console.log(` ${explanation.summary}`);
|
|
182
|
+
if (explanation.established_authority?.value !== issueFact.value) {
|
|
183
|
+
throw new Error('task-first authority-delta explanation lost the established issue authority');
|
|
184
|
+
}
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
if (providerMutationCalls !== 1) {
|
|
195
188
|
throw new Error(`expected exactly one task-side provider mutation before completion, got ${providerMutationCalls}`);
|
|
196
189
|
}
|
|
197
190
|
|
|
198
|
-
|
|
191
|
+
task.complete('live task-first evidence-derived mutation validation complete');
|
|
199
192
|
try {
|
|
200
193
|
await commentOnIssue(issueFact.value, 'THIS MUST NOT RUN AFTER TASK COMPLETION');
|
|
201
194
|
throw new Error('post-completion mutation unexpectedly executed');
|
|
@@ -213,8 +206,8 @@ try {
|
|
|
213
206
|
throw new Error(`expected exactly one provider mutation after blocked attempts, got ${providerMutationCalls}`);
|
|
214
207
|
}
|
|
215
208
|
|
|
216
|
-
console.log('PASS -> GitHub provider output became downstream authority
|
|
217
|
-
console.log('PASS -> unrelated and post-completion
|
|
209
|
+
console.log('PASS -> real GitHub provider output became task-first downstream authority through execution evidence and the reviewed extractor');
|
|
210
|
+
console.log('PASS -> unrelated and post-completion task-first calls produced zero additional provider mutations');
|
|
218
211
|
} finally {
|
|
219
212
|
if (createdCommentId) {
|
|
220
213
|
await cleanupComment(createdCommentId);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createTask } from '@nullsquare/agent-authority/task';
|
|
2
|
+
import { AuthorityApprovalRequiredError } from '@nullsquare/agent-authority/guard';
|
|
3
|
+
import { createRuntimeEnvironment } from '@nullsquare/agent-authority/runtime-env';
|
|
4
|
+
|
|
5
|
+
const repository = process.argv[2] || process.env.GITHUB_REPOSITORY || 'Null-Square/agent-authority';
|
|
6
|
+
const unrelatedRepository = process.argv[3] || 'octocat/Hello-World';
|
|
7
|
+
const env = createRuntimeEnvironment({ home: process.env.AGENT_AUTHORITY_HOME });
|
|
8
|
+
|
|
9
|
+
const connection = env.broker.getConnection({
|
|
10
|
+
principal_id: env.config.principal_id,
|
|
11
|
+
service: 'github'
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!connection) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'No unambiguous GitHub connection. Run: printf %s "$GITHUB_TOKEN" | agent-authority connect github --token-stdin'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const task = createTask({
|
|
21
|
+
principal: env.config.principal_id,
|
|
22
|
+
agent: 'agent:connected-quickstart',
|
|
23
|
+
request: `Inspect only ${repository} through the connected GitHub account`,
|
|
24
|
+
permissions: {
|
|
25
|
+
github: {
|
|
26
|
+
allow: ['repo.read'],
|
|
27
|
+
deny: ['repo.write', 'repo.delete'],
|
|
28
|
+
constraints: {}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
authority: {
|
|
32
|
+
repository: { kind: 'github.repository', value: repository }
|
|
33
|
+
},
|
|
34
|
+
bindings: [
|
|
35
|
+
{ service: 'github', action: 'repo.read', field: 'repository', authority: 'repository' }
|
|
36
|
+
],
|
|
37
|
+
runtime: env.runtime
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`Connected account -> ${connection.metadata?.login || connection.account_id}`);
|
|
41
|
+
console.log('Credential location -> Agent Authority broker/vault (not task context)');
|
|
42
|
+
console.log('Standing GitHub permission -> repo.read');
|
|
43
|
+
console.log(`Task authority -> ${repository}`);
|
|
44
|
+
|
|
45
|
+
const allowed = await task.execute({
|
|
46
|
+
service: 'github',
|
|
47
|
+
action: 'repo.read',
|
|
48
|
+
context: { repository }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(`ALLOW -> connected GitHub returned ${allowed.output.body.full_name}`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await task.execute({
|
|
55
|
+
service: 'github',
|
|
56
|
+
action: 'repo.read',
|
|
57
|
+
context: { repository: unrelatedRepository }
|
|
58
|
+
});
|
|
59
|
+
throw new Error('unrelated repository unexpectedly executed');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
console.log(`STEP-UP -> ${task.explain(error).summary}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const visibleState = JSON.stringify({
|
|
68
|
+
mission: task.mission,
|
|
69
|
+
authorities: task.authorities(),
|
|
70
|
+
connection: env.broker.listConnections(env.config.principal_id),
|
|
71
|
+
allowed_output: allowed.output
|
|
72
|
+
});
|
|
73
|
+
if (/github_pat_|gh[pousr]_|Bearer\s/i.test(visibleState)) {
|
|
74
|
+
throw new Error('credential-like value leaked into public task state');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log('PASS -> connected credential stayed broker-internal and unrelated repository stayed outside task authority');
|
|
@@ -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');
|