@datalayer/agent-runtimes 1.5.44 → 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.
Files changed (69) hide show
  1. package/dist-embed/chunks/{ChatView-kCmBbnDa.js → ChatView-Dy-IgGFL.js} +1 -1
  2. package/dist-embed/datalayer-app-main.js +99 -94
  3. package/lib/apps/plugins/chat/ChatView.js +2 -2
  4. package/lib/chat/ChatFloating.js +1 -1
  5. package/lib/chat/base/ChatBase.js +23 -8
  6. package/lib/chat/messages/ChatMessageList.d.ts +7 -1
  7. package/lib/chat/messages/ChatMessageList.js +8 -2
  8. package/lib/portable/hosts/__tests__/fixtures/lock-with-jupyter.json +40 -0
  9. package/lib/portable/hosts/__tests__/fixtures/npm-ls-with-jupyter.json +21 -0
  10. package/lib/portable/hosts/agentClient.d.ts +167 -0
  11. package/lib/portable/hosts/agentClient.js +346 -0
  12. package/lib/portable/hosts/approvalRenderers.d.ts +43 -0
  13. package/lib/portable/hosts/approvalRenderers.js +180 -0
  14. package/lib/portable/hosts/client.d.ts +95 -0
  15. package/lib/portable/hosts/client.js +174 -0
  16. package/lib/portable/hosts/contracts.d.ts +199 -0
  17. package/lib/portable/hosts/contracts.js +235 -0
  18. package/lib/portable/hosts/contracts.json +381 -0
  19. package/lib/portable/hosts/deadLetters.d.ts +41 -0
  20. package/lib/portable/hosts/deadLetters.js +124 -0
  21. package/lib/portable/hosts/deliveries.d.ts +76 -0
  22. package/lib/portable/hosts/deliveries.js +147 -0
  23. package/lib/portable/hosts/forbiddenGraph.d.ts +77 -0
  24. package/lib/portable/hosts/forbiddenGraph.js +174 -0
  25. package/lib/portable/hosts/health.d.ts +35 -0
  26. package/lib/portable/hosts/health.js +92 -0
  27. package/lib/portable/hosts/index.d.ts +40 -0
  28. package/lib/portable/hosts/index.js +45 -0
  29. package/lib/portable/hosts/observability.d.ts +67 -0
  30. package/lib/portable/hosts/observability.js +171 -0
  31. package/lib/portable/hosts/postgresQueue.d.ts +55 -0
  32. package/lib/portable/hosts/postgresQueue.js +217 -0
  33. package/lib/portable/hosts/queue.d.ts +47 -0
  34. package/lib/portable/hosts/queue.js +63 -0
  35. package/lib/portable/hosts/quotas.d.ts +81 -0
  36. package/lib/portable/hosts/quotas.js +147 -0
  37. package/lib/portable/hosts/rateLimit.d.ts +13 -0
  38. package/lib/portable/hosts/rateLimit.js +8 -0
  39. package/lib/portable/hosts/resolution.d.ts +54 -0
  40. package/lib/portable/hosts/resolution.js +46 -0
  41. package/lib/portable/hosts/schema.d.ts +19 -0
  42. package/lib/portable/hosts/schema.js +81 -0
  43. package/lib/portable/hosts/secretCipher.d.ts +23 -0
  44. package/lib/portable/hosts/secretCipher.js +45 -0
  45. package/lib/portable/hosts/sessionUid.d.ts +7 -0
  46. package/lib/portable/hosts/sessionUid.js +28 -0
  47. package/lib/portable/hosts/turnState.d.ts +45 -0
  48. package/lib/portable/hosts/turnState.js +113 -0
  49. package/lib/portable/index.d.ts +2 -0
  50. package/lib/portable/index.js +3 -0
  51. package/lib/portable/semantic-events.json +480 -0
  52. package/lib/portable/semantic.d.ts +78 -0
  53. package/lib/portable/semantic.js +100 -0
  54. package/lib/portable/semanticOfAgUi.d.ts +41 -0
  55. package/lib/portable/semanticOfAgUi.js +108 -0
  56. package/lib/protocols/A2AAdapter.js +19 -0
  57. package/lib/protocols/VercelAIAdapter.js +11 -0
  58. package/lib/specs/voices.js +62 -0
  59. package/lib/test-setup.js +17 -14
  60. package/lib/types/chat.d.ts +10 -0
  61. package/lib/voice/ComposerVoice.js +12 -3
  62. package/lib/voice/captions.d.ts +31 -0
  63. package/lib/voice/captions.js +156 -0
  64. package/lib/voice/handsFree.d.ts +12 -0
  65. package/lib/voice/handsFree.js +18 -0
  66. package/lib/voice/serverHearing.d.ts +29 -0
  67. package/lib/voice/serverHearing.js +78 -2
  68. package/package.json +1 -1
  69. package/scripts/voice/measure.py +77 -17
@@ -0,0 +1,174 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The ai-agents host client every chat host calls (moved from the Slack
7
+ * service, plans/DISCORD.md DC-004).
8
+ *
9
+ * @module portable/hosts/client
10
+ */
11
+ import { z } from 'zod';
12
+ import { bindSchema, decisionSchema, deliveryOfOutbox, HOST_KEY_HEADER, linkCodeSchema, linkStateSchema, handoffSchema, presentationSchema, selfSchema, webhookSchema, } from './contracts';
13
+ /** What ai-agents refused, with its status and its sentence. */
14
+ export class HostRefusedError extends Error {
15
+ status;
16
+ detail;
17
+ what;
18
+ constructor(status, detail, what) {
19
+ super(`${what} (${status}): ${detail}`);
20
+ this.status = status;
21
+ this.detail = detail;
22
+ this.what = what;
23
+ }
24
+ }
25
+ /**
26
+ * The generic host routes of ai-agents (plans/SLACK.md §4.3, plans/DISCORD.md
27
+ * §4.3) a host calls besides the credential exchange: as the installation,
28
+ * with its key in `X-Datalayer-Host-Key`, or as a person, with the host token
29
+ * the exchange answered. Any host speaks them.
30
+ */
31
+ export class DatalayerHostClient {
32
+ apiUrl;
33
+ fetchImplementation;
34
+ constructor(apiUrl, fetchImplementation = fetch) {
35
+ this.apiUrl = apiUrl;
36
+ this.fetchImplementation = fetchImplementation;
37
+ }
38
+ #url(path) {
39
+ return `${this.apiUrl}/api/ai-agents/v1${path}`;
40
+ }
41
+ async #call(what, path, init, schema) {
42
+ const headers = {};
43
+ if (init.body !== undefined)
44
+ headers['content-type'] = 'application/json';
45
+ if (init.hostKey)
46
+ headers[HOST_KEY_HEADER] = init.hostKey;
47
+ if (init.token)
48
+ headers.authorization = `Bearer ${init.token}`;
49
+ const response = await this.fetchImplementation(this.#url(path), {
50
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
51
+ headers,
52
+ method: init.method ?? 'GET',
53
+ });
54
+ const body = (await response.json().catch(() => ({})));
55
+ if (!response.ok || body.success === false)
56
+ throw new HostRefusedError(response.status, typeof body.detail === 'string' ? body.detail : 'refused', what);
57
+ return schema.parse(body);
58
+ }
59
+ /** Bind the installation its owner set up, with the code their browser carried (§3.1). */
60
+ async bind(input) {
61
+ const bound = await this.#call('Datalayer did not bind the installation', '/apps/hosts/bind', {
62
+ body: {
63
+ code: input.code,
64
+ external_id: input.externalId,
65
+ name: input.name ?? '',
66
+ },
67
+ method: 'POST',
68
+ }, bindSchema);
69
+ return {
70
+ defaultDeployment: bound.host.default_deployment || undefined,
71
+ hostKey: bound.host_key,
72
+ hostUid: bound.host.uid,
73
+ name: bound.host.name,
74
+ };
75
+ }
76
+ /** The installation was removed from its host: it ends at Datalayer (revocation). */
77
+ async uninstalled(hostKey) {
78
+ await this.#call('Datalayer did not end the installation', '/apps/hosts/self', { hostKey, method: 'DELETE' }, z.unknown());
79
+ }
80
+ /**
81
+ * The installation as Datalayer keeps it: the deployments it allows (with
82
+ * their application's name and whether each is kept on), the default, the
83
+ * channel policy and whether it is paused.
84
+ */
85
+ async self(hostKey) {
86
+ const answered = await this.#call('Datalayer did not answer the installation', '/apps/hosts/self', { hostKey }, selfSchema);
87
+ const host = answered.host;
88
+ return {
89
+ accountUid: host.account_uid ?? '',
90
+ channels: (host.channels ?? []).map(one => ({
91
+ deployment: one.deployment ?? '',
92
+ id: one.id,
93
+ state: one.state,
94
+ })),
95
+ defaultDeployment: host.default_deployment || undefined,
96
+ deployments: host.deployments.map(one => ({
97
+ appName: one.app_name ?? '',
98
+ kept: one.kept ?? false,
99
+ uid: one.uid,
100
+ })),
101
+ directOnly: host.direct_only ?? [],
102
+ paused: host.paused ?? false,
103
+ };
104
+ }
105
+ /** The host unlinks one of its users: how many links ended. */
106
+ async unlink(hostKey, externalUserId) {
107
+ const answered = await this.#call('Datalayer did not unlink the account', `/apps/hosts/self/links/${encodeURIComponent(externalUserId)}`, { hostKey, method: 'DELETE' }, z.object({ revoked: z.number().int() }));
108
+ return answered.revoked;
109
+ }
110
+ /** A one-use code a host user redeems, signed in to Datalayer, to link their account. */
111
+ async linkCode(hostKey, externalUserId, externalUserName = '') {
112
+ const answered = await this.#call('Datalayer did not answer a link code', '/apps/hosts/self/link-codes', {
113
+ body: {
114
+ external_user_id: externalUserId,
115
+ external_user_name: externalUserName,
116
+ },
117
+ hostKey,
118
+ method: 'POST',
119
+ }, linkCodeSchema);
120
+ return { code: answered.code, expiresIn: answered.expires_in };
121
+ }
122
+ /** Whether a host user is linked, and to which principal. */
123
+ async linkState(hostKey, externalUserId) {
124
+ const state = await this.#call('Datalayer did not say whether the account is linked', `/apps/hosts/self/links/${encodeURIComponent(externalUserId)}`, { hostKey }, linkStateSchema);
125
+ return { linked: state.linked, principalUid: state.principal_uid };
126
+ }
127
+ /** Where deliveries are pushed: answered the secret they are signed with, once. */
128
+ async setWebhook(hostKey, url) {
129
+ const hook = await this.#call('Datalayer did not register the delivery address', '/apps/hosts/self/delivery', {
130
+ body: {
131
+ events: ['approval.requested', 'approval.decided', 'turn.completed'],
132
+ url,
133
+ },
134
+ hostKey,
135
+ method: 'PUT',
136
+ }, webhookSchema);
137
+ return { signingSecret: hook.signing_secret, version: hook.version };
138
+ }
139
+ /** What the installation has not acknowledged, oldest first: pushed or not. */
140
+ async outbox(hostKey, limit = 100) {
141
+ const listed = await this.#call('Datalayer did not answer the outbox', `/apps/hosts/self/deliveries?limit=${limit}`, { hostKey }, z.object({ deliveries: z.array(z.unknown()) }));
142
+ return listed.deliveries.map(deliveryOfOutbox);
143
+ }
144
+ async acknowledge(hostKey, deliveryUid) {
145
+ await this.#call('Datalayer did not take the acknowledgement', `/apps/hosts/self/deliveries/${encodeURIComponent(deliveryUid)}/ack`, { hostKey, method: 'POST' }, z.unknown());
146
+ }
147
+ /**
148
+ * *Open in Datalayer* (S-305): a one-use code for the host token's person
149
+ * that opens one session or approval after sign-in. Answers the landing's
150
+ * path that redeems it; never a bearer token in a URL.
151
+ */
152
+ async handoff(token, kind, target) {
153
+ const made = await this.#call('Datalayer did not make the link', '/apps/hosts/handoffs', { body: { kind, target }, method: 'POST', token }, handoffSchema);
154
+ return { expiresIn: made.expires_in, path: made.path };
155
+ }
156
+ /** How an approval is shown, read as the person it waits on (their host token). */
157
+ async presentation(token, approvalId) {
158
+ const answered = await this.#call('Datalayer did not answer the approval', `/tool-approvals/${encodeURIComponent(approvalId)}/presentation`, { token }, z.object({ presentation: presentationSchema }));
159
+ return answered.presentation;
160
+ }
161
+ /** Decide an approval as its person, on the very presentation shown (its digest). */
162
+ async decide(token, approvalId, decision) {
163
+ const answered = await this.#call('Datalayer did not take the decision', `/apps/hosts/approvals/${encodeURIComponent(approvalId)}/decision`, {
164
+ body: {
165
+ approve: decision.approve,
166
+ digest: decision.digest,
167
+ note: decision.note || null,
168
+ },
169
+ method: 'POST',
170
+ token,
171
+ }, decisionSchema);
172
+ return { status: answered.approval.status };
173
+ }
174
+ }
@@ -0,0 +1,199 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The generic Datalayer host contracts (plans/SLACK.md §4.3, plans/DISCORD.md
4
+ * §4.3), as ai-agents answers them, for every chat host (Slack, Discord).
5
+ * These shapes are shared with ai-agents through `contracts.json`, beside
6
+ * this file and byte-identical to ai-agents' `tests/fixtures/hosts/`, which
7
+ * both sides test. A delivery's `target` stays opaque here: each host
8
+ * validates its own.
9
+ *
10
+ * @module portable/hosts/contracts
11
+ */
12
+ export declare const HOST_KEY_HEADER = "X-Datalayer-Host-Key";
13
+ export declare const SIGNATURE_HEADER = "X-Datalayer-Signature";
14
+ export declare const TIMESTAMP_HEADER = "X-Datalayer-Timestamp";
15
+ export declare const DELIVERY_HEADER = "X-Datalayer-Delivery";
16
+ export declare const IDEMPOTENCY_HEADER = "Idempotency-Key";
17
+ /** The presentation version the hosts render; another is reviewed in Datalayer. */
18
+ export declare const PRESENTATION_VERSION = 1;
19
+ export declare const bindSchema: z.ZodObject<{
20
+ host: z.ZodObject<{
21
+ uid: z.ZodString;
22
+ state: z.ZodString;
23
+ name: z.ZodOptional<z.ZodString>;
24
+ default_deployment: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$loose>;
26
+ host_key: z.ZodString;
27
+ }, z.core.$strip>;
28
+ /** What a link code is redeemed at: the landing's confirm page. */
29
+ export declare const LINK_PAGE_PATH = "/hosts/link";
30
+ /**
31
+ * `GET /apps/hosts/self`: the installation with its allowed deployments.
32
+ * `channels`, `direct_only` and `paused` are the installation's channel
33
+ * policy and pause (S-104, S-105, S-403).
34
+ */
35
+ export declare const selfSchema: z.ZodObject<{
36
+ host: z.ZodObject<{
37
+ uid: z.ZodString;
38
+ account_uid: z.ZodOptional<z.ZodString>;
39
+ default_deployment: z.ZodOptional<z.ZodString>;
40
+ deployments: z.ZodArray<z.ZodObject<{
41
+ uid: z.ZodString;
42
+ app_name: z.ZodOptional<z.ZodString>;
43
+ kept: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$loose>>;
45
+ channels: z.ZodOptional<z.ZodArray<z.ZodObject<{
46
+ id: z.ZodString;
47
+ state: z.ZodEnum<{
48
+ allow: "allow";
49
+ deny: "deny";
50
+ }>;
51
+ deployment: z.ZodOptional<z.ZodString>;
52
+ }, z.core.$strip>>>;
53
+ direct_only: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ paused: z.ZodOptional<z.ZodBoolean>;
55
+ }, z.core.$loose>;
56
+ }, z.core.$strip>;
57
+ /** `POST /apps/hosts/handoffs`: a one-use code that opens one session or approval (S-305). */
58
+ export declare const handoffSchema: z.ZodObject<{
59
+ code: z.ZodString;
60
+ expires_in: z.ZodNumber;
61
+ path: z.ZodString;
62
+ }, z.core.$strip>;
63
+ export declare const linkStateSchema: z.ZodObject<{
64
+ linked: z.ZodBoolean;
65
+ principal_uid: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strip>;
67
+ export declare const webhookSchema: z.ZodObject<{
68
+ url: z.ZodURL;
69
+ events: z.ZodArray<z.ZodString>;
70
+ version: z.ZodNumber;
71
+ signing_secret: z.ZodString;
72
+ }, z.core.$strip>;
73
+ /** One delivery of the outbox, as `GET …/self/deliveries` lists it. */
74
+ export declare const outboxItemSchema: z.ZodObject<{
75
+ uid: z.ZodString;
76
+ event: z.ZodString;
77
+ payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
78
+ state: z.ZodString;
79
+ }, z.core.$loose>;
80
+ export declare const decisionSchema: z.ZodObject<{
81
+ approval: z.ZodObject<{
82
+ id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
83
+ status: z.ZodString;
84
+ }, z.core.$loose>;
85
+ }, z.core.$strip>;
86
+ export declare const linkCodeSchema: z.ZodObject<{
87
+ code: z.ZodString;
88
+ expires_in: z.ZodNumber;
89
+ confirm: z.ZodString;
90
+ }, z.core.$strip>;
91
+ export declare const exchangeSchema: z.ZodObject<{
92
+ access_token: z.ZodString;
93
+ token_type: z.ZodLiteral<"Bearer">;
94
+ expires_in: z.ZodNumber;
95
+ exp: z.ZodNumber;
96
+ principal_uid: z.ZodString;
97
+ host_uid: z.ZodString;
98
+ deployment: z.ZodObject<{
99
+ uid: z.ZodString;
100
+ app_uid: z.ZodString;
101
+ app_name: z.ZodString;
102
+ version: z.ZodNumber;
103
+ }, z.core.$strip>;
104
+ session_api: z.ZodObject<{
105
+ url: z.ZodURL;
106
+ agent: z.ZodString;
107
+ }, z.core.$strip>;
108
+ user_token: z.ZodOptional<z.ZodString>;
109
+ }, z.core.$strip>;
110
+ export type HostExchange = z.infer<typeof exchangeSchema>;
111
+ export declare const presentationSchema: z.ZodObject<{
112
+ version: z.ZodNumber;
113
+ approval_id: z.ZodString;
114
+ status: z.ZodString;
115
+ owner_uid: z.ZodString;
116
+ tool_name: z.ZodString;
117
+ action_class: z.ZodEnum<{
118
+ unknown: "unknown";
119
+ read: "read";
120
+ write: "write";
121
+ send: "send";
122
+ delete: "delete";
123
+ publish: "publish";
124
+ execute: "execute";
125
+ financial: "financial";
126
+ }>;
127
+ session_uid: z.ZodString;
128
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
129
+ expires_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
130
+ fields: z.ZodArray<z.ZodObject<{
131
+ name: z.ZodString;
132
+ label: z.ZodString;
133
+ value: z.ZodString;
134
+ }, z.core.$strip>>;
135
+ arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
136
+ secret_fields: z.ZodArray<z.ZodString>;
137
+ complete: z.ZodBoolean;
138
+ host_may_decide: z.ZodBoolean;
139
+ reasons: z.ZodArray<z.ZodString>;
140
+ digest: z.ZodString;
141
+ }, z.core.$strip>;
142
+ export type ApprovalPresentation = z.infer<typeof presentationSchema>;
143
+ export declare const deliverySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
144
+ delivery: z.ZodString;
145
+ event: z.ZodLiteral<"approval.requested">;
146
+ payload: z.ZodObject<{
147
+ approval_id: z.ZodString;
148
+ tool_name: z.ZodString;
149
+ presentation: z.ZodString;
150
+ session_uid: z.ZodString;
151
+ deployment_uid: z.ZodString;
152
+ target: z.ZodRecord<z.ZodString, z.ZodUnknown>;
153
+ }, z.core.$loose>;
154
+ }, z.core.$strip>, z.ZodObject<{
155
+ delivery: z.ZodString;
156
+ event: z.ZodLiteral<"approval.decided">;
157
+ payload: z.ZodObject<{
158
+ approval_id: z.ZodString;
159
+ status: z.ZodString;
160
+ decided_by: z.ZodString;
161
+ session_uid: z.ZodString;
162
+ deployment_uid: z.ZodString;
163
+ target: z.ZodRecord<z.ZodString, z.ZodUnknown>;
164
+ }, z.core.$loose>;
165
+ }, z.core.$strip>, z.ZodObject<{
166
+ delivery: z.ZodString;
167
+ event: z.ZodLiteral<"turn.completed">;
168
+ payload: z.ZodObject<{
169
+ record_entry: z.ZodString;
170
+ session_uid: z.ZodString;
171
+ deployment_uid: z.ZodString;
172
+ target: z.ZodRecord<z.ZodString, z.ZodUnknown>;
173
+ }, z.core.$loose>;
174
+ }, z.core.$strip>], "event">;
175
+ export type HostDelivery = z.infer<typeof deliverySchema>;
176
+ /** An outbox item read as the delivery a push would have carried. */
177
+ export declare function deliveryOfOutbox(item: unknown): HostDelivery;
178
+ /**
179
+ * Whether a host may offer Approve and Reject: only what the server says a
180
+ * host may decide, in a version the hosts render, shown whole, pending, with
181
+ * no secret field. Anything else gets *Review in Datalayer* (SLACK §2 rule 6,
182
+ * DISCORD §2 rule 6). Each host also checks the card fits its own limits.
183
+ */
184
+ export declare function hostMayDecide(presentation: ApprovalPresentation): boolean;
185
+ /** `v1=` HMAC-SHA256 of `"<timestamp>.<raw body>"`, as ai-agents signs a push. */
186
+ export declare function deliverySignature(secret: string, timestamp: number, rawBody: string | Buffer): string;
187
+ /**
188
+ * Verify a pushed delivery before trusting it: its signature in constant
189
+ * time, and a timestamp within five minutes, so a captured push cannot be
190
+ * replayed later.
191
+ */
192
+ export declare function verifyDelivery(input: {
193
+ nowSeconds?: number;
194
+ rawBody: string | Buffer;
195
+ secret: string;
196
+ signature: string | undefined;
197
+ timestamp: string | undefined;
198
+ toleranceSeconds?: number;
199
+ }): HostDelivery;
@@ -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
+ }