@datalayer/agent-runtimes 1.5.45 → 1.5.46
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/dist-embed/datalayer-app-main.js +3 -3
- package/lib/portable/hosts/__tests__/fixtures/lock-with-jupyter.json +40 -0
- package/lib/portable/hosts/__tests__/fixtures/npm-ls-with-jupyter.json +21 -0
- package/lib/portable/hosts/agentClient.d.ts +167 -0
- package/lib/portable/hosts/agentClient.js +346 -0
- package/lib/portable/hosts/approvalRenderers.d.ts +43 -0
- package/lib/portable/hosts/approvalRenderers.js +180 -0
- package/lib/portable/hosts/client.d.ts +95 -0
- package/lib/portable/hosts/client.js +174 -0
- package/lib/portable/hosts/contracts.d.ts +199 -0
- package/lib/portable/hosts/contracts.js +235 -0
- package/lib/portable/hosts/contracts.json +381 -0
- package/lib/portable/hosts/deadLetters.d.ts +41 -0
- package/lib/portable/hosts/deadLetters.js +124 -0
- package/lib/portable/hosts/deliveries.d.ts +76 -0
- package/lib/portable/hosts/deliveries.js +147 -0
- package/lib/portable/hosts/forbiddenGraph.d.ts +77 -0
- package/lib/portable/hosts/forbiddenGraph.js +174 -0
- package/lib/portable/hosts/health.d.ts +35 -0
- package/lib/portable/hosts/health.js +92 -0
- package/lib/portable/hosts/index.d.ts +40 -0
- package/lib/portable/hosts/index.js +45 -0
- package/lib/portable/hosts/observability.d.ts +67 -0
- package/lib/portable/hosts/observability.js +171 -0
- package/lib/portable/hosts/postgresQueue.d.ts +55 -0
- package/lib/portable/hosts/postgresQueue.js +217 -0
- package/lib/portable/hosts/queue.d.ts +47 -0
- package/lib/portable/hosts/queue.js +63 -0
- package/lib/portable/hosts/quotas.d.ts +81 -0
- package/lib/portable/hosts/quotas.js +147 -0
- package/lib/portable/hosts/rateLimit.d.ts +13 -0
- package/lib/portable/hosts/rateLimit.js +8 -0
- package/lib/portable/hosts/resolution.d.ts +54 -0
- package/lib/portable/hosts/resolution.js +46 -0
- package/lib/portable/hosts/schema.d.ts +19 -0
- package/lib/portable/hosts/schema.js +81 -0
- package/lib/portable/hosts/secretCipher.d.ts +23 -0
- package/lib/portable/hosts/secretCipher.js +45 -0
- package/lib/portable/hosts/sessionUid.d.ts +7 -0
- package/lib/portable/hosts/sessionUid.js +28 -0
- package/lib/portable/hosts/turnState.d.ts +45 -0
- package/lib/portable/hosts/turnState.js +113 -0
- package/lib/portable/index.d.ts +2 -0
- package/lib/portable/index.js +3 -0
- package/lib/portable/semantic-events.json +480 -0
- package/lib/portable/semantic.d.ts +78 -0
- package/lib/portable/semantic.js +100 -0
- package/lib/portable/semanticOfAgUi.d.ts +41 -0
- package/lib/portable/semanticOfAgUi.js +108 -0
- package/lib/protocols/A2AAdapter.js +19 -0
- package/lib/test-setup.js +17 -14
- package/package.json +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/**
|
|
8
|
+
* The generic Datalayer host contracts (plans/SLACK.md §4.3, plans/DISCORD.md
|
|
9
|
+
* §4.3), as ai-agents answers them, for every chat host (Slack, Discord).
|
|
10
|
+
* These shapes are shared with ai-agents through `contracts.json`, beside
|
|
11
|
+
* this file and byte-identical to ai-agents' `tests/fixtures/hosts/`, which
|
|
12
|
+
* both sides test. A delivery's `target` stays opaque here: each host
|
|
13
|
+
* validates its own.
|
|
14
|
+
*
|
|
15
|
+
* @module portable/hosts/contracts
|
|
16
|
+
*/
|
|
17
|
+
export const HOST_KEY_HEADER = 'X-Datalayer-Host-Key';
|
|
18
|
+
export const SIGNATURE_HEADER = 'X-Datalayer-Signature';
|
|
19
|
+
export const TIMESTAMP_HEADER = 'X-Datalayer-Timestamp';
|
|
20
|
+
export const DELIVERY_HEADER = 'X-Datalayer-Delivery';
|
|
21
|
+
export const IDEMPOTENCY_HEADER = 'Idempotency-Key';
|
|
22
|
+
/** The presentation version the hosts render; another is reviewed in Datalayer. */
|
|
23
|
+
export const PRESENTATION_VERSION = 1;
|
|
24
|
+
export const bindSchema = z.object({
|
|
25
|
+
host: z
|
|
26
|
+
.object({
|
|
27
|
+
uid: z.string().min(1),
|
|
28
|
+
state: z.string(),
|
|
29
|
+
name: z.string().optional(),
|
|
30
|
+
default_deployment: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.loose(),
|
|
33
|
+
host_key: z.string().startsWith('dlhk_'),
|
|
34
|
+
});
|
|
35
|
+
/** What a link code is redeemed at: the landing's confirm page. */
|
|
36
|
+
export const LINK_PAGE_PATH = '/hosts/link';
|
|
37
|
+
/**
|
|
38
|
+
* `GET /apps/hosts/self`: the installation with its allowed deployments.
|
|
39
|
+
* `channels`, `direct_only` and `paused` are the installation's channel
|
|
40
|
+
* policy and pause (S-104, S-105, S-403).
|
|
41
|
+
*/
|
|
42
|
+
export const selfSchema = z.object({
|
|
43
|
+
host: z
|
|
44
|
+
.object({
|
|
45
|
+
uid: z.string().min(1),
|
|
46
|
+
account_uid: z.string().optional(),
|
|
47
|
+
default_deployment: z.string().optional(),
|
|
48
|
+
deployments: z.array(z
|
|
49
|
+
.object({
|
|
50
|
+
uid: z.string().min(1),
|
|
51
|
+
app_name: z.string().optional(),
|
|
52
|
+
kept: z.boolean().optional(),
|
|
53
|
+
})
|
|
54
|
+
.loose()),
|
|
55
|
+
channels: z
|
|
56
|
+
.array(z.object({
|
|
57
|
+
id: z.string().min(1),
|
|
58
|
+
state: z.enum(['allow', 'deny']),
|
|
59
|
+
deployment: z.string().optional(),
|
|
60
|
+
}))
|
|
61
|
+
.optional(),
|
|
62
|
+
direct_only: z.array(z.string()).optional(),
|
|
63
|
+
paused: z.boolean().optional(),
|
|
64
|
+
})
|
|
65
|
+
.loose(),
|
|
66
|
+
});
|
|
67
|
+
/** `POST /apps/hosts/handoffs`: a one-use code that opens one session or approval (S-305). */
|
|
68
|
+
export const handoffSchema = z.object({
|
|
69
|
+
code: z.string().startsWith('dlho_'),
|
|
70
|
+
expires_in: z.number().int().nonnegative(),
|
|
71
|
+
path: z.string().startsWith('/hosts/open?code='),
|
|
72
|
+
});
|
|
73
|
+
export const linkStateSchema = z.object({
|
|
74
|
+
linked: z.boolean(),
|
|
75
|
+
principal_uid: z.string().optional(),
|
|
76
|
+
});
|
|
77
|
+
export const webhookSchema = z.object({
|
|
78
|
+
url: z.url(),
|
|
79
|
+
events: z.array(z.string()),
|
|
80
|
+
version: z.number().int(),
|
|
81
|
+
signing_secret: z.string().min(32),
|
|
82
|
+
});
|
|
83
|
+
/** One delivery of the outbox, as `GET …/self/deliveries` lists it. */
|
|
84
|
+
export const outboxItemSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
uid: z.string().min(1),
|
|
87
|
+
event: z.string(),
|
|
88
|
+
payload: z.record(z.string(), z.unknown()),
|
|
89
|
+
state: z.string(),
|
|
90
|
+
})
|
|
91
|
+
.loose();
|
|
92
|
+
export const decisionSchema = z.object({
|
|
93
|
+
approval: z
|
|
94
|
+
.object({ id: z.string().nullable().optional(), status: z.string() })
|
|
95
|
+
.loose(),
|
|
96
|
+
});
|
|
97
|
+
export const linkCodeSchema = z.object({
|
|
98
|
+
code: z.string().startsWith('dlhc_'),
|
|
99
|
+
expires_in: z.number().int().nonnegative(),
|
|
100
|
+
confirm: z.string(),
|
|
101
|
+
});
|
|
102
|
+
export const exchangeSchema = z.object({
|
|
103
|
+
access_token: z.string().min(1),
|
|
104
|
+
token_type: z.literal('Bearer'),
|
|
105
|
+
expires_in: z.number().int().nonnegative(),
|
|
106
|
+
exp: z.number().int(),
|
|
107
|
+
principal_uid: z.string().min(1),
|
|
108
|
+
host_uid: z.string().min(1),
|
|
109
|
+
deployment: z.object({
|
|
110
|
+
uid: z.string().min(1),
|
|
111
|
+
app_uid: z.string(),
|
|
112
|
+
app_name: z.string(),
|
|
113
|
+
version: z.number().int(),
|
|
114
|
+
}),
|
|
115
|
+
session_api: z.object({ url: z.url(), agent: z.string() }),
|
|
116
|
+
user_token: z.string().optional(),
|
|
117
|
+
});
|
|
118
|
+
const fieldSchema = z.object({
|
|
119
|
+
name: z.string(),
|
|
120
|
+
label: z.string(),
|
|
121
|
+
value: z.string(),
|
|
122
|
+
});
|
|
123
|
+
export const presentationSchema = z.object({
|
|
124
|
+
version: z.number().int(),
|
|
125
|
+
approval_id: z.string().min(1),
|
|
126
|
+
status: z.string(),
|
|
127
|
+
owner_uid: z.string(),
|
|
128
|
+
tool_name: z.string(),
|
|
129
|
+
action_class: z.enum([
|
|
130
|
+
'read',
|
|
131
|
+
'send',
|
|
132
|
+
'write',
|
|
133
|
+
'execute',
|
|
134
|
+
'publish',
|
|
135
|
+
'delete',
|
|
136
|
+
'financial',
|
|
137
|
+
'unknown',
|
|
138
|
+
]),
|
|
139
|
+
session_uid: z.string(),
|
|
140
|
+
created_at: z.string().nullable().optional(),
|
|
141
|
+
expires_at: z.string().nullable().optional(),
|
|
142
|
+
fields: z.array(fieldSchema),
|
|
143
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
144
|
+
secret_fields: z.array(z.string()),
|
|
145
|
+
complete: z.boolean(),
|
|
146
|
+
host_may_decide: z.boolean(),
|
|
147
|
+
reasons: z.array(z.string()),
|
|
148
|
+
digest: z.string().regex(/^[0-9a-f]{64}$/),
|
|
149
|
+
});
|
|
150
|
+
export const deliverySchema = z.discriminatedUnion('event', [
|
|
151
|
+
z.object({
|
|
152
|
+
delivery: z.string().min(1),
|
|
153
|
+
event: z.literal('approval.requested'),
|
|
154
|
+
payload: z
|
|
155
|
+
.object({
|
|
156
|
+
approval_id: z.string().min(1),
|
|
157
|
+
tool_name: z.string(),
|
|
158
|
+
presentation: z.string(),
|
|
159
|
+
session_uid: z.string(),
|
|
160
|
+
deployment_uid: z.string(),
|
|
161
|
+
target: z.record(z.string(), z.unknown()),
|
|
162
|
+
})
|
|
163
|
+
.loose(),
|
|
164
|
+
}),
|
|
165
|
+
z.object({
|
|
166
|
+
delivery: z.string().min(1),
|
|
167
|
+
event: z.literal('approval.decided'),
|
|
168
|
+
payload: z
|
|
169
|
+
.object({
|
|
170
|
+
approval_id: z.string().min(1),
|
|
171
|
+
status: z.string(),
|
|
172
|
+
decided_by: z.string(),
|
|
173
|
+
session_uid: z.string(),
|
|
174
|
+
deployment_uid: z.string(),
|
|
175
|
+
target: z.record(z.string(), z.unknown()),
|
|
176
|
+
})
|
|
177
|
+
.loose(),
|
|
178
|
+
}),
|
|
179
|
+
z.object({
|
|
180
|
+
delivery: z.string().min(1),
|
|
181
|
+
event: z.literal('turn.completed'),
|
|
182
|
+
payload: z
|
|
183
|
+
.object({
|
|
184
|
+
record_entry: z.string(),
|
|
185
|
+
session_uid: z.string(),
|
|
186
|
+
deployment_uid: z.string(),
|
|
187
|
+
target: z.record(z.string(), z.unknown()),
|
|
188
|
+
})
|
|
189
|
+
.loose(),
|
|
190
|
+
}),
|
|
191
|
+
]);
|
|
192
|
+
/** An outbox item read as the delivery a push would have carried. */
|
|
193
|
+
export function deliveryOfOutbox(item) {
|
|
194
|
+
const { uid, event, payload } = outboxItemSchema.parse(item);
|
|
195
|
+
return deliverySchema.parse({ delivery: uid, event, payload });
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether a host may offer Approve and Reject: only what the server says a
|
|
199
|
+
* host may decide, in a version the hosts render, shown whole, pending, with
|
|
200
|
+
* no secret field. Anything else gets *Review in Datalayer* (SLACK §2 rule 6,
|
|
201
|
+
* DISCORD §2 rule 6). Each host also checks the card fits its own limits.
|
|
202
|
+
*/
|
|
203
|
+
export function hostMayDecide(presentation) {
|
|
204
|
+
return (presentation.version === PRESENTATION_VERSION &&
|
|
205
|
+
presentation.host_may_decide &&
|
|
206
|
+
presentation.complete &&
|
|
207
|
+
presentation.secret_fields.length === 0 &&
|
|
208
|
+
presentation.status === 'pending');
|
|
209
|
+
}
|
|
210
|
+
/** `v1=` HMAC-SHA256 of `"<timestamp>.<raw body>"`, as ai-agents signs a push. */
|
|
211
|
+
export function deliverySignature(secret, timestamp, rawBody) {
|
|
212
|
+
const mac = createHmac('sha256', secret);
|
|
213
|
+
mac.update(`${timestamp}.`);
|
|
214
|
+
mac.update(rawBody);
|
|
215
|
+
return `v1=${mac.digest('hex')}`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Verify a pushed delivery before trusting it: its signature in constant
|
|
219
|
+
* time, and a timestamp within five minutes, so a captured push cannot be
|
|
220
|
+
* replayed later.
|
|
221
|
+
*/
|
|
222
|
+
export function verifyDelivery(input) {
|
|
223
|
+
const stamp = Number(input.timestamp);
|
|
224
|
+
const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
225
|
+
if (!Number.isInteger(stamp) ||
|
|
226
|
+
Math.abs(now - stamp) > (input.toleranceSeconds ?? 300)) {
|
|
227
|
+
throw new Error("The delivery's timestamp is missing or too old.");
|
|
228
|
+
}
|
|
229
|
+
const expected = Buffer.from(deliverySignature(input.secret, stamp, input.rawBody));
|
|
230
|
+
const given = Buffer.from(input.signature ?? '');
|
|
231
|
+
if (expected.length !== given.length || !timingSafeEqual(expected, given)) {
|
|
232
|
+
throw new Error("The delivery's signature does not match.");
|
|
233
|
+
}
|
|
234
|
+
return deliverySchema.parse(JSON.parse(input.rawBody.toString()));
|
|
235
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"about": "The generic host contracts of plans/SLACK.md §4.3, as ai-agents and the runtime's session API answer them. Shared verbatim by ai-agents (tests/fixtures/hosts) and the Slack adapter (tech/datalayer/slack, test/contracts): each side's tests check its half against this file.",
|
|
4
|
+
"headers": {
|
|
5
|
+
"host_key": "X-Datalayer-Host-Key",
|
|
6
|
+
"signature": "X-Datalayer-Signature",
|
|
7
|
+
"timestamp": "X-Datalayer-Timestamp",
|
|
8
|
+
"delivery": "X-Datalayer-Delivery",
|
|
9
|
+
"idempotency_key": "Idempotency-Key"
|
|
10
|
+
},
|
|
11
|
+
"routes": {
|
|
12
|
+
"bind": "POST /api/ai-agents/v1/apps/hosts/bind",
|
|
13
|
+
"self": "GET /api/ai-agents/v1/apps/hosts/self",
|
|
14
|
+
"uninstalled": "DELETE /api/ai-agents/v1/apps/hosts/self",
|
|
15
|
+
"link_code": "POST /api/ai-agents/v1/apps/hosts/self/link-codes",
|
|
16
|
+
"link_state": "GET /api/ai-agents/v1/apps/hosts/self/links/{external_user_id}",
|
|
17
|
+
"unlink": "DELETE /api/ai-agents/v1/apps/hosts/self/links/{external_user_id}",
|
|
18
|
+
"exchange": "POST /api/ai-agents/v1/apps/hosts/self/token",
|
|
19
|
+
"webhook": "PUT /api/ai-agents/v1/apps/hosts/self/delivery",
|
|
20
|
+
"outbox": "GET /api/ai-agents/v1/apps/hosts/self/deliveries",
|
|
21
|
+
"ack": "POST /api/ai-agents/v1/apps/hosts/self/deliveries/{delivery_uid}/ack",
|
|
22
|
+
"bind_session": "PUT /api/ai-agents/v1/apps/hosts/sessions/{session_uid}",
|
|
23
|
+
"whoami": "GET /api/ai-agents/v1/apps/hosts/whoami",
|
|
24
|
+
"presentation": "GET /api/ai-agents/v1/tool-approvals/{approval_id}/presentation",
|
|
25
|
+
"decision": "POST /api/ai-agents/v1/apps/hosts/approvals/{approval_id}/decision",
|
|
26
|
+
"start_session": "POST {session_api.url}",
|
|
27
|
+
"send_message": "POST {session_api.url}/{session_uid}/messages",
|
|
28
|
+
"confirm_link": "POST /api/ai-agents/v1/apps/hosts/links",
|
|
29
|
+
"preview_link": "GET /api/ai-agents/v1/apps/hosts/links/preview?code={code}",
|
|
30
|
+
"change_host": "PATCH /api/ai-agents/v1/apps/hosts/{uid}",
|
|
31
|
+
"handoff": "POST /api/ai-agents/v1/apps/hosts/handoffs",
|
|
32
|
+
"redeem_handoff": "POST /api/ai-agents/v1/apps/hosts/handoffs/redeem"
|
|
33
|
+
},
|
|
34
|
+
"bind": {
|
|
35
|
+
"request": {
|
|
36
|
+
"code": "dlhc_…",
|
|
37
|
+
"external_id": "A0123:-:T0123",
|
|
38
|
+
"name": "Acme"
|
|
39
|
+
},
|
|
40
|
+
"response_keys": [
|
|
41
|
+
"success",
|
|
42
|
+
"host",
|
|
43
|
+
"host_key"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"link_code": {
|
|
47
|
+
"request": {
|
|
48
|
+
"external_user_id": "U0ALICE",
|
|
49
|
+
"external_user_name": "Alice"
|
|
50
|
+
},
|
|
51
|
+
"response_keys": [
|
|
52
|
+
"success",
|
|
53
|
+
"code",
|
|
54
|
+
"expires_in",
|
|
55
|
+
"confirm"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"exchange": {
|
|
59
|
+
"request": {
|
|
60
|
+
"external_user_id": "U0ALICE",
|
|
61
|
+
"deployment_uid": "dep-1"
|
|
62
|
+
},
|
|
63
|
+
"response_keys": [
|
|
64
|
+
"success",
|
|
65
|
+
"access_token",
|
|
66
|
+
"token_type",
|
|
67
|
+
"expires_in",
|
|
68
|
+
"exp",
|
|
69
|
+
"principal_uid",
|
|
70
|
+
"host_uid",
|
|
71
|
+
"deployment",
|
|
72
|
+
"session_api"
|
|
73
|
+
],
|
|
74
|
+
"deployment_keys": [
|
|
75
|
+
"uid",
|
|
76
|
+
"app_uid",
|
|
77
|
+
"app_name",
|
|
78
|
+
"version"
|
|
79
|
+
],
|
|
80
|
+
"session_api_keys": [
|
|
81
|
+
"url",
|
|
82
|
+
"agent"
|
|
83
|
+
],
|
|
84
|
+
"optional_keys": [
|
|
85
|
+
"user_token"
|
|
86
|
+
],
|
|
87
|
+
"token_claims": [
|
|
88
|
+
"jti",
|
|
89
|
+
"iss",
|
|
90
|
+
"iat",
|
|
91
|
+
"exp",
|
|
92
|
+
"aud",
|
|
93
|
+
"sub",
|
|
94
|
+
"azp",
|
|
95
|
+
"host_kind",
|
|
96
|
+
"link",
|
|
97
|
+
"deployment_uid",
|
|
98
|
+
"app_uid",
|
|
99
|
+
"scope"
|
|
100
|
+
],
|
|
101
|
+
"audience": "datalayer:app:host",
|
|
102
|
+
"max_ttl_seconds": 900,
|
|
103
|
+
"optional_request": {
|
|
104
|
+
"place": {
|
|
105
|
+
"kind": "channel",
|
|
106
|
+
"id": "C0"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"place_kinds": [
|
|
110
|
+
"channel",
|
|
111
|
+
"direct"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"whoami": {
|
|
115
|
+
"response_keys": [
|
|
116
|
+
"success",
|
|
117
|
+
"sub",
|
|
118
|
+
"azp",
|
|
119
|
+
"host_kind",
|
|
120
|
+
"deployment_uid",
|
|
121
|
+
"app_uid",
|
|
122
|
+
"link",
|
|
123
|
+
"scope",
|
|
124
|
+
"expires_in"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"external_turn": {
|
|
128
|
+
"start_request": {
|
|
129
|
+
"agent": "agent-desk",
|
|
130
|
+
"deployment_uid": "dep-1",
|
|
131
|
+
"opener": "Summarize the launch risks",
|
|
132
|
+
"session": "slack-T0-C0-1700000000-000001",
|
|
133
|
+
"external_event_id": "Ev0123",
|
|
134
|
+
"user_token": ""
|
|
135
|
+
},
|
|
136
|
+
"message_request": {
|
|
137
|
+
"text": "And the costs?",
|
|
138
|
+
"external_event_id": "Ev0124"
|
|
139
|
+
},
|
|
140
|
+
"turn_event": {
|
|
141
|
+
"type": "CUSTOM",
|
|
142
|
+
"name": "loop.turn",
|
|
143
|
+
"value_keys": [
|
|
144
|
+
"turn",
|
|
145
|
+
"external_event_id",
|
|
146
|
+
"replayed",
|
|
147
|
+
"state"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"replay": "The same Idempotency-Key (or, without one, the same external_event_id) and the same request answer the original session and turn with replayed=true, and run nothing; the same key with another request is 409."
|
|
151
|
+
},
|
|
152
|
+
"webhook": {
|
|
153
|
+
"request": {
|
|
154
|
+
"url": "https://slack.example/datalayer/deliveries",
|
|
155
|
+
"events": [
|
|
156
|
+
"approval.requested",
|
|
157
|
+
"approval.decided",
|
|
158
|
+
"turn.completed"
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
"response_keys": [
|
|
162
|
+
"success",
|
|
163
|
+
"url",
|
|
164
|
+
"events",
|
|
165
|
+
"version",
|
|
166
|
+
"signing_secret"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
"delivery": {
|
|
170
|
+
"events": [
|
|
171
|
+
"approval.requested",
|
|
172
|
+
"approval.decided",
|
|
173
|
+
"turn.completed"
|
|
174
|
+
],
|
|
175
|
+
"payload_keys": {
|
|
176
|
+
"approval.requested": [
|
|
177
|
+
"approval_id",
|
|
178
|
+
"tool_name",
|
|
179
|
+
"presentation",
|
|
180
|
+
"session_uid",
|
|
181
|
+
"deployment_uid",
|
|
182
|
+
"target"
|
|
183
|
+
],
|
|
184
|
+
"approval.decided": [
|
|
185
|
+
"approval_id",
|
|
186
|
+
"status",
|
|
187
|
+
"decided_by",
|
|
188
|
+
"session_uid",
|
|
189
|
+
"deployment_uid",
|
|
190
|
+
"target"
|
|
191
|
+
],
|
|
192
|
+
"turn.completed": [
|
|
193
|
+
"record_entry",
|
|
194
|
+
"session_uid",
|
|
195
|
+
"deployment_uid",
|
|
196
|
+
"target"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
"signing": {
|
|
200
|
+
"secret": "contract-signing-secret",
|
|
201
|
+
"timestamp": 1700000000,
|
|
202
|
+
"body": "{\"delivery\":\"d0c1\",\"event\":\"approval.requested\",\"payload\":{\"approval_id\":\"ap-1\",\"tool_name\":\"send_email\",\"presentation\":\"/api/ai-agents/v1/tool-approvals/ap-1/presentation\",\"session_uid\":\"slack-T0-C0-1700000000-000001\",\"deployment_uid\":\"dep-1\",\"target\":{\"channel\":\"C0\",\"thread_ts\":\"1700000000.000001\"}}}",
|
|
203
|
+
"signature": "v1=35630034244d3edab52b937cf4437542ec87958014351d587806b267f805b05f",
|
|
204
|
+
"rule": "v1= HMAC-SHA256(secret, \"<timestamp>.<raw body>\") in hex"
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"presentation": {
|
|
208
|
+
"version": 1,
|
|
209
|
+
"response_keys": [
|
|
210
|
+
"success",
|
|
211
|
+
"presentation"
|
|
212
|
+
],
|
|
213
|
+
"keys": [
|
|
214
|
+
"version",
|
|
215
|
+
"approval_id",
|
|
216
|
+
"status",
|
|
217
|
+
"owner_uid",
|
|
218
|
+
"tool_name",
|
|
219
|
+
"action_class",
|
|
220
|
+
"session_uid",
|
|
221
|
+
"created_at",
|
|
222
|
+
"expires_at",
|
|
223
|
+
"fields",
|
|
224
|
+
"arguments",
|
|
225
|
+
"secret_fields",
|
|
226
|
+
"complete",
|
|
227
|
+
"host_may_decide",
|
|
228
|
+
"reasons",
|
|
229
|
+
"digest"
|
|
230
|
+
],
|
|
231
|
+
"action_classes": [
|
|
232
|
+
"read",
|
|
233
|
+
"send",
|
|
234
|
+
"write",
|
|
235
|
+
"execute",
|
|
236
|
+
"publish",
|
|
237
|
+
"delete",
|
|
238
|
+
"financial",
|
|
239
|
+
"unknown"
|
|
240
|
+
],
|
|
241
|
+
"host_decidable": [
|
|
242
|
+
"read",
|
|
243
|
+
"send",
|
|
244
|
+
"write"
|
|
245
|
+
],
|
|
246
|
+
"example": {
|
|
247
|
+
"version": 1,
|
|
248
|
+
"approval_id": "ap-1",
|
|
249
|
+
"status": "pending",
|
|
250
|
+
"owner_uid": "alice",
|
|
251
|
+
"tool_name": "send_email",
|
|
252
|
+
"action_class": "send",
|
|
253
|
+
"session_uid": "slack-T0-C0-1700000000-000001",
|
|
254
|
+
"created_at": "2026-10-07T10:00:00Z",
|
|
255
|
+
"expires_at": null,
|
|
256
|
+
"fields": [
|
|
257
|
+
{
|
|
258
|
+
"name": "to",
|
|
259
|
+
"label": "To",
|
|
260
|
+
"value": "release@example.com"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "subject",
|
|
264
|
+
"label": "Subject",
|
|
265
|
+
"value": "Launch decision"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "body",
|
|
269
|
+
"label": "Body",
|
|
270
|
+
"value": "We go with option B."
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"arguments": {
|
|
274
|
+
"to": "release@example.com",
|
|
275
|
+
"subject": "Launch decision",
|
|
276
|
+
"body": "We go with option B."
|
|
277
|
+
},
|
|
278
|
+
"secret_fields": [],
|
|
279
|
+
"complete": true,
|
|
280
|
+
"host_may_decide": true,
|
|
281
|
+
"reasons": [],
|
|
282
|
+
"digest": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
"decision": {
|
|
286
|
+
"request": {
|
|
287
|
+
"approve": true,
|
|
288
|
+
"digest": "<presentation.digest>",
|
|
289
|
+
"note": null
|
|
290
|
+
},
|
|
291
|
+
"response_keys": [
|
|
292
|
+
"success",
|
|
293
|
+
"approval"
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
"self": {
|
|
297
|
+
"host_keys": [
|
|
298
|
+
"uid",
|
|
299
|
+
"state",
|
|
300
|
+
"deployments",
|
|
301
|
+
"default_deployment",
|
|
302
|
+
"channels",
|
|
303
|
+
"direct_only",
|
|
304
|
+
"paused"
|
|
305
|
+
],
|
|
306
|
+
"deployment_keys": [
|
|
307
|
+
"uid",
|
|
308
|
+
"app_uid",
|
|
309
|
+
"app_name",
|
|
310
|
+
"state",
|
|
311
|
+
"kept"
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
"preview": {
|
|
315
|
+
"response_keys": [
|
|
316
|
+
"success",
|
|
317
|
+
"host",
|
|
318
|
+
"deployments",
|
|
319
|
+
"external_user_id",
|
|
320
|
+
"external_user_name",
|
|
321
|
+
"expires_in"
|
|
322
|
+
],
|
|
323
|
+
"deployment_keys": [
|
|
324
|
+
"uid",
|
|
325
|
+
"app_name",
|
|
326
|
+
"default"
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"change_host": {
|
|
330
|
+
"request": {
|
|
331
|
+
"channels": [
|
|
332
|
+
{
|
|
333
|
+
"id": "C0",
|
|
334
|
+
"state": "deny"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "C1",
|
|
338
|
+
"state": "allow",
|
|
339
|
+
"deployment": "dep-1"
|
|
340
|
+
}
|
|
341
|
+
],
|
|
342
|
+
"direct_only": [
|
|
343
|
+
"dep-1"
|
|
344
|
+
],
|
|
345
|
+
"paused": false
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
"handoff": {
|
|
349
|
+
"request": {
|
|
350
|
+
"kind": "approval",
|
|
351
|
+
"target": "ap-1"
|
|
352
|
+
},
|
|
353
|
+
"kinds": [
|
|
354
|
+
"session",
|
|
355
|
+
"approval"
|
|
356
|
+
],
|
|
357
|
+
"response_keys": [
|
|
358
|
+
"success",
|
|
359
|
+
"code",
|
|
360
|
+
"expires_in",
|
|
361
|
+
"path"
|
|
362
|
+
],
|
|
363
|
+
"code_prefix": "dlho_",
|
|
364
|
+
"redeem_request": {
|
|
365
|
+
"code": "dlho_…"
|
|
366
|
+
},
|
|
367
|
+
"redeem_response_keys": [
|
|
368
|
+
"success",
|
|
369
|
+
"kind",
|
|
370
|
+
"target",
|
|
371
|
+
"deployment_uid",
|
|
372
|
+
"path"
|
|
373
|
+
],
|
|
374
|
+
"refusals": {
|
|
375
|
+
"unauthenticated": 401,
|
|
376
|
+
"another_account": 403,
|
|
377
|
+
"expired": 410,
|
|
378
|
+
"spent_or_unknown": 404
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Logger } from './observability';
|
|
2
|
+
import { PostgresWorkQueue } from './postgresQueue';
|
|
3
|
+
import { type ConnectionPool } from './queue';
|
|
4
|
+
export type DeadLetter = Readonly<{
|
|
5
|
+
id: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
attempts: number;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
/** The last error, redacted: a credential in an error message is not shown. */
|
|
10
|
+
lastError: string;
|
|
11
|
+
/** sha256 of the payload: tells two jobs apart without showing either. */
|
|
12
|
+
payloadHash: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function payloadHash(payload: unknown): string;
|
|
15
|
+
export declare class DeadLetters {
|
|
16
|
+
#private;
|
|
17
|
+
readonly database: ConnectionPool;
|
|
18
|
+
readonly queue: PostgresWorkQueue<unknown>;
|
|
19
|
+
readonly logger: Logger;
|
|
20
|
+
constructor(database: ConnectionPool, queue: PostgresWorkQueue<unknown>, logger?: Logger);
|
|
21
|
+
list(limit?: number): Promise<DeadLetter[]>;
|
|
22
|
+
replay(id: string, operator: string): Promise<boolean>;
|
|
23
|
+
/** A payload, for a stated reason, recorded and audited; secrets still redacted. */
|
|
24
|
+
payload(id: string, operator: string, reason: string): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
/** The dead letters of a host's queue (`<prefix>_jobs`). */
|
|
27
|
+
export declare function deadLetters(database: ConnectionPool, tablePrefix: string, logger?: Logger): DeadLetters;
|
|
28
|
+
/** What the dead-letters bin prints or says, and its exit code. */
|
|
29
|
+
export type CliOutput = {
|
|
30
|
+
table: (rows: unknown) => void;
|
|
31
|
+
info: (line: string) => void;
|
|
32
|
+
error: (line: string) => void;
|
|
33
|
+
};
|
|
34
|
+
export declare const DEAD_LETTERS_USAGE = "Usage: dead-letters list | replay <id> | payload <id> <reason>";
|
|
35
|
+
/**
|
|
36
|
+
* `dead-letters list | replay <id> | payload <id> <reason…>`: metadata and
|
|
37
|
+
* hashes by default (S-405). Who acts is said in `operator`
|
|
38
|
+
* (`DATALAYER_OPERATOR`), never taken from the container's user: a replay
|
|
39
|
+
* and a payload read are kept with this name. Answers the exit code.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runDeadLettersCli(letters: DeadLetters, argv: readonly string[], operator: string, out?: CliOutput): Promise<number>;
|