@nullsquare/agent-authority 0.4.5 → 0.4.6
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 +245 -300
- package/ROADMAP.md +128 -105
- package/benchmarks/task-utility.mjs +130 -0
- package/docs/npm-release.md +13 -9
- package/docs/product-proof.md +185 -0
- package/examples/task-first-github.js +102 -0
- package/package.json +7 -4
- package/src/task.js +238 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Product proof gate
|
|
2
|
+
|
|
3
|
+
Agent Authority has enough security machinery to validate its core thesis. The next risk is no longer "can we make the invariant stronger?" It is "will an agent developer actually install and keep this layer?"
|
|
4
|
+
|
|
5
|
+
The product thesis is:
|
|
6
|
+
|
|
7
|
+
> **Your agent may use the permissions it already has only for the task the user actually gave it.**
|
|
8
|
+
|
|
9
|
+
The differentiated mechanism is narrower:
|
|
10
|
+
|
|
11
|
+
> **Authority may follow exact resources discovered through already-authorized execution, without turning those resources into standing account permissions.**
|
|
12
|
+
|
|
13
|
+
Everything else in the repository exists to make those two statements true.
|
|
14
|
+
|
|
15
|
+
## Developer mental model
|
|
16
|
+
|
|
17
|
+
The preferred public experience should stay close to three concepts:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Task -> Effect -> Authority
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A developer should not need to understand Mission internals, Task Lease hashing, execution evidence envelopes, CAS persistence or transport adapters before getting value.
|
|
24
|
+
|
|
25
|
+
Those primitives remain available for advanced integrations and audits.
|
|
26
|
+
|
|
27
|
+
## Product-facing API
|
|
28
|
+
|
|
29
|
+
The task-first facade intentionally composes the existing primitives instead of replacing them:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { createTask } from '@nullsquare/agent-authority/task';
|
|
33
|
+
|
|
34
|
+
const task = createTask({
|
|
35
|
+
principal: 'user:me',
|
|
36
|
+
agent: 'agent:assistant',
|
|
37
|
+
request: 'Handle issue #42',
|
|
38
|
+
permissions: {
|
|
39
|
+
github: {
|
|
40
|
+
allow: ['issue.list', 'issue.comment'],
|
|
41
|
+
deny: ['repo.delete'],
|
|
42
|
+
constraints: { repository: ['acme/app'] }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
authority: {
|
|
46
|
+
repository: { kind: 'github.repository', value: 'acme/app' }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const discovery = await task.run(request, () => github.listIssues());
|
|
51
|
+
const issue = task.authorityFrom(discovery, {
|
|
52
|
+
name: 'issue',
|
|
53
|
+
kind: 'github.issue.number',
|
|
54
|
+
from: 'repository',
|
|
55
|
+
extractor
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
task.bind({
|
|
59
|
+
service: 'github',
|
|
60
|
+
action: 'issue.comment',
|
|
61
|
+
field: 'issue_number',
|
|
62
|
+
authority: 'issue'
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The low-level Mission and Task Lease APIs remain the source of truth. The facade must never add authority that those lower layers would reject.
|
|
67
|
+
|
|
68
|
+
## Adoption gate
|
|
69
|
+
|
|
70
|
+
Do not prioritize another deep authorization subsystem until the following are demonstrated:
|
|
71
|
+
|
|
72
|
+
- [ ] a new developer can run a meaningful task-first example in under 10 minutes;
|
|
73
|
+
- [ ] at least three real workflow examples exist: coding, support/communications, and operations/finance;
|
|
74
|
+
- [ ] the same task-first API works in-memory and with durable local state;
|
|
75
|
+
- [ ] useful-task completion stays high under the product benchmark;
|
|
76
|
+
- [ ] normal task actions do not trigger unnecessary approvals;
|
|
77
|
+
- [ ] unrelated-resource effects execute zero provider callbacks;
|
|
78
|
+
- [ ] approval/step-up output explains the established authority and requested delta clearly;
|
|
79
|
+
- [ ] at least one external developer uses the package without project-author assistance.
|
|
80
|
+
|
|
81
|
+
## Utility metrics
|
|
82
|
+
|
|
83
|
+
Security tests remain required, but product work should additionally track:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
normal task completion rate
|
|
87
|
+
false approval rate
|
|
88
|
+
true authority-delta step-up rate
|
|
89
|
+
unauthorized effect rate
|
|
90
|
+
provider effects per completed task
|
|
91
|
+
integration lines required for a representative workflow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`npm run benchmark:task` is the first deterministic fixture for these metrics. It is not a real-world benchmark and must not be marketed as one. Its purpose is to make utility regressions visible alongside security regressions.
|
|
95
|
+
|
|
96
|
+
The current fixture target is:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
normal task completion rate = 100%
|
|
100
|
+
false approval rate = 0%
|
|
101
|
+
true authority-delta step-up rate = 100%
|
|
102
|
+
unauthorized effect rate = 0%
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Real provider/harness benchmarks should replace or supplement the fixture as the product matures.
|
|
106
|
+
|
|
107
|
+
## Three product proofs
|
|
108
|
+
|
|
109
|
+
### Coding agent
|
|
110
|
+
|
|
111
|
+
Task:
|
|
112
|
+
|
|
113
|
+
> Fix issue #42 and open a PR. Do not merge or deploy.
|
|
114
|
+
|
|
115
|
+
Desired authority lineage:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
repository -> issue -> task branch -> changed files -> pull request
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Unrelated repositories, issues, merge and deploy remain outside the task.
|
|
122
|
+
|
|
123
|
+
### Support / communications agent
|
|
124
|
+
|
|
125
|
+
Task:
|
|
126
|
+
|
|
127
|
+
> Handle this customer email.
|
|
128
|
+
|
|
129
|
+
Desired authority lineage:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
email thread -> customer -> meeting / CRM record / reply target
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The underlying connected account may have broad access; task authority follows only the customer/resource discovered through the authorized thread.
|
|
136
|
+
|
|
137
|
+
### Operations / finance agent
|
|
138
|
+
|
|
139
|
+
Task:
|
|
140
|
+
|
|
141
|
+
> Resolve this ticket and refund the affected order.
|
|
142
|
+
|
|
143
|
+
Desired authority lineage:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
ticket -> customer -> order -> payment -> refund <= original payment
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is the strongest long-term proof because it combines resource lineage with an amount ceiling.
|
|
150
|
+
|
|
151
|
+
## Freeze list
|
|
152
|
+
|
|
153
|
+
Until the adoption gate moves, the following remain research backlog unless a real workflow proves they are blocking adoption or safety:
|
|
154
|
+
|
|
155
|
+
- distributed Task Lease databases;
|
|
156
|
+
- generic storage abstraction layers;
|
|
157
|
+
- provider-signed attestation protocols;
|
|
158
|
+
- another token or identity format;
|
|
159
|
+
- a general delegation standard;
|
|
160
|
+
- a proprietary policy DSL;
|
|
161
|
+
- broad OAuth/OIDC platform work;
|
|
162
|
+
- another MCP control plane;
|
|
163
|
+
- A2A protocol implementation;
|
|
164
|
+
- large connector-count expansion;
|
|
165
|
+
- full distributed transaction semantics across arbitrary remote providers.
|
|
166
|
+
|
|
167
|
+
The existing durability, evidence, transport and credential primitives should be reused rather than deepened by default.
|
|
168
|
+
|
|
169
|
+
## Boundary discipline
|
|
170
|
+
|
|
171
|
+
Agent Authority should integrate with identity providers, OAuth systems, MCP gateways, policy engines and agent frameworks rather than compete with all of them.
|
|
172
|
+
|
|
173
|
+
The intended position is:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
agent reasoning
|
|
177
|
+
|
|
|
178
|
+
v
|
|
179
|
+
Agent Authority
|
|
180
|
+
|
|
|
181
|
+
v
|
|
182
|
+
existing SDK / MCP / gateway / OAuth / provider
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The product wins if that middle layer is small to adopt, preserves useful autonomy, and technically prevents the same standing account permission from becoming unrelated task authority.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createTask } from '../src/task.js';
|
|
2
|
+
import { AuthorityApprovalRequiredError } from '../src/guard.js';
|
|
3
|
+
|
|
4
|
+
const repository = 'Null-Square/agent-authority';
|
|
5
|
+
|
|
6
|
+
function issueNumberExtractor({ receipt, output } = {}) {
|
|
7
|
+
if (receipt?.service !== 'github' || receipt?.action !== 'issue.list') {
|
|
8
|
+
const error = new Error('extractor only accepts github:issue.list');
|
|
9
|
+
error.code = 'trusted_extractor_operation_mismatch';
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
if (!Number.isSafeInteger(output?.selected_issue_number)) {
|
|
13
|
+
const error = new Error('discovery did not produce a canonical issue number');
|
|
14
|
+
error.code = 'trusted_extractor_output_invalid';
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
extractor_id: 'demo.github.selected-issue.v1',
|
|
19
|
+
selector: 'output.selected_issue_number'
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const task = createTask({
|
|
24
|
+
principal: 'user:demo',
|
|
25
|
+
agent: 'agent:demo',
|
|
26
|
+
request: 'Find issue #42 and leave one comment only on that issue',
|
|
27
|
+
permissions: {
|
|
28
|
+
github: {
|
|
29
|
+
allow: ['issue.list', 'issue.comment'],
|
|
30
|
+
deny: ['issue.close', 'repo.delete'],
|
|
31
|
+
constraints: { repository: [repository] }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
authority: {
|
|
35
|
+
repository: { kind: 'github.repository', value: repository }
|
|
36
|
+
},
|
|
37
|
+
bindings: [
|
|
38
|
+
{ service: 'github', action: 'issue.list', field: 'repository', authority: 'repository' },
|
|
39
|
+
{ service: 'github', action: 'issue.comment', field: 'repository', authority: 'repository' }
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let providerEffects = 0;
|
|
44
|
+
|
|
45
|
+
console.log('Task: Find issue #42 and leave one comment only on that issue');
|
|
46
|
+
console.log('1. Discover the task resource through an authorized read');
|
|
47
|
+
const discovery = await task.run({
|
|
48
|
+
service: 'github',
|
|
49
|
+
action: 'issue.list',
|
|
50
|
+
context: { repository }
|
|
51
|
+
}, async () => {
|
|
52
|
+
providerEffects += 1;
|
|
53
|
+
// Replace this callback with your existing GitHub SDK/provider call.
|
|
54
|
+
return { selected_issue_number: 42, selected_issue_title: 'Example issue' };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(` ALLOW -> discovered issue #${discovery.output.selected_issue_number}`);
|
|
58
|
+
|
|
59
|
+
const issue = task.authorityFrom(discovery, {
|
|
60
|
+
name: 'issue',
|
|
61
|
+
kind: 'github.issue.number',
|
|
62
|
+
from: 'repository',
|
|
63
|
+
extractor: issueNumberExtractor
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
task.bind({
|
|
67
|
+
service: 'github',
|
|
68
|
+
action: 'issue.comment',
|
|
69
|
+
field: 'issue_number',
|
|
70
|
+
authority: 'issue'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(`2. Authority follows the guarded result -> issue #${issue.value}`);
|
|
74
|
+
|
|
75
|
+
await task.run({
|
|
76
|
+
service: 'github',
|
|
77
|
+
action: 'issue.comment',
|
|
78
|
+
context: { repository, issue_number: issue.value, body: 'Handled by the task.' }
|
|
79
|
+
}, async () => {
|
|
80
|
+
providerEffects += 1;
|
|
81
|
+
return { comment_id: 1001 };
|
|
82
|
+
});
|
|
83
|
+
console.log('3. ALLOW -> comment on issue #42 executed');
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await task.run({
|
|
87
|
+
service: 'github',
|
|
88
|
+
action: 'issue.comment',
|
|
89
|
+
context: { repository, issue_number: 7, body: 'This must not execute.' }
|
|
90
|
+
}, async () => {
|
|
91
|
+
providerEffects += 1;
|
|
92
|
+
return { comment_id: 1002 };
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!(error instanceof AuthorityApprovalRequiredError)) throw error;
|
|
96
|
+
const explanation = task.explain(error);
|
|
97
|
+
console.log('4. STEP-UP -> unrelated issue blocked before the provider callback');
|
|
98
|
+
console.log(` ${explanation.summary}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`Provider effects executed: ${providerEffects} (expected: 2)`);
|
|
102
|
+
console.log('PASS -> useful task actions proceed while unrelated account authority does not become task authority');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullsquare/agent-authority",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|
|
@@ -32,18 +32,20 @@
|
|
|
32
32
|
"setup": "node src/cli.js setup",
|
|
33
33
|
"doctor": "node src/cli.js doctor",
|
|
34
34
|
"demo": "node examples/demo.js",
|
|
35
|
+
"demo:task": "node examples/task-first-github.js",
|
|
35
36
|
"demo:task-lease": "node examples/task-lease-demo.js",
|
|
36
37
|
"demo:live-github": "node examples/live-github-task-lease.js",
|
|
37
38
|
"demo:live-derived-github": "node examples/live-github-derived-mutation.js",
|
|
38
39
|
"demo:live-google": "node examples/live-google-cross-provider.js",
|
|
39
40
|
"demo:mcp-upstream": "node examples/validation-mcp-upstream.js",
|
|
40
41
|
"demo:guard": "node examples/direct-guard.js",
|
|
42
|
+
"benchmark:task": "node benchmarks/task-utility.mjs",
|
|
41
43
|
"test": "node --test test/*.test.js",
|
|
42
44
|
"test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
|
|
43
45
|
"test:coverage": "node --experimental-test-coverage --test test/*.test.js",
|
|
44
|
-
"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/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-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",
|
|
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",
|
|
45
47
|
"check:package": "npm pack --dry-run",
|
|
46
|
-
"check": "npm run check:syntax && npm test && npm run demo:task-lease && npm run check:package"
|
|
48
|
+
"check": "npm run check:syntax && npm test && npm run demo:task && npm run benchmark:task && npm run demo:task-lease && npm run check:package"
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
@@ -73,11 +75,12 @@
|
|
|
73
75
|
"./mcp-server": "./src/mcp-server.js",
|
|
74
76
|
"./sdk": "./src/sdk.js",
|
|
75
77
|
"./storage": "./src/storage.js",
|
|
78
|
+
"./task": "./src/task.js",
|
|
76
79
|
"./task-lease": "./src/task-lease.js",
|
|
77
80
|
"./providers/github": "./src/providers/github.js",
|
|
78
81
|
"./providers/google": "./src/providers/google.js"
|
|
79
82
|
},
|
|
80
|
-
"files": ["src", "docs", "examples", "README.md", "LICENSE", "SECURITY.md", "ROADMAP.md", "CONTRIBUTING.md"],
|
|
83
|
+
"files": ["src", "docs", "examples", "benchmarks", "README.md", "LICENSE", "SECURITY.md", "ROADMAP.md", "CONTRIBUTING.md"],
|
|
81
84
|
"repository": { "type": "git", "url": "git+https://github.com/Null-Square/agent-authority.git" },
|
|
82
85
|
"bugs": { "url": "https://github.com/Null-Square/agent-authority/issues" },
|
|
83
86
|
"homepage": "https://github.com/Null-Square/agent-authority#readme"
|
package/src/task.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { AuthorityRuntime } from './index.js';
|
|
4
|
+
import { createTaskLease } from './task-lease.js';
|
|
5
|
+
import { createTaskLeaseGuard } from './guard.js';
|
|
6
|
+
import { createDurableTaskLeaseSession } from './durable-task-lease.js';
|
|
7
|
+
|
|
8
|
+
function requiredString(value, label) {
|
|
9
|
+
if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} is required`);
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function principalRecord(value) {
|
|
14
|
+
if (typeof value === 'string') return { id: requiredString(value, 'principal') };
|
|
15
|
+
if (value?.id) return structuredClone(value);
|
|
16
|
+
throw new Error('principal must be an id string or { id } object');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function agentRecord(value) {
|
|
20
|
+
if (typeof value === 'string') return { id: requiredString(value, 'agent') };
|
|
21
|
+
if (value?.id) return structuredClone(value);
|
|
22
|
+
throw new Error('agent must be an id string or { id } object');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function factId(name) {
|
|
26
|
+
const normalized = requiredString(name, 'authority name');
|
|
27
|
+
return normalized.startsWith('fact:') ? normalized : `fact:${normalized}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePermissions(permissions) {
|
|
31
|
+
if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions)) {
|
|
32
|
+
throw new Error('permissions must be a service -> policy object');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resources = Object.entries(permissions).map(([service, policy]) => {
|
|
36
|
+
requiredString(service, 'permission service');
|
|
37
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
38
|
+
throw new Error(`permissions.${service} must be an object`);
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(policy.allow) || policy.allow.length === 0) {
|
|
41
|
+
throw new Error(`permissions.${service}.allow must contain at least one action`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
service,
|
|
45
|
+
allow: [...policy.allow],
|
|
46
|
+
deny: Array.isArray(policy.deny) ? [...policy.deny] : [],
|
|
47
|
+
constraints: structuredClone(policy.constraints || {})
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (resources.length === 0) throw new Error('permissions must contain at least one service');
|
|
52
|
+
return resources;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeAuthorityRoots(authority = {}) {
|
|
56
|
+
if (!authority || typeof authority !== 'object' || Array.isArray(authority)) {
|
|
57
|
+
throw new Error('authority must be a name -> value definition object');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.entries(authority).map(([name, definition]) => {
|
|
61
|
+
const normalized = definition && typeof definition === 'object' && !Array.isArray(definition) && Object.hasOwn(definition, 'value')
|
|
62
|
+
? definition
|
|
63
|
+
: { value: definition };
|
|
64
|
+
if (normalized.value === undefined) throw new Error(`authority.${name}.value is required`);
|
|
65
|
+
return {
|
|
66
|
+
fact_id: factId(name),
|
|
67
|
+
kind: normalized.kind || 'opaque',
|
|
68
|
+
value: structuredClone(normalized.value),
|
|
69
|
+
source: normalized.source || 'task-entry'
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeBinding(binding) {
|
|
75
|
+
if (!binding || typeof binding !== 'object') throw new Error('binding must be an object');
|
|
76
|
+
return {
|
|
77
|
+
service: requiredString(binding.service, 'binding.service'),
|
|
78
|
+
action: requiredString(binding.action, 'binding.action'),
|
|
79
|
+
context_field: requiredString(binding.field || binding.context_field, 'binding.field'),
|
|
80
|
+
fact_id: factId(binding.authority || binding.fact_id)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildMission({
|
|
85
|
+
mission,
|
|
86
|
+
principal,
|
|
87
|
+
agent,
|
|
88
|
+
request,
|
|
89
|
+
objective,
|
|
90
|
+
permissions,
|
|
91
|
+
constraints = {},
|
|
92
|
+
approvals = [],
|
|
93
|
+
mission_id
|
|
94
|
+
}) {
|
|
95
|
+
if (mission) return structuredClone(mission);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
version: '0.1',
|
|
99
|
+
mission_id: mission_id || `mission:task:${randomUUID()}`,
|
|
100
|
+
principal: principalRecord(principal),
|
|
101
|
+
agent: agentRecord(agent),
|
|
102
|
+
objective: objective || requiredString(request, 'request'),
|
|
103
|
+
resources: normalizePermissions(permissions),
|
|
104
|
+
constraints: structuredClone(constraints || {}),
|
|
105
|
+
approvals: structuredClone(approvals || [])
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resultFrom(value) {
|
|
110
|
+
if (!value) return null;
|
|
111
|
+
if (value.result?.decision) return value.result;
|
|
112
|
+
if (value.decision) return value;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Product-facing task authority facade.
|
|
118
|
+
*
|
|
119
|
+
* It intentionally does not replace Mission/TaskLease. It composes those
|
|
120
|
+
* primitives into the small surface most agent developers need: run an effect,
|
|
121
|
+
* derive named authority from guarded output, bind that authority to later
|
|
122
|
+
* effects, explain step-up decisions, and complete the task.
|
|
123
|
+
*/
|
|
124
|
+
export class AgentTask {
|
|
125
|
+
constructor({ lease, runtime = new AuthorityRuntime() } = {}) {
|
|
126
|
+
if (!lease || typeof lease.evaluate !== 'function') throw new Error('task lease/session is required');
|
|
127
|
+
if (!runtime || typeof runtime.evaluate !== 'function') throw new Error('authority runtime is required');
|
|
128
|
+
this._lease = lease;
|
|
129
|
+
this.runtime = runtime;
|
|
130
|
+
this.guard = createTaskLeaseGuard({ lease, runtime });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get id() { return this._lease.lease_id; }
|
|
134
|
+
get status() { return this._lease.status; }
|
|
135
|
+
get mission() { return structuredClone(this._lease.mission); }
|
|
136
|
+
|
|
137
|
+
run(request, effect) {
|
|
138
|
+
return this.guard.run(request, effect);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
authorityFrom(execution, { name, fact_id, kind = 'opaque', from = [], extractor } = {}) {
|
|
142
|
+
if (!execution?.receipt || !execution?.evidence || !Object.hasOwn(execution, 'output')) {
|
|
143
|
+
throw new Error('authorityFrom() requires the result returned by task.run()');
|
|
144
|
+
}
|
|
145
|
+
const parents = Array.isArray(from) ? from : [from];
|
|
146
|
+
return this._lease.deriveFromEvidence({
|
|
147
|
+
fact_id: factId(fact_id || name),
|
|
148
|
+
kind,
|
|
149
|
+
from: parents.map(factId),
|
|
150
|
+
receipt: execution.receipt,
|
|
151
|
+
evidence: execution.evidence,
|
|
152
|
+
output: execution.output,
|
|
153
|
+
extractor
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bind(binding) {
|
|
158
|
+
return this._lease.bind(normalizeBinding(binding));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
authority(name) {
|
|
162
|
+
return this._lease.fact(factId(name));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
authorities() {
|
|
166
|
+
return this._lease.listFacts();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
complete(reason = 'task completed') {
|
|
170
|
+
return this._lease.complete(reason);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
explain(value) {
|
|
174
|
+
const result = resultFrom(value);
|
|
175
|
+
if (!result) return { decision: 'unknown', code: 'unknown', summary: 'No Agent Authority decision was available.' };
|
|
176
|
+
|
|
177
|
+
if (result.code === 'authority_delta_required') {
|
|
178
|
+
const delta = result.authority_delta || {};
|
|
179
|
+
const established = delta.current_fact_id ? this._lease.fact(delta.current_fact_id) : null;
|
|
180
|
+
return {
|
|
181
|
+
decision: result.decision,
|
|
182
|
+
code: result.code,
|
|
183
|
+
summary: `The task established authority for ${JSON.stringify(established?.value)} but this action requested ${JSON.stringify(delta.requested_value)}.`,
|
|
184
|
+
service: delta.service,
|
|
185
|
+
action: delta.action,
|
|
186
|
+
field: delta.context_field,
|
|
187
|
+
established_authority: established,
|
|
188
|
+
requested_value: structuredClone(delta.requested_value)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
decision: result.decision,
|
|
194
|
+
code: result.code || null,
|
|
195
|
+
summary: result.reason || 'Agent Authority returned a decision.'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createTask({
|
|
201
|
+
mission = null,
|
|
202
|
+
principal = null,
|
|
203
|
+
agent = null,
|
|
204
|
+
request,
|
|
205
|
+
objective = null,
|
|
206
|
+
permissions = null,
|
|
207
|
+
constraints = {},
|
|
208
|
+
approvals = [],
|
|
209
|
+
mission_id = null,
|
|
210
|
+
authority = {},
|
|
211
|
+
bindings = [],
|
|
212
|
+
expires_at = null,
|
|
213
|
+
runtime = new AuthorityRuntime(),
|
|
214
|
+
store = null
|
|
215
|
+
} = {}) {
|
|
216
|
+
const resolvedMission = buildMission({
|
|
217
|
+
mission,
|
|
218
|
+
principal,
|
|
219
|
+
agent,
|
|
220
|
+
request,
|
|
221
|
+
objective,
|
|
222
|
+
permissions,
|
|
223
|
+
constraints,
|
|
224
|
+
approvals,
|
|
225
|
+
mission_id
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const lease = createTaskLease({
|
|
229
|
+
mission: resolvedMission,
|
|
230
|
+
request,
|
|
231
|
+
roots: normalizeAuthorityRoots(authority),
|
|
232
|
+
bindings: bindings.map(normalizeBinding),
|
|
233
|
+
expires_at
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const authorityLease = store ? createDurableTaskLeaseSession({ store, lease }) : lease;
|
|
237
|
+
return new AgentTask({ lease: authorityLease, runtime });
|
|
238
|
+
}
|