@nullsquare/agent-authority 0.4.0
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/CONTRIBUTING.md +93 -0
- package/LICENSE +201 -0
- package/README.md +390 -0
- package/ROADMAP.md +149 -0
- package/SECURITY.md +116 -0
- package/docs/account-connections.md +173 -0
- package/docs/announcement-draft.md +13 -0
- package/docs/architecture.md +106 -0
- package/docs/assets/agent-authority-cover.svg +41 -0
- package/docs/clear-path.md +53 -0
- package/docs/cli.md +130 -0
- package/docs/evidence.md +143 -0
- package/docs/harness-bridge-mode.md +136 -0
- package/docs/harness-integration.md +223 -0
- package/docs/integration-contract.md +132 -0
- package/docs/integrations/vercel-ai-sdk.md +161 -0
- package/docs/launch-checklist.md +29 -0
- package/docs/npm-release.md +19 -0
- package/docs/openclaw-integration.md +97 -0
- package/docs/package-consumer-validation.md +18 -0
- package/docs/release-candidate-status.md +3 -0
- package/docs/release-guardrails.md +8 -0
- package/docs/release-notes-v0.4.md +26 -0
- package/docs/release-scope.md +3 -0
- package/docs/ship-criteria.md +3 -0
- package/docs/task-leases.md +253 -0
- package/docs/validation.md +124 -0
- package/examples/demo.js +19 -0
- package/examples/direct-guard.js +50 -0
- package/examples/harness-managed-connectors.js +72 -0
- package/examples/live-github-derived-mutation.js +208 -0
- package/examples/live-github-task-lease.js +80 -0
- package/examples/mission.json +20 -0
- package/examples/missions/chatgpt-web-validation.json +33 -0
- package/examples/openclaw-tool-wrapper.js +49 -0
- package/examples/task-lease-demo.js +98 -0
- package/examples/validation-mcp-upstream.js +112 -0
- package/package.json +80 -0
- package/src/agent-auth.js +135 -0
- package/src/approvals.js +157 -0
- package/src/cli.js +335 -0
- package/src/connections.js +203 -0
- package/src/execution.js +174 -0
- package/src/guard.js +79 -0
- package/src/harness-bridge.js +131 -0
- package/src/idempotency.js +118 -0
- package/src/index.js +291 -0
- package/src/integrations/ai-sdk.js +59 -0
- package/src/keys.js +15 -0
- package/src/mcp-gateway.js +142 -0
- package/src/mcp-remote.js +102 -0
- package/src/mcp-server.js +102 -0
- package/src/providers/github.js +149 -0
- package/src/runtime-env.js +53 -0
- package/src/sdk.js +75 -0
- package/src/server.js +146 -0
- package/src/storage.js +213 -0
- package/src/task-lease.js +266 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Task Leases and Derived Authority
|
|
2
|
+
|
|
3
|
+
Agent Authority's current product thesis is simple:
|
|
4
|
+
|
|
5
|
+
> Give an agent a task, not standing account permissions.
|
|
6
|
+
|
|
7
|
+
A **Task Lease** is a temporary enforcement context around an existing mission. The mission remains the maximum authority ceiling. The lease can only narrow that authority as the task discovers concrete resources.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
A human request often does not contain every resource identifier the agent will need.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
|
|
15
|
+
> Handle the demo request in this email thread.
|
|
16
|
+
|
|
17
|
+
At task start the runtime may know only the Gmail thread ID. During authorized execution it discovers the sender email. That sender may then become the only attendee the agent is allowed to use when creating a calendar event.
|
|
18
|
+
|
|
19
|
+
Without a task-aware authority layer, the application usually chooses between two bad options:
|
|
20
|
+
|
|
21
|
+
1. give the agent broad `calendar.write` / `mail.send` permissions; or
|
|
22
|
+
2. ask the human to approve every individual tool call.
|
|
23
|
+
|
|
24
|
+
Task Leases aim for a third option: **task-bounded autonomy**.
|
|
25
|
+
|
|
26
|
+
## Core model
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Human-approved task
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
authority roots
|
|
33
|
+
|
|
|
34
|
+
authorized execution
|
|
35
|
+
|
|
|
36
|
+
v
|
|
37
|
+
derived facts
|
|
38
|
+
|
|
|
39
|
+
exact action bindings
|
|
40
|
+
|
|
|
41
|
+
v
|
|
42
|
+
effect
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Authority root
|
|
46
|
+
|
|
47
|
+
A value explicitly trusted at task entry.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
|
|
51
|
+
- `gmail.thread = thread:91`
|
|
52
|
+
- `github.issue = 42`
|
|
53
|
+
- `invoice.id = INV-2026-18`
|
|
54
|
+
|
|
55
|
+
Roots do not require a prior execution receipt because they come from the task's trusted entry boundary.
|
|
56
|
+
|
|
57
|
+
### Derived fact
|
|
58
|
+
|
|
59
|
+
A value learned while performing the task.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
|
|
63
|
+
- sender email discovered from an authorized Gmail thread read;
|
|
64
|
+
- customer ID discovered from an authorized support-ticket lookup;
|
|
65
|
+
- order ID discovered from an authorized customer lookup.
|
|
66
|
+
|
|
67
|
+
In v0.4, a derived fact must reference an `ALLOW` receipt from the same mission. It may also reference existing parent facts.
|
|
68
|
+
|
|
69
|
+
### Binding
|
|
70
|
+
|
|
71
|
+
A binding narrows an otherwise permitted action to the exact value held by an authority fact.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
{
|
|
75
|
+
service: 'calendar',
|
|
76
|
+
action: 'event.create',
|
|
77
|
+
context_field: 'attendee',
|
|
78
|
+
fact_id: 'fact:requester-email'
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If `fact:requester-email` has not been established, the action is denied.
|
|
83
|
+
|
|
84
|
+
If it has value `customer@example.com`, this request can proceed:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
{
|
|
88
|
+
service: 'calendar',
|
|
89
|
+
action: 'event.create',
|
|
90
|
+
context: { attendee: 'customer@example.com' }
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This request does not proceed automatically:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
{
|
|
98
|
+
service: 'calendar',
|
|
99
|
+
action: 'event.create',
|
|
100
|
+
context: { attendee: 'other@example.com' }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It returns `REQUIRE_APPROVAL` with `authority_delta_required`.
|
|
105
|
+
|
|
106
|
+
## Non-amplification rule
|
|
107
|
+
|
|
108
|
+
A Task Lease is not a second policy engine and cannot grant a new action class.
|
|
109
|
+
|
|
110
|
+
The existing mission is always evaluated first.
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
mission says DENY
|
|
114
|
+
|
|
|
115
|
+
v
|
|
116
|
+
Task Lease cannot override it
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The lease can only add restrictions to an action that the mission already allows.
|
|
120
|
+
|
|
121
|
+
This is the key invariant:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
lease authority <= mission authority
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Authority may stay the same or shrink. It must never grow silently.
|
|
128
|
+
|
|
129
|
+
## Example
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { AuthorityRuntime } from '@nullsquare/agent-authority';
|
|
133
|
+
import { createTaskLease } from '@nullsquare/agent-authority/task-lease';
|
|
134
|
+
import { createTaskLeaseGuard } from '@nullsquare/agent-authority/guard';
|
|
135
|
+
|
|
136
|
+
const lease = createTaskLease({
|
|
137
|
+
mission,
|
|
138
|
+
request: 'Handle the demo request in thread:demo-91',
|
|
139
|
+
roots: [
|
|
140
|
+
{ fact_id: 'fact:thread', kind: 'gmail.thread', value: 'thread:demo-91' }
|
|
141
|
+
],
|
|
142
|
+
bindings: [
|
|
143
|
+
{
|
|
144
|
+
service: 'calendar',
|
|
145
|
+
action: 'event.create',
|
|
146
|
+
context_field: 'attendee',
|
|
147
|
+
fact_id: 'fact:sender-email'
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const guard = createTaskLeaseGuard({
|
|
153
|
+
lease,
|
|
154
|
+
runtime: new AuthorityRuntime()
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const read = await guard.run({
|
|
158
|
+
service: 'gmail',
|
|
159
|
+
action: 'thread.read',
|
|
160
|
+
context: { thread: 'thread:demo-91' }
|
|
161
|
+
}, () => gmail.readThread('thread:demo-91'));
|
|
162
|
+
|
|
163
|
+
lease.derive({
|
|
164
|
+
fact_id: 'fact:sender-email',
|
|
165
|
+
kind: 'email.address',
|
|
166
|
+
value: read.output.sender,
|
|
167
|
+
from: ['fact:thread'],
|
|
168
|
+
receipt: read.receipt,
|
|
169
|
+
selector: 'output.sender'
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await guard.run({
|
|
173
|
+
service: 'calendar',
|
|
174
|
+
action: 'event.create',
|
|
175
|
+
context: { attendee: read.output.sender }
|
|
176
|
+
}, () => calendar.createEvent({ attendee: read.output.sender }));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Run the self-contained example:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm run demo:task-lease
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Task completion
|
|
186
|
+
|
|
187
|
+
Task authority should not outlive the task.
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
lease.complete('demo request handled');
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
After completion, every guarded action returns `DENY` with `task_lease_completed` even if the underlying OAuth token or provider connection still exists.
|
|
194
|
+
|
|
195
|
+
A lease may also have its own `expires_at` independent of provider credential expiry.
|
|
196
|
+
|
|
197
|
+
## Authority delta
|
|
198
|
+
|
|
199
|
+
When the agent asks to use a concrete value outside an established binding, Agent Authority returns a step-up signal rather than silently broadening the lease.
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
current task authority
|
|
203
|
+
+
|
|
204
|
+
requested new resource
|
|
205
|
+
|
|
|
206
|
+
v
|
|
207
|
+
authority_delta_required
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The existing approval store can handle the human decision. Automatically applying approved deltas to a live Task Lease is a later milestone; v0.4 deliberately stops at the safe enforcement signal.
|
|
211
|
+
|
|
212
|
+
## Current security properties
|
|
213
|
+
|
|
214
|
+
The v0.4 implementation tests that:
|
|
215
|
+
|
|
216
|
+
- a bound action cannot run before its fact exists;
|
|
217
|
+
- derived facts require an `ALLOW` receipt;
|
|
218
|
+
- the receipt must belong to the same mission;
|
|
219
|
+
- explicit mission denies cannot be overridden by lease bindings;
|
|
220
|
+
- an exact derived resource can execute;
|
|
221
|
+
- a different resource becomes an authority delta and the effect does not run;
|
|
222
|
+
- completed and expired leases stop execution;
|
|
223
|
+
- Task Lease receipts include the lease ID and lease hash.
|
|
224
|
+
|
|
225
|
+
## Current limitations
|
|
226
|
+
|
|
227
|
+
This is still a validation implementation.
|
|
228
|
+
|
|
229
|
+
1. **Extraction trust:** the trusted host/adapter supplies the derived value and selector. Agent Authority records lineage but does not yet cryptographically prove that the selected output field contained that value.
|
|
230
|
+
2. **In-memory lease state:** TaskLease instances are currently process-local. Durable lease persistence/recovery is not implemented yet.
|
|
231
|
+
3. **Top-level binding fields:** v0.4 binds top-level request context fields only. Nested JSON-path policy is intentionally deferred.
|
|
232
|
+
4. **Step-up application:** authority deltas are surfaced but approved deltas are not yet automatically applied back into the lease.
|
|
233
|
+
5. **Adapter semantics:** providers still need trustworthy mappings from an external operation to `service`, `action`, and resource context fields.
|
|
234
|
+
|
|
235
|
+
These constraints are deliberate. The next work should be driven by real integrations rather than by adding a general policy language.
|
|
236
|
+
|
|
237
|
+
## Validation target
|
|
238
|
+
|
|
239
|
+
The product thesis is validated when the same Task Lease can safely govern a real multi-step workflow across more than one execution transport, for example:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
one human task
|
|
243
|
+
|
|
|
244
|
+
+--> ordinary SDK through guard.run()
|
|
245
|
+
|
|
|
246
|
+
+--> MCP tool through Agent Authority gateway
|
|
247
|
+
|
|
|
248
|
+
+--> brokered provider execution
|
|
249
|
+
|
|
250
|
+
same authority lineage
|
|
251
|
+
same non-amplification rule
|
|
252
|
+
same completion boundary
|
|
253
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Validate Agent Authority in 5 minutes
|
|
2
|
+
|
|
3
|
+
The goal of this validation is deliberately narrow:
|
|
4
|
+
|
|
5
|
+
> A real MCP host should only be able to call the tool + resource allowed by a human mission, while the upstream provider/tool remains unaware of Agent Authority.
|
|
6
|
+
|
|
7
|
+
This is a product-layer test, not a benchmark and not a claim of production readiness.
|
|
8
|
+
|
|
9
|
+
## What this proves
|
|
10
|
+
|
|
11
|
+
The repository includes:
|
|
12
|
+
|
|
13
|
+
- an Agent Authority MCP gateway (`aauth mcp proxy`)
|
|
14
|
+
- a tiny validation upstream with one real read-only GitHub metadata tool
|
|
15
|
+
- one fake write-capable tool that must never pass the gateway in read-only mode
|
|
16
|
+
- a canonical mission restricted to `Null-Square/agent-authority`
|
|
17
|
+
- wire-level tests using the official MCP v2 client SDK
|
|
18
|
+
|
|
19
|
+
The path is:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
MCP host
|
|
23
|
+
-> Agent Authority
|
|
24
|
+
-> mission policy
|
|
25
|
+
-> allowed read-only tool
|
|
26
|
+
-> validation MCP upstream
|
|
27
|
+
-> public GitHub API
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 1. Install and initialize
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
npm link
|
|
35
|
+
|
|
36
|
+
aauth setup --principal user:local
|
|
37
|
+
aauth doctor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 2. Run the tiny validation upstream
|
|
41
|
+
|
|
42
|
+
Terminal A:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run demo:mcp-upstream
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
It listens on `http://127.0.0.1:8791/mcp` and advertises two tools:
|
|
49
|
+
|
|
50
|
+
- `github_repo_metadata` — explicitly read-only; reads public GitHub repository metadata
|
|
51
|
+
- `dangerous_demo_write` — harmless fake mutation used only to prove the gateway blocks write-capable tools
|
|
52
|
+
|
|
53
|
+
## 3. Put Agent Authority in front of it
|
|
54
|
+
|
|
55
|
+
Terminal B:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
aauth mcp proxy \
|
|
59
|
+
--upstream http://127.0.0.1:8791/mcp \
|
|
60
|
+
--mission examples/missions/chatgpt-web-validation.json \
|
|
61
|
+
--service mcp:validation-upstream
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The gateway listens on `http://127.0.0.1:8790/mcp`.
|
|
65
|
+
|
|
66
|
+
It intentionally binds loopback only and refuses a public bind in this release.
|
|
67
|
+
|
|
68
|
+
## Expected behavior
|
|
69
|
+
|
|
70
|
+
A client connected to the Agent Authority gateway should:
|
|
71
|
+
|
|
72
|
+
1. see `github_repo_metadata`
|
|
73
|
+
2. not see the fake write-capable tool in read-only mode
|
|
74
|
+
3. succeed for:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"name": "github_repo_metadata",
|
|
79
|
+
"arguments": {
|
|
80
|
+
"repository": "Null-Square/agent-authority"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
4. be denied if it changes the repository to another value
|
|
86
|
+
5. be denied if it tries to call the write-capable tool directly even if it knows the upstream tool name
|
|
87
|
+
|
|
88
|
+
The key property is #5: hiding a tool is UX; enforcing `tools/call` is the security boundary.
|
|
89
|
+
|
|
90
|
+
## Automated proof
|
|
91
|
+
|
|
92
|
+
Run:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm test
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The MCP tests cover the gateway policy directly and use the official MCP v2 client against the actual Agent Authority handler. No external provider credentials are required.
|
|
99
|
+
|
|
100
|
+
## ChatGPT / hosted OpenAI validation
|
|
101
|
+
|
|
102
|
+
ChatGPT cannot connect directly to a localhost MCP server. OpenAI supports Secure MCP Tunnel for connecting local/private MCP servers to supported OpenAI products without exposing the server publicly.
|
|
103
|
+
|
|
104
|
+
Use the official `openai/tunnel-client` quickstart (`tunnel-client help quickstart`) and point the tunnel at:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
http://127.0.0.1:8790/mcp
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then configure the resulting tunnel-backed MCP endpoint/app in the supported OpenAI product.
|
|
111
|
+
|
|
112
|
+
Product/plan availability for custom MCP apps changes over time; if the ChatGPT workspace does not expose custom MCP app creation, the same tunnel endpoint can be validated from another supported OpenAI surface or any MCP v2 client. Do not weaken Agent Authority security merely to work around a UI/plan limitation.
|
|
113
|
+
|
|
114
|
+
## Pass/fail criterion
|
|
115
|
+
|
|
116
|
+
The validation passes only if all of these are true:
|
|
117
|
+
|
|
118
|
+
- the host discovers the authorized read-only tool
|
|
119
|
+
- the authorized repository read succeeds
|
|
120
|
+
- an out-of-mission repository is rejected before reaching the upstream tool
|
|
121
|
+
- a write-capable tool is rejected before reaching the upstream tool
|
|
122
|
+
- changing the host does not require changing the mission semantics
|
|
123
|
+
|
|
124
|
+
If those conditions hold from ChatGPT and from at least one non-OpenAI MCP client, we have useful validation that Agent Authority is the policy layer rather than an OpenAI-specific connector.
|
package/examples/demo.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { AdapterRegistry, AuthorityRuntime, descriptorAdapter } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
const mission = JSON.parse(fs.readFileSync(new URL('./mission.json', import.meta.url)));
|
|
5
|
+
const adapters = new AdapterRegistry()
|
|
6
|
+
.register(descriptorAdapter('oauth', ['github', 'google']))
|
|
7
|
+
.register(descriptorAdapter('api-key', ['cloudflare']));
|
|
8
|
+
const runtime = new AuthorityRuntime({ adapters });
|
|
9
|
+
|
|
10
|
+
for (const request of [
|
|
11
|
+
{ service: 'github', action: 'repo.write' },
|
|
12
|
+
{ service: 'github', action: 'repo.delete' },
|
|
13
|
+
{ service: 'google', action: 'gmail.send' },
|
|
14
|
+
{ service: 'cloudflare', action: 'workers.deploy' },
|
|
15
|
+
{ service: 'stripe', action: 'payment.create' }
|
|
16
|
+
]) {
|
|
17
|
+
const output = await runtime.prepare(mission, request);
|
|
18
|
+
console.log(`${request.service}:${request.action} -> ${output.result.decision}`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AuthorityRuntime } from '../src/index.js';
|
|
2
|
+
import { createAuthorityGuard } from '../src/guard.js';
|
|
3
|
+
|
|
4
|
+
const mission = {
|
|
5
|
+
version: '0.1',
|
|
6
|
+
mission_id: 'mission:direct-guard-demo',
|
|
7
|
+
principal: { id: 'user:demo' },
|
|
8
|
+
agent: { id: 'agent:demo' },
|
|
9
|
+
objective: 'Inspect only the Agent Authority public repository',
|
|
10
|
+
resources: [{
|
|
11
|
+
service: 'github',
|
|
12
|
+
allow: ['repo.read'],
|
|
13
|
+
deny: ['repo.delete', 'repo.write'],
|
|
14
|
+
constraints: { repository: ['Null-Square/agent-authority'] }
|
|
15
|
+
}]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const guard = createAuthorityGuard({
|
|
19
|
+
mission,
|
|
20
|
+
runtime: new AuthorityRuntime(),
|
|
21
|
+
onDecision: ({ result, receipt }, request) => {
|
|
22
|
+
console.log(`${result.decision.toUpperCase()} ${request.service}:${request.action} receipt=${receipt.receipt_hash}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const repository = process.argv[2] || 'Null-Square/agent-authority';
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const { output } = await guard.run({
|
|
30
|
+
service: 'github',
|
|
31
|
+
action: 'repo.read',
|
|
32
|
+
context: { repository }
|
|
33
|
+
}, async () => {
|
|
34
|
+
const response = await fetch(`https://api.github.com/repos/${repository}`, {
|
|
35
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': 'agent-authority-direct-guard-demo' }
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) throw new Error(`GitHub returned ${response.status}`);
|
|
38
|
+
const repo = await response.json();
|
|
39
|
+
return {
|
|
40
|
+
full_name: repo.full_name,
|
|
41
|
+
visibility: repo.visibility,
|
|
42
|
+
default_branch: repo.default_branch
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(JSON.stringify(output, null, 2));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`${error.name}: ${error.message}`);
|
|
49
|
+
process.exitCode = 2;
|
|
50
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { AuthorityRuntime } from '../src/index.js';
|
|
3
|
+
import { issueHarnessActionGrant, createHarnessConnectorGate } from '../src/harness-bridge.js';
|
|
4
|
+
|
|
5
|
+
const signingKey = randomBytes(32);
|
|
6
|
+
const authority = new AuthorityRuntime();
|
|
7
|
+
const gate = createHarnessConnectorGate({ key: signingKey });
|
|
8
|
+
|
|
9
|
+
const mission = {
|
|
10
|
+
version: '0.1',
|
|
11
|
+
mission_id: 'mission:hosted-harness-demo',
|
|
12
|
+
principal: { id: 'user:example' },
|
|
13
|
+
agent: { id: 'agent:hosted-harness:session-1' },
|
|
14
|
+
objective: 'Read an approved GitHub repository through a harness-managed connector',
|
|
15
|
+
resources: [
|
|
16
|
+
{
|
|
17
|
+
service: 'github',
|
|
18
|
+
allow: ['repo.read'],
|
|
19
|
+
deny: ['repo.write', 'repo.delete'],
|
|
20
|
+
constraints: { repository: ['Null-Square/agent-authority'] }
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const request = {
|
|
26
|
+
service: 'github',
|
|
27
|
+
action: 'repo.read',
|
|
28
|
+
context: { repository: 'Null-Square/agent-authority' }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 1. Agent Authority evaluates the human-approved mission.
|
|
32
|
+
const evaluation = authority.evaluate(mission, request);
|
|
33
|
+
if (evaluation.result.decision !== 'allow') {
|
|
34
|
+
console.error(evaluation.result);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Trusted authority code issues a short-lived grant for the exact request.
|
|
39
|
+
const { token: grant } = issueHarnessActionGrant({
|
|
40
|
+
key: signingKey,
|
|
41
|
+
mission,
|
|
42
|
+
request,
|
|
43
|
+
ttl_seconds: 30
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 3. This represents trusted harness connector middleware. The provider token
|
|
47
|
+
// stays inside the hosted platform; Agent Authority never needs to see it.
|
|
48
|
+
async function harnessGitHubConnector({ grant, mission, request }) {
|
|
49
|
+
gate.verify({ grant, mission, request });
|
|
50
|
+
|
|
51
|
+
// Replace this stub with the harness-owned GitHub connector. For example,
|
|
52
|
+
// a hosted platform may already have the user's GitHub OAuth connection.
|
|
53
|
+
return {
|
|
54
|
+
provider: 'github',
|
|
55
|
+
repository: request.context.repository,
|
|
56
|
+
executed_by: 'harness-managed-connector',
|
|
57
|
+
provider_credential_exposed_to_agent_authority: false
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(await harnessGitHubConnector({ grant, mission, request }));
|
|
62
|
+
|
|
63
|
+
// Changing the resource after authorization must fail verification.
|
|
64
|
+
try {
|
|
65
|
+
await harnessGitHubConnector({
|
|
66
|
+
grant,
|
|
67
|
+
mission,
|
|
68
|
+
request: { ...request, context: { repository: 'Null-Square/other-repository' } }
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.log(`blocked substitution: ${error.code}`);
|
|
72
|
+
}
|