@nylorun/runtime 0.20.0-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 (95) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +15 -10
  3. package/dist/api/components.d.ts +155 -36
  4. package/dist/api/components.js +3 -3
  5. package/dist/api/http/app.js +2 -0
  6. package/dist/api/http/routes/mcp.d.ts +9 -0
  7. package/dist/api/http/routes/mcp.js +32 -0
  8. package/dist/api/http/routes/vaults.d.ts +1 -9
  9. package/dist/api/http/routes/vaults.js +3 -103
  10. package/dist/api/openapi.js +6 -3
  11. package/dist/artifacts/media-types.d.ts +2 -0
  12. package/dist/artifacts/media-types.js +5 -0
  13. package/dist/gates/http-tool.d.ts +7 -0
  14. package/dist/gates/http-tool.js +34 -3
  15. package/dist/gates/mcp-handler.d.ts +4 -1
  16. package/dist/gates/mcp-handler.js +64 -8
  17. package/dist/gates/tenant-vaults.d.ts +9 -15
  18. package/dist/gates/tenant-vaults.js +9 -17
  19. package/dist/gates/tool-client.js +36 -17
  20. package/dist/gates/tool-contract.d.ts +19 -3
  21. package/dist/gates/tool-contract.js +9 -3
  22. package/dist/gates/tool-gate.d.ts +2 -2
  23. package/dist/harness/calls.d.ts +2 -0
  24. package/dist/harness/calls.js +13 -3
  25. package/dist/harness-api/record.d.ts +7 -6
  26. package/dist/harness-api/record.js +99 -17
  27. package/dist/harness-api/server.js +35 -27
  28. package/dist/harness-api/start.js +2 -1
  29. package/dist/host/config-for.d.ts +0 -2
  30. package/dist/host/config-for.js +0 -1
  31. package/dist/host/gates.js +0 -1
  32. package/dist/host/main.js +13 -3
  33. package/dist/host/stack-config.d.ts +5 -8
  34. package/dist/host/stack-config.js +8 -19
  35. package/dist/index.d.ts +0 -1
  36. package/dist/keys/client.js +1 -9
  37. package/dist/keys/keys.d.ts +11 -13
  38. package/dist/keys/keys.js +5 -7
  39. package/dist/management-openapi.json +315 -345
  40. package/dist/mcp/connect.d.ts +159 -13
  41. package/dist/mcp/connect.js +538 -55
  42. package/dist/mcp/pool.d.ts +11 -2
  43. package/dist/mcp/pool.js +74 -27
  44. package/dist/mcp/preview.d.ts +35 -0
  45. package/dist/mcp/preview.js +215 -0
  46. package/dist/mcp/snapshot.d.ts +79 -5
  47. package/dist/mcp/snapshot.js +145 -27
  48. package/dist/model/catalog.d.ts +14 -0
  49. package/dist/model/catalog.js +14 -1
  50. package/dist/model/pi-model.js +36 -2
  51. package/dist/openapi.json +362 -123
  52. package/dist/redact.d.ts +19 -1
  53. package/dist/redact.js +42 -13
  54. package/dist/sandbox/session-sandbox.d.ts +7 -0
  55. package/dist/sandbox/session-sandbox.js +89 -28
  56. package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
  57. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  58. package/dist/store/postgres/schema.d.ts +2 -261
  59. package/dist/store/postgres/schema.js +2 -36
  60. package/dist/store/postgres/store.js +1 -18
  61. package/dist/store/postgres/tenant.d.ts +14 -0
  62. package/dist/store/postgres/tenant.js +22 -2
  63. package/dist/store/types.d.ts +2 -8
  64. package/dist/tenant/artifact-tool.d.ts +9 -1
  65. package/dist/tenant/artifact-tool.js +113 -9
  66. package/dist/tenant/context.d.ts +5 -6
  67. package/dist/tenant/effects.d.ts +2 -2
  68. package/dist/tenant/effects.js +20 -5
  69. package/dist/tenant/ephemeral.d.ts +0 -6
  70. package/dist/tenant/ephemeral.js +0 -1
  71. package/dist/tenant/outbound.d.ts +14 -5
  72. package/dist/tenant/outbound.js +21 -9
  73. package/dist/tenant/runtime.js +9 -14
  74. package/dist/tenant/sessions.js +20 -3
  75. package/dist/tenant/store-pg.js +9 -0
  76. package/dist/tenant/tool-results.d.ts +57 -0
  77. package/dist/tenant/tool-results.js +298 -0
  78. package/dist/tenant/tool-search.d.ts +43 -0
  79. package/dist/tenant/tool-search.js +100 -0
  80. package/dist/tenant/transcript.d.ts +108 -0
  81. package/dist/tenant/transcript.js +13 -0
  82. package/dist/tenant/types.d.ts +0 -14
  83. package/dist/vault/crypto.d.ts +0 -7
  84. package/dist/vault/crypto.js +0 -16
  85. package/dist/vault/headers.d.ts +17 -0
  86. package/dist/vault/headers.js +61 -0
  87. package/dist/vault/service.d.ts +11 -68
  88. package/dist/vault/service.js +83 -438
  89. package/dist/vault/sources.d.ts +20 -48
  90. package/dist/vault/sources.js +25 -189
  91. package/dist/version.d.ts +1 -1
  92. package/dist/version.js +1 -1
  93. package/package.json +3 -3
  94. package/dist/vault/oauth.d.ts +0 -55
  95. package/dist/vault/oauth.js +0 -186
