@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
package/src/task.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { AuthorityRuntime } from './index.js';
|
|
4
|
+
import { createTaskLease } from './task-lease.js';
|
|
5
|
+
import {
|
|
6
|
+
AuthorityApprovalRequiredError,
|
|
7
|
+
AuthorityDeniedError,
|
|
8
|
+
createTaskLeaseGuard
|
|
9
|
+
} from './guard.js';
|
|
10
|
+
import { createDurableTaskLeaseSession } from './durable-task-lease.js';
|
|
11
|
+
|
|
12
|
+
function requiredString(value, label) {
|
|
13
|
+
if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} is required`);
|
|
14
|
+
return value.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function principalRecord(value) {
|
|
18
|
+
if (typeof value === 'string') return { id: requiredString(value, 'principal') };
|
|
19
|
+
if (value?.id) return structuredClone(value);
|
|
20
|
+
throw new Error('principal must be an id string or { id } object');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function agentRecord(value) {
|
|
24
|
+
if (typeof value === 'string') return { id: requiredString(value, 'agent') };
|
|
25
|
+
if (value?.id) return structuredClone(value);
|
|
26
|
+
throw new Error('agent must be an id string or { id } object');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function factId(name) {
|
|
30
|
+
const normalized = requiredString(name, 'authority name');
|
|
31
|
+
return normalized.startsWith('fact:') ? normalized : `fact:${normalized}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizePermissions(permissions) {
|
|
35
|
+
if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions)) {
|
|
36
|
+
throw new Error('permissions must be a service -> policy object');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resources = Object.entries(permissions).map(([service, policy]) => {
|
|
40
|
+
requiredString(service, 'permission service');
|
|
41
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
42
|
+
throw new Error(`permissions.${service} must be an object`);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(policy.allow) || policy.allow.length === 0) {
|
|
45
|
+
throw new Error(`permissions.${service}.allow must contain at least one action`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
service,
|
|
49
|
+
allow: [...policy.allow],
|
|
50
|
+
deny: Array.isArray(policy.deny) ? [...policy.deny] : [],
|
|
51
|
+
constraints: structuredClone(policy.constraints || {})
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (resources.length === 0) throw new Error('permissions must contain at least one service');
|
|
56
|
+
return resources;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeAuthorityRoots(authority = {}) {
|
|
60
|
+
if (!authority || typeof authority !== 'object' || Array.isArray(authority)) {
|
|
61
|
+
throw new Error('authority must be a name -> value definition object');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.entries(authority).map(([name, definition]) => {
|
|
65
|
+
const normalized = definition && typeof definition === 'object' && !Array.isArray(definition) && Object.hasOwn(definition, 'value')
|
|
66
|
+
? definition
|
|
67
|
+
: { value: definition };
|
|
68
|
+
if (normalized.value === undefined) throw new Error(`authority.${name}.value is required`);
|
|
69
|
+
return {
|
|
70
|
+
fact_id: factId(name),
|
|
71
|
+
kind: normalized.kind || 'opaque',
|
|
72
|
+
value: structuredClone(normalized.value),
|
|
73
|
+
source: normalized.source || 'task-entry'
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeBinding(binding) {
|
|
79
|
+
if (!binding || typeof binding !== 'object') throw new Error('binding must be an object');
|
|
80
|
+
return {
|
|
81
|
+
service: requiredString(binding.service, 'binding.service'),
|
|
82
|
+
action: requiredString(binding.action, 'binding.action'),
|
|
83
|
+
context_field: requiredString(binding.field || binding.context_field, 'binding.field'),
|
|
84
|
+
fact_id: factId(binding.authority || binding.fact_id)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildMission({
|
|
89
|
+
mission,
|
|
90
|
+
principal,
|
|
91
|
+
agent,
|
|
92
|
+
request,
|
|
93
|
+
objective,
|
|
94
|
+
permissions,
|
|
95
|
+
constraints = {},
|
|
96
|
+
approvals = [],
|
|
97
|
+
mission_id
|
|
98
|
+
}) {
|
|
99
|
+
if (mission) return structuredClone(mission);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
version: '0.1',
|
|
103
|
+
mission_id: mission_id || `mission:task:${randomUUID()}`,
|
|
104
|
+
principal: principalRecord(principal),
|
|
105
|
+
agent: agentRecord(agent),
|
|
106
|
+
objective: objective || requiredString(request, 'request'),
|
|
107
|
+
resources: normalizePermissions(permissions),
|
|
108
|
+
constraints: structuredClone(constraints || {}),
|
|
109
|
+
approvals: structuredClone(approvals || [])
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resultFrom(value) {
|
|
114
|
+
if (!value) return null;
|
|
115
|
+
if (value.result?.decision) return value.result;
|
|
116
|
+
if (value.decision) return value;
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
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
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Product-facing task authority facade.
|
|
140
|
+
*
|
|
141
|
+
* It intentionally does not replace Mission/TaskLease. It composes those
|
|
142
|
+
* primitives into the small surface most agent developers need: run an effect,
|
|
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.
|
|
146
|
+
*/
|
|
147
|
+
export class AgentTask {
|
|
148
|
+
constructor({ lease, runtime = new AuthorityRuntime() } = {}) {
|
|
149
|
+
if (!lease || typeof lease.evaluate !== 'function') throw new Error('task lease/session is required');
|
|
150
|
+
if (!runtime || typeof runtime.evaluate !== 'function') throw new Error('authority runtime is required');
|
|
151
|
+
this._lease = lease;
|
|
152
|
+
this.runtime = runtime;
|
|
153
|
+
this.guard = createTaskLeaseGuard({ lease, runtime });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get id() { return this._lease.lease_id; }
|
|
157
|
+
get status() { return this._lease.status; }
|
|
158
|
+
get mission() { return structuredClone(this._lease.mission); }
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Guard an application-owned effect callback.
|
|
162
|
+
*/
|
|
163
|
+
run(request, effect) {
|
|
164
|
+
return this.guard.run(request, effect);
|
|
165
|
+
}
|
|
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
|
+
|
|
182
|
+
authorityFrom(execution, { name, fact_id, kind = 'opaque', from = [], extractor } = {}) {
|
|
183
|
+
if (!execution?.receipt || !execution?.evidence || !Object.hasOwn(execution, 'output')) {
|
|
184
|
+
throw new Error('authorityFrom() requires the result returned by task.run() or task.execute()');
|
|
185
|
+
}
|
|
186
|
+
const parents = Array.isArray(from) ? from : [from];
|
|
187
|
+
return this._lease.deriveFromEvidence({
|
|
188
|
+
fact_id: factId(fact_id || name),
|
|
189
|
+
kind,
|
|
190
|
+
from: parents.map(factId),
|
|
191
|
+
receipt: execution.receipt,
|
|
192
|
+
evidence: execution.evidence,
|
|
193
|
+
output: execution.output,
|
|
194
|
+
extractor
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
bind(binding) {
|
|
199
|
+
return this._lease.bind(normalizeBinding(binding));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
authority(name) {
|
|
203
|
+
return this._lease.fact(factId(name));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
authorities() {
|
|
207
|
+
return this._lease.listFacts();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
complete(reason = 'task completed') {
|
|
211
|
+
return this._lease.complete(reason);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
explain(value) {
|
|
215
|
+
const result = resultFrom(value);
|
|
216
|
+
if (!result) return { decision: 'unknown', code: 'unknown', summary: 'No Agent Authority decision was available.' };
|
|
217
|
+
|
|
218
|
+
if (result.code === 'authority_delta_required') {
|
|
219
|
+
const delta = result.authority_delta || {};
|
|
220
|
+
const established = delta.current_fact_id ? this._lease.fact(delta.current_fact_id) : null;
|
|
221
|
+
return {
|
|
222
|
+
decision: result.decision,
|
|
223
|
+
code: result.code,
|
|
224
|
+
summary: `The task established authority for ${JSON.stringify(established?.value)} but this action requested ${JSON.stringify(delta.requested_value)}.`,
|
|
225
|
+
service: delta.service,
|
|
226
|
+
action: delta.action,
|
|
227
|
+
field: delta.context_field,
|
|
228
|
+
established_authority: established,
|
|
229
|
+
requested_value: structuredClone(delta.requested_value)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
decision: result.decision,
|
|
235
|
+
code: result.code || null,
|
|
236
|
+
summary: result.reason || 'Agent Authority returned a decision.'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createTask({
|
|
242
|
+
mission = null,
|
|
243
|
+
principal = null,
|
|
244
|
+
agent = null,
|
|
245
|
+
request,
|
|
246
|
+
objective = null,
|
|
247
|
+
permissions = null,
|
|
248
|
+
constraints = {},
|
|
249
|
+
approvals = [],
|
|
250
|
+
mission_id = null,
|
|
251
|
+
authority = {},
|
|
252
|
+
bindings = [],
|
|
253
|
+
expires_at = null,
|
|
254
|
+
runtime = new AuthorityRuntime(),
|
|
255
|
+
store = null
|
|
256
|
+
} = {}) {
|
|
257
|
+
const resolvedMission = buildMission({
|
|
258
|
+
mission,
|
|
259
|
+
principal,
|
|
260
|
+
agent,
|
|
261
|
+
request,
|
|
262
|
+
objective,
|
|
263
|
+
permissions,
|
|
264
|
+
constraints,
|
|
265
|
+
approvals,
|
|
266
|
+
mission_id
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const lease = createTaskLease({
|
|
270
|
+
mission: resolvedMission,
|
|
271
|
+
request,
|
|
272
|
+
roots: normalizeAuthorityRoots(authority),
|
|
273
|
+
bindings: bindings.map(normalizeBinding),
|
|
274
|
+
expires_at
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const authorityLease = store ? createDurableTaskLeaseSession({ store, lease }) : lease;
|
|
278
|
+
return new AgentTask({ lease: authorityLease, runtime });
|
|
279
|
+
}
|