@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { openMcpServer, } from "../mcp/connect.js";
|
|
1
|
+
import { McpCallFailed, McpConnectionStale, openMcpServer, unreachable, } from "../mcp/connect.js";
|
|
2
2
|
import { findServer } from "../mcp/pool.js";
|
|
3
3
|
import { GateRefusal } from "./tenant-vaults.js";
|
|
4
4
|
/** A connection unused this long is closed; the next request opens it again. */
|
|
@@ -65,29 +65,79 @@ export function createMcpHandler(options) {
|
|
|
65
65
|
...fields(server),
|
|
66
66
|
...extra,
|
|
67
67
|
ms: now() - started,
|
|
68
|
-
outcome: failed.code === undefined ? "failed" : String(failed.code),
|
|
68
|
+
outcome: failed.failure?.code ?? (failed.code === undefined ? "failed" : String(failed.code)),
|
|
69
69
|
});
|
|
70
70
|
return { ok: false, error: failed };
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
/** The server's connection; for a tool call, one that cannot be opened is `mcp.unreachable`. */
|
|
74
|
+
async function opened(tenantId, server, call) {
|
|
75
|
+
try {
|
|
76
|
+
return await connection(tenantId, server);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// The call was never sent, so the model may try again (R2b C7). A gate that cannot serve
|
|
80
|
+
// the session is not the server's failure, and stays as it was.
|
|
81
|
+
if (!call || error instanceof GateRefusal || error instanceof NotDeclared)
|
|
82
|
+
throw error;
|
|
83
|
+
throw unreachable(server.serverName, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function use(tenantId, server, run, call = false) {
|
|
87
|
+
const entry = await opened(tenantId, server, call);
|
|
75
88
|
entry.active += 1;
|
|
76
89
|
try {
|
|
77
90
|
return await run(entry);
|
|
78
91
|
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// The connection can no longer make requests: the next one opens a new connection, and
|
|
94
|
+
// the calls still running on this one finish first (the last closes it).
|
|
95
|
+
if (error instanceof McpConnectionStale) {
|
|
96
|
+
const key = keyOf(server);
|
|
97
|
+
if (live.get(key) === entry)
|
|
98
|
+
live.delete(key);
|
|
99
|
+
entry.evicted = true;
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
79
103
|
finally {
|
|
80
104
|
entry.active -= 1;
|
|
81
105
|
entry.lastUsedAt = now();
|
|
106
|
+
if (entry.evicted && entry.active === 0)
|
|
107
|
+
void entry.connection.close().catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* One `tools/call`. A connection found stale before the tool saw the call is opened again, and
|
|
112
|
+
* the call sent once more (R2b C7).
|
|
113
|
+
*/
|
|
114
|
+
async function callOnce(tenantId, request, signal) {
|
|
115
|
+
for (let attempt = 0;; attempt += 1) {
|
|
116
|
+
try {
|
|
117
|
+
return await use(tenantId, request.server, (entry) => entry.connection.client.callTool({ name: request.name, arguments: request.arguments }, { signal }), true);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!(error instanceof McpConnectionStale) || attempt > 0)
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
82
123
|
}
|
|
83
124
|
}
|
|
84
125
|
return {
|
|
85
126
|
connect: (tenantId, server) => answer("connect", server, async () => {
|
|
86
|
-
await connection(tenantId, server);
|
|
87
|
-
|
|
127
|
+
const { instructions } = (await connection(tenantId, server)).connection;
|
|
128
|
+
// The server's instructions, for the note on its deferred tools (R2b C10).
|
|
129
|
+
return instructions === undefined ? null : { instructions };
|
|
88
130
|
}),
|
|
89
131
|
list: (tenantId, server, cursor, signal) => answer("list", server, () => use(tenantId, server, (entry) => entry.connection.client.listTools(cursor ? { cursor } : undefined, { signal }))),
|
|
90
|
-
|
|
132
|
+
async call(tenantId, request, signal) {
|
|
133
|
+
const answered = await answer("call", request.server, () => callOnce(tenantId, request, signal), {
|
|
134
|
+
effect: request.effectId,
|
|
135
|
+
});
|
|
136
|
+
if (!answered.ok)
|
|
137
|
+
return answered;
|
|
138
|
+
const { result, redacted } = answered.result;
|
|
139
|
+
return { ok: true, result, ...(redacted ? { redacted } : {}) };
|
|
140
|
+
},
|
|
91
141
|
async close(server) {
|
|
92
142
|
const key = keyOf(server);
|
|
93
143
|
const entry = live.get(key);
|
|
@@ -125,10 +175,16 @@ function fields(server) {
|
|
|
125
175
|
...(server.agentId === undefined ? {} : { agent: server.agentId }),
|
|
126
176
|
};
|
|
127
177
|
}
|
|
128
|
-
/** An error as the loop's `diagnosticFromError` reads it. */
|
|
178
|
+
/** An error as the loop's `diagnosticFromError` reads it, with a coded failure's code (R2b C7). */
|
|
129
179
|
function errorOf(error) {
|
|
130
180
|
if (error instanceof GateRefusal)
|
|
131
181
|
return { message: error.outcome.message };
|
|
182
|
+
if (error instanceof McpCallFailed)
|
|
183
|
+
return {
|
|
184
|
+
message: error.message,
|
|
185
|
+
...(error.code === "credential_rejected" ? { code: 401 } : {}),
|
|
186
|
+
failure: error.failure,
|
|
187
|
+
};
|
|
132
188
|
const message = error instanceof Error ? error.message.slice(0, 2000) : String(error);
|
|
133
189
|
const code = error && typeof error === "object" && "code" in error && typeof error.code === "number"
|
|
134
190
|
? error.code
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
import type { Credential } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { ModelFailureOutcome } from "@nylorun/core/define";
|
|
11
11
|
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
12
|
-
import { type AuthorizeResult, type HostModelSecret } from "../vault/service.js";
|
|
13
|
-
import {
|
|
12
|
+
import { VaultService, type AuthorizeResult, type HostModelSecret } from "../vault/service.js";
|
|
13
|
+
import { type McpCredentialRequest } from "../vault/sources.js";
|
|
14
14
|
import type { SessionStore } from "../store/types.js";
|
|
15
|
+
import type { OutboundPolicy } from "../tenant/outbound.js";
|
|
15
16
|
import type { Session } from "../tenant/context.js";
|
|
16
17
|
import { type Keys } from "../keys/keys.js";
|
|
17
|
-
import { type OutboundPolicy } from "../tenant/outbound.js";
|
|
18
18
|
/** The Tenant, as a model call needs it. */
|
|
19
19
|
export interface TenantVault {
|
|
20
20
|
readonly tenantId: string;
|
|
@@ -29,8 +29,7 @@ export interface TenantVault {
|
|
|
29
29
|
session(sessionId: string): Promise<Session | undefined>;
|
|
30
30
|
/**
|
|
31
31
|
* The authorization of one request to a session's remote MCP server or HTTP tool: its
|
|
32
|
-
* credential from the session's attached vaults
|
|
33
|
-
* operator's credential resolver (F9 C1).
|
|
32
|
+
* credential from the session's attached vaults.
|
|
34
33
|
*/
|
|
35
34
|
authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
36
35
|
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
@@ -52,18 +51,13 @@ export interface TenantVaultsOptions {
|
|
|
52
51
|
readonly sql: PostgresClient;
|
|
53
52
|
/** The Host root; the gate reads `keys/vault-kek` and `tenant/home` under it. */
|
|
54
53
|
readonly hostRoot: string;
|
|
55
|
-
/**
|
|
56
|
-
readonly resolver?: ResolverConfig;
|
|
57
|
-
/**
|
|
58
|
-
* The Host's address policy (`NYLORUN_ENDPOINT_*`): OAuth refresh and MCP OAuth connect call
|
|
59
|
-
* authorization servers through `guardedFetch` under it (F9 C2).
|
|
60
|
-
*/
|
|
54
|
+
/** How the keys service's tool preview reaches a server (`NYLORUN_ENDPOINT_*`, R2b C12). */
|
|
61
55
|
readonly delivery?: OutboundPolicy;
|
|
62
56
|
}
|
|
63
57
|
export declare function createTenantVaults(options: TenantVaultsOptions): TenantVaults;
|
|
64
58
|
/**
|
|
65
|
-
* A request to a session's remote MCP server, authorized from the session's attached vaults
|
|
66
|
-
*
|
|
67
|
-
*
|
|
59
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults:
|
|
60
|
+
* what the loop does in its own process (`tenant/effects.ts` `authorize`). The vaults come from
|
|
61
|
+
* the session row.
|
|
68
62
|
*/
|
|
69
|
-
export declare function authorizeSessionMcp(
|
|
63
|
+
export declare function authorizeSessionMcp(vault: Pick<VaultService, "authorize">, session: (sessionId: string) => Promise<Session | undefined>, sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
@@ -8,10 +8,9 @@ import { tenantPaths } from "../tenant/paths.js";
|
|
|
8
8
|
import { readVaultKek } from "../vault/kek.js";
|
|
9
9
|
import { HostModelVault } from "../vault/host-model.js";
|
|
10
10
|
import { VaultService } from "../vault/service.js";
|
|
11
|
-
import {
|
|
11
|
+
import { sessionCredentials } from "../vault/sources.js";
|
|
12
12
|
import { inProcessKeys } from "../keys/keys.js";
|
|
13
13
|
import { SigningKeys } from "../tenant/signing-keys.js";
|
|
14
|
-
import { guardedFetch } from "../tenant/outbound.js";
|
|
15
14
|
/** A call the gate refuses before the provider is called; it answers with `outcome`. */
|
|
16
15
|
export class GateRefusal extends Error {
|
|
17
16
|
outcome;
|
|
@@ -35,20 +34,13 @@ export function createTenantVaults(options) {
|
|
|
35
34
|
return kek;
|
|
36
35
|
};
|
|
37
36
|
const vault = new HostModelVault({ store, kek: readKek });
|
|
38
|
-
const credentials = new VaultService({
|
|
39
|
-
store,
|
|
40
|
-
kek: readKek,
|
|
41
|
-
fetch: guardedFetch(options.delivery ?? {}),
|
|
42
|
-
});
|
|
43
|
-
const sources = new CredentialSources({
|
|
44
|
-
vault: credentials,
|
|
45
|
-
...(options.resolver ? { resolver: options.resolver } : {}),
|
|
46
|
-
});
|
|
37
|
+
const credentials = new VaultService({ store, kek: readKek });
|
|
47
38
|
const keys = inProcessKeys({
|
|
48
39
|
store,
|
|
49
40
|
vault: credentials,
|
|
50
41
|
signingKeys: new SigningKeys({ tenantId, kek: readKek }),
|
|
51
42
|
kek: readKek,
|
|
43
|
+
...(options.delivery ? { policy: options.delivery } : {}),
|
|
52
44
|
});
|
|
53
45
|
const session = (sessionId) => store.tx((t) => t.get("sessions", sessionId));
|
|
54
46
|
return {
|
|
@@ -58,7 +50,7 @@ export function createTenantVaults(options) {
|
|
|
58
50
|
readHostModel: () => vault.readHostModel(),
|
|
59
51
|
writeHostCredential: (credential) => vault.updateHostCredential(credential),
|
|
60
52
|
session,
|
|
61
|
-
authorizeMcp: (sessionId, request) => authorizeSessionMcp(
|
|
53
|
+
authorizeMcp: (sessionId, request) => authorizeSessionMcp(credentials, session, sessionId, request),
|
|
62
54
|
keys: () => keys,
|
|
63
55
|
};
|
|
64
56
|
}
|
|
@@ -87,13 +79,13 @@ export function createTenantVaults(options) {
|
|
|
87
79
|
};
|
|
88
80
|
}
|
|
89
81
|
/**
|
|
90
|
-
* A request to a session's remote MCP server, authorized from the session's attached vaults
|
|
91
|
-
*
|
|
92
|
-
*
|
|
82
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults:
|
|
83
|
+
* what the loop does in its own process (`tenant/effects.ts` `authorize`). The vaults come from
|
|
84
|
+
* the session row.
|
|
93
85
|
*/
|
|
94
|
-
export async function authorizeSessionMcp(
|
|
86
|
+
export async function authorizeSessionMcp(vault, session, sessionId, request) {
|
|
95
87
|
const found = await session(sessionId);
|
|
96
88
|
if (!found)
|
|
97
89
|
throw new Error(`Session ${sessionId} not found`);
|
|
98
|
-
return
|
|
90
|
+
return sessionCredentials(vault, { ...found, id: sessionId }, request);
|
|
99
91
|
}
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* has finished.
|
|
7
7
|
*
|
|
8
8
|
* Failures keep the meaning they had in the loop's own process:
|
|
9
|
-
* - An MCP request that fails
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - An MCP request that fails throws, so the pool reports a diagnostic. A tool call's coded
|
|
10
|
+
* failure (R2b C7) throws the `McpCallFailed` the loop's own call would have, which becomes a
|
|
11
|
+
* failed outcome the model sees. Only the hop's failures and a call lost with the gateway leave
|
|
12
|
+
* it `uncertain`.
|
|
13
|
+
* - An HTTP tool call throws only for the hop, or a call lost with the gateway: its own failures
|
|
14
|
+
* are outcomes.
|
|
12
15
|
*
|
|
13
16
|
* Credentials (F5): a session's MCP requests and keyed cancels carry its run token, read per
|
|
14
17
|
* request from the advance's grant, and leave the session out of the body: the gate takes it
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
import { randomUUID } from "node:crypto";
|
|
20
23
|
import { request as httpRequest } from "node:http";
|
|
21
24
|
import { request as httpsRequest } from "node:https";
|
|
25
|
+
import { McpCallFailed, } from "../mcp/connect.js";
|
|
22
26
|
import { TENANT_HEADER } from "./contract.js";
|
|
23
27
|
import { GATE_CLIENT_TIMEOUT_MS } from "./http-client.js";
|
|
24
28
|
import { HTTP_CALLS_PATH, MCP_CLOSE_PATH, MCP_CONNECT_PATH, MCP_LIST_PATH, TOOL_CALLS_PATH, } from "./tool-contract.js";
|
|
@@ -106,10 +110,11 @@ export function httpToolGate(options) {
|
|
|
106
110
|
return { body: body(unnamed), bearer: token };
|
|
107
111
|
}
|
|
108
112
|
/**
|
|
109
|
-
* The
|
|
110
|
-
* before
|
|
113
|
+
* The answer of an MCP or HTTP tool request, or a throw: a coded MCP failure (`McpCallFailed`),
|
|
114
|
+
* or one the pool and `resolveEffect` read as before, the call's fate unknown and the effect
|
|
115
|
+
* `uncertain`.
|
|
111
116
|
*/
|
|
112
|
-
async function
|
|
117
|
+
async function answer(path, scope, request) {
|
|
113
118
|
const result = await exchange(path, scope.body, {
|
|
114
119
|
...request,
|
|
115
120
|
timeoutMs,
|
|
@@ -123,9 +128,12 @@ export function httpToolGate(options) {
|
|
|
123
128
|
if (result.status !== 200 || !parsed || !("ok" in parsed))
|
|
124
129
|
throw new Error(gateRefusal(result.status, parsed));
|
|
125
130
|
if (parsed.ok)
|
|
126
|
-
return parsed
|
|
131
|
+
return parsed;
|
|
127
132
|
throw mcpError(parsed.error);
|
|
128
133
|
}
|
|
134
|
+
async function answered(path, scope, request) {
|
|
135
|
+
return (await answer(path, scope, request)).result;
|
|
136
|
+
}
|
|
129
137
|
async function fireAndForget(path, scope) {
|
|
130
138
|
await exchange(path, scope.body, {
|
|
131
139
|
timeoutMs: SHORT_TIMEOUT_MS,
|
|
@@ -136,24 +144,32 @@ export function httpToolGate(options) {
|
|
|
136
144
|
recovers: true,
|
|
137
145
|
async openMcp(server) {
|
|
138
146
|
// The credential is read per request: the connection outlives the advance that opened it.
|
|
139
|
-
|
|
147
|
+
// A gate answers the server's instructions (R2b C10); an older one answers null.
|
|
148
|
+
const connected = await answered(MCP_CONNECT_PATH, scoped(server, (named) => ({ server: named })), {});
|
|
149
|
+
const instructions = typeof connected?.instructions === "string" && connected.instructions.length > 0
|
|
150
|
+
? connected.instructions
|
|
151
|
+
: undefined;
|
|
140
152
|
const client = {
|
|
141
153
|
listTools: (params, request) => answered(MCP_LIST_PATH, scoped(server, (named) => ({
|
|
142
154
|
server: named,
|
|
143
155
|
...(params?.cursor ? { cursor: params.cursor } : {}),
|
|
144
156
|
})), request?.signal ? { signal: request.signal } : {}),
|
|
145
|
-
callTool
|
|
146
|
-
server
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
async callTool(params, request) {
|
|
158
|
+
const called = await answer(TOOL_CALLS_PATH, scoped(server, (named) => ({
|
|
159
|
+
server: named,
|
|
160
|
+
effectId: request?.key ?? randomUUID(),
|
|
161
|
+
name: params.name,
|
|
162
|
+
arguments: params.arguments,
|
|
163
|
+
})), {
|
|
164
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
165
|
+
...(request?.key ? { idempotencyKey: request.key } : {}),
|
|
166
|
+
});
|
|
167
|
+
return { result: called.result, ...(called.redacted ? { redacted: called.redacted } : {}) };
|
|
168
|
+
},
|
|
154
169
|
};
|
|
155
170
|
return {
|
|
156
171
|
client,
|
|
172
|
+
...(instructions === undefined ? {} : { instructions }),
|
|
157
173
|
close: () => fireAndForget(MCP_CLOSE_PATH, scoped(server, (named) => ({ server: named }))),
|
|
158
174
|
};
|
|
159
175
|
},
|
|
@@ -174,6 +190,9 @@ export function httpToolGate(options) {
|
|
|
174
190
|
}
|
|
175
191
|
/** An MCP failure the gate reported, as the loop's own MCP code would have thrown it. */
|
|
176
192
|
function mcpError(error) {
|
|
193
|
+
// Coded (R2b C7): the model sees it. A call lost with the gateway never is.
|
|
194
|
+
if (error.failure && !error.uncertain)
|
|
195
|
+
return McpCallFailed.from(error.failure);
|
|
177
196
|
const thrown = new Error(error.uncertain ? `${error.message} (the call may have run; it is not sent again)` : error.message);
|
|
178
197
|
if (error.code !== undefined)
|
|
179
198
|
thrown.code = error.code;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* Remote MCP. The gate holds the connection and the vault credential; the loop names the
|
|
12
12
|
* server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
|
|
13
13
|
* - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
|
|
14
|
-
* authorization included), or keeps the open one.
|
|
14
|
+
* authorization included), or keeps the open one. Its result is `{instructions}` when the
|
|
15
|
+
* server gave instructions at `initialize` (R2b C10, for the note on its deferred tools), else
|
|
16
|
+
* `null`.
|
|
15
17
|
* - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
|
|
16
18
|
* - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
|
|
17
19
|
* `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
|
|
@@ -20,8 +22,12 @@
|
|
|
20
22
|
* gate_forbidden` for another session's call under a run token.
|
|
21
23
|
* - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
|
|
22
24
|
* Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
|
|
23
|
-
* answer, not a gate error. A
|
|
24
|
-
*
|
|
25
|
+
* answer, not a gate error. A tool call's failure is coded (`error.failure`, R2b C7), and neither
|
|
26
|
+
* a result nor a failure carries a credential value the gate sent (C8). An answer past 8 MiB is
|
|
27
|
+
* `mcp.too-large` (C11), as an HTTP tool's is `http.too-large`; core shapes what fits into a
|
|
28
|
+
* result the model can take (`tenant/tool-results.ts`). A keyed call whose
|
|
29
|
+
* earlier attempt was lost with the gateway answers `{ok: false, error: {uncertain: true}}` and
|
|
30
|
+
* is never run again.
|
|
25
31
|
*
|
|
26
32
|
* HTTP tools (R2 M3). `POST /nylorun/v1/http-calls` `{tool, effectId, turnId, input}`: one call
|
|
27
33
|
* of an HTTP tool. The loop names the tool (`{sessionId?, agentId?, capabilityId, toolName}`), or a
|
|
@@ -33,6 +39,7 @@
|
|
|
33
39
|
* model sees), or `{ok: false, error: {uncertain: true}}` for a call lost with the gateway.
|
|
34
40
|
*/
|
|
35
41
|
import { z } from "zod";
|
|
42
|
+
import type { McpCallFailure } from "../mcp/connect.js";
|
|
36
43
|
export declare const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
|
|
37
44
|
export declare const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
|
|
38
45
|
export declare const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
|
|
@@ -99,10 +106,19 @@ export interface McpGateError {
|
|
|
99
106
|
readonly credentialIds?: readonly string[];
|
|
100
107
|
/** The call may have run: its earlier attempt was lost with the gateway. Never re-run. */
|
|
101
108
|
readonly uncertain?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* How the request failed, coded (R2b C1, C7): the loop's `McpCallFailed`, which a tool call
|
|
111
|
+
* turns into a failed outcome the model sees (`mcp.lost` only for a read-only or idempotent
|
|
112
|
+
* tool). `credential_rejected` for a `401`, with the server and the scope of the vault whose
|
|
113
|
+
* credential was sent. Its message has no credential value in it (C8).
|
|
114
|
+
*/
|
|
115
|
+
readonly failure?: McpCallFailure;
|
|
102
116
|
}
|
|
103
117
|
export type McpAnswer<T> = {
|
|
104
118
|
readonly ok: true;
|
|
105
119
|
readonly result: T;
|
|
120
|
+
/** A `tools/call`'s: how many credential values its result echoed, replaced (R2b C8). */
|
|
121
|
+
readonly redacted?: number;
|
|
106
122
|
} | {
|
|
107
123
|
readonly ok: false;
|
|
108
124
|
readonly error: McpGateError;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* Remote MCP. The gate holds the connection and the vault credential; the loop names the
|
|
12
12
|
* server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
|
|
13
13
|
* - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
|
|
14
|
-
* authorization included), or keeps the open one.
|
|
14
|
+
* authorization included), or keeps the open one. Its result is `{instructions}` when the
|
|
15
|
+
* server gave instructions at `initialize` (R2b C10, for the note on its deferred tools), else
|
|
16
|
+
* `null`.
|
|
15
17
|
* - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
|
|
16
18
|
* - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
|
|
17
19
|
* `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
|
|
@@ -20,8 +22,12 @@
|
|
|
20
22
|
* gate_forbidden` for another session's call under a run token.
|
|
21
23
|
* - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
|
|
22
24
|
* Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
|
|
23
|
-
* answer, not a gate error. A
|
|
24
|
-
*
|
|
25
|
+
* answer, not a gate error. A tool call's failure is coded (`error.failure`, R2b C7), and neither
|
|
26
|
+
* a result nor a failure carries a credential value the gate sent (C8). An answer past 8 MiB is
|
|
27
|
+
* `mcp.too-large` (C11), as an HTTP tool's is `http.too-large`; core shapes what fits into a
|
|
28
|
+
* result the model can take (`tenant/tool-results.ts`). A keyed call whose
|
|
29
|
+
* earlier attempt was lost with the gateway answers `{ok: false, error: {uncertain: true}}` and
|
|
30
|
+
* is never run again.
|
|
25
31
|
*
|
|
26
32
|
* HTTP tools (R2 M3). `POST /nylorun/v1/http-calls` `{tool, effectId, turnId, input}`: one call
|
|
27
33
|
* of an HTTP tool. The loop names the tool (`{sessionId?, agentId?, capabilityId, toolName}`), or a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Tool Gate seam (blueprint §12, F4.1). Every tool call that leaves the loop crosses one
|
|
3
3
|
* `ToolGate`: a remote MCP server's connection and calls, and an HTTP tool's request (R2 M3).
|
|
4
|
-
* The gate owns what those need that the loop must not hold: the MCP and HTTP tool credentials
|
|
5
|
-
* and
|
|
4
|
+
* The gate owns what those need that the loop must not hold: the MCP and HTTP tool credentials,
|
|
5
|
+
* and the outbound route.
|
|
6
6
|
*
|
|
7
7
|
* Two implementations: `inProcessToolGate` (embedding, the ephemeral Runtime, tests), which
|
|
8
8
|
* opens remote servers and calls HTTP tools in this process, and the HTTP client of the gates
|
package/dist/harness/calls.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export declare function invokeModel(route: ModelRoute, request: HostEffect, sign
|
|
|
46
46
|
export declare function abortOn(signal: AbortSignal, kinds: readonly AbortReason[]): AbortSignal;
|
|
47
47
|
/** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
|
|
48
48
|
export declare function isSaveArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
|
|
49
|
+
/** True when `request` calls `read_artifact` (R2b C11) of the `nylorun.artifacts` capability. */
|
|
50
|
+
export declare function isReadArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
|
|
49
51
|
/** True when `request` calls a tool of a declared MCP server (every one is remote). */
|
|
50
52
|
export declare function isRemoteMcpCall(routing: Pick<ToolRouting, "rootManifest" | "mcpSnapshot">, request: HostEffect): boolean;
|
|
51
53
|
export declare function callMcpTool(mcp: McpPool, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
|
package/dist/harness/calls.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tenant's provider), the Tool Gate, the MCP pool, the SandboxManager. No store, no record: the
|
|
4
4
|
* session's routing comes with the run (`TurnStart.routing`).
|
|
5
5
|
*/
|
|
6
|
-
import { ARTIFACTS_CAPABILITY_ID, SAVE_ARTIFACT_TOOL } from "@nylorun/core/define";
|
|
6
|
+
import { ARTIFACTS_CAPABILITY_ID, READ_ARTIFACT_TOOL, SAVE_ARTIFACT_TOOL, } from "@nylorun/core/define";
|
|
7
7
|
import { runAbortKind } from "@nylorun/harness/api";
|
|
8
8
|
import { httpToolRefOf } from "../gates/http-tool.js";
|
|
9
9
|
import { classifyThrown } from "../model/classify.js";
|
|
@@ -67,10 +67,18 @@ export function abortOn(signal, kinds) {
|
|
|
67
67
|
}
|
|
68
68
|
/** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
|
|
69
69
|
export function isSaveArtifactCall(manifest, request) {
|
|
70
|
+
return isArtifactsToolCall(manifest, request, SAVE_ARTIFACT_TOOL);
|
|
71
|
+
}
|
|
72
|
+
/** True when `request` calls `read_artifact` (R2b C11) of the `nylorun.artifacts` capability. */
|
|
73
|
+
export function isReadArtifactCall(manifest, request) {
|
|
74
|
+
return isArtifactsToolCall(manifest, request, READ_ARTIFACT_TOOL);
|
|
75
|
+
}
|
|
76
|
+
function isArtifactsToolCall(manifest, request, toolName) {
|
|
70
77
|
return (request.kind === "tool" &&
|
|
71
78
|
request.capabilityId === ARTIFACTS_CAPABILITY_ID &&
|
|
72
|
-
request.toolName ===
|
|
73
|
-
manifest?.capabilities.some((capability) => capability.id === ARTIFACTS_CAPABILITY_ID
|
|
79
|
+
request.toolName === toolName &&
|
|
80
|
+
manifest?.capabilities.some((capability) => capability.id === ARTIFACTS_CAPABILITY_ID &&
|
|
81
|
+
capability.tools?.some((tool) => tool.name === toolName) === true) === true);
|
|
74
82
|
}
|
|
75
83
|
/** True when `request` calls a tool of a declared MCP server (every one is remote). */
|
|
76
84
|
export function isRemoteMcpCall(routing, request) {
|
|
@@ -94,6 +102,8 @@ export async function callMcpTool(mcp, routing, request, signal) {
|
|
|
94
102
|
args: request.input,
|
|
95
103
|
manifest: routing.rootManifest,
|
|
96
104
|
effectId: request.effectId,
|
|
105
|
+
// A read-only or idempotent tool's lost answer is the model's to retry (R2b C7, Q15).
|
|
106
|
+
retrySafe: tool.annotations?.readOnlyHint === true || tool.annotations?.idempotentHint === true,
|
|
97
107
|
signal,
|
|
98
108
|
});
|
|
99
109
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Record seam of the Harness API: core's journal for the effects of a run. `recordIntent`
|
|
3
3
|
* journals an effect before anything runs it, and either answers it from the journal, hands it
|
|
4
|
-
* to core's own executors (flow work, delegation, `save_artifact`
|
|
5
|
-
* the harness to `execute` it (model calls; MCP, HTTP
|
|
6
|
-
* and
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* to core's own executors (flow work, delegation, `save_artifact`, `read_artifact`,
|
|
5
|
+
* `tool_search` and the skill tools), tells the harness to `execute` it (model calls; MCP, HTTP
|
|
6
|
+
* and sandbox tools, a deferred MCP tool's `tool_call` among them, whose effect names the tool;
|
|
7
|
+
* a flow's HTTP stages and HTTP verifiers), or fails a tool the Runtime cannot run (one that
|
|
8
|
+
* would run the developer's code, R2 M6). `recordOutcome` records what the harness's call returned, an
|
|
9
|
+
* agent's MCP or HTTP tool result first shaped to fit (R2b C11). Both run under the advance's
|
|
10
|
+
* lease: every write is epoch-checked (`ownedSession`), and a lost epoch writes nothing.
|
|
10
11
|
*
|
|
11
12
|
* Rows store the request's hash (`requestHash`) and, for a model call, the request without its
|
|
12
13
|
* prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
|