@@ -11,7 +11,9 @@
11
11
  * Remote MCP. The gate holds the connection and the vault credential; the loop names the
12
12
  * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
13
13
  * - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
14
- * authorization included), or keeps the open one.
14
+ * authorization included), or keeps the open one. Its result is `{instructions}` when the
15
+ * server gave instructions at `initialize` (R2b C10, for the note on its deferred tools), else
16
+ * `null`.
15
17
  * - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
16
18
  * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
17
19
  * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
@@ -20,8 +22,12 @@
20
22
  * gate_forbidden` for another session's call under a run token.
21
23
  * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
22
24
  * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
23
- * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
24
- * `{ok: false, error: {uncertain: true}}` and is never run again.
25
+ * answer, not a gate error. A tool call's failure is coded (`error.failure`, R2b C7), and neither
26
+ * a result nor a failure carries a credential value the gate sent (C8). An answer past 8 MiB is
27
+ * `mcp.too-large` (C11), as an HTTP tool's is `http.too-large`; core shapes what fits into a
28
+ * result the model can take (`tenant/tool-results.ts`). A keyed call whose
29
+ * earlier attempt was lost with the gateway answers `{ok: false, error: {uncertain: true}}` and
30
+ * is never run again.
25
31
  *
26
32
  * HTTP tools (R2 M3). `POST /nylorun/v1/http-calls` `{tool, effectId, turnId, input}`: one call
27
33
  * of an HTTP tool. The loop names the tool (`{sessionId?, agentId?, capabilityId, toolName}`), or a
@@ -33,6 +39,7 @@
33
39
  * model sees), or `{ok: false, error: {uncertain: true}}` for a call lost with the gateway.
34
40
  */
35
41
  import { z } from "zod";
42
+ import type { McpCallFailure } from "../mcp/connect.js";
36
43
  export declare const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
37
44
  export declare const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
38
45
  export declare const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
@@ -99,10 +106,19 @@ export interface McpGateError {
99
106
  readonly credentialIds?: readonly string[];
100
107
  /** The call may have run: its earlier attempt was lost with the gateway. Never re-run. */
101
108
  readonly uncertain?: boolean;
109
+ /**
110
+ * How the request failed, coded (R2b C1, C7): the loop's `McpCallFailed`, which a tool call
111
+ * turns into a failed outcome the model sees (`mcp.lost` only for a read-only or idempotent
112
+ * tool). `credential_rejected` for a `401`, with the server and the scope of the vault whose
113
+ * credential was sent. Its message has no credential value in it (C8).
114
+ */
115
+ readonly failure?: McpCallFailure;
102
116
  }
103
117
  export type McpAnswer<T> = {
104
118
  readonly ok: true;
105
119
  readonly result: T;
120
+ /** A `tools/call`'s: how many credential values its result echoed, replaced (R2b C8). */
121
+ readonly redacted?: number;
106
122
  } | {
107
123
  readonly ok: false;
108
124
  readonly error: McpGateError;
@@ -11,7 +11,9 @@
11
11
  * Remote MCP. The gate holds the connection and the vault credential; the loop names the
12
12
  * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
13
13
  * - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
14
- * authorization included), or keeps the open one.
14
+ * authorization included), or keeps the open one. Its result is `{instructions}` when the
15
+ * server gave instructions at `initialize` (R2b C10, for the note on its deferred tools), else
16
+ * `null`.
15
17
  * - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
16
18
  * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
17
19
  * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
@@ -20,8 +22,12 @@
20
22
  * gate_forbidden` for another session's call under a run token.
21
23
  * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
22
24
  * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
23
- * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
24
- * `{ok: false, error: {uncertain: true}}` and is never run again.
25
+ * answer, not a gate error. A tool call's failure is coded (`error.failure`, R2b C7), and neither
26
+ * a result nor a failure carries a credential value the gate sent (C8). An answer past 8 MiB is
27
+ * `mcp.too-large` (C11), as an HTTP tool's is `http.too-large`; core shapes what fits into a
28
+ * result the model can take (`tenant/tool-results.ts`). A keyed call whose
29
+ * earlier attempt was lost with the gateway answers `{ok: false, error: {uncertain: true}}` and
30
+ * is never run again.
25
31
  *
26
32
  * HTTP tools (R2 M3). `POST /nylorun/v1/http-calls` `{tool, effectId, turnId, input}`: one call
27
33
  * of an HTTP tool. The loop names the tool (`{sessionId?, agentId?, capabilityId, toolName}`), or a
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The Tool Gate seam (blueprint §12, F4.1). Every tool call that leaves the loop crosses one
3
3
  * `ToolGate`: a remote MCP server's connection and calls, and an HTTP tool's request (R2 M3).
4
- * The gate owns what those need that the loop must not hold: the MCP and HTTP tool credentials
5
- * and their OAuth refresh, and the outbound route.
4
+ * The gate owns what those need that the loop must not hold: the MCP and HTTP tool credentials,
5
+ * and the outbound route.
6
6
  *
7
7
  * Two implementations: `inProcessToolGate` (embedding, the ephemeral Runtime, tests), which
8
8
  * opens remote servers and calls HTTP tools in this process, and the HTTP client of the gates
