@nylorun/runtime 0.14.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 +104 -0
- package/README.md +75 -33
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +184 -685
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- 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/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +4 -5
- 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 +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- 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 +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- 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 +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +887 -694
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- 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 +2 -8
- package/dist/store/postgres/record.js +50 -54
- 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 +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- 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/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- 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 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- 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/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
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loop's client of the Tool Gate in the gates service (F4.1, `tool-contract.ts`): remote
|
|
3
|
+
* MCP servers opened and called through the gate, which holds their connections and
|
|
4
|
+
* credentials, and Action deliveries POSTed by the gate. Over `node:http`, like the model gate's
|
|
5
|
+
* client (`http-client.ts`): a call answers only when it has finished.
|
|
6
|
+
*
|
|
7
|
+
* Failures keep the meaning they had in the loop's own process:
|
|
8
|
+
* - An MCP request that fails, at the server or on the hop, throws, so the pool reports a
|
|
9
|
+
* diagnostic and `resolveEffect` marks a call `uncertain`, as before.
|
|
10
|
+
* - A delivery never throws. A hop that failed before the request was sent is `not_sent`
|
|
11
|
+
* (the deliverer sends it again, even a tool); one that failed after is `lost`.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { request as httpRequest } from "node:http";
|
|
15
|
+
import { request as httpsRequest } from "node:https";
|
|
16
|
+
import { TENANT_HEADER } from "./contract.js";
|
|
17
|
+
import { GATE_CLIENT_TIMEOUT_MS } from "./http-client.js";
|
|
18
|
+
import { DELIVERIES_PATH, MCP_CLOSE_PATH, MCP_CONNECT_PATH, MCP_LIST_PATH, TOOL_CALLS_PATH, } from "./tool-contract.js";
|
|
19
|
+
/** How long a close or a cancel may take; neither changes an outcome. */
|
|
20
|
+
const SHORT_TIMEOUT_MS = 2_000;
|
|
21
|
+
/** Extra silence allowed on a delivery beyond the endpoint's own timeout. */
|
|
22
|
+
const DELIVERY_SLACK_MS = 30_000;
|
|
23
|
+
export function httpToolGate(options) {
|
|
24
|
+
const where = new URL(options.url).origin;
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? GATE_CLIENT_TIMEOUT_MS;
|
|
26
|
+
function exchange(path, body, request) {
|
|
27
|
+
const url = new URL(path, options.url);
|
|
28
|
+
const send = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
29
|
+
const payload = body === undefined ? Buffer.alloc(0) : Buffer.from(JSON.stringify(body), "utf8");
|
|
30
|
+
const signal = request.signal;
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return resolve({ kind: "aborted", sent: false, reason: signal.reason });
|
|
34
|
+
let settled = false;
|
|
35
|
+
let sent = false;
|
|
36
|
+
let responded = false;
|
|
37
|
+
let timedOut = false;
|
|
38
|
+
const settle = (result) => {
|
|
39
|
+
if (settled)
|
|
40
|
+
return;
|
|
41
|
+
settled = true;
|
|
42
|
+
signal?.removeEventListener("abort", onAbort);
|
|
43
|
+
resolve(result);
|
|
44
|
+
};
|
|
45
|
+
const onAbort = () => {
|
|
46
|
+
settle({ kind: "aborted", sent, reason: signal.reason });
|
|
47
|
+
outgoing.destroy();
|
|
48
|
+
};
|
|
49
|
+
const failed = (error) => settle(sent
|
|
50
|
+
? {
|
|
51
|
+
kind: "lost",
|
|
52
|
+
message: timedOut
|
|
53
|
+
? `Tool gate at ${where} sent nothing for ${Math.round(request.timeoutMs / 1000)} s`
|
|
54
|
+
: `Tool gate connection lost${error ? ` (${error.message})` : ""}`,
|
|
55
|
+
}
|
|
56
|
+
: { kind: "unreachable", message: `Tool gate unreachable at ${where}${error ? ` (${error.message})` : ""}` });
|
|
57
|
+
const outgoing = send(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
authorization: `Bearer ${options.token}`,
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
"content-length": payload.byteLength,
|
|
63
|
+
...(options.tenantId ? { [TENANT_HEADER]: options.tenantId } : {}),
|
|
64
|
+
...(request.idempotencyKey ? { "idempotency-key": request.idempotencyKey } : {}),
|
|
65
|
+
},
|
|
66
|
+
timeout: request.timeoutMs,
|
|
67
|
+
}, (response) => {
|
|
68
|
+
responded = true;
|
|
69
|
+
readBody(response).then((text) => settle({ kind: "answer", status: response.statusCode ?? 0, text }), (error) => failed(error));
|
|
70
|
+
});
|
|
71
|
+
outgoing.on("socket", (socket) => socket.setKeepAlive(true, 30_000));
|
|
72
|
+
outgoing.on("finish", () => {
|
|
73
|
+
sent = true;
|
|
74
|
+
});
|
|
75
|
+
outgoing.on("timeout", () => {
|
|
76
|
+
timedOut = true;
|
|
77
|
+
outgoing.destroy();
|
|
78
|
+
});
|
|
79
|
+
outgoing.on("error", (error) => {
|
|
80
|
+
if (!signal?.aborted)
|
|
81
|
+
failed(error);
|
|
82
|
+
});
|
|
83
|
+
outgoing.on("close", () => {
|
|
84
|
+
if (!signal?.aborted && !responded)
|
|
85
|
+
failed();
|
|
86
|
+
});
|
|
87
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
outgoing.end(payload);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** The result of an MCP request, or a throw the pool and `resolveEffect` read as before. */
|
|
92
|
+
async function mcp(path, body, request) {
|
|
93
|
+
const result = await exchange(path, body, { ...request, timeoutMs });
|
|
94
|
+
if (result.kind === "aborted")
|
|
95
|
+
throw result.reason ?? new Error("aborted");
|
|
96
|
+
if (result.kind !== "answer")
|
|
97
|
+
throw new Error(result.message);
|
|
98
|
+
const parsed = parse(result.text);
|
|
99
|
+
if (result.status !== 200 || !parsed || !("ok" in parsed))
|
|
100
|
+
throw new Error(gateRefusal(result.status, parsed));
|
|
101
|
+
if (parsed.ok)
|
|
102
|
+
return parsed.result;
|
|
103
|
+
throw mcpError(parsed.error);
|
|
104
|
+
}
|
|
105
|
+
async function fireAndForget(path, body) {
|
|
106
|
+
await exchange(path, body, { timeoutMs: SHORT_TIMEOUT_MS });
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
recovers: true,
|
|
110
|
+
async openMcp(server) {
|
|
111
|
+
await mcp(MCP_CONNECT_PATH, { server }, {});
|
|
112
|
+
const client = {
|
|
113
|
+
listTools: (params, request) => mcp(MCP_LIST_PATH, { server, ...(params?.cursor ? { cursor: params.cursor } : {}) }, request?.signal ? { signal: request.signal } : {}),
|
|
114
|
+
callTool: (params, request) => {
|
|
115
|
+
const body = {
|
|
116
|
+
server: { ...server },
|
|
117
|
+
effectId: request?.key ?? randomUUID(),
|
|
118
|
+
name: params.name,
|
|
119
|
+
arguments: params.arguments,
|
|
120
|
+
};
|
|
121
|
+
return mcp(TOOL_CALLS_PATH, body, {
|
|
122
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
123
|
+
...(request?.key ? { idempotencyKey: request.key } : {}),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return { client, close: () => fireAndForget(MCP_CLOSE_PATH, { server }) };
|
|
128
|
+
},
|
|
129
|
+
async cancel(request) {
|
|
130
|
+
await fireAndForget(`${TOOL_CALLS_PATH}/${encodeURIComponent(request.effectId)}/cancel`, undefined);
|
|
131
|
+
},
|
|
132
|
+
async post(delivery, signal) {
|
|
133
|
+
const result = await exchange(DELIVERIES_PATH, {
|
|
134
|
+
url: delivery.url,
|
|
135
|
+
body: delivery.body,
|
|
136
|
+
headers: { ...delivery.headers },
|
|
137
|
+
timeoutMs: delivery.timeoutMs,
|
|
138
|
+
}, { signal, timeoutMs: delivery.timeoutMs + DELIVERY_SLACK_MS });
|
|
139
|
+
if (result.kind === "aborted")
|
|
140
|
+
return {
|
|
141
|
+
kind: result.sent ? "lost" : "not_sent",
|
|
142
|
+
code: "ABORTED",
|
|
143
|
+
message: "The delivery was aborted",
|
|
144
|
+
};
|
|
145
|
+
if (result.kind === "unreachable")
|
|
146
|
+
return { kind: "not_sent", code: "gateway.unreachable", message: result.message };
|
|
147
|
+
if (result.kind === "lost")
|
|
148
|
+
return { kind: "lost", code: "gateway.lost", message: result.message };
|
|
149
|
+
const parsed = parse(result.text);
|
|
150
|
+
if (result.status !== 200 || !parsed?.result)
|
|
151
|
+
// The gate refused the request before sending anything.
|
|
152
|
+
return { kind: "not_sent", code: "gateway.refused", message: gateRefusal(result.status, parsed) };
|
|
153
|
+
const wire = parsed.result;
|
|
154
|
+
if (wire.kind !== "response")
|
|
155
|
+
return wire;
|
|
156
|
+
return {
|
|
157
|
+
kind: "response",
|
|
158
|
+
status: wire.status,
|
|
159
|
+
headers: wire.headers,
|
|
160
|
+
body: Buffer.from(wire.body, "base64"),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** An MCP failure the gate reported, as the loop's own MCP code would have thrown it. */
|
|
166
|
+
function mcpError(error) {
|
|
167
|
+
const thrown = new Error(error.uncertain ? `${error.message} (the call may have run; it is not sent again)` : error.message);
|
|
168
|
+
if (error.code !== undefined)
|
|
169
|
+
thrown.code = error.code;
|
|
170
|
+
if (error.credentialIds !== undefined)
|
|
171
|
+
thrown.credentialIds = error.credentialIds;
|
|
172
|
+
return thrown;
|
|
173
|
+
}
|
|
174
|
+
function gateRefusal(status, parsed) {
|
|
175
|
+
const message = parsed?.error?.message;
|
|
176
|
+
if (status === 401)
|
|
177
|
+
return "The tool gate refused this Runtime's token: the runtime and gateway containers must share NYLORUN_GATES_TOKEN";
|
|
178
|
+
return `Tool gate answered ${status}${typeof message === "string" ? `: ${message}` : ""}`;
|
|
179
|
+
}
|
|
180
|
+
function parse(text) {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(text);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function readBody(response) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const chunks = [];
|
|
191
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
192
|
+
response.on("end", () => {
|
|
193
|
+
if (response.complete)
|
|
194
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
195
|
+
else
|
|
196
|
+
reject(new Error("response ended early"));
|
|
197
|
+
});
|
|
198
|
+
response.on("error", reject);
|
|
199
|
+
response.on("aborted", () => reject(new Error("response aborted")));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
|
|
3
|
+
* service for remote MCP servers and Action deliveries. Internal, like the model call route
|
|
4
|
+
* (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
|
|
5
|
+
* documents.
|
|
6
|
+
*
|
|
7
|
+
* Remote MCP. The gate holds the connection and the vault credential; the loop names the
|
|
8
|
+
* server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
|
|
9
|
+
* - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
|
|
10
|
+
* authorization included), or keeps the open one.
|
|
11
|
+
* - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
|
|
12
|
+
* - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
|
|
13
|
+
* `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
|
|
14
|
+
* once (G3); a re-send joins it or gets its outcome.
|
|
15
|
+
* - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
|
|
16
|
+
* - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
|
|
17
|
+
* Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
|
|
18
|
+
* answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
|
|
19
|
+
* `{ok: false, error: {uncertain: true}}` and is never run again.
|
|
20
|
+
*
|
|
21
|
+
* Action deliveries. `POST /nylorun/v1/deliveries` `{url, body, headers, timeoutMs}`: the gate
|
|
22
|
+
* POSTs the signed delivery under the gateway's own address policy and answers `200 {result}`,
|
|
23
|
+
* what `tenant/outbound.ts` `post` returned, with the answer's body in base64.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
export declare const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
|
|
27
|
+
export declare const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
|
|
28
|
+
export declare const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
|
|
29
|
+
export declare const TOOL_CALLS_PATH = "/nylorun/v1/tool-calls";
|
|
30
|
+
export declare const DELIVERIES_PATH = "/nylorun/v1/deliveries";
|
|
31
|
+
/** Largest tool call or delivery body the gate reads. */
|
|
32
|
+
export declare const MAX_TOOL_BODY_BYTES: number;
|
|
33
|
+
/** The longest delivery timeout the gate accepts: an endpoint's maximum `timeoutMs`. */
|
|
34
|
+
export declare const MAX_DELIVERY_TIMEOUT_MS: number;
|
|
35
|
+
/** The only headers the gate forwards on a delivery, lower-cased. */
|
|
36
|
+
export declare const DELIVERY_HEADERS: ReadonlySet<string>;
|
|
37
|
+
export declare const McpServerRefSchema: z.ZodObject<{
|
|
38
|
+
sessionId: z.ZodString;
|
|
39
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
40
|
+
capabilityId: z.ZodString;
|
|
41
|
+
serverName: z.ZodString;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
export declare const McpServerBodySchema: z.ZodObject<{
|
|
44
|
+
server: z.ZodObject<{
|
|
45
|
+
sessionId: z.ZodString;
|
|
46
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
47
|
+
capabilityId: z.ZodString;
|
|
48
|
+
serverName: z.ZodString;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
export declare const McpListBodySchema: z.ZodObject<{
|
|
52
|
+
server: z.ZodObject<{
|
|
53
|
+
sessionId: z.ZodString;
|
|
54
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
55
|
+
capabilityId: z.ZodString;
|
|
56
|
+
serverName: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export declare const ToolCallBodySchema: z.ZodObject<{
|
|
61
|
+
server: z.ZodObject<{
|
|
62
|
+
sessionId: z.ZodString;
|
|
63
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
64
|
+
capabilityId: z.ZodString;
|
|
65
|
+
serverName: z.ZodString;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
effectId: z.ZodString;
|
|
68
|
+
name: z.ZodString;
|
|
69
|
+
arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
export type ToolCallBody = z.infer<typeof ToolCallBodySchema>;
|
|
72
|
+
/** Why an MCP request failed, as the loop's diagnostics read it (`mcp/connect.ts`). */
|
|
73
|
+
export interface McpGateError {
|
|
74
|
+
readonly message: string;
|
|
75
|
+
/** The MCP SDK's error code, an HTTP status for transport errors (401 for a refused token). */
|
|
76
|
+
readonly code?: number;
|
|
77
|
+
/** The vault credentials a refused authorization names. */
|
|
78
|
+
readonly credentialIds?: readonly string[];
|
|
79
|
+
/** The call may have run: its earlier attempt was lost with the gateway. Never re-run. */
|
|
80
|
+
readonly uncertain?: boolean;
|
|
81
|
+
}
|
|
82
|
+
export type McpAnswer<T> = {
|
|
83
|
+
readonly ok: true;
|
|
84
|
+
readonly result: T;
|
|
85
|
+
} | {
|
|
86
|
+
readonly ok: false;
|
|
87
|
+
readonly error: McpGateError;
|
|
88
|
+
};
|
|
89
|
+
export declare const DeliveryBodySchema: z.ZodObject<{
|
|
90
|
+
url: z.ZodString;
|
|
91
|
+
body: z.ZodString;
|
|
92
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
93
|
+
timeoutMs: z.ZodNumber;
|
|
94
|
+
}, z.core.$strip>;
|
|
95
|
+
export type DeliveryBody = z.infer<typeof DeliveryBodySchema>;
|
|
96
|
+
/** `OutboundResult` on the wire: the answer's body in base64. */
|
|
97
|
+
export type WireOutboundResult = {
|
|
98
|
+
readonly kind: "response";
|
|
99
|
+
readonly status: number;
|
|
100
|
+
readonly headers: Record<string, string | string[] | undefined>;
|
|
101
|
+
readonly body: string;
|
|
102
|
+
} | {
|
|
103
|
+
readonly kind: "too_large";
|
|
104
|
+
readonly status: number;
|
|
105
|
+
} | {
|
|
106
|
+
readonly kind: "not_sent";
|
|
107
|
+
readonly code: string;
|
|
108
|
+
readonly message: string;
|
|
109
|
+
} | {
|
|
110
|
+
readonly kind: "lost";
|
|
111
|
+
readonly code: string;
|
|
112
|
+
readonly message: string;
|
|
113
|
+
};
|
|
114
|
+
export interface DeliveryAnswer {
|
|
115
|
+
readonly result: WireOutboundResult;
|
|
116
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
|
|
3
|
+
* service for remote MCP servers and Action deliveries. Internal, like the model call route
|
|
4
|
+
* (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
|
|
5
|
+
* documents.
|
|
6
|
+
*
|
|
7
|
+
* Remote MCP. The gate holds the connection and the vault credential; the loop names the
|
|
8
|
+
* server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
|
|
9
|
+
* - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
|
|
10
|
+
* authorization included), or keeps the open one.
|
|
11
|
+
* - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
|
|
12
|
+
* - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
|
|
13
|
+
* `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
|
|
14
|
+
* once (G3); a re-send joins it or gets its outcome.
|
|
15
|
+
* - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
|
|
16
|
+
* - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
|
|
17
|
+
* Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
|
|
18
|
+
* answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
|
|
19
|
+
* `{ok: false, error: {uncertain: true}}` and is never run again.
|
|
20
|
+
*
|
|
21
|
+
* Action deliveries. `POST /nylorun/v1/deliveries` `{url, body, headers, timeoutMs}`: the gate
|
|
22
|
+
* POSTs the signed delivery under the gateway's own address policy and answers `200 {result}`,
|
|
23
|
+
* what `tenant/outbound.ts` `post` returned, with the answer's body in base64.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { PROTOCOL_HEADER, SIGNATURE_HEADER } from "@nylorun/core/compatibility";
|
|
27
|
+
export const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
|
|
28
|
+
export const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
|
|
29
|
+
export const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
|
|
30
|
+
export const TOOL_CALLS_PATH = "/nylorun/v1/tool-calls";
|
|
31
|
+
export const DELIVERIES_PATH = "/nylorun/v1/deliveries";
|
|
32
|
+
/** Largest tool call or delivery body the gate reads. */
|
|
33
|
+
export const MAX_TOOL_BODY_BYTES = 8 * 1024 * 1024;
|
|
34
|
+
/** The longest delivery timeout the gate accepts: an endpoint's maximum `timeoutMs`. */
|
|
35
|
+
export const MAX_DELIVERY_TIMEOUT_MS = 15 * 60_000;
|
|
36
|
+
/** The only headers the gate forwards on a delivery, lower-cased. */
|
|
37
|
+
export const DELIVERY_HEADERS = new Set([
|
|
38
|
+
SIGNATURE_HEADER.toLowerCase(),
|
|
39
|
+
PROTOCOL_HEADER.toLowerCase(),
|
|
40
|
+
"idempotency-key",
|
|
41
|
+
]);
|
|
42
|
+
export const McpServerRefSchema = z.object({
|
|
43
|
+
sessionId: z.string().min(1),
|
|
44
|
+
agentId: z.string().min(1).optional(),
|
|
45
|
+
capabilityId: z.string().min(1),
|
|
46
|
+
serverName: z.string().min(1),
|
|
47
|
+
});
|
|
48
|
+
export const McpServerBodySchema = z.object({ server: McpServerRefSchema });
|
|
49
|
+
export const McpListBodySchema = z.object({
|
|
50
|
+
server: McpServerRefSchema,
|
|
51
|
+
cursor: z.string().optional(),
|
|
52
|
+
});
|
|
53
|
+
export const ToolCallBodySchema = z.object({
|
|
54
|
+
server: McpServerRefSchema,
|
|
55
|
+
effectId: z.string().min(1),
|
|
56
|
+
name: z.string().min(1),
|
|
57
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
58
|
+
});
|
|
59
|
+
export const DeliveryBodySchema = z.object({
|
|
60
|
+
url: z.string().url(),
|
|
61
|
+
body: z.string(),
|
|
62
|
+
headers: z.record(z.string(), z.string()),
|
|
63
|
+
timeoutMs: z.number().int().positive().max(MAX_DELIVERY_TIMEOUT_MS),
|
|
64
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tool Gate seam (blueprint §12, F4.1). Every tool call that leaves the loop crosses one
|
|
3
|
+
* `ToolGate`: a remote MCP server's connection and calls, and every Action delivery to an
|
|
4
|
+
* Action endpoint. The gate owns what those need that the loop must not hold: the MCP
|
|
5
|
+
* credential and its OAuth refresh, and the outbound route.
|
|
6
|
+
*
|
|
7
|
+
* Two implementations: `inProcessToolGate` (embedding, the ephemeral Runtime, tests), which
|
|
8
|
+
* opens remote servers and POSTs deliveries in this process, and the HTTP client of the gates
|
|
9
|
+
* service (`tool-client.ts`, the `gateway` container). Stdio MCP servers and the sandbox tools
|
|
10
|
+
* never cross it: they run beside the loop until the session sandbox takes them (P4).
|
|
11
|
+
*/
|
|
12
|
+
import type { LiveConnection } from "../mcp/connect.js";
|
|
13
|
+
import type { McpServerRef } from "../mcp/pool.js";
|
|
14
|
+
import { type OutboundPolicy, type OutboundResult } from "../tenant/outbound.js";
|
|
15
|
+
/** One Action delivery or endpoint ping, signed by the caller. */
|
|
16
|
+
export interface DeliveryRequest {
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly body: string;
|
|
19
|
+
/** `Nylorun-Signature`, `Nylorun-Protocol` and `Idempotency-Key`; nothing else. */
|
|
20
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
21
|
+
/** How long the endpoint may take to answer. */
|
|
22
|
+
readonly timeoutMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ToolGate {
|
|
25
|
+
/**
|
|
26
|
+
* Opens a remote (`streamable-http` or `sse`) MCP server of a session. Absent: the loop's
|
|
27
|
+
* pool opens it itself, with the Tenant vault.
|
|
28
|
+
*/
|
|
29
|
+
openMcp?(server: McpServerRef): Promise<LiveConnection>;
|
|
30
|
+
/** POSTs a delivery. Never throws; `signal` aborts it (see `tenant/outbound.ts`). */
|
|
31
|
+
post(request: DeliveryRequest, signal: AbortSignal): Promise<OutboundResult>;
|
|
32
|
+
/**
|
|
33
|
+
* True when a keyed MCP call outlives the caller's process (the gates service, G3): after a
|
|
34
|
+
* takeover or a shutdown, re-sending it joins the call or returns its outcome, and a call
|
|
35
|
+
* lost with the gateway answers `uncertain` instead of running again.
|
|
36
|
+
*/
|
|
37
|
+
readonly recovers?: boolean;
|
|
38
|
+
/** Stops a keyed MCP call that outlives its caller (a user cancel). Never rejects. */
|
|
39
|
+
cancel?(request: {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
effectId: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** The Tool Gate in the caller's process: remote MCP through the pool, deliveries by `post`. */
|
|
45
|
+
export declare function inProcessToolGate(policy?: OutboundPolicy): ToolGate;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { post } from "../tenant/outbound.js";
|
|
2
|
+
/** The Tool Gate in the caller's process: remote MCP through the pool, deliveries by `post`. */
|
|
3
|
+
export function inProcessToolGate(policy = {}) {
|
|
4
|
+
return {
|
|
5
|
+
post: (request, signal) => post(request.url, request.body, { ...request.headers }, {
|
|
6
|
+
policy,
|
|
7
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(request.timeoutMs)]),
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
}
|
package/dist/host/admin-api.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Admin API (`/v1/admin/**`):
|
|
3
|
-
* key.
|
|
4
|
-
*
|
|
2
|
+
* The Admin API (`/v1/admin/**`): the Host's status and shutdown, for a caller with the admin
|
|
3
|
+
* key. A Host serves one Tenant, so there are no Tenant routes: the status names the Tenant
|
|
4
|
+
* and, when it could not be opened, why (tenancy.md §9). The Host checks the listener, the
|
|
5
|
+
* protocol and the admin key first (`app.ts`); what gets here is authorized. Routes are
|
|
6
|
+
* declared once, for serving and for the OpenAPI document.
|
|
5
7
|
*/
|
|
6
8
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import type { AdminStatus } from "@nylorun/core/contracts";
|
|
10
|
+
import type { NodeBindings } from "../tenant/types.js";
|
|
9
11
|
export interface AdminApiOptions {
|
|
10
|
-
module: TenantModule;
|
|
11
12
|
/** `GET /v1/admin/status`. */
|
|
12
13
|
status(): Promise<AdminStatus>;
|
|
13
14
|
/** Stops the Host once the shutdown answer is sent. */
|
package/dist/host/admin-api.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Admin API (`/v1/admin/**`):
|
|
3
|
-
* key.
|
|
4
|
-
*
|
|
2
|
+
* The Admin API (`/v1/admin/**`): the Host's status and shutdown, for a caller with the admin
|
|
3
|
+
* key. A Host serves one Tenant, so there are no Tenant routes: the status names the Tenant
|
|
4
|
+
* and, when it could not be opened, why (tenancy.md §9). The Host checks the listener, the
|
|
5
|
+
* protocol and the admin key first (`app.ts`); what gets here is authorized. Routes are
|
|
6
|
+
* declared once, for serving and for the OpenAPI document.
|
|
5
7
|
*/
|
|
6
8
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
7
9
|
import { z } from "zod";
|
|
8
|
-
import {
|
|
9
|
-
import { AdminStatus as AdminStatusBody, AdminTenantList, AdminTenantStatus, CreateTenantRequest, HostShutdownResponse, ProtocolRejected, Rejected, TenantEnvelope, } from "../api/components.js";
|
|
10
|
+
import { AdminStatus as AdminStatusBody, HostShutdownResponse, ProtocolRejected, Rejected, } from "../api/components.js";
|
|
10
11
|
import { serveRoute } from "../api/route.js";
|
|
11
|
-
import {
|
|
12
|
-
import { TenantNotFoundError, } from "../tenant/types.js";
|
|
13
|
-
import { jsonResponse, opaqueNotFoundResponse, readJsonBody, rejectedResponse, } from "./http.js";
|
|
12
|
+
import { jsonResponse, rejectedResponse } from "./http.js";
|
|
14
13
|
const json = (schema, description) => ({
|
|
15
14
|
description,
|
|
16
15
|
content: { "application/json": { schema } },
|
|
@@ -18,12 +17,9 @@ const json = (schema, description) => ({
|
|
|
18
17
|
const rejected = (description) => json(Rejected, description);
|
|
19
18
|
/** What every Admin route may answer before its own logic runs. */
|
|
20
19
|
const gated = {
|
|
21
|
-
404: rejected("Not found: an unknown admin key, the public listener
|
|
20
|
+
404: rejected("Not found: an unknown admin key, or the public listener"),
|
|
22
21
|
426: json(ProtocolRejected, "`Nylorun-Protocol` missing or unsupported"),
|
|
23
22
|
};
|
|
24
|
-
const tenantId = z.object({
|
|
25
|
-
tenantId: z.string().meta({ example: "tn_0123456789abcdefghjkmnpqrs" }),
|
|
26
|
-
});
|
|
27
23
|
function adminRoute(route) {
|
|
28
24
|
return {
|
|
29
25
|
...route,
|
|
@@ -33,7 +29,6 @@ function adminRoute(route) {
|
|
|
33
29
|
};
|
|
34
30
|
}
|
|
35
31
|
export function createAdminApi(options) {
|
|
36
|
-
const { module } = options;
|
|
37
32
|
const api = new OpenAPIHono();
|
|
38
33
|
// HEAD is not a GET here: an Admin route answers only the methods it declares.
|
|
39
34
|
api.use(async (c, next) => {
|
|
@@ -41,112 +36,13 @@ export function createAdminApi(options) {
|
|
|
41
36
|
return notFound();
|
|
42
37
|
await next();
|
|
43
38
|
});
|
|
44
|
-
serveRoute(api, adminRoute({
|
|
45
|
-
method: "get",
|
|
46
|
-
path: "/v1/admin/tenants",
|
|
47
|
-
summary: "List Tenants",
|
|
48
|
-
responses: { 200: json(AdminTenantList, "Every Tenant on the Host") },
|
|
49
|
-
}), async () => jsonResponse(200, await module.list()));
|
|
50
|
-
serveRoute(api, adminRoute({
|
|
51
|
-
method: "post",
|
|
52
|
-
path: "/v1/admin/tenants",
|
|
53
|
-
summary: "Create a Tenant",
|
|
54
|
-
description: "Idempotent on `idempotencyKey`: a replay answers 200 with the same Tenant.",
|
|
55
|
-
request: {
|
|
56
|
-
body: {
|
|
57
|
-
required: true,
|
|
58
|
-
content: { "application/json": { schema: CreateTenantRequest } },
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
responses: {
|
|
62
|
-
200: json(TenantEnvelope, "The Tenant, created earlier by the same request"),
|
|
63
|
-
201: json(TenantEnvelope, "The Tenant, created"),
|
|
64
|
-
400: rejected("Invalid JSON or request body (`invalid_request`)"),
|
|
65
|
-
409: rejected("Another Tenant has this id or idempotency key (`tenant_conflict`)"),
|
|
66
|
-
413: rejected("Request body over 1 MiB"),
|
|
67
|
-
},
|
|
68
|
-
}), async (c) => {
|
|
69
|
-
const body = CreateTenantRequestSchema.parse(await readJsonBody(c.req.raw));
|
|
70
|
-
try {
|
|
71
|
-
const result = await module.create({
|
|
72
|
-
tenantId: body.tenantId,
|
|
73
|
-
name: body.name,
|
|
74
|
-
principalId: body.principalId,
|
|
75
|
-
credentialHash: body.credentialHash,
|
|
76
|
-
idempotencyKey: body.idempotencyKey,
|
|
77
|
-
...(body.studioCredentialHash
|
|
78
|
-
? { studioCredentialHash: body.studioCredentialHash }
|
|
79
|
-
: {}),
|
|
80
|
-
...(body.derivedPrincipals?.length
|
|
81
|
-
? { derivedPrincipals: body.derivedPrincipals }
|
|
82
|
-
: {}),
|
|
83
|
-
});
|
|
84
|
-
return jsonResponse(result.created ? 201 : 200, result.envelope);
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
const code = error.code;
|
|
88
|
-
if (error instanceof TenantConflictError ||
|
|
89
|
-
error.name === "TenantConflictError" ||
|
|
90
|
-
code === "conflict" ||
|
|
91
|
-
code === "tenant_conflict")
|
|
92
|
-
return rejectedResponse(409, "tenant_conflict", error instanceof Error ? error.message : "Tenant conflict");
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
serveRoute(api, adminRoute({
|
|
97
|
-
method: "get",
|
|
98
|
-
path: "/v1/admin/tenants/{tenantId}",
|
|
99
|
-
summary: "Get a Tenant's status",
|
|
100
|
-
request: { params: tenantId },
|
|
101
|
-
responses: { 200: json(AdminTenantStatus, "The Tenant and its state") },
|
|
102
|
-
}), async (c) => {
|
|
103
|
-
const status = await module.status(c.req.param("tenantId"));
|
|
104
|
-
return status ? jsonResponse(200, status) : opaqueNotFoundResponse();
|
|
105
|
-
});
|
|
106
|
-
serveRoute(api, adminRoute({
|
|
107
|
-
method: "delete",
|
|
108
|
-
path: "/v1/admin/tenants/{tenantId}",
|
|
109
|
-
summary: "Delete a Tenant",
|
|
110
|
-
request: {
|
|
111
|
-
params: tenantId,
|
|
112
|
-
query: z.object({
|
|
113
|
-
activeWork: z.enum(["refuse", "drain", "cancel"]).optional().meta({
|
|
114
|
-
description: "What to do with turns in progress: refuse to delete (default), wait for them, or cancel them",
|
|
115
|
-
}),
|
|
116
|
-
}),
|
|
117
|
-
},
|
|
118
|
-
responses: {
|
|
119
|
-
204: { description: "Deleted" },
|
|
120
|
-
400: rejected("`activeWork` is not refuse, drain or cancel (`invalid_request`)"),
|
|
121
|
-
409: rejected("The Tenant has work in progress and `activeWork` is refuse (`active_work`)"),
|
|
122
|
-
},
|
|
123
|
-
}), async (c) => {
|
|
124
|
-
const activeWork = c.req.query("activeWork") ?? "refuse";
|
|
125
|
-
if (activeWork !== "refuse" && activeWork !== "drain" && activeWork !== "cancel")
|
|
126
|
-
return rejectedResponse(400, "invalid_request", "activeWork must be refuse, drain, or cancel");
|
|
127
|
-
try {
|
|
128
|
-
await module.delete(c.req.param("tenantId"), activeWork);
|
|
129
|
-
return new Response(null, { status: 204 });
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (error instanceof TenantNotFoundError)
|
|
133
|
-
return opaqueNotFoundResponse();
|
|
134
|
-
const code = error.code;
|
|
135
|
-
if (error instanceof TenantBusyError ||
|
|
136
|
-
error.name === "TenantBusyError" ||
|
|
137
|
-
code === "active_work" ||
|
|
138
|
-
code === "conflict")
|
|
139
|
-
return rejectedResponse(409, "active_work", error instanceof Error ? error.message : "Active work");
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
39
|
// D12: `/v1/admin/status` and `/v1/admin/host` are one answer.
|
|
144
40
|
for (const path of ["/v1/admin/status", "/v1/admin/host"])
|
|
145
41
|
serveRoute(api, adminRoute({
|
|
146
42
|
method: "get",
|
|
147
43
|
path,
|
|
148
44
|
summary: path.endsWith("host") ? "Get the Host's status (alias)" : "Get the Host's status",
|
|
149
|
-
responses: { 200: json(AdminStatusBody, "Version, protocol,
|
|
45
|
+
responses: { 200: json(AdminStatusBody, "Version, protocol, the Tenant and the Host") },
|
|
150
46
|
}), async () => jsonResponse(200, await options.status()));
|
|
151
47
|
serveRoute(api, adminRoute({
|
|
152
48
|
method: "post",
|
package/dist/host/app.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type HostBindings = NodeBindings & {
|
|
|
7
7
|
export type HostEnv = {
|
|
8
8
|
Bindings: HostBindings;
|
|
9
9
|
Variables: {
|
|
10
|
-
/** The Tenant a request
|
|
10
|
+
/** The Tenant a request reached, for the request log. */
|
|
11
11
|
tenantId?: string;
|
|
12
12
|
/** The status to log when it is not the answer's (a rejection after a stream started). */
|
|
13
13
|
status?: number;
|