@nylorun/runtime 0.20.0-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 +64 -0
- package/README.md +15 -10
- 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/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/gates.js +0 -1
- package/dist/host/main.js +13 -3
- package/dist/host/stack-config.d.ts +5 -8
- package/dist/host/stack-config.js +8 -19
- 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/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +2 -261
- package/dist/store/postgres/schema.js +2 -36
- package/dist/store/postgres/store.js +1 -18
- package/dist/store/postgres/tenant.d.ts +14 -0
- package/dist/store/postgres/tenant.js +22 -2
- package/dist/store/types.d.ts +2 -8
- package/dist/tenant/artifact-tool.d.ts +9 -1
- package/dist/tenant/artifact-tool.js +113 -9
- package/dist/tenant/context.d.ts +5 -6
- 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 +9 -14
- package/dist/tenant/sessions.js +20 -3
- package/dist/tenant/store-pg.js +9 -0
- 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
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* tables of a pre-release build of it. It is never touched: this release starts fresh.
|
|
12
12
|
* 2. Apply the missing migrations (`migrate.ts`). A database with migrations this Runtime
|
|
13
13
|
* does not know is `schema-too-new`; a migration Postgres rejects is `migration-failed`.
|
|
14
|
+
* `0016_mcp_oauth_removed` deletes the vault's MCP OAuth credentials (protocol 10), each
|
|
15
|
+
* with an audit row; the open returns them (`removedCredentials`) for a boot warning.
|
|
14
16
|
* 3. When `nylorun.tenant` is empty, create the Tenant: its row (id, name, created time) and
|
|
15
17
|
* its first principals. A database whose row is gone but that holds a Tenant's data
|
|
16
18
|
* (principals, sessions, agents, vaults, keys) is `envelope-invalid`, never given a new
|
|
@@ -74,6 +76,18 @@ export interface OpenedTenantDatabase {
|
|
|
74
76
|
from: number;
|
|
75
77
|
to: number;
|
|
76
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* The MCP OAuth credentials this call's migration `0016_mcp_oauth_removed` deleted (protocol
|
|
81
|
+
* 10), from the audit rows it wrote: the Host names each in a boot warning. Empty otherwise.
|
|
82
|
+
*/
|
|
83
|
+
removedCredentials: RemovedCredential[];
|
|
84
|
+
}
|
|
85
|
+
/** A vault credential a migration deleted. */
|
|
86
|
+
export interface RemovedCredential {
|
|
87
|
+
vaultId: string;
|
|
88
|
+
credentialId: string;
|
|
89
|
+
/** The URL it was bound to. */
|
|
90
|
+
url: string | null;
|
|
77
91
|
}
|
|
78
92
|
/**
|
|
79
93
|
* Migrates the database, creates its Tenant when it has none, and opens its Session Store.
|
|
@@ -4,8 +4,10 @@ import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
|
4
4
|
import { openError, TenantOpenError } from "../../tenant/cause.js";
|
|
5
5
|
import { database, driverError } from "./db.js";
|
|
6
6
|
import { applyMigrations, assertCurrentLayout, isStatementError, lockMigrations, shippedMigrations, } from "./migrate.js";
|
|
7
|
-
import { definitions, principals, sessions, signingKeys, TENANT_SCHEMA, tenant, vaults, } from "./schema.js";
|
|
7
|
+
import { definitions, principals, sessions, signingKeys, TENANT_SCHEMA, tenant, vaultAudit, vaults, } from "./schema.js";
|
|
8
8
|
import { createPostgresSessionStore } from "./store.js";
|
|
9
|
+
/** The migration that deletes the vault's `oauth` credentials, each with an audit row. */
|
|
10
|
+
const OAUTH_REMOVED_MIGRATION = "0016_mcp_oauth_removed";
|
|
9
11
|
/**
|
|
10
12
|
* Migrates the database, creates its Tenant when it has none, and opens its Session Store.
|
|
11
13
|
* Throws a `TenantOpenError` with the cause when the Tenant cannot be opened (see above).
|
|
@@ -22,7 +24,8 @@ export async function openTenantDatabase(options) {
|
|
|
22
24
|
await assertCurrentLayout(tx);
|
|
23
25
|
const migrated = await applyMigrations(tx, migrations);
|
|
24
26
|
const created = await ensureTenant(tx, options.create, migrated, now);
|
|
25
|
-
|
|
27
|
+
const removedCredentials = await removedBy(tx, migrations, migrated);
|
|
28
|
+
return { created, migrated, removedCredentials };
|
|
26
29
|
});
|
|
27
30
|
}
|
|
28
31
|
catch (thrown) {
|
|
@@ -82,6 +85,23 @@ export async function readTenantEnvelope(q) {
|
|
|
82
85
|
throw driverError(error);
|
|
83
86
|
}
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The credentials `0016_mcp_oauth_removed` deleted, when this transaction applied it: its
|
|
90
|
+
* audit rows (actor `migration`). Empty when it ran before, or is not among `migrations`.
|
|
91
|
+
*/
|
|
92
|
+
async function removedBy(tx, migrations, migrated) {
|
|
93
|
+
const index = migrations.findIndex((migration) => migration.tag === OAUTH_REMOVED_MIGRATION);
|
|
94
|
+
if (index < migrated.from || index >= migrated.to)
|
|
95
|
+
return [];
|
|
96
|
+
const rows = await tx
|
|
97
|
+
.select({ vaultId: vaultAudit.vaultId, credentialId: vaultAudit.credentialId, url: vaultAudit.target })
|
|
98
|
+
.from(vaultAudit)
|
|
99
|
+
.where(and(eq(vaultAudit.actor, "migration"), eq(vaultAudit.action, "delete")))
|
|
100
|
+
.orderBy(vaultAudit.ord);
|
|
101
|
+
return rows.flatMap((row) => row.vaultId && row.credentialId
|
|
102
|
+
? [{ vaultId: row.vaultId, credentialId: row.credentialId, url: row.url }]
|
|
103
|
+
: []);
|
|
104
|
+
}
|
|
85
105
|
/**
|
|
86
106
|
* Creates the Tenant when there is none, and adds the principals it is missing. Records the
|
|
87
107
|
* schema version on the Tenant row when this transaction migrated.
|
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";
|
|
@@ -483,12 +483,6 @@ export interface Tx {
|
|
|
483
483
|
getVaultIdempotency(id: string): Promise<VaultIdempotencyRow | undefined>;
|
|
484
484
|
/** Rejects when the id exists. */
|
|
485
485
|
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
486
|
/**
|
|
493
487
|
* Serializes signing key changes: held until the transaction ends. Take it before reading
|
|
494
488
|
* the keys a change depends on, so concurrent first uses or rotations do not collide.
|
|
@@ -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/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).
|
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
|
});
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -5,7 +5,6 @@ import { tenantModelGate } from "../gates/in-process.js";
|
|
|
5
5
|
import { createKekFile, readVaultKek } from "../vault/kek.js";
|
|
6
6
|
import { SigningKeys } from "./signing-keys.js";
|
|
7
7
|
import { VaultService } from "../vault/service.js";
|
|
8
|
-
import { CredentialSources } from "../vault/sources.js";
|
|
9
8
|
import { McpPool } from "../mcp/pool.js";
|
|
10
9
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
11
10
|
import { storeSandboxRecords } from "../sandbox/records.js";
|
|
@@ -22,7 +21,6 @@ import { sweep } from "./sweep.js";
|
|
|
22
21
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
23
22
|
import { authorize } from "./effects.js";
|
|
24
23
|
import { inProcessToolGate } from "../gates/tool-gate.js";
|
|
25
|
-
import { guardedFetch } from "./outbound.js";
|
|
26
24
|
import { inProcessKeys } from "../keys/keys.js";
|
|
27
25
|
import { tenantApi } from "../api/http/app.js";
|
|
28
26
|
import { createFsBlobStore } from "../blob/index.js";
|
|
@@ -118,16 +116,7 @@ export class TenantRuntime {
|
|
|
118
116
|
const opened = store;
|
|
119
117
|
// A seeded Tenant setting wins over the Host default (A18).
|
|
120
118
|
const seededBackend = await opened.tx((t) => t.getSetting("sandbox.backend"));
|
|
121
|
-
const vault = new VaultService({
|
|
122
|
-
store: opened,
|
|
123
|
-
kek: ensureKek,
|
|
124
|
-
// OAuth refresh and connect follow the Host's address policy (F9 C2).
|
|
125
|
-
fetch: config.vaultFetch ?? guardedFetch(config.delivery ?? {}),
|
|
126
|
-
});
|
|
127
|
-
const credentials = new CredentialSources({
|
|
128
|
-
vault,
|
|
129
|
-
...(config.resolver ? { resolver: config.resolver } : {}),
|
|
130
|
-
});
|
|
119
|
+
const vault = new VaultService({ store: opened, kek: ensureKek });
|
|
131
120
|
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
132
121
|
let ctx;
|
|
133
122
|
const toolGate = hooks.toolGate ??
|
|
@@ -137,7 +126,14 @@ export class TenantRuntime {
|
|
|
137
126
|
logger: config.logger,
|
|
138
127
|
});
|
|
139
128
|
const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
|
|
140
|
-
const keys = hooks.keys ??
|
|
129
|
+
const keys = hooks.keys ??
|
|
130
|
+
inProcessKeys({
|
|
131
|
+
store: opened,
|
|
132
|
+
vault,
|
|
133
|
+
signingKeys,
|
|
134
|
+
kek: ensureKek,
|
|
135
|
+
policy: config.delivery ?? {},
|
|
136
|
+
});
|
|
141
137
|
const harnessMode = hooks.harness ?? "memory";
|
|
142
138
|
const inProcess = harnessMode === "memory" || harnessMode === "json";
|
|
143
139
|
const preference = seededBackend ?? config.sandbox.backend;
|
|
@@ -242,7 +238,6 @@ export class TenantRuntime {
|
|
|
242
238
|
reads: hooks.reads,
|
|
243
239
|
store: opened,
|
|
244
240
|
vault,
|
|
245
|
-
credentials,
|
|
246
241
|
...(mcp ? { mcp } : {}),
|
|
247
242
|
sandbox,
|
|
248
243
|
flowLimits,
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -10,7 +10,7 @@ import { aggregateWaits, isWorkflowManifest } from "../core/flow-host.js";
|
|
|
10
10
|
import { canonical } from "../store/canonical.js";
|
|
11
11
|
import { validateSandboxAttach } from "../core/sandbox-routes.js";
|
|
12
12
|
import { resolveSandbox } from "../sandbox/resolve.js";
|
|
13
|
-
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
13
|
+
import { withPlatformTools, withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
14
14
|
import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
15
15
|
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
16
16
|
import { checkPlacement, requirePods } from "../sandbox/placement.js";
|
|
@@ -156,12 +156,14 @@ export async function putSession(ctx, id, body, access, options = {}) {
|
|
|
156
156
|
checkPlacement(await readSandboxConfig(t), sandbox.kind);
|
|
157
157
|
if (sandbox.kind === "pod")
|
|
158
158
|
await requirePods(ctx.pods, `Sandbox ${sandbox.sandboxId}`);
|
|
159
|
+
const pinned = sandbox.manifest === undefined ? platformTools(definition) : sandbox;
|
|
159
160
|
const created = {
|
|
160
161
|
id,
|
|
161
162
|
agentId: body.agentId,
|
|
162
163
|
ownerUserId: body.ownerUserId,
|
|
163
|
-
manifest:
|
|
164
|
-
manifestHash:
|
|
164
|
+
manifest: pinned.manifest ?? definition.manifest,
|
|
165
|
+
manifestHash: pinned.manifestHash ?? definition.manifestHash,
|
|
166
|
+
definitionHash: definition.manifestHash,
|
|
165
167
|
implementationVersion: definition.implementationVersion,
|
|
166
168
|
info: body.info,
|
|
167
169
|
status: "idle",
|
|
@@ -232,6 +234,20 @@ async function sessionSandbox(t, body, definition, options) {
|
|
|
232
234
|
return fail(resolved.status, resolved.errors.join(" "));
|
|
233
235
|
return pin(definition, resolved.spec, resolved.source);
|
|
234
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The manifest a session without a sandbox pins: its agents' `read_artifact` (R2b C11), when
|
|
239
|
+
* one has a remote MCP server or an HTTP tool; nothing otherwise, and for a workflow.
|
|
240
|
+
*/
|
|
241
|
+
function platformTools(definition) {
|
|
242
|
+
if (isWorkflowManifest(definition.manifest))
|
|
243
|
+
return {};
|
|
244
|
+
const pinned = withPlatformTools(definition.manifest);
|
|
245
|
+
if (pinned === undefined)
|
|
246
|
+
return {};
|
|
247
|
+
if (!pinned.ok)
|
|
248
|
+
return fail(400, pinned.message);
|
|
249
|
+
return { manifest: pinned.manifest, manifestHash: pinned.manifestHash };
|
|
250
|
+
}
|
|
235
251
|
async function pin(definition, spec, source) {
|
|
236
252
|
// A workflow's manifest stays as registered: its agents inherit the sandbox when they start.
|
|
237
253
|
if (isWorkflowManifest(definition.manifest))
|
|
@@ -269,6 +285,7 @@ export async function sessionView(t, s) {
|
|
|
269
285
|
agentId: s.agentId,
|
|
270
286
|
ownerUserId: s.ownerUserId,
|
|
271
287
|
manifestHash: s.manifestHash,
|
|
288
|
+
...(s.definitionHash === undefined ? {} : { definitionHash: s.definitionHash }),
|
|
272
289
|
implementationVersion: s.implementationVersion,
|
|
273
290
|
status: s.status,
|
|
274
291
|
activeTurnId: s.activeTurnId,
|