@@ -46,6 +46,8 @@ export declare function invokeModel(route: ModelRoute, request: HostEffect, sign
46
46
  export declare function abortOn(signal: AbortSignal, kinds: readonly AbortReason[]): AbortSignal;
47
47
  /** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
48
48
  export declare function isSaveArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
49
+ /** True when `request` calls `read_artifact` (R2b C11) of the `nylorun.artifacts` capability. */
50
+ export declare function isReadArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
49
51
  /** True when `request` calls a tool of a declared MCP server (every one is remote). */
50
52
  export declare function isRemoteMcpCall(routing: Pick<ToolRouting, "rootManifest" | "mcpSnapshot">, request: HostEffect): boolean;
51
53
  export declare function callMcpTool(mcp: McpPool, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
@@ -3,7 +3,7 @@
3
3
  * Tenant's provider), the Tool Gate, the MCP pool, the SandboxManager. No store, no record: the
4
4
  * session's routing comes with the run (`TurnStart.routing`).
5
5
  */
6
- import { ARTIFACTS_CAPABILITY_ID, SAVE_ARTIFACT_TOOL } from "@nylorun/core/define";
6
+ import { ARTIFACTS_CAPABILITY_ID, READ_ARTIFACT_TOOL, SAVE_ARTIFACT_TOOL, } from "@nylorun/core/define";
7
7
  import { runAbortKind } from "@nylorun/harness/api";
8
8
  import { httpToolRefOf } from "../gates/http-tool.js";
9
9
  import { classifyThrown } from "../model/classify.js";
@@ -67,10 +67,18 @@ export function abortOn(signal, kinds) {
67
67
  }
68
68
  /** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
69
69
  export function isSaveArtifactCall(manifest, request) {
70
+ return isArtifactsToolCall(manifest, request, SAVE_ARTIFACT_TOOL);
71
+ }
72
+ /** True when `request` calls `read_artifact` (R2b C11) of the `nylorun.artifacts` capability. */
73
+ export function isReadArtifactCall(manifest, request) {
74
+ return isArtifactsToolCall(manifest, request, READ_ARTIFACT_TOOL);
75
+ }
76
+ function isArtifactsToolCall(manifest, request, toolName) {
70
77
  return (request.kind === "tool" &&
71
78
  request.capabilityId === ARTIFACTS_CAPABILITY_ID &&
72
- request.toolName === SAVE_ARTIFACT_TOOL &&
73
- manifest?.capabilities.some((capability) => capability.id === ARTIFACTS_CAPABILITY_ID) === true);
79
+ request.toolName === toolName &&
80
+ manifest?.capabilities.some((capability) => capability.id === ARTIFACTS_CAPABILITY_ID &&
81
+ capability.tools?.some((tool) => tool.name === toolName) === true) === true);
74
82
  }
75
83
  /** True when `request` calls a tool of a declared MCP server (every one is remote). */
76
84
  export function isRemoteMcpCall(routing, request) {
@@ -94,6 +102,8 @@ export async function callMcpTool(mcp, routing, request, signal) {
94
102
  args: request.input,
95
103
  manifest: routing.rootManifest,
96
104
  effectId: request.effectId,
105
+ // A read-only or idempotent tool's lost answer is the model's to retry (R2b C7, Q15).
106
+ retrySafe: tool.annotations?.readOnlyHint === true || tool.annotations?.idempotentHint === true,
97
107
  signal,
98
108
  });
99
109
  }
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * The Record seam of the Harness API: core's journal for the effects of a run. `recordIntent`
3
3
  * journals an effect before anything runs it, and either answers it from the journal, hands it
