@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/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
+ }