@nylorun/runtime 0.17.0-beta → 0.19.1-beta
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/CHANGELOG.md +155 -0
- package/README.md +89 -58
- package/dist/adapters/execution/restate.d.ts +1 -3
- package/dist/adapters/execution/restate.js +4 -31
- package/dist/adapters/replication/pgoutput.js +1 -1
- package/dist/adapters/sandbox/local.js +1 -1
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/api/ag-ui/routes.js +1 -1
- package/dist/api/ag-ui/translate.d.ts +1 -3
- package/dist/api/ag-ui/translate.js +2 -17
- package/dist/api/components.d.ts +4871 -2614
- package/dist/api/components.js +12 -11
- package/dist/api/gate/routes.d.ts +7 -8
- package/dist/api/gate/routes.js +58 -52
- package/dist/api/http/app.d.ts +6 -0
- package/dist/api/http/app.js +18 -14
- package/dist/api/http/define.d.ts +14 -2
- package/dist/api/http/define.js +34 -15
- package/dist/api/http/routes/access.d.ts +3 -3
- package/dist/api/http/routes/access.js +18 -17
- package/dist/api/http/routes/artifacts.js +2 -4
- package/dist/api/http/routes/files.d.ts +8 -0
- package/dist/api/http/routes/files.js +77 -0
- package/dist/api/http/routes/keys.d.ts +10 -0
- package/dist/api/http/routes/keys.js +64 -0
- package/dist/api/http/routes/me.js +5 -2
- package/dist/api/http/routes/reads.d.ts +66 -0
- package/dist/api/http/routes/reads.js +127 -0
- package/dist/api/http/routes/sandboxes.js +7 -4
- package/dist/api/http/routes/sessions.js +25 -8
- package/dist/api/http/routes/tenant.js +15 -18
- package/dist/api/http/routes/vaults.d.ts +10 -9
- package/dist/api/http/routes/vaults.js +27 -23
- package/dist/api/openapi.d.ts +10 -11
- package/dist/api/openapi.js +288 -63
- package/dist/artifacts/links.js +1 -1
- package/dist/core/flow-host.d.ts +44 -23
- package/dist/core/flow-host.js +88 -106
- package/dist/core/provider.d.ts +4 -1
- package/dist/core/provider.js +6 -1
- package/dist/core/sandbox-routes.d.ts +0 -9
- package/dist/core/sandbox-routes.js +2 -21
- package/dist/execution/memory.d.ts +4 -5
- package/dist/execution/memory.js +11 -25
- package/dist/execution/types.d.ts +3 -33
- package/dist/execution/types.js +2 -12
- package/dist/gates/http-tool.d.ts +86 -0
- package/dist/gates/http-tool.js +197 -0
- package/dist/gates/mcp-handler.d.ts +3 -0
- package/dist/gates/mcp-handler.js +1 -3
- package/dist/gates/meter.d.ts +2 -2
- package/dist/gates/meter.js +6 -0
- package/dist/gates/tenant-vaults.d.ts +5 -9
- package/dist/gates/tool-calls.d.ts +7 -15
- package/dist/gates/tool-calls.js +4 -4
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +23 -47
- package/dist/gates/tool-contract.d.ts +27 -39
- package/dist/gates/tool-contract.js +30 -21
- package/dist/gates/tool-gate.d.ts +25 -24
- package/dist/gates/tool-gate.js +11 -7
- package/dist/harness/calls.d.ts +10 -4
- package/dist/harness/calls.js +15 -6
- package/dist/harness/executors.d.ts +1 -1
- package/dist/harness/executors.js +17 -12
- package/dist/harness/main.d.ts +2 -2
- package/dist/harness/main.js +5 -13
- package/dist/harness/service.d.ts +4 -5
- package/dist/harness/service.js +9 -2
- package/dist/harness-api/in-process.js +2 -2
- package/dist/harness-api/record.d.ts +6 -3
- package/dist/harness-api/record.js +65 -72
- package/dist/harness-api/server.d.ts +1 -9
- package/dist/harness-api/server.js +19 -17
- package/dist/harness-api/start.js +1 -5
- package/dist/harness-api/workspace.js +2 -3
- package/dist/harness-api/ws-server.js +1 -1
- package/dist/host/app.d.ts +2 -9
- package/dist/host/app.js +15 -25
- package/dist/host/config-for.d.ts +1 -5
- package/dist/host/config-for.js +0 -3
- package/dist/host/config.d.ts +0 -5
- package/dist/host/create-host.d.ts +3 -33
- package/dist/host/create-host.js +18 -94
- package/dist/host/environment.d.ts +0 -3
- package/dist/host/environment.js +0 -9
- package/dist/host/gates.d.ts +4 -1
- package/dist/host/gates.js +3 -2
- package/dist/host/http.d.ts +1 -2
- package/dist/host/http.js +1 -7
- package/dist/host/index.d.ts +2 -2
- package/dist/host/index.js +2 -2
- package/dist/host/main.js +27 -34
- package/dist/host/operate.d.ts +29 -0
- package/dist/host/operate.js +223 -0
- package/dist/host/stack-config.d.ts +12 -12
- package/dist/host/stack-config.js +5 -32
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/infra/workers.d.ts +1 -1
- package/dist/keys/keys.js +2 -2
- package/dist/management-openapi.json +6491 -0
- package/dist/mcp/connect.d.ts +8 -4
- package/dist/mcp/connect.js +6 -26
- package/dist/mcp/pool.d.ts +5 -9
- package/dist/mcp/pool.js +8 -19
- package/dist/mcp/snapshot.d.ts +8 -2
- package/dist/mcp/snapshot.js +29 -2
- package/dist/media.d.ts +1 -1
- package/dist/media.js +1 -1
- package/dist/model/pi-model.js +3 -0
- package/dist/openapi.json +9372 -17030
- package/dist/reads/access.d.ts +4 -0
- package/dist/reads/access.js +12 -0
- package/dist/reads/cursor.d.ts +11 -0
- package/dist/reads/cursor.js +80 -0
- package/dist/reads/types.d.ts +25 -0
- package/dist/reads/types.js +1 -0
- package/dist/sandbox/manager.d.ts +16 -0
- package/dist/sandbox/manager.js +40 -0
- package/dist/sandbox/session-sandbox.js +13 -3
- package/dist/sandbox/skills.d.ts +13 -0
- package/dist/sandbox/skills.js +46 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/ownership.d.ts +9 -0
- package/dist/store/ownership.js +13 -0
- package/dist/store/postgres/connect.d.ts +2 -0
- package/dist/store/postgres/connect.js +29 -0
- package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
- package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
- package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
- package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.d.ts +3 -0
- package/dist/store/postgres/reads.js +272 -0
- package/dist/store/postgres/schema.d.ts +294 -489
- package/dist/store/postgres/schema.js +41 -76
- package/dist/store/postgres/store.js +51 -154
- package/dist/store/postgres/tenant.d.ts +34 -2
- package/dist/store/postgres/tenant.js +16 -32
- package/dist/store/types.d.ts +27 -96
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +0 -2
- package/dist/streams/control.js +0 -5
- package/dist/streams/types.d.ts +1 -10
- package/dist/tenant/advance.d.ts +3 -3
- package/dist/tenant/advance.js +10 -7
- package/dist/tenant/auth.d.ts +14 -9
- package/dist/tenant/auth.js +24 -28
- package/dist/tenant/cause.d.ts +3 -3
- package/dist/tenant/cause.js +1 -1
- package/dist/tenant/commands.d.ts +2 -18
- package/dist/tenant/commands.js +35 -172
- package/dist/tenant/context.d.ts +14 -24
- package/dist/tenant/definition-files.d.ts +24 -0
- package/dist/tenant/definition-files.js +103 -0
- package/dist/tenant/effects.d.ts +18 -20
- package/dist/tenant/effects.js +56 -119
- package/dist/tenant/ephemeral.d.ts +7 -6
- package/dist/tenant/ephemeral.js +3 -11
- package/dist/tenant/host-token.js +1 -1
- package/dist/tenant/jwt.js +1 -1
- package/dist/tenant/module.d.ts +1 -4
- package/dist/tenant/module.js +3 -12
- package/dist/tenant/operator-keys.d.ts +34 -8
- package/dist/tenant/operator-keys.js +50 -18
- package/dist/tenant/outbound.d.ts +10 -1
- package/dist/tenant/outbound.js +44 -27
- package/dist/tenant/paths.d.ts +1 -1
- package/dist/tenant/paths.js +1 -3
- package/dist/tenant/principals.d.ts +11 -2
- package/dist/tenant/principals.js +27 -8
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -6
- package/dist/tenant/runtime.js +19 -18
- package/dist/tenant/sandboxes.d.ts +4 -1
- package/dist/tenant/sandboxes.js +11 -0
- package/dist/tenant/scheduler.d.ts +1 -5
- package/dist/tenant/scheduler.js +2 -5
- package/dist/tenant/session-streams.d.ts +2 -1
- package/dist/tenant/session-streams.js +39 -5
- package/dist/tenant/session.d.ts +8 -41
- package/dist/tenant/session.js +0 -37
- package/dist/tenant/sessions.js +16 -8
- package/dist/tenant/signing-keys.d.ts +3 -3
- package/dist/tenant/signing-keys.js +5 -5
- package/dist/tenant/skill-tool.d.ts +20 -0
- package/dist/tenant/skill-tool.js +72 -0
- package/dist/tenant/slim.d.ts +1 -1
- package/dist/tenant/slim.js +1 -1
- package/dist/tenant/status.js +7 -12
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +7 -3
- package/dist/tenant/streams.d.ts +3 -10
- package/dist/tenant/streams.js +1 -24
- package/dist/tenant/sweep.js +0 -5
- package/dist/tenant/transcript.d.ts +2 -2
- package/dist/tenant/transcript.js +2 -2
- package/dist/tenant/types.d.ts +15 -22
- package/dist/tenant/worker.d.ts +1 -3
- package/dist/tenant/worker.js +3 -8
- package/dist/vault/sources.d.ts +7 -1
- package/dist/vault/sources.js +22 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/admin-openapi.json +0 -904
- package/dist/api/http/routes/actions.d.ts +0 -8
- package/dist/api/http/routes/actions.js +0 -75
- package/dist/api/http/routes/endpoints.d.ts +0 -7
- package/dist/api/http/routes/endpoints.js +0 -100
- package/dist/host/admin-api.d.ts +0 -30
- package/dist/host/admin-api.js +0 -140
- package/dist/mcp/stdio.d.ts +0 -17
- package/dist/mcp/stdio.js +0 -82
- package/dist/tenant/actions.d.ts +0 -14
- package/dist/tenant/actions.js +0 -29
- package/dist/tenant/delivery-token.d.ts +0 -41
- package/dist/tenant/delivery-token.js +0 -104
- package/dist/tenant/delivery.d.ts +0 -62
- package/dist/tenant/delivery.js +0 -481
- package/dist/tenant/endpoints.d.ts +0 -67
- package/dist/tenant/endpoints.js +0 -90
package/dist/tenant/outbound.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Runtime's requests to
|
|
3
|
-
*
|
|
4
|
-
* connected to, so a DNS answer cannot
|
|
2
|
+
* The Runtime's requests to developer URLs: HTTP tools (`gates/http-tool.ts`) and, through
|
|
3
|
+
* `guardedFetch`, remote MCP servers and OAuth: one request, no redirects, a bounded answer, and
|
|
4
|
+
* the Host's address policy checked on the address actually connected to, so a DNS answer cannot
|
|
5
|
+
* steer a request somewhere the Host forbids.
|
|
5
6
|
*
|
|
6
|
-
* The result says whether anything reached the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The result says whether anything reached the URL. A request that failed before its body was
|
|
8
|
+
* flushed (refused connection, unknown host, TLS failure, a refused address) was `not_sent`;
|
|
9
|
+
* one that failed after (reset, timeout) is `lost`.
|
|
9
10
|
*/
|
|
10
11
|
import { lookup as dnsLookup } from "node:dns";
|
|
11
12
|
import { request as httpRequest } from "node:http";
|
|
12
13
|
import { request as httpsRequest } from "node:https";
|
|
13
14
|
import { BlockList, isIP } from "node:net";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
14
16
|
/** The largest answer read from an endpoint. */
|
|
15
17
|
export const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
16
18
|
const PRIVATE = new BlockList();
|
|
@@ -73,7 +75,10 @@ function guardedLookup(policy) {
|
|
|
73
75
|
return callback(null, allowed[0].address, allowed[0].family);
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* POSTs (or, with `method`, PUTs or PATCHes) the JSON `body` to `url` under `policy`. Never
|
|
80
|
+
* throws; `signal` aborts the request.
|
|
81
|
+
*/
|
|
77
82
|
export async function post(target, body, headers, options) {
|
|
78
83
|
const { policy, signal } = options;
|
|
79
84
|
const limit = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
|
|
@@ -96,10 +101,10 @@ export async function post(target, body, headers, options) {
|
|
|
96
101
|
const failed = (error) => finish({
|
|
97
102
|
kind: sent ? "lost" : "not_sent",
|
|
98
103
|
code: signal.aborted ? "ABORTED" : (error.code ?? error.name),
|
|
99
|
-
message: signal.aborted ? "The
|
|
104
|
+
message: signal.aborted ? "The request was aborted" : error.message,
|
|
100
105
|
});
|
|
101
106
|
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
102
|
-
method: "POST",
|
|
107
|
+
method: options.method ?? "POST",
|
|
103
108
|
headers: {
|
|
104
109
|
...headers,
|
|
105
110
|
host: originalHost,
|
|
@@ -165,10 +170,15 @@ export const FETCH_TIMEOUT_MS = 30_000;
|
|
|
165
170
|
* unless the caller passes a signal. The gateway calls OAuth discovery, registration, the code
|
|
166
171
|
* exchange and refresh with it. A refused URL or address rejects with `OutboundRefused`, any
|
|
167
172
|
* other failure with `OutboundFailed`.
|
|
173
|
+
*
|
|
174
|
+
* `stream`: the Response resolves once the headers arrive and its body streams, unbounded and
|
|
175
|
+
* with no timeout but the caller's signal: remote MCP servers, whose answers and event streams
|
|
176
|
+
* stay open (`mcp/connect.ts`).
|
|
168
177
|
*/
|
|
169
178
|
export function guardedFetch(policy, options = {}) {
|
|
170
179
|
const limit = options.maxResponseBytes ?? MAX_FETCH_RESPONSE_BYTES;
|
|
171
180
|
const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
181
|
+
const stream = options.stream === true;
|
|
172
182
|
const lookup = guardedLookup(policy);
|
|
173
183
|
const guarded = async (input, init = {}) => {
|
|
174
184
|
const request = input instanceof Request ? input : undefined;
|
|
@@ -199,7 +209,7 @@ export function guardedFetch(policy, options = {}) {
|
|
|
199
209
|
}
|
|
200
210
|
const originalHost = url.host;
|
|
201
211
|
dockerHost(url, policy);
|
|
202
|
-
const signal = init.signal ?? request?.signal ?? AbortSignal.timeout(timeoutMs);
|
|
212
|
+
const signal = init.signal ?? request?.signal ?? (stream ? undefined : AbortSignal.timeout(timeoutMs));
|
|
203
213
|
const outgoing = { host: originalHost };
|
|
204
214
|
for (const [name, value] of headers)
|
|
205
215
|
outgoing[name] = value;
|
|
@@ -215,13 +225,34 @@ export function guardedFetch(policy, options = {}) {
|
|
|
215
225
|
};
|
|
216
226
|
const failed = (error) => settle(() => reject(error instanceof RefusedAddress
|
|
217
227
|
? new OutboundRefused(error.message)
|
|
218
|
-
: new OutboundFailed(signal
|
|
219
|
-
const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, signal }, (response) => {
|
|
228
|
+
: new OutboundFailed(signal?.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal?.aborted ? "ABORTED" : (error.code ?? error.name))));
|
|
229
|
+
const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, ...(signal ? { signal } : {}) }, (response) => {
|
|
220
230
|
const status = response.statusCode ?? 0;
|
|
221
231
|
if (status >= 300 && status < 400 && response.headers.location !== undefined) {
|
|
222
232
|
response.resume();
|
|
223
233
|
return settle(() => reject(new OutboundFailed(`${originalHost} answered a redirect, which this Runtime does not follow`, "REDIRECT")));
|
|
224
234
|
}
|
|
235
|
+
const noBody = status === 204 || status === 304 || method === "HEAD";
|
|
236
|
+
const answer = (body) => {
|
|
237
|
+
const headers = new Headers();
|
|
238
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
239
|
+
if (value === undefined)
|
|
240
|
+
continue;
|
|
241
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
242
|
+
headers.append(name, item);
|
|
243
|
+
}
|
|
244
|
+
return new Response(noBody ? null : body, {
|
|
245
|
+
status,
|
|
246
|
+
statusText: response.statusMessage ?? "",
|
|
247
|
+
headers,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
if (stream) {
|
|
251
|
+
// A failure after this reaches the reader as the body's error.
|
|
252
|
+
if (noBody)
|
|
253
|
+
response.resume();
|
|
254
|
+
return settle(() => resolve(answer(noBody ? null : Readable.toWeb(response))));
|
|
255
|
+
}
|
|
225
256
|
const chunks = [];
|
|
226
257
|
let size = 0;
|
|
227
258
|
response.on("data", (chunk) => {
|
|
@@ -233,21 +264,7 @@ export function guardedFetch(policy, options = {}) {
|
|
|
233
264
|
}
|
|
234
265
|
chunks.push(chunk);
|
|
235
266
|
});
|
|
236
|
-
response.on("end", () => settle(() =>
|
|
237
|
-
const answer = new Headers();
|
|
238
|
-
for (const [name, value] of Object.entries(response.headers)) {
|
|
239
|
-
if (value === undefined)
|
|
240
|
-
continue;
|
|
241
|
-
for (const item of Array.isArray(value) ? value : [value])
|
|
242
|
-
answer.append(name, item);
|
|
243
|
-
}
|
|
244
|
-
const noBody = status === 204 || status === 304 || method === "HEAD";
|
|
245
|
-
resolve(new Response(noBody ? null : Buffer.concat(chunks), {
|
|
246
|
-
status,
|
|
247
|
-
statusText: response.statusMessage ?? "",
|
|
248
|
-
headers: answer,
|
|
249
|
-
}));
|
|
250
|
-
}));
|
|
267
|
+
response.on("end", () => settle(() => resolve(answer(Buffer.concat(chunks)))));
|
|
251
268
|
response.on("error", failed);
|
|
252
269
|
response.on("aborted", () => failed(Object.assign(new Error("The answer was cut off"), { code: "ECONNRESET" })));
|
|
253
270
|
});
|
package/dist/tenant/paths.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function hostPaths(hostRoot: string): HostPaths;
|
|
|
16
16
|
* root after realpath.
|
|
17
17
|
*
|
|
18
18
|
* The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
|
|
19
|
-
* `
|
|
19
|
+
* `sandboxes/`, `blobs/` (the `fs` BlobStore, when the Host has no
|
|
20
20
|
* Object store), `logs/tenant.log`. The vault key is the one exception:
|
|
21
21
|
* it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
|
|
22
22
|
* mounts only into the gateway (F4.2), never into the runtime container.
|
package/dist/tenant/paths.js
CHANGED
|
@@ -24,7 +24,7 @@ export function hostPaths(hostRoot) {
|
|
|
24
24
|
* root after realpath.
|
|
25
25
|
*
|
|
26
26
|
* The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
|
|
27
|
-
* `
|
|
27
|
+
* `sandboxes/`, `blobs/` (the `fs` BlobStore, when the Host has no
|
|
28
28
|
* Object store), `logs/tenant.log`. The vault key is the one exception:
|
|
29
29
|
* it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
|
|
30
30
|
* mounts only into the gateway (F4.2), never into the runtime container.
|
|
@@ -39,9 +39,7 @@ export function tenantPaths(hostRoot) {
|
|
|
39
39
|
root,
|
|
40
40
|
kek: join(host, "keys", "vault-kek"),
|
|
41
41
|
home: join(root, "home"),
|
|
42
|
-
tmp: join(root, "tmp"),
|
|
43
42
|
sandboxes: join(root, "sandboxes"),
|
|
44
|
-
pluginData: join(root, "plugin-data"),
|
|
45
43
|
blobs: join(root, "blobs"),
|
|
46
44
|
logs: join(root, "logs"),
|
|
47
45
|
log: join(root, "logs", "tenant.log"),
|
|
@@ -2,8 +2,12 @@ import type { InitialPrincipal } from "../store/postgres/tenant.js";
|
|
|
2
2
|
export type { PrincipalRow } from "../store/types.js";
|
|
3
3
|
/** Principal id of the Studio key derived from the admin key. */
|
|
4
4
|
export declare const STUDIO_PRINCIPAL_ID = "studio";
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The Studio key: HMAC-SHA256 of the admin key over `nylorun/studio/v2`. It names no Tenant
|
|
7
|
+
* (protocol 8): Studio derives it before it can learn the Tenant's id, and the admin key is
|
|
8
|
+
* already one per installation.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveStudioKey(adminKey: string): string;
|
|
7
11
|
/**
|
|
8
12
|
* The principals a Host's Tenant is created with: `studio`, and an application principal when
|
|
9
13
|
* a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
@@ -16,4 +20,9 @@ export declare function hostPrincipals(options: {
|
|
|
16
20
|
};
|
|
17
21
|
/** Registers `studio` with this hash instead of the admin key's derivation. */
|
|
18
22
|
studioCredentialHash?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The management key of `NYLORUN_MANAGEMENT_KEY_FILE`: registered as `bootstrap`, and
|
|
25
|
+
* replaced at the next start when the file changed.
|
|
26
|
+
*/
|
|
27
|
+
bootstrapKey?: string;
|
|
19
28
|
}): (tenantId: string) => InitialPrincipal[];
|
|
@@ -2,27 +2,42 @@
|
|
|
2
2
|
* The principals the Host gives its Tenant when it creates it (tenancy.md §4): the Studio
|
|
3
3
|
* principal, whose key the admin key derives, so Studio (and an app embedding it) stores none,
|
|
4
4
|
* and, for the ephemeral Runtime, one application principal with a key of its own. The Tenant
|
|
5
|
-
* keeps only their hashes. Every other
|
|
6
|
-
*
|
|
5
|
+
* keeps only their hashes. Every other key is put by name (`PUT /v1/tenant/keys/{keyId}`,
|
|
6
|
+
* `nylorun-operate keys`, `operator-keys.ts`); no other key is derived.
|
|
7
7
|
*
|
|
8
8
|
* The derivation is the one `@nylorun/admin` computes on the client side
|
|
9
9
|
* (`admin/src/derived-credentials.ts`: `deriveStudioToken`); both sides must stay byte for byte
|
|
10
10
|
* the same.
|
|
11
11
|
*/
|
|
12
12
|
import { createHmac } from "node:crypto";
|
|
13
|
+
import { BOOTSTRAP_KEY_ID } from "@nylorun/core/compatibility";
|
|
13
14
|
import { hashToken } from "../core/bearer.js";
|
|
14
15
|
/** Principal id of the Studio key derived from the admin key. */
|
|
15
16
|
export const STUDIO_PRINCIPAL_ID = "studio";
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The Studio key: HMAC-SHA256 of the admin key over `nylorun/studio/v2`. It names no Tenant
|
|
19
|
+
* (protocol 8): Studio derives it before it can learn the Tenant's id, and the admin key is
|
|
20
|
+
* already one per installation.
|
|
21
|
+
*/
|
|
22
|
+
export function deriveStudioKey(adminKey) {
|
|
23
|
+
return hmac(adminKey, ["nylorun/studio/v2"]);
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* The principals a Host's Tenant is created with: `studio`, and an application principal when
|
|
22
27
|
* a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
23
28
|
*/
|
|
24
29
|
export function hostPrincipals(options) {
|
|
25
|
-
return (
|
|
30
|
+
return () => [
|
|
31
|
+
...(options.bootstrapKey
|
|
32
|
+
? [
|
|
33
|
+
{
|
|
34
|
+
id: BOOTSTRAP_KEY_ID,
|
|
35
|
+
role: "management",
|
|
36
|
+
credentialHash: hashToken(options.bootstrapKey),
|
|
37
|
+
replace: true,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
26
41
|
...(options.application
|
|
27
42
|
? [
|
|
28
43
|
{
|
|
@@ -33,8 +48,12 @@ export function hostPrincipals(options) {
|
|
|
33
48
|
: []),
|
|
34
49
|
{
|
|
35
50
|
id: STUDIO_PRINCIPAL_ID,
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
// Studio runs sessions and edits the Tenant's settings: both APIs (protocol 8).
|
|
52
|
+
role: "studio",
|
|
53
|
+
credentialHash: options.studioCredentialHash ?? hashToken(deriveStudioKey(options.adminKey)),
|
|
54
|
+
// Its key derives from the admin key: a database holding an older derivation (v1, with
|
|
55
|
+
// the Tenant id) takes this one at the next start.
|
|
56
|
+
replace: true,
|
|
38
57
|
},
|
|
39
58
|
];
|
|
40
59
|
}
|
package/dist/tenant/run-token.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* remote MCP calls. The gates take session, turn and agent from it, never from the body, and
|
|
5
5
|
* check on every call that the lease it names is still the session's (`staleRun`).
|
|
6
6
|
*
|
|
7
|
-
* Signed by the keys service with the Tenant's current signing key, like
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Signed by the keys service with the Tenant's current signing key, like capability links,
|
|
8
|
+
* and told apart from them by `typ` and `aud`. Internal: not in `openapi.json`. A token lives
|
|
9
|
+
* `RUN_TOKEN_TTL_SECONDS`, no longer than `TOKEN_TTL_MAX_SECONDS`, so a key rotation never
|
|
10
10
|
* revokes a live one (`signing-keys.ts`).
|
|
11
11
|
*
|
|
12
12
|
* ```text
|
package/dist/tenant/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TenantEnvelope } from "@nylorun/core/contracts";
|
|
1
|
+
import type { HarnessStatus, TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
2
|
import { type ModelProvider } from "../core/provider.js";
|
|
3
3
|
import type { ModelGate } from "../gates/model-gate.js";
|
|
4
4
|
import { type AuthorizeResult } from "../vault/service.js";
|
|
@@ -14,7 +14,6 @@ import type { HarnessChannel, MemoryPortsOptions } from "@nylorun/core/harness-a
|
|
|
14
14
|
import { type HarnessPeer } from "../harness-api/server.js";
|
|
15
15
|
import { type HostAuthority } from "../sandbox/join.js";
|
|
16
16
|
import type { TenantPods } from "./context.js";
|
|
17
|
-
import { type OperatorKeys } from "./operator-keys.js";
|
|
18
17
|
/** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
|
|
19
18
|
export type TenantOpenHooks = {
|
|
20
19
|
modelProvider?: ModelProvider;
|
|
@@ -24,8 +23,8 @@ export type TenantOpenHooks = {
|
|
|
24
23
|
*/
|
|
25
24
|
modelGate?: ModelGate;
|
|
26
25
|
/**
|
|
27
|
-
* Serves the Tenant's remote MCP servers and
|
|
28
|
-
* Without one, the Tenant opens
|
|
26
|
+
* Serves the Tenant's remote MCP servers and HTTP tools (the gates service's client).
|
|
27
|
+
* Without one, the Tenant opens and calls them in this process.
|
|
29
28
|
*/
|
|
30
29
|
toolGate?: ToolGate;
|
|
31
30
|
/**
|
|
@@ -120,8 +119,7 @@ export declare class TenantRuntime implements TenantHandle {
|
|
|
120
119
|
/** Adds a callback to the Tenant sweep. Returns a function that removes it. */
|
|
121
120
|
onSweep(hook: () => Promise<void>): () => void;
|
|
122
121
|
attachHarness(channel: HarnessChannel, peer: HarnessPeer): () => void;
|
|
123
|
-
|
|
124
|
-
operatorKeys(): OperatorKeys;
|
|
122
|
+
harnessStatus(): HarnessStatus;
|
|
125
123
|
close(): Promise<void>;
|
|
126
124
|
}
|
|
127
125
|
/**
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -18,7 +18,6 @@ import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
|
|
|
18
18
|
import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
|
|
19
19
|
import { advance } from "./advance.js";
|
|
20
20
|
import { sweep } from "./sweep.js";
|
|
21
|
-
import { deliverAction } from "./delivery.js";
|
|
22
21
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
23
22
|
import { authorize } from "./effects.js";
|
|
24
23
|
import { inProcessToolGate } from "../gates/tool-gate.js";
|
|
@@ -33,7 +32,7 @@ import { localWorkspace, remoteWorkspace, withPodWorkspaces, } from "../harness-
|
|
|
33
32
|
import { hostAuthority } from "../sandbox/join.js";
|
|
34
33
|
import { reconcileSandbox } from "../sandbox/pods/reconcile.js";
|
|
35
34
|
import { sandboxWorkspaceReader } from "../artifacts/workspace.js";
|
|
36
|
-
import {
|
|
35
|
+
import { readDefinitionFile } from "./definition-files.js";
|
|
37
36
|
/** Default ownership lease of an advance; the heartbeat renews it every third. */
|
|
38
37
|
const DEFAULT_OWNER_LEASE_MS = 30_000;
|
|
39
38
|
function validateConfig(config) {
|
|
@@ -82,9 +81,7 @@ export class TenantRuntime {
|
|
|
82
81
|
const paths = config.paths;
|
|
83
82
|
mkdirSync(paths.root, { recursive: true });
|
|
84
83
|
mkdirSync(paths.home, { recursive: true });
|
|
85
|
-
mkdirSync(paths.tmp, { recursive: true });
|
|
86
84
|
mkdirSync(paths.sandboxes, { recursive: true });
|
|
87
|
-
mkdirSync(paths.pluginData, { recursive: true });
|
|
88
85
|
mkdirSync(paths.logs, { recursive: true });
|
|
89
86
|
const store = hooks.store;
|
|
90
87
|
let wired;
|
|
@@ -131,7 +128,12 @@ export class TenantRuntime {
|
|
|
131
128
|
});
|
|
132
129
|
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
133
130
|
let ctx;
|
|
134
|
-
const toolGate = hooks.toolGate ??
|
|
131
|
+
const toolGate = hooks.toolGate ??
|
|
132
|
+
inProcessToolGate(config.delivery ?? {}, {
|
|
133
|
+
session: (sessionId) => opened.tx((t) => t.get("sessions", sessionId)),
|
|
134
|
+
authorize: (sessionId, request) => authorize(ctx, sessionId, request),
|
|
135
|
+
logger: config.logger,
|
|
136
|
+
});
|
|
135
137
|
const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
|
|
136
138
|
const keys = hooks.keys ?? inProcessKeys({ store: opened, vault, signingKeys, kek: ensureKek });
|
|
137
139
|
const harnessMode = hooks.harness ?? "memory";
|
|
@@ -141,9 +143,8 @@ export class TenantRuntime {
|
|
|
141
143
|
// executors; otherwise the harness holds both (F6.2).
|
|
142
144
|
const mcp = inProcess
|
|
143
145
|
? new McpPool({
|
|
144
|
-
pluginData: paths.pluginData,
|
|
145
|
-
childEnv: config.childEnv,
|
|
146
146
|
authorize: (sessionId, request) => authorize(ctx, sessionId, request),
|
|
147
|
+
policy: config.delivery ?? {},
|
|
147
148
|
...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
|
|
148
149
|
})
|
|
149
150
|
: undefined;
|
|
@@ -156,6 +157,12 @@ export class TenantRuntime {
|
|
|
156
157
|
defaultSandboxBackends({ root: paths.sandboxes }),
|
|
157
158
|
preference,
|
|
158
159
|
ephemeral,
|
|
160
|
+
definitionFile: async (sha256) => {
|
|
161
|
+
const bytes = await readDefinitionFile(blobs, sha256);
|
|
162
|
+
if (!bytes)
|
|
163
|
+
throw new Error(`The Object store has no bytes for ${sha256}`);
|
|
164
|
+
return bytes;
|
|
165
|
+
},
|
|
159
166
|
emit: async (sessionId, turnId, type, payload) => {
|
|
160
167
|
if (ctx.closed)
|
|
161
168
|
return;
|
|
@@ -230,6 +237,7 @@ export class TenantRuntime {
|
|
|
230
237
|
ctx = {
|
|
231
238
|
config,
|
|
232
239
|
envelope,
|
|
240
|
+
reads: hooks.reads,
|
|
233
241
|
store: opened,
|
|
234
242
|
vault,
|
|
235
243
|
credentials,
|
|
@@ -260,11 +268,6 @@ export class TenantRuntime {
|
|
|
260
268
|
await execution.wake(config.tenantId, sessionId, wake);
|
|
261
269
|
},
|
|
262
270
|
abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
|
|
263
|
-
deliver: async (actionId) => {
|
|
264
|
-
if (ctx.closing || ctx.closed)
|
|
265
|
-
return;
|
|
266
|
-
await execution.deliver(config.tenantId, actionId);
|
|
267
|
-
},
|
|
268
271
|
...(hooks.pods ? { pods: hooks.pods } : {}),
|
|
269
272
|
sandboxSignal: async (sandboxId, signal) => {
|
|
270
273
|
if (ctx.closing || ctx.closed || !execution.sandbox)
|
|
@@ -304,7 +307,6 @@ export class TenantRuntime {
|
|
|
304
307
|
// sessions a previous process left runnable or running (takeover handles the rest).
|
|
305
308
|
const worker = {
|
|
306
309
|
advance: (sessionId, signal) => advance(ctx, sessionId, signal),
|
|
307
|
-
deliver: (actionId, signal) => deliverAction(ctx, actionId, signal),
|
|
308
310
|
sweep: () => sweep(ctx),
|
|
309
311
|
sandbox: (sandboxId, trigger) => reconcileSandbox(ctx, sandboxId, trigger),
|
|
310
312
|
};
|
|
@@ -322,6 +324,7 @@ export class TenantRuntime {
|
|
|
322
324
|
await detach?.().catch(() => undefined);
|
|
323
325
|
await harness?.stop().catch(() => undefined);
|
|
324
326
|
await wired?.close().catch(() => undefined);
|
|
327
|
+
await hooks.reads?.close().catch(() => undefined);
|
|
325
328
|
await store.close().catch(() => undefined);
|
|
326
329
|
throw error;
|
|
327
330
|
}
|
|
@@ -338,8 +341,6 @@ export class TenantRuntime {
|
|
|
338
341
|
return {
|
|
339
342
|
ready: !this.ctx.closing && !this.ctx.closed,
|
|
340
343
|
runningSessions: counts.runningSessions,
|
|
341
|
-
inFlightDeliveries: [...this.ctx.work.deliveries.values()].reduce((n, set) => n + set.size, 0),
|
|
342
|
-
pendingActions: counts.pendingActions,
|
|
343
344
|
uncertainEffects: counts.uncertainEffects,
|
|
344
345
|
harness: harnessStatusOf(this.ctx),
|
|
345
346
|
};
|
|
@@ -368,9 +369,8 @@ export class TenantRuntime {
|
|
|
368
369
|
attachHarness(channel, peer) {
|
|
369
370
|
return this.ctx.harness.attach(channel, peer);
|
|
370
371
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return operatorKeys(this.ctx.store);
|
|
372
|
+
harnessStatus() {
|
|
373
|
+
return harnessStatusOf(this.ctx);
|
|
374
374
|
}
|
|
375
375
|
async close() {
|
|
376
376
|
const ctx = this.ctx;
|
|
@@ -402,6 +402,7 @@ export class TenantRuntime {
|
|
|
402
402
|
return closeStreams(ctx);
|
|
403
403
|
},
|
|
404
404
|
],
|
|
405
|
+
["reads", () => ctx.reads?.close()],
|
|
405
406
|
["store", () => ctx.store.close()],
|
|
406
407
|
];
|
|
407
408
|
let failure;
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { type PutSandboxRequest, type SandboxView } from "@nylorun/core/contracts";
|
|
32
32
|
import type { SandboxResource, Tx } from "../store/types.js";
|
|
33
|
-
import type {
|
|
33
|
+
import type { SandboxRouteDeps } from "../core/sandbox-routes.js";
|
|
34
|
+
import { type AuthScope, type Session, type TenantContext } from "./context.js";
|
|
34
35
|
/** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
|
|
35
36
|
export declare function sandboxGrantsOf(scope: AuthScope): readonly string[] | undefined;
|
|
36
37
|
/** A sandbox resource, unless it is gone or being deleted. */
|
|
@@ -117,3 +118,5 @@ export declare function touchPodSandbox(ctx: TenantContext, id: string): Promise
|
|
|
117
118
|
* timer is set). Best effort.
|
|
118
119
|
*/
|
|
119
120
|
export declare function rearmPodTimers(ctx: TenantContext): Promise<void>;
|
|
121
|
+
/** Session lookups for the sandbox route handlers (`core/sandbox-routes.ts`). */
|
|
122
|
+
export declare function sandboxRouteDeps(ctx: TenantContext): SandboxRouteDeps;
|
package/dist/tenant/sandboxes.js
CHANGED
|
@@ -37,6 +37,7 @@ import { podName } from "../sandbox/pods/name.js";
|
|
|
37
37
|
import { expiresAtOf, podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
38
38
|
import { requirePods } from "../sandbox/placement.js";
|
|
39
39
|
import { parseSandboxDuration } from "@nylorun/core/define";
|
|
40
|
+
import { sandboxLookup, sessionOf } from "./context.js";
|
|
40
41
|
import { fail } from "./http.js";
|
|
41
42
|
/** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
|
|
42
43
|
export function sandboxGrantsOf(scope) {
|
|
@@ -549,3 +550,13 @@ export async function rearmPodTimers(ctx) {
|
|
|
549
550
|
});
|
|
550
551
|
}
|
|
551
552
|
}
|
|
553
|
+
/** Session lookups for the sandbox route handlers (`core/sandbox-routes.ts`). */
|
|
554
|
+
export function sandboxRouteDeps(ctx) {
|
|
555
|
+
return {
|
|
556
|
+
sandbox: ctx.sandbox,
|
|
557
|
+
session: (id) => ctx.store.tx(async (t) => {
|
|
558
|
+
const session = await sessionOf(t, id);
|
|
559
|
+
return { session, lookup: await sandboxLookup(t, id) };
|
|
560
|
+
}),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
@@ -5,14 +5,10 @@ export interface WorkState {
|
|
|
5
5
|
readonly running: Map<string, AbortController>;
|
|
6
6
|
/** The turn each running advance belongs to, by session id (`null`: none). */
|
|
7
7
|
readonly runningTurns: Map<string, string | null>;
|
|
8
|
-
/** Deliveries to Action endpoints in flight on this process, by their Action's session id. */
|
|
9
|
-
readonly deliveries: Map<string, Set<AbortController>>;
|
|
10
|
-
/** When each Action last reported a failed delivery (`action.delivery_failed` throttling). */
|
|
11
|
-
readonly deliveryNotices: Map<string, number>;
|
|
12
8
|
}
|
|
13
9
|
export declare function createWorkState(): WorkState;
|
|
14
10
|
/**
|
|
15
|
-
* Abort the advance of `id` if it runs on this process
|
|
11
|
+
* Abort the advance of `id` if it runs on this process.
|
|
16
12
|
* Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
|
|
17
13
|
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
18
14
|
*/
|
package/dist/tenant/scheduler.js
CHANGED
|
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
|
|
|
13
13
|
import { command } from "./commands.js";
|
|
14
14
|
import { AdvanceAbort } from "./worker.js";
|
|
15
15
|
export function createWorkState() {
|
|
16
|
-
return { running: new Map(), runningTurns: new Map()
|
|
16
|
+
return { running: new Map(), runningTurns: new Map() };
|
|
17
17
|
}
|
|
18
18
|
const MESSAGES = {
|
|
19
19
|
cancel: "Turn cancelled",
|
|
@@ -26,7 +26,7 @@ function abortWith(controller, kind) {
|
|
|
26
26
|
controller.abort(new AdvanceAbort(kind, MESSAGES[kind]));
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Abort the advance of `id` if it runs on this process
|
|
29
|
+
* Abort the advance of `id` if it runs on this process.
|
|
30
30
|
* Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
|
|
31
31
|
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
32
32
|
*/
|
|
@@ -34,9 +34,6 @@ export function abortLocal(ctx, id, kind = "cancel", turnId) {
|
|
|
34
34
|
const controller = ctx.work.running.get(id);
|
|
35
35
|
if (controller && (turnId === undefined || ctx.work.runningTurns.get(id) === turnId))
|
|
36
36
|
abortWith(controller, kind);
|
|
37
|
-
// A cancel also closes the session's deliveries, which aborts the tools' `ctx.signal`.
|
|
38
|
-
for (const delivery of ctx.work.deliveries.get(id) ?? [])
|
|
39
|
-
abortWith(delivery, kind);
|
|
40
37
|
}
|
|
41
38
|
/** Reset: abort every advance running on this process; their sessions are being cleared. */
|
|
42
39
|
export function clearWork(ctx) {
|
|
@@ -79,9 +79,10 @@ export declare function currentBasin(ctx: TenantContext): string;
|
|
|
79
79
|
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
80
80
|
* was), whatever the `agent` filter kept.
|
|
81
81
|
*/
|
|
82
|
-
export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined): Promise<{
|
|
82
|
+
export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined, limit?: number): Promise<{
|
|
83
83
|
items: LiveEvent[];
|
|
84
84
|
cursor: string | null;
|
|
85
|
+
tail?: boolean;
|
|
85
86
|
}>;
|
|
86
87
|
/**
|
|
87
88
|
* `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live delivery over Durable Streams (architecture §12.4, "Reading"): session history
|
|
3
|
+
* (`GET /v1/sessions/:id/items`) and session SSE (`GET /v1/sessions/:id/events`).
|
|
4
|
+
*
|
|
5
|
+
* - **Streams.** A session's events are in `sessions/<id>` in the Tenant's current basin
|
|
6
|
+
* generation (`currentBasin`); cursors carry only the session id and sequence.
|
|
7
|
+
* - **History** reads the session's stream from the cursor up to the tail seen when the read
|
|
8
|
+
* starts. The `agent` filter runs here, and the response cursor is the last record read. If
|
|
9
|
+
* the streams fail, history answers `503`.
|
|
10
|
+
* - **SSE** shares one stream read per observed session in this process (a `SessionStream`)
|
|
11
|
+
* among that session's observers. Each observer keeps the next sequence it needs and skips
|
|
12
|
+
* what it already has, so a client resuming from `Last-Event-ID` sees no gap and no
|
|
13
|
+
* duplicate. An observer behind the feed restarts the shared read from its own position.
|
|
14
|
+
* A feed follows one basin generation: it ends once its session is gone or the Tenant moved
|
|
15
|
+
* to a new generation (a reset, possibly on another node), checked when a client joins, when
|
|
16
|
+
* its read fails, on a `sessions.reset` signal, and periodically (`checkSessionStreams`).
|
|
17
|
+
*
|
|
18
|
+
* Business code never writes here: events reach streams only through the stream relay, from
|
|
19
|
+
* the record, after commit.
|
|
20
|
+
*/
|
|
21
|
+
import { historyCursor, historyResume } from "../reads/cursor.js";
|
|
1
22
|
import { decodeCursor, encodeCursor } from "../record/index.js";
|
|
2
23
|
import { basinOf } from "../streams/basin.js";
|
|
3
24
|
import { sessionStream, } from "../streams/types.js";
|
|
@@ -85,17 +106,27 @@ function streamsOf(ctx) {
|
|
|
85
106
|
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
86
107
|
* was), whatever the `agent` filter kept.
|
|
87
108
|
*/
|
|
88
|
-
export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
89
|
-
const
|
|
109
|
+
export async function readHistory(ctx, sessionId, cursor, agent, limit) {
|
|
110
|
+
const codec = limit === undefined ? undefined : historyCursor(ctx.config.tenantId, sessionId, agent);
|
|
111
|
+
const from = startSeq(sessionId, codec ? codec.decode(cursor) : historyResume(ctx.config.tenantId, sessionId, cursor));
|
|
90
112
|
const streams = streamsOf(ctx);
|
|
91
113
|
const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
|
|
92
114
|
const items = [];
|
|
93
115
|
let last;
|
|
116
|
+
let atTail = true;
|
|
117
|
+
let examined = 0;
|
|
94
118
|
try {
|
|
119
|
+
const tail = limit === undefined ? undefined : await streams.tail(ref.basin, ref.stream);
|
|
95
120
|
for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
|
|
121
|
+
if (tail !== undefined && record.seq >= tail)
|
|
122
|
+
break;
|
|
96
123
|
last = record.seq;
|
|
97
124
|
if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
|
|
98
125
|
items.push(record.body);
|
|
126
|
+
if (limit !== undefined && ++examined >= limit) {
|
|
127
|
+
atTail = last >= tail - 1;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
99
130
|
}
|
|
100
131
|
}
|
|
101
132
|
catch (error) {
|
|
@@ -107,7 +138,10 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
|
107
138
|
}
|
|
108
139
|
return {
|
|
109
140
|
items,
|
|
110
|
-
|
|
141
|
+
...(limit === undefined ? {} : { tail: atTail }),
|
|
142
|
+
cursor: last !== undefined
|
|
143
|
+
? (codec ? codec.encode(encodeCursor(sessionId, last)) : encodeCursor(sessionId, last))
|
|
144
|
+
: cursor ?? null,
|
|
111
145
|
};
|
|
112
146
|
}
|
|
113
147
|
/**
|
|
@@ -117,7 +151,7 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
|
117
151
|
export async function streamSessionEvents(ctx, request, response, sessionId, cursor, holder) {
|
|
118
152
|
const observer = {
|
|
119
153
|
sink: sseSink(response),
|
|
120
|
-
next: startSeq(sessionId, cursor),
|
|
154
|
+
next: startSeq(sessionId, historyResume(ctx.config.tenantId, sessionId, cursor)),
|
|
121
155
|
...(holder ? { holder } : {}),
|
|
122
156
|
};
|
|
123
157
|
const joined = await join(ctx, sessionId, observer);
|
|
@@ -162,7 +196,7 @@ export async function* observeSession(ctx, sessionId, cursor, options) {
|
|
|
162
196
|
notify();
|
|
163
197
|
},
|
|
164
198
|
},
|
|
165
|
-
next: startSeq(sessionId, cursor),
|
|
199
|
+
next: startSeq(sessionId, historyResume(ctx.config.tenantId, sessionId, cursor)),
|
|
166
200
|
...(options.holder ? { holder: options.holder } : {}),
|
|
167
201
|
};
|
|
168
202
|
const joined = await join(ctx, sessionId, observer);
|