4
- * to core's own executors (flow work, delegation, `save_artifact` and the skill tools), tells
5
- * the harness to `execute` it (model calls; MCP, HTTP and sandbox tools; a flow's HTTP stages
6
- * and HTTP verifiers), or fails a tool the Runtime cannot run (one that would run the
7
- * developer's code, R2 M6). `recordOutcome`
8
- * records what the harness's call returned. Both run under the advance's lease: every write is
9
- * epoch-checked (`ownedSession`), and a lost epoch writes nothing.
4
+ * to core's own executors (flow work, delegation, `save_artifact`, `read_artifact`,
5
+ * `tool_search` and the skill tools), tells the harness to `execute` it (model calls; MCP, HTTP
6
+ * and sandbox tools, a deferred MCP tool's `tool_call` among them, whose effect names the tool;
7
+ * a flow's HTTP stages and HTTP verifiers), or fails a tool the Runtime cannot run (one that
8
+ * would run the developer's code, R2 M6). `recordOutcome` records what the harness's call returned, an
9
+ * agent's MCP or HTTP tool result first shaped to fit (R2b C11). Both run under the advance's
10
+ * lease: every write is epoch-checked (`ownedSession`), and a lost epoch writes nothing.
10
11
  *
11
12
  * Rows store the request's hash (`requestHash`) and, for a model call, the request without its
12
13
  * prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * The Record seam of the Harness API: core's journal for the effects of a run. `recordIntent`
3
3
  * journals an effect before anything runs it, and either answers it from the journal, hands it
4
- * to core's own executors (flow work, delegation, `save_artifact` and the skill tools), tells
5
- * the harness to `execute` it (model calls; MCP, HTTP and sandbox tools; a flow's HTTP stages
6
- * and HTTP verifiers), or fails a tool the Runtime cannot run (one that would run the
7
- * developer's code, R2 M6). `recordOutcome`
8
- * records what the harness's call returned. Both run under the advance's lease: every write is
9
- * epoch-checked (`ownedSession`), and a lost epoch writes nothing.
4
+ * to core's own executors (flow work, delegation, `save_artifact`, `read_artifact`,
5
+ * `tool_search` and the skill tools), tells the harness to `execute` it (model calls; MCP, HTTP
6
+ * and sandbox tools, a deferred MCP tool's `tool_call` among them, whose effect names the tool;
7
+ * a flow's HTTP stages and HTTP verifiers), or fails a tool the Runtime cannot run (one that
8
+ * would run the developer's code, R2 M6). `recordOutcome` records what the harness's call returned, an
9
+ * agent's MCP or HTTP tool result first shaped to fit (R2b C11). Both run under the advance's
10
+ * lease: every write is epoch-checked (`ownedSession`), and a lost epoch writes nothing.
10
11
  *
11
12
  * Rows store the request's hash (`requestHash`) and, for a model call, the request without its
12
13
  * prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
@@ -14,9 +15,12 @@
14
15
  import { HarnessApiError, effectRequestHash, } from "@nylorun/core/harness-api";
15
16
  import { isFlowEffect, isFlowToolEffect, recordVerdict, settleAgentEffect, } from "../core/flow-host.js";
16
17
  import { sandboxCapabilityOf } from "../sandbox/capability.js";
17
- import { isSaveArtifactCall } from "../harness/calls.js";
18
- import { callSaveArtifact } from "../tenant/artifact-tool.js";
18
+ import { abortOn, isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
19
+ import { callReadArtifact, callSaveArtifact } from "../tenant/artifact-tool.js";
20
+ import { mayNeedShaping, resultBytes, resultLimit, shapeToolOutcome, } from "../tenant/tool-results.js";
19
21
  import { callSkillTool, isSkillToolCall } from "../tenant/skill-tool.js";
22
+ import { callToolSearch, isToolSearchCall } from "../tenant/tool-search.js";
23
+ import { turnManifestOf } from "../tenant/session.js";
20
24
  import { isOwnershipLost } from "../store/ownership.js";
21
25
  import { manifestFor, mcpToolOf } from "../mcp/snapshot.js";
22
26
  import { isHttpToolCall } from "../gates/http-tool.js";
@@ -109,17 +113,24 @@ export async function recordIntent(scope, effect, requestHash) {
109
113
  throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
110
114
  const executed = request.kind === "model" ||
111
115
  (request.kind === "tool" &&
112
- (mcpToolOf(s.mcpSnapshot, request) !== undefined ||
116
+ // An MCP tool the turn's manifest disables is not one (R2b C9).
117
+ (mcpToolOf(s.mcpSnapshot, request, turnManifestOf(s)) !== undefined ||
113
118
  // HTTP tools (R2 M3) cross the Tool Gate.
114
119
  isHttpToolCall(s.manifest, request) ||
115
120
  sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName) !== undefined ||
116
- // `save_artifact` (F8.1) runs beside the sandbox tools, and the skill tools (R2 M4).
121
+ // `save_artifact` (F8.1) runs beside the sandbox tools, as do `read_artifact` (R2b
122
+ // C11) and the skill tools (R2 M4).
117
123
  isSaveArtifactCall(agentManifest, request) ||
118
- isSkillToolCall(agentManifest, request)));
124
+ isReadArtifactCall(agentManifest, request) ||
125
+ isSkillToolCall(agentManifest, request) ||
126
+ // `tool_search` (R2b C10) reads the session's pinned MCP snapshot: core serves it.
127
+ isToolSearchCall(agentManifest, request)));
119
128
  if (!executed) {
120
129
  // A tool that would run the developer's code: refused at save (a turn's manifest only
121
- // removes tools), so this is a backstop. The model sees the failure; nothing runs.
122
- const outcome = { value: unrunnableTool(request) };
130
+ // removes tools), so this is a backstop, as is an MCP tool the turn disabled, which the
131
+ // model was never offered. The model sees the failure; nothing runs.
132
+ const disabled = request.kind === "tool" && mcpToolOf(s.mcpSnapshot, request) !== undefined;
133
+ const outcome = { value: disabled ? disabledTool(request) : unrunnableTool(request) };
123
134
  await t.put("effects", request.effectId, { request, requestHash, status: "completed", outcome });
124
135
  const transcript = toolCompleted(request, outcome.value);
125
136
  if (transcript)
@@ -135,13 +146,22 @@ export async function recordIntent(scope, effect, requestHash) {
135
146
  // workspace capability, wherever the sandbox is.
136
147
  if (request.kind === "tool" && isSaveArtifactCall(agentManifest, request))
137
148
  return "save";
149
+ // `read_artifact` reads the session's artifacts: core serves it, with or without a sandbox.
150
+ if (isReadArtifactCall(agentManifest, request))
151
+ return "read";
138
152
  // The skill tools read the agent's definition files: core serves them.
139
153
  if (isSkillToolCall(agentManifest, request))
140
154
  return { skill: agentManifest };
155
+ if (isToolSearchCall(agentManifest, request))
156
+ return "search";
141
157
  return { status: "execute" };
142
158
  });
143
159
  if (answer === "save")
144
160
  return runInCore(scope, request, () => callSaveArtifact(scope.ctx, request, scope.signal));
161
+ if (answer === "read")
162
+ return runInCore(scope, request, () => callReadArtifact(scope.ctx, request, scope.signal));
163
+ if (answer === "search")
164
+ return runInCore(scope, request, () => callToolSearch(scope.ctx, request));
145
165
  if (typeof answer === "object" && "skill" in answer)
146
166
  return runInCore(scope, request, () => callSkillTool(scope.ctx, answer.skill, request, scope.signal));
147
167
  if (answer !== "flow")
@@ -151,9 +171,10 @@ export async function recordIntent(scope, effect, requestHash) {
151
171
  return resolveNewFlowEffect(ctx, request, signal, lease);
152
172
  }
153
173
  /**
154
- * Runs a tool core serves (`save_artifact`, F8.1; the skill tools, R2 M4) for the run and records
155
- * its outcome, as a harness would record a call it ran: the run gets the outcome as the intent's
156
- * answer.
174
+ * Runs a tool core serves (`save_artifact`, F8.1; `read_artifact`, R2b C11; `tool_search`, R2b
175
+ * C10; the skill tools, R2 M4) for the run and records its outcome, as a harness would record a
176
+ * call it ran: the run gets
177
+ * the outcome as the intent's answer.
157
178
  */
158
179
  async function runInCore(scope, request, call) {
159
180
  let value;
@@ -169,6 +190,14 @@ async function runInCore(scope, request, call) {
169
190
  }
170
191
  return recordOutcome(scope, request.effectId, { value });
171
192
  }
193
+ /** The failed outcome of an MCP tool the turn's manifest disables (R2b C9). */
194
+ function disabledTool(request) {
195
+ return {
196
+ kind: "failed",
197
+ code: "tool.disabled",
198
+ message: `Tool '${request.toolName ?? ""}' is disabled by its MCP server's tools settings.`,
199
+ };
200
+ }
172
201
  /** The failed outcome of a tool the Runtime cannot run: it would run the developer's code. */
173
202
  function unrunnableTool(request) {
174
203
  return {
@@ -199,7 +228,7 @@ export async function recordOutcome(scope, effectId, result) {
199
228
  });
200
229
  return { status: "uncertain" };
201
230
  }
202
- const { value } = result;
231
+ const value = await shaped(scope, effectId, result.value);
203
232
  return ctx.store.tx(async (t) => {
204
233
  const s = await t.assertEpoch(lease.sessionId, lease.epoch);
205
234
  const effect = await t.get("effects", effectId);
@@ -234,6 +263,59 @@ export async function recordOutcome(scope, effectId, result) {
234
263
  return { status: "completed", outcome: effect.outcome };
235
264
  });
236
265
  }
266
+ /**
267
+ * The outcome of an agent's remote MCP or HTTP tool call, shaped to fit before anything records
268
+ * it (R2b C11, `tenant/tool-results.ts`): what does not fit becomes the session's artifacts. Any
269
+ * other outcome, and one a cancel will discard, as it is. Only a cancel stops the upload: after a
270
+ * shutdown or a deadline the outcome is still recorded, so the next advance replays it.
271
+ *
272
+ * Its cap is what its step's budget has left (`resultLimit`): a step's results are one transcript
273
+ * entry. Parallel results are counted under the session lock, each reserving its share on its
274
+ * row (`resultBytes`) before it is shaped, so two never spend the same room.
275
+ */
276
+ async function shaped(scope, effectId, value) {
277
+ const { ctx, lease, signal } = scope;
278
+ if (abortKind(signal) === "cancel" || !mayNeedShaping(value))
279
+ return value;
280
+ const call = await ctx.store.tx(async (t) => {
281
+ const s = await t.lockSession(lease.sessionId);
282
+ const effect = await t.get("effects", effectId);
283
+ if (!s || !effect || effect.status === "completed")
284
+ return undefined;
285
+ const request = effect.request;
286
+ // An agent's tool; a flow's HTTP stage or verifier names no capability, and its output is data.
287
+ if (request.kind !== "tool" || request.capabilityId === undefined)
288
+ return undefined;
289
+ const mcp = mcpToolOf(s.mcpSnapshot, request) !== undefined;
290
+ if (!mcp && !isHttpToolCall(s.manifest, request))
291
+ return undefined;
292
+ const step = stepOf(request);
293
+ let used = 0;
294
+ if (step !== undefined)
295
+ for (const other of await t.effectsForTurn(s.id, request.turnId, [
296
+ "invoking",
297
+ "completed",
298
+ ])) {
299
+ if (other.request.effectId === effectId || stepOf(other.request) !== step)
300
+ continue;
301
+ used +=
302
+ other.resultBytes ??
303
+ (other.status === "completed" ? resultBytes(other.outcome?.value) : 0);
304
+ }
305
+ const limit = resultLimit(used);
306
+ effect.resultBytes = Math.min(resultBytes(value), limit);
307
+ await t.put("effects", effectId, effect);
308
+ return { request, session: { id: s.id, ownerUserId: s.ownerUserId }, mcp, limit };
309
+ });
310
+ return call ? shapeToolOutcome(ctx, call, value, abortOn(signal, ["cancel"])) : value;
311
+ }
312
+ /** The model step a tool call answers, and whose agent: its results share one transcript entry. */
313
+ function stepOf(request) {
314
+ if (request.kind !== "tool")
315
+ return undefined;
316
+ const stepId = request.context?.stepId;
317
+ return typeof stepId === "string" ? JSON.stringify([request.agent?.id ?? null, stepId]) : undefined;
318
+ }
237
319
  /** What a journal row keeps of a request: a model call without its prompt. */
238
320
  function storedRequest(request) {
239
321
  if (request.kind !== "model" || request.input === undefined)
@@ -29,7 +29,9 @@ import { HARNESS_API_VERSION, HARNESS_CLAIMS, HarnessApiError, } from "@nylorun/
29
29
  import { definitionFilesOf } from "@nylorun/core/contracts";
30
30
  import { isOwnershipLost } from "../store/ownership.js";
31
31
  import { ownedSession } from "../tenant/context.js";
32
- import { mcpDiscovered, sessionToolsOf } from "../mcp/snapshot.js";
32
+ import { mcpDiscovered, pinDeferral, sessionToolsOf, } from "../mcp/snapshot.js";
33
+ import { contextWindowOf } from "../model/catalog.js";
34
+ import { turnManifestOf } from "../tenant/session.js";
33
35
  import { sandboxWorkspaceKey, workspacePrefix } from "../sandbox/records.js";
34
36
  import { abortKind } from "../tenant/worker.js";
35
37
  import { readDefinitionFile } from "../tenant/definition-files.js";
@@ -194,36 +196,42 @@ export function createHarnessApiServer(ctx, options = {}) {
194
196
  * session wins, with `mcp.discovered` in its log; diagnostics replace those of the same server.
195
197
  * Answers the session's snapshot and the tools the engine advertises.
196
198
  */
197
- const recordMcp = (run, params) => ctx.store.tx(async (t) => {
198
- const { lease } = run.offer;
199
- const current = await ownedSession(t, lease, lease.sessionId);
200
- const diagnostics = params.diagnostics;
199
+ const recordMcp = async (run, params) => {
201
200
  const snapshot = params.snapshot;
202
201
  if (snapshot !== undefined && !isSnapshot(snapshot))
203
202
  throw new HarnessApiError("invalid", "The MCP snapshot is malformed");
204
- if (snapshot && !current.mcpSnapshot) {
205
- current.mcpSnapshot = snapshot;
206
- current.mcpDiagnostics = diagnostics;
207
- await t.put("sessions", current.id, current);
208
- await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(snapshot, diagnostics));
209
- }
210
- else if (diagnostics.length > 0) {
211
- const prior = [...(current.mcpDiagnostics ?? [])];
212
- for (const item of diagnostics) {
213
- const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId && existing.serverName === item.serverName);
214
- if (index >= 0)
215
- prior[index] = item;
216
- else
217
- prior.push(item);
203
+ // Which MCP tools the model reaches through tool_search, decided once for the session from
204
+ // the Tenant's model's context window (R2b C10, Q21).
205
+ const contextWindow = snapshot ? contextWindowOf(await ctx.vault.getHostModel()) : 0;
206
+ return ctx.store.tx(async (t) => {
207
+ const { lease } = run.offer;
208
+ const current = await ownedSession(t, lease, lease.sessionId);
209
+ const diagnostics = params.diagnostics;
210
+ if (snapshot && !current.mcpSnapshot) {
211
+ current.mcpSnapshot = pinDeferral(snapshot, current.manifest, contextWindow);
212
+ current.mcpDiagnostics = diagnostics;
213
+ await t.put("sessions", current.id, current);
214
+ await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics));
218
215
  }
219
- current.mcpDiagnostics = prior;
220
- await t.put("sessions", current.id, current);
221
- }
222
- return {
223
- ...(current.mcpSnapshot ? { snapshot: current.mcpSnapshot } : {}),
224
- sessionTools: [...(sessionToolsOf(current.mcpSnapshot, current.manifest) ?? [])],
225
- };
226
- });
216
+ else if (diagnostics.length > 0) {
217
+ const prior = [...(current.mcpDiagnostics ?? [])];
218
+ for (const item of diagnostics) {
219
+ const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId && existing.serverName === item.serverName);
220
+ if (index >= 0)
221
+ prior[index] = item;
222
+ else
223
+ prior.push(item);
224
+ }
225
+ current.mcpDiagnostics = prior;
226
+ await t.put("sessions", current.id, current);
227
+ }
228
+ return {
229
+ ...(current.mcpSnapshot ? { snapshot: current.mcpSnapshot } : {}),
230
+ // The turn's manifest: a variant may have disabled tools or required their approval.
231
+ sessionTools: [...(sessionToolsOf(current.mcpSnapshot, turnManifestOf(current)) ?? [])],
232
+ };
233
+ });
234
+ };
227
235
  /** A definition file the run's definition names (its skills', for its sandbox), base64. */
