@nylorun/runtime 0.19.1-beta → 0.21.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +22 -13
  3. package/dist/adapters/streams/s2.js +5 -11
  4. package/dist/api/components.d.ts +326 -205
  5. package/dist/api/components.js +3 -3
  6. package/dist/api/http/app.js +2 -0
  7. package/dist/api/http/define.d.ts +3 -2
  8. package/dist/api/http/define.js +10 -11
  9. package/dist/api/http/routes/mcp.d.ts +9 -0
  10. package/dist/api/http/routes/mcp.js +32 -0
  11. package/dist/api/http/routes/vaults.d.ts +1 -9
  12. package/dist/api/http/routes/vaults.js +3 -103
  13. package/dist/api/openapi.js +37 -6
  14. package/dist/artifacts/media-types.d.ts +2 -0
  15. package/dist/artifacts/media-types.js +5 -0
  16. package/dist/execution/types.d.ts +1 -1
  17. package/dist/gates/http-tool.d.ts +7 -0
  18. package/dist/gates/http-tool.js +34 -3
  19. package/dist/gates/mcp-handler.d.ts +4 -1
  20. package/dist/gates/mcp-handler.js +64 -8
  21. package/dist/gates/tenant-vaults.d.ts +9 -15
  22. package/dist/gates/tenant-vaults.js +9 -17
  23. package/dist/gates/tool-client.js +36 -17
  24. package/dist/gates/tool-contract.d.ts +19 -3
  25. package/dist/gates/tool-contract.js +9 -3
  26. package/dist/gates/tool-gate.d.ts +2 -2
  27. package/dist/harness/calls.d.ts +2 -0
  28. package/dist/harness/calls.js +13 -3
  29. package/dist/harness-api/record.d.ts +7 -6
  30. package/dist/harness-api/record.js +99 -17
  31. package/dist/harness-api/server.js +35 -27
  32. package/dist/harness-api/start.js +2 -1
  33. package/dist/host/app.d.ts +3 -0
  34. package/dist/host/app.js +33 -4
  35. package/dist/host/config-for.d.ts +0 -2
  36. package/dist/host/config-for.js +0 -1
  37. package/dist/host/create-host.d.ts +6 -0
  38. package/dist/host/create-host.js +1 -0
  39. package/dist/host/gates.js +0 -1
  40. package/dist/host/main.js +24 -7
  41. package/dist/host/stack-config.d.ts +5 -8
  42. package/dist/host/stack-config.js +9 -20
  43. package/dist/index.d.ts +0 -1
  44. package/dist/infra/index.d.ts +2 -1
  45. package/dist/infra/index.js +0 -1
  46. package/dist/infra/readiness.d.ts +6 -6
  47. package/dist/infra/readiness.js +2 -5
  48. package/dist/keys/client.js +1 -9
  49. package/dist/keys/keys.d.ts +11 -13
  50. package/dist/keys/keys.js +5 -7
  51. package/dist/management-openapi.json +671 -378
  52. package/dist/mcp/connect.d.ts +159 -13
  53. package/dist/mcp/connect.js +538 -55
  54. package/dist/mcp/pool.d.ts +11 -2
  55. package/dist/mcp/pool.js +74 -27
  56. package/dist/mcp/preview.d.ts +35 -0
  57. package/dist/mcp/preview.js +215 -0
  58. package/dist/mcp/snapshot.d.ts +79 -5
  59. package/dist/mcp/snapshot.js +145 -27
  60. package/dist/model/catalog.d.ts +14 -0
  61. package/dist/model/catalog.js +14 -1
  62. package/dist/model/pi-model.js +36 -2
  63. package/dist/openapi.json +644 -266
  64. package/dist/redact.d.ts +19 -1
  65. package/dist/redact.js +42 -13
  66. package/dist/sandbox/session-sandbox.d.ts +7 -0
  67. package/dist/sandbox/session-sandbox.js +89 -28
  68. package/dist/store/postgres/connect.d.ts +10 -0
  69. package/dist/store/postgres/connect.js +30 -3
  70. package/dist/store/postgres/control.d.ts +14 -0
  71. package/dist/store/postgres/control.js +187 -0
  72. package/dist/store/postgres/drizzle/0015_control_signals.sql +11 -0
  73. package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
  74. package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
  75. package/dist/store/postgres/schema.d.ts +122 -261
  76. package/dist/store/postgres/schema.js +22 -36
  77. package/dist/store/postgres/store.js +16 -18
  78. package/dist/store/postgres/tenant.d.ts +14 -0
  79. package/dist/store/postgres/tenant.js +22 -2
  80. package/dist/store/types.d.ts +42 -8
  81. package/dist/streams/types.d.ts +6 -29
  82. package/dist/streams/types.js +5 -4
  83. package/dist/tenant/artifact-tool.d.ts +9 -1
  84. package/dist/tenant/artifact-tool.js +113 -9
  85. package/dist/tenant/auth.d.ts +16 -6
  86. package/dist/tenant/auth.js +41 -17
  87. package/dist/tenant/commands.js +10 -6
  88. package/dist/tenant/context.d.ts +9 -6
  89. package/dist/tenant/control.d.ts +26 -0
  90. package/dist/tenant/control.js +35 -0
  91. package/dist/tenant/effects.d.ts +2 -2
  92. package/dist/tenant/effects.js +20 -5
  93. package/dist/tenant/ephemeral.d.ts +0 -6
  94. package/dist/tenant/ephemeral.js +1 -1
  95. package/dist/tenant/http.d.ts +4 -2
  96. package/dist/tenant/http.js +4 -2
  97. package/dist/tenant/identity-file.d.ts +16 -12
  98. package/dist/tenant/identity-file.js +41 -29
  99. package/dist/tenant/issuers.d.ts +4 -3
  100. package/dist/tenant/issuers.js +22 -16
  101. package/dist/tenant/outbound.d.ts +14 -5
  102. package/dist/tenant/outbound.js +21 -9
  103. package/dist/tenant/reset.d.ts +2 -2
  104. package/dist/tenant/reset.js +8 -2
  105. package/dist/tenant/resource-server.d.ts +56 -0
  106. package/dist/tenant/resource-server.js +62 -0
  107. package/dist/tenant/runtime.d.ts +1 -1
  108. package/dist/tenant/runtime.js +16 -15
  109. package/dist/tenant/scheduler.d.ts +1 -1
  110. package/dist/tenant/scheduler.js +1 -1
  111. package/dist/tenant/sessions.js +20 -3
  112. package/dist/tenant/store-pg.js +9 -0
  113. package/dist/tenant/streams.d.ts +16 -22
  114. package/dist/tenant/streams.js +7 -87
  115. package/dist/tenant/sweep.js +9 -1
  116. package/dist/tenant/tool-results.d.ts +57 -0
  117. package/dist/tenant/tool-results.js +298 -0
  118. package/dist/tenant/tool-search.d.ts +43 -0
  119. package/dist/tenant/tool-search.js +100 -0
  120. package/dist/tenant/transcript.d.ts +108 -0
  121. package/dist/tenant/transcript.js +13 -0
  122. package/dist/tenant/types.d.ts +0 -14
  123. package/dist/vault/crypto.d.ts +0 -7
  124. package/dist/vault/crypto.js +0 -16
  125. package/dist/vault/headers.d.ts +17 -0
  126. package/dist/vault/headers.js +61 -0
  127. package/dist/vault/service.d.ts +11 -68
  128. package/dist/vault/service.js +83 -438
  129. package/dist/vault/sources.d.ts +20 -48
  130. package/dist/vault/sources.js +25 -189
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/package.json +3 -3
  134. package/dist/streams/control.d.ts +0 -13
  135. package/dist/streams/control.js +0 -25
  136. package/dist/vault/oauth.d.ts +0 -55
  137. package/dist/vault/oauth.js +0 -186
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Tool results that fit (R2b C11, Q22–Q24): core shapes what an agent's remote MCP tool or HTTP
3
+ * tool returned before it records the outcome (`harness-api/record.ts`), so no journal row,
4
+ * event or prompt holds more of one result than the inline cap. The gate already refused an
5
+ * answer past 8 MiB (`mcp.too-large`, `http.too-large`).
6
+ *
7
+ * - An `image`, `audio` or blob `resource` part becomes a file artifact of the session, and the
8
+ * part says `{artifactId, version, contentType, size}`. An image also goes to the model as a
9
+ * file (`ToolOutcome.files`), which the model call shows to a model that reads images (Q23).
10
+ * - Text or JSON past `TOOL_RESULT_INLINE_BYTES` (32 KiB, Q22) becomes a file artifact, and the
11
+ * model gets `{truncated: true, artifactId, size, preview}`: the first 4 KiB and the last 1 KiB.
12
+ * In a result of several parts each part is shaped alone, the largest text first, until the
13
+ * whole fits; one that still does not is stored whole.
14
+ * - A `resource_link` stays a link, never fetched, and a text resource that fits stays inline.
15
+ * - A failed outcome's message is cut to its start and end.
16
+ * - When an artifact cannot be stored (the Tenant's artifact limits, an Object store that
17
+ * fails), the preview stays and `dropped` says why the rest is gone: a result never passes
18
+ * the cap. Only an abort throws.
19
+ * - **One step's results share a budget** (`STEP_RESULTS_BUDGET_BYTES`, 256 KiB), since the
20
+ * transcript keeps a step's results in one entry, one event: the record seam counts what the
21
+ * step's other results took, under the session lock, and gives this one the rest as its cap
22
+ * (`resultLimit`). Once the budget is spent, a result past `TOOL_RESULT_SMALL_BYTES` becomes a
23
+ * stub naming its artifact, its preview cut to fit `TOOL_RESULT_STUB_BYTES`.
24
+ *
25
+ * The model reads a stored result with `read_artifact` (`artifact-tool.ts`). Artifacts are the
26
+ * session's, saved by our engine (`source: "engine"`), with the turn and the tool call on their
27
+ * `artifact.created` event.
28
+ */
29
+ import { READ_ARTIFACT_MAX_BYTES } from "@nylorun/core/define";
30
+ import { MODEL_FILE_MAX_BYTES, extensionFor, isModelImage, isText, normalizeContentType, } from "../artifacts/media-types.js";
31
+ import { uploadArtifact } from "../artifacts/service.js";
32
+ import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
33
+ import { HttpError } from "./http.js";
34
+ import { toolIds } from "./transcript.js";
35
+ /** The most of one tool result that is recorded inline (Q22): 32 KiB, as JSON. */
36
+ export const TOOL_RESULT_INLINE_BYTES = READ_ARTIFACT_MAX_BYTES;
37
+ /** How much of a stored result's start, and of its end, the preview keeps. */
38
+ export const PREVIEW_HEAD_BYTES = 4 * 1024;
39
+ export const PREVIEW_TAIL_BYTES = 1024;
40
+ /**
41
+ * The most one step's results may hold together: a step's results are one transcript entry, so
42
+ * one event, which must stay well under S2's 1 MiB record.
43
+ */
44
+ export const STEP_RESULTS_BUDGET_BYTES = 256 * 1024;
45
+ /** A result's cap once its step's budget is spent: a stub naming its artifact. */
46
+ export const TOOL_RESULT_STUB_BYTES = 512;
47
+ /** A result this small is never shaped, nor counted against its step's budget. */
48
+ export const TOOL_RESULT_SMALL_BYTES = 1024;
49
+ /** A result's cap when the other results of its step already took `used` bytes. */
50
+ export function resultLimit(used) {
51
+ return Math.max(TOOL_RESULT_STUB_BYTES, Math.min(TOOL_RESULT_INLINE_BYTES, STEP_RESULTS_BUDGET_BYTES - used));
52
+ }
53
+ /** The JSON size of `value` in bytes: what it adds to an event or a prompt. */
54
+ export function jsonBytes(value) {
55
+ return Buffer.byteLength(JSON.stringify(value) ?? "null");
56
+ }
57
+ /**
58
+ * What a tool outcome's result weighs in its step's transcript entry: a completed one's output,
59
+ * a failed one's message; 0 for anything else, such as a model call's outcome.
60
+ */
61
+ export function resultBytes(value) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value))
63
+ return 0;
64
+ const outcome = value;
65
+ if (outcome.kind === "failed")
66
+ return typeof outcome.message === "string" ? Buffer.byteLength(outcome.message) : 0;
67
+ if (outcome.kind !== "completed" || outcome.output === undefined)
68
+ return 0;
69
+ return jsonBytes(outcome.output);
70
+ }
71
+ /**
72
+ * False for an outcome `shapeToolOutcome` returns as it is, whatever made it: a model call's, a
73
+ * small tool result with no parts. Spares a look at the effect for most outcomes.
74
+ */
75
+ export function mayNeedShaping(value) {
76
+ const output = value?.output;
77
+ if (value?.kind === "completed" && Array.isArray(output))
78
+ return true;
79
+ return resultBytes(value) > TOOL_RESULT_SMALL_BYTES;
80
+ }
81
+ /**
82
+ * The outcome `value` of `call`, shaped to fit its cap (see the module comment). An outcome that
83
+ * fits and has no binary part comes back as it is.
84
+ */
85
+ export async function shapeToolOutcome(ctx, call, value, signal) {
86
+ if (!value || typeof value !== "object" || Array.isArray(value))
87
+ return value;
88
+ const outcome = value;
89
+ const limit = call.limit ?? TOOL_RESULT_INLINE_BYTES;
90
+ if (outcome.kind === "failed" && typeof outcome.message === "string")
91
+ return Buffer.byteLength(outcome.message) > limit
92
+ ? { ...outcome, message: cutMessage(outcome.message, limit) }
93
+ : value;
94
+ if (outcome.kind !== "completed" || outcome.output === undefined)
95
+ return value;
96
+ const shaping = { ...call, limit, ctx, signal, files: [] };
97
+ let output = outcome.output;
98
+ if (call.mcp && isContentParts(output))
99
+ output = await shapeParts(shaping, output);
100
+ let truncated = false;
101
+ if (jsonBytes(output) > limit) {
102
+ output = await storedWhole(shaping, output);
103
+ truncated = true;
104
+ }
105
+ if (output === outcome.output)
106
+ return value;
107
+ return {
108
+ ...outcome,
109
+ output,
110
+ ...(shaping.files.length > 0 ? { files: shaping.files } : {}),
111
+ ...(truncated ? { truncated: true } : {}),
112
+ };
113
+ }
114
+ /** True for an MCP `content` array: objects with a `type` each. */
115
+ function isContentParts(value) {
116
+ return (Array.isArray(value) &&
117
+ value.length > 0 &&
118
+ value.every((part) => !!part &&
119
+ typeof part === "object" &&
120
+ !Array.isArray(part) &&
121
+ typeof part.type === "string"));
122
+ }
123
+ /** An MCP result's parts: binary ones stored, then the largest text ones until the whole fits. */
124
+ async function shapeParts(shaping, parts) {
125
+ const shaped = [];
126
+ for (const [index, part] of parts.entries())
127
+ shaped.push(await shapeBinary(shaping, part, index));
128
+ const texts = shaped
129
+ .map((part, index) => ({ index, bytes: textOf(part) === undefined ? 0 : jsonBytes(part) }))
130
+ .filter((item) => item.bytes > 0)
131
+ .sort((a, b) => b.bytes - a.bytes);
132
+ for (const { index } of texts) {
133
+ if (jsonBytes(shaped) <= shaping.limit)
134
+ break;
135
+ const part = shaped[index];
136
+ const text = textOf(part);
137
+ const resource = part.type === "resource" ? part.resource : undefined;
138
+ const contentType = resource ? textType(resource.mimeType) : "text/plain; charset=utf-8";
139
+ const kept = await store(shaping, text, contentType, nameOf(shaping, contentType, index));
140
+ shaped[index] = withPreview({
141
+ type: part.type,
142
+ ...(resource && typeof resource.uri === "string" ? { uri: resource.uri } : {}),
143
+ truncated: true,
144
+ ...kept,
145
+ }, text, shaping.limit);
146
+ }
147
+ return shaped;
148
+ }
149
+ /** An `image`, `audio` or blob `resource` part, stored; any other part as it is. */
150
+ async function shapeBinary(shaping, part, index) {
151
+ const resource = part.type === "resource" && part.resource && typeof part.resource === "object"
152
+ ? part.resource
153
+ : undefined;
154
+ const data = part.type === "image" || part.type === "audio"
155
+ ? part.data
156
+ : resource !== undefined
157
+ ? resource.blob
158
+ : undefined;
159
+ if (typeof data !== "string")
160
+ return part;
161
+ const declared = resource ? resource.mimeType : part.mimeType;
162
+ const contentType = (typeof declared === "string" ? normalizeContentType(declared) : undefined) ??
163
+ DEFAULT_CONTENT_TYPE;
164
+ const name = nameOf(shaping, contentType, index);
165
+ const kept = await store(shaping, Buffer.from(data, "base64"), contentType, name);
166
+ // An image the model reads goes to it as a file, beside the part (Q23).
167
+ if (part.type === "image" &&
168
+ "artifactId" in kept &&
169
+ isModelImage(contentType) &&
170
+ kept.size <= MODEL_FILE_MAX_BYTES)
171
+ shaping.files.push({
172
+ mediaType: contentType,
173
+ reference: { artifactId: kept.artifactId, version: kept.version, name },
174
+ });
175
+ return {
176
+ type: part.type,
177
+ ...(resource && typeof resource.uri === "string" ? { uri: resource.uri } : {}),
178
+ ...("artifactId" in kept ? kept : { contentType, ...kept }),
179
+ };
180
+ }
181
+ /** The text a part carries inline: a text part's, or a text resource's. */
182
+ function textOf(part) {
183
+ if (part.type === "text" && typeof part.text === "string")
184
+ return part.text;
185
+ if (part.type === "resource" && part.resource && typeof part.resource === "object") {
186
+ const text = part.resource.text;
187
+ if (typeof text === "string")
188
+ return text;
189
+ }
190
+ return undefined;
191
+ }
192
+ function textType(declared) {
193
+ const type = typeof declared === "string" ? normalizeContentType(declared) : undefined;
194
+ return type !== undefined && isText(type) ? type : "text/plain; charset=utf-8";
195
+ }
196
+ /** `value` stored whole, as text or JSON, and the preview the model gets. */
197
+ async function storedWhole(shaping, value) {
198
+ const text = typeof value === "string" ? value : JSON.stringify(value);
199
+ const contentType = typeof value === "string" ? "text/plain; charset=utf-8" : "application/json";
200
+ const kept = await store(shaping, text, contentType, nameOf(shaping, contentType));
201
+ return withPreview({ truncated: true, ...kept }, text, shaping.limit);
202
+ }
203
+ /**
204
+ * `stub` with the preview of `text`: the first 4 KiB and the last 1 KiB, or less, so that the
205
+ * whole fits `limit` (a step's budget spent, R2b C11); none when even a little does not.
206
+ */
207
+ function withPreview(stub, text, limit) {
208
+ let budget = Math.min(PREVIEW_HEAD_BYTES + PREVIEW_TAIL_BYTES, limit);
209
+ for (;;) {
210
+ const shaped = { ...stub, preview: previewOf(text, budget) };
211
+ const over = jsonBytes(shaped) - limit;
212
+ if (over <= 0 || budget === 0)
213
+ return shaped;
214
+ budget = budget - over < 64 ? 0 : budget - over;
215
+ }
216
+ }
217
+ /** Stores `body` as an artifact of the call's session, or says why it was not. */
218
+ async function store(shaping, body, contentType, name) {
219
+ const bytes = typeof body === "string" ? Buffer.from(body, "utf8") : body;
220
+ const { ctx, request, session, signal } = shaping;
221
+ const ids = toolIds(request.context);
222
+ try {
223
+ const saved = await uploadArtifact(ctx, {
224
+ name,
225
+ contentType,
226
+ sessionId: session.id,
227
+ source: "engine",
228
+ turnId: request.turnId,
229
+ ...("callId" in ids ? { callId: ids.callId } : {}),
230
+ }, bytes,
231
+ // Into the call's own session only.
232
+ { owner: session.ownerUserId }, signal);
233
+ return {
234
+ artifactId: saved.artifact.artifactId,
235
+ version: saved.version.version,
236
+ contentType: saved.version.contentType,
237
+ size: saved.version.size,
238
+ };
239
+ }
240
+ catch (error) {
241
+ if (signal.aborted)
242
+ throw error;
243
+ // A limit is the Tenant's to raise; any other failure is the store's. Never the bytes.
244
+ ctx.config.logger.warn("tool_result_not_stored", {
245
+ session: session.id,
246
+ effect: request.effectId,
247
+ error: error instanceof Error ? error.message : String(error),
248
+ });
249
+ return {
250
+ dropped: error instanceof HttpError
251
+ ? `The result could not be stored as an artifact: ${error.message}`
252
+ : "The result could not be stored as an artifact",
253
+ size: bytes.byteLength,
254
+ };
255
+ }
256
+ }
257
+ /** A name for a stored result: the tool's, with the part's number and the type's extension. */
258
+ function nameOf(shaping, contentType, index) {
259
+ const tool = (shaping.request.toolName ?? "tool").replace(/[^A-Za-z0-9_.-]/gu, "_");
260
+ const extension = extensionFor(contentType) ?? "bin";
261
+ return `${tool.slice(0, 200)}-result${index === undefined ? "" : `-${index + 1}`}.${extension}`;
262
+ }
263
+ /**
264
+ * The start and end of `text` in whole characters, `budget` bytes split four to one: by default
265
+ * the first `PREVIEW_HEAD_BYTES` and the last `PREVIEW_TAIL_BYTES`.
266
+ */
267
+ export function previewOf(text, budget = PREVIEW_HEAD_BYTES + PREVIEW_TAIL_BYTES) {
268
+ const bytes = Buffer.from(text, "utf8");
269
+ if (bytes.byteLength <= budget)
270
+ return text;
271
+ if (budget <= 0)
272
+ return "";
273
+ const tailBytes = Math.floor(budget / 5);
274
+ const head = utf8Boundary(bytes, budget - tailBytes, "down");
275
+ const tail = utf8Boundary(bytes, bytes.byteLength - tailBytes, "up");
276
+ return `${bytes.subarray(0, head).toString("utf8")}\n…\n${bytes.subarray(tail).toString("utf8")}`;
277
+ }
278
+ /** A message past `limit`: its start and end, and how long it was. */
279
+ function cutMessage(message, limit) {
280
+ const note = `\n(The message was ${Buffer.byteLength(message)} bytes; its middle is not shown.)`;
281
+ const budget = Math.min(PREVIEW_HEAD_BYTES + PREVIEW_TAIL_BYTES, limit - note.length - 16);
282
+ return `${previewOf(message, Math.max(0, budget))}${note}`;
283
+ }
284
+ /**
285
+ * The nearest character boundary of the UTF-8 `bytes` at or before (`down`) or at or after
286
+ * (`up`) byte `at`: never inside a character.
287
+ */
288
+ export function utf8Boundary(bytes, at, direction) {
289
+ let position = Math.max(0, Math.min(at, bytes.byteLength));
290
+ const inside = (index) => index < bytes.byteLength && (bytes[index] & 0xc0) === 0x80;
291
+ if (direction === "down")
292
+ while (position > 0 && inside(position))
293
+ position -= 1;
294
+ else
295
+ while (inside(position))
296
+ position += 1;
297
+ return position;
298
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `tool_search` (R2b C10, Q20): core finds an agent's deferred MCP tools in the session's pinned
3
+ * snapshot, ranked with BM25 over their names and descriptions, and returns each match's name,
4
+ * description and `inputSchema`, for the model to run with `tool_call`. It reads only the pinned
5
+ * snapshot, so the same query finds the same tools for the session's life; a tool the turn's
6
+ * manifest disables is never found.
7
+ */
8
+ import { type AgentManifest } from "@nylorun/core/define";
9
+ import type { HostEffect } from "@nylorun/harness/run";
10
+ import { type McpToolRecord } from "../mcp/snapshot.js";
11
+ import { type TenantContext } from "./context.js";
12
+ /** True when `request` calls `tool_search` of the `nylorun.tools` capability `manifest` has. */
13
+ export declare function isToolSearchCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
14
+ /** Runs `tool_search` for the run's agent. */
15
+ export declare function callToolSearch(ctx: TenantContext, request: HostEffect): Promise<{
16
+ output?: undefined;
17
+ kind: string;
18
+ code: string;
19
+ message: string;
20
+ } | {
21
+ code?: undefined;
22
+ message?: undefined;
23
+ kind: string;
24
+ output: {
25
+ tools: {
26
+ name: string;
27
+ description?: string | undefined;
28
+ inputSchema: import("@nylorun/core/define").JsonObject;
29
+ }[];
30
+ message?: string | undefined;
31
+ };
32
+ }>;
33
+ /**
34
+ * Lower-case words of `text`: `camelCase`, `snake_case` and `kebab-case` split into words, and a
35
+ * plural `s` dropped (`issues` finds `issue`).
36
+ */
37
+ export declare function tokens(text: string): string[];
38
+ /**
39
+ * The tools that match `query`, best first, by BM25 over each tool's name (counted twice, with
40
+ * the server's own name for it) and description. A tool no word of the query is in is left out;
41
+ * ties keep the snapshot's order.
42
+ */
43
+ export declare function rank(tools: readonly McpToolRecord[], query: string): McpToolRecord[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `tool_search` (R2b C10, Q20): core finds an agent's deferred MCP tools in the session's pinned
3
+ * snapshot, ranked with BM25 over their names and descriptions, and returns each match's name,
4
+ * description and `inputSchema`, for the model to run with `tool_call`. It reads only the pinned
5
+ * snapshot, so the same query finds the same tools for the session's life; a tool the turn's
6
+ * manifest disables is never found.
7
+ */
8
+ import { TOOL_SEARCH_DEFAULT_LIMIT, TOOL_SEARCH_MAX_LIMIT, TOOL_SEARCH_TOOL, TOOLS_CAPABILITY_ID, } from "@nylorun/core/define";
9
+ import { isEnabledIn } from "../mcp/snapshot.js";
10
+ import { sessionOf } from "./context.js";
11
+ import { turnManifestOf } from "./session.js";
12
+ /** BM25's term saturation and length normalization, the usual values. */
13
+ const K1 = 1.2;
14
+ const B = 0.75;
15
+ /** True when `request` calls `tool_search` of the `nylorun.tools` capability `manifest` has. */
16
+ export function isToolSearchCall(manifest, request) {
17
+ return (request.kind === "tool" &&
18
+ request.capabilityId === TOOLS_CAPABILITY_ID &&
19
+ request.toolName === TOOL_SEARCH_TOOL &&
20
+ manifest?.capabilities.some((capability) => capability.id === TOOLS_CAPABILITY_ID) === true);
21
+ }
22
+ /** Runs `tool_search` for the run's agent. */
23
+ export async function callToolSearch(ctx, request) {
24
+ const input = (request.input ?? {});
25
+ const query = typeof input.query === "string" ? input.query : "";
26
+ const limit = typeof input.limit === "number" && Number.isSafeInteger(input.limit)
27
+ ? Math.min(Math.max(input.limit, 1), TOOL_SEARCH_MAX_LIMIT)
28
+ : TOOL_SEARCH_DEFAULT_LIMIT;
29
+ if (query.trim() === "")
30
+ return { kind: "failed", code: "tools.invalid_input", message: "Give a query: words of what the tool does." };
31
+ const session = await ctx.store.tx((t) => sessionOf(t, request.sessionId));
32
+ const manifest = turnManifestOf(session);
33
+ const deferred = (session.mcpSnapshot?.mcpTools ?? []).filter((tool) => tool.deferred === true &&
34
+ tool.agentId === request.agent?.id &&
35
+ // Enabled in the turn's manifest, which a variant may have tightened (R2b C9).
36
+ isEnabledIn(manifest, tool));
37
+ const found = rank(deferred, query).slice(0, limit);
38
+ return {
39
+ kind: "completed",
40
+ output: {
41
+ tools: found.map((tool) => ({
42
+ name: tool.name,
43
+ ...(tool.description === undefined ? {} : { description: tool.description }),
44
+ inputSchema: tool.inputSchema,
45
+ })),
46
+ ...(found.length === 0
47
+ ? { message: `No tool matches '${query}'. Try other words; ${deferred.length} tools are searchable.` }
48
+ : {}),
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * Lower-case words of `text`: `camelCase`, `snake_case` and `kebab-case` split into words, and a
54
+ * plural `s` dropped (`issues` finds `issue`).
55
+ */
56
+ export function tokens(text) {
57
+ return text
58
+ .replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
59
+ .toLowerCase()
60
+ .split(/[^\p{L}\p{N}]+/u)
61
+ .filter((word) => word.length > 0)
62
+ .map((word) => (word.length > 3 && word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word));
63
+ }
64
+ /**
65
+ * The tools that match `query`, best first, by BM25 over each tool's name (counted twice, with
66
+ * the server's own name for it) and description. A tool no word of the query is in is left out;
67
+ * ties keep the snapshot's order.
68
+ */
69
+ export function rank(tools, query) {
70
+ const words = [...new Set(tokens(query))];
71
+ if (words.length === 0 || tools.length === 0)
72
+ return [];
73
+ const docs = tools.map((tool) => {
74
+ const name = [...tokens(tool.name), ...tokens(tool.serverToolName)];
75
+ const terms = [...name, ...name, ...tokens(tool.description ?? "")];
76
+ const counts = new Map();
77
+ for (const term of terms)
78
+ counts.set(term, (counts.get(term) ?? 0) + 1);
79
+ return { tool, counts, length: terms.length };
80
+ });
81
+ const average = docs.reduce((sum, doc) => sum + doc.length, 0) / docs.length || 1;
82
+ const idf = new Map(words.map((word) => {
83
+ const df = docs.filter((doc) => doc.counts.has(word)).length;
84
+ return [word, Math.log(1 + (docs.length - df + 0.5) / (df + 0.5))];
85
+ }));
86
+ return docs
87
+ .map((doc, order) => {
88
+ let score = 0;
89
+ for (const word of words) {
90
+ const tf = doc.counts.get(word) ?? 0;
91
+ if (tf === 0)
92
+ continue;
93
+ score += idf.get(word) * ((tf * (K1 + 1)) / (tf + K1 * (1 - B + (B * doc.length) / average)));
94
+ }
95
+ return { tool: doc.tool, score, order };
96
+ })
97
+ .filter((item) => item.score > 0)
98
+ .sort((a, b) => b.score - a.score || a.order - b.order)
99
+ .map((item) => item.tool);
100
+ }
@@ -81,8 +81,12 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
81
81
  error: {
82
82
  code: string;
83
83
  message: string;
84
+ server?: string | undefined;
85
+ vault?: string | undefined;
86
+ retryable?: boolean | undefined;
84
87
  };
85
88
  output?: undefined;
89
+ redacted: number;
86
90
  capabilityId: string;
87
91
  toolName: string;
88
92
  } | {
@@ -90,20 +94,96 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
90
94
  error: {
91
95
  code: string;
92
96
  message: string;
97
+ server?: string | undefined;
98
+ vault?: string | undefined;
99
+ retryable?: boolean | undefined;
93
100
  };
94
101
  output?: undefined;
102
+ redacted: number;
103
+ capabilityId: string;
104
+ toolName: string;
105
+ } | {
106
+ agent: import("@nylorun/core/define").AgentRef;
107
+ error: {
108
+ code: string;
109
+ message: string;
110
+ server?: string | undefined;
111
+ vault?: string | undefined;
112
+ retryable?: boolean | undefined;
113
+ };
114
+ output?: undefined;
115
+ redacted?: undefined;
116
+ capabilityId: string;
117
+ toolName: string;
118
+ } | {
119
+ agent?: undefined;
120
+ error: {
121
+ code: string;
122
+ message: string;
123
+ server?: string | undefined;
124
+ vault?: string | undefined;
125
+ retryable?: boolean | undefined;
126
+ };
127
+ output?: undefined;
128
+ redacted?: undefined;
95
129
  capabilityId: string;
96
130
  toolName: string;
97
131
  } | {
98
132
  agent: import("@nylorun/core/define").AgentRef;
99
133
  error?: undefined;
100
134
  output: unknown;
135
+ redacted: number;
101
136
  capabilityId: string;
102
137
  toolName: string;
103
138
  } | {
104
139
  agent?: undefined;
105
140
  error?: undefined;
106
141
  output: unknown;
142
+ redacted: number;
143
+ capabilityId: string;
144
+ toolName: string;
145
+ } | {
146
+ agent: import("@nylorun/core/define").AgentRef;
147
+ error?: undefined;
148
+ output: unknown;
149
+ redacted?: undefined;
150
+ capabilityId: string;
151
+ toolName: string;
152
+ } | {
153
+ agent?: undefined;
154
+ error?: undefined;
155
+ output: unknown;
156
+ redacted?: undefined;
157
+ capabilityId: string;
158
+ toolName: string;
159
+ } | {
160
+ callId: string;
161
+ invocationId: string;
162
+ agent: import("@nylorun/core/define").AgentRef;
163
+ error: {
164
+ code: string;
165
+ message: string;
166
+ server?: string | undefined;
167
+ vault?: string | undefined;
168
+ retryable?: boolean | undefined;
169
+ };
170
+ output?: undefined;
171
+ redacted: number;
172
+ capabilityId: string;
173
+ toolName: string;
174
+ } | {
175
+ callId: string;
176
+ invocationId: string;
177
+ agent?: undefined;
178
+ error: {
179
+ code: string;
180
+ message: string;
181
+ server?: string | undefined;
182
+ vault?: string | undefined;
183
+ retryable?: boolean | undefined;
184
+ };
185
+ output?: undefined;
186
+ redacted: number;
107
187
  capabilityId: string;
108
188
  toolName: string;
109
189
  } | {
@@ -113,8 +193,12 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
113
193
  error: {
114
194
  code: string;
115
195
  message: string;
196
+ server?: string | undefined;
197
+ vault?: string | undefined;
198
+ retryable?: boolean | undefined;
116
199
  };
117
200
  output?: undefined;
201
+ redacted?: undefined;
118
202
  capabilityId: string;
119
203
  toolName: string;
120
204
  } | {
@@ -124,8 +208,30 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
124
208
  error: {
125
209
  code: string;
126
210
  message: string;
211
+ server?: string | undefined;
212
+ vault?: string | undefined;
213
+ retryable?: boolean | undefined;
127
214
  };
128
215
  output?: undefined;
216
+ redacted?: undefined;
217
+ capabilityId: string;
218
+ toolName: string;
219
+ } | {
220
+ callId: string;
221
+ invocationId: string;
222
+ agent: import("@nylorun/core/define").AgentRef;
223
+ error?: undefined;
224
+ output: unknown;
225
+ redacted: number;
226
+ capabilityId: string;
227
+ toolName: string;
228
+ } | {
229
+ callId: string;
230
+ invocationId: string;
231
+ agent?: undefined;
232
+ error?: undefined;
233
+ output: unknown;
234
+ redacted: number;
129
235
  capabilityId: string;
130
236
  toolName: string;
131
237
  } | {
@@ -134,6 +240,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
134
240
  agent: import("@nylorun/core/define").AgentRef;
135
241
  error?: undefined;
136
242
  output: unknown;
243
+ redacted?: undefined;
137
244
  capabilityId: string;
138
245
  toolName: string;
139
246
  } | {
@@ -142,6 +249,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
142
249
  agent?: undefined;
143
250
  error?: undefined;
144
251
  output: unknown;
252
+ redacted?: undefined;
145
253
  capabilityId: string;
146
254
  toolName: string;
147
255
  } | undefined;
@@ -102,14 +102,27 @@ export function toolCompleted(request, value) {
102
102
  error: {
103
103
  code: String(outcome.code ?? "tool.failed"),
104
104
  message: String(outcome.message ?? ""),
105
+ // `credential_rejected` (R2b C1): which server, and which vault's credential.
106
+ ...(typeof outcome.server === "string" ? { server: outcome.server } : {}),
107
+ ...(outcome.vault === "installation" || outcome.vault === "user"
108
+ ? { vault: outcome.vault }
109
+ : {}),
110
+ ...(typeof outcome.retryable === "boolean" ? { retryable: outcome.retryable } : {}),
105
111
  },
106
112
  }
107
113
  : { output: outcome?.kind === "completed" ? outcome.output : value };
114
+ // Credential values the server echoed, replaced before the outcome was recorded (R2b C8).
115
+ const redacted = (outcome?.kind === "completed" || outcome?.kind === "failed") &&
116
+ typeof outcome.redacted === "number" &&
117
+ outcome.redacted > 0
118
+ ? { redacted: outcome.redacted }
119
+ : {};
108
120
  return {
109
121
  ...ids,
110
122
  capabilityId: request.capabilityId,
111
123
  toolName: request.toolName,
112
124
  ...result,
125
+ ...redacted,
113
126
  ...agentOf(request),
114
127
  };
115
128
  }
@@ -8,7 +8,6 @@ import type { SessionStore } from "../store/types.js";
8
8
  import type { SandboxBackend } from "../sandbox/types.js";
9
9
  import type { TenantCause } from "./cause.js";
10
10
  import type { OutboundPolicy } from "./outbound.js";
11
- import type { ResolverConfig } from "../vault/sources.js";
12
11
  import type { TenantWorker } from "./worker.js";
13
12
  export type TenantMode = "shared" | "ephemeral" | "test";
14
13
  /**
@@ -75,24 +74,11 @@ export interface TenantConfig {
75
74
  * never reads ambient process environment for flow limits.
76
75
  */
77
76
  flowEnv?: Readonly<Record<string, string | undefined>>;
78
- vaultFetch?: typeof fetch;
79
77
  /**
80
78
  * The trusted issuers whose JWTs the Tenant API accepts (Host feature `trusted-issuers`), from
81
79
  * the identity file (`NYLORUN_IDENTITY_FILE`). Absent: none.
82
80
  */
83
81
  issuers?: import("./issuers.js").TrustedIssuers;
84
- /**
85
- * The operator's credential resolver (F9 C1), asked for a person's MCP credential when the
86
- * session's vaults hold none. Used where this process authorizes MCP calls itself (embedding,
87
- * the ephemeral Runtime, tests); the gateway takes `NYLORUN_RESOLVER_*` instead.
88
- */
89
- resolver?: ResolverConfig;
90
- /**
91
- * The URL browsers reach this Tenant API at (`NYLORUN_PUBLIC_URL`, no trailing slash): the
92
- * base of the MCP OAuth callback, `<publicUrl>/v1/oauth/callback` (F9 C2). Absent (embedding,
93
- * tests): the origin the start request was sent to.
94
- */
95
- publicUrl?: string;
96
82
  logger: Logger;
97
83
  }
98
84
  export interface Logger {
@@ -10,10 +10,3 @@ export declare function encryptSecret(kek: Buffer, aad: Buffer, plaintext: Buffe
10
10
  kekId: string;
11
11
  };
12
12
  export declare function decryptSecret(kek: Buffer, aad: Buffer, nonce: Buffer, ciphertext: Buffer, wrappedDek: Buffer, expectedKekId: string): Buffer;
13
- /**
14
- * `encryptSecret` in one column: nonce, wrapped DEK, then ciphertext and tag. For rows that
15
- * seal several short secrets under one `kekId` (`oauth_pending`, F9 C2).
16
- */
17
- export declare function sealBytes(kek: Buffer, aad: Buffer, plaintext: Buffer): Buffer;
18
- /** Opens what `sealBytes` sealed; `VaultCryptoError` when it cannot. */
19
- export declare function openBytes(kek: Buffer, aad: Buffer, sealed: Buffer, expectedKekId: string): Buffer;