@nylorun/runtime 0.14.0-beta → 0.15.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +82 -40
- 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 +185 -686
- 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 +6 -7
- 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 +888 -695
- 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,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's HTTP routes (blueprint §15, P1.1): the Model Gate's IR endpoint, the Tool
|
|
3
|
+
* Gate's remote MCP and delivery routes (F4.1), and the health and readiness probes. Internal:
|
|
4
|
+
* only the loop calls them, with the stack's gates token, so they are not in the published
|
|
5
|
+
* OpenAPI documents. The wire formats are `gates/contract.ts` and `gates/tool-contract.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { bodyLimit } from "hono/body-limit";
|
|
10
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
11
|
+
import { MAX_MODEL_CALL_BYTES, MODEL_CALLS_PATH, ModelCallBodySchema, TENANT_HEADER, } from "../../gates/contract.js";
|
|
12
|
+
import { InflightConflict } from "../../gates/inflight.js";
|
|
13
|
+
import { mountKeysRoutes } from "./keys.js";
|
|
14
|
+
import { DELIVERIES_PATH, DELIVERY_HEADERS, DeliveryBodySchema, MAX_TOOL_BODY_BYTES, MCP_CLOSE_PATH, MCP_CONNECT_PATH, MCP_LIST_PATH, McpListBodySchema, McpServerBodySchema, TOOL_CALLS_PATH, ToolCallBodySchema, } from "../../gates/tool-contract.js";
|
|
15
|
+
import { post } from "../../tenant/outbound.js";
|
|
16
|
+
import { adminKeyMatches } from "../../host/http.js";
|
|
17
|
+
import { canonical } from "../../store/canonical.js";
|
|
18
|
+
const invalid = (message) => ({
|
|
19
|
+
error: { code: "invalid_request", message },
|
|
20
|
+
});
|
|
21
|
+
export function createGatesApp(options) {
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
const maxSize = options.maxBodyBytes ?? MAX_MODEL_CALL_BYTES;
|
|
24
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
25
|
+
app.get("/ready", async (c) => (await options.ready()) ? c.json({ ready: true }) : c.json({ ready: false }, 503));
|
|
26
|
+
const authorized = async (c, next) => {
|
|
27
|
+
const presented = /^Bearer (.+)$/.exec(c.req.header("authorization") ?? "")?.[1];
|
|
28
|
+
if (!adminKeyMatches(presented, options.token)) {
|
|
29
|
+
const body = {
|
|
30
|
+
error: {
|
|
31
|
+
code: "gate_unauthorized",
|
|
32
|
+
message: "Missing or wrong gates token; the caller and the gateway must share NYLORUN_GATES_TOKEN",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return c.json(body, 401);
|
|
36
|
+
}
|
|
37
|
+
await next();
|
|
38
|
+
};
|
|
39
|
+
const tenantOf = (c) => {
|
|
40
|
+
const tenantId = c.req.header(TENANT_HEADER);
|
|
41
|
+
return tenantId !== undefined && !isTenantId(tenantId) ? false : tenantId;
|
|
42
|
+
};
|
|
43
|
+
const toolBodies = bodyLimit({
|
|
44
|
+
maxSize: MAX_TOOL_BODY_BYTES,
|
|
45
|
+
onError: (c) => c.json(invalid(`A request body may be at most ${MAX_TOOL_BODY_BYTES} bytes`), 400),
|
|
46
|
+
});
|
|
47
|
+
/** The parsed body, or the 400 to answer. */
|
|
48
|
+
async function read(c, schema) {
|
|
49
|
+
const tenantId = tenantOf(c);
|
|
50
|
+
if (tenantId === false)
|
|
51
|
+
return { ok: false, response: c.json(invalid(`The ${TENANT_HEADER} header must name a Tenant`), 400) };
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = await c.req.json();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { ok: false, response: c.json(invalid("The body must be JSON"), 400) };
|
|
58
|
+
}
|
|
59
|
+
const parsed = schema.safeParse(raw);
|
|
60
|
+
if (!parsed.success)
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
response: c.json(invalid(`Invalid request: ${parsed.error.issues[0]?.message ?? "malformed"}`), 400),
|
|
64
|
+
};
|
|
65
|
+
return { ok: true, tenantId, body: parsed.data };
|
|
66
|
+
}
|
|
67
|
+
if (options.keys)
|
|
68
|
+
mountKeysRoutes(app, {
|
|
69
|
+
authorized,
|
|
70
|
+
keys: options.keys,
|
|
71
|
+
logger: options.logger ?? { info() { }, warn() { }, error() { } },
|
|
72
|
+
});
|
|
73
|
+
const mcp = options.mcp;
|
|
74
|
+
if (mcp) {
|
|
75
|
+
app.post(MCP_CONNECT_PATH, authorized, toolBodies, async (c) => {
|
|
76
|
+
const request = await read(c, McpServerBodySchema);
|
|
77
|
+
if (!request.ok)
|
|
78
|
+
return request.response;
|
|
79
|
+
return c.json(await mcp.connect(request.tenantId, request.body.server));
|
|
80
|
+
});
|
|
81
|
+
app.post(MCP_LIST_PATH, authorized, toolBodies, async (c) => {
|
|
82
|
+
const request = await read(c, McpListBodySchema);
|
|
83
|
+
if (!request.ok)
|
|
84
|
+
return request.response;
|
|
85
|
+
const { server, cursor } = request.body;
|
|
86
|
+
return c.json(await mcp.list(request.tenantId, server, cursor, c.req.raw.signal));
|
|
87
|
+
});
|
|
88
|
+
app.post(MCP_CLOSE_PATH, authorized, toolBodies, async (c) => {
|
|
89
|
+
const request = await read(c, McpServerBodySchema);
|
|
90
|
+
if (!request.ok)
|
|
91
|
+
return request.response;
|
|
92
|
+
await mcp.close(request.body.server);
|
|
93
|
+
return c.body(null, 204);
|
|
94
|
+
});
|
|
95
|
+
const toolCalls = options.toolCalls;
|
|
96
|
+
app.post(`${TOOL_CALLS_PATH}/:key/cancel`, authorized, (c) => {
|
|
97
|
+
if (tenantOf(c) === false)
|
|
98
|
+
return c.json(invalid(`The ${TENANT_HEADER} header must name a Tenant`), 400);
|
|
99
|
+
toolCalls?.cancel(c.req.param("key"));
|
|
100
|
+
return c.body(null, 204);
|
|
101
|
+
});
|
|
102
|
+
app.post(TOOL_CALLS_PATH, authorized, toolBodies, async (c) => {
|
|
103
|
+
const request = await read(c, ToolCallBodySchema);
|
|
104
|
+
if (!request.ok)
|
|
105
|
+
return request.response;
|
|
106
|
+
const signal = c.req.raw.signal;
|
|
107
|
+
const key = c.req.header("idempotency-key");
|
|
108
|
+
try {
|
|
109
|
+
// Keyed: the call runs under its own signal, once, and outlives this request (G3).
|
|
110
|
+
const answer = key && toolCalls
|
|
111
|
+
? await untilAborted(toolCalls.run(request.tenantId, key, createHash("sha256").update(canonical(request.body)).digest("hex"), request.body), signal)
|
|
112
|
+
: await mcp.call(request.tenantId, request.body, signal);
|
|
113
|
+
if (signal.aborted)
|
|
114
|
+
return new Response(null, { status: 499 });
|
|
115
|
+
return c.json(answer);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof InflightConflict) {
|
|
119
|
+
const body = { error: { code: "gate_conflict", message: error.message } };
|
|
120
|
+
return c.json(body, 409);
|
|
121
|
+
}
|
|
122
|
+
if (signal.aborted)
|
|
123
|
+
return new Response(null, { status: 499 });
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const policy = options.delivery;
|
|
129
|
+
if (policy) {
|
|
130
|
+
app.post(DELIVERIES_PATH, authorized, toolBodies, async (c) => {
|
|
131
|
+
const request = await read(c, DeliveryBodySchema);
|
|
132
|
+
if (!request.ok)
|
|
133
|
+
return request.response;
|
|
134
|
+
const { url, body, headers, timeoutMs } = request.body;
|
|
135
|
+
const forwarded = {};
|
|
136
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
137
|
+
if (!DELIVERY_HEADERS.has(name.toLowerCase()))
|
|
138
|
+
return c.json(invalid(`A delivery may not carry the header ${name}`), 400);
|
|
139
|
+
forwarded[name] = value;
|
|
140
|
+
}
|
|
141
|
+
const signal = c.req.raw.signal;
|
|
142
|
+
const result = await post(url, body, forwarded, {
|
|
143
|
+
policy,
|
|
144
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]),
|
|
145
|
+
});
|
|
146
|
+
if (signal.aborted)
|
|
147
|
+
return new Response(null, { status: 499 });
|
|
148
|
+
const answer = {
|
|
149
|
+
result: result.kind === "response"
|
|
150
|
+
? {
|
|
151
|
+
kind: "response",
|
|
152
|
+
status: result.status,
|
|
153
|
+
headers: result.headers,
|
|
154
|
+
body: result.body.toString("base64"),
|
|
155
|
+
}
|
|
156
|
+
: result,
|
|
157
|
+
};
|
|
158
|
+
return c.json(answer);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
app.post(`${MODEL_CALLS_PATH}/:key/cancel`, authorized, (c) => {
|
|
162
|
+
// Optional, as on a call: the gate serves one Tenant, so the key alone names the call.
|
|
163
|
+
const tenantId = c.req.header(TENANT_HEADER);
|
|
164
|
+
if (tenantId !== undefined && !isTenantId(tenantId))
|
|
165
|
+
return c.json(invalid(`The ${TENANT_HEADER} header must name a Tenant`), 400);
|
|
166
|
+
options.inflight.cancel(c.req.param("key"));
|
|
167
|
+
return c.body(null, 204);
|
|
168
|
+
});
|
|
169
|
+
app.post(MODEL_CALLS_PATH, authorized, bodyLimit({
|
|
170
|
+
maxSize,
|
|
171
|
+
onError: (c) => c.json(invalid(`A model call body may be at most ${maxSize} bytes`), 400),
|
|
172
|
+
}), async (c) => {
|
|
173
|
+
// Optional: the gate serves its database's one Tenant, and refuses a call naming another.
|
|
174
|
+
const tenantId = c.req.header(TENANT_HEADER);
|
|
175
|
+
if (tenantId !== undefined && !isTenantId(tenantId))
|
|
176
|
+
return c.json(invalid(`The ${TENANT_HEADER} header must name a Tenant`), 400);
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = await c.req.json();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return c.json(invalid("The body must be JSON"), 400);
|
|
183
|
+
}
|
|
184
|
+
const parsed = ModelCallBodySchema.safeParse(raw);
|
|
185
|
+
if (!parsed.success)
|
|
186
|
+
return c.json(invalid(`Invalid model call: ${parsed.error.issues[0]?.message ?? "malformed"}`), 400);
|
|
187
|
+
const { call, ...ids } = parsed.data;
|
|
188
|
+
const request = {
|
|
189
|
+
...(tenantId === undefined ? {} : { tenantId }),
|
|
190
|
+
...ids,
|
|
191
|
+
call: call,
|
|
192
|
+
};
|
|
193
|
+
const signal = c.req.raw.signal;
|
|
194
|
+
const key = c.req.header("idempotency-key");
|
|
195
|
+
try {
|
|
196
|
+
let outcome;
|
|
197
|
+
if (key) {
|
|
198
|
+
// Keyed: the call runs under its own signal and outlives this request (P1.2). The
|
|
199
|
+
// gate serves one Tenant, so the effect id alone is the key.
|
|
200
|
+
const hash = createHash("sha256").update(canonical(parsed.data)).digest("hex");
|
|
201
|
+
outcome = await untilAborted(options.inflight.run(key, hash, (own) => options.modelGate.call(request, own)), signal);
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
outcome = await options.modelGate.call(request, signal);
|
|
205
|
+
return c.json({ outcome });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof InflightConflict) {
|
|
209
|
+
const body = { error: { code: "gate_conflict", message: error.message } };
|
|
210
|
+
return c.json(body, 409);
|
|
211
|
+
}
|
|
212
|
+
// The caller went away (or cancelled): nobody reads the answer.
|
|
213
|
+
if (signal.aborted)
|
|
214
|
+
return new Response(null, { status: 499 });
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return app;
|
|
219
|
+
}
|
|
220
|
+
/** `promise`, or a rejection as soon as `signal` aborts (the call itself keeps running). */
|
|
221
|
+
function untilAborted(promise, signal) {
|
|
222
|
+
if (signal.aborted)
|
|
223
|
+
return Promise.reject(signal.reason);
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const onAbort = () => reject(signal.reason);
|
|
226
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
227
|
+
promise.then((value) => {
|
|
228
|
+
signal.removeEventListener("abort", onAbort);
|
|
229
|
+
resolve(value);
|
|
230
|
+
}, (error) => {
|
|
231
|
+
signal.removeEventListener("abort", onAbort);
|
|
232
|
+
reject(error);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
package/dist/api/http/define.js
CHANGED
|
@@ -70,14 +70,6 @@ export function tenantRoute(api, access, route, handler) {
|
|
|
70
70
|
const schemes = [...new Set(access.credentials.map((credential) => SCHEMES[credential]))];
|
|
71
71
|
const headers = z.object({
|
|
72
72
|
"Nylorun-Protocol": z.string().meta({ description: `The protocol version, \`${PROTOCOL_VERSION}\`` }),
|
|
73
|
-
...(access.browser || takes("publishable")
|
|
74
|
-
? {
|
|
75
|
-
"Nylorun-Tenant": z
|
|
76
|
-
.string()
|
|
77
|
-
.optional()
|
|
78
|
-
.meta({ description: "The Tenant; optional with a publishable key, which names it" }),
|
|
79
|
-
}
|
|
80
|
-
: { "Nylorun-Tenant": z.string().meta({ description: "The Tenant" }) }),
|
|
81
73
|
...(takes("subject")
|
|
82
74
|
? {
|
|
83
75
|
"Nylorun-Subject": z
|
|
@@ -35,15 +35,18 @@ export function accessRoutes(api) {
|
|
|
35
35
|
tags: ["Access"],
|
|
36
36
|
summary: "Get the public keys subject and delivery tokens are signed with",
|
|
37
37
|
description: "A JSON Web Key Set, to verify a subject token or a delivery token without calling the " +
|
|
38
|
-
"Runtime. No credential is needed
|
|
38
|
+
"Runtime. No credential is needed.",
|
|
39
39
|
responses: { 200: json(Jwks, "The public keys") },
|
|
40
40
|
}, async (c) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
// Anonymous: reads the public keys. Only when the current or standby key is missing does
|
|
42
|
+
// it ask the keys service to create it, so a verifier never caches a set without them.
|
|
43
|
+
const ctx = c.env.tenant;
|
|
44
|
+
const read = () => ctx.store.tx((t) => t.signingKeys(["standby", "current", "previous"]));
|
|
45
|
+
let rows = await read();
|
|
46
|
+
if (!complete(rows)) {
|
|
47
|
+
await ctx.keys.ensureSigningKeys();
|
|
48
|
+
rows = await read();
|
|
49
|
+
}
|
|
47
50
|
return jsonResponse(200, { keys: rows.map(publicJwk) });
|
|
48
51
|
});
|
|
49
52
|
tenantRoute(api, APPLICATION, {
|
|
@@ -89,12 +92,13 @@ export function accessRoutes(api) {
|
|
|
89
92
|
responses: { 200: json(SigningKeyList, "Standby, current, previous and revoked keys") },
|
|
90
93
|
}, async (c) => {
|
|
91
94
|
requireApplication(c.get("scope"));
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
const ctx = c.env.tenant;
|
|
96
|
+
const read = () => ctx.store.tx((t) => t.signingKeys());
|
|
97
|
+
let rows = await read();
|
|
98
|
+
if (!complete(rows)) {
|
|
99
|
+
await ctx.keys.ensureSigningKeys();
|
|
100
|
+
rows = await read();
|
|
101
|
+
}
|
|
98
102
|
return jsonResponse(200, { keys: rows.map(signingKeyView) });
|
|
99
103
|
});
|
|
100
104
|
tenantRoute(api, APPLICATION, {
|
|
@@ -112,11 +116,10 @@ export function accessRoutes(api) {
|
|
|
112
116
|
requireApplication(c.get("scope"));
|
|
113
117
|
const ctx = c.env.tenant;
|
|
114
118
|
const request = RotateSigningKeysRequestSchema.parse(await readJson(c.req.raw));
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const rows = await ctx.store.tx(async (t) => keys.rotate(t, kek, (await readPolicy(t)).tokens.maxTtlSeconds, request.force === true));
|
|
119
|
+
const maxTtlSeconds = await ctx.store.tx(async (t) => (await readPolicy(t)).tokens.maxTtlSeconds);
|
|
120
|
+
const keys = await ctx.keys.rotateSigningKeys({ maxTtlSeconds, force: request.force === true });
|
|
118
121
|
ctx.config.logger.info("signing keys rotated", { force: request.force === true });
|
|
119
|
-
return jsonResponse(200, { keys
|
|
122
|
+
return jsonResponse(200, { keys });
|
|
120
123
|
});
|
|
121
124
|
tenantRoute(api, APPLICATION, {
|
|
122
125
|
method: "post",
|
|
@@ -153,7 +156,7 @@ export function accessRoutes(api) {
|
|
|
153
156
|
path: "/v1/access/publishable-keys",
|
|
154
157
|
tags: ["Access"],
|
|
155
158
|
summary: "Create a publishable key",
|
|
156
|
-
description: "A key a browser page on one of the origins sends as `Nylorun-Key`. Public by design; it
|
|
159
|
+
description: "A key a browser page on one of the origins sends as `Nylorun-Key`. Public by design; it grants the policy's anonymous scopes.",
|
|
157
160
|
request: { body: body(CreatePublishableKeyRequest) },
|
|
158
161
|
responses: {
|
|
159
162
|
200: json(PublishableKey, "The key"),
|
|
@@ -257,3 +260,7 @@ export async function revokeSubject(ctx, subject) {
|
|
|
257
260
|
ctx.config.logger.info("subject revoked", { epoch });
|
|
258
261
|
return { subject, epoch };
|
|
259
262
|
}
|
|
263
|
+
/** True when the Tenant has a current and a standby signing key. */
|
|
264
|
+
function complete(rows) {
|
|
265
|
+
return rows.some((row) => row.state === "current") && rows.some((row) => row.state === "standby");
|
|
266
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Tenant's own settings (`/v1/tenant/**`): its status, reset, a first configuration, the
|
|
3
|
-
* model it calls and the sandboxes its
|
|
3
|
+
* model it calls, what its model calls cost and may cost (budgets), and the sandboxes its
|
|
4
|
+
* sessions get.
|
|
4
5
|
*
|
|
5
6
|
* A reset that deletes sessions moves the Tenant to a new basin generation, so a session
|
|
6
7
|
* created again starts in an empty basin; the old basin is deleted after a grace period.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { PutHostModelRequestSchema, PutTenantSandboxRequestSchema, ResetTenantRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
-
import { HostModelCatalog, HostModelView, ListProvidersResponse, PutHostModelRequest, PutTenantSandboxRequest, ResetTenantRequest, ResetTenantResponse, SeedTenantConfigRequest, SeedTenantConfigResponse, SelectHostModelRequest, TenantSandboxView, TenantStatus, } from "../../components.js";
|
|
2
|
+
import { ModelUsageQuerySchema, PutModelBudgetsRequestSchema, PutHostModelRequestSchema, PutTenantSandboxRequestSchema, ResetTenantRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { HostModelCatalog, HostModelView, ListProvidersResponse, ModelBudgets, ModelUsageTotals, PutModelBudgetsRequest, PutHostModelRequest, PutTenantSandboxRequest, ResetTenantRequest, ResetTenantResponse, SeedTenantConfigRequest, SeedTenantConfigResponse, SelectHostModelRequest, TenantSandboxView, TenantStatus, } from "../../components.js";
|
|
4
|
+
import { periodStart } from "../../../gates/meter.js";
|
|
4
5
|
import { hostModelCatalog } from "../../../model/catalog.js";
|
|
5
6
|
import { sandboxConfigErrors } from "../../../sandbox/resolve.js";
|
|
6
7
|
import { effectiveSandboxConfig, readSandboxConfig, writeSandboxConfig, } from "../../../sandbox/tenant-config.js";
|
|
@@ -27,6 +28,21 @@ const body = (schema) => ({
|
|
|
27
28
|
required: true,
|
|
28
29
|
content: { "application/json": { schema } },
|
|
29
30
|
});
|
|
31
|
+
const budgetRow = (budget, updatedAt) => ({
|
|
32
|
+
scope: budget.scope,
|
|
33
|
+
scopeId: budget.scope === "agent" ? budget.id : "*",
|
|
34
|
+
period: budget.period ?? null,
|
|
35
|
+
limitUsd: budget.limitUsd ?? null,
|
|
36
|
+
limitTokens: budget.limitTokens ?? null,
|
|
37
|
+
updatedAt,
|
|
38
|
+
});
|
|
39
|
+
const budgetView = (row) => ({
|
|
40
|
+
scope: row.scope,
|
|
41
|
+
...(row.scope === "agent" ? { id: row.scopeId } : {}),
|
|
42
|
+
...(row.period ? { period: row.period } : {}),
|
|
43
|
+
...(row.limitUsd !== null ? { limitUsd: row.limitUsd } : {}),
|
|
44
|
+
...(row.limitTokens !== null ? { limitTokens: row.limitTokens } : {}),
|
|
45
|
+
});
|
|
30
46
|
/** `GET /v1/tenant/sandbox`: the backend report and the configuration with defaults applied. */
|
|
31
47
|
async function sandboxView(ctx) {
|
|
32
48
|
const config = await ctx.store.tx((t) => readSandboxConfig(t));
|
|
@@ -97,7 +113,7 @@ export function tenantRoutes(api) {
|
|
|
97
113
|
}, async (c) => {
|
|
98
114
|
const ctx = c.env.tenant;
|
|
99
115
|
const request = SeedTenantConfigRequestSchema.parse(await readJson(c.req.raw));
|
|
100
|
-
return jsonResponse(200, await seedTenantConfig({ store: ctx.store, vault: ctx.vault }, request));
|
|
116
|
+
return jsonResponse(200, await seedTenantConfig({ store: ctx.store, vault: ctx.vault, keys: ctx.keys }, request));
|
|
101
117
|
});
|
|
102
118
|
tenantRoute(api, settings(["tenant:settings", "agents:write"]), {
|
|
103
119
|
method: "get",
|
|
@@ -133,6 +149,62 @@ export function tenantRoutes(api) {
|
|
|
133
149
|
await ctx.store.tx((t) => writeSandboxConfig(t, config));
|
|
134
150
|
return jsonResponse(200, await sandboxView(ctx));
|
|
135
151
|
});
|
|
152
|
+
tenantRoute(api, SETTINGS, {
|
|
153
|
+
method: "get",
|
|
154
|
+
path: "/v1/tenant/usage",
|
|
155
|
+
tags: ["Tenant"],
|
|
156
|
+
summary: "Get what the Tenant's model calls used",
|
|
157
|
+
description: "Totals from the model usage ledger, which the model gate writes once per call: for the Tenant, one agent or one turn, over the current UTC day or month, or all time.",
|
|
158
|
+
request: { query: ModelUsageQuerySchema },
|
|
159
|
+
responses: { 200: json(ModelUsageTotals, "The calls, tokens and cost") },
|
|
160
|
+
}, async (c) => {
|
|
161
|
+
const parsed = ModelUsageQuerySchema.safeParse(c.req.query());
|
|
162
|
+
if (!parsed.success)
|
|
163
|
+
return fail(400, parsed.error.issues[0]?.message ?? "Invalid query");
|
|
164
|
+
const { scope, id, period } = parsed.data;
|
|
165
|
+
if (scope !== "tenant" && !id)
|
|
166
|
+
fail(400, `The ${scope} scope needs an id`);
|
|
167
|
+
const since = period === "total" ? undefined : periodStart(period, new Date());
|
|
168
|
+
const totals = await c.env.tenant.store.tx((t) => t.modelUsageTotals({
|
|
169
|
+
scope,
|
|
170
|
+
...(scope !== "tenant" ? { id: id } : {}),
|
|
171
|
+
...(since ? { since } : {}),
|
|
172
|
+
}));
|
|
173
|
+
return jsonResponse(200, {
|
|
174
|
+
scope,
|
|
175
|
+
...(scope !== "tenant" ? { id } : {}),
|
|
176
|
+
period,
|
|
177
|
+
...(since ? { since } : {}),
|
|
178
|
+
...totals,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
tenantRoute(api, SETTINGS, {
|
|
182
|
+
method: "get",
|
|
183
|
+
path: "/v1/tenant/budgets",
|
|
184
|
+
tags: ["Tenant"],
|
|
185
|
+
summary: "Get the Tenant's model budgets",
|
|
186
|
+
responses: { 200: json(ModelBudgets, "The hard caps on model spend") },
|
|
187
|
+
}, async (c) => {
|
|
188
|
+
const rows = await c.env.tenant.store.tx((t) => t.listModelBudgets());
|
|
189
|
+
return jsonResponse(200, { budgets: rows.map(budgetView) });
|
|
190
|
+
});
|
|
191
|
+
tenantRoute(api, SETTINGS, {
|
|
192
|
+
method: "put",
|
|
193
|
+
path: "/v1/tenant/budgets",
|
|
194
|
+
tags: ["Tenant"],
|
|
195
|
+
summary: "Set the Tenant's model budgets",
|
|
196
|
+
description: "Replaces every budget. Before each model call the gate checks the scope's spend against its cap; once a cap is reached the call fails with `budget_exhausted` and the turn with `model.budget_exhausted`. An empty list removes every cap.",
|
|
197
|
+
request: { body: body(PutModelBudgetsRequest) },
|
|
198
|
+
responses: { 200: json(ModelBudgets, "The budgets now in force") },
|
|
199
|
+
}, async (c) => {
|
|
200
|
+
const { budgets } = PutModelBudgetsRequestSchema.parse(await readJson(c.req.raw));
|
|
201
|
+
const updatedAt = new Date().toISOString();
|
|
202
|
+
const rows = await c.env.tenant.store.tx(async (t) => {
|
|
203
|
+
await t.putModelBudgets(budgets.map((budget) => budgetRow(budget, updatedAt)));
|
|
204
|
+
return t.listModelBudgets();
|
|
205
|
+
});
|
|
206
|
+
return jsonResponse(200, { budgets: rows.map(budgetView) });
|
|
207
|
+
});
|
|
136
208
|
tenantRoute(api, settings(["tenant:settings", "agents:write"]), {
|
|
137
209
|
method: "get",
|
|
138
210
|
path: "/v1/tenant/providers",
|
|
@@ -156,7 +228,7 @@ export function tenantRoutes(api) {
|
|
|
156
228
|
responses: { 200: json(HostModelView, "The model now in force") },
|
|
157
229
|
}, async (c) => {
|
|
158
230
|
const ctx = c.env.tenant;
|
|
159
|
-
return jsonResponse(200, await ctx.
|
|
231
|
+
return jsonResponse(200, await ctx.keys.putHostModel(PutHostModelRequestSchema.parse(await readJson(c.req.raw))));
|
|
160
232
|
});
|
|
161
233
|
tenantRoute(api, SETTINGS, {
|
|
162
234
|
method: "put",
|
|
@@ -167,6 +239,6 @@ export function tenantRoutes(api) {
|
|
|
167
239
|
responses: { 200: json(HostModelView, "The model now in force") },
|
|
168
240
|
}, async (c) => {
|
|
169
241
|
const ctx = c.env.tenant;
|
|
170
|
-
return jsonResponse(200, await ctx.
|
|
242
|
+
return jsonResponse(200, await ctx.keys.selectHostModel(SelectHostModelRequestSchema.parse(await readJson(c.req.raw))));
|
|
171
243
|
});
|
|
172
244
|
}
|
|
@@ -112,7 +112,7 @@ export function vaultRoutes(api) {
|
|
|
112
112
|
fail(403, "A subject token cannot store OAuth refresh credentials", {
|
|
113
113
|
code: "scope_required",
|
|
114
114
|
});
|
|
115
|
-
return jsonResponse(200, await c.env.tenant.
|
|
115
|
+
return jsonResponse(200, await c.env.tenant.keys.createCredential(c.req.param("vaultId"), request));
|
|
116
116
|
});
|
|
117
117
|
tenantRoute(api, OWN_VAULTS, {
|
|
118
118
|
method: "get",
|
|
@@ -151,7 +151,7 @@ export function vaultRoutes(api) {
|
|
|
151
151
|
}, async (c) => {
|
|
152
152
|
await ownerFor(c);
|
|
153
153
|
const request = RotateCredentialRequestSchema.parse(await readJson(c.req.raw));
|
|
154
|
-
return jsonResponse(200, await c.env.tenant.
|
|
154
|
+
return jsonResponse(200, await c.env.tenant.keys.rotateCredential(c.req.param("vaultId"), c.req.param("credentialId"), request));
|
|
155
155
|
});
|
|
156
156
|
tenantRoute(api, OWN_VAULTS, {
|
|
157
157
|
method: "delete",
|
package/dist/api/openapi.js
CHANGED
|
@@ -49,7 +49,7 @@ function hostRoutes() {
|
|
|
49
49
|
path: "/ready",
|
|
50
50
|
tags: ["Host"],
|
|
51
51
|
summary: "Check the Runtime is ready",
|
|
52
|
-
description: "The listeners, Tenant
|
|
52
|
+
description: "The listeners, the Tenant (open), Postgres, Restate and S2.",
|
|
53
53
|
responses: {
|
|
54
54
|
200: {
|
|
55
55
|
description: "Ready",
|
|
@@ -75,7 +75,7 @@ function tenantSchemes(registry) {
|
|
|
75
75
|
type: "apiKey",
|
|
76
76
|
in: "header",
|
|
77
77
|
name: "Nylorun-Key",
|
|
78
|
-
description: "A publishable key, sent by a browser page on one of its origins. Public by design: it
|
|
78
|
+
description: "A publishable key, sent by a browser page on one of its origins. Public by design: it grants the access policy's anonymous scopes.",
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
let tenant;
|
|
@@ -94,13 +94,13 @@ export function tenantDocument() {
|
|
|
94
94
|
info: {
|
|
95
95
|
title: "Nylorun Runtime: Tenant API",
|
|
96
96
|
version: RUNTIME_VERSION,
|
|
97
|
-
description: "Agents, sessions and their events, vaults, access and the Tenant's settings
|
|
97
|
+
description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials`, `x-nylorun-scopes` (the subject scopes that reach it) and `x-nylorun-browser` fields.",
|
|
98
98
|
"x-nylorun-protocol": PROTOCOL,
|
|
99
99
|
},
|
|
100
100
|
servers: [
|
|
101
101
|
{
|
|
102
102
|
url: "{origin}",
|
|
103
|
-
description: "A Runtime:
|
|
103
|
+
description: "A Runtime: a local Tenant (`nylorun start`), or where yours runs",
|
|
104
104
|
variables: { origin: { default: "http://localhost:8787" } },
|
|
105
105
|
},
|
|
106
106
|
],
|
|
@@ -113,7 +113,6 @@ export function adminDocument() {
|
|
|
113
113
|
return admin;
|
|
114
114
|
// Only the routes' declarations are read: nothing is served from this app.
|
|
115
115
|
const api = createAdminApi({
|
|
116
|
-
module: undefined,
|
|
117
116
|
status: () => Promise.reject(new Error("Not served")),
|
|
118
117
|
shutdown: () => { },
|
|
119
118
|
});
|
|
@@ -123,13 +122,13 @@ export function adminDocument() {
|
|
|
123
122
|
info: {
|
|
124
123
|
title: "Nylorun Runtime: Admin API",
|
|
125
124
|
version: RUNTIME_VERSION,
|
|
126
|
-
description: "
|
|
125
|
+
description: "The Host's status, its Tenant's state and shutdown, with the admin key. Served on the operator listener when the Host has one; the public listener answers these routes with the opaque 404.",
|
|
127
126
|
"x-nylorun-protocol": PROTOCOL,
|
|
128
127
|
},
|
|
129
128
|
servers: [
|
|
130
129
|
{
|
|
131
130
|
url: "{origin}",
|
|
132
|
-
description: "The operator listener:
|
|
131
|
+
description: "The operator listener: a local Tenant's, or where yours runs",
|
|
133
132
|
variables: { origin: { default: "http://localhost:8788" } },
|
|
134
133
|
},
|
|
135
134
|
],
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
|
|
3
|
+
* it is not in the published OpenAPI documents.
|
|
4
|
+
*
|
|
5
|
+
* `POST /nylorun/v1/model-calls`
|
|
6
|
+
* - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
|
|
7
|
+
* and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
|
|
8
|
+
* Tenant (a call naming another is refused).
|
|
9
|
+
* - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
|
|
10
|
+
* - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
|
|
11
|
+
* failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
|
|
12
|
+
* token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
|
|
13
|
+
* gate_conflict` when a different request already runs under the same key.
|
|
14
|
+
* - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
|
|
15
|
+
* and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
|
|
16
|
+
* away aborts the call.
|
|
17
|
+
*
|
|
18
|
+
* `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
|
|
19
|
+
* whether or not it exists.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import type { ModelGateOutcome } from "./model-gate.js";
|
|
23
|
+
export declare const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
|
|
24
|
+
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
25
|
+
export declare const TENANT_HEADER = "nylorun-tenant";
|
|
26
|
+
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
27
|
+
export declare const MAX_MODEL_CALL_BYTES: number;
|
|
28
|
+
export declare const ModelCallBodySchema: z.ZodObject<{
|
|
29
|
+
sessionId: z.ZodString;
|
|
30
|
+
turnId: z.ZodString;
|
|
31
|
+
agentId: z.ZodString;
|
|
32
|
+
effectId: z.ZodString;
|
|
33
|
+
invocationId: z.ZodString;
|
|
34
|
+
call: z.ZodObject<{
|
|
35
|
+
prompt: z.ZodArray<z.ZodUnknown>;
|
|
36
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
}, z.core.$loose>>;
|
|
39
|
+
}, z.core.$loose>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
|
|
42
|
+
export interface ModelCallResponse {
|
|
43
|
+
readonly outcome: ModelGateOutcome;
|
|
44
|
+
}
|
|
45
|
+
/** An error answer: `{error: {code, message}}`. */
|
|
46
|
+
export interface GateErrorBody {
|
|
47
|
+
readonly error: {
|
|
48
|
+
readonly code: "gate_unauthorized" | "invalid_request" | "gate_conflict";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
|
|
3
|
+
* it is not in the published OpenAPI documents.
|
|
4
|
+
*
|
|
5
|
+
* `POST /nylorun/v1/model-calls`
|
|
6
|
+
* - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
|
|
7
|
+
* and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
|
|
8
|
+
* Tenant (a call naming another is refused).
|
|
9
|
+
* - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
|
|
10
|
+
* - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
|
|
11
|
+
* failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
|
|
12
|
+
* token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
|
|
13
|
+
* gate_conflict` when a different request already runs under the same key.
|
|
14
|
+
* - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
|
|
15
|
+
* and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
|
|
16
|
+
* away aborts the call.
|
|
17
|
+
*
|
|
18
|
+
* `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
|
|
19
|
+
* whether or not it exists.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
|
|
23
|
+
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
24
|
+
export const TENANT_HEADER = "nylorun-tenant";
|
|
25
|
+
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
26
|
+
export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
|
|
27
|
+
export const ModelCallBodySchema = z.object({
|
|
28
|
+
sessionId: z.string().min(1),
|
|
29
|
+
turnId: z.string().min(1),
|
|
30
|
+
agentId: z.string().min(1),
|
|
31
|
+
effectId: z.string().min(1),
|
|
32
|
+
invocationId: z.string().min(1),
|
|
33
|
+
call: z.looseObject({
|
|
34
|
+
prompt: z.array(z.unknown()),
|
|
35
|
+
tools: z.array(z.looseObject({ name: z.string() })),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Logger } from "../tenant/types.js";
|
|
2
|
+
import type { ModelCallSettings, ModelGate, ModelGateRequest } from "./model-gate.js";
|
|
3
|
+
import { type TenantVaults } from "./tenant-vaults.js";
|
|
4
|
+
export interface ModelCallHandlerOptions {
|
|
5
|
+
readonly vaults: TenantVaults;
|
|
6
|
+
readonly logger: Logger;
|
|
7
|
+
/** Retries and timeouts; `piModel`'s defaults when absent. */
|
|
8
|
+
readonly settings?: ModelCallSettings;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A model call as the gate receives it: the Tenant is optional (the loop of a protocol 4
|
|
12
|
+
* Runtime names it; the gate serves the database's one Tenant either way).
|
|
13
|
+
*/
|
|
14
|
+
export type ModelCallRequest = Omit<ModelGateRequest, "tenantId"> & {
|
|
15
|
+
readonly tenantId?: string;
|
|
16
|
+
};
|
|
17
|
+
/** What the gate's route calls: a `ModelGate` whose request may leave the Tenant out. */
|
|
18
|
+
export interface ModelCallHandler {
|
|
19
|
+
call(request: ModelCallRequest, signal: AbortSignal): ReturnType<ModelGate["call"]>;
|
|
20
|
+
}
|
|
21
|
+
export declare function createModelCallHandler(options: ModelCallHandlerOptions): ModelCallHandler;
|