228
236
  const definitionFile = async (run, sha256, signal) => {
229
237
  if (!definitionFilesOf(run.offer.start.routing.rootManifest).has(sha256))
@@ -57,7 +57,8 @@ export function buildTurnStart(ctx, segment) {
57
57
  checkpoint: !flow && cp.state
58
58
  ? { ...cp, state: withTranscript(cp.state, []) }
59
59
  : cp,
60
- ...(flow ? {} : { sessionTools: sessionToolsOf(current.mcpSnapshot, current.manifest) ?? [] }),
60
+ // The turn's manifest: a variant may have disabled MCP tools or required their approval.
61
+ ...(flow ? {} : { sessionTools: sessionToolsOf(current.mcpSnapshot, turnManifestOf(current)) ?? [] }),
61
62
  outcomes: segment.outcomes,
62
63
  transcript: { cursor: segment.cursor },
63
64
  options: {
@@ -14,6 +14,4 @@ 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
- /** `NYLORUN_PUBLIC_URL`: the base of the MCP OAuth callback (F9 C2). */
18
- publicUrl?: string;
19
17
  }): (id: string) => TenantConfig;
@@ -16,7 +16,6 @@ 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 } : {}),
20
19
  logger: {
21
20
  info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
22
21
  warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
@@ -38,7 +38,6 @@ export async function startGates(options) {
38
38
  vaults = createTenantVaults({
39
39
  sql: database,
40
40
  hostRoot: options.hostRoot,
41
- ...(gates.resolver ? { resolver: gates.resolver } : {}),
42
41
  ...(options.delivery ? { delivery: options.delivery } : {}),
43
42
  });
44
43
  }
package/dist/host/main.js CHANGED
@@ -139,6 +139,17 @@ async function ensureBucket(store, bucket, logger) {
139
139
  }
140
140
  }
