@nylorun/runtime 0.29.0-beta → 0.31.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 +36 -0
- package/dist/api/components.d.ts +240 -2
- package/dist/api/components.js +3 -1
- package/dist/api/http/routes/agent-manifests.d.ts +2 -1
- package/dist/api/http/routes/agent-manifests.js +62 -10
- package/dist/api/openapi.js +2 -0
- package/dist/gates/egress-credentials.js +2 -13
- package/dist/gates/handler.js +2 -1
- package/dist/gates/in-process.d.ts +9 -0
- package/dist/gates/in-process.js +21 -0
- package/dist/host/http.js +3 -1
- package/dist/host/main.js +2 -0
- package/dist/host/stack-config.d.ts +10 -0
- package/dist/host/stack-config.js +36 -0
- package/dist/management-openapi.json +361 -5
- package/dist/mcp/connect.d.ts +34 -5
- package/dist/mcp/connect.js +75 -14
- 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 +29 -3
- package/dist/openapi.json +654 -30
- package/dist/store/postgres/drizzle/0038_t6_agent_manifest_source.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +21 -2
- package/dist/store/postgres/schema.js +7 -1
- package/dist/store/postgres/store.js +1 -0
- package/dist/store/types.d.ts +14 -0
- package/dist/tenant/agent-manifests.d.ts +18 -25
- package/dist/tenant/agent-manifests.js +136 -4
- package/dist/tenant/agents.js +7 -3
- package/dist/tenant/transcript.d.ts +8 -0
- package/dist/tenant/transcript.js +2 -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
package/dist/vault/service.js
CHANGED
|
@@ -83,7 +83,7 @@ import { canonical } from "../store/canonical.js";
|
|
|
83
83
|
import { decryptSecret, encryptSecret, VaultCryptoError } from "./crypto.js";
|
|
84
84
|
import { VaultError } from "./error.js";
|
|
85
85
|
import { credentialNeeds } from "./coverage.js";
|
|
86
|
-
import { checkCredentialHeaders } from "./headers.js";
|
|
86
|
+
import { checkCredentialHeaders, modelHeaderError } from "./headers.js";
|
|
87
87
|
import { hostModelCatalog, inputsOfHostModel } from "../model/catalog.js";
|
|
88
88
|
import { inputsError } from "../model/capabilities.js";
|
|
89
89
|
import { readAddedInputs } from "../tenant/model-setting.js";
|
|
@@ -247,7 +247,8 @@ export class VaultService {
|
|
|
247
247
|
({ payload, binding } = environmentCredential(name, body));
|
|
248
248
|
else {
|
|
249
249
|
const url = normalizeVaultUrl(body.url);
|
|
250
|
-
|
|
250
|
+
// A workload credential holds no value: the bearer is the Runtime's workload token (#496).
|
|
251
|
+
payload = body.type === "workload" ? {} : payloadOf(body);
|
|
251
252
|
binding = bindingOf(url, payload, body);
|
|
252
253
|
}
|
|
253
254
|
const kek = this.kek();
|
|
@@ -466,6 +467,8 @@ export class VaultService {
|
|
|
466
467
|
url,
|
|
467
468
|
headers,
|
|
468
469
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
470
|
+
...(binding.toolPrefix === undefined ? {} : { toolPrefix: binding.toolPrefix }),
|
|
471
|
+
...(row.type === "workload" ? { workload: true } : {}),
|
|
469
472
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
470
473
|
};
|
|
471
474
|
}
|
|
@@ -541,15 +544,20 @@ export class VaultService {
|
|
|
541
544
|
const kek = this.kek();
|
|
542
545
|
return this.store.tx((t) => this.replay(t, `host-model:${body.idempotencyKey}`, body, async () => {
|
|
543
546
|
this.validateHostModel(body);
|
|
547
|
+
if (body.auth.type === "workload" && body.provider !== "custom")
|
|
548
|
+
throw new VaultError(400, "The workload token authenticates a custom provider only.");
|
|
544
549
|
await this.ensureHostVault(t);
|
|
545
550
|
const credentialId = hostModelCredentialId(body.provider);
|
|
546
551
|
await this.deleteHostProviderRows(t, body.provider);
|
|
552
|
+
// The workload token is read on every call and never stored (#496).
|
|
547
553
|
const payload = body.auth.type === "api_key"
|
|
548
554
|
? {
|
|
549
555
|
apiKey: body.auth.key,
|
|
550
556
|
...(body.auth.env ? { env: body.auth.env } : {}),
|
|
551
557
|
}
|
|
552
|
-
:
|
|
558
|
+
: body.auth.type === "oauth"
|
|
559
|
+
? { oauth: body.auth }
|
|
560
|
+
: {};
|
|
553
561
|
await this.insertHostCredential(t, kek, {
|
|
554
562
|
id: credentialId,
|
|
555
563
|
provider: body.provider,
|
|
@@ -673,6 +681,7 @@ export class VaultService {
|
|
|
673
681
|
}
|
|
674
682
|
if (!["http:", "https:"].includes(url.protocol))
|
|
675
683
|
throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
|
|
684
|
+
checkModelHeaders(body.settings);
|
|
676
685
|
return;
|
|
677
686
|
}
|
|
678
687
|
if (body.baseUrl)
|
|
@@ -800,8 +809,9 @@ function conflictOf(type, binding, others) {
|
|
|
800
809
|
});
|
|
801
810
|
}
|
|
802
811
|
}
|
|
803
|
-
else if (type === "bearer" || type === "headers") {
|
|
804
|
-
const existing = others.find((row) => (row.type === "bearer" || row.type === "headers"
|
|
812
|
+
else if (type === "bearer" || type === "headers" || type === "workload") {
|
|
813
|
+
const existing = others.find((row) => (row.type === "bearer" || row.type === "headers" || row.type === "workload") &&
|
|
814
|
+
credentialBinding(row).url === binding.url);
|
|
805
815
|
if (existing)
|
|
806
816
|
throw new VaultError(409, `${existing.name} is already bound to ${binding.url}`, {
|
|
807
817
|
code: "credential_conflict",
|
|
@@ -828,6 +838,7 @@ function secretViewOf(row) {
|
|
|
828
838
|
: { url: binding.url },
|
|
829
839
|
...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
|
|
830
840
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
841
|
+
...(binding.toolPrefix === undefined ? {} : { toolPrefix: binding.toolPrefix }),
|
|
831
842
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
832
843
|
createdAt: row.createdAt,
|
|
833
844
|
...(row.rotatedAt ? { rotatedAt: row.rotatedAt } : {}),
|
|
@@ -909,6 +920,23 @@ export function readHostPayload(kek, row, binding) {
|
|
|
909
920
|
plaintext.fill(0);
|
|
910
921
|
}
|
|
911
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* A custom endpoint's `headers` and `callHeaders` (#493): no name the model's auth or the
|
|
925
|
+
* transport sets, and each name once across both maps, whatever its case.
|
|
926
|
+
*/
|
|
927
|
+
function checkModelHeaders(settings) {
|
|
928
|
+
const names = [...Object.keys(settings?.headers ?? {}), ...Object.keys(settings?.callHeaders ?? {})];
|
|
929
|
+
const seen = new Set();
|
|
930
|
+
for (const name of names) {
|
|
931
|
+
const refused = modelHeaderError(name);
|
|
932
|
+
if (refused)
|
|
933
|
+
throw new VaultError(400, refused);
|
|
934
|
+
const lower = name.toLowerCase();
|
|
935
|
+
if (seen.has(lower))
|
|
936
|
+
throw new VaultError(400, `The model header ${lower} is set twice`);
|
|
937
|
+
seen.add(lower);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
912
940
|
/** The sealed part of a tool credential: a token, or a header map with lower-cased names. */
|
|
913
941
|
function payloadOf(auth) {
|
|
914
942
|
if (auth.type === "bearer")
|
|
@@ -924,12 +952,16 @@ function bindingOf(url, payload, routing) {
|
|
|
924
952
|
return {
|
|
925
953
|
url,
|
|
926
954
|
...(routing.via ? { via: routing.via } : {}),
|
|
955
|
+
...(routing.via && routing.toolPrefix ? { toolPrefix: routing.toolPrefix } : {}),
|
|
927
956
|
...(routing.identity ? { identity: { header: routing.identity.header.toLowerCase() } } : {}),
|
|
928
957
|
...(payload.headers ? { headerNames: sent } : {}),
|
|
929
958
|
};
|
|
930
959
|
}
|
|
931
960
|
/** The headers a credential sends, or undefined when its payload holds none. */
|
|
932
961
|
function sendHeaders(row, payload) {
|
|
962
|
+
// Its bearer is the Runtime's workload token, which `sessionCredentials` adds.
|
|
963
|
+
if (row.type === "workload")
|
|
964
|
+
return {};
|
|
933
965
|
if (row.type === "headers")
|
|
934
966
|
return payload.headers && Object.keys(payload.headers).length > 0
|
|
935
967
|
? { ...payload.headers }
|
|
@@ -999,7 +1031,12 @@ function rebind(name, slot, row, payload, binding) {
|
|
|
999
1031
|
return undefined;
|
|
1000
1032
|
const sealed = payloadOf(auth);
|
|
1001
1033
|
const routing = slot;
|
|
1002
|
-
|
|
1034
|
+
// The gateway's tool prefix is the vault's, not the slot's: it stays while the slot keeps `via`.
|
|
1035
|
+
return {
|
|
1036
|
+
type,
|
|
1037
|
+
payload: sealed,
|
|
1038
|
+
binding: bindingOf(normalizeVaultUrl(routing.url), sealed, { ...routing, toolPrefix: binding.toolPrefix ?? null }),
|
|
1039
|
+
};
|
|
1003
1040
|
}
|
|
1004
1041
|
catch (error) {
|
|
1005
1042
|
if (error instanceof VaultError)
|
package/dist/vault/sources.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* and Tool Gate). The session comes from its row, never from the harness.
|
|
9
9
|
*/
|
|
10
10
|
import { isSubject } from "@nylorun/core/contracts";
|
|
11
|
+
import { workloadOriginRefused, workloadToken, WorkloadTokenUnavailable } from "./workload-token.js";
|
|
11
12
|
/**
|
|
12
13
|
* The credential of one request made for `session`: the answer of its Environment's vault
|
|
13
14
|
* (`VaultService.authorize`). A credential with an identity header (R2b C2)
|
|
@@ -16,11 +17,12 @@ import { isSubject } from "@nylorun/core/contracts";
|
|
|
16
17
|
* person (the reserved `installation`) sends none (Q3).
|
|
17
18
|
*/
|
|
18
19
|
export async function sessionCredentials(vault, session, request) {
|
|
19
|
-
const
|
|
20
|
+
const authorized = await vault.authorize({
|
|
20
21
|
sessionId: session.id,
|
|
21
22
|
...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
|
|
22
23
|
url: request.url,
|
|
23
24
|
});
|
|
25
|
+
const result = authorized.status === "authorized" && authorized.workload ? await withWorkloadToken(authorized) : authorized;
|
|
24
26
|
if (result.status !== "authorized" || !result.identity || !isSubject(session.ownerUserId))
|
|
25
27
|
return result;
|
|
26
28
|
return {
|
|
@@ -28,3 +30,22 @@ export async function sessionCredentials(vault, session, request) {
|
|
|
28
30
|
headers: { ...result.headers, [result.identity.header]: session.ownerUserId },
|
|
29
31
|
};
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A `workload` credential's request with the Runtime's workload token as its bearer, read now
|
|
35
|
+
* (#496), or refused: when `via`'s origin is not one the operator lists, or the token cannot be
|
|
36
|
+
* read. Never sent without it.
|
|
37
|
+
*/
|
|
38
|
+
async function withWorkloadToken(result) {
|
|
39
|
+
const refused = (reason) => ({ status: "refused", url: result.url, credentialIds: [], reason });
|
|
40
|
+
const origin = result.via === undefined ? "a workload credential needs via" : workloadOriginRefused(result.via);
|
|
41
|
+
if (origin !== undefined)
|
|
42
|
+
return refused(origin);
|
|
43
|
+
try {
|
|
44
|
+
return { ...result, headers: { ...result.headers, authorization: `Bearer ${await workloadToken()}` } };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof WorkloadTokenUnavailable)
|
|
48
|
+
return refused(error.message);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface WorkloadTokenSettings {
|
|
2
|
+
/** The token's file (`NYLORUN_WORKLOAD_TOKEN_FILE`). */
|
|
3
|
+
readonly file?: string;
|
|
4
|
+
/** The origins a `workload` credential's `via` may name (`NYLORUN_WORKLOAD_TOKEN_ORIGINS`). */
|
|
5
|
+
readonly origins?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/** Why the workload token could not be used: the message names the setting, never the token. */
|
|
8
|
+
export declare class WorkloadTokenUnavailable extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/** Sets where this process reads the workload token and where it may send it (at startup). */
|
|
12
|
+
export declare function useWorkloadToken(next: WorkloadTokenSettings): void;
|
|
13
|
+
/** The workload token, read from its file now. */
|
|
14
|
+
export declare function workloadToken(): Promise<string>;
|
|
15
|
+
/** Why the workload token may not go to `via`, or undefined when its origin is listed. */
|
|
16
|
+
export declare function workloadOriginRefused(via: string): string | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Runtime's workload token (#496): a short-lived token a platform gives each installation and
|
|
3
|
+
* rotates through a file, so a gateway trusts the workload instead of a stored key. The file
|
|
4
|
+
* (`NYLORUN_WORKLOAD_TOKEN_FILE`) is read on every use and nothing of it is kept, so a rotation
|
|
5
|
+
* applies to the next request with no restart. A missing, unreadable or empty file fails the
|
|
6
|
+
* request, never sends it unauthenticated.
|
|
7
|
+
*
|
|
8
|
+
* Only the gate side reads it: the model call and the MCP and HTTP tool requests a `workload`
|
|
9
|
+
* credential makes. It never reaches a sandbox (`NYLORUN_*` is not passed to one), the model, an
|
|
10
|
+
* event or a log, and requests scrub it like any credential value. An Agent's writer chooses a
|
|
11
|
+
* credential's `via`, so the token goes only to the origins the operator lists
|
|
12
|
+
* (`NYLORUN_WORKLOAD_TOKEN_ORIGINS`), checked on every use; the model's `baseUrl` is the
|
|
13
|
+
* operator's own.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
/** Why the workload token could not be used: the message names the setting, never the token. */
|
|
17
|
+
export class WorkloadTokenUnavailable extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "WorkloadTokenUnavailable";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let settings = {};
|
|
24
|
+
/** Sets where this process reads the workload token and where it may send it (at startup). */
|
|
25
|
+
export function useWorkloadToken(next) {
|
|
26
|
+
settings = { ...next };
|
|
27
|
+
}
|
|
28
|
+
/** The workload token, read from its file now. */
|
|
29
|
+
export async function workloadToken() {
|
|
30
|
+
const file = settings.file;
|
|
31
|
+
if (file === undefined)
|
|
32
|
+
throw new WorkloadTokenUnavailable("The Runtime has no workload token: set NYLORUN_WORKLOAD_TOKEN_FILE");
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = await readFile(file, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const code = error.code ?? "unreadable";
|
|
39
|
+
throw new WorkloadTokenUnavailable(`The workload token file ${file} (NYLORUN_WORKLOAD_TOKEN_FILE) cannot be read: ${code}`);
|
|
40
|
+
}
|
|
41
|
+
const token = text.trim();
|
|
42
|
+
if (token.length === 0)
|
|
43
|
+
throw new WorkloadTokenUnavailable(`The workload token file ${file} (NYLORUN_WORKLOAD_TOKEN_FILE) is empty`);
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
/** Why the workload token may not go to `via`, or undefined when its origin is listed. */
|
|
47
|
+
export function workloadOriginRefused(via) {
|
|
48
|
+
let origin;
|
|
49
|
+
try {
|
|
50
|
+
origin = new URL(via).origin;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return `${via} is not a URL`;
|
|
54
|
+
}
|
|
55
|
+
return (settings.origins ?? []).includes(origin)
|
|
56
|
+
? undefined
|
|
57
|
+
: `The workload token may not go to ${origin}: list it in NYLORUN_WORKLOAD_TOKEN_ORIGINS`;
|
|
58
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.31.0-beta";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
|
|
2
2
|
// release:prepare also rewrites this when it bumps @nylorun/runtime.
|
|
3
|
-
export const RUNTIME_VERSION = "0.
|
|
3
|
+
export const RUNTIME_VERSION = "0.31.0-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"@hono/node-server": "^2.1.5",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.29.
|
|
68
|
+
"@nylorun/core": "0.27.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.29.3-beta",
|
|
70
70
|
"@restatedev/restate-sdk": "1.17.3",
|
|
71
71
|
"@restatedev/restate-sdk-clients": "1.17.3",
|
|
72
72
|
"@s2-dev/streamstore": "0.28.0",
|