@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.
- package/README.md +59 -3
- package/ROADMAP.md +20 -3
- package/docs/connected-execution-api.md +60 -0
- package/docs/connected-github.md +140 -0
- package/docs/npm-release.md +25 -9
- package/docs/product-proof.md +134 -8
- 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-support.js +127 -0
- package/package.json +4 -2
- package/src/cli.js +3 -3
- package/src/connections.js +19 -4
- package/src/task.js +45 -4
|
@@ -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.
|
|
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 = '
|
|
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
|
|
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 ${
|
|
187
|
+
console.log(`✓ GitHub ${result.account_id} disconnected and local credential removed`);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
async function agentCommand(args) {
|
package/src/connections.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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({
|
|
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 {
|
|
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
|
-
*
|
|
122
|
-
* effects, explain step-up decisions, and
|
|
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({
|