@nylorun/runtime 0.19.1-beta → 0.21.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 +90 -0
- package/README.md +22 -13
- package/dist/adapters/streams/s2.js +5 -11
- package/dist/api/components.d.ts +326 -205
- package/dist/api/components.js +3 -3
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +3 -2
- package/dist/api/http/define.js +10 -11
- package/dist/api/http/routes/mcp.d.ts +9 -0
- package/dist/api/http/routes/mcp.js +32 -0
- package/dist/api/http/routes/vaults.d.ts +1 -9
- package/dist/api/http/routes/vaults.js +3 -103
- package/dist/api/openapi.js +37 -6
- package/dist/artifacts/media-types.d.ts +2 -0
- package/dist/artifacts/media-types.js +5 -0
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/http-tool.d.ts +7 -0
- package/dist/gates/http-tool.js +34 -3
- package/dist/gates/mcp-handler.d.ts +4 -1
- package/dist/gates/mcp-handler.js +64 -8
- package/dist/gates/tenant-vaults.d.ts +9 -15
- package/dist/gates/tenant-vaults.js +9 -17
- package/dist/gates/tool-client.js +36 -17
- package/dist/gates/tool-contract.d.ts +19 -3
- package/dist/gates/tool-contract.js +9 -3
- package/dist/gates/tool-gate.d.ts +2 -2
- package/dist/harness/calls.d.ts +2 -0
- package/dist/harness/calls.js +13 -3
- package/dist/harness-api/record.d.ts +7 -6
- package/dist/harness-api/record.js +99 -17
- package/dist/harness-api/server.js +35 -27
- package/dist/harness-api/start.js +2 -1
- package/dist/host/app.d.ts +3 -0
- package/dist/host/app.js +33 -4
- package/dist/host/config-for.d.ts +0 -2
- package/dist/host/config-for.js +0 -1
- package/dist/host/create-host.d.ts +6 -0
- package/dist/host/create-host.js +1 -0
- package/dist/host/gates.js +0 -1
- package/dist/host/main.js +24 -7
- package/dist/host/stack-config.d.ts +5 -8
- package/dist/host/stack-config.js +9 -20
- package/dist/index.d.ts +0 -1
- package/dist/infra/index.d.ts +2 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/readiness.d.ts +6 -6
- package/dist/infra/readiness.js +2 -5
- package/dist/keys/client.js +1 -9
- package/dist/keys/keys.d.ts +11 -13
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +671 -378
- package/dist/mcp/connect.d.ts +159 -13
- package/dist/mcp/connect.js +538 -55
- package/dist/mcp/pool.d.ts +11 -2
- package/dist/mcp/pool.js +74 -27
- package/dist/mcp/preview.d.ts +35 -0
- package/dist/mcp/preview.js +215 -0
- package/dist/mcp/snapshot.d.ts +79 -5
- package/dist/mcp/snapshot.js +145 -27
- package/dist/model/catalog.d.ts +14 -0
- package/dist/model/catalog.js +14 -1
- package/dist/model/pi-model.js +36 -2
- package/dist/openapi.json +644 -266
- package/dist/redact.d.ts +19 -1
- package/dist/redact.js +42 -13
- package/dist/sandbox/session-sandbox.d.ts +7 -0
- package/dist/sandbox/session-sandbox.js +89 -28
- package/dist/store/postgres/connect.d.ts +10 -0
- package/dist/store/postgres/connect.js +30 -3
- package/dist/store/postgres/control.d.ts +14 -0
- package/dist/store/postgres/control.js +187 -0
- package/dist/store/postgres/drizzle/0015_control_signals.sql +11 -0
- package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +122 -261
- package/dist/store/postgres/schema.js +22 -36
- package/dist/store/postgres/store.js +16 -18
- package/dist/store/postgres/tenant.d.ts +14 -0
- package/dist/store/postgres/tenant.js +22 -2
- package/dist/store/types.d.ts +42 -8
- package/dist/streams/types.d.ts +6 -29
- package/dist/streams/types.js +5 -4
- package/dist/tenant/artifact-tool.d.ts +9 -1
- package/dist/tenant/artifact-tool.js +113 -9
- package/dist/tenant/auth.d.ts +16 -6
- package/dist/tenant/auth.js +41 -17
- package/dist/tenant/commands.js +10 -6
- package/dist/tenant/context.d.ts +9 -6
- package/dist/tenant/control.d.ts +26 -0
- package/dist/tenant/control.js +35 -0
- package/dist/tenant/effects.d.ts +2 -2
- package/dist/tenant/effects.js +20 -5
- package/dist/tenant/ephemeral.d.ts +0 -6
- package/dist/tenant/ephemeral.js +1 -1
- package/dist/tenant/http.d.ts +4 -2
- package/dist/tenant/http.js +4 -2
- package/dist/tenant/identity-file.d.ts +16 -12
- package/dist/tenant/identity-file.js +41 -29
- package/dist/tenant/issuers.d.ts +4 -3
- package/dist/tenant/issuers.js +22 -16
- package/dist/tenant/outbound.d.ts +14 -5
- package/dist/tenant/outbound.js +21 -9
- package/dist/tenant/reset.d.ts +2 -2
- package/dist/tenant/reset.js +8 -2
- package/dist/tenant/resource-server.d.ts +56 -0
- package/dist/tenant/resource-server.js +62 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +16 -15
- package/dist/tenant/scheduler.d.ts +1 -1
- package/dist/tenant/scheduler.js +1 -1
- package/dist/tenant/sessions.js +20 -3
- package/dist/tenant/store-pg.js +9 -0
- package/dist/tenant/streams.d.ts +16 -22
- package/dist/tenant/streams.js +7 -87
- package/dist/tenant/sweep.js +9 -1
- package/dist/tenant/tool-results.d.ts +57 -0
- package/dist/tenant/tool-results.js +298 -0
- package/dist/tenant/tool-search.d.ts +43 -0
- package/dist/tenant/tool-search.js +100 -0
- package/dist/tenant/transcript.d.ts +108 -0
- package/dist/tenant/transcript.js +13 -0
- package/dist/tenant/types.d.ts +0 -14
- package/dist/vault/crypto.d.ts +0 -7
- package/dist/vault/crypto.js +0 -16
- package/dist/vault/headers.d.ts +17 -0
- package/dist/vault/headers.js +61 -0
- package/dist/vault/service.d.ts +11 -68
- package/dist/vault/service.js +83 -438
- package/dist/vault/sources.d.ts +20 -48
- package/dist/vault/sources.js +25 -189
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/streams/control.d.ts +0 -13
- package/dist/streams/control.js +0 -25
- package/dist/vault/oauth.d.ts +0 -55
- package/dist/vault/oauth.js +0 -186
package/dist/store/types.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ import type { RecordReader } from "../streams/relay/types.js";
|
|
|
59
59
|
import type { EventPayload, EventType, LiveEvent, SandboxEvent, SandboxEventPayload, SandboxEventType, SandboxKind, SandboxPodDesired, SandboxPodObserved, SessionEventOf } from "@nylorun/core/contracts";
|
|
60
60
|
import type { SandboxManifest } from "@nylorun/core/define";
|
|
61
61
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
62
|
-
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow
|
|
62
|
+
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
|
|
63
63
|
/** Tables holding JSON documents keyed by `id`. */
|
|
64
64
|
export type DocTable = "definitions" | "sessions" | "commands" | "effects" | "sandboxes" | "links";
|
|
65
65
|
export declare const DOC_TABLES: readonly DocTable[];
|
|
@@ -195,7 +195,7 @@ export type CommitListener = (commit: Commit) => void;
|
|
|
195
195
|
* `bytea` columns, never inside a JSON body), the vault's audit and idempotency records, the
|
|
196
196
|
* Tenant's signing keys, a model call in the usage ledger and a model budget.
|
|
197
197
|
*/
|
|
198
|
-
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow,
|
|
198
|
+
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
|
|
199
199
|
/** Which rows of the usage ledger a total covers. */
|
|
200
200
|
export interface ModelUsageQuery {
|
|
201
201
|
scope: "tenant" | "agent" | "turn";
|
|
@@ -261,6 +261,39 @@ export interface SessionStore {
|
|
|
261
261
|
record(): RecordReader;
|
|
262
262
|
/** Reachability and schema check for Tenant status and `/ready`. Never throws. */
|
|
263
263
|
health(): Promise<StoreHealth>;
|
|
264
|
+
/**
|
|
265
|
+
* Follows the control bus (D21): calls `onSignal` once for each signal committed after the
|
|
266
|
+
* returned promise resolves, on every process that follows. Delivery is by notification,
|
|
267
|
+
* backed by a poll of recent signals (every `pollMs`, and each time the listener connects
|
|
268
|
+
* again) that catches the notifications a dropped connection missed. `onSignal` must not
|
|
269
|
+
* throw; what it throws goes to `onError`, as do listener failures.
|
|
270
|
+
*/
|
|
271
|
+
followSignals(onSignal: (signal: ControlSignal) => void, options?: FollowSignalsOptions): Promise<SignalFollower>;
|
|
272
|
+
close(): Promise<void>;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A signal on the control bus (D21, D48): one process tells the others with the Tenant open.
|
|
276
|
+
* Signals are not events: they are never recorded in a session's log or streamed, and a
|
|
277
|
+
* lost one costs latency, never correctness. They never go through S2.
|
|
278
|
+
*/
|
|
279
|
+
export type ControlSignal = {
|
|
280
|
+
/** The process running the session's advance aborts it. */
|
|
281
|
+
type: "session.cancel";
|
|
282
|
+
sessionId: string;
|
|
283
|
+
/** The cancelled turn: an advance of another turn keeps running. Absent: any advance. */
|
|
284
|
+
turnId?: string;
|
|
285
|
+
} | {
|
|
286
|
+
/** The Tenant's sessions were reset and it moved to basin generation `generation`. */
|
|
287
|
+
type: "sessions.reset";
|
|
288
|
+
generation: number;
|
|
289
|
+
};
|
|
290
|
+
export interface FollowSignalsOptions {
|
|
291
|
+
/** How often recent signals are read back. Default 5 s. */
|
|
292
|
+
pollMs?: number;
|
|
293
|
+
onError?: (error: unknown) => void;
|
|
294
|
+
}
|
|
295
|
+
export interface SignalFollower {
|
|
296
|
+
/** Stops following and ends the listener's connection. */
|
|
264
297
|
close(): Promise<void>;
|
|
265
298
|
}
|
|
266
299
|
export interface StoreHealth {
|
|
@@ -393,6 +426,13 @@ export interface Tx {
|
|
|
393
426
|
/** Links of one workflow session joined to their existing agent sessions, ordered by agent session id. */
|
|
394
427
|
linkedSessions<S extends SessionDoc = SessionDoc>(workflowSessionId: string): Promise<LinkedSession<S>[]>;
|
|
395
428
|
counts(): Promise<StoreCounts>;
|
|
429
|
+
/**
|
|
430
|
+
* Puts `signal` on the control bus: a row, and a notification Postgres sends every
|
|
431
|
+
* following process when this transaction commits (and never on rollback).
|
|
432
|
+
*/
|
|
433
|
+
signal(signal: ControlSignal): Promise<void>;
|
|
434
|
+
/** Deletes signals written before `before`. Returns how many. */
|
|
435
|
+
pruneSignals(before: Date): Promise<number>;
|
|
396
436
|
basinGenerations(): Promise<BasinGenerations>;
|
|
397
437
|
/** Forgets a retired generation once its basin is deleted. */
|
|
398
438
|
forgetRetiredGeneration(generation: number): Promise<void>;
|
|
@@ -443,12 +483,6 @@ export interface Tx {
|
|
|
443
483
|
getVaultIdempotency(id: string): Promise<VaultIdempotencyRow | undefined>;
|
|
444
484
|
/** Rejects when the id exists. */
|
|
445
485
|
insertVaultIdempotency(row: VaultIdempotencyRow): Promise<void>;
|
|
446
|
-
/** An MCP OAuth connect's start (F9 C2). Rejects on a duplicate state or an unknown vault. */
|
|
447
|
-
insertOAuthPending(row: OAuthPendingRow): Promise<void>;
|
|
448
|
-
/** Deletes and returns the pending connect of `stateHash`, expired or not: each is used once. */
|
|
449
|
-
takeOAuthPending(stateHash: string): Promise<OAuthPendingRow | undefined>;
|
|
450
|
-
/** Deletes the pending connects that expired before `now` (ISO); returns how many. */
|
|
451
|
-
deleteExpiredOAuthPending(now: string): Promise<number>;
|
|
452
486
|
/**
|
|
453
487
|
* Serializes signing key changes: held until the transaction ends. Take it before reading
|
|
454
488
|
* the keys a change depends on, so concurrent first uses or rotations do not collide.
|
package/dist/streams/types.d.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* | Stream | Contents | Written by | Read by |
|
|
8
8
|
* | --- | --- | --- | --- |
|
|
9
9
|
* | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* S2 serves API listeners only (history, SSE, AG-UI, A2A): nothing inside the Runtime signals
|
|
12
|
+
* through it or waits on it (D48). Signals between processes go over the control bus on
|
|
13
|
+
* Postgres (`store/postgres/control.ts`).
|
|
11
14
|
*
|
|
12
15
|
* **Basin generations.** A session id is unique within its Tenant's basin generation. A
|
|
13
16
|
* Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
|
|
@@ -104,38 +107,12 @@ export interface DurableStreams {
|
|
|
104
107
|
listStreams(tenantId: string, prefix: string): Promise<string[]>;
|
|
105
108
|
close(): Promise<void>;
|
|
106
109
|
/**
|
|
107
|
-
* Resolves when the backing service answers, rejects otherwise (
|
|
108
|
-
* `
|
|
110
|
+
* Resolves when the backing service answers, rejects otherwise (the Tenant's status,
|
|
111
|
+
* `streamsStatus` in `tenant/streams.ts`; never readiness, D48). Absent for in-process implementations, which are
|
|
109
112
|
* always reachable.
|
|
110
113
|
*/
|
|
111
114
|
probe?(signal: AbortSignal): Promise<void>;
|
|
112
115
|
}
|
|
113
116
|
export declare const SESSION_STREAM_PREFIX = "sessions/";
|
|
114
|
-
/** `session.cancel` and `sessions.reset` signals for every process with the Tenant open. */
|
|
115
|
-
export declare const CONTROL_STREAM = "tenant/control";
|
|
116
117
|
/** The stream of a session: `sessions/<sessionId>`. */
|
|
117
118
|
export declare function sessionStream(sessionId: string): string;
|
|
118
|
-
/**
|
|
119
|
-
* Signals are not events: they are appended directly, never recorded, and a
|
|
120
|
-
* lost signal costs latency, not correctness.
|
|
121
|
-
*/
|
|
122
|
-
/** Ends the advance of `sessionId` on the process running it. */
|
|
123
|
-
export interface SessionCancelSignal {
|
|
124
|
-
type: "session.cancel";
|
|
125
|
-
sessionId: string;
|
|
126
|
-
/**
|
|
127
|
-
* The cancelled turn. Only an advance of this turn stops, so a signal delivered late (an
|
|
128
|
-
* append retried after S2 returns) never stops a later turn. Absent: any advance stops.
|
|
129
|
-
*/
|
|
130
|
-
turnId?: string;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* The Tenant was reset and moved to basin generation `generation`. Appended to the old
|
|
134
|
-
* generation's `tenant/control`: each process switches its readers to the new basin and
|
|
135
|
-
* ends the streams of sessions the reset deleted.
|
|
136
|
-
*/
|
|
137
|
-
export interface SessionsResetSignal {
|
|
138
|
-
type: "sessions.reset";
|
|
139
|
-
generation?: number;
|
|
140
|
-
}
|
|
141
|
-
export type ControlSignal = SessionCancelSignal | SessionsResetSignal;
|
package/dist/streams/types.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* | Stream | Contents | Written by | Read by |
|
|
8
8
|
* | --- | --- | --- | --- |
|
|
9
9
|
* | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* S2 serves API listeners only (history, SSE, AG-UI, A2A): nothing inside the Runtime signals
|
|
12
|
+
* through it or waits on it (D48). Signals between processes go over the control bus on
|
|
13
|
+
* Postgres (`store/postgres/control.ts`).
|
|
11
14
|
*
|
|
12
15
|
* **Basin generations.** A session id is unique within its Tenant's basin generation. A
|
|
13
16
|
* Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
|
|
@@ -39,10 +42,8 @@
|
|
|
39
42
|
* encode them as they need.
|
|
40
43
|
*/
|
|
41
44
|
// ---------------------------------------------------------------------------
|
|
42
|
-
// Stream names
|
|
45
|
+
// Stream names
|
|
43
46
|
export const SESSION_STREAM_PREFIX = "sessions/";
|
|
44
|
-
/** `session.cancel` and `sessions.reset` signals for every process with the Tenant open. */
|
|
45
|
-
export const CONTROL_STREAM = "tenant/control";
|
|
46
47
|
/** The stream of a session: `sessions/<sessionId>`. */
|
|
47
48
|
export function sessionStream(sessionId) {
|
|
48
49
|
if (!sessionId)
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { SandboxToolOutcome } from "@nylorun/core/contracts";
|
|
2
|
+
import { type ToolResultFile } from "@nylorun/core/define";
|
|
2
3
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
3
4
|
import { type TenantContext } from "./context.js";
|
|
4
|
-
export { isSaveArtifactCall } from "../harness/calls.js";
|
|
5
|
+
export { isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
|
|
5
6
|
export declare function callSaveArtifact(ctx: TenantContext, request: HostEffect, signal: AbortSignal): Promise<SandboxToolOutcome>;
|
|
7
|
+
/** What `read_artifact` returns: a page of a text artifact, or an image as a file. */
|
|
8
|
+
interface ReadOutcome {
|
|
9
|
+
readonly kind: "completed";
|
|
10
|
+
readonly output: Record<string, unknown>;
|
|
11
|
+
readonly files?: readonly ToolResultFile[];
|
|
12
|
+
}
|
|
13
|
+
export declare function callReadArtifact(ctx: TenantContext, request: HostEffect, signal: AbortSignal): Promise<ReadOutcome | SandboxToolOutcome>;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* (`sandbox/session-sandbox.ts`); the Runtime runs the tool itself, like the sandbox tools:
|
|
2
|
+
* Our engine's built-in artifact tools, of the `nylorun.artifacts` capability
|
|
3
|
+
* (`sandbox/session-sandbox.ts`). The Runtime runs them itself, like the sandbox tools.
|
|
5
4
|
*
|
|
5
|
+
* `save_artifact` (F8.1), which comes with a session's sandbox, saves a file as an artifact of
|
|
6
|
+
* its session:
|
|
6
7
|
* - `path`: a file in the session's sandbox, read as bytes in the sandbox's queue;
|
|
7
8
|
* - `content`: text the model passes.
|
|
8
|
-
*
|
|
9
9
|
* The version commits with `artifact.created` (or `artifact.version.created`) on the session's
|
|
10
|
-
* stream, carrying the turn and the tool call.
|
|
11
|
-
*
|
|
10
|
+
* stream, carrying the turn and the tool call.
|
|
11
|
+
*
|
|
12
|
+
* `read_artifact` (R2b C11, Q24), which an agent with a remote MCP server or an HTTP tool has,
|
|
13
|
+
* reads a text artifact of its own session in pages of at most 32 KiB, from a byte `offset`:
|
|
14
|
+
* how the model reads a tool result the Runtime stored because it was too large to show
|
|
15
|
+
* (`tool-results.ts`). An image is shown to a model that reads images, as a file of the result.
|
|
16
|
+
* It reaches only the session's own artifacts, never a Tenant-wide one or another session's.
|
|
17
|
+
*
|
|
18
|
+
* Expected problems (no such file, too large, the Tenant's total reached, not text) are failed
|
|
19
|
+
* outcomes the model sees; only infrastructure errors throw.
|
|
12
20
|
*/
|
|
13
21
|
import { basename } from "node:path";
|
|
14
|
-
import { SANDBOX_CAPABILITY_ID } from "@nylorun/core/define";
|
|
22
|
+
import { READ_ARTIFACT_MAX_BYTES, SANDBOX_CAPABILITY_ID, } from "@nylorun/core/define";
|
|
15
23
|
import { isArtifactId } from "@nylorun/core/compatibility";
|
|
16
24
|
import { readArtifactLimits } from "../artifacts/config.js";
|
|
17
|
-
import { contentTypeFor, normalizeContentType } from "../artifacts/media-types.js";
|
|
25
|
+
import { MODEL_FILE_MAX_BYTES, contentTypeFor, essence, isModelImage, isText, normalizeContentType, } from "../artifacts/media-types.js";
|
|
18
26
|
import { uploadArtifact } from "../artifacts/service.js";
|
|
19
27
|
import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
|
|
20
28
|
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
@@ -22,8 +30,9 @@ import { sandboxWorkspaceOf } from "../sandbox/share.js";
|
|
|
22
30
|
import { sandboxLookup, sessionOf } from "./context.js";
|
|
23
31
|
import { HttpError } from "./http.js";
|
|
24
32
|
import { manifestFor } from "./session.js";
|
|
33
|
+
import { jsonBytes, utf8Boundary } from "./tool-results.js";
|
|
25
34
|
import { toolIds } from "./transcript.js";
|
|
26
|
-
export { isSaveArtifactCall } from "../harness/calls.js";
|
|
35
|
+
export { isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
|
|
27
36
|
const failed = (code, message) => ({
|
|
28
37
|
kind: "failed",
|
|
29
38
|
code,
|
|
@@ -117,3 +126,98 @@ export async function callSaveArtifact(ctx, request, signal) {
|
|
|
117
126
|
throw error;
|
|
118
127
|
}
|
|
119
128
|
}
|
|
129
|
+
export async function callReadArtifact(ctx, request, signal) {
|
|
130
|
+
const input = (request.input ?? {});
|
|
131
|
+
const artifactId = typeof input.artifactId === "string" ? input.artifactId : "";
|
|
132
|
+
const offset = input.offset ?? 0;
|
|
133
|
+
const length = input.length ?? READ_ARTIFACT_MAX_BYTES;
|
|
134
|
+
if (artifactId === "" ||
|
|
135
|
+
typeof offset !== "number" ||
|
|
136
|
+
!Number.isSafeInteger(offset) ||
|
|
137
|
+
offset < 0 ||
|
|
138
|
+
typeof length !== "number" ||
|
|
139
|
+
!Number.isSafeInteger(length) ||
|
|
140
|
+
length < 1 ||
|
|
141
|
+
length > READ_ARTIFACT_MAX_BYTES)
|
|
142
|
+
return failed("artifact.invalid_input", `Give artifactId, an offset of 0 or more, and a length of 1 to ${READ_ARTIFACT_MAX_BYTES}.`);
|
|
143
|
+
const found = await ctx.store.tx(async (t) => {
|
|
144
|
+
const s = await sessionOf(t, request.sessionId);
|
|
145
|
+
if (!isArtifactId(artifactId))
|
|
146
|
+
return undefined;
|
|
147
|
+
// Only an artifact of this session: never a Tenant-wide one, nor another session's.
|
|
148
|
+
const artifact = await t.artifact(artifactId);
|
|
149
|
+
if (!artifact || artifact.sessionId !== s.id)
|
|
150
|
+
return undefined;
|
|
151
|
+
const version = await t.artifactVersion(artifact.id, artifact.latestVersion);
|
|
152
|
+
return version ? { artifact, version } : undefined;
|
|
153
|
+
});
|
|
154
|
+
if (!found)
|
|
155
|
+
return failed("artifact.not_found", `Artifact ${artifactId} is not an artifact of this session.`);
|
|
156
|
+
const { artifact, version } = found;
|
|
157
|
+
const about = {
|
|
158
|
+
artifactId: artifact.id,
|
|
159
|
+
version: version.version,
|
|
160
|
+
name: artifact.name,
|
|
161
|
+
contentType: version.contentType,
|
|
162
|
+
size: version.size,
|
|
163
|
+
};
|
|
164
|
+
if (artifact.kind !== "file")
|
|
165
|
+
return failed("artifact.not_text", `${artifact.name} is a folder; read_artifact reads a file.`);
|
|
166
|
+
if (isModelImage(version.contentType) && version.size <= MODEL_FILE_MAX_BYTES)
|
|
167
|
+
return {
|
|
168
|
+
kind: "completed",
|
|
169
|
+
output: about,
|
|
170
|
+
files: [
|
|
171
|
+
{
|
|
172
|
+
mediaType: version.contentType,
|
|
173
|
+
reference: { artifactId: artifact.id, version: version.version, name: artifact.name },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
if (!isText(version.contentType))
|
|
178
|
+
return failed("artifact.not_text", `${artifact.name} is ${essence(version.contentType)}, not text; read_artifact reads text and shows images.`);
|
|
179
|
+
if (offset > version.size || (offset === version.size && version.size > 0))
|
|
180
|
+
return failed("artifact.invalid_input", `Offset ${offset} is past the end of ${artifact.name}, which is ${version.size} bytes.`);
|
|
181
|
+
const end = Math.min(offset + length, version.size);
|
|
182
|
+
const bytes = end === offset ? new Uint8Array() : await readRange(ctx, version.blobKey, offset, end - 1, signal);
|
|
183
|
+
const page = textPage(bytes, end === version.size);
|
|
184
|
+
const next = offset + page.bytes;
|
|
185
|
+
return {
|
|
186
|
+
kind: "completed",
|
|
187
|
+
output: {
|
|
188
|
+
...about,
|
|
189
|
+
offset,
|
|
190
|
+
content: page.text,
|
|
191
|
+
...(next < version.size ? { nextOffset: next } : {}),
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Bytes `start` to `end` (both inclusive) of a version's blob. */
|
|
196
|
+
async function readRange(ctx, blobKey, start, end, signal) {
|
|
197
|
+
const got = await ctx.blobs.get(blobKey, { range: { start, end }, signal });
|
|
198
|
+
if (!got)
|
|
199
|
+
throw new Error("The artifact's bytes are missing from the Object store");
|
|
200
|
+
return new Uint8Array(await new Response(got.body).arrayBuffer());
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The text of a page of UTF-8 `bytes` in whole characters, at most `READ_ARTIFACT_MAX_BYTES`
|
|
204
|
+
* once escaped as JSON, and how many bytes it used: the next page starts there. A page that
|
|
205
|
+
* starts inside a character skips its rest, and one that ends inside one (not `last`) stops
|
|
206
|
+
* before it.
|
|
207
|
+
*/
|
|
208
|
+
function textPage(bytes, last) {
|
|
209
|
+
const from = utf8Boundary(bytes, 0, "up");
|
|
210
|
+
let to = last ? bytes.byteLength : utf8Boundary(bytes, bytes.byteLength, "down");
|
|
211
|
+
// A page shorter than one character: take what there is.
|
|
212
|
+
if (to <= from)
|
|
213
|
+
to = bytes.byteLength;
|
|
214
|
+
const decode = () => Buffer.from(bytes.subarray(from, to)).toString("utf8");
|
|
215
|
+
let text = decode();
|
|
216
|
+
// Escaped (quotes, newlines, control characters), text takes more room than its bytes: each
|
|
217
|
+
// byte left out frees at least one.
|
|
218
|
+
for (let size = jsonBytes(text); to > from && size > READ_ARTIFACT_MAX_BYTES; size = jsonBytes(text)) {
|
|
219
|
+
to = utf8Boundary(bytes, Math.max(from, to - (size - READ_ARTIFACT_MAX_BYTES)), "down");
|
|
220
|
+
text = decode();
|
|
221
|
+
}
|
|
222
|
+
return { text, bytes: to };
|
|
223
|
+
}
|
package/dist/tenant/auth.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tenant request authentication and authorization: a bearer token resolves to an application
|
|
3
|
-
* principal or a trusted issuer's token
|
|
4
|
-
* (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* principal or a trusted issuer's token. Anything else is `401` with a `Bearer` challenge
|
|
4
|
+
* (protocol 9, `resource-server.ts`): `credential_required` without one, `credential_invalid`
|
|
5
|
+
* for one the Tenant refuses, its reason only logged. An application principal may act for a
|
|
6
|
+
* subject (`Nylorun-Subject`, `Nylorun-Scopes`); an issuer token names its subject itself.
|
|
7
|
+
* `requireScopes` limits them to the routes their scopes allow, decided from the route alone.
|
|
7
8
|
*
|
|
8
9
|
* Browsers: a request with `Origin` may carry an issuer token (CORS is the operator's proxy's),
|
|
9
10
|
* never an application key, which is a server secret.
|
|
@@ -21,6 +22,11 @@ export interface KeyAccess {
|
|
|
21
22
|
}
|
|
22
23
|
/** Every route that does not say otherwise is a Runtime API route. */
|
|
23
24
|
export declare const RUNTIME_KEYS: KeyAccess;
|
|
25
|
+
/**
|
|
26
|
+
* The `resource_metadata` a route's challenges point at: a Runtime API route's, when the Runtime
|
|
27
|
+
* trusts issuers. The Management API is no OAuth resource.
|
|
28
|
+
*/
|
|
29
|
+
export declare function challengeMetadata(ctx: TenantContext, request: IncomingMessage, keys: KeyAccess): string | undefined;
|
|
24
30
|
export declare function authenticate(ctx: TenantContext, request: IncomingMessage, keys?: KeyAccess): Promise<AuthScope>;
|
|
25
31
|
/**
|
|
26
32
|
* What a subject needs for a route: any one of the scopes, nothing grants it (`never`), or
|
|
@@ -30,9 +36,13 @@ export declare function authenticate(ctx: TenantContext, request: IncomingMessag
|
|
|
30
36
|
export type SubjectAccess = readonly SubjectScope[] | "never" | "any";
|
|
31
37
|
/**
|
|
32
38
|
* A subject or a token caller reaches a route only with one of the scopes it declares:
|
|
33
|
-
* `403 scope_required`, decided from the route alone before anything is read.
|
|
39
|
+
* `403 scope_required`, decided from the route alone before anything is read. A token caller
|
|
40
|
+
* also gets the OAuth challenge (`error="insufficient_scope"` and the scopes that would do), so
|
|
41
|
+
* its client can ask its identity provider for them; a route no scope reaches has none.
|
|
34
42
|
*/
|
|
35
|
-
export declare function requireScopes(scope: AuthScope, access: SubjectAccess
|
|
43
|
+
export declare function requireScopes(scope: AuthScope, access: SubjectAccess,
|
|
44
|
+
/** The route's `resource_metadata` (`challengeMetadata`), for a token caller's challenge. */
|
|
45
|
+
resourceMetadata?: string): void;
|
|
36
46
|
export declare function accessOf(scope: AuthScope): SessionAccess | undefined;
|
|
37
47
|
/** The subject whose sessions the request is limited to, if it acts for one. */
|
|
38
48
|
export declare function ownerOf(scope: AuthScope): string | undefined;
|
package/dist/tenant/auth.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
|
|
2
|
-
import { parseSubjectHeaders } from "@nylorun/core/contracts";
|
|
2
|
+
import { parseSubjectHeaders, TOKEN_SCOPES } from "@nylorun/core/contracts";
|
|
3
3
|
import { hashToken } from "../core/bearer.js";
|
|
4
|
-
import { fail
|
|
4
|
+
import { fail } from "./http.js";
|
|
5
5
|
import { verifyIssuerToken } from "./issuers.js";
|
|
6
6
|
import { looksLikeToken } from "./jwt.js";
|
|
7
|
+
import { bearerChallenge, failCredential, protectedResourceMetadataUrl } from "./resource-server.js";
|
|
7
8
|
const SUBJECT_INVALID = { code: "subject_invalid" };
|
|
8
9
|
/** One header's value, or `undefined`; a header sent twice is refused. */
|
|
9
10
|
function singleHeader(request, name) {
|
|
@@ -18,20 +19,30 @@ function singleHeader(request, name) {
|
|
|
18
19
|
}
|
|
19
20
|
/** Every route that does not say otherwise is a Runtime API route. */
|
|
20
21
|
export const RUNTIME_KEYS = { application: true, management: false };
|
|
22
|
+
/** The bearer of an `Authorization` header: the scheme is case-insensitive (RFC 9110 §11.1). */
|
|
23
|
+
const BEARER = /^bearer +(\S+) *$/i;
|
|
24
|
+
/**
|
|
25
|
+
* The `resource_metadata` a route's challenges point at: a Runtime API route's, when the Runtime
|
|
26
|
+
* trusts issuers. The Management API is no OAuth resource.
|
|
27
|
+
*/
|
|
28
|
+
export function challengeMetadata(ctx, request, keys) {
|
|
29
|
+
return keys.application ? protectedResourceMetadataUrl(ctx.config, request) : undefined;
|
|
30
|
+
}
|
|
21
31
|
export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
|
|
22
|
-
const
|
|
23
|
-
const token =
|
|
24
|
-
|
|
32
|
+
const metadata = challengeMetadata(ctx, request, keys);
|
|
33
|
+
const token = BEARER.exec(request.headers.authorization ?? "")?.[1];
|
|
34
|
+
// No credential, or another scheme: the client may not know it needs one (no error code).
|
|
35
|
+
if (!token) {
|
|
25
36
|
ctx.config.logger.warn("credential rejected", {
|
|
26
37
|
reason: "missing_bearer",
|
|
27
38
|
});
|
|
28
|
-
return
|
|
39
|
+
return failCredential("required", metadata);
|
|
29
40
|
}
|
|
30
41
|
// A trusted issuer's token (Host feature `trusted-issuers`), from a server or a browser: its
|
|
31
42
|
// unverified `iss` names an issuer of the identity file, and only that issuer verifies it.
|
|
32
43
|
const issuer = ctx.config.issuers?.claimed(token);
|
|
33
44
|
if (issuer) {
|
|
34
|
-
const scope = await verifyIssuerToken(ctx, issuer, token);
|
|
45
|
+
const scope = await verifyIssuerToken(ctx, issuer, token, metadata);
|
|
35
46
|
if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
|
|
36
47
|
singleHeader(request, SCOPES_HEADER) !== undefined)
|
|
37
48
|
fail(403, "An issuer token cannot act for another subject");
|
|
@@ -40,7 +51,7 @@ export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
|
|
|
40
51
|
// Any other JWT: no issuer of this Host signed it (subject tokens are gone, protocol 7).
|
|
41
52
|
if (looksLikeToken(token)) {
|
|
42
53
|
ctx.config.logger.warn("credential rejected", { reason: "token_unknown_issuer" });
|
|
43
|
-
return
|
|
54
|
+
return failCredential("invalid", metadata);
|
|
44
55
|
}
|
|
45
56
|
// Application keys never come from a browser or a shipped app: refused before they are even
|
|
46
57
|
// looked up.
|
|
@@ -54,9 +65,9 @@ export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
|
|
|
54
65
|
ctx.config.logger.warn("credential rejected", {
|
|
55
66
|
reason: "unknown_token",
|
|
56
67
|
});
|
|
57
|
-
return
|
|
68
|
+
return failCredential("invalid", metadata);
|
|
58
69
|
}
|
|
59
|
-
// Read only after authentication, so an unknown caller
|
|
70
|
+
// Read only after authentication, so an unknown caller learns nothing of them.
|
|
60
71
|
const subject = singleHeader(request, SUBJECT_HEADER);
|
|
61
72
|
const scopes = singleHeader(request, SCOPES_HEADER);
|
|
62
73
|
// A management key acts as itself on the Management API; Studio's key does too on a route
|
|
@@ -92,9 +103,13 @@ export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
|
|
|
92
103
|
}
|
|
93
104
|
/**
|
|
94
105
|
* A subject or a token caller reaches a route only with one of the scopes it declares:
|
|
95
|
-
* `403 scope_required`, decided from the route alone before anything is read.
|
|
106
|
+
* `403 scope_required`, decided from the route alone before anything is read. A token caller
|
|
107
|
+
* also gets the OAuth challenge (`error="insufficient_scope"` and the scopes that would do), so
|
|
108
|
+
* its client can ask its identity provider for them; a route no scope reaches has none.
|
|
96
109
|
*/
|
|
97
|
-
export function requireScopes(scope, access
|
|
110
|
+
export function requireScopes(scope, access,
|
|
111
|
+
/** The route's `resource_metadata` (`challengeMetadata`), for a token caller's challenge. */
|
|
112
|
+
resourceMetadata) {
|
|
98
113
|
if (scope.kind !== "subject" && scope.kind !== "token")
|
|
99
114
|
return;
|
|
100
115
|
if (access === "any")
|
|
@@ -105,11 +120,20 @@ export function requireScopes(scope, access) {
|
|
|
105
120
|
details: { scopes: [] },
|
|
106
121
|
});
|
|
107
122
|
const held = scope.scopes;
|
|
108
|
-
if (!access.some((name) => held.has(name)))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
123
|
+
if (!access.some((name) => held.has(name))) {
|
|
124
|
+
// Any one of the route's scopes reaches it, but OAuth's list is of scopes held together:
|
|
125
|
+
// the challenge names the first a token can carry, or none when no token reaches the route.
|
|
126
|
+
const askable = access.find((name) => TOKEN_SCOPES.includes(name));
|
|
127
|
+
fail(403, `Scope ${access.join(" or ")} required`, { code: "scope_required", details: { scopes: access } }, scope.kind === "token" && askable
|
|
128
|
+
? {
|
|
129
|
+
"www-authenticate": bearerChallenge({
|
|
130
|
+
error: "insufficient_scope",
|
|
131
|
+
scope: askable,
|
|
132
|
+
resourceMetadata,
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
: {});
|
|
136
|
+
}
|
|
113
137
|
}
|
|
114
138
|
export function accessOf(scope) {
|
|
115
139
|
switch (scope.kind) {
|
package/dist/tenant/commands.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* (every accepted one writes a new checkpoint segment), except that a workflow's `approve` and
|
|
11
11
|
* `respond` name `<type>:<turnId>:<interactionId>`: a flow resumes in the same segment.
|
|
12
12
|
*
|
|
13
|
-
* Cancel commits `cancelled` first
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Cancel commits `cancelled` first, with a `session.cancel` signal on the control bus in the
|
|
14
|
+
* same transaction; the engine host sees it before its next effect and before settlement on
|
|
15
|
+
* any Worker. It then aborts an advance running on this process (`ctx.abortLocal`); every
|
|
16
|
+
* other process aborts its own on the signal (`control.ts`).
|
|
16
17
|
*/
|
|
17
18
|
import { randomUUID } from "node:crypto";
|
|
18
19
|
import { createDurableCheckpoint, createFlowCheckpoint, resumeFlowCheckpoint, } from "@nylorun/harness/run";
|
|
@@ -24,7 +25,6 @@ import { fail } from "./http.js";
|
|
|
24
25
|
import { accessOf } from "./auth.js";
|
|
25
26
|
import { checkSandboxTurn } from "./sandboxes.js";
|
|
26
27
|
import { rebaseSessionState, turnManifestOf, variantStore } from "./session.js";
|
|
27
|
-
import { signalSessionCancel } from "./streams.js";
|
|
28
28
|
import { slimModelEffects } from "./slim.js";
|
|
29
29
|
import { resolveMessageParts } from "../artifacts/parts.js";
|
|
30
30
|
/** A command's identity for idempotency: everything but the request id. */
|
|
@@ -83,8 +83,12 @@ export async function command(ctx, id, input, scope) {
|
|
|
83
83
|
reason: command.reason,
|
|
84
84
|
});
|
|
85
85
|
await slimModelEffects(t, id, cancelledTurnId);
|
|
86
|
-
// The process running the advance aborts it on
|
|
87
|
-
t.
|
|
86
|
+
// The process running the advance aborts it on this signal (control.ts), sent at commit.
|
|
87
|
+
await t.signal({
|
|
88
|
+
type: "session.cancel",
|
|
89
|
+
sessionId: id,
|
|
90
|
+
...(cancelledTurnId !== null ? { turnId: cancelledTurnId } : {}),
|
|
91
|
+
});
|
|
88
92
|
s.activeTurnId = null;
|
|
89
93
|
if (cancelledTurnId !== null)
|
|
90
94
|
s.lastTurnId = cancelledTurnId;
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -24,7 +24,6 @@ import type { ModelGate } from "../gates/model-gate.js";
|
|
|
24
24
|
import type { ToolGate } from "../gates/tool-gate.js";
|
|
25
25
|
import type { Keys } from "../keys/keys.js";
|
|
26
26
|
import type { VaultService } from "../vault/service.js";
|
|
27
|
-
import type { CredentialSources } from "../vault/sources.js";
|
|
28
27
|
import type { McpPool } from "../mcp/pool.js";
|
|
29
28
|
import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
|
|
30
29
|
import type { HarnessApiServer } from "../harness-api/server.js";
|
|
@@ -46,6 +45,11 @@ export interface Session {
|
|
|
46
45
|
/** Session pin — the manifest the session was created with. */
|
|
47
46
|
manifest: any;
|
|
48
47
|
manifestHash: string;
|
|
48
|
+
/**
|
|
49
|
+
* The hash of the definition the session was opened from; `manifestHash` differs when the
|
|
50
|
+
* Runtime pinned its own capabilities beside it (R2b C11). Absent on older sessions.
|
|
51
|
+
*/
|
|
52
|
+
definitionHash?: string;
|
|
49
53
|
/** Validated turn-manifest variants, keyed by hash (loops.md §4.2). */
|
|
50
54
|
variants?: Record<string, AgentManifest>;
|
|
51
55
|
implementationVersion: string;
|
|
@@ -139,11 +143,6 @@ export interface TenantContext {
|
|
|
139
143
|
readonly envelope: TenantEnvelope;
|
|
140
144
|
readonly store: SessionStore;
|
|
141
145
|
readonly vault: VaultService;
|
|
142
|
-
/**
|
|
143
|
-
* A session's MCP credentials, for the in-process MCP pool: its attached vaults, then the
|
|
144
|
-
* operator's credential resolver (`TenantConfig.resolver`, F9 C1).
|
|
145
|
-
*/
|
|
146
|
-
readonly credentials: CredentialSources;
|
|
147
146
|
/**
|
|
148
147
|
* The MCP pool of the Tenant's in-process harness. Absent when harnesses run elsewhere
|
|
149
148
|
* (`NYLORUN_HARNESS=remote`): each keeps its own (F6.2).
|
|
@@ -201,6 +200,10 @@ export interface TenantContext {
|
|
|
201
200
|
readonly work: WorkState;
|
|
202
201
|
/** Live delivery over Durable Streams: one `SessionStream` per observed session, and the streams wiring. */
|
|
203
202
|
readonly sessionStreams: SessionStreams;
|
|
203
|
+
/** This process's follower of the control bus (`control.ts`), once wired. */
|
|
204
|
+
control?: {
|
|
205
|
+
close(): Promise<void>;
|
|
206
|
+
};
|
|
204
207
|
/** The Tenant's signing keys: capability links, run and host tokens. */
|
|
205
208
|
readonly signingKeys: SigningKeys;
|
|
206
209
|
/** The Worker id this process writes as session `owner` (§10.6). */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control bus of an open Tenant (blueprint D21, D48): the signals one process sends the
|
|
3
|
+
* others with the Tenant open, on Postgres (`Tx.signal`, `SessionStore.followSignals`). They
|
|
4
|
+
* never go through S2, which only serves API listeners.
|
|
5
|
+
*
|
|
6
|
+
* - `session.cancel` is written by the cancel's own transaction (`commands.ts`): every process
|
|
7
|
+
* aborts the cancelled turn's advance if it runs there (`ctx.abortLocal`).
|
|
8
|
+
* - `sessions.reset` is written by the reset's transaction (`reset.ts`): every process moves
|
|
9
|
+
* its session streams to the new basin generation and ends those of deleted sessions
|
|
10
|
+
* (`checkSessionStreams`).
|
|
11
|
+
*
|
|
12
|
+
* A lost signal costs latency, never correctness: the advance checks the Session Store before
|
|
13
|
+
* every effect, and the session streams are checked periodically (`streams.ts`).
|
|
14
|
+
*/
|
|
15
|
+
import type { TenantContext } from "./context.js";
|
|
16
|
+
export interface WireControlOptions {
|
|
17
|
+
/** How often the follower reads recent signals back (`FollowSignalsOptions.pollMs`). */
|
|
18
|
+
pollMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Follows the control bus for this process and keeps the follower on `ctx.control`. Call it
|
|
22
|
+
* once, after `ctx` is built; signals committed from its return on reach this process.
|
|
23
|
+
*/
|
|
24
|
+
export declare function wireControl(ctx: TenantContext, options?: WireControlOptions): Promise<void>;
|
|
25
|
+
/** Close: stops following the control bus. */
|
|
26
|
+
export declare function closeControl(ctx: TenantContext): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { checkSessionStreams } from "./session-streams.js";
|
|
2
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
3
|
+
/**
|
|
4
|
+
* Follows the control bus for this process and keeps the follower on `ctx.control`. Call it
|
|
5
|
+
* once, after `ctx` is built; signals committed from its return on reach this process.
|
|
6
|
+
*/
|
|
7
|
+
export async function wireControl(ctx, options = {}) {
|
|
8
|
+
if (ctx.control)
|
|
9
|
+
throw new Error("The control bus is already wired");
|
|
10
|
+
let closed = false;
|
|
11
|
+
// Nothing is reported once closed: the Tenant (and its log directory) may be gone.
|
|
12
|
+
const report = (what) => (error) => {
|
|
13
|
+
if (!closed)
|
|
14
|
+
ctx.config.logger.warn(what, { message: messageOf(error) });
|
|
15
|
+
};
|
|
16
|
+
const follower = await ctx.store.followSignals((signal) => {
|
|
17
|
+
if (signal.type === "session.cancel")
|
|
18
|
+
ctx.abortLocal(signal.sessionId, signal.turnId);
|
|
19
|
+
else
|
|
20
|
+
void checkSessionStreams(ctx).catch(report("session stream check failed"));
|
|
21
|
+
}, {
|
|
22
|
+
...(options.pollMs !== undefined ? { pollMs: options.pollMs } : {}),
|
|
23
|
+
onError: report("control bus unavailable; retrying"),
|
|
24
|
+
});
|
|
25
|
+
ctx.control = {
|
|
26
|
+
async close() {
|
|
27
|
+
closed = true;
|
|
28
|
+
await follower.close();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Close: stops following the control bus. */
|
|
33
|
+
export async function closeControl(ctx) {
|
|
34
|
+
await ctx.control?.close();
|
|
35
|
+
}
|
package/dist/tenant/effects.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { EffectOutcome } from "@nylorun/core/contracts";
|
|
|
14
14
|
import type { EffectResolution, HostEffect } from "@nylorun/harness/run";
|
|
15
15
|
import type { Tx } from "../store/types.js";
|
|
16
16
|
import type { AuthorizeResult } from "../vault/service.js";
|
|
17
|
-
import type
|
|
17
|
+
import { type McpCredentialRequest } from "../vault/sources.js";
|
|
18
18
|
import { type Lease, type Session, type TenantContext } from "./context.js";
|
|
19
19
|
/**
|
|
20
20
|
* The outcome of a flow `agent` effect once the linked turn it started ended, or undefined
|
|
@@ -42,6 +42,6 @@ export declare function recoversModelCalls(ctx: TenantContext): boolean;
|
|
|
42
42
|
export declare function resolveNewFlowEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease): Promise<EffectResolution>;
|
|
43
43
|
/**
|
|
44
44
|
* The credential of an MCP server or HTTP tool request made on behalf of a session: from its
|
|
45
|
-
* attached vaults
|
|
45
|
+
* attached vaults (`vault/sources.ts`).
|
|
46
46
|
*/
|
|
47
47
|
export declare function authorize(ctx: TenantContext, sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|