141
141
  }
142
+ /**
143
+ * Settings of a removed feature that are still set: logged, never fatal. The credential
144
+ * resolver (`NYLORUN_RESOLVER_*`) is gone in protocol 10. Never logs the values.
145
+ */
146
+ function warnRemovedSettings(stack, logger) {
147
+ if (stack.removedResolverVariables)
148
+ logger.warn("resolver_removed", {
149
+ variables: stack.removedResolverVariables,
150
+ message: "The credential resolver is gone (protocol 10) and these variables are ignored: keep a person's keys in their user vault, or reach their accounts through a gateway",
151
+ });
152
+ }
142
153
  /**
143
154
  * The gateway's services: the Model Gate's listener (gates, keys) and egress-gate's (egress), over
144
155
  * the Postgres pool and the Host's tenant directory. Writes nothing to the Host root (the local
@@ -153,9 +164,8 @@ async function runGates(stack) {
153
164
  ...(stack.packing ? { packing: stack.packing } : {}),
154
165
  endpoints: describeEndpoints(stack.endpoints),
155
166
  objectStore: stack.objectStore ? "s3" : "none",
156
- // Never the resolver's URL or token: only whether one is set (F9 C1).
157
- resolver: stack.gates?.resolver ? "configured" : "none",
158
167
  });
168
+ warnRemovedSettings(stack, logger);
159
169
  const database = createDatabase(stack);
160
170
  const servers = [];
161
171
  try {
@@ -234,6 +244,7 @@ export async function main() {
234
244
  file: identityFile,
235
245
  issuers: issuers.issuers.map((issuer) => issuer.config.name),
236
246
  });
247
+ warnRemovedSettings(stack, logger);
237
248
  if (stack.deprecatedRole)
238
249
  logger.warn("deprecated_flag", {
239
250
  flag: "--role",
@@ -261,7 +272,6 @@ export async function main() {
261
272
  logger,
262
273
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
263
274
  ...(issuers ? { issuers } : {}),
264
- ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
265
275
  });
266
276
  // The process's Durable Session Execution: Restate when its endpoints are set, else the
267
277
  // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
@@ -54,14 +54,6 @@ export interface GatesConfig {
54
54
  listen: ContainerListen;
55
55
  /** `NYLORUN_GATES_TOKEN`: the bearer the loop presents; at least 32 bytes as hex. */
