@nylorun/runtime 0.20.0-beta → 0.21.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 +86 -0
- package/README.md +19 -12
- package/dist/adapters/execution/restate.js +26 -1
- package/dist/api/components.d.ts +155 -36
- package/dist/api/components.js +3 -3
- package/dist/api/http/app.js +2 -0
- 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 +6 -3
- package/dist/artifacts/media-types.d.ts +2 -0
- package/dist/artifacts/media-types.js +5 -0
- package/dist/core/flow-host.d.ts +2 -3
- package/dist/core/flow-host.js +10 -7
- package/dist/execution/types.d.ts +13 -5
- package/dist/execution/types.js +13 -5
- 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/config-for.d.ts +0 -2
- package/dist/host/config-for.js +0 -1
- package/dist/host/create-host.d.ts +1 -1
- package/dist/host/create-host.js +2 -1
- package/dist/host/gates.js +0 -1
- package/dist/host/http.d.ts +0 -1
- package/dist/host/http.js +0 -6
- package/dist/host/main.js +13 -3
- package/dist/host/stack-config.d.ts +7 -8
- package/dist/host/stack-config.js +50 -92
- package/dist/index.d.ts +0 -1
- 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 +315 -345
- 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 +362 -123
- 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/drizzle/0016_mcp_oauth_removed.sql +5 -0
- package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +105 -261
- package/dist/store/postgres/schema.js +18 -36
- package/dist/store/postgres/store.js +54 -21
- package/dist/store/postgres/tenant.d.ts +14 -0
- package/dist/store/postgres/tenant.js +22 -2
- package/dist/store/types.d.ts +48 -13
- package/dist/tenant/advance.js +1 -4
- package/dist/tenant/artifact-tool.d.ts +9 -1
- package/dist/tenant/artifact-tool.js +113 -9
- package/dist/tenant/commands.js +5 -5
- package/dist/tenant/context.d.ts +15 -13
- 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 +0 -1
- package/dist/tenant/outbound.d.ts +14 -5
- package/dist/tenant/outbound.js +21 -9
- package/dist/tenant/runtime.js +11 -15
- package/dist/tenant/sessions.js +20 -3
- package/dist/tenant/store-pg.js +9 -0
- package/dist/tenant/sweep.d.ts +14 -0
- package/dist/tenant/sweep.js +69 -10
- 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/vault/oauth.d.ts +0 -55
- package/dist/vault/oauth.js +0 -186
package/dist/store/types.d.ts
CHANGED
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
* generation, `streams/basin.ts`), and the cursor is
|
|
26
26
|
* `base64url("<sessionId>:<seq>")` (see `record/cursor.ts`).
|
|
27
27
|
* 4. **No external I/O inside `fn`.** No model, tool, MCP, sandbox, Restate or
|
|
28
|
-
* S2 call, and no `fetch`, runs inside a transaction.
|
|
29
|
-
*
|
|
30
|
-
* commit (seam rule 1 and 2).
|
|
28
|
+
* S2 call, and no `fetch`, runs inside a transaction. A wake is written in the
|
|
29
|
+
* transaction (`Tx.wake`, the wake outbox) and delivered after commit, and events
|
|
30
|
+
* are delivered to commit listeners after commit (seam rule 1 and 2).
|
|
31
31
|
* 5. **No nested transactions.** Calling `store.tx` from inside `fn` rejects.
|
|
32
32
|
* A `Tx` must not be used after its `tx` call settles.
|
|
33
33
|
* 6. **Post-commit order.** After a commit, the store first calls every commit
|
|
34
34
|
* listener once with the transaction's events (in allocation order), then runs the `afterCommit` callbacks in registration
|
|
35
35
|
* order and awaits them. Listener and callback failures are reported to the
|
|
36
36
|
* store's error hook; they never reject `tx`, because the commit stands.
|
|
37
|
-
* Recovery from a lost post-commit step is the Tenant sweep's job
|
|
37
|
+
* Recovery from a lost post-commit step is the Tenant sweep's job: a wake's
|
|
38
|
+
* delivery is a callback too, and its outbox row is what the sweep delivers
|
|
39
|
+
* again (`Tx.wake`).
|
|
38
40
|
* 7. **Documents are values.** `get` and queries return fresh copies; mutating
|
|
39
41
|
* them changes nothing until `put`.
|
|
40
42
|
* 8. **Ownership columns are store-managed.** `owner`, `epoch` and
|
|
@@ -56,10 +58,11 @@
|
|
|
56
58
|
*/
|
|
57
59
|
import type { KeyRole } from "@nylorun/core/compatibility";
|
|
58
60
|
import type { RecordReader } from "../streams/relay/types.js";
|
|
61
|
+
import type { Wake } from "../execution/types.js";
|
|
59
62
|
import type { EventPayload, EventType, LiveEvent, SandboxEvent, SandboxEventPayload, SandboxEventType, SandboxKind, SandboxPodDesired, SandboxPodObserved, SessionEventOf } from "@nylorun/core/contracts";
|
|
60
63
|
import type { SandboxManifest } from "@nylorun/core/define";
|
|
61
64
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
62
|
-
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow
|
|
65
|
+
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
|
|
63
66
|
/** Tables holding JSON documents keyed by `id`. */
|
|
64
67
|
export type DocTable = "definitions" | "sessions" | "commands" | "effects" | "sandboxes" | "links";
|
|
65
68
|
export declare const DOC_TABLES: readonly DocTable[];
|
|
@@ -195,7 +198,7 @@ export type CommitListener = (commit: Commit) => void;
|
|
|
195
198
|
* `bytea` columns, never inside a JSON body), the vault's audit and idempotency records, the
|
|
196
199
|
* Tenant's signing keys, a model call in the usage ledger and a model budget.
|
|
197
200
|
*/
|
|
198
|
-
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow,
|
|
201
|
+
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
|
|
199
202
|
/** Which rows of the usage ledger a total covers. */
|
|
200
203
|
export interface ModelUsageQuery {
|
|
201
204
|
scope: "tenant" | "agent" | "turn";
|
|
@@ -271,6 +274,24 @@ export interface SessionStore {
|
|
|
271
274
|
followSignals(onSignal: (signal: ControlSignal) => void, options?: FollowSignalsOptions): Promise<SignalFollower>;
|
|
272
275
|
close(): Promise<void>;
|
|
273
276
|
}
|
|
277
|
+
/** A wake as the outbox delivers it: its `dedupeKey` is the request's idempotency key. */
|
|
278
|
+
export type OutboxWake = Wake & {
|
|
279
|
+
dedupeKey: string;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Hands an outbox request to Durable Session Execution (`Tx.wake`). Resolving means the
|
|
283
|
+
* execution accepted it; `false` means it was not delivered and stays in the outbox, as a
|
|
284
|
+
* rejection does, without an error.
|
|
285
|
+
*/
|
|
286
|
+
export type WakeDelivery = (sessionId: string, wake: OutboxWake) => boolean | void | Promise<boolean | void>;
|
|
287
|
+
/** A wake outbox request not yet delivered (`Tx.pendingWakes`). */
|
|
288
|
+
export interface PendingWake {
|
|
289
|
+
id: string;
|
|
290
|
+
sessionId: string;
|
|
291
|
+
wake: OutboxWake;
|
|
292
|
+
/** ISO time it was written. */
|
|
293
|
+
createdAt: string;
|
|
294
|
+
}
|
|
274
295
|
/**
|
|
275
296
|
* A signal on the control bus (D21, D48): one process tells the others with the Tenant open.
|
|
276
297
|
* Signals are not events: they are never recorded in a session's log or streamed, and a
|
|
@@ -345,8 +366,28 @@ export interface Tx {
|
|
|
345
366
|
* cursor is final.
|
|
346
367
|
*/
|
|
347
368
|
event<T extends EventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>): Promise<SessionEventOf<T>>;
|
|
348
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Runs `fn` after a successful commit (never on rollback): a sandbox signal or a host
|
|
371
|
+
* revocation. Nothing writes it down, so a crash after commit loses it. A wake goes
|
|
372
|
+
* through `wake`, which does.
|
|
373
|
+
*/
|
|
349
374
|
afterCommit(fn: () => void | Promise<void>): void;
|
|
375
|
+
/**
|
|
376
|
+
* Asks for an advance of `sessionId` once this transaction commits. The request is a row
|
|
377
|
+
* of the wake outbox written in this transaction, so it commits, or rolls back, with what
|
|
378
|
+
* caused it: a commit never loses its wake. After commit, in `afterCommit` order, the
|
|
379
|
+
* store calls `deliver` (`DurableExecution.wake`, through the Tenant) and deletes the row
|
|
380
|
+
* once it resolves to anything but `false`. A request `deliver` declines (`false`) or
|
|
381
|
+
* rejects, or one a crash cut off, stays in the outbox until the Tenant sweep delivers it
|
|
382
|
+
* (`pendingWakes`, `deleteWakes`). `deliver` gets the request's idempotency key as
|
|
383
|
+
* `dedupeKey`: the wake's own, or `wake:<row id>` without one, so a request delivered
|
|
384
|
+
* twice still causes one advance.
|
|
385
|
+
*/
|
|
386
|
+
wake(sessionId: string, wake: Wake, deliver: WakeDelivery): Promise<void>;
|
|
387
|
+
/** Outbox requests written before `before`, oldest first. */
|
|
388
|
+
pendingWakes(before: Date, limit: number): Promise<PendingWake[]>;
|
|
389
|
+
/** Deletes delivered outbox requests. */
|
|
390
|
+
deleteWakes(ids: readonly string[]): Promise<void>;
|
|
350
391
|
/**
|
|
351
392
|
* Locks the session and takes ownership when it has no owner or the owner's
|
|
352
393
|
* lease ended at or before `now`; then increments `epoch` and sets the lease
|
|
@@ -483,12 +524,6 @@ export interface Tx {
|
|
|
483
524
|
getVaultIdempotency(id: string): Promise<VaultIdempotencyRow | undefined>;
|
|
484
525
|
/** Rejects when the id exists. */
|
|
485
526
|
insertVaultIdempotency(row: VaultIdempotencyRow): Promise<void>;
|
|
486
|
-
/** An MCP OAuth connect's start (F9 C2). Rejects on a duplicate state or an unknown vault. */
|
|
487
|
-
insertOAuthPending(row: OAuthPendingRow): Promise<void>;
|
|
488
|
-
/** Deletes and returns the pending connect of `stateHash`, expired or not: each is used once. */
|
|
489
|
-
takeOAuthPending(stateHash: string): Promise<OAuthPendingRow | undefined>;
|
|
490
|
-
/** Deletes the pending connects that expired before `now` (ISO); returns how many. */
|
|
491
|
-
deleteExpiredOAuthPending(now: string): Promise<number>;
|
|
492
527
|
/**
|
|
493
528
|
* Serializes signing key changes: held until the transaction ends. Take it before reading
|
|
494
529
|
* the keys a change depends on, so concurrent first uses or rotations do not collide.
|
package/dist/tenant/advance.js
CHANGED
|
@@ -473,10 +473,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
473
473
|
await slimModelEffects(t, id, s.activeTurnId);
|
|
474
474
|
await t.put("sessions", id, current);
|
|
475
475
|
const segment = finished.segment + 1;
|
|
476
|
-
t.
|
|
477
|
-
reason: "rollover",
|
|
478
|
-
dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
|
|
479
|
-
}));
|
|
476
|
+
await t.wake(id, { reason: "rollover", dedupeKey: `rollover:${s.activeTurnId}:${segment}` }, ctx.wake);
|
|
480
477
|
return siblings;
|
|
481
478
|
}
|
|
482
479
|
const flow = isWorkflowManifest(current.manifest);
|
|
@@ -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/commands.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session command service: `message`, `approve`/`respond` and `cancel`,
|
|
3
3
|
* with per-session idempotency keys. A command locks its session and commits its state
|
|
4
|
-
* change, events and wakes in one transaction (the store publishes the events and
|
|
5
|
-
* `
|
|
6
|
-
* linked agent sessions, one transaction each.
|
|
4
|
+
* change, events and wakes in one transaction (the store publishes the events and delivers the
|
|
5
|
+
* wakes from its outbox after commit, `Tx.wake`), then aborts a cancelled advance and cascades
|
|
6
|
+
* workflow cancels to the linked agent sessions, one transaction each.
|
|
7
7
|
*
|
|
8
|
-
* Wakes (`
|
|
8
|
+
* Wakes (`t.wake`, architecture §12.3) carry the command type as the reason and a dedupe
|
|
9
9
|
* key naming the cause: `<type>:<turnId>:<segment>` for `message`, `approve` and `respond`
|
|
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.
|
|
@@ -231,7 +231,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
231
231
|
reason: command.type,
|
|
232
232
|
dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
|
|
233
233
|
};
|
|
234
|
-
t.
|
|
234
|
+
await t.wake(id, commandWake, ctx.wake);
|
|
235
235
|
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
236
236
|
}
|
|
237
237
|
await t.put("sessions", id, s);
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Business code changes state only inside `ctx.store.tx(async (t) => …)` and follows the
|
|
7
7
|
* seam rules: events through `t.event(...)` (the relay appends them to Durable Streams after
|
|
8
|
-
* commit), and advances through `t.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* commit), and advances through `await t.wake(id, { reason, dedupeKey }, ctx.wake)`, which
|
|
9
|
+
* writes the wake to the outbox in the transaction and delivers it after commit. It never
|
|
10
|
+
* publishes or notifies itself; `runtime.ts` wires the streams with `wireStreams()`. No
|
|
11
|
+
* external I/O runs inside a tx.
|
|
11
12
|
*
|
|
12
13
|
* `wake` goes to `DurableExecution.wake`, which calls the Tenant's `advance` (`advance.ts`)
|
|
13
14
|
* under ownership (§10.6); `abortLocal` aborts an advance running on this process.
|
|
@@ -24,7 +25,6 @@ import type { ModelGate } from "../gates/model-gate.js";
|
|
|
24
25
|
import type { ToolGate } from "../gates/tool-gate.js";
|
|
25
26
|
import type { Keys } from "../keys/keys.js";
|
|
26
27
|
import type { VaultService } from "../vault/service.js";
|
|
27
|
-
import type { CredentialSources } from "../vault/sources.js";
|
|
28
28
|
import type { McpPool } from "../mcp/pool.js";
|
|
29
29
|
import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
|
|
30
30
|
import type { HarnessApiServer } from "../harness-api/server.js";
|
|
@@ -46,6 +46,11 @@ export interface Session {
|
|
|
46
46
|
/** Session pin — the manifest the session was created with. */
|
|
47
47
|
manifest: any;
|
|
48
48
|
manifestHash: string;
|
|
49
|
+
/**
|
|
50
|
+
* The hash of the definition the session was opened from; `manifestHash` differs when the
|
|
51
|
+
* Runtime pinned its own capabilities beside it (R2b C11). Absent on older sessions.
|
|
52
|
+
*/
|
|
53
|
+
definitionHash?: string;
|
|
49
54
|
/** Validated turn-manifest variants, keyed by hash (loops.md §4.2). */
|
|
50
55
|
variants?: Record<string, AgentManifest>;
|
|
51
56
|
implementationVersion: string;
|
|
@@ -139,11 +144,6 @@ export interface TenantContext {
|
|
|
139
144
|
readonly envelope: TenantEnvelope;
|
|
140
145
|
readonly store: SessionStore;
|
|
141
146
|
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
147
|
/**
|
|
148
148
|
* The MCP pool of the Tenant's in-process harness. Absent when harnesses run elsewhere
|
|
149
149
|
* (`NYLORUN_HARNESS=remote`): each keeps its own (F6.2).
|
|
@@ -212,11 +212,13 @@ export interface TenantContext {
|
|
|
212
212
|
/** How long an advance's ownership lease lasts; the heartbeat renews it. */
|
|
213
213
|
readonly ownerLeaseMs: number;
|
|
214
214
|
/**
|
|
215
|
-
* Seam:
|
|
216
|
-
* `t.
|
|
217
|
-
*
|
|
215
|
+
* Seam: hand a wake to `DurableExecution.wake`, outside any tx. Business code passes it to
|
|
216
|
+
* `t.wake` (the outbox), which calls it after commit; the sweep calls it for outbox rows
|
|
217
|
+
* left undelivered and for orphaned sessions. Resolves `true` once the execution accepted
|
|
218
|
+
* the wake, `false` while the Tenant is closing: the wake is not sent, and its outbox row
|
|
219
|
+
* stays for the next sweep.
|
|
218
220
|
*/
|
|
219
|
-
wake(sessionId: string, wake: Wake): Promise<
|
|
221
|
+
wake(sessionId: string, wake: Wake): Promise<boolean>;
|
|
220
222
|
/**
|
|
221
223
|
* Seam: abort the advance of a session running in this process, if any. With `turnId`, only
|
|
222
224
|
* an advance of that turn (a cancel signal names the turn it cancelled).
|
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>;
|
package/dist/tenant/effects.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { embeddedAgent, flowDelegateManifest, hashManifest, } from "@nylorun/core/define";
|
|
2
2
|
import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowToolEffect, isWorkflowManifest, linkedMessageInput, linkedMessageKey, linkedTurnEnd, settleAgentEffect, } from "../core/flow-host.js";
|
|
3
3
|
import { mayDispatchMore } from "../core/limits.js";
|
|
4
|
+
import { sessionCredentials } from "../vault/sources.js";
|
|
4
5
|
import { isRemoteMcpCall } from "../harness/calls.js";
|
|
5
6
|
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
6
7
|
import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
7
|
-
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
8
|
+
import { withPlatformTools, withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
8
9
|
import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
|
|
9
10
|
import { fail } from "./http.js";
|
|
10
11
|
import { turnManifestOf } from "./session.js";
|
|
@@ -150,12 +151,15 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
|
|
|
150
151
|
: undefined;
|
|
151
152
|
// A sandbox chosen when the tree was opened reaches every agent in it, with its tools.
|
|
152
153
|
const inherited = inheritedSandbox(sandboxOwnerId === undefined ? undefined : lookup(sandboxOwnerId) ?? workflow, definition);
|
|
154
|
+
// Without one, an agent with MCP or HTTP tools still reads its stored results (R2b C11).
|
|
155
|
+
const pinned = inherited?.manifest ? inherited : platformTools(definition.manifest);
|
|
153
156
|
const created = {
|
|
154
157
|
id: agentSessionId,
|
|
155
158
|
agentId: body.agentId,
|
|
156
159
|
ownerUserId: workflow.ownerUserId,
|
|
157
|
-
manifest:
|
|
158
|
-
manifestHash:
|
|
160
|
+
manifest: pinned?.manifest ?? definition.manifest,
|
|
161
|
+
manifestHash: pinned?.manifestHash ?? definition.manifestHash,
|
|
162
|
+
definitionHash: definition.manifestHash,
|
|
159
163
|
implementationVersion: definition.implementationVersion,
|
|
160
164
|
status: "idle",
|
|
161
165
|
activeTurnId: null,
|
|
@@ -241,13 +245,24 @@ function inheritedSandbox(owner, definition) {
|
|
|
241
245
|
throw new Error(sandboxed.message);
|
|
242
246
|
return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
|
|
243
247
|
}
|
|
248
|
+
/** A linked agent session's `read_artifact` without a sandbox (R2b C11); undefined for none. */
|
|
249
|
+
function platformTools(manifest) {
|
|
250
|
+
if (isWorkflowManifest(manifest))
|
|
251
|
+
return undefined;
|
|
252
|
+
const pinned = withPlatformTools(manifest);
|
|
253
|
+
if (pinned === undefined)
|
|
254
|
+
return undefined;
|
|
255
|
+
if (!pinned.ok)
|
|
256
|
+
throw new Error(pinned.message);
|
|
257
|
+
return { manifest: pinned.manifest, manifestHash: pinned.manifestHash };
|
|
258
|
+
}
|
|
244
259
|
/**
|
|
245
260
|
* The credential of an MCP server or HTTP tool request made on behalf of a session: from its
|
|
246
|
-
* attached vaults
|
|
261
|
+
* attached vaults (`vault/sources.ts`).
|
|
247
262
|
*/
|
|
248
263
|
export async function authorize(ctx, sessionId, request) {
|
|
249
264
|
const s = await loadSession(ctx, sessionId);
|
|
250
|
-
return ctx.
|
|
265
|
+
return sessionCredentials(ctx.vault, { ...s, id: sessionId }, request);
|
|
251
266
|
}
|
|
252
267
|
/**
|
|
253
268
|
* The definition a flow leaf's session runs. A flow agent embeds its leaves, so they come
|
|
@@ -2,7 +2,6 @@ import { type PostgresClient } from "../store/postgres/connect.js";
|
|
|
2
2
|
import type { TrustedIssuerConfig } from "./identity-file.js";
|
|
3
3
|
import { type TrustedIssuers } from "./issuers.js";
|
|
4
4
|
import type { Logger, TenantModelConfig } from "./types.js";
|
|
5
|
-
import type { ResolverConfig } from "../vault/sources.js";
|
|
6
5
|
export interface StartEphemeralRuntimeOptions {
|
|
7
6
|
/**
|
|
8
7
|
* Absolute Host root for `host.json`, the admin key and the Tenant's files (vault key,
|
|
@@ -37,11 +36,6 @@ export interface StartEphemeralRuntimeOptions {
|
|
|
37
36
|
* `createTrustedIssuers`. Default none.
|
|
38
37
|
*/
|
|
39
38
|
issuers?: readonly TrustedIssuerConfig[] | TrustedIssuers;
|
|
40
|
-
/**
|
|
41
|
-
* The operator's credential resolver (F9 C1): asked for a person's MCP credential when the
|
|
42
|
-
* session's vaults hold none. Default: none (vaults only).
|
|
43
|
-
*/
|
|
44
|
-
resolver?: ResolverConfig;
|
|
45
39
|
logger?: Logger;
|
|
46
40
|
/**
|
|
47
41
|
* The Postgres database of the Tenant (one Tenant per database): a pool, which the caller
|
package/dist/tenant/ephemeral.js
CHANGED
|
@@ -82,7 +82,6 @@ export async function startEphemeralRuntime(options) {
|
|
|
82
82
|
sandbox: { backend: sandboxBackend },
|
|
83
83
|
model,
|
|
84
84
|
...(issuers ? { issuers } : {}),
|
|
85
|
-
...(options.resolver ? { resolver: options.resolver } : {}),
|
|
86
85
|
logger: createTenantLogger({
|
|
87
86
|
tenantId,
|
|
88
87
|
logPath: tenant.log,
|
|
@@ -58,9 +58,19 @@ export declare class OutboundRefused extends Error {
|
|
|
58
58
|
/** A request `guardedFetch` sent that failed: refused connection, reset, timeout, redirect. */
|
|
59
59
|
export declare class OutboundFailed extends Error {
|
|
60
60
|
readonly code: string;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The request was sent in full before it failed, as `post()`'s `lost` (R2b C7): the server
|
|
63
|
+
* may have acted on it. False for a failure to resolve or connect.
|
|
64
|
+
*/
|
|
65
|
+
readonly sent: boolean;
|
|
66
|
+
constructor(message: string, code: string,
|
|
67
|
+
/**
|
|
68
|
+
* The request was sent in full before it failed, as `post()`'s `lost` (R2b C7): the server
|
|
69
|
+
* may have acted on it. False for a failure to resolve or connect.
|
|
70
|
+
*/
|
|
71
|
+
sent?: boolean);
|
|
62
72
|
}
|
|
63
|
-
/** The largest answer `guardedFetch` reads.
|
|
73
|
+
/** The largest answer `guardedFetch` reads. */
|
|
64
74
|
export declare const MAX_FETCH_RESPONSE_BYTES: number;
|
|
65
75
|
/** How long a `guardedFetch` request may take without a signal of its own. */
|
|
66
76
|
export declare const FETCH_TIMEOUT_MS = 30000;
|
|
@@ -68,9 +78,8 @@ export declare const FETCH_TIMEOUT_MS = 30000;
|
|
|
68
78
|
* A `fetch` under the Host's address policy (F9 C2): the literal-IP and DNS checks of `post`
|
|
69
79
|
* on the address actually connected to, `localhost` rewritten for the local stack, no
|
|
70
80
|
* redirects (`redirect: "error"`; a 3xx answer rejects), a bounded answer and a 30 s timeout
|
|
71
|
-
* unless the caller passes a signal.
|
|
72
|
-
*
|
|
73
|
-
* other failure with `OutboundFailed`.
|
|
81
|
+
* unless the caller passes a signal. A refused URL or address rejects with `OutboundRefused`,
|
|
82
|
+
* any other failure with `OutboundFailed`.
|
|
74
83
|
*
|
|
75
84
|
* `stream`: the Response resolves once the headers arrive and its body streams, unbounded and
|
|
76
85
|
* with no timeout but the caller's signal: remote MCP servers, whose answers and event streams
|
package/dist/tenant/outbound.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Runtime's requests to developer URLs: HTTP tools (`gates/http-tool.ts`) and, through
|
|
3
|
-
* `guardedFetch`, remote MCP servers
|
|
3
|
+
* `guardedFetch`, remote MCP servers: one request, no redirects, a bounded answer, and
|
|
4
4
|
* the Host's address policy checked on the address actually connected to, so a DNS answer cannot
|
|
5
5
|
* steer a request somewhere the Host forbids.
|
|
6
6
|
*
|
|
@@ -153,13 +153,20 @@ export class OutboundRefused extends Error {
|
|
|
153
153
|
/** A request `guardedFetch` sent that failed: refused connection, reset, timeout, redirect. */
|
|
154
154
|
export class OutboundFailed extends Error {
|
|
155
155
|
code;
|
|
156
|
-
|
|
156
|
+
sent;
|
|
157
|
+
constructor(message, code,
|
|
158
|
+
/**
|
|
159
|
+
* The request was sent in full before it failed, as `post()`'s `lost` (R2b C7): the server
|
|
160
|
+
* may have acted on it. False for a failure to resolve or connect.
|
|
161
|
+
*/
|
|
162
|
+
sent = false) {
|
|
157
163
|
super(message);
|
|
158
164
|
this.code = code;
|
|
165
|
+
this.sent = sent;
|
|
159
166
|
this.name = "OutboundFailed";
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
|
-
/** The largest answer `guardedFetch` reads.
|
|
169
|
+
/** The largest answer `guardedFetch` reads. */
|
|
163
170
|
export const MAX_FETCH_RESPONSE_BYTES = 1024 * 1024;
|
|
164
171
|
/** How long a `guardedFetch` request may take without a signal of its own. */
|
|
165
172
|
export const FETCH_TIMEOUT_MS = 30_000;
|
|
@@ -167,9 +174,8 @@ export const FETCH_TIMEOUT_MS = 30_000;
|
|
|
167
174
|
* A `fetch` under the Host's address policy (F9 C2): the literal-IP and DNS checks of `post`
|
|
168
175
|
* on the address actually connected to, `localhost` rewritten for the local stack, no
|
|
169
176
|
* redirects (`redirect: "error"`; a 3xx answer rejects), a bounded answer and a 30 s timeout
|
|
170
|
-
* unless the caller passes a signal.
|
|
171
|
-
*
|
|
172
|
-
* other failure with `OutboundFailed`.
|
|
177
|
+
* unless the caller passes a signal. A refused URL or address rejects with `OutboundRefused`,
|
|
178
|
+
* any other failure with `OutboundFailed`.
|
|
173
179
|
*
|
|
174
180
|
* `stream`: the Response resolves once the headers arrive and its body streams, unbounded and
|
|
175
181
|
* with no timeout but the caller's signal: remote MCP servers, whose answers and event streams
|
|
@@ -217,6 +223,9 @@ export function guardedFetch(policy, options = {}) {
|
|
|
217
223
|
outgoing["content-length"] = String(body.byteLength);
|
|
218
224
|
return await new Promise((resolve, reject) => {
|
|
219
225
|
let settled = false;
|
|
226
|
+
// Set when the request is fully sent, as `post()` does: a failure after it may follow
|
|
227
|
+
// the server acting on the request.
|
|
228
|
+
let delivered = false;
|
|
220
229
|
const settle = (fn) => {
|
|
221
230
|
if (settled)
|
|
222
231
|
return;
|
|
@@ -225,12 +234,12 @@ export function guardedFetch(policy, options = {}) {
|
|
|
225
234
|
};
|
|
226
235
|
const failed = (error) => settle(() => reject(error instanceof RefusedAddress
|
|
227
236
|
? new OutboundRefused(error.message)
|
|
228
|
-
: new OutboundFailed(signal?.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal?.aborted ? "ABORTED" : (error.code ?? error.name))));
|
|
237
|
+
: new OutboundFailed(signal?.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal?.aborted ? "ABORTED" : (error.code ?? error.name), delivered)));
|
|
229
238
|
const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, ...(signal ? { signal } : {}) }, (response) => {
|
|
230
239
|
const status = response.statusCode ?? 0;
|
|
231
240
|
if (status >= 300 && status < 400 && response.headers.location !== undefined) {
|
|
232
241
|
response.resume();
|
|
233
|
-
return settle(() => reject(new OutboundFailed(`${originalHost} answered a redirect, which this Runtime does not follow`, "REDIRECT")));
|
|
242
|
+
return settle(() => reject(new OutboundFailed(`${originalHost} answered a redirect, which this Runtime does not follow`, "REDIRECT", true)));
|
|
234
243
|
}
|
|
235
244
|
const noBody = status === 204 || status === 304 || method === "HEAD";
|
|
236
245
|
const answer = (body) => {
|
|
@@ -258,7 +267,7 @@ export function guardedFetch(policy, options = {}) {
|
|
|
258
267
|
response.on("data", (chunk) => {
|
|
259
268
|
size += chunk.length;
|
|
260
269
|
if (size > limit) {
|
|
261
|
-
settle(() => reject(new OutboundFailed(`The answer from ${originalHost} is larger than ${limit} bytes`, "TOO_LARGE")));
|
|
270
|
+
settle(() => reject(new OutboundFailed(`The answer from ${originalHost} is larger than ${limit} bytes`, "TOO_LARGE", true)));
|
|
262
271
|
response.destroy();
|
|
263
272
|
return;
|
|
264
273
|
}
|
|
@@ -268,6 +277,9 @@ export function guardedFetch(policy, options = {}) {
|
|
|
268
277
|
response.on("error", failed);
|
|
269
278
|
response.on("aborted", () => failed(Object.assign(new Error("The answer was cut off"), { code: "ECONNRESET" })));
|
|
270
279
|
});
|
|
280
|
+
sent.on("finish", () => {
|
|
281
|
+
delivered = true;
|
|
282
|
+
});
|
|
271
283
|
sent.on("error", failed);
|
|
272
284
|
sent.end(body);
|
|
273
285
|
});
|