@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { openMcpServer, type McpToolPage } from "../mcp/connect.js";
|
|
2
|
+
import { type McpServerRef } from "../mcp/pool.js";
|
|
3
|
+
import type { Logger } from "../tenant/types.js";
|
|
4
|
+
import type { McpAnswer } from "./tool-contract.js";
|
|
5
|
+
import { type TenantVaults } from "./tenant-vaults.js";
|
|
6
|
+
/** A connection unused this long is closed; the next request opens it again. */
|
|
7
|
+
export declare const GATE_MCP_IDLE_MS: number;
|
|
8
|
+
export interface McpHandlerOptions {
|
|
9
|
+
readonly vaults: TenantVaults;
|
|
10
|
+
readonly logger: Logger;
|
|
11
|
+
/** Default `GATE_MCP_IDLE_MS`. */
|
|
12
|
+
readonly idleMs?: number;
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
/** Tests replace how a server is opened. */
|
|
15
|
+
readonly open?: typeof openMcpServer;
|
|
16
|
+
}
|
|
17
|
+
export interface McpHandler {
|
|
18
|
+
connect(tenantId: string | undefined, server: McpServerRef): Promise<McpAnswer<null>>;
|
|
19
|
+
list(tenantId: string | undefined, server: McpServerRef, cursor: string | undefined, signal: AbortSignal): Promise<McpAnswer<McpToolPage>>;
|
|
20
|
+
/** One `tools/call`; the raw `CallToolResult` on success. */
|
|
21
|
+
call(tenantId: string | undefined, request: {
|
|
22
|
+
server: McpServerRef;
|
|
23
|
+
effectId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
arguments: Record<string, unknown>;
|
|
26
|
+
}, signal: AbortSignal): Promise<McpAnswer<Record<string, unknown>>>;
|
|
27
|
+
close(server: McpServerRef): Promise<void>;
|
|
28
|
+
/** Closes connections idle past the timeout. */
|
|
29
|
+
sweep(): Promise<void>;
|
|
30
|
+
/** Closes every connection (gateway shutdown). */
|
|
31
|
+
closeAll(): Promise<void>;
|
|
32
|
+
/** Open connections (tests). */
|
|
33
|
+
readonly size: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function createMcpHandler(options: McpHandlerOptions): McpHandler;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { openMcpServer, } from "../mcp/connect.js";
|
|
2
|
+
import { findServer } from "../mcp/pool.js";
|
|
3
|
+
import { GateRefusal } from "./tenant-vaults.js";
|
|
4
|
+
/** A connection unused this long is closed; the next request opens it again. */
|
|
5
|
+
export const GATE_MCP_IDLE_MS = 15 * 60_000;
|
|
6
|
+
/** The server is not one the session may reach through the gate. */
|
|
7
|
+
class NotDeclared extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function createMcpHandler(options) {
|
|
10
|
+
const { vaults, logger } = options;
|
|
11
|
+
const idleMs = options.idleMs ?? GATE_MCP_IDLE_MS;
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
const open = options.open ?? openMcpServer;
|
|
14
|
+
const live = new Map();
|
|
15
|
+
const opening = new Map();
|
|
16
|
+
const keyOf = (server) => JSON.stringify([server.sessionId, server.agentId ?? null, server.capabilityId, server.serverName]);
|
|
17
|
+
/** The declared remote server, from the session's pinned manifest. */
|
|
18
|
+
async function declared(vault, server) {
|
|
19
|
+
const session = await vault.session(server.sessionId);
|
|
20
|
+
if (!session)
|
|
21
|
+
throw new NotDeclared(`Session ${server.sessionId} not found`);
|
|
22
|
+
const found = findServer(session.manifest, server.agentId, server.capabilityId, server.serverName);
|
|
23
|
+
if (!found)
|
|
24
|
+
throw new NotDeclared(`MCP server '${server.serverName}' is not declared`);
|
|
25
|
+
if (found.server.type === "stdio")
|
|
26
|
+
throw new NotDeclared(`MCP server '${server.serverName}' is a stdio server, which runs beside the loop`);
|
|
27
|
+
return found.server;
|
|
28
|
+
}
|
|
29
|
+
async function connection(tenantId, server) {
|
|
30
|
+
const key = keyOf(server);
|
|
31
|
+
const existing = live.get(key);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
let pending = opening.get(key);
|
|
35
|
+
if (!pending) {
|
|
36
|
+
pending = (async () => {
|
|
37
|
+
const vault = await vaults.open(tenantId);
|
|
38
|
+
const manifest = await declared(vault, server);
|
|
39
|
+
const connection = await open({
|
|
40
|
+
server: manifest,
|
|
41
|
+
authorize: (url) => vault.authorizeMcp(server.sessionId, { url, serverName: server.serverName }),
|
|
42
|
+
pluginData: "",
|
|
43
|
+
});
|
|
44
|
+
const entry = { connection, lastUsedAt: now(), active: 0 };
|
|
45
|
+
live.set(key, entry);
|
|
46
|
+
return entry;
|
|
47
|
+
})().finally(() => opening.delete(key));
|
|
48
|
+
opening.set(key, pending);
|
|
49
|
+
}
|
|
50
|
+
return pending;
|
|
51
|
+
}
|
|
52
|
+
async function answer(what, server, run, extra = {}) {
|
|
53
|
+
const started = now();
|
|
54
|
+
try {
|
|
55
|
+
const result = await run();
|
|
56
|
+
logger.info("mcp_request", { what, ...fields(server), ...extra, ms: now() - started, outcome: "ok" });
|
|
57
|
+
return { ok: true, result };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const failed = errorOf(error);
|
|
61
|
+
logger.info("mcp_request", {
|
|
62
|
+
what,
|
|
63
|
+
...fields(server),
|
|
64
|
+
...extra,
|
|
65
|
+
ms: now() - started,
|
|
66
|
+
outcome: failed.code === undefined ? "failed" : String(failed.code),
|
|
67
|
+
});
|
|
68
|
+
return { ok: false, error: failed };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function use(tenantId, server, run) {
|
|
72
|
+
const entry = await connection(tenantId, server);
|
|
73
|
+
entry.active += 1;
|
|
74
|
+
try {
|
|
75
|
+
return await run(entry);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
entry.active -= 1;
|
|
79
|
+
entry.lastUsedAt = now();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
connect: (tenantId, server) => answer("connect", server, async () => {
|
|
84
|
+
await connection(tenantId, server);
|
|
85
|
+
return null;
|
|
86
|
+
}),
|
|
87
|
+
list: (tenantId, server, cursor, signal) => answer("list", server, () => use(tenantId, server, (entry) => entry.connection.client.listTools(cursor ? { cursor } : undefined, { signal }))),
|
|
88
|
+
call: (tenantId, request, signal) => answer("call", request.server, () => use(tenantId, request.server, (entry) => entry.connection.client.callTool({ name: request.name, arguments: request.arguments }, { signal })), { effect: request.effectId }),
|
|
89
|
+
async close(server) {
|
|
90
|
+
const key = keyOf(server);
|
|
91
|
+
const entry = live.get(key);
|
|
92
|
+
if (!entry)
|
|
93
|
+
return;
|
|
94
|
+
live.delete(key);
|
|
95
|
+
await entry.connection.close().catch(() => { });
|
|
96
|
+
},
|
|
97
|
+
async sweep() {
|
|
98
|
+
const cutoff = now() - idleMs;
|
|
99
|
+
const idle = [];
|
|
100
|
+
for (const [key, entry] of live) {
|
|
101
|
+
if (entry.active > 0 || entry.lastUsedAt > cutoff)
|
|
102
|
+
continue;
|
|
103
|
+
live.delete(key);
|
|
104
|
+
idle.push(entry);
|
|
105
|
+
}
|
|
106
|
+
await Promise.all(idle.map((entry) => entry.connection.close().catch(() => { })));
|
|
107
|
+
},
|
|
108
|
+
async closeAll() {
|
|
109
|
+
const all = [...live.values()];
|
|
110
|
+
live.clear();
|
|
111
|
+
await Promise.all(all.map((entry) => entry.connection.close().catch(() => { })));
|
|
112
|
+
},
|
|
113
|
+
get size() {
|
|
114
|
+
return live.size;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function fields(server) {
|
|
119
|
+
return {
|
|
120
|
+
session: server.sessionId,
|
|
121
|
+
capability: server.capabilityId,
|
|
122
|
+
server: server.serverName,
|
|
123
|
+
...(server.agentId === undefined ? {} : { agent: server.agentId }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** An error as the loop's `diagnosticFromError` reads it. */
|
|
127
|
+
function errorOf(error) {
|
|
128
|
+
if (error instanceof GateRefusal)
|
|
129
|
+
return { message: error.outcome.message };
|
|
130
|
+
const message = error instanceof Error ? error.message.slice(0, 2000) : String(error);
|
|
131
|
+
const code = error && typeof error === "object" && "code" in error && typeof error.code === "number"
|
|
132
|
+
? error.code
|
|
133
|
+
: undefined;
|
|
134
|
+
const credentialIds = error && typeof error === "object" && "credentialIds" in error
|
|
135
|
+
? error.credentialIds
|
|
136
|
+
: undefined;
|
|
137
|
+
return {
|
|
138
|
+
message,
|
|
139
|
+
...(code === undefined ? {} : { code }),
|
|
140
|
+
...(credentialIds === undefined ? {} : { credentialIds: [...credentialIds] }),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModelBudgetRow, ModelUsageRow, ModelUsageTotals, SessionStore } from "../store/types.js";
|
|
2
|
+
import type { Logger } from "../tenant/types.js";
|
|
3
|
+
import type { ModelGateOutcome, ModelGateRequest } from "./model-gate.js";
|
|
4
|
+
export interface Meter {
|
|
5
|
+
/**
|
|
6
|
+
* Runs `call` for `request` unless a budget's cap is reached, then records its usage in
|
|
7
|
+
* `store`. A reached cap answers a `budget_exhausted` failure without calling.
|
|
8
|
+
*/
|
|
9
|
+
call(store: SessionStore, request: ModelGateRequest, call: () => Promise<ModelGateOutcome>): Promise<ModelGateOutcome>;
|
|
10
|
+
}
|
|
11
|
+
export interface MeterOptions {
|
|
12
|
+
readonly logger: Logger;
|
|
13
|
+
readonly now?: () => Date;
|
|
14
|
+
}
|
|
15
|
+
export declare function createMeter(options: MeterOptions): Meter;
|
|
16
|
+
/** The ledger row of an outcome that carries usage; undefined for a failure. */
|
|
17
|
+
export declare function usageRow(request: ModelGateRequest, outcome: ModelGateOutcome, at: Date): Omit<ModelUsageRow, "duplicate"> | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Why the budget refuses another call, or undefined. Calls in flight count at the scope's
|
|
20
|
+
* average cost per recorded call.
|
|
21
|
+
*/
|
|
22
|
+
export declare function capReached(budget: Pick<ModelBudgetRow, "scope" | "scopeId" | "period" | "limitUsd" | "limitTokens">, spent: ModelUsageTotals, inflight: number): string | undefined;
|
|
23
|
+
/** When the UTC `period` containing `now` started, as an ISO time. */
|
|
24
|
+
export declare function periodStart(period: "day" | "month", now: Date): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meter (blueprint §15, P1.3). Every model call a gate serves is recorded once in the
|
|
3
|
+
* Tenant's usage ledger (`model_usage`), by the gate itself: the gates service for the stack,
|
|
4
|
+
* the in-process gate otherwise. A failed ledger write is logged and never fails the call.
|
|
5
|
+
*
|
|
6
|
+
* Before the call the meter checks the Tenant's budgets (hard caps). There is no reservation
|
|
7
|
+
* (M7): a cap is reached when the scope's recorded spend, plus this gate's calls in flight in
|
|
8
|
+
* the scope at the scope's average cost per call, reaches its limit. Calls in a turn run one
|
|
9
|
+
* at a time, so a scope overspends by at most one call per concurrent call. The refusal is a
|
|
10
|
+
* `budget_exhausted` failure, which the turn does not retry.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { failure } from "../model/classify.js";
|
|
14
|
+
export function createMeter(options) {
|
|
15
|
+
const now = options.now ?? (() => new Date());
|
|
16
|
+
/** Calls running through this gate, per Tenant and budget scope. */
|
|
17
|
+
const inflight = new Map();
|
|
18
|
+
async function check(store, request) {
|
|
19
|
+
const at = now();
|
|
20
|
+
try {
|
|
21
|
+
return await store.tx(async (t) => {
|
|
22
|
+
const keys = [];
|
|
23
|
+
for (const budget of await t.listModelBudgets()) {
|
|
24
|
+
const query = scopeOf(budget, request, at);
|
|
25
|
+
if (!query)
|
|
26
|
+
continue;
|
|
27
|
+
const key = `${request.tenantId}|${query.scope}|${query.id ?? "*"}|${query.since ?? ""}`;
|
|
28
|
+
keys.push(key);
|
|
29
|
+
const reached = capReached(budget, await t.modelUsageTotals(query), inflight.get(key) ?? 0);
|
|
30
|
+
if (reached) {
|
|
31
|
+
options.logger.warn("model_budget_exhausted", {
|
|
32
|
+
tenant: request.tenantId,
|
|
33
|
+
session: request.sessionId,
|
|
34
|
+
effect: request.effectId,
|
|
35
|
+
scope: budget.scope,
|
|
36
|
+
...(budget.scope === "agent" ? { agent: budget.scopeId } : {}),
|
|
37
|
+
});
|
|
38
|
+
return { keys, refusal: failure("budget_exhausted", reached, false) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { keys };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// The caps can't be read: the call goes ahead, as it would without budgets.
|
|
46
|
+
options.logger.warn("model_budget_check_failed", {
|
|
47
|
+
tenant: request.tenantId,
|
|
48
|
+
effect: request.effectId,
|
|
49
|
+
message: error instanceof Error ? error.message : String(error),
|
|
50
|
+
});
|
|
51
|
+
return { keys: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
async call(store, request, call) {
|
|
56
|
+
const { keys, refusal } = await check(store, request);
|
|
57
|
+
if (refusal)
|
|
58
|
+
return refusal;
|
|
59
|
+
// A call counts as in flight until its usage is recorded, so the check never misses it.
|
|
60
|
+
for (const key of keys)
|
|
61
|
+
inflight.set(key, (inflight.get(key) ?? 0) + 1);
|
|
62
|
+
try {
|
|
63
|
+
const outcome = await call();
|
|
64
|
+
await record(store, request, outcome);
|
|
65
|
+
return outcome;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const left = (inflight.get(key) ?? 1) - 1;
|
|
70
|
+
if (left > 0)
|
|
71
|
+
inflight.set(key, left);
|
|
72
|
+
else
|
|
73
|
+
inflight.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
async function record(store, request, outcome) {
|
|
79
|
+
const row = usageRow(request, outcome, now());
|
|
80
|
+
if (!row)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const recorded = await store.tx((t) => t.recordModelUsage(row));
|
|
84
|
+
if (recorded.duplicate)
|
|
85
|
+
options.logger.warn("model_usage_duplicate", {
|
|
86
|
+
tenant: request.tenantId,
|
|
87
|
+
effect: request.effectId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
options.logger.warn("model_usage_failed", {
|
|
92
|
+
tenant: request.tenantId,
|
|
93
|
+
effect: request.effectId,
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The ledger row of an outcome that carries usage; undefined for a failure. */
|
|
100
|
+
export function usageRow(request, outcome, at) {
|
|
101
|
+
if (typeof outcome !== "object" || "kind" in outcome || !outcome.usage)
|
|
102
|
+
return undefined;
|
|
103
|
+
const usage = outcome.usage;
|
|
104
|
+
const producer = producerOf(outcome);
|
|
105
|
+
const inputTokens = count(usage.inputTokens);
|
|
106
|
+
const outputTokens = count(usage.outputTokens);
|
|
107
|
+
return {
|
|
108
|
+
id: randomUUID(),
|
|
109
|
+
effectKey: request.effectId,
|
|
110
|
+
sessionId: request.sessionId,
|
|
111
|
+
turnId: request.turnId,
|
|
112
|
+
agentId: request.agentId,
|
|
113
|
+
provider: producer.provider ?? null,
|
|
114
|
+
model: producer.model ?? outcome.evidence?.resolvedModel ?? request.call.model?.id ?? null,
|
|
115
|
+
inputTokens,
|
|
116
|
+
outputTokens,
|
|
117
|
+
totalTokens: count(usage.totalTokens) || inputTokens + outputTokens,
|
|
118
|
+
cachedTokens: count(usage.cachedTokens),
|
|
119
|
+
cacheWriteTokens: count(usage.cacheWriteTokens),
|
|
120
|
+
reasoningTokens: count(usage.reasoningTokens),
|
|
121
|
+
costUsd: Number.isFinite(usage.costUsd) && usage.costUsd > 0 ? usage.costUsd : 0,
|
|
122
|
+
createdAt: at.toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** The ledger rows a budget counts for `request`; undefined when it doesn't apply. */
|
|
126
|
+
function scopeOf(budget, request, at) {
|
|
127
|
+
const since = budget.period ? { since: periodStart(budget.period, at) } : {};
|
|
128
|
+
if (budget.scope === "turn")
|
|
129
|
+
return { scope: "turn", id: request.turnId };
|
|
130
|
+
if (budget.scope === "agent")
|
|
131
|
+
return budget.scopeId === request.agentId ? { scope: "agent", id: request.agentId, ...since } : undefined;
|
|
132
|
+
return { scope: "tenant", ...since };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Why the budget refuses another call, or undefined. Calls in flight count at the scope's
|
|
136
|
+
* average cost per recorded call.
|
|
137
|
+
*/
|
|
138
|
+
export function capReached(budget, spent, inflight) {
|
|
139
|
+
const projected = (total) => total + (spent.calls > 0 ? (inflight * total) / spent.calls : 0);
|
|
140
|
+
const name = budget.scope === "turn"
|
|
141
|
+
? "The turn's cap"
|
|
142
|
+
: `${budget.scope === "agent" ? `Agent ${budget.scopeId}'s` : "The Tenant's"} ${budget.period === "day" ? "daily" : "monthly"} cap`;
|
|
143
|
+
const raise = "raise it with PUT /v1/tenant/budgets";
|
|
144
|
+
if (budget.limitTokens !== null && projected(spent.tokens) >= budget.limitTokens)
|
|
145
|
+
return `${name} of ${budget.limitTokens} tokens is reached (${spent.tokens} used); ${raise}`;
|
|
146
|
+
if (budget.limitUsd !== null && projected(spent.costUsd) >= budget.limitUsd)
|
|
147
|
+
return `${name} of $${budget.limitUsd} is reached ($${spent.costUsd.toFixed(4)} spent); ${raise}`;
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/** When the UTC `period` containing `now` started, as an ISO time. */
|
|
151
|
+
export function periodStart(period, now) {
|
|
152
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), period === "day" ? now.getUTCDate() : 1)).toISOString();
|
|
153
|
+
}
|
|
154
|
+
const count = (value) => Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
|
|
155
|
+
/** The provider and model `piModel` records as the candidate's producer. */
|
|
156
|
+
function producerOf(candidate) {
|
|
157
|
+
const producer = candidate.evidence?.extras?.producer;
|
|
158
|
+
if (typeof producer !== "object" || producer === null || Array.isArray(producer))
|
|
159
|
+
return {};
|
|
160
|
+
const { provider, model } = producer;
|
|
161
|
+
return {
|
|
162
|
+
...(typeof provider === "string" ? { provider } : {}),
|
|
163
|
+
...(typeof model === "string" ? { model } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model Gate seam (blueprint §15, P1.1). Every vault-backed model call of the loop goes
|
|
3
|
+
* through one `ModelGate`: the call, who it is for, and nothing else. The gate owns what the
|
|
4
|
+
* call needs that the loop must not hold: the provider credential, pi-ai, its retries and
|
|
5
|
+
* idle watchdog, failure classification and scrubbing.
|
|
6
|
+
*
|
|
7
|
+
* Two implementations: `inProcessModelGate` (embedding, the ephemeral Runtime, tests) and the
|
|
8
|
+
* HTTP client of the gates service (the `gateway` container), which keeps the credential out
|
|
9
|
+
* of the loop process. Both return what `piModel` returns, so the effect journal is the same
|
|
10
|
+
* either way.
|
|
11
|
+
*/
|
|
12
|
+
import type { RuntimeModelAdapter, RuntimeModelCall } from "../contracts.js";
|
|
13
|
+
export type { ModelCallSettings } from "../model/pi-model.js";
|
|
14
|
+
/** One model call, as the loop hands it to the gate. JSON-safe: it crosses the network. */
|
|
15
|
+
export interface ModelGateRequest {
|
|
16
|
+
readonly tenantId: string;
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly turnId: string;
|
|
19
|
+
/** The session's agent, for the usage ledger and its budgets (P1.3). */
|
|
20
|
+
readonly agentId: string;
|
|
21
|
+
/** The effect's journal id; sent as `Idempotency-Key` (acted on from P1.2). */
|
|
22
|
+
readonly effectId: string;
|
|
23
|
+
readonly invocationId: string;
|
|
24
|
+
readonly call: RuntimeModelCall;
|
|
25
|
+
}
|
|
26
|
+
/** A model candidate or a failure outcome: exactly what `piModel` returns. */
|
|
27
|
+
export type ModelGateOutcome = Awaited<ReturnType<RuntimeModelAdapter>>;
|
|
28
|
+
export interface ModelGate {
|
|
29
|
+
/**
|
|
30
|
+
* Calls the model. A provider or gate failure comes back as a failure outcome; only an abort
|
|
31
|
+
* of `signal` throws, so the advance decides what the abort means.
|
|
32
|
+
*/
|
|
33
|
+
call(request: ModelGateRequest, signal: AbortSignal): Promise<ModelGateOutcome>;
|
|
34
|
+
/**
|
|
35
|
+
* True when a call outlives the caller's process (the gates service, P1.2): after a takeover
|
|
36
|
+
* or a shutdown, re-sending the journaled request joins the call or returns its outcome
|
|
37
|
+
* instead of calling the provider again. The in-process gate dies with the loop.
|
|
38
|
+
*/
|
|
39
|
+
readonly recovers?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Stops a call that outlives its caller (a user cancel). Best-effort: never rejects. Only a
|
|
42
|
+
* gate that `recovers` needs it; for the others aborting `signal` already stops the call.
|
|
43
|
+
*/
|
|
44
|
+
cancel?(request: Pick<ModelGateRequest, "tenantId" | "effectId">): Promise<void>;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the gates service opens of the Tenant: its vault's host model, and nothing else. The
|
|
3
|
+
* full Tenant (`TenantRuntime.open`) migrates its database, creates its key file and starts
|
|
4
|
+
* its services; the gate does none of that. The database holds one Tenant (tenancy.md §1):
|
|
5
|
+
* the gate reads which one from it, refuses a database whose Tenant schema isn't at this
|
|
6
|
+
* build's version (the Runtime migrates it when it opens the Tenant) and never creates a vault
|
|
7
|
+
* key. The vault is opened once and kept.
|
|
8
|
+
*/
|
|
9
|
+
import type { Credential } from "@earendil-works/pi-ai";
|
|
10
|
+
import type { ModelFailureOutcome } from "@nylorun/core/define";
|
|
11
|
+
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
12
|
+
import { VaultService, type AuthorizeResult, type HostModelSecret } from "../vault/service.js";
|
|
13
|
+
import type { SessionStore } from "../store/types.js";
|
|
14
|
+
import type { Session } from "../tenant/context.js";
|
|
15
|
+
import { type Keys } from "../keys/keys.js";
|
|
16
|
+
/** The Tenant, as a model call needs it. */
|
|
17
|
+
export interface TenantVault {
|
|
18
|
+
readonly tenantId: string;
|
|
19
|
+
/** The Tenant's store, for the usage ledger (P1.3). */
|
|
20
|
+
readonly store: SessionStore;
|
|
21
|
+
/** The Tenant's home: secrets found there are redacted from failure messages. */
|
|
22
|
+
readonly root: string;
|
|
23
|
+
readHostModel(): Promise<HostModelSecret | undefined>;
|
|
24
|
+
/** Writes back a credential pi-ai refreshed (OAuth). */
|
|
25
|
+
writeHostCredential(credential: Credential): Promise<void>;
|
|
26
|
+
/** A session, for the remote MCP servers its pinned manifest declares (F4.1). */
|
|
27
|
+
session(sessionId: string): Promise<Session | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* The vault authorization of one request to a session's remote MCP server: its credential
|
|
30
|
+
* from the session's attached vaults, refreshed when due (F4.1).
|
|
31
|
+
*/
|
|
32
|
+
authorizeMcp(sessionId: string, request: {
|
|
33
|
+
url: string;
|
|
34
|
+
serverName: string;
|
|
35
|
+
}): Promise<AuthorizeResult>;
|
|
36
|
+
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
37
|
+
keys(): Keys;
|
|
38
|
+
}
|
|
39
|
+
export interface TenantVaults {
|
|
40
|
+
/**
|
|
41
|
+
* Opens the Tenant's vault, or rejects with a `GateRefusal`. `tenantId`, when the caller
|
|
42
|
+
* names one, must be the database's Tenant.
|
|
43
|
+
*/
|
|
44
|
+
open(tenantId?: string): Promise<TenantVault>;
|
|
45
|
+
}
|
|
46
|
+
/** A call the gate refuses before the provider is called; it answers with `outcome`. */
|
|
47
|
+
export declare class GateRefusal extends Error {
|
|
48
|
+
readonly outcome: ModelFailureOutcome;
|
|
49
|
+
constructor(outcome: ModelFailureOutcome);
|
|
50
|
+
}
|
|
51
|
+
export interface TenantVaultsOptions {
|
|
52
|
+
readonly sql: PostgresClient;
|
|
53
|
+
/** The Host root; the gate reads `keys/vault-kek` and `tenant/home` under it. */
|
|
54
|
+
readonly hostRoot: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function createTenantVaults(options: TenantVaultsOptions): TenantVaults;
|
|
57
|
+
/**
|
|
58
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults:
|
|
59
|
+
* what the loop did in its own process before F4.1 (`tenant/effects.ts` `authorize`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function authorizeSessionMcp(credentials: VaultService, session: (sessionId: string) => Promise<Session | undefined>, sessionId: string, request: {
|
|
62
|
+
url: string;
|
|
63
|
+
serverName: string;
|
|
64
|
+
}): Promise<AuthorizeResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
2
|
+
import { failure } from "../model/classify.js";
|
|
3
|
+
import { database } from "../store/postgres/db.js";
|
|
4
|
+
import { expectedSchemaVersion, readSchemaVersion } from "../store/postgres/migrate.js";
|
|
5
|
+
import { readTenantEnvelope } from "../store/postgres/tenant.js";
|
|
6
|
+
import { createPostgresSessionStore } from "../store/postgres/store.js";
|
|
7
|
+
import { tenantPaths } from "../tenant/paths.js";
|
|
8
|
+
import { readVaultKek } from "../vault/kek.js";
|
|
9
|
+
import { HostModelVault } from "../vault/host-model.js";
|
|
10
|
+
import { VaultService } from "../vault/service.js";
|
|
11
|
+
import { inProcessKeys } from "../keys/keys.js";
|
|
12
|
+
import { SigningKeys } from "../tenant/signing-keys.js";
|
|
13
|
+
/** A call the gate refuses before the provider is called; it answers with `outcome`. */
|
|
14
|
+
export class GateRefusal extends Error {
|
|
15
|
+
outcome;
|
|
16
|
+
constructor(outcome) {
|
|
17
|
+
super(outcome.message);
|
|
18
|
+
this.outcome = outcome;
|
|
19
|
+
this.name = "GateRefusal";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createTenantVaults(options) {
|
|
23
|
+
const { sql, hostRoot } = options;
|
|
24
|
+
let opened;
|
|
25
|
+
function build(tenantId) {
|
|
26
|
+
const paths = tenantPaths(hostRoot);
|
|
27
|
+
const store = createPostgresSessionStore({ sql, tenantId });
|
|
28
|
+
let kek;
|
|
29
|
+
const readKek = () => {
|
|
30
|
+
kek ??= readVaultKek({ vaultKekPath: paths.kek });
|
|
31
|
+
if (!kek)
|
|
32
|
+
throw new GateRefusal(failure("auth", "The Tenant's vault key is missing on the gateway container; check that it mounts the Host's keys directory, which `nylorun start` creates", false));
|
|
33
|
+
return kek;
|
|
34
|
+
};
|
|
35
|
+
const vault = new HostModelVault({ store, kek: readKek });
|
|
36
|
+
const credentials = new VaultService({ store, kek: readKek, fetch: globalThis.fetch });
|
|
37
|
+
const keys = inProcessKeys({
|
|
38
|
+
store,
|
|
39
|
+
vault: credentials,
|
|
40
|
+
signingKeys: new SigningKeys({ tenantId, kek: readKek }),
|
|
41
|
+
kek: readKek,
|
|
42
|
+
});
|
|
43
|
+
const session = (sessionId) => store.tx((t) => t.get("sessions", sessionId));
|
|
44
|
+
return {
|
|
45
|
+
tenantId,
|
|
46
|
+
store,
|
|
47
|
+
root: paths.home,
|
|
48
|
+
readHostModel: () => vault.readHostModel(),
|
|
49
|
+
writeHostCredential: (credential) => vault.updateHostCredential(credential),
|
|
50
|
+
session,
|
|
51
|
+
authorizeMcp: (sessionId, request) => authorizeSessionMcp(credentials, session, sessionId, request),
|
|
52
|
+
keys: () => keys,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The database's Tenant, once its schema is at this build's version. */
|
|
56
|
+
async function load() {
|
|
57
|
+
const version = await readSchemaVersion(database(sql));
|
|
58
|
+
const expected = expectedSchemaVersion();
|
|
59
|
+
if (version === undefined || version === 0)
|
|
60
|
+
throw new GateRefusal(failure("transient", "The Tenant's database is not migrated yet; the Runtime does that when it starts", true));
|
|
61
|
+
if (version !== expected)
|
|
62
|
+
throw new GateRefusal(failure("transient", `The Tenant is at schema version ${version}; the gateway serves version ${expected}`, version < expected));
|
|
63
|
+
const envelope = await readTenantEnvelope(sql).catch(() => undefined);
|
|
64
|
+
if (!envelope)
|
|
65
|
+
throw new GateRefusal(failure("transient", "The database holds no Tenant yet; the Runtime creates it when it starts", true));
|
|
66
|
+
return build(envelope.id);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
async open(tenantId) {
|
|
70
|
+
if (tenantId !== undefined && !isTenantId(tenantId))
|
|
71
|
+
throw new GateRefusal(failure("invalid_request", "Not a Tenant id", false));
|
|
72
|
+
opened ??= await load();
|
|
73
|
+
if (tenantId !== undefined && tenantId !== opened.tenantId)
|
|
74
|
+
throw new GateRefusal(failure("invalid_request", `Tenant ${tenantId} is not this installation's Tenant`, false));
|
|
75
|
+
return opened;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults:
|
|
81
|
+
* what the loop did in its own process before F4.1 (`tenant/effects.ts` `authorize`).
|
|
82
|
+
*/
|
|
83
|
+
export async function authorizeSessionMcp(credentials, session, sessionId, request) {
|
|
84
|
+
const found = await session(sessionId);
|
|
85
|
+
if (!found)
|
|
86
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
87
|
+
return credentials.authorize({
|
|
88
|
+
sessionId,
|
|
89
|
+
vaultIds: found.vaultIds ?? [],
|
|
90
|
+
credentialSelections: found.credentialSelections ?? [],
|
|
91
|
+
url: request.url,
|
|
92
|
+
serverName: request.serverName,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyed remote MCP calls in the gates service (F4.1 G3). A call sent with an `Idempotency-Key`
|
|
3
|
+
* (the loop's effect id) runs once:
|
|
4
|
+
*
|
|
5
|
+
* - While it runs, and for 30 minutes after, a re-send joins it or gets its answer from memory
|
|
6
|
+
* (`inflight.ts`), as model calls do (P1.2). So a runtime that dies or shuts down mid-call
|
|
7
|
+
* leaves nothing `uncertain`: the next owner re-sends the call and gets its answer.
|
|
8
|
+
* - The gate also writes a `tool_crossings` row before it calls the server, and the answer
|
|
9
|
+
* after. A re-send that finds the answer gets it. One that finds a row without an answer,
|
|
10
|
+
* for a call this gateway is not running, was lost with an earlier gateway (a restart mid
|
|
11
|
+
* call): it answers `uncertain`, because the call may have run, and is never run again.
|
|
12
|
+
*
|
|
13
|
+
* A cancel aborts the call and leaves its row without an answer. Settled rows are deleted a day
|
|
14
|
+
* later (`prune`).
|
|
15
|
+
*/
|
|
16
|
+
import type { McpServerRef } from "../mcp/pool.js";
|
|
17
|
+
import type { Logger } from "../tenant/types.js";
|
|
18
|
+
import { type InflightCallsOptions } from "./inflight.js";
|
|
19
|
+
import type { McpHandler } from "./mcp-handler.js";
|
|
20
|
+
import type { TenantVaults } from "./tenant-vaults.js";
|
|
21
|
+
import type { McpAnswer } from "./tool-contract.js";
|
|
22
|
+
/** How long a settled crossing is kept. */
|
|
23
|
+
export declare const TOOL_CROSSING_TTL_MS: number;
|
|
24
|
+
export type ToolCallAnswer = McpAnswer<Record<string, unknown>>;
|
|
25
|
+
export interface ToolCallRequest {
|
|
26
|
+
readonly server: McpServerRef;
|
|
27
|
+
readonly effectId: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly arguments: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface ToolCalls {
|
|
32
|
+
/**
|
|
33
|
+
* Runs, joins or answers the call under `key`. Rejects with `InflightConflict` when a
|
|
34
|
+
* different request already ran under it.
|
|
35
|
+
*/
|
|
36
|
+
run(tenantId: string | undefined, key: string, hash: string, request: ToolCallRequest): Promise<ToolCallAnswer>;
|
|
37
|
+
/** Aborts the call under `key`; it is never run again under that key. */
|
|
38
|
+
cancel(key: string): void;
|
|
39
|
+
/** Aborts every running call (gateway shutdown). */
|
|
40
|
+
close(): void;
|
|
41
|
+
/** Deletes crossings settled more than `TOOL_CROSSING_TTL_MS` ago. */
|
|
42
|
+
prune(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export interface ToolCallsOptions {
|
|
45
|
+
readonly vaults: TenantVaults;
|
|
46
|
+
readonly mcp: McpHandler;
|
|
47
|
+
readonly logger: Logger;
|
|
48
|
+
readonly inflight?: InflightCallsOptions;
|
|
49
|
+
readonly now?: () => number;
|
|
50
|
+
}
|
|
51
|
+
export declare function createToolCalls(options: ToolCallsOptions): ToolCalls;
|