56
56
  token: string;
57
- /**
58
- * The operator's credential resolver (`NYLORUN_RESOLVER_URL`, `NYLORUN_RESOLVER_TOKEN`), asked
59
- * for a person's MCP credential when the session's vaults hold none (F9 C1). Absent: vaults only.
60
- */
61
- resolver?: {
62
- url: string;
63
- token: string;
64
- };
65
57
  }
66
58
  /** egress-gate's listener (`NYLORUN_EGRESS_LISTEN_HOST`, `NYLORUN_EGRESS_LISTEN_PORT`). */
67
59
  export interface EgressConfig {
@@ -168,6 +160,11 @@ export interface StackConfig {
168
160
  packing?: "combined" | "split";
169
161
  /** Set when the process was started with the deprecated `--role` (logged at startup). */
170
162
  deprecatedRole?: RuntimeRole;
163
+ /**
164
+ * The removed credential resolver's variables (`NYLORUN_RESOLVER_*`, protocol 10) the
165
+ * environment still sets: ignored, and logged at startup as `resolver_removed`.
166
+ */
167
+ removedResolverVariables?: string[];
171
168
  /** Present in container mode; absent means bind what host.json names. */
172
169
  listen?: ContainerListen;
173
170
  endpoints: StackEndpoints;
@@ -15,8 +15,8 @@
15
15
  * `NYLORUN_GATES_TOKEN`. In a container, loop requires the gate: the loop
16
16
  * process must never hold a model credential. `egress` (egress-gate, F7.2) joins gates and keys
17
17
  * in the gateway and parses its listener (`NYLORUN_EGRESS_LISTEN_*`, default `0.0.0.0:4200`).
18
- * Only gates reads the operator's credential resolver (`NYLORUN_RESOLVER_URL`,
19
- * `NYLORUN_RESOLVER_TOKEN`, F9 C1): remote MCP calls are authorized in the gateway.
18
+ * The credential resolver is gone (protocol 10): its `NYLORUN_RESOLVER_*` variables are ignored,
19
+ * and the Host logs `resolver_removed` when one is set.
20
20
  *
21
21
  * `harness` (F6.2) runs alone: it holds the harness credential and nothing else. It connects to
22
22
  * core's Harness API listener (`NYLORUN_HARNESS_URL`, `NYLORUN_HARNESS_TOKEN`), calls models and
@@ -304,9 +304,13 @@ export function parseStackConfig(env, argv) {
304
304
  const delivery = parseDelivery(env);
305
305
  const tenant = servesApi ? parseTenant(env) : undefined;
306
306
  const objectStore = parseObjectStore(env);
307
+ const removedResolverVariables = Object.keys(env)
308
+ .filter((name) => name.startsWith(REMOVED_RESOLVER_PREFIX) && read(env, name) !== undefined)
309
+ .sort();
307
310
  return {
308
311
  services,
309
312
  ...(deprecatedRole ? { deprecatedRole } : {}),
313
+ ...(removedResolverVariables.length ? { removedResolverVariables } : {}),
310
314
  ...(gates ? { gates } : {}),
311
315
  ...(egress ? { egress } : {}),
312
316
  ...(modelGate ? { modelGate } : {}),
@@ -509,28 +513,13 @@ function parseGates(env) {
509
513
  throw new StackConfigError("NYLORUN_GATES_TOKEN is required for --service gates: the token the loop presents (`nylorun start` sets it)");
510
514
  if (!GATES_TOKEN.test(token))
511
515
  throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
512
- const resolver = parseResolver(env);
513
516
  return {
514
517
  listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
515
518
  token,
516
- ...(resolver ? { resolver } : {}),
517
519
  };
518
520
  }
519
- /** `GatesConfig.resolver` from `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN`: both or neither. */
520
- function parseResolver(env) {
521
- const url = parseUrl(env, "NYLORUN_RESOLVER_URL", ["http:", "https:"]);
522
- const token = read(env, "NYLORUN_RESOLVER_TOKEN");
523
- if (url === undefined) {
524
- if (token !== undefined)
525
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is set without NYLORUN_RESOLVER_URL: set the URL of your credential resolver");
526
- return undefined;
527
- }
528
- if (token === undefined)
529
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is required with NYLORUN_RESOLVER_URL: the bearer the gateway presents to your credential resolver");
530
- if (/\s/.test(token))
531
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN cannot contain whitespace");
532
- return { url, token };
533
- }
521
+ /** The prefix of the credential resolver's variables (removed in protocol 10). */
522
+ const REMOVED_RESOLVER_PREFIX = "NYLORUN_RESOLVER_";
534
523
  /** `StackConfig.egress` from `NYLORUN_EGRESS_LISTEN_*`. CONNECT has no Host header to check. */
535
524
  function parseEgress(env) {
536
525
  const host = read(env, "NYLORUN_EGRESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
package/dist/index.d.ts CHANGED
@@ -4,7 +4,6 @@ export { parseIdentityFile, IdentityFileError, type TrustedIssuerConfig, } from
4
4
  export { createTrustedIssuers, type TrustedIssuers, type TrustedIssuersOptions, } from "./tenant/issuers.js";
5
5
  export type { TenantConfig, TenantHandle, TenantPaths, Logger, OpenTenantRuntime, } from "./tenant/types.js";
6
6
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
7
- export type { ResolverConfig } from "./vault/sources.js";
8
7
  export type { RuntimeAgent } from "./contracts.js";
9
8
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
10
9
  export type { RuntimeModelAdapter } from "./contracts.js";
@@ -12,18 +12,10 @@ import { KEYS_PATH } from "./contract.js";
12
12
  import { KEYS_OPERATIONS } from "./keys.js";
13
13
  /** Signing and vault writes are short; anything slower is a gateway in trouble. */
14
14
  const KEYS_TIMEOUT_MS = 30_000;
15
- /**
16
- * MCP OAuth connect calls the authorization server several times (discovery, registration, the
17
- * code exchange), each bounded by `guardedFetch`'s 30 s: these wait longer.
18
- */
19
- const SLOW_OPERATIONS = {
20
- startOAuth: 120_000,
21
- finishOAuth: 60_000,
22
- };
23
15
  export function httpKeys(options) {
24
16
  const where = new URL(options.url).origin;
25
17
  function call(operation, args) {
26
- const timeoutMs = Math.max(options.timeoutMs ?? KEYS_TIMEOUT_MS, SLOW_OPERATIONS[operation] ?? 0);
18
+ const timeoutMs = options.timeoutMs ?? KEYS_TIMEOUT_MS;
27
19
  const url = new URL(`${KEYS_PATH}/${operation}`, options.url);
28
20
  const send = url.protocol === "https:" ? httpsRequest : httpRequest;
29
21
  const payload = Buffer.from(JSON.stringify({ args }), "utf8");