@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,147 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { DELIVERY_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER, verifyDelivery, } from './contracts';
6
+ /** Where Datalayer pushes an installation's deliveries, under the service's public address. */
7
+ export const DELIVERY_PATH = '/datalayer/deliveries';
8
+ export const DELIVERY_ROUTE = `${DELIVERY_PATH}/:hostUid`;
9
+ const DELIVERY_BODY_LIMIT = 64 * 1024;
10
+ /**
11
+ * Handle one delivery of an installation once (claimed by its uid), then
12
+ * acknowledge it at Datalayer. A removed installation's delivery is dropped;
13
+ * a handling that fails releases its claim, so the queue tries again.
14
+ */
15
+ export async function receiveDelivery(job, input) {
16
+ const hostKey = await input.sink.hostKeyOf(job.hostUid);
17
+ if (!hostKey) {
18
+ return;
19
+ }
20
+ const claim = `delivery:${job.hostUid}:${job.delivery.delivery}`;
21
+ if (await input.claims.claimEvent(claim)) {
22
+ try {
23
+ await input.handle(job.delivery);
24
+ }
25
+ catch (error) {
26
+ await input.claims.releaseEvent(claim);
27
+ throw error;
28
+ }
29
+ }
30
+ await input.client.acknowledge(hostKey, job.delivery.delivery);
31
+ }
32
+ async function rawBodyOf(request) {
33
+ const chunks = [];
34
+ let size = 0;
35
+ for await (const chunk of request) {
36
+ size += chunk.length;
37
+ if (size > DELIVERY_BODY_LIMIT) {
38
+ throw new Error('The delivery is too large.');
39
+ }
40
+ chunks.push(chunk);
41
+ }
42
+ return Buffer.concat(chunks);
43
+ }
44
+ /**
45
+ * The signed webhook Datalayer pushes to (`POST /datalayer/deliveries/{host}`,
46
+ * registered with `PUT …/self/delivery`): verified with that installation's
47
+ * secret — HMAC over `<timestamp>.<body>`, five minutes at most — then queued
48
+ * and acknowledged with 2xx at once; the work runs from the queue.
49
+ */
50
+ export function deliveryRoute(sink, enqueue, nowSeconds) {
51
+ return async (request, response) => {
52
+ const answer = (status, body) => {
53
+ response.writeHead(status, { 'content-type': 'application/json' });
54
+ response.end(JSON.stringify(body));
55
+ };
56
+ try {
57
+ const named = request.params?.hostUid;
58
+ const hostUid = typeof named === 'string' ? named : '';
59
+ const secret = hostUid ? await sink.deliverySecretOf(hostUid) : undefined;
60
+ if (!secret) {
61
+ return answer(404, { ok: false, error: 'unknown installation' });
62
+ }
63
+ const header = (name) => {
64
+ const value = request.headers[name.toLowerCase()];
65
+ return Array.isArray(value) ? value[0] : value;
66
+ };
67
+ const rawBody = await rawBodyOf(request);
68
+ let delivery;
69
+ try {
70
+ delivery = verifyDelivery({
71
+ nowSeconds: nowSeconds?.(),
72
+ rawBody,
73
+ secret,
74
+ signature: header(SIGNATURE_HEADER),
75
+ timestamp: header(TIMESTAMP_HEADER),
76
+ });
77
+ }
78
+ catch {
79
+ return answer(401, { ok: false, error: 'not verified' });
80
+ }
81
+ if (header(DELIVERY_HEADER) &&
82
+ header(DELIVERY_HEADER) !== delivery.delivery) {
83
+ return answer(400, { ok: false, error: 'delivery mismatch' });
84
+ }
85
+ await enqueue({ delivery, hostUid });
86
+ return answer(200, { ok: true });
87
+ }
88
+ catch {
89
+ return answer(500, { ok: false });
90
+ }
91
+ };
92
+ }
93
+ /**
94
+ * The outbox, read when a push was missed or no address is registered
95
+ * (`GET …/self/deliveries`): each delivery of every bound installation
96
+ * queued, handled once, then acknowledged.
97
+ */
98
+ export class DeliveryPoller {
99
+ sink;
100
+ client;
101
+ enqueue;
102
+ logger;
103
+ #timer;
104
+ #running = false;
105
+ constructor(sink, client, enqueue, logger = console) {
106
+ this.sink = sink;
107
+ this.client = client;
108
+ this.enqueue = enqueue;
109
+ this.logger = logger;
110
+ }
111
+ async pollOnce() {
112
+ let queued = 0;
113
+ for (const { hostKey, hostUid } of await this.sink.boundHosts()) {
114
+ try {
115
+ for (const delivery of await this.client.outbox(hostKey)) {
116
+ await this.enqueue({ delivery, hostUid });
117
+ queued += 1;
118
+ }
119
+ }
120
+ catch (error) {
121
+ this.logger.warn(`The outbox of one installation was not read: ${error.message}`);
122
+ }
123
+ }
124
+ return queued;
125
+ }
126
+ start(intervalSeconds) {
127
+ if (intervalSeconds <= 0 || this.#timer) {
128
+ return;
129
+ }
130
+ this.#timer = setInterval(() => {
131
+ if (this.#running) {
132
+ return;
133
+ }
134
+ this.#running = true;
135
+ void this.pollOnce().finally(() => {
136
+ this.#running = false;
137
+ });
138
+ }, intervalSeconds * 1000);
139
+ this.#timer.unref?.();
140
+ }
141
+ stop() {
142
+ if (this.#timer) {
143
+ clearInterval(this.#timer);
144
+ }
145
+ this.#timer = undefined;
146
+ }
147
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * What may never reach a host service's production code (SLACK §4.2, S-005;
3
+ * DISCORD §4.2): Jupyter, JupyterLab, Lumino, Datalayer's Jupyter packages,
4
+ * React, Primer, Reactor and the other host services. Checked over the
5
+ * service's source imports, its esbuild bundle's module graph
6
+ * (`forbiddenInBundle`, over the metafile) and the installed graph of the
7
+ * bundle's externals (`npm ls --omit=dev --omit=peer --all`). Pure functions:
8
+ * each service reads its files and runs them. Moved from the Slack service
9
+ * (plans/DISCORD.md DC-004).
10
+ *
11
+ * @module portable/hosts/forbiddenGraph
12
+ */
13
+ export declare const FORBIDDEN: readonly RegExp[];
14
+ /** A package reached, with the chain of dependencies that reached it. */
15
+ export type Reached = Readonly<{
16
+ chain: string[];
17
+ name: string;
18
+ }>;
19
+ type LockPackage = {
20
+ dependencies?: Record<string, string>;
21
+ optionalDependencies?: Record<string, string>;
22
+ peerDependencies?: Record<string, string>;
23
+ peerDependenciesMeta?: Record<string, {
24
+ optional?: boolean;
25
+ }>;
26
+ };
27
+ /** A lockfile, v2 or v3: its `packages`. */
28
+ export type Lockfile = {
29
+ packages?: Record<string, LockPackage>;
30
+ };
31
+ /** What `npm ls --json` answers: each package and its own dependencies. */
32
+ export type InstalledTree = {
33
+ dependencies?: Record<string, InstalledTree | undefined>;
34
+ };
35
+ export declare const isForbidden: (name: string) => boolean;
36
+ /** The package name an import specifier names: `@scope/name/deep` → `@scope/name`. */
37
+ export declare function packageOf(specifier: string): string | undefined;
38
+ /** Every bare import of one source file. */
39
+ export declare function importsOf(source: string): string[];
40
+ /**
41
+ * The production graph of a lockfile (v2/v3 `packages`): from the root's
42
+ * `dependencies` and `optionalDependencies` (never `devDependencies`), each
43
+ * package's own dependencies resolved the way Node resolves them — the
44
+ * nearest `node_modules` up from the dependent. Answers each package reached
45
+ * with the chain that reached it.
46
+ */
47
+ export declare function productionGraph(lock: Lockfile): Reached[];
48
+ /**
49
+ * The production graph as `npm ls --omit=dev --all --json` answers it, from
50
+ * what is installed: what the service runs with, whether a lockfile is kept
51
+ * or not (the monorepo keeps none). Each package with the chain reaching it.
52
+ */
53
+ export declare function installedGraph(tree: InstalledTree): Reached[];
54
+ /** The forbidden packages an installed graph reaches, each with its chain. */
55
+ export declare function forbiddenInInstalled(tree: InstalledTree): string[];
56
+ /** The forbidden packages a lockfile's production graph reaches, each with its chain. */
57
+ export declare function forbiddenInGraph(lock: Lockfile): string[];
58
+ /** The parts of an esbuild metafile the bundle gate reads. */
59
+ export type BundleMetafile = {
60
+ inputs: Record<string, unknown>;
61
+ outputs: Record<string, {
62
+ imports?: ReadonlyArray<{
63
+ path: string;
64
+ external?: boolean;
65
+ }>;
66
+ }>;
67
+ };
68
+ /**
69
+ * What a service's esbuild bundle may hold (DISCORD §4.2): its own sources,
70
+ * and agent-runtimes' `lib/portable` (by its real path: the monorepo's
71
+ * workspace link resolves to `ai/agent-runtimes/lib/portable/…`). Every
72
+ * other module is a package's, and refused; every external is a `node:`
73
+ * builtin or a name in the service's `dependencies`; nothing matches
74
+ * `FORBIDDEN`. Answers each violation in words.
75
+ */
76
+ export declare function forbiddenInBundle(metafile: BundleMetafile, dependencies: readonly string[]): string[];
77
+ export {};
@@ -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
+ * What may never reach a host service's production code (SLACK §4.2, S-005;
7
+ * DISCORD §4.2): Jupyter, JupyterLab, Lumino, Datalayer's Jupyter packages,
8
+ * React, Primer, Reactor and the other host services. Checked over the
9
+ * service's source imports, its esbuild bundle's module graph
10
+ * (`forbiddenInBundle`, over the metafile) and the installed graph of the
11
+ * bundle's externals (`npm ls --omit=dev --omit=peer --all`). Pure functions:
12
+ * each service reads its files and runs them. Moved from the Slack service
13
+ * (plans/DISCORD.md DC-004).
14
+ *
15
+ * @module portable/hosts/forbiddenGraph
16
+ */
17
+ export const FORBIDDEN = [
18
+ /^@jupyter\//,
19
+ /^@jupyterlab\//,
20
+ /^@jupyter-widgets\//,
21
+ /^@lumino\//,
22
+ /^@datalayer\/jupyter-/,
23
+ /^react-dom$/,
24
+ /^react$/,
25
+ /^@primer\//,
26
+ /^@datalayer\/reactor$/,
27
+ /^@datalayer\/slack$/,
28
+ /^@datalayer\/discord$/,
29
+ ];
30
+ export const isForbidden = (name) => FORBIDDEN.some(pattern => pattern.test(name));
31
+ /** The package name an import specifier names: `@scope/name/deep` → `@scope/name`. */
32
+ export function packageOf(specifier) {
33
+ if (specifier.startsWith('.') || specifier.startsWith('node:'))
34
+ return undefined;
35
+ const parts = specifier.split('/');
36
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
37
+ }
38
+ /** Every bare import of one source file. */
39
+ export function importsOf(source) {
40
+ const found = new Set();
41
+ const patterns = [
42
+ /\bimport\s+(?:type\s+)?(?:[^'"]*?\sfrom\s+)?["']([^"']+)["']/g,
43
+ /\bexport\s+(?:type\s+)?[^'"]*?\sfrom\s+["']([^"']+)["']/g,
44
+ /\bimport\(\s*["']([^"']+)["']\s*\)/g,
45
+ /\brequire\(\s*["']([^"']+)["']\s*\)/g,
46
+ ];
47
+ for (const pattern of patterns)
48
+ for (const match of source.matchAll(pattern)) {
49
+ const name = packageOf(match[1] ?? '');
50
+ if (name)
51
+ found.add(name);
52
+ }
53
+ return [...found];
54
+ }
55
+ /**
56
+ * The production graph of a lockfile (v2/v3 `packages`): from the root's
57
+ * `dependencies` and `optionalDependencies` (never `devDependencies`), each
58
+ * package's own dependencies resolved the way Node resolves them — the
59
+ * nearest `node_modules` up from the dependent. Answers each package reached
60
+ * with the chain that reached it.
61
+ */
62
+ export function productionGraph(lock) {
63
+ const packages = lock.packages ?? {};
64
+ const root = packages[''] ?? {};
65
+ const resolve = (from, name) => {
66
+ let base = from;
67
+ while (true) {
68
+ const candidate = `${base ? `${base}/` : ''}node_modules/${name}`;
69
+ if (packages[candidate])
70
+ return candidate;
71
+ if (!base)
72
+ return undefined;
73
+ const index = base.lastIndexOf('/node_modules/');
74
+ base = index >= 0 ? base.slice(0, index) : '';
75
+ }
76
+ };
77
+ const reached = new Map();
78
+ const queue = [];
79
+ for (const name of Object.keys({
80
+ ...root.dependencies,
81
+ ...root.optionalDependencies,
82
+ }))
83
+ queue.push({ chain: [name], from: '', name });
84
+ while (queue.length > 0) {
85
+ const { chain, from, name } = queue.shift();
86
+ const path = resolve(from, name);
87
+ if (!path || reached.has(path))
88
+ continue;
89
+ reached.set(path, { chain, name });
90
+ const meta = packages[path] ?? {};
91
+ for (const next of Object.keys({
92
+ ...meta.dependencies,
93
+ ...meta.optionalDependencies,
94
+ ...meta.peerDependencies,
95
+ }))
96
+ if (!(meta.peerDependenciesMeta?.[next]?.optional && !resolve(path, next)))
97
+ queue.push({ chain: [...chain, next], from: path, name: next });
98
+ }
99
+ return [...reached.values()];
100
+ }
101
+ /**
102
+ * The production graph as `npm ls --omit=dev --all --json` answers it, from
103
+ * what is installed: what the service runs with, whether a lockfile is kept
104
+ * or not (the monorepo keeps none). Each package with the chain reaching it.
105
+ */
106
+ export function installedGraph(tree) {
107
+ const reached = [];
108
+ const seen = new Set();
109
+ const walk = (dependencies, chain) => {
110
+ for (const [name, node] of Object.entries(dependencies ?? {})) {
111
+ const id = `${chain.join('>')}>${name}`;
112
+ if (seen.has(id) || chain.includes(name))
113
+ continue;
114
+ seen.add(id);
115
+ reached.push({ chain: [...chain, name], name });
116
+ walk(node?.dependencies, [...chain, name]);
117
+ }
118
+ };
119
+ walk(tree.dependencies, []);
120
+ return reached;
121
+ }
122
+ /** The forbidden packages an installed graph reaches, each with its chain. */
123
+ export function forbiddenInInstalled(tree) {
124
+ return installedGraph(tree)
125
+ .filter(one => isForbidden(one.name))
126
+ .map(one => one.chain.join(' → '));
127
+ }
128
+ /** The forbidden packages a lockfile's production graph reaches, each with its chain. */
129
+ export function forbiddenInGraph(lock) {
130
+ return productionGraph(lock)
131
+ .filter(one => isForbidden(one.name))
132
+ .map(one => one.chain.join(' → '));
133
+ }
134
+ /** Where agent-runtimes' portable code lies, by the real path esbuild reads it from. */
135
+ const PORTABLE = /(^|\/)agent-runtimes\/lib\/portable\//;
136
+ /**
137
+ * What a service's esbuild bundle may hold (DISCORD §4.2): its own sources,
138
+ * and agent-runtimes' `lib/portable` (by its real path: the monorepo's
139
+ * workspace link resolves to `ai/agent-runtimes/lib/portable/…`). Every
140
+ * other module is a package's, and refused; every external is a `node:`
141
+ * builtin or a name in the service's `dependencies`; nothing matches
142
+ * `FORBIDDEN`. Answers each violation in words.
143
+ */
144
+ export function forbiddenInBundle(metafile, dependencies) {
145
+ const violations = [];
146
+ for (const input of Object.keys(metafile.inputs)) {
147
+ const portable = PORTABLE.test(input);
148
+ const modules = input.lastIndexOf('node_modules/');
149
+ const name = modules >= 0
150
+ ? packageOf(input.slice(modules + 'node_modules/'.length))
151
+ : undefined;
152
+ if (name && isForbidden(name))
153
+ violations.push(`${input}: ${name} is forbidden`);
154
+ else if (!portable && (modules >= 0 || input.startsWith('../')))
155
+ violations.push(`${input}: bundled from outside the service and agent-runtimes' lib/portable`);
156
+ }
157
+ const externals = new Set();
158
+ for (const output of Object.values(metafile.outputs))
159
+ for (const one of output.imports ?? [])
160
+ if (one.external)
161
+ externals.add(one.path);
162
+ for (const path of externals) {
163
+ if (path.startsWith('node:'))
164
+ continue;
165
+ const name = packageOf(path);
166
+ if (!name)
167
+ violations.push(`${path}: a relative external`);
168
+ else if (isForbidden(name))
169
+ violations.push(`${path}: ${name} is forbidden`);
170
+ else if (!dependencies.includes(name))
171
+ violations.push(`${path}: external, but ${name} is not one of the service's dependencies`);
172
+ }
173
+ return violations;
174
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Liveness and readiness (SLACK §10, S-101; DISCORD §10): `/healthz` says the
3
+ * process answers; `/readyz` says it can take durable work now — its store
4
+ * and its queue answer — and never asks the host or Datalayer, so a
5
+ * transient outage of either does not take the pods out of service: accepted
6
+ * work waits in the queue instead. And the window a signed host request's
7
+ * timestamp must fall in. Moved from the Slack service (plans/DISCORD.md
8
+ * DC-004).
9
+ *
10
+ * @module portable/hosts/health
11
+ */
12
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
13
+ export type ReadinessChecks = Readonly<Record<string, () => Promise<boolean>>>;
14
+ /** Each check, at most `timeoutMs`: a check that throws or is late is not ready. */
15
+ export declare function readiness(checks: ReadinessChecks, timeoutMs?: number): Promise<{
16
+ ready: boolean;
17
+ checks: Record<string, boolean>;
18
+ }>;
19
+ /** The health paths, and what each answers. */
20
+ export declare const HEALTH_PATHS: readonly ["/healthz", "/readyz"];
21
+ /**
22
+ * Answer `GET /healthz` or `GET /readyz`; `false` for any other request,
23
+ * which the caller answers.
24
+ */
25
+ export declare function answerHealth(checks: ReadinessChecks, request: IncomingMessage, response: ServerResponse): boolean;
26
+ /** A worker's own server: its health and readiness only (no host route). */
27
+ export declare function healthServer(checks: ReadinessChecks): Server;
28
+ /** How far a signed host request's timestamp may be from now, either way (SLACK §7.1, 1; DISCORD §7.1, 1). */
29
+ export declare const REQUEST_WINDOW_SECONDS: number;
30
+ /**
31
+ * Whether a request's timestamp header (Slack's `X-Slack-Request-Timestamp`,
32
+ * Discord's `X-Signature-Timestamp`) is within five minutes of now, in the
33
+ * past or the future: a replay with a skewed clock is refused too.
34
+ */
35
+ export declare function timestampWithinWindow(header: string | string[] | undefined, nowSeconds?: number): boolean;
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Liveness and readiness (SLACK §10, S-101; DISCORD §10): `/healthz` says the
7
+ * process answers; `/readyz` says it can take durable work now — its store
8
+ * and its queue answer — and never asks the host or Datalayer, so a
9
+ * transient outage of either does not take the pods out of service: accepted
10
+ * work waits in the queue instead. And the window a signed host request's
11
+ * timestamp must fall in. Moved from the Slack service (plans/DISCORD.md
12
+ * DC-004).
13
+ *
14
+ * @module portable/hosts/health
15
+ */
16
+ import { createServer, } from 'node:http';
17
+ /** Each check, at most `timeoutMs`: a check that throws or is late is not ready. */
18
+ export async function readiness(checks, timeoutMs = 2_000) {
19
+ const results = {};
20
+ await Promise.all(Object.entries(checks).map(async ([name, check]) => {
21
+ let timer;
22
+ try {
23
+ results[name] = await Promise.race([
24
+ check(),
25
+ new Promise(resolve => {
26
+ timer = setTimeout(() => resolve(false), timeoutMs);
27
+ }),
28
+ ]);
29
+ }
30
+ catch {
31
+ results[name] = false;
32
+ }
33
+ finally {
34
+ if (timer) {
35
+ clearTimeout(timer);
36
+ }
37
+ }
38
+ }));
39
+ return {
40
+ checks: results,
41
+ ready: Object.values(results).every(Boolean),
42
+ };
43
+ }
44
+ function json(response, status, body) {
45
+ response.writeHead(status, {
46
+ 'cache-control': 'no-store',
47
+ 'content-type': 'application/json',
48
+ });
49
+ response.end(JSON.stringify(body));
50
+ }
51
+ /** The health paths, and what each answers. */
52
+ export const HEALTH_PATHS = ['/healthz', '/readyz'];
53
+ /**
54
+ * Answer `GET /healthz` or `GET /readyz`; `false` for any other request,
55
+ * which the caller answers.
56
+ */
57
+ export function answerHealth(checks, request, response) {
58
+ const path = (request.url ?? '').split('?')[0];
59
+ if (request.method !== 'GET') {
60
+ return false;
61
+ }
62
+ if (path === '/healthz') {
63
+ json(response, 200, { status: 'ok' });
64
+ return true;
65
+ }
66
+ if (path === '/readyz') {
67
+ void readiness(checks).then(answer => json(response, answer.ready ? 200 : 503, answer));
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+ /** A worker's own server: its health and readiness only (no host route). */
73
+ export function healthServer(checks) {
74
+ return createServer((request, response) => {
75
+ if (!answerHealth(checks, request, response)) {
76
+ response.writeHead(404);
77
+ response.end();
78
+ }
79
+ });
80
+ }
81
+ /** How far a signed host request's timestamp may be from now, either way (SLACK §7.1, 1; DISCORD §7.1, 1). */
82
+ export const REQUEST_WINDOW_SECONDS = 5 * 60;
83
+ /**
84
+ * Whether a request's timestamp header (Slack's `X-Slack-Request-Timestamp`,
85
+ * Discord's `X-Signature-Timestamp`) is within five minutes of now, in the
86
+ * past or the future: a replay with a skewed clock is refused too.
87
+ */
88
+ export function timestampWithinWindow(header, nowSeconds = Math.floor(Date.now() / 1000)) {
89
+ const value = Number(Array.isArray(header) ? header[0] : header);
90
+ return (Number.isInteger(value) &&
91
+ Math.abs(nowSeconds - value) <= REQUEST_WINDOW_SECONDS);
92
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What a chat host service (Slack, Discord) shares, on Node: the ai-agents
3
+ * host contracts and client, the agent client over the session API, the
4
+ * semantic events a turn is reduced to, approval renderers, deliveries, the
5
+ * turn's state, application resolution, the durable queue, quotas, secrets
6
+ * at rest, observability, dead letters, health, the shared schema and the
7
+ * forbidden-graph gate (plans/DISCORD.md §4.2, DC-004, DC-005).
8
+ *
9
+ * Imported as `@datalayer/agent-runtimes/lib/portable/hosts`, and only by a
10
+ * Node service, which bundles it with esbuild. It reaches `src/portable/` and
11
+ * this folder only, and the packages `zod`, `node:crypto` and `node:http`;
12
+ * never React, DOM, Jupyter, Lumino, Primer, Reactor, `pg` or another folder
13
+ * of Agent Runtimes (`src/__tests__/portable.unit.test.ts` holds it to that).
14
+ * It imports its siblings by name, never `'../index'`, whose types reach
15
+ * Reactor; so a service never imports `@datalayer/agent-runtimes/lib/portable`
16
+ * itself: what it needs of it is re-exported here.
17
+ *
18
+ * @module portable/hosts
19
+ */
20
+ export { AgUiFrames, readRunStream, type AgUiEvent } from '../agui';
21
+ export * from '../semantic';
22
+ export { askQuestionOf, semanticOf, turnOf, type TurnInfo, } from '../semanticOfAgUi';
23
+ export * from './contracts';
24
+ export { DatalayerHostClient, HostRefusedError, type BoundHost, type HostSelf, } from './client';
25
+ export { agUiEventsOf, assertTrustedStreamUrl, DevelopmentCredentials, HostKeyCredentials, hostSessionOf, HostStarters, HttpAgentClient, InstallationCredentials, lastAnswer, type AgentClient, type HostCredentials, type HostKeyLookup, type HostSession, type HostShape, type HostTurnRequest, type Place, type Starter, type StarterSource, type TurnStream, } from './agentClient';
26
+ export { hostSessionUid } from './sessionUid';
27
+ export { DATALAYER_ONLY, RENDERERS, renderApproval, type RenderedAction, type RenderedField, } from './approvalRenderers';
28
+ export { DELIVERY_PATH, DELIVERY_ROUTE, DeliveryPoller, deliveryRoute, receiveDelivery, type BoundHostKey, type DeliveryClaimStore, type DeliveryJob, type DeliverySink, } from './deliveries';
29
+ export { TurnState, type TurnEnding, type TurnFile, type TurnTool, } from './turnState';
30
+ export { resolveApplication, usableAllowances, type AllowanceView, type ApplicationRefusal, type ApplicationResolution, type ChannelRule, } from './resolution';
31
+ export { MemoryWorkQueue, tablePrefixOf, type ConnectionPool, type Queryable, type QueryAnswer, type WorkQueue, } from './queue';
32
+ export { PostgresWorkQueue } from './postgresQueue';
33
+ export { DEFAULT_QUOTAS, MemoryQuotaStore, PostgresQuotaStore, quotaEntries, quotaWords, resetOf, userQuotaKey, windowOf, type QuotaEntry, type QuotaLimits, type QuotaScope, type QuotaStore, type QuotaTake, type QuotaTaken, } from './quotas';
34
+ export { SecretCipher, type EncryptedSecret } from './secretCipher';
35
+ export { Logger, LogTracer, noTracer, pseudonym, redact, redactCredentials, REDACTED, TOKEN_PATTERNS, type LogSink, type Tracer, } from './observability';
36
+ export { DEAD_LETTERS_USAGE, DeadLetters, deadLetters, payloadHash, runDeadLettersCli, type CliOutput, type DeadLetter, } from './deadLetters';
37
+ export { answerHealth, HEALTH_PATHS, healthServer, readiness, REQUEST_WINDOW_SECONDS, timestampWithinWindow, type ReadinessChecks, } from './health';
38
+ export { systemClock, type Clock } from './rateLimit';
39
+ export { applyHostSchema, hostSchemaSql, hostTables } from './schema';
40
+ export { FORBIDDEN, forbiddenInBundle, forbiddenInGraph, forbiddenInInstalled, importsOf, installedGraph, isForbidden, packageOf, productionGraph, type BundleMetafile, type InstalledTree, type Lockfile, type Reached, } from './forbiddenGraph';
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * What a chat host service (Slack, Discord) shares, on Node: the ai-agents
7
+ * host contracts and client, the agent client over the session API, the
8
+ * semantic events a turn is reduced to, approval renderers, deliveries, the
9
+ * turn's state, application resolution, the durable queue, quotas, secrets
10
+ * at rest, observability, dead letters, health, the shared schema and the
11
+ * forbidden-graph gate (plans/DISCORD.md §4.2, DC-004, DC-005).
12
+ *
13
+ * Imported as `@datalayer/agent-runtimes/lib/portable/hosts`, and only by a
14
+ * Node service, which bundles it with esbuild. It reaches `src/portable/` and
15
+ * this folder only, and the packages `zod`, `node:crypto` and `node:http`;
16
+ * never React, DOM, Jupyter, Lumino, Primer, Reactor, `pg` or another folder
17
+ * of Agent Runtimes (`src/__tests__/portable.unit.test.ts` holds it to that).
18
+ * It imports its siblings by name, never `'../index'`, whose types reach
19
+ * Reactor; so a service never imports `@datalayer/agent-runtimes/lib/portable`
20
+ * itself: what it needs of it is re-exported here.
21
+ *
22
+ * @module portable/hosts
23
+ */
24
+ // The run stream and its reduction, from the portable entry's own modules.
25
+ export { AgUiFrames, readRunStream } from '../agui';
26
+ export * from '../semantic';
27
+ export { askQuestionOf, semanticOf, turnOf, } from '../semanticOfAgUi';
28
+ export * from './contracts';
29
+ export { DatalayerHostClient, HostRefusedError, } from './client';
30
+ export { agUiEventsOf, assertTrustedStreamUrl, DevelopmentCredentials, HostKeyCredentials, hostSessionOf, HostStarters, HttpAgentClient, InstallationCredentials, lastAnswer, } from './agentClient';
31
+ export { hostSessionUid } from './sessionUid';
32
+ export { DATALAYER_ONLY, RENDERERS, renderApproval, } from './approvalRenderers';
33
+ export { DELIVERY_PATH, DELIVERY_ROUTE, DeliveryPoller, deliveryRoute, receiveDelivery, } from './deliveries';
34
+ export { TurnState, } from './turnState';
35
+ export { resolveApplication, usableAllowances, } from './resolution';
36
+ export { MemoryWorkQueue, tablePrefixOf, } from './queue';
37
+ export { PostgresWorkQueue } from './postgresQueue';
38
+ export { DEFAULT_QUOTAS, MemoryQuotaStore, PostgresQuotaStore, quotaEntries, quotaWords, resetOf, userQuotaKey, windowOf, } from './quotas';
39
+ export { SecretCipher } from './secretCipher';
40
+ export { Logger, LogTracer, noTracer, pseudonym, redact, redactCredentials, REDACTED, TOKEN_PATTERNS, } from './observability';
41
+ export { DEAD_LETTERS_USAGE, DeadLetters, deadLetters, payloadHash, runDeadLettersCli, } from './deadLetters';
42
+ export { answerHealth, HEALTH_PATHS, healthServer, readiness, REQUEST_WINDOW_SECONDS, timestampWithinWindow, } from './health';
43
+ export { systemClock } from './rateLimit';
44
+ export { applyHostSchema, hostSchemaSql, hostTables } from './schema';
45
+ export { FORBIDDEN, forbiddenInBundle, forbiddenInGraph, forbiddenInInstalled, importsOf, installedGraph, isForbidden, packageOf, productionGraph, } from './forbiddenGraph';