@nylorun/runtime 0.28.0-beta → 0.30.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 +47 -1
- package/README.md +1 -1
- package/dist/api/components.d.ts +109 -2
- package/dist/api/http/define.d.ts +4 -0
- package/dist/api/http/define.js +20 -9
- package/dist/api/http/routes/agent-manifests.d.ts +1 -1
- package/dist/api/http/routes/agent-manifests.js +1 -1
- package/dist/api/http/routes/agents.js +6 -6
- package/dist/api/http/routes/reads.js +29 -4
- package/dist/api/http/routes/tenant.js +5 -2
- package/dist/api/http/routes/triggers.js +15 -12
- package/dist/api/openapi.js +3 -3
- package/dist/gates/egress-credentials.js +2 -13
- package/dist/gates/handler.js +2 -1
- package/dist/gates/http-tool.js +1 -1
- package/dist/gates/in-process.d.ts +9 -0
- package/dist/gates/in-process.js +21 -0
- package/dist/gates/meter.js +16 -0
- package/dist/gates/tenant-vaults.d.ts +1 -1
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +3 -0
- package/dist/host/stack-config.d.ts +10 -0
- package/dist/host/stack-config.js +36 -0
- package/dist/management-openapi.json +492 -80
- package/dist/mcp/connect.d.ts +36 -7
- package/dist/mcp/connect.js +76 -15
- package/dist/mcp/gateway-outcomes.d.ts +14 -0
- package/dist/mcp/gateway-outcomes.js +46 -0
- package/dist/mcp/pool.js +5 -0
- package/dist/model/call-headers.d.ts +15 -0
- package/dist/model/call-headers.js +9 -0
- package/dist/model/models.d.ts +7 -0
- package/dist/model/models.js +3 -0
- package/dist/model/pi-model.d.ts +6 -0
- package/dist/model/pi-model.js +36 -3
- package/dist/openapi.json +331 -215
- package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/reads.js +31 -1
- package/dist/store/postgres/schema.d.ts +78 -5
- package/dist/store/postgres/schema.js +12 -3
- package/dist/tenant/agents.js +7 -3
- package/dist/tenant/transcript.d.ts +8 -0
- package/dist/tenant/transcript.js +2 -0
- package/dist/tenant/types.d.ts +5 -0
- package/dist/vault/headers.d.ts +8 -0
- package/dist/vault/headers.js +25 -0
- package/dist/vault/host-model.js +8 -5
- package/dist/vault/service.d.ts +12 -5
- package/dist/vault/service.js +43 -6
- package/dist/vault/sources.js +22 -1
- package/dist/vault/workload-token.d.ts +16 -0
- package/dist/vault/workload-token.js +58 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -307,7 +307,10 @@ export function tenantRoutes(api) {
|
|
|
307
307
|
path: "/v1/tenant/tool-policy",
|
|
308
308
|
tags: ["Tenant"],
|
|
309
309
|
summary: "Get the Tenant's tool policy",
|
|
310
|
-
responses: {
|
|
310
|
+
responses: {
|
|
311
|
+
200: json(ToolPolicy, "The rules the Tool Gate applies to every MCP and HTTP tool call"),
|
|
312
|
+
409: { description: "A stored rule names an MCP tool by its model-facing `server__tool` name: every call is refused until the policy is set again" },
|
|
313
|
+
},
|
|
311
314
|
}, async (c) => {
|
|
312
315
|
try {
|
|
313
316
|
return jsonResponse(200, await c.env.tenant.store.tx((t) => readToolPolicy(t)));
|
|
@@ -325,7 +328,7 @@ export function tenantRoutes(api) {
|
|
|
325
328
|
path: "/v1/tenant/tool-policy",
|
|
326
329
|
tags: ["Tenant"],
|
|
327
330
|
summary: "Set the Tenant's tool policy",
|
|
328
|
-
description: "Replaces every rule. The Tool Gate applies them to each MCP and HTTP tool call of every harness, before it sends the call: the strictest rule that matches decides. `deny` refuses the call (the model gets `tool.denied`), `ask` holds it and pauses the turn until a person answers it with an `approve` command on the session, `allow` runs it. A call no rule matches runs. A rule matches when every selector it sets matches: `tool` (the name the model calls it by; `*` matches any run of characters), `server` (an MCP server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it) and `when` (fields of the call's input by dotted path, each tested with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). Sandbox tools never cross the gate. An empty list allows every call.",
|
|
331
|
+
description: "Replaces every rule. The Tool Gate applies them to each MCP and HTTP tool call of every harness, before it sends the call: the strictest rule that matches decides. `deny` refuses the call (the model gets `tool.denied`), `ask` holds it and pauses the turn until a person answers it with an `approve` command on the session, `allow` runs it. A call no rule matches runs. A rule matches when every selector it sets matches: `tool` (the tool's identity: an MCP tool's own name on its `server`, or an HTTP tool's declared name, never the name the model calls it by; `*` matches any run of characters), `server` (an MCP server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id) and `when` (fields of the call's input by dotted path, each tested with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). A `tool` with `__` in it, a model-facing `server__tool` name, is refused (`400`): name an MCP tool by `server` and `tool`. A stored policy with such a rule refuses every call until it is set again. Sandbox tools never cross the gate. An empty list allows every call.",
|
|
329
332
|
request: { body: body(PutToolPolicyRequest) },
|
|
330
333
|
responses: { 200: json(ToolPolicy, "The policy now in force") },
|
|
331
334
|
}, async (c) => {
|
|
@@ -44,7 +44,7 @@ export function triggerRoutes(api) {
|
|
|
44
44
|
}, async (c) => {
|
|
45
45
|
const id = idOf(c.req.param("agentId"));
|
|
46
46
|
assertReaches(c.get("scope"), id);
|
|
47
|
-
return jsonResponse(200, await listTriggers(c.env.tenant, id, originOf(c
|
|
47
|
+
return jsonResponse(200, await listTriggers(c.env.tenant, id, originOf(c)));
|
|
48
48
|
});
|
|
49
49
|
for (const action of ["pause", "resume"])
|
|
50
50
|
tenantRoute(api, WRITE, {
|
|
@@ -63,7 +63,7 @@ export function triggerRoutes(api) {
|
|
|
63
63
|
}, async (c) => {
|
|
64
64
|
const id = idOf(c.req.param("agentId"));
|
|
65
65
|
assertReaches(c.get("scope"), id);
|
|
66
|
-
return jsonResponse(200, await setTriggerStatus(c.env.tenant, id, c.req.param("triggerId"), action, originOf(c
|
|
66
|
+
return jsonResponse(200, await setTriggerStatus(c.env.tenant, id, c.req.param("triggerId"), action, originOf(c)));
|
|
67
67
|
});
|
|
68
68
|
tenantRoute(api, WRITE, {
|
|
69
69
|
method: "post",
|
|
@@ -111,7 +111,7 @@ export function triggerRoutes(api) {
|
|
|
111
111
|
}
|
|
112
112
|
/** A delivery: no credential, no protocol header, any body; the endpoint id and the signature are the grant. */
|
|
113
113
|
const DELIVERY = {
|
|
114
|
-
credentials: [
|
|
114
|
+
credentials: [],
|
|
115
115
|
scopes: "any",
|
|
116
116
|
anonymous: true,
|
|
117
117
|
unversioned: true,
|
|
@@ -132,20 +132,20 @@ export function webhookRoutes(api) {
|
|
|
132
132
|
}, async (c) => {
|
|
133
133
|
const id = idOf(c.req.param("agentId"));
|
|
134
134
|
assertReaches(c.get("scope"), id);
|
|
135
|
-
return jsonResponse(200, await rotateWebhookSecret(c.env.tenant, id, c.req.param("triggerId"), originOf(c
|
|
135
|
+
return jsonResponse(200, await rotateWebhookSecret(c.env.tenant, id, c.req.param("triggerId"), originOf(c)));
|
|
136
136
|
});
|
|
137
137
|
tenantRoute(api, DELIVERY, {
|
|
138
138
|
method: "post",
|
|
139
139
|
path: "/v1/webhooks/{endpointId}",
|
|
140
140
|
tags: ["Triggers"],
|
|
141
141
|
summary: "Deliver to a webhook trigger",
|
|
142
|
-
description: "A sender's delivery, signed with Standard Webhooks (`webhook-id`, `webhook-timestamp` within 5 minutes by default, `webhook-signature`): no credential and no `Nylorun-Protocol`. The body, any media type, at most the Runtime limit `webhookBodyBytes`, is read as bytes and verified before anything else. It runs the trigger once per `webhook-id`: a retry answers the first run. The body reaches the model as data under the trigger's prompt. Other senders (GitHub, Stripe, Slack) are relayed by an app.",
|
|
142
|
+
description: "A sender's delivery, signed with Standard Webhooks (`webhook-id`, `webhook-timestamp` within 5 minutes by default, `webhook-signature`): no credential (an `Authorization` header is ignored) and no `Nylorun-Protocol`. The body, any media type, at most the Runtime limit `webhookBodyBytes`, is read as bytes and verified before anything else. It runs the trigger once per `webhook-id`: a retry answers the first run. The body reaches the model as data under the trigger's prompt. Other senders (GitHub, Stripe, Slack) are relayed by an app.",
|
|
143
143
|
request: {
|
|
144
144
|
params: z.object({ endpointId: z.string().meta({ description: "The trigger's endpoint id, `wh_…`" }) }),
|
|
145
145
|
headers: z.object({
|
|
146
|
-
"webhook-id": z.string().
|
|
147
|
-
"webhook-timestamp": z.string().
|
|
148
|
-
"webhook-signature": z.string().
|
|
146
|
+
"webhook-id": z.string().meta({ description: "The delivery's id; a retry sends the same" }),
|
|
147
|
+
"webhook-timestamp": z.string().meta({ description: "Seconds since the epoch" }),
|
|
148
|
+
"webhook-signature": z.string().meta({ description: "`v1,<base64>`, space-separated" }),
|
|
149
149
|
}),
|
|
150
150
|
body: {
|
|
151
151
|
required: true,
|
|
@@ -155,7 +155,7 @@ export function webhookRoutes(api) {
|
|
|
155
155
|
},
|
|
156
156
|
responses: {
|
|
157
157
|
202: json(WebhookAcceptedResponse, "The run it made: started, queued, skipped or failed"),
|
|
158
|
-
401: { description: "
|
|
158
|
+
401: { description: "Unsigned, not signed with the endpoint's secret, or a timestamp out of tolerance" },
|
|
159
159
|
404: { description: "No endpoint has this id" },
|
|
160
160
|
409: { description: "The trigger is paused (`trigger_paused`)" },
|
|
161
161
|
413: { description: "Over `webhookBodyBytes`" },
|
|
@@ -174,7 +174,10 @@ export function webhookRoutes(api) {
|
|
|
174
174
|
}));
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Where its webhook URLs live: the Runtime's public URL (`NYLORUN_PUBLIC_URL`), else the origin
|
|
179
|
+
* the request reached, which behind a proxy that rewrites `Host` is the Runtime's own.
|
|
180
|
+
*/
|
|
181
|
+
function originOf(c) {
|
|
182
|
+
return c.env.tenant.config.publicUrl ?? new URL(c.req.url).origin;
|
|
180
183
|
}
|
package/dist/api/openapi.js
CHANGED
|
@@ -112,7 +112,7 @@ const RUNTIME_TAGS = [
|
|
|
112
112
|
{
|
|
113
113
|
name: "Agent manifests",
|
|
114
114
|
group: "Agents",
|
|
115
|
-
description: "Named, versioned agent manifests (protocol 16): one document with what runs inside an agent's loop (`harness`) and what the Runtime provides around it (`runtime`), checked against each other when it is saved. Save one with a plan first (`?dryRun=true`) and `If-Match`; an Agent
|
|
115
|
+
description: "Named, versioned agent manifests (protocol 16): one document with what runs inside an agent's loop (`harness`) and what the Runtime provides around it (`runtime`), checked against each other when it is saved. Save one with a plan first (`?dryRun=true`) and `If-Match`; an Agent runs one version at a time, and a save moves every Agent that tracks the manifest to the new version, while a pinned one stays. `client.agentManifests` in `@nylorun/agents`, and `nylorun apply`.",
|
|
116
116
|
operations: [
|
|
117
117
|
"PUT /v1/agent-manifests/{name}",
|
|
118
118
|
"GET /v1/agent-manifests",
|
|
@@ -155,7 +155,7 @@ const RUNTIME_TAGS = [
|
|
|
155
155
|
{
|
|
156
156
|
name: "Triggers",
|
|
157
157
|
group: "Agents",
|
|
158
|
-
description: "What starts an Agent's turns with no client connected
|
|
158
|
+
description: "What starts an Agent's turns with no client connected: the schedules, one-shot times and webhook endpoints its manifest's `runtime.triggers` declare. Each trigger's status is the Agent's: pause and resume it, run it now, make a webhook trigger's signing secret, and read its runs (`started`, `queued`, `skipped` or `failed`, with a reason). In `@nylorun/agents` and `nylorun agents triggers`.",
|
|
159
159
|
operations: [
|
|
160
160
|
"GET /v1/agents/{agentId}/triggers",
|
|
161
161
|
"POST /v1/agents/{agentId}/triggers/{triggerId}/pause",
|
|
@@ -169,7 +169,7 @@ const RUNTIME_TAGS = [
|
|
|
169
169
|
{
|
|
170
170
|
name: "Memory",
|
|
171
171
|
group: "Agents",
|
|
172
|
-
description: "An Agent's memories
|
|
172
|
+
description: "An Agent's memories: Markdown files under `/memories` it keeps across sessions, wakes and upgrades, shaped like Anthropic's memory stores. List and read them, write one whole (with a `content_sha256` precondition), delete one or a folder, and read the version every change leaves, with who made it. In `@nylorun/agents` and `nylorun agents memories`.",
|
|
173
173
|
operations: [
|
|
174
174
|
"GET /v1/agents/{agentId}/memories",
|
|
175
175
|
"GET /v1/agents/{agentId}/memories/{path}",
|
|
@@ -19,26 +19,15 @@ import { createServer } from "node:http";
|
|
|
19
19
|
import { Agent, request } from "node:https";
|
|
20
20
|
import { createSecureContext, TLSSocket } from "node:tls";
|
|
21
21
|
import { hostTuning } from "../tuning.js";
|
|
22
|
+
import { HOP_BY_HOP_HEADERS } from "../vault/headers.js";
|
|
22
23
|
/** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
|
|
23
24
|
export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
24
|
-
/** Headers that describe one hop and are never forwarded. */
|
|
25
|
-
const HOP_BY_HOP = new Set([
|
|
26
|
-
"connection",
|
|
27
|
-
"keep-alive",
|
|
28
|
-
"proxy-authenticate",
|
|
29
|
-
"proxy-authorization",
|
|
30
|
-
"proxy-connection",
|
|
31
|
-
"te",
|
|
32
|
-
"trailer",
|
|
33
|
-
"transfer-encoding",
|
|
34
|
-
"upgrade",
|
|
35
|
-
]);
|
|
36
25
|
/** `rawHeaders` without hop-by-hop headers and `drop`, as lowercase names to values. */
|
|
37
26
|
function forwardable(raw, drop) {
|
|
38
27
|
const headers = {};
|
|
39
28
|
for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
40
29
|
const name = raw[i].toLowerCase();
|
|
41
|
-
if (
|
|
30
|
+
if (HOP_BY_HOP_HEADERS.has(name) || name === drop)
|
|
42
31
|
continue;
|
|
43
32
|
(headers[name] ??= []).push(raw[i + 1]);
|
|
44
33
|
}
|
package/dist/gates/handler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the Tenant's ledger. Logs one line per call, never the prompt, the output or a credential.
|
|
7
7
|
*/
|
|
8
8
|
import { classifyThrown } from "../model/classify.js";
|
|
9
|
-
import { callFiles, inProcessModelGate, modelCallSettings, serve } from "./in-process.js";
|
|
9
|
+
import { callCaller, callFiles, inProcessModelGate, modelCallSettings, serve } from "./in-process.js";
|
|
10
10
|
import { createMeter } from "./meter.js";
|
|
11
11
|
import { GateRefusal } from "./tenant-vaults.js";
|
|
12
12
|
export function createModelCallHandler(options) {
|
|
@@ -28,6 +28,7 @@ export function createModelCallHandler(options) {
|
|
|
28
28
|
// The Tenant's Runtime limits `model*`, read for this call.
|
|
29
29
|
settings: await modelCallSettings(vault.store),
|
|
30
30
|
...(blobs ? { files: callFiles(vault.store, blobs) } : {}),
|
|
31
|
+
caller: callCaller(vault.store),
|
|
31
32
|
});
|
|
32
33
|
outcome = await meter.call(vault.store, call, () => serve(vault.store, gate, call, signal));
|
|
33
34
|
}
|
package/dist/gates/http-tool.js
CHANGED
|
@@ -198,7 +198,7 @@ export async function runHttpTool(tenant, call, options) {
|
|
|
198
198
|
if (authorized.status === "refused")
|
|
199
199
|
return failed("http.credential", `The credential '${http.credential}' was refused: ${authorized.reason}`);
|
|
200
200
|
if (authorized.status === "unauthenticated")
|
|
201
|
-
return failed("http.credential", `The session's
|
|
201
|
+
return failed("http.credential", `The session's Agent holds no credential '${http.credential}' for ${authorized.url}`);
|
|
202
202
|
credential = authorized.headers;
|
|
203
203
|
identity = authorized.identity;
|
|
204
204
|
// A credential with `via` sends the call there, a gateway (R2b C2, Q9): to its one URL, so
|
|
@@ -11,6 +11,7 @@ import type { HostModelSecret } from "../vault/service.js";
|
|
|
11
11
|
import type { ModelGate, ModelGateRequest } from "./model-gate.js";
|
|
12
12
|
import { type FileResolver } from "../artifacts/files.js";
|
|
13
13
|
import type { BlobStore } from "../blob/index.js";
|
|
14
|
+
import type { ModelCaller } from "../model/call-headers.js";
|
|
14
15
|
export interface InProcessModelGateOptions {
|
|
15
16
|
/** The Tenant's home: `piModel` redacts secrets it finds there from failure messages. */
|
|
16
17
|
readonly root?: string;
|
|
@@ -20,6 +21,8 @@ export interface InProcessModelGateOptions {
|
|
|
20
21
|
readonly settings?: ModelCallSettings;
|
|
21
22
|
/** Reads the files a call's prompt names (protocol 6): artifacts, for model-gate. */
|
|
22
23
|
readonly files?: (request: ModelGateRequest) => FileResolver;
|
|
24
|
+
/** Reads who makes a call, for a custom endpoint's per-call headers (#493). */
|
|
25
|
+
readonly caller?: (request: ModelGateRequest) => Promise<ModelCaller | undefined>;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* The files a call names, from the Tenant's artifacts, limited to the call's session: in
|
|
@@ -27,6 +30,12 @@ export interface InProcessModelGateOptions {
|
|
|
27
30
|
* gate's route puts on the request (F5). A request without a session reads no file at all.
|
|
28
31
|
*/
|
|
29
32
|
export declare function callFiles(store: SessionStore, blobs: BlobStore): (request: Pick<ModelGateRequest, "sessionId">) => FileResolver;
|
|
33
|
+
/**
|
|
34
|
+
* Who makes a call, from the Tenant's store: the Agent its session runs on, with its labels, and
|
|
35
|
+
* the session's owner when it is a person. The session is the loop's own in process, and the run
|
|
36
|
+
* token's over HTTP; the request's `agentId` names the manifest's agent, not the Agent.
|
|
37
|
+
*/
|
|
38
|
+
export declare function callCaller(store: SessionStore): (request: Pick<ModelGateRequest, "sessionId">) => Promise<ModelCaller | undefined>;
|
|
30
39
|
/**
|
|
31
40
|
* The in-process gate of an open Tenant: reads its host model from its own vault and records
|
|
32
41
|
* each call in its usage ledger. Only a Runtime without the gates service (embedding, the
|
package/dist/gates/in-process.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createMeter } from "./meter.js";
|
|
|
5
5
|
import { artifactFiles } from "../artifacts/files.js";
|
|
6
6
|
import { usesFixtureModel } from "../tenant/model-setting.js";
|
|
7
7
|
import { fixtureAnswer } from "./fixture-model.js";
|
|
8
|
+
import { isSubject } from "@nylorun/core/contracts";
|
|
8
9
|
/**
|
|
9
10
|
* The files a call names, from the Tenant's artifacts, limited to the call's session: in
|
|
10
11
|
* process, the loop's own request; over HTTP, the session of the run token's claims, which the
|
|
@@ -13,6 +14,24 @@ import { fixtureAnswer } from "./fixture-model.js";
|
|
|
13
14
|
export function callFiles(store, blobs) {
|
|
14
15
|
return (request) => artifactFiles({ store, blobs, sessionId: request.sessionId });
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Who makes a call, from the Tenant's store: the Agent its session runs on, with its labels, and
|
|
19
|
+
* the session's owner when it is a person. The session is the loop's own in process, and the run
|
|
20
|
+
* token's over HTTP; the request's `agentId` names the manifest's agent, not the Agent.
|
|
21
|
+
*/
|
|
22
|
+
export function callCaller(store) {
|
|
23
|
+
return (request) => store.tx(async (t) => {
|
|
24
|
+
const session = await t.get("sessions", request.sessionId);
|
|
25
|
+
const agent = session && (await t.environment(session.agentId));
|
|
26
|
+
if (!session || !agent)
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
agentId: agent.id,
|
|
30
|
+
labels: agent.labels,
|
|
31
|
+
...(isSubject(session.ownerUserId) ? { owner: session.ownerUserId } : {}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
16
35
|
/**
|
|
17
36
|
* The in-process gate of an open Tenant: reads its host model from its own vault and records
|
|
18
37
|
* each call in its usage ledger. Only a Runtime without the gates service (embedding, the
|
|
@@ -30,6 +49,7 @@ export function tenantModelGate(options) {
|
|
|
30
49
|
writeHostCredential: (credential) => vault.updateHostCredential(credential),
|
|
31
50
|
settings: await modelCallSettings(options.store),
|
|
32
51
|
...(options.blobs ? { files: callFiles(options.store, options.blobs) } : {}),
|
|
52
|
+
caller: callCaller(options.store),
|
|
33
53
|
});
|
|
34
54
|
return serve(options.store, gate, request, signal);
|
|
35
55
|
}),
|
|
@@ -58,6 +78,7 @@ export function inProcessModelGate(options) {
|
|
|
58
78
|
writeHostCredential: options.writeHostCredential,
|
|
59
79
|
...(options.settings ? { settings: options.settings } : {}),
|
|
60
80
|
...(options.files ? { files: options.files(request) } : {}),
|
|
81
|
+
...(options.caller ? { caller: () => options.caller(request) } : {}),
|
|
61
82
|
});
|
|
62
83
|
return adapter(request.call, {
|
|
63
84
|
// The Runtime never journals the provider request (harness `durable.ts`), and
|
package/dist/gates/meter.js
CHANGED
|
@@ -142,6 +142,22 @@ export function usageRow(request, outcome, at, timing) {
|
|
|
142
142
|
...(typeof firstTokenMs === "number" && Number.isFinite(firstTokenMs) && firstTokenMs >= 0
|
|
143
143
|
? { firstTokenMs: Math.round(firstTokenMs) }
|
|
144
144
|
: {}),
|
|
145
|
+
...costSplitColumns(outcome.evidence?.extras?.costSplit),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The ledger's cost-by-kind columns from the adapter's `costSplit`; none unless all four are costs. */
|
|
149
|
+
function costSplitColumns(split) {
|
|
150
|
+
if (!split || typeof split !== "object" || Array.isArray(split))
|
|
151
|
+
return {};
|
|
152
|
+
const { input, cacheRead, cacheWrite, output } = split;
|
|
153
|
+
const parts = [input, cacheRead, cacheWrite, output];
|
|
154
|
+
if (!parts.every((part) => typeof part === "number" && Number.isFinite(part) && part >= 0))
|
|
155
|
+
return {};
|
|
156
|
+
return {
|
|
157
|
+
costInputUsd: input,
|
|
158
|
+
costCacheReadUsd: cacheRead,
|
|
159
|
+
costCacheWriteUsd: cacheWrite,
|
|
160
|
+
costOutputUsd: output,
|
|
145
161
|
};
|
|
146
162
|
}
|
|
147
163
|
/** The ledger rows a budget counts for `request`; undefined when it doesn't apply. */
|
|
@@ -29,7 +29,7 @@ export interface TenantVault {
|
|
|
29
29
|
session(sessionId: string): Promise<Session | undefined>;
|
|
30
30
|
/**
|
|
31
31
|
* The authorization of one request to a session's remote MCP server or HTTP tool: its
|
|
32
|
-
* credential from the vault of the session's
|
|
32
|
+
* credential from the vault of the session's Agent.
|
|
33
33
|
*/
|
|
34
34
|
authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
35
35
|
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
@@ -14,4 +14,6 @@ export declare function configForFactory(options: {
|
|
|
14
14
|
delivery?: TenantConfig["delivery"];
|
|
15
15
|
/** The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`). */
|
|
16
16
|
issuers?: TenantConfig["issuers"];
|
|
17
|
+
/** The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`). */
|
|
18
|
+
publicUrl?: string;
|
|
17
19
|
}): (id: string) => TenantConfig;
|
package/dist/host/config-for.js
CHANGED
|
@@ -16,6 +16,7 @@ export function configForFactory(options) {
|
|
|
16
16
|
model: options.model ?? { kind: "vault" },
|
|
17
17
|
...(options.delivery ? { delivery: options.delivery } : {}),
|
|
18
18
|
...(options.issuers ? { issuers: options.issuers } : {}),
|
|
19
|
+
...(options.publicUrl ? { publicUrl: options.publicUrl } : {}),
|
|
19
20
|
logger: {
|
|
20
21
|
info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
|
|
21
22
|
warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
|
package/dist/host/main.js
CHANGED
|
@@ -65,6 +65,7 @@ import { startHarnessListener } from "../harness-api/ws-server.js";
|
|
|
65
65
|
import { httpSandboxesClient } from "../sandbox/pods/client.js";
|
|
66
66
|
import { retry } from "../retry.js";
|
|
67
67
|
import { hostTuning, useHostTuning } from "../tuning.js";
|
|
68
|
+
import { useWorkloadToken } from "../vault/workload-token.js";
|
|
68
69
|
const nodeRequire = createRequire(import.meta.url);
|
|
69
70
|
function coreVersion() {
|
|
70
71
|
try {
|
|
@@ -218,6 +219,7 @@ export async function main() {
|
|
|
218
219
|
// Read once, before any service starts: each reads the parts it uses where it uses them.
|
|
219
220
|
if (stack.tuning)
|
|
220
221
|
useHostTuning(stack.tuning.values, stack.tuning.overrides);
|
|
222
|
+
useWorkloadToken(stack.workloadToken ?? {});
|
|
221
223
|
if (stack.services.has("gates") || stack.services.has("keys") || stack.services.has("egress"))
|
|
222
224
|
return runGates(stack);
|
|
223
225
|
if (stack.services.has("harness"))
|
|
@@ -279,6 +281,7 @@ export async function main() {
|
|
|
279
281
|
logger,
|
|
280
282
|
...(stack.delivery ? { delivery: stack.delivery } : {}),
|
|
281
283
|
...(issuers ? { issuers } : {}),
|
|
284
|
+
...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
|
|
282
285
|
});
|
|
283
286
|
// The process's Durable Session Execution: Restate when its endpoints are set, else the
|
|
284
287
|
// in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
|
|
@@ -178,6 +178,16 @@ export interface HarnessServiceConfig {
|
|
|
178
178
|
export interface StackConfig {
|
|
179
179
|
/** The Runtime services this process runs. */
|
|
180
180
|
services: RuntimeServices;
|
|
181
|
+
/**
|
|
182
|
+
* `NYLORUN_WORKLOAD_TOKEN_FILE`, the file of the Runtime's workload token, which a platform
|
|
183
|
+
* rotates and the Runtime reads at each use, and `NYLORUN_WORKLOAD_TOKEN_ORIGINS`, the
|
|
184
|
+
* comma-separated origins a `workload` credential's `via` may send it to (#496,
|
|
185
|
+
* `vault/workload-token.ts`). Absent: no workload token.
|
|
186
|
+
*/
|
|
187
|
+
workloadToken?: {
|
|
188
|
+
file?: string;
|
|
189
|
+
origins?: string[];
|
|
190
|
+
};
|
|
181
191
|
/** Present when the process runs gates. */
|
|
182
192
|
gates?: GatesConfig;
|
|
183
193
|
/** Present when the process runs egress. */
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
import { DEFAULT_SERVICE_NAME } from "../traces/otlp.js";
|
|
48
48
|
import { TRACE_CONTENT_KINDS } from "../traces/map.js";
|
|
49
49
|
import { isTenantId } from "@nylorun/core/compatibility";
|
|
50
|
+
import { LOOPBACK_HOSTS } from "@nylorun/core/contracts";
|
|
50
51
|
import { blockedAddresses } from "../sandbox/pods/network-gate.js";
|
|
51
52
|
import { HostTuningError, parseHostTuning } from "../tuning.js";
|
|
52
53
|
export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
|
|
@@ -325,6 +326,7 @@ export function parseStackConfig(env, argv) {
|
|
|
325
326
|
throw new StackConfigError("NYLORUN_HARNESS_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT");
|
|
326
327
|
const delivery = parseDelivery(env);
|
|
327
328
|
const tenant = servesApi ? parseTenant(env) : undefined;
|
|
329
|
+
const workloadToken = parseWorkloadToken(env);
|
|
328
330
|
const objectStore = parseObjectStore(env);
|
|
329
331
|
const traces = services.has("core") ? parseTraces(env) : undefined;
|
|
330
332
|
const removedResolverVariables = Object.keys(env)
|
|
@@ -347,6 +349,7 @@ export function parseStackConfig(env, argv) {
|
|
|
347
349
|
...(sandboxes ? { sandboxes } : {}),
|
|
348
350
|
...(delivery ? { delivery } : {}),
|
|
349
351
|
...(tenant ? { tenant } : {}),
|
|
352
|
+
...(workloadToken ? { workloadToken } : {}),
|
|
350
353
|
...(objectStore ? { objectStore } : {}),
|
|
351
354
|
...(traces ? { traces } : {}),
|
|
352
355
|
...(tuning ? { tuning } : {}),
|
|
@@ -454,6 +457,39 @@ function parseTenant(env) {
|
|
|
454
457
|
...(managementKeyFile ? { managementKeyFile } : {}),
|
|
455
458
|
};
|
|
456
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* `StackConfig.workloadToken` (#496): the Runtime's workload token, which the gates read for a
|
|
462
|
+
* custom model and for `workload` credentials, and the in-process Tenant does without them.
|
|
463
|
+
*/
|
|
464
|
+
function parseWorkloadToken(env) {
|
|
465
|
+
const file = read(env, "NYLORUN_WORKLOAD_TOKEN_FILE");
|
|
466
|
+
const origins = parseOrigins(read(env, "NYLORUN_WORKLOAD_TOKEN_ORIGINS"));
|
|
467
|
+
if (file === undefined && origins === undefined)
|
|
468
|
+
return undefined;
|
|
469
|
+
return { ...(file ? { file } : {}), ...(origins ? { origins } : {}) };
|
|
470
|
+
}
|
|
471
|
+
/** `NYLORUN_WORKLOAD_TOKEN_ORIGINS`: each an `https` origin (`http` only to a loopback host). */
|
|
472
|
+
function parseOrigins(raw) {
|
|
473
|
+
if (raw === undefined)
|
|
474
|
+
return undefined;
|
|
475
|
+
return raw
|
|
476
|
+
.split(",")
|
|
477
|
+
.map((item) => item.trim())
|
|
478
|
+
.filter((item) => item !== "")
|
|
479
|
+
.map((item) => {
|
|
480
|
+
let url;
|
|
481
|
+
try {
|
|
482
|
+
url = new URL(item);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
throw new StackConfigError(`NYLORUN_WORKLOAD_TOKEN_ORIGINS: ${item} is not a URL`);
|
|
486
|
+
}
|
|
487
|
+
const loopback = url.protocol === "http:" && LOOPBACK_HOSTS.test(url.hostname);
|
|
488
|
+
if ((url.protocol !== "https:" && !loopback) || url.origin !== item.replace(/\/$/, ""))
|
|
489
|
+
throw new StackConfigError(`NYLORUN_WORKLOAD_TOKEN_ORIGINS: ${item} must be an https origin, such as https://gateway.example.com`);
|
|
490
|
+
return url.origin;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
457
493
|
const GATES_TOKEN = /^[0-9a-f]{64,}$/i;
|
|
458
494
|
/** `NYLORUN_HARNESS`: `in-process` (the default when unset) or `remote`. */
|
|
459
495
|
function parseHarnessMode(env) {
|