@nylorun/runtime 0.12.0-beta → 0.14.0-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 +361 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +7825 -0
- package/dist/api/components.js +83 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +130 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +259 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +10 -0
- package/dist/api/http/routes/tenant.js +172 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +138 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +19 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25696 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +29 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.js +32 -0
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +121 -129
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +138 -48
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
package/dist/tenant/live.d.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live delivery over Durable Streams (architecture §12.4, "Reading"): session history
|
|
3
|
-
* (`GET /v1/sessions/:id/items`), session SSE (`GET /v1/sessions/:id/events`) and executor
|
|
4
|
-
* work streams (`GET /v1/executors/connect`).
|
|
5
|
-
*
|
|
6
|
-
* - **Streams.** A session's events are in the stream of its current incarnation
|
|
7
|
-
* (`streamOfSession`, `sessions/<id>/<incarnation>`), resolved from the session on every
|
|
8
|
-
* request; cursors carry only the session id and sequence.
|
|
9
|
-
* - **History** reads the session's stream from the cursor up to the tail seen when the read
|
|
10
|
-
* starts. The `agent` filter runs here, and the response cursor is the last record read. If
|
|
11
|
-
* the streams fail, history answers `503`.
|
|
12
|
-
* - **SSE** shares one stream read per observed session in this process (a `SessionFeed`)
|
|
13
|
-
* among that session's observers. Each observer keeps the next sequence it needs and skips
|
|
14
|
-
* what it already has, so a client resuming from `Last-Event-ID` sees no gap and no
|
|
15
|
-
* duplicate. An observer behind the feed restarts the shared read from its own position.
|
|
16
|
-
* A feed follows one incarnation: it ends once its session is gone or has a new
|
|
17
|
-
* incarnation (a reset, possibly on another node), checked when a client joins, when its
|
|
18
|
-
* read fails, on a `sessions.reset` signal, and periodically (`checkFeeds`).
|
|
19
|
-
* - **Executor streams** receive `work_available` from the `tenant/work` reader that
|
|
20
|
-
* `tenant/streams.ts` runs (one per Tenant per process); discovery stays `GET /v1/actions`.
|
|
21
|
-
*
|
|
22
|
-
* Business code never writes here: events reach streams only through the relay, after commit.
|
|
23
|
-
*/
|
|
24
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
25
|
-
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
26
|
-
import type { TenantContext } from "./context.js";
|
|
27
|
-
import type { StreamsWiring } from "./streams.js";
|
|
28
|
-
/** One SSE client of a session: the next sequence it needs. */
|
|
29
|
-
interface Observer {
|
|
30
|
-
readonly response: ServerResponse;
|
|
31
|
-
next: number;
|
|
32
|
-
}
|
|
33
|
-
/** The shared stream read of one observed session. */
|
|
34
|
-
interface SessionFeed {
|
|
35
|
-
readonly sessionId: string;
|
|
36
|
-
/** The stream of the session's incarnation when the feed started. */
|
|
37
|
-
readonly stream: string;
|
|
38
|
-
/** The sequence the shared read yields next. Every observer's `next` is at or past it. */
|
|
39
|
-
next: number;
|
|
40
|
-
readonly observers: Set<Observer>;
|
|
41
|
-
/** Aborts the current read; replaced when the read restarts from an earlier sequence. */
|
|
42
|
-
read: AbortController;
|
|
43
|
-
}
|
|
44
|
-
export interface LiveHub {
|
|
45
|
-
/** Set once by `wireStreams`. */
|
|
46
|
-
wiring: StreamsWiring | undefined;
|
|
47
|
-
/** Session id → its shared read and observers. */
|
|
48
|
-
readonly feeds: Map<string, SessionFeed>;
|
|
49
|
-
readonly executorStreams: Map<string, Set<ServerResponse>>;
|
|
50
|
-
}
|
|
51
|
-
export declare function createLiveHub(): LiveHub;
|
|
52
|
-
export declare function send(response: ServerResponse, data: string): void;
|
|
53
|
-
/** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
|
|
54
|
-
export declare function openSse(request: IncomingMessage, response: ServerResponse, onClose: () => void): void;
|
|
55
|
-
/** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
|
|
56
|
-
export declare function requestCursor(request: IncomingMessage, url: URL): string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
* `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
|
|
59
|
-
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
60
|
-
* was), whatever the `agent` filter kept.
|
|
61
|
-
*/
|
|
62
|
-
export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined): Promise<{
|
|
63
|
-
items: LiveEvent[];
|
|
64
|
-
cursor: string | null;
|
|
65
|
-
}>;
|
|
66
|
-
/**
|
|
67
|
-
* `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
|
|
68
|
-
* follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
|
|
69
|
-
*/
|
|
70
|
-
export declare function streamSessionEvents(ctx: TenantContext, request: IncomingMessage, response: ServerResponse, sessionId: string, cursor: string | undefined): Promise<void>;
|
|
71
|
-
/**
|
|
72
|
-
* Ends every feed in this process whose session is gone or has a new incarnation. Run on a
|
|
73
|
-
* `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
|
|
74
|
-
* the Tenant end too.
|
|
75
|
-
*/
|
|
76
|
-
export declare function checkFeeds(ctx: TenantContext): Promise<void>;
|
|
77
|
-
/** `GET /v1/executors/connect`: an executor's work stream, primed with one `work_available`. */
|
|
78
|
-
export declare function streamExecutorWork(hub: LiveHub, request: IncomingMessage, response: ServerResponse, tokenHash: string): void;
|
|
79
|
-
/** Called by the `tenant/work` reader for each signal: wake every connected executor. */
|
|
80
|
-
export declare function announceWork(hub: LiveHub): void;
|
|
81
|
-
export declare function executorConnected(hub: LiveHub, tokenHash: string): boolean;
|
|
82
|
-
export declare function connectedExecutorCount(hub: LiveHub): number;
|
|
83
|
-
/** End the streams a token owns (rotation or deregistration). */
|
|
84
|
-
export declare function endExecutorStreams(hub: LiveHub, tokenHash: string): void;
|
|
85
|
-
/** Reset or close: end every session observer and stop every shared read. */
|
|
86
|
-
export declare function clearObservers(hub: LiveHub): void;
|
|
87
|
-
/** Reset: end and forget every executor stream. */
|
|
88
|
-
export declare function clearExecutorStreams(hub: LiveHub): void;
|
|
89
|
-
/** Close: end every executor stream, then every session observer. */
|
|
90
|
-
export declare function endAllStreams(hub: LiveHub): void;
|
|
91
|
-
export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
92
|
-
export {};
|
package/dist/tenant/live.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { decodeCursor, encodeCursor } from "../store/cursor.js";
|
|
2
|
-
import { streamOfSession, } from "../streams/types.js";
|
|
3
|
-
import { fail } from "./http.js";
|
|
4
|
-
export function createLiveHub() {
|
|
5
|
-
return { wiring: undefined, feeds: new Map(), executorStreams: new Map() };
|
|
6
|
-
}
|
|
7
|
-
const WORK_AVAILABLE = 'event: work_available\ndata: {"type":"work_available"}\n\n';
|
|
8
|
-
const RETRY_MIN_MS = 100;
|
|
9
|
-
const RETRY_MAX_MS = 2000;
|
|
10
|
-
function frame(event) {
|
|
11
|
-
return `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
12
|
-
}
|
|
13
|
-
export function send(response, data) {
|
|
14
|
-
if (!response.write(data))
|
|
15
|
-
response.destroy();
|
|
16
|
-
}
|
|
17
|
-
/** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
|
|
18
|
-
export function openSse(request, response, onClose) {
|
|
19
|
-
response.writeHead(200, {
|
|
20
|
-
"content-type": "text/event-stream",
|
|
21
|
-
"cache-control": "no-cache",
|
|
22
|
-
connection: "keep-alive",
|
|
23
|
-
"x-accel-buffering": "no",
|
|
24
|
-
});
|
|
25
|
-
response.flushHeaders();
|
|
26
|
-
const timer = setInterval(() => send(response, ": keepalive\n\n"), 15000);
|
|
27
|
-
timer.unref();
|
|
28
|
-
request.on("close", () => {
|
|
29
|
-
clearInterval(timer);
|
|
30
|
-
onClose();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
/** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
|
|
34
|
-
export function requestCursor(request, url) {
|
|
35
|
-
return (url.searchParams.get("cursor") ??
|
|
36
|
-
(typeof request.headers["last-event-id"] === "string"
|
|
37
|
-
? request.headers["last-event-id"]
|
|
38
|
-
: undefined));
|
|
39
|
-
}
|
|
40
|
-
/** The first sequence to read: after the cursor's, or the session's first event. */
|
|
41
|
-
function startSeq(sessionId, cursor) {
|
|
42
|
-
return cursor ? decodeCursor(sessionId, cursor) + 1 : 0;
|
|
43
|
-
}
|
|
44
|
-
/** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
|
|
45
|
-
function belongsTo(event, agent) {
|
|
46
|
-
const ref = event.payload?.agent;
|
|
47
|
-
return ref?.delegationId === agent || ref?.path === agent;
|
|
48
|
-
}
|
|
49
|
-
/** The stream of the session's current incarnation, or undefined when the session is gone. */
|
|
50
|
-
async function currentStream(ctx, sessionId) {
|
|
51
|
-
const session = await ctx.store.tx((t) => t.get("sessions", sessionId));
|
|
52
|
-
return session && streamOfSession(session);
|
|
53
|
-
}
|
|
54
|
-
function streamsOf(ctx) {
|
|
55
|
-
return (ctx.live.wiring?.streams ?? fail(503, "Session streams are unavailable"));
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
|
|
59
|
-
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
60
|
-
* was), whatever the `agent` filter kept.
|
|
61
|
-
*/
|
|
62
|
-
export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
63
|
-
const from = startSeq(sessionId, cursor);
|
|
64
|
-
const streams = streamsOf(ctx);
|
|
65
|
-
const stream = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
|
|
66
|
-
const items = [];
|
|
67
|
-
let last;
|
|
68
|
-
try {
|
|
69
|
-
for await (const record of streams.read(ctx.config.tenantId, stream, from, { follow: false })) {
|
|
70
|
-
last = record.seq;
|
|
71
|
-
if (agent === undefined || belongsTo(record.body, agent))
|
|
72
|
-
items.push(record.body);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
ctx.config.logger.warn("session history read failed", {
|
|
77
|
-
sessionId,
|
|
78
|
-
message: error instanceof Error ? error.message : String(error),
|
|
79
|
-
});
|
|
80
|
-
fail(503, "Session history is unavailable");
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
items,
|
|
84
|
-
cursor: last !== undefined ? encodeCursor(sessionId, last) : cursor ?? null,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
|
|
89
|
-
* follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
|
|
90
|
-
*/
|
|
91
|
-
export async function streamSessionEvents(ctx, request, response, sessionId, cursor) {
|
|
92
|
-
const from = startSeq(sessionId, cursor);
|
|
93
|
-
streamsOf(ctx);
|
|
94
|
-
const stream = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
|
|
95
|
-
const hub = ctx.live;
|
|
96
|
-
const observer = { response, next: from };
|
|
97
|
-
let feed = hub.feeds.get(sessionId);
|
|
98
|
-
if (feed && feed.stream !== stream) {
|
|
99
|
-
// The session was created again since that feed started: it follows an abandoned stream.
|
|
100
|
-
endFeed(hub, feed);
|
|
101
|
-
feed = undefined;
|
|
102
|
-
}
|
|
103
|
-
if (!feed) {
|
|
104
|
-
feed = {
|
|
105
|
-
sessionId,
|
|
106
|
-
stream,
|
|
107
|
-
next: from,
|
|
108
|
-
observers: new Set(),
|
|
109
|
-
read: new AbortController(),
|
|
110
|
-
};
|
|
111
|
-
hub.feeds.set(sessionId, feed);
|
|
112
|
-
runFeed(ctx, feed);
|
|
113
|
-
}
|
|
114
|
-
else if (from < feed.next) {
|
|
115
|
-
// Behind the shared read: restart it here; observers ahead skip what they have.
|
|
116
|
-
feed.read.abort();
|
|
117
|
-
feed.read = new AbortController();
|
|
118
|
-
feed.next = from;
|
|
119
|
-
runFeed(ctx, feed);
|
|
120
|
-
}
|
|
121
|
-
const joined = feed;
|
|
122
|
-
joined.observers.add(observer);
|
|
123
|
-
openSse(request, response, () => leave(hub, joined, observer));
|
|
124
|
-
}
|
|
125
|
-
function leave(hub, feed, observer) {
|
|
126
|
-
feed.observers.delete(observer);
|
|
127
|
-
if (feed.observers.size > 0)
|
|
128
|
-
return;
|
|
129
|
-
feed.read.abort();
|
|
130
|
-
if (hub.feeds.get(feed.sessionId) === feed)
|
|
131
|
-
hub.feeds.delete(feed.sessionId);
|
|
132
|
-
}
|
|
133
|
-
function deliver(observer, record) {
|
|
134
|
-
if (record.seq < observer.next)
|
|
135
|
-
return;
|
|
136
|
-
observer.next = record.seq + 1;
|
|
137
|
-
send(observer.response, frame(record.body));
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
|
|
141
|
-
* reads from where it stopped while the session still has that stream. A read that ends by
|
|
142
|
-
* itself (the stream's Tenant is gone or the streams closed), or a session that is gone or
|
|
143
|
-
* has a new incarnation, ends the observers.
|
|
144
|
-
*/
|
|
145
|
-
function runFeed(ctx, feed) {
|
|
146
|
-
const read = feed.read;
|
|
147
|
-
const signal = read.signal;
|
|
148
|
-
void (async () => {
|
|
149
|
-
let delay = RETRY_MIN_MS;
|
|
150
|
-
while (!signal.aborted) {
|
|
151
|
-
const streams = ctx.live.wiring?.streams;
|
|
152
|
-
if (!streams)
|
|
153
|
-
break;
|
|
154
|
-
try {
|
|
155
|
-
for await (const record of streams.read(ctx.config.tenantId, feed.stream, feed.next, { signal })) {
|
|
156
|
-
// A restart replaced this read: the new one owns `feed.next`.
|
|
157
|
-
if (signal.aborted || feed.read !== read)
|
|
158
|
-
return;
|
|
159
|
-
if (record.seq < feed.next)
|
|
160
|
-
continue;
|
|
161
|
-
feed.next = record.seq + 1;
|
|
162
|
-
delay = RETRY_MIN_MS;
|
|
163
|
-
for (const observer of feed.observers)
|
|
164
|
-
deliver(observer, record);
|
|
165
|
-
}
|
|
166
|
-
if (signal.aborted)
|
|
167
|
-
return;
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
if (signal.aborted)
|
|
172
|
-
return;
|
|
173
|
-
ctx.config.logger.warn("session stream read failed; retrying", {
|
|
174
|
-
sessionId: feed.sessionId,
|
|
175
|
-
message: error instanceof Error ? error.message : String(error),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (!(await stillCurrent(ctx, feed)))
|
|
179
|
-
break;
|
|
180
|
-
await sleep(delay, signal);
|
|
181
|
-
delay = Math.min(delay * 2, RETRY_MAX_MS);
|
|
182
|
-
}
|
|
183
|
-
if (signal.aborted || feed.read !== read)
|
|
184
|
-
return;
|
|
185
|
-
endFeed(ctx.live, feed);
|
|
186
|
-
})();
|
|
187
|
-
}
|
|
188
|
-
/** False when the feed's session is gone or has a new incarnation. True when unsure. */
|
|
189
|
-
async function stillCurrent(ctx, feed) {
|
|
190
|
-
try {
|
|
191
|
-
return (await currentStream(ctx, feed.sessionId)) === feed.stream;
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Ends every feed in this process whose session is gone or has a new incarnation. Run on a
|
|
199
|
-
* `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
|
|
200
|
-
* the Tenant end too.
|
|
201
|
-
*/
|
|
202
|
-
export async function checkFeeds(ctx) {
|
|
203
|
-
const feeds = [...ctx.live.feeds.values()];
|
|
204
|
-
if (feeds.length === 0)
|
|
205
|
-
return;
|
|
206
|
-
const current = await ctx.store.tx(async (t) => {
|
|
207
|
-
const streams = new Map();
|
|
208
|
-
for (const feed of feeds) {
|
|
209
|
-
const session = await t.get("sessions", feed.sessionId);
|
|
210
|
-
streams.set(feed.sessionId, session && streamOfSession(session));
|
|
211
|
-
}
|
|
212
|
-
return streams;
|
|
213
|
-
});
|
|
214
|
-
for (const feed of feeds)
|
|
215
|
-
if (ctx.live.feeds.get(feed.sessionId) === feed &&
|
|
216
|
-
current.get(feed.sessionId) !== feed.stream)
|
|
217
|
-
endFeed(ctx.live, feed);
|
|
218
|
-
}
|
|
219
|
-
function endFeed(hub, feed) {
|
|
220
|
-
feed.read.abort();
|
|
221
|
-
if (hub.feeds.get(feed.sessionId) === feed)
|
|
222
|
-
hub.feeds.delete(feed.sessionId);
|
|
223
|
-
for (const observer of feed.observers)
|
|
224
|
-
observer.response.end();
|
|
225
|
-
feed.observers.clear();
|
|
226
|
-
}
|
|
227
|
-
/** `GET /v1/executors/connect`: an executor's work stream, primed with one `work_available`. */
|
|
228
|
-
export function streamExecutorWork(hub, request, response, tokenHash) {
|
|
229
|
-
let streams = hub.executorStreams.get(tokenHash);
|
|
230
|
-
if (!streams)
|
|
231
|
-
hub.executorStreams.set(tokenHash, (streams = new Set()));
|
|
232
|
-
const set = streams;
|
|
233
|
-
set.add(response);
|
|
234
|
-
openSse(request, response, () => {
|
|
235
|
-
set.delete(response);
|
|
236
|
-
if (!set.size && hub.executorStreams.get(tokenHash) === set)
|
|
237
|
-
hub.executorStreams.delete(tokenHash);
|
|
238
|
-
});
|
|
239
|
-
send(response, WORK_AVAILABLE);
|
|
240
|
-
}
|
|
241
|
-
/** Called by the `tenant/work` reader for each signal: wake every connected executor. */
|
|
242
|
-
export function announceWork(hub) {
|
|
243
|
-
for (const streams of hub.executorStreams.values())
|
|
244
|
-
for (const response of streams)
|
|
245
|
-
send(response, WORK_AVAILABLE);
|
|
246
|
-
}
|
|
247
|
-
export function executorConnected(hub, tokenHash) {
|
|
248
|
-
return (hub.executorStreams.get(tokenHash)?.size ?? 0) > 0;
|
|
249
|
-
}
|
|
250
|
-
export function connectedExecutorCount(hub) {
|
|
251
|
-
return [...hub.executorStreams.values()].filter((set) => set.size > 0)
|
|
252
|
-
.length;
|
|
253
|
-
}
|
|
254
|
-
/** End the streams a token owns (rotation or deregistration). */
|
|
255
|
-
export function endExecutorStreams(hub, tokenHash) {
|
|
256
|
-
for (const r of hub.executorStreams.get(tokenHash) ?? [])
|
|
257
|
-
r.end();
|
|
258
|
-
hub.executorStreams.delete(tokenHash);
|
|
259
|
-
}
|
|
260
|
-
/** Reset or close: end every session observer and stop every shared read. */
|
|
261
|
-
export function clearObservers(hub) {
|
|
262
|
-
for (const feed of [...hub.feeds.values()])
|
|
263
|
-
endFeed(hub, feed);
|
|
264
|
-
hub.feeds.clear();
|
|
265
|
-
}
|
|
266
|
-
/** Reset: end and forget every executor stream. */
|
|
267
|
-
export function clearExecutorStreams(hub) {
|
|
268
|
-
for (const streams of hub.executorStreams.values())
|
|
269
|
-
for (const r of streams)
|
|
270
|
-
r.end();
|
|
271
|
-
hub.executorStreams.clear();
|
|
272
|
-
}
|
|
273
|
-
/** Close: end every executor stream, then every session observer. */
|
|
274
|
-
export function endAllStreams(hub) {
|
|
275
|
-
clearExecutorStreams(hub);
|
|
276
|
-
clearObservers(hub);
|
|
277
|
-
}
|
|
278
|
-
export function sleep(ms, signal) {
|
|
279
|
-
return new Promise((resolve) => {
|
|
280
|
-
if (signal.aborted)
|
|
281
|
-
return resolve();
|
|
282
|
-
const done = () => {
|
|
283
|
-
clearTimeout(timer);
|
|
284
|
-
signal.removeEventListener("abort", done);
|
|
285
|
-
resolve();
|
|
286
|
-
};
|
|
287
|
-
const timer = setTimeout(done, ms);
|
|
288
|
-
signal.addEventListener("abort", done, { once: true });
|
|
289
|
-
});
|
|
290
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tenant administration and vault routes: `/v1/tenant` (status, reset, config seed, host
|
|
3
|
-
* model and providers, sandbox report) and `/v1/vaults` (vaults and credentials).
|
|
4
|
-
* A reset that deletes sessions abandons their streams (a session created again gets a new
|
|
5
|
-
* incarnation) and collects them afterwards; it never deletes the Tenant's basin.
|
|
6
|
-
*
|
|
7
|
-
* Later waves: Wave 2 / X changes what reset clears once work leaves process memory.
|
|
8
|
-
*/
|
|
9
|
-
import type { IncomingMessage } from "node:http";
|
|
10
|
-
import type { AuthScope, TenantContext } from "./context.js";
|
|
11
|
-
export declare function dispatchTenant(ctx: TenantContext, scope: AuthScope, method: string | undefined, path: string[], request: IncomingMessage): Promise<unknown>;
|
|
12
|
-
export declare function dispatchVault(ctx: TenantContext, scope: AuthScope, method: string | undefined, path: string[], url: URL, request: IncomingMessage): Promise<unknown>;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { CreateCredentialRequestSchema, CreateVaultRequestSchema, PutHostModelRequestSchema, ResetTenantRequestSchema, RotateCredentialRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
2
|
-
import { hostModelCatalog } from "../model/catalog.js";
|
|
3
|
-
import { resetTenant } from "./reset.js";
|
|
4
|
-
import { buildTenantStatus, seedTenantConfig } from "./status.js";
|
|
5
|
-
import { ownerOf } from "./auth.js";
|
|
6
|
-
import { fail, readBody } from "./http.js";
|
|
7
|
-
import { clearExecutorStreams, clearObservers } from "./live.js";
|
|
8
|
-
import { requestStreamCollection, sessionStreamsAbandoned, streamsStatus, } from "./streams.js";
|
|
9
|
-
import { clearWork, drain } from "./scheduler.js";
|
|
10
|
-
import { usesFixtureModel } from "./model-setting.js";
|
|
11
|
-
export async function dispatchTenant(ctx, scope, method, path, request) {
|
|
12
|
-
const { vault } = ctx;
|
|
13
|
-
// A subject reaches only the routes `authorize` allowed its scopes.
|
|
14
|
-
if (scope.kind === "executor") {
|
|
15
|
-
await vault.reject(path.join("/"));
|
|
16
|
-
fail(403, "Application credential required");
|
|
17
|
-
}
|
|
18
|
-
if (path.length === 2 && method === "GET")
|
|
19
|
-
return await buildTenantStatus({
|
|
20
|
-
envelope: ctx.envelope,
|
|
21
|
-
config: ctx.config,
|
|
22
|
-
store: ctx.store,
|
|
23
|
-
registry: ctx.registry,
|
|
24
|
-
vault,
|
|
25
|
-
sandbox: ctx.sandbox,
|
|
26
|
-
closing: ctx.closing || ctx.closed,
|
|
27
|
-
modelConfigured: !ctx.useVaultModel ||
|
|
28
|
-
(await ctx.store.tx((t) => usesFixtureModel(t))) ||
|
|
29
|
-
(await vault.getHostModel()).configured,
|
|
30
|
-
executorStreams: ctx.live.executorStreams,
|
|
31
|
-
...(ctx.stuckInvocations
|
|
32
|
-
? { stuckInvocations: ctx.stuckInvocations }
|
|
33
|
-
: {}),
|
|
34
|
-
streamsStatus: () => streamsStatus(ctx),
|
|
35
|
-
});
|
|
36
|
-
if (path[2] === "reset" && path.length === 3 && method === "POST") {
|
|
37
|
-
const body = ResetTenantRequestSchema.parse(await readBody(request));
|
|
38
|
-
await drain(ctx, body.activeWork, 30_000);
|
|
39
|
-
// The deleted sessions' streams are abandoned: a session created again with the same id,
|
|
40
|
-
// during or after the reset, gets a new incarnation and starts at sequence 0. The pending
|
|
41
|
-
// collection is recorded first, so a crash before it runs leaves it to the sweep.
|
|
42
|
-
const sessionsReset = body.scope !== "sandboxes";
|
|
43
|
-
if (sessionsReset)
|
|
44
|
-
await requestStreamCollection(ctx);
|
|
45
|
-
await resetTenant({
|
|
46
|
-
store: ctx.store,
|
|
47
|
-
registry: ctx.registry,
|
|
48
|
-
sandbox: ctx.sandbox,
|
|
49
|
-
paths: ctx.config.paths,
|
|
50
|
-
clearSessionState: () => {
|
|
51
|
-
clearWork(ctx);
|
|
52
|
-
clearObservers(ctx.live);
|
|
53
|
-
},
|
|
54
|
-
clearExecutorStreams: () => clearExecutorStreams(ctx.live),
|
|
55
|
-
}, body.scope);
|
|
56
|
-
if (sessionsReset)
|
|
57
|
-
sessionStreamsAbandoned(ctx);
|
|
58
|
-
// Reset leaves the Tenant open for new work.
|
|
59
|
-
ctx.closing = false;
|
|
60
|
-
return { ok: true };
|
|
61
|
-
}
|
|
62
|
-
if (path[2] === "config" &&
|
|
63
|
-
path[3] === "seed" &&
|
|
64
|
-
path.length === 4 &&
|
|
65
|
-
method === "PUT") {
|
|
66
|
-
const body = SeedTenantConfigRequestSchema.parse(await readBody(request));
|
|
67
|
-
return await seedTenantConfig({ store: ctx.store, vault }, body);
|
|
68
|
-
}
|
|
69
|
-
if (path[2] === "models" && path.length === 3 && method === "GET")
|
|
70
|
-
return hostModelCatalog();
|
|
71
|
-
if (path[2] === "sandbox" && path.length === 3 && method === "GET")
|
|
72
|
-
return ctx.sandbox.report();
|
|
73
|
-
if (path[2] === "providers" && path.length === 3 && method === "GET")
|
|
74
|
-
return vault.listHostProviders();
|
|
75
|
-
if (path[2] === "model" && path.length === 3 && method === "GET")
|
|
76
|
-
return vault.getHostModel();
|
|
77
|
-
if (path[2] === "model" && path.length === 3 && method === "PUT")
|
|
78
|
-
return vault.putHostModel(PutHostModelRequestSchema.parse(await readBody(request)));
|
|
79
|
-
if (path[2] === "model" &&
|
|
80
|
-
path[3] === "selection" &&
|
|
81
|
-
path.length === 4 &&
|
|
82
|
-
method === "PUT")
|
|
83
|
-
return vault.selectHostModel(SelectHostModelRequestSchema.parse(await readBody(request)));
|
|
84
|
-
fail(404, "Route not found");
|
|
85
|
-
}
|
|
86
|
-
export async function dispatchVault(ctx, scope, method, path, url, request) {
|
|
87
|
-
const { vault } = ctx;
|
|
88
|
-
if (scope.kind === "executor") {
|
|
89
|
-
await vault.reject(path.join("/"));
|
|
90
|
-
fail(403, "Application credential required");
|
|
91
|
-
}
|
|
92
|
-
// Acting for a subject: only the subject's own vaults, and never on another's behalf.
|
|
93
|
-
const owner = ownerOf(scope);
|
|
94
|
-
if (path.length === 2 && method === "POST") {
|
|
95
|
-
const body = CreateVaultRequestSchema.parse(await readBody(request));
|
|
96
|
-
if (owner !== undefined && body.ownerUserId !== owner)
|
|
97
|
-
fail(403, "ownerUserId must be the subject");
|
|
98
|
-
return vault.createVault(body);
|
|
99
|
-
}
|
|
100
|
-
if (path.length === 2 && method === "GET") {
|
|
101
|
-
const ownerUserId = url.searchParams.get("ownerUserId") ??
|
|
102
|
-
owner ??
|
|
103
|
-
fail(400, "ownerUserId is required");
|
|
104
|
-
if (owner !== undefined && ownerUserId !== owner)
|
|
105
|
-
fail(403, "ownerUserId must be the subject");
|
|
106
|
-
return { vaults: await vault.listVaults(ownerUserId) };
|
|
107
|
-
}
|
|
108
|
-
const vaultId = path[2];
|
|
109
|
-
if (!vaultId)
|
|
110
|
-
fail(404, "Vault not found");
|
|
111
|
-
if (owner !== undefined)
|
|
112
|
-
await vault.assertOwner(vaultId, owner);
|
|
113
|
-
if (path.length === 3 && method === "GET")
|
|
114
|
-
return vault.getVault(vaultId);
|
|
115
|
-
if (path.length === 3 && method === "DELETE")
|
|
116
|
-
return vault.deleteVault(vaultId);
|
|
117
|
-
if (path[3] !== "credentials")
|
|
118
|
-
fail(404, "Route not found");
|
|
119
|
-
if (path.length === 4 && method === "POST") {
|
|
120
|
-
const body = CreateCredentialRequestSchema.parse(await readBody(request));
|
|
121
|
-
return vault.createCredential(vaultId, body);
|
|
122
|
-
}
|
|
123
|
-
if (path.length === 4 && method === "GET")
|
|
124
|
-
return { credentials: await vault.listCredentials(vaultId) };
|
|
125
|
-
const credentialId = path[4];
|
|
126
|
-
if (!credentialId)
|
|
127
|
-
fail(404, "Credential not found");
|
|
128
|
-
if (path.length === 5 && method === "GET")
|
|
129
|
-
return vault.getCredential(vaultId, credentialId);
|
|
130
|
-
if (path.length === 5 && method === "POST") {
|
|
131
|
-
const body = RotateCredentialRequestSchema.parse(await readBody(request));
|
|
132
|
-
return vault.rotateCredential(vaultId, credentialId, body);
|
|
133
|
-
}
|
|
134
|
-
if (path.length === 5 && method === "DELETE")
|
|
135
|
-
return vault.deleteCredential(vaultId, credentialId);
|
|
136
|
-
fail(404, "Route not found");
|
|
137
|
-
}
|
package/dist/tenant/routes.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Tenant HTTP router: authenticates the bearer token, matches `/v1/...` routes and
|
|
3
|
-
* delegates to the service modules (commands, actions, sessions, live, routes-tenant). It
|
|
4
|
-
* maps typed errors to responses and owns no business logic of its own. History, SSE and
|
|
5
|
-
* executor work streams read Durable Streams (`live.ts`).
|
|
6
|
-
*/
|
|
7
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
-
import { type TenantContext } from "./context.js";
|
|
9
|
-
export declare function handle(ctx: TenantContext, request: IncomingMessage, response: ServerResponse, _url?: URL): Promise<void>;
|