@nylorun/runtime 0.29.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.
@@ -7,6 +7,7 @@ import { MCP_TOOL_DEFAULTS, mcpToolSettings } from "@nylorun/core/define";
7
7
  import { scrub, scrubValues } from "../redact.js";
8
8
  import { guardedFetch, OutboundFailed, OutboundRefused, } from "../tenant/outbound.js";
9
9
  import { credentialSecrets } from "../vault/headers.js";
10
+ import { gatewayAuthFailure } from "./gateway-outcomes.js";
10
11
  import { hostTuning } from "../tuning.js";
11
12
  /** A tool call that failed with a code the model can act on (R2b C7). */
12
13
  export class McpCallFailed extends Error {
@@ -22,7 +23,7 @@ export class McpCallFailed extends Error {
22
23
  /** The failure the gate answered, as the class the loop's own call would have thrown. */
23
24
  static from(failure) {
24
25
  return failure.code === "credential_rejected" && failure.server !== undefined
25
- ? new CredentialRejected(failure.server, !failure.message.includes("holds none"))
26
+ ? new CredentialRejected(failure.server, !failure.message.includes("holds none"), undefined, failure.gateway)
26
27
  : new McpCallFailed(failure);
27
28
  }
28
29
  /** The failed tool outcome the model sees. */
@@ -34,13 +35,15 @@ export class McpCallFailed extends Error {
34
35
  message: failure.message,
35
36
  retryable: failure.retryable,
36
37
  ...(failure.server === undefined ? {} : { server: failure.server }),
38
+ ...(failure.gateway === undefined ? {} : { gateway: failure.gateway }),
37
39
  ...(failure.redacted ? { redacted: failure.redacted } : {}),
38
40
  };
39
41
  }
40
42
  }
41
43
  /**
42
44
  * A request the server answered `401` (R2b C1): it rejected the vault's credential, or wanted one
43
- * and got none. Never retried, since nothing the gate holds changes between tries (Q4). A tool
45
+ * and got none. Through a credential's `via`, also a gateway's answer reporting that the server
46
+ * behind it did (`gateway-outcomes.ts`, #495). Never retried, since nothing the gate holds changes between tries (Q4). A tool
44
47
  * call that meets it fails with `credential_rejected` for the model to see; at discovery it is the
45
48
  * server's `failed` diagnostic.
46
49
  */
@@ -48,6 +51,7 @@ export class CredentialRejected extends McpCallFailed {
48
51
  server;
49
52
  sent;
50
53
  challenge;
54
+ gateway;
51
55
  constructor(server,
52
56
  /** Whether a credential of the session's Agent was sent. */
53
57
  sent,
@@ -55,19 +59,25 @@ export class CredentialRejected extends McpCallFailed {
55
59
  * The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
56
60
  * (RFC 9728 `resource_metadata`): a tool preview reads it (R2b C12). Never shown to the model.
57
61
  */
58
- challenge) {
62
+ challenge,
63
+ /** The gateway at `via` that refused the Runtime's workload token (#496): an expired or unrotated token. */
64
+ gateway) {
59
65
  super({
60
66
  code: "credential_rejected",
61
- message: sent
62
- ? `The MCP server '${server}' answered HTTP 401: it rejected the credential from the session's Agent`
63
- : `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's Agent holds none for it`,
67
+ message: gateway
68
+ ? `The gateway ${gateway} answered HTTP 401 for the MCP server '${server}': it rejected the Runtime's workload token`
69
+ : sent
70
+ ? `The MCP server '${server}' answered HTTP 401: it rejected the credential from the session's Agent`
71
+ : `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's Agent holds none for it`,
64
72
  sent: true,
65
73
  retryable: false,
66
74
  server,
75
+ ...(gateway === undefined ? {} : { gateway }),
67
76
  });
68
77
  this.server = server;
69
78
  this.sent = sent;
70
79
  this.challenge = challenge;
80
+ this.gateway = gateway;
71
81
  this.name = "CredentialRejected";
72
82
  }
73
83
  }
@@ -121,17 +131,58 @@ function tracked(call, run, options) {
121
131
  * so a server that reads it can run the call once.
122
132
  */
123
133
  export const IDEMPOTENCY_META_KEY = "nylorun/idempotency-key";
134
+ /**
135
+ * Where a `tools/list` page names the tools a gateway listed without the credential's
136
+ * `toolPrefix` (#494): kept under their listed names, for discovery's diagnostic.
137
+ */
138
+ export const UNPREFIXED_TOOLS_META_KEY = "nylorun/unprefixed-tools";
124
139
  /**
125
140
  * An SDK `Client` as an `McpClient`. Its answers and failures carry no credential value sent
126
141
  * with them (R2b C8), and a failed call is an `McpCallFailed` (C7). A keyed call sends its key in
127
- * `_meta` (R5.5).
142
+ * `_meta` (R5.5). Through a `gateway`, an answer that reports the server refused the credential
143
+ * is `CredentialRejected`, as a `401` is (#495), and a gateway's `toolPrefix` is taken off the
144
+ * tools it lists and put back on the calls (#494): everything after this client names the
145
+ * server's own tools.
128
146
  */
129
- export function sdkClient(client, serverName, errorChars) {
147
+ export function sdkClient(client, serverName, errorChars, gateway) {
148
+ const prefix = gateway?.toolPrefix;
149
+ // The gateway may name the server by the prefix's stem (LiteLLM's `github-` for `github`).
150
+ const keys = prefix === undefined ? [serverName] : [serverName, prefix.replace(/[^A-Za-z0-9]+$/, "")];
151
+ const rejected = (answer) => gateway !== undefined && gatewayAuthFailure(answer, keys);
152
+ // The name the gateway listed for each tool, by the server's own name, and the names it listed
153
+ // without the prefix: a call sends the tool's name as the gateway listed it.
154
+ const listedAs = new Map();
155
+ const unprefixed = new Set();
156
+ const listedName = (name) => prefix === undefined || unprefixed.has(name) ? name : (listedAs.get(name) ?? `${prefix}${name}`);
157
+ const stripped = (page) => {
158
+ if (prefix === undefined)
159
+ return page;
160
+ const missing = [];
161
+ const tools = page.tools.map((tool) => {
162
+ if (typeof tool.name === "string" && tool.name.startsWith(prefix) && tool.name.length > prefix.length) {
163
+ const name = tool.name.slice(prefix.length);
164
+ listedAs.set(name, tool.name);
165
+ return { ...tool, name };
166
+ }
167
+ if (typeof tool.name === "string") {
168
+ unprefixed.add(tool.name);
169
+ missing.push(tool.name);
170
+ }
171
+ return tool;
172
+ });
173
+ return {
174
+ ...page,
175
+ tools,
176
+ ...(missing.length === 0 ? {} : { _meta: { ...page._meta, [UNPREFIXED_TOOLS_META_KEY]: missing } }),
177
+ };
178
+ };
130
179
  return {
131
180
  listTools: (params, options) => tracked(false, async (tracker) => {
132
181
  try {
133
182
  const page = await client.listTools(params, options?.signal ? { signal: options.signal } : undefined);
134
- return scrubbed(page, tracker);
183
+ if (rejected(page))
184
+ throw new CredentialRejected(serverName, true);
185
+ return stripped(scrubbed(page, tracker));
135
186
  }
136
187
  catch (error) {
137
188
  throw scrubError(error, tracker);
@@ -144,15 +195,18 @@ export function sdkClient(client, serverName, errorChars) {
144
195
  return tracked(true, async (tracker) => {
145
196
  const lost = tracker.calling.signal;
146
197
  try {
198
+ const named = { ...params, name: listedName(params.name) };
147
199
  const keyed = options?.key === undefined
148
- ? params
149
- : { ...params, _meta: { [IDEMPOTENCY_META_KEY]: options.key } };
200
+ ? named
201
+ : { ...named, _meta: { [IDEMPOTENCY_META_KEY]: options.key } };
150
202
  const result = (await client.callTool(keyed, undefined, {
151
203
  signal: options?.signal ? AbortSignal.any([options.signal, lost]) : lost,
152
204
  }));
153
205
  // An answer the stream did not count (an `sse` server answers on its event stream).
154
206
  if (Buffer.byteLength(JSON.stringify(result)) > maxResultBytes)
155
207
  throw tooLargeFailure(serverName, maxResultBytes);
208
+ if (result.isError === true && rejected(result))
209
+ throw new CredentialRejected(serverName, true);
156
210
  const answer = scrubValues(result, [...tracker.secrets]);
157
211
  return { result: answer.value, ...(answer.redacted > 0 ? { redacted: answer.redacted } : {}) };
158
212
  }
@@ -202,8 +256,11 @@ export async function openMcpServer(input) {
202
256
  serverInfo: info ? scrubbed({ ...info }, tracker) : undefined,
203
257
  };
204
258
  }, { errorChars });
259
+ const gateway = initial.status === "authorized" && initial.via !== undefined
260
+ ? { via: initial.via, ...(initial.toolPrefix === undefined ? {} : { toolPrefix: initial.toolPrefix }) }
261
+ : undefined;
205
262
  return {
206
- client: sdkClient(client, input.server.name, errorChars),
263
+ client: sdkClient(client, input.server.name, errorChars, gateway),
207
264
  ...(instructions === undefined ? {} : { instructions }),
208
265
  ...(serverInfo === undefined ? {} : { serverInfo }),
209
266
  close: () => client.close(),
@@ -227,11 +284,15 @@ export function serverInstructions(text, secrets) {
227
284
  */
228
285
  export async function listMcpTools(client, input) {
229
286
  const listed = [];
287
+ const unprefixed = [];
230
288
  let cursor;
231
289
  const pages = hostTuning().harness.mcpListPages;
232
290
  for (let page = 0; page < pages; page += 1) {
233
291
  const answer = await client.listTools(cursor ? { cursor } : undefined, input.signal ? { signal: input.signal } : undefined);
234
292
  listed.push(...answer.tools);
293
+ const missing = answer._meta?.[UNPREFIXED_TOOLS_META_KEY];
294
+ if (Array.isArray(missing))
295
+ unprefixed.push(...missing.filter((name) => typeof name === "string"));
235
296
  cursor = answer.nextCursor;
236
297
  if (!cursor)
237
298
  break;
@@ -267,7 +328,7 @@ export async function listMcpTools(client, input) {
267
328
  ...(annotations === undefined || Object.keys(annotations).length === 0 ? {} : { annotations }),
268
329
  });
269
330
  }
270
- return { tools, omitted, disabled, unknownTools };
331
+ return { tools, omitted, disabled, unknownTools, unprefixed };
271
332
  }
272
333
  /**
273
334
  * One tool call and what it came to. Rejects with `McpConnectionStale` when the connection must
@@ -413,7 +474,7 @@ function authorizedFetch(server, endpoint, authorize, policy, errorChars) {
413
474
  tracker.sent = true;
414
475
  if (response.status === 401) {
415
476
  await response.body?.cancel().catch(() => undefined);
416
- throw new CredentialRejected(server.name, result.status === "authorized", response.headers.get("www-authenticate") ?? undefined);
477
+ throw new CredentialRejected(server.name, result.status === "authorized", response.headers.get("www-authenticate") ?? undefined, result.status === "authorized" && result.workload ? endpoint.origin : undefined);
417
478
  }
418
479
  // The server ended the connection's session: the tool never saw the request.
419
480
  if (post && response.status === 404 && headers.has("mcp-session-id")) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A gateway's report that the server behind it refused its credential (#495). A gateway that holds
3
+ * people's tokens may answer an upstream `401` with HTTP 200: LiteLLM lists no tools and names the
4
+ * outcome in the answer's `_meta`, and fails a tool call with `isError` and the upstream error's
5
+ * text. Only answers that came through a credential's `via` are read so: a server the Runtime talks
6
+ * to directly says `401` itself.
7
+ */
8
+ /** The names a gateway may give the server under: the manifest's first, then any others. */
9
+ export type GatewayServerKeys = readonly string[];
10
+ /**
11
+ * Whether a `tools/list` page or a `tools/call` result reports that the server behind the
12
+ * gateway refused the credential. A call's text is read only when the result is an error.
13
+ */
14
+ export declare function gatewayAuthFailure(answer: unknown, keys: GatewayServerKeys): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A gateway's report that the server behind it refused its credential (#495). A gateway that holds
3
+ * people's tokens may answer an upstream `401` with HTTP 200: LiteLLM lists no tools and names the
4
+ * outcome in the answer's `_meta`, and fails a tool call with `isError` and the upstream error's
5
+ * text. Only answers that came through a credential's `via` are read so: a server the Runtime talks
6
+ * to directly says `401` itself.
7
+ */
8
+ /** LiteLLM's outcome per server it reached: `{ status: "auth_required", http_status: 401 }`. */
9
+ const LITELLM_SERVER_OUTCOMES = "litellm.ai/server_outcomes";
10
+ /** LiteLLM's text for a tool call the upstream server answered `401` (httpx's error message). */
11
+ const LITELLM_UNAUTHORIZED_TEXT = /HTTPStatusError: Client error '401 Unauthorized'/;
12
+ /**
13
+ * Whether a `tools/list` page or a `tools/call` result reports that the server behind the
14
+ * gateway refused the credential. A call's text is read only when the result is an error.
15
+ */
16
+ export function gatewayAuthFailure(answer, keys) {
17
+ if (!isRecord(answer))
18
+ return false;
19
+ const outcome = serverOutcome(answer._meta, keys);
20
+ if (outcome && (outcome.status === "auth_required" || outcome.http_status === 401))
21
+ return true;
22
+ return answer.isError === true && LITELLM_UNAUTHORIZED_TEXT.test(textOf(answer.content));
23
+ }
24
+ /** The gateway's outcome for this server: the entry one of `keys` names, else the only entry. */
25
+ function serverOutcome(meta, keys) {
26
+ if (!isRecord(meta))
27
+ return undefined;
28
+ const outcomes = meta[LITELLM_SERVER_OUTCOMES];
29
+ if (!isRecord(outcomes))
30
+ return undefined;
31
+ for (const key of keys)
32
+ if (isRecord(outcomes[key]))
33
+ return outcomes[key];
34
+ const entries = Object.values(outcomes);
35
+ return entries.length === 1 && isRecord(entries[0]) ? entries[0] : undefined;
36
+ }
37
+ function textOf(content) {
38
+ if (!Array.isArray(content))
39
+ return "";
40
+ return content
41
+ .map((part) => (isRecord(part) && typeof part.text === "string" ? part.text : ""))
42
+ .join("\n");
43
+ }
44
+ function isRecord(value) {
45
+ return !!value && typeof value === "object" && !Array.isArray(value);
46
+ }
package/dist/mcp/pool.js CHANGED
@@ -52,6 +52,11 @@ export class McpPool {
52
52
  ...(listed.unknownTools.length === 0
53
53
  ? []
54
54
  : [`Its tools settings name tools it does not list: ${listed.unknownTools.join(", ")}`]),
55
+ ...(listed.unprefixed.length === 0
56
+ ? []
57
+ : [
58
+ `${listed.unprefixed.length} of its tools lack the gateway's tool prefix and keep the names it listed: ${listed.unprefixed.join(", ")}`,
59
+ ]),
55
60
  ...(tooLong
56
61
  ? [
57
62
  `Its instructions are ${Buffer.byteLength(text)} bytes, more than a record entry holds (the Runtime limit recordEntryBytes, ${input.maxInstructionBytes} bytes), and are left out`,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A custom endpoint's per-call headers (#493): each `callHeaders` entry's value, from the calling
3
+ * Agent, for a gateway that attributes or budgets each call, such as by the Agent's `org` label.
4
+ * A source with no value (a missing or empty label, a session whose owner is no person) sends no
5
+ * header, never an empty one.
6
+ */
7
+ import type { ModelCallHeaderSource } from "@nylorun/core/contracts";
8
+ /** What a model call knows of who makes it, read for the call. */
9
+ export interface ModelCaller {
10
+ readonly agentId: string;
11
+ readonly labels: Readonly<Record<string, string>>;
12
+ /** The session's owner, when it is a person. */
13
+ readonly owner?: string;
14
+ }
15
+ export declare function callHeaders(sources: Readonly<Record<string, ModelCallHeaderSource>>, caller: ModelCaller): Record<string, string>;
@@ -0,0 +1,9 @@
1
+ export function callHeaders(sources, caller) {
2
+ const headers = {};
3
+ for (const [name, source] of Object.entries(sources)) {
4
+ const value = source.from === "agent.label" ? caller.labels[source.key] : source.from === "agent.id" ? caller.agentId : caller.owner;
5
+ if (value)
6
+ headers[name] = value;
7
+ }
8
+ return headers;
9
+ }
@@ -1,4 +1,5 @@
1
1
  import { type AuthContext, type CredentialStore } from "@earendil-works/pi-ai";
2
+ import { type ModelCallHeaderSource } from "@nylorun/core/contracts";
2
3
  export type Selection = Readonly<{
3
4
  provider: string;
4
5
  model: string;
@@ -11,6 +12,10 @@ export type Selection = Readonly<{
11
12
  /** What the endpoint reads (protocol 12); text only unless it says so. */
12
13
  inputs?: readonly string[];
13
14
  compat?: Readonly<Record<string, unknown>>;
15
+ /** Sent on every call (#493). */
16
+ headers?: Readonly<Record<string, string>>;
17
+ /** Resolved per call from the calling Agent (`call-headers.ts`, #493). */
18
+ callHeaders?: Readonly<Record<string, ModelCallHeaderSource>>;
14
19
  }>;
15
20
  }>;
16
21
  /**
@@ -20,4 +25,6 @@ export type Selection = Readonly<{
20
25
  export declare function modelsFor(selection: Selection, credentials: CredentialStore, options?: {
21
26
  environment?: boolean;
22
27
  authContext?: AuthContext;
28
+ /** A custom endpoint's bearer read at each request instead of a stored key: the workload token (#496). */
29
+ bearer?: () => Promise<string>;
23
30
  }): import("@earendil-works/pi-ai").MutableModels;
@@ -30,6 +30,7 @@ export function modelsFor(selection, credentials, options = {}) {
30
30
  ...(selection.custom.compat
31
31
  ? { compat: { ...selection.custom.compat } }
32
32
  : {}),
33
+ ...(selection.custom.headers ? { headers: { ...selection.custom.headers } } : {}),
33
34
  };
34
35
  models.setProvider(createProvider({
35
36
  id: "custom",
@@ -39,6 +40,8 @@ export function modelsFor(selection, credentials, options = {}) {
39
40
  apiKey: {
40
41
  name: "Custom API key",
41
42
  async resolve() {
43
+ if (options.bearer)
44
+ return { type: "api_key", auth: { apiKey: await options.bearer() } };
42
45
  const credential = await credentials.read("custom");
43
46
  if (!credential || credential.type !== "api_key")
44
47
  return undefined;
@@ -3,6 +3,7 @@ import type { RuntimeModelAdapter } from "../contracts.js";
3
3
  import { type RuntimeLimits } from "@nylorun/core/contracts";
4
4
  import { type FileResolver } from "../artifacts/files.js";
5
5
  import type { HostModelSecret } from "../vault/service.js";
6
+ import { type ModelCaller } from "./call-headers.js";
6
7
  export interface ModelPreview {
7
8
  readonly invocationId: string;
8
9
  readonly text: string;
@@ -39,6 +40,11 @@ export interface PiModelOptions {
39
40
  readonly readHostModel?: () => HostModelSecret | undefined | Promise<HostModelSecret | undefined>;
40
41
  readonly writeHostCredential?: (credential: Credential) => void | Promise<void>;
41
42
  readonly settings?: ModelCallSettings;
43
+ /**
44
+ * Who makes the call: read only when the custom endpoint has `callHeaders` (#493), whose values
45
+ * come from it. Without it, a call sends none of them.
46
+ */
47
+ readonly caller?: () => Promise<ModelCaller | undefined>;
42
48
  }
43
49
  /**
44
50
  * Node model adapter. Local provider configuration is read only when invoked.
@@ -7,6 +7,8 @@ import { scrub } from "../redact.js";
7
7
  import { classifyAssistantError, failure } from "./classify.js";
8
8
  import { addedInputsOf, describeModel, selectionOf } from "./descriptor.js";
9
9
  import { modelsFor } from "./models.js";
10
+ import { callHeaders } from "./call-headers.js";
11
+ import { workloadToken, WorkloadTokenUnavailable } from "../vault/workload-token.js";
10
12
  import { NativeBlocks, nativeKindsOf } from "./native-blocks.js";
11
13
  import { projectSecrets } from "./settings.js";
12
14
  /** The model call settings the Runtime limits `limits` set. */
@@ -53,16 +55,37 @@ export function piModel(options = {}) {
53
55
  const stored = await options.readHostModel?.();
54
56
  if (!stored)
55
57
  return failure("auth", "Model provider is not configured. Set it in Studio, or run nylorun tenant models set.", false);
58
+ // The workload token (#496): read from its file now and at each request, never stored.
59
+ let workload;
60
+ if (stored.authType === "workload") {
61
+ try {
62
+ workload = await workloadToken();
63
+ }
64
+ catch (error) {
65
+ if (error instanceof WorkloadTokenUnavailable)
66
+ return failure("auth", error.message, false);
67
+ throw error;
68
+ }
69
+ }
56
70
  const secrets = [
57
71
  ...projectSecrets(root),
58
72
  ...credentialSecrets(stored.credential),
73
+ ...(workload === undefined ? [] : [workload]),
59
74
  ];
60
75
  const redacted = (outcome) => ({
61
76
  ...outcome,
62
77
  message: String(scrub(outcome.message, secrets)),
63
78
  });
64
79
  const selection = selectionOf(stored, call.model?.id);
65
- const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false, authContext: noAmbientAuth });
80
+ // A custom endpoint's per-call headers (#493), from the calling Agent; static ones are the model's.
81
+ const sources = selection.custom?.callHeaders;
82
+ const caller = sources && options.caller ? await options.caller() : undefined;
83
+ const perCall = sources && caller ? callHeaders(sources, caller) : {};
84
+ const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), {
85
+ environment: false,
86
+ authContext: noAmbientAuth,
87
+ ...(workload === undefined ? {} : { bearer: workloadToken }),
88
+ });
66
89
  const found = registry.getModel(selection.provider, selection.model);
67
90
  if (!found)
68
91
  return failure("invalid_request", "Unknown model. Run nylorun tenant models set.", false);
@@ -95,6 +118,7 @@ export function piModel(options = {}) {
95
118
  // The session id keys the provider's prompt cache and session affinity.
96
119
  sessionId: call.executionId,
97
120
  cacheRetention: "short",
121
+ ...(Object.keys(perCall).length > 0 ? { headers: perCall } : {}),
98
122
  timeoutMs: settings.requestTimeoutMs,
99
123
  maxRetries: settings.providerRetries,
100
124
  maxRetryDelayMs: settings.maxRetryDelayMs,
@@ -481,14 +505,16 @@ function idleTimeout(selected, idleTimeoutMs) {
481
505
  }
482
506
  function hostCredentialStore(stored, write) {
483
507
  let current = stored.credential;
508
+ // A workload model stores no credential: pi-ai then asks the provider's `resolve` (`modelsFor`).
509
+ const stores = stored.authType !== "workload";
484
510
  return {
485
511
  async read(providerId) {
486
- if (providerId !== stored.provider)
512
+ if (providerId !== stored.provider || !stores)
487
513
  return undefined;
488
514
  return current;
489
515
  },
490
516
  async list() {
491
- return [{ providerId: stored.provider, type: current.type }];
517
+ return stores ? [{ providerId: stored.provider, type: current.type }] : [];
492
518
  },
493
519
  async modify(providerId, fn) {
494
520
  if (providerId !== stored.provider)
package/dist/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Runtime API",
5
- "version": "0.29.0-beta",
5
+ "version": "0.30.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
@@ -32,7 +32,8 @@
32
32
  "environment-disk-versions",
33
33
  "tool-policy",
34
34
  "traces",
35
- "artifact-reads"
35
+ "artifact-reads",
36
+ "gateway-credentials"
36
37
  ]
37
38
  },
38
39
  "description": "For developers: agent manifests, Agents, sessions (with AG-UI and A2A) and artifacts, through `@nylorun/agents`. Every Tenant serves it on its one URL, beside the Management API (`/openapi/management.json`). Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields: an application key (servers) or a trusted issuer's token (browsers and apps)."
@@ -12922,6 +12923,9 @@
12922
12923
  "server": {
12923
12924
  "type": "string"
12924
12925
  },
12926
+ "gateway": {
12927
+ "type": "string"
12928
+ },
12925
12929
  "retryable": {
12926
12930
  "type": "boolean"
12927
12931
  }
@@ -20801,6 +20805,7 @@
20801
20805
  "enum": [
20802
20806
  "bearer",
20803
20807
  "headers",
20808
+ "workload",
20804
20809
  "environment_secret",
20805
20810
  "environment_variable"
20806
20811
  ]
@@ -20847,6 +20852,9 @@
20847
20852
  "via": {
20848
20853
  "type": "string"
20849
20854
  },
20855
+ "toolPrefix": {
20856
+ "type": "string"
20857
+ },
20850
20858
  "identity": {
20851
20859
  "type": "object",
20852
20860
  "properties": {
@@ -20893,6 +20901,7 @@
20893
20901
  "enum": [
20894
20902
  "bearer",
20895
20903
  "headers",
20904
+ "workload",
20896
20905
  "environment_secret",
20897
20906
  "environment_variable"
20898
20907
  ]
@@ -20939,6 +20948,9 @@
20939
20948
  "via": {
20940
20949
  "type": "string"
20941
20950
  },
20951
+ "toolPrefix": {
20952
+ "type": "string"
20953
+ },
20942
20954
  "identity": {
20943
20955
  "type": "object",
20944
20956
  "properties": {
@@ -20984,6 +20996,12 @@
20984
20996
  "type": "string",
20985
20997
  "minLength": 1
20986
20998
  }
20999
+ },
21000
+ "toolPrefix": {
21001
+ "type": "string",
21002
+ "minLength": 1,
21003
+ "maxLength": 64,
21004
+ "pattern": "^[\\x21-\\x7e]+$"
20987
21005
  }
20988
21006
  },
20989
21007
  "additionalProperties": false
@@ -27597,6 +27615,12 @@
27597
27615
  "header"
27598
27616
  ],
27599
27617
  "additionalProperties": false
27618
+ },
27619
+ "toolPrefix": {
27620
+ "type": "string",
27621
+ "minLength": 1,
27622
+ "maxLength": 64,
27623
+ "pattern": "^[\\x21-\\x7e]+$"
27600
27624
  }
27601
27625
  },
27602
27626
  "required": [
@@ -27645,6 +27669,12 @@
27645
27669
  "header"
27646
27670
  ],
27647
27671
  "additionalProperties": false
27672
+ },
27673
+ "toolPrefix": {
27674
+ "type": "string",
27675
+ "minLength": 1,
27676
+ "maxLength": 64,
27677
+ "pattern": "^[\\x21-\\x7e]+$"
27648
27678
  }
27649
27679
  },
27650
27680
  "required": [
@@ -27654,6 +27684,53 @@
27654
27684
  ],
27655
27685
  "additionalProperties": false
27656
27686
  },
27687
+ {
27688
+ "type": "object",
27689
+ "properties": {
27690
+ "requestId": {
27691
+ "type": "string",
27692
+ "minLength": 1
27693
+ },
27694
+ "type": {
27695
+ "type": "string",
27696
+ "enum": [
27697
+ "workload"
27698
+ ]
27699
+ },
27700
+ "url": {
27701
+ "type": "string",
27702
+ "minLength": 1
27703
+ },
27704
+ "via": {
27705
+ "type": "string"
27706
+ },
27707
+ "identity": {
27708
+ "type": "object",
27709
+ "properties": {
27710
+ "header": {
27711
+ "type": "string",
27712
+ "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"
27713
+ }
27714
+ },
27715
+ "required": [
27716
+ "header"
27717
+ ],
27718
+ "additionalProperties": false
27719
+ },
27720
+ "toolPrefix": {
27721
+ "type": "string",
27722
+ "minLength": 1,
27723
+ "maxLength": 64,
27724
+ "pattern": "^[\\x21-\\x7e]+$"
27725
+ }
27726
+ },
27727
+ "required": [
27728
+ "type",
27729
+ "url",
27730
+ "via"
27731
+ ],
27732
+ "additionalProperties": false
27733
+ },
27657
27734
  {
27658
27735
  "$ref": "#/components/schemas/PutAgentSecretValueRequest"
27659
27736
  }
@@ -2737,14 +2737,14 @@ export declare const vaultCredentials: import("drizzle-orm/pg-core").PgTableWith
2737
2737
  tableName: "vault_credentials";
2738
2738
  dataType: "string";
2739
2739
  columnType: "PgText";
2740
- data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model";
2740
+ data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model" | "workload";
2741
2741
  driverParam: string;
2742
2742
  notNull: true;
2743
2743
  hasDefault: false;
2744
2744
  isPrimaryKey: false;
2745
2745
  isAutoincrement: false;
2746
2746
  hasRuntimeDefault: false;
2747
- enumValues: ["bearer", "headers", "environment_secret", "environment_variable", "model"];
2747
+ enumValues: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"];
2748
2748
  baseColumn: never;
2749
2749
  identity: undefined;
2750
2750
  generated: undefined;
@@ -353,7 +353,7 @@ export const vaultCredentials = nylorun.table("vault_credentials", {
353
353
  id: textC().primaryKey(),
354
354
  vaultId: text().notNull(),
355
355
  name: text().notNull(),
356
- type: text({ enum: ["bearer", "headers", "environment_secret", "environment_variable", "model"] }).notNull(),
356
+ type: text({ enum: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"] }).notNull(),
357
357
  /** JSON text of the non-secret binding: url (and via, identity, header names), or provider and model. */
358
358
  bindingJson: text().notNull(),
359
359
  expiresAt: text(),
@@ -562,15 +562,19 @@ async function parseSecretRequest(ctx, id, name, raw) {
562
562
  const full = slotRequest(slot);
563
563
  if (raw !== null && typeof raw === "object" && "type" in raw) {
564
564
  const body = PutAgentSecretRequestSchema.parse(raw);
565
- const { requestId: _, value: _v, headers: _h, ...binding } = body;
565
+ // The gateway's tool prefix is this installation's to add (`gateway-credentials`).
566
+ const { requestId: _, value: _v, headers: _h, toolPrefix: _p, ...binding } = body;
566
567
  if (canonical(binding) !== canonical(full))
567
568
  fail(409, `Secret ${name}'s type and binding come from Agent ${id}'s manifest: send only its value ({ "value": "…" }).`, {
568
569
  code: "secret_not_in_manifest",
569
570
  });
570
571
  return body;
571
572
  }
572
- const { requestId, value, headers } = PutAgentSecretValueRequestSchema.parse(raw);
573
- const withId = requestId === undefined ? {} : { requestId };
573
+ const { requestId, value, headers, toolPrefix } = PutAgentSecretValueRequestSchema.parse(raw);
574
+ const withId = {
575
+ ...(requestId === undefined ? {} : { requestId }),
576
+ ...(toolPrefix === undefined ? {} : { toolPrefix }),
577
+ };
574
578
  if (slot.type === "headers") {
575
579
  if (headers === undefined)
576
580
  fail(400, `Secret ${name} is a headers credential: send { "headers": { … } }.`);