@nylorun/runtime 0.38.0-beta → 0.40.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 (40) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/api/a2a/endpoint.js +1 -1
  3. package/dist/api/a2a/protocol.d.ts +6 -0
  4. package/dist/api/a2a/protocol.js +13 -0
  5. package/dist/api/a2a/routes.js +1 -0
  6. package/dist/api/components.d.ts +1726 -80
  7. package/dist/api/http/routes/agents.js +3 -3
  8. package/dist/core/turn-manifest.d.ts +23 -0
  9. package/dist/core/turn-manifest.js +36 -1
  10. package/dist/gates/policy.d.ts +13 -3
  11. package/dist/gates/policy.js +17 -4
  12. package/dist/harness/executors.d.ts +4 -2
  13. package/dist/harness/executors.js +26 -4
  14. package/dist/harness-api/record.js +4 -2
  15. package/dist/harness-api/server.js +37 -13
  16. package/dist/harness-api/start.js +43 -5
  17. package/dist/keys/keys.d.ts +6 -3
  18. package/dist/keys/keys.js +1 -1
  19. package/dist/management-openapi.json +6 -4
  20. package/dist/mcp/pool.d.ts +5 -0
  21. package/dist/mcp/pool.js +1 -1
  22. package/dist/mcp/snapshot.d.ts +3 -1
  23. package/dist/mcp/snapshot.js +5 -3
  24. package/dist/openapi.json +1637 -564
  25. package/dist/store/postgres/store.js +14 -0
  26. package/dist/store/types.d.ts +9 -0
  27. package/dist/tenant/agent-versions.d.ts +13 -5
  28. package/dist/tenant/agent-versions.js +30 -6
  29. package/dist/tenant/agents.d.ts +14 -2
  30. package/dist/tenant/agents.js +51 -13
  31. package/dist/tenant/commands.js +16 -8
  32. package/dist/tenant/context.d.ts +30 -1
  33. package/dist/tenant/session-model.d.ts +35 -6
  34. package/dist/tenant/session-model.js +77 -9
  35. package/dist/tenant/sessions.js +13 -3
  36. package/dist/vault/service.d.ts +44 -3
  37. package/dist/vault/service.js +68 -27
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +3 -3
@@ -378,7 +378,7 @@ export function agentRoutes(api) {
378
378
  path: "/v1/agents/{agentId}/secrets/{name}",
379
379
  tags: ["Agents"],
380
380
  summary: "Write an Agent's secret",
381
- description: "Creates or replaces one credential of the Agent's vault: an `environment_secret` (the variable `name` reads the sentinel `nylorun_managed` in the sandbox, and egress-gate sets the value as a header on requests to `allowedHosts`), an `environment_variable` (a plain variable), or a `bearer` token or `headers` map for the MCP servers and HTTP tools at `url`. The same name and type re-seals the value and binding; another type replaces it. A rotated value is used from the next request. A second credential for a host or a URL is `409 credential_conflict`. When the Agent's manifest declares secret slots (while it moves to another version, that version's), send only the value (`{ value }`, or `{ headers }` for a `headers` slot): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`.",
381
+ description: "Creates or replaces one credential of the Agent's vault: an `environment_secret` (the variable `name` reads the sentinel `nylorun_managed` in the sandbox, and egress-gate sets the value as a header on requests to `allowedHosts`), an `environment_variable` (a plain variable), or a `bearer` token or `headers` map for the MCP servers and HTTP tools at `url`. The same name and type re-seals the value and binding; another type replaces it. A rotated value is used from the next request. A second credential for a host or a URL is `409 credential_conflict`. When the Agent's manifest declares secret slots (while it moves to another version, that version's), send only the value (`{ value }`, or `{ headers }` for a `headers` slot): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. A `bearer` or `headers` slot's write may add `via`, where this installation sends the credential's requests (it wins over the slot's `via`; the slot's `url` still picks the credential), and the gateway's `toolPrefix`, which needs a `via` and gives way to the slot's.",
382
382
  request: {
383
383
  params: secretPath,
384
384
  body: {
@@ -395,8 +395,8 @@ export function agentRoutes(api) {
395
395
  const scope = c.get("scope");
396
396
  assertReaches(scope, id);
397
397
  const name = c.req.param("name") ?? "";
398
- const body = await secretRequestFor(c.env.tenant, id, name, await readJson(c));
399
- const secret = await c.env.tenant.keys.putEnvironmentSecret(id, name, body, actorOf(scope));
398
+ const write = await secretRequestFor(c.env.tenant, id, name, await readJson(c));
399
+ const secret = await c.env.tenant.keys.putEnvironmentSecret(id, name, write.request, actorOf(scope), ...(write.viaFromSlot ? [{ viaFromSlot: true }] : []));
400
400
  // A move that waited for this secret goes on (track T3).
401
401
  await secretWritten(c.env.tenant, id);
402
402
  return jsonResponse(200, secret);
@@ -31,6 +31,29 @@ export declare function resolveMessageManifest(args: {
31
31
  readonly messageManifest?: unknown;
32
32
  readonly store: TurnManifestStore;
33
33
  }): TurnManifestOk | TurnManifestErr;
34
+ /** What a message asks of its turn's manifest: a variant, and the tools it may not use. */
35
+ export interface TurnManifestRequest {
36
+ readonly manifest?: unknown;
37
+ readonly removeTools?: readonly string[];
38
+ }
39
+ /**
40
+ * The manifest a turn runs on `pinned`: the message's variant when it named one (validated as a
41
+ * variant of `pinned`), else `pinned`, without the tools `removeTools` names (`withoutTools`; names
42
+ * `pinned` does not have are ignored). Stores nothing: the caller keeps a manifest whose hash is
43
+ * not the pin's (`variantStore`).
44
+ */
45
+ export declare function turnManifestFor(args: {
46
+ readonly pinned: AgentManifest;
47
+ readonly pinnedHash: string;
48
+ readonly request: TurnManifestRequest;
49
+ }): (TurnManifestOk & {
50
+ readonly variant?: AgentManifest;
51
+ }) | TurnManifestErr;
52
+ /** A message's request of its turn's manifest, or undefined when it asks for none. */
53
+ export declare function turnManifestRequestOf(command: {
54
+ readonly manifest?: unknown;
55
+ readonly removeTools?: readonly string[];
56
+ }): TurnManifestRequest | undefined;
34
57
  /**
35
58
  * At turn start: if state's hash differs from the turn's, set `state.manifestHash`
36
59
  * to the turn's when both are the pin or validated variants (`loops.md` §4.2).
@@ -1,6 +1,6 @@
1
1
  import { AgentManifestSchema, } from "@nylorun/core/contracts";
2
2
  import { hashManifest } from "@nylorun/core/compatibility";
3
- import { isVariantOf } from "@nylorun/core/define";
3
+ import { isVariantOf, withoutTools } from "@nylorun/core/define";
4
4
  /**
5
5
  * Parse `message.manifest`, require it to be a variant of the session pin, and store the
6
6
  * manifest the turn runs with, by its hash. Applies to that turn only (caller must not latch).
@@ -41,6 +41,41 @@ export function resolveMessageManifest(args) {
41
41
  store: args.store,
42
42
  });
43
43
  }
44
+ /**
45
+ * The manifest a turn runs on `pinned`: the message's variant when it named one (validated as a
46
+ * variant of `pinned`), else `pinned`, without the tools `removeTools` names (`withoutTools`; names
47
+ * `pinned` does not have are ignored). Stores nothing: the caller keeps a manifest whose hash is
48
+ * not the pin's (`variantStore`).
49
+ */
50
+ export function turnManifestFor(args) {
51
+ const scratch = new Map();
52
+ const resolved = resolveMessageManifest({
53
+ pinned: args.pinned,
54
+ pinnedHash: args.pinnedHash,
55
+ messageManifest: args.request.manifest,
56
+ store: { get: (hash) => scratch.get(hash), put: (hash, manifest) => void scratch.set(hash, manifest) },
57
+ });
58
+ if (!resolved.ok)
59
+ return resolved;
60
+ // The message's variant as validated, which a re-pin validates again against the new version.
61
+ const variant = args.request.manifest === undefined ? {} : { variant: resolved.manifest };
62
+ const manifest = args.request.removeTools?.length
63
+ ? withoutTools(resolved.manifest, args.request.removeTools)
64
+ : resolved.manifest;
65
+ if (manifest === resolved.manifest)
66
+ return { ...resolved, ...variant };
67
+ return { ok: true, manifest, hash: hashManifest(manifest), ...variant };
68
+ }
69
+ /** A message's request of its turn's manifest, or undefined when it asks for none. */
70
+ export function turnManifestRequestOf(command) {
71
+ const removeTools = command.removeTools?.length ? [...new Set(command.removeTools)] : undefined;
72
+ if (command.manifest === undefined && removeTools === undefined)
73
+ return undefined;
74
+ return {
75
+ ...(command.manifest === undefined ? {} : { manifest: command.manifest }),
76
+ ...(removeTools === undefined ? {} : { removeTools }),
77
+ };
78
+ }
44
79
  /**
45
80
  * At turn start: if state's hash differs from the turn's, set `state.manifestHash`
46
81
  * to the turn's when both are the pin or validated variants (`loops.md` §4.2).
@@ -11,6 +11,10 @@
11
11
  * manifest saved before R5 step 2 keeps its `approval: "always"` as an ask: no session loses
12
12
  * the approval it was opened with. A policy that cannot be read refuses every call.
13
13
  *
14
+ * A rule's `agent` matches the agent that calls: the session's root agent by its manifest's name,
15
+ * an agent it uses as a tool by its id, and that subagent's calls by the root's name too, so a rule
16
+ * for an agent covers the agents it uses as tools.
17
+ *
14
18
  * A rule's `tool` matches the tool's identity (protocol 15): an MCP tool's own name on its
15
19
  * server (`server` names the server), an HTTP tool's declared name, a flow stage's key. The name
16
20
  * a harness gave the model never matches. A stored rule whose `tool` has `__` in it (an MCP tool
@@ -37,6 +41,11 @@ export interface PolicyCall {
37
41
  readonly server?: string;
38
42
  /** The agent that calls it: its manifest's id (an agent manifest's name), never an Agent's id. */
39
43
  readonly agent: string;
44
+ /**
45
+ * The session's root agent (its loop manifest's id), when `agent` is an agent it uses as a
46
+ * tool: a rule whose `agent` names the root matches its subagents' calls too.
47
+ */
48
+ readonly root?: string;
40
49
  readonly input: unknown;
41
50
  /** The name the model called it by, for display only: no rule matches it. */
42
51
  readonly name?: string;
@@ -63,9 +72,10 @@ export declare function readToolPolicy(t: Pick<Tx, "getSetting">): Promise<ToolP
63
72
  export declare function toolCallDecision(t: Pick<Tx, "getSetting">, s: Session, request: HostEffect, kind: "mcp" | "http"): Promise<PolicyDecision>;
64
73
  /**
65
74
  * `request` as the policy reads it: by its target's identity (protocol 15), and by the agent that
66
- * calls it: an agent used as a tool by its id, the session's root agent by its loop manifest's id
67
- * (protocol 16: the agent manifest's name). Never the session's Agent id (`s.agentId`), which
68
- * protocol 16 made the Agent the session runs on (`users/ada/desk` for the manifest `desk`).
75
+ * calls it: an agent used as a tool by its id, with the session's root agent as its `root`, and
76
+ * the root agent by its loop manifest's id (protocol 16: the agent manifest's name). Never the
77
+ * session's Agent id (`s.agentId`), which protocol 16 made the Agent the session runs on
78
+ * (`users/ada/desk` for the manifest `desk`).
69
79
  */
70
80
  export declare function policyCallOf(s: Pick<Session, "manifest">, request: HostEffect, kind: "mcp" | "http"): PolicyCall;
71
81
  /** Decides `call` by `rules`; `pinnedAsk` adds the ask of a manifest saved before R5 step 2. */
@@ -11,6 +11,10 @@
11
11
  * manifest saved before R5 step 2 keeps its `approval: "always"` as an ask: no session loses
12
12
  * the approval it was opened with. A policy that cannot be read refuses every call.
13
13
  *
14
+ * A rule's `agent` matches the agent that calls: the session's root agent by its manifest's name,
15
+ * an agent it uses as a tool by its id, and that subagent's calls by the root's name too, so a rule
16
+ * for an agent covers the agents it uses as tools.
17
+ *
14
18
  * A rule's `tool` matches the tool's identity (protocol 15): an MCP tool's own name on its
15
19
  * server (`server` names the server), an HTTP tool's declared name, a flow stage's key. The name
16
20
  * a harness gave the model never matches. A stored rule whose `tool` has `__` in it (an MCP tool
@@ -70,9 +74,10 @@ export async function toolCallDecision(t, s, request, kind) {
70
74
  }
71
75
  /**
72
76
  * `request` as the policy reads it: by its target's identity (protocol 15), and by the agent that
73
- * calls it: an agent used as a tool by its id, the session's root agent by its loop manifest's id
74
- * (protocol 16: the agent manifest's name). Never the session's Agent id (`s.agentId`), which
75
- * protocol 16 made the Agent the session runs on (`users/ada/desk` for the manifest `desk`).
77
+ * calls it: an agent used as a tool by its id, with the session's root agent as its `root`, and
78
+ * the root agent by its loop manifest's id (protocol 16: the agent manifest's name). Never the
79
+ * session's Agent id (`s.agentId`), which protocol 16 made the Agent the session runs on
80
+ * (`users/ada/desk` for the manifest `desk`).
76
81
  */
77
82
  export function policyCallOf(s, request, kind) {
78
83
  const target = request.target;
@@ -83,6 +88,7 @@ export function policyCallOf(s, request, kind) {
83
88
  : (request.key ?? request.effectId),
84
89
  ...(target?.kind === "mcp" ? { server: target.server } : {}),
85
90
  agent: request.agent?.id ?? s.manifest.id,
91
+ ...(request.agent === undefined ? {} : { root: s.manifest.id }),
86
92
  input: request.input,
87
93
  ...(request.toolName === undefined ? {} : { name: request.toolName }),
88
94
  };
@@ -125,13 +131,20 @@ function matches(rule, call) {
125
131
  return false;
126
132
  if (rule.server !== undefined && (call.server === undefined || !glob(rule.server).test(call.server)))
127
133
  return false;
128
- if (rule.agent !== undefined && !glob(rule.agent).test(call.agent))
134
+ if (rule.agent !== undefined && !agentMatches(glob(rule.agent), call))
129
135
  return false;
130
136
  for (const [path, condition] of Object.entries(rule.when ?? {}))
131
137
  if (!holds(condition, fieldOf(call.input, path)))
132
138
  return false;
133
139
  return true;
134
140
  }
141
+ /**
142
+ * A rule's `agent` matches the agent that calls (an agent used as a tool by its id), and the
143
+ * session's root agent for its subagents' calls: a rule for the agent covers the agents it uses.
144
+ */
145
+ function agentMatches(pattern, call) {
146
+ return pattern.test(call.agent) || (call.root !== undefined && pattern.test(call.root));
147
+ }
135
148
  /** A pattern where `*` is any run of characters, matched whole. */
136
149
  function glob(pattern) {
137
150
  return new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
@@ -21,8 +21,10 @@ export declare function harnessExecutors(options: HarnessExecutorsOptions): Harn
21
21
  /**
22
22
  * Readies the run's MCP servers (§10.5 step 3): a run without MCP facts (`turn.start.mcp`, the
23
23
  * session's first) discovers their tools and records the snapshot (the first recorded wins);
24
- * later ones reconnect the snapshot's servers and record what failed. Answers the facts the run
25
- * goes on with. Nothing for a session without MCP servers.
24
+ * later ones reconnect the snapshot's servers and record what failed. A turn's first segment also
25
+ * discovers again each declared server the snapshot lacks (one that failed or was refused before),
26
+ * and records the ones that answer now: they join the snapshot, and the turn offers their tools.
27
+ * Answers the facts the run goes on with. Nothing for a session without MCP servers.
26
28
  */
27
29
  export declare function prepareMcp(pool: McpPool, run: HarnessRun, record: McpRecorder): Promise<PreparedRun | undefined>;
28
30
  /** A run's routing, as the call helpers take it. Its sandbox events are claimed for the run. */
@@ -91,8 +91,10 @@ async function withSandbox(run, use) {
91
91
  /**
92
92
  * Readies the run's MCP servers (§10.5 step 3): a run without MCP facts (`turn.start.mcp`, the
93
93
  * session's first) discovers their tools and records the snapshot (the first recorded wins);
94
- * later ones reconnect the snapshot's servers and record what failed. Answers the facts the run
95
- * goes on with. Nothing for a session without MCP servers.
94
+ * later ones reconnect the snapshot's servers and record what failed. A turn's first segment also
95
+ * discovers again each declared server the snapshot lacks (one that failed or was refused before),
96
+ * and records the ones that answer now: they join the snapshot, and the turn offers their tools.
97
+ * Answers the facts the run goes on with. Nothing for a session without MCP servers.
96
98
  */
97
99
  export async function prepareMcp(pool, run, record) {
98
100
  const routing = routingOf(run);
@@ -114,9 +116,29 @@ export async function prepareMcp(pool, run, record) {
114
116
  }
115
117
  else {
116
118
  const diagnostics = await pool.reconnect({ sessionId, manifest, servers: facts.servers, signal: run.signal });
117
- if (diagnostics.length === 0)
119
+ // Not mid-turn: a turn's tools stay what its first segment offered.
120
+ const first = run.start.checkpoint?.segment === 0;
121
+ const known = new Set(facts.servers.map((server) => JSON.stringify([server.agentId ?? null, server.name])));
122
+ const missing = first
123
+ ? serversOf(manifest).filter((item) => !known.has(JSON.stringify([item.agentId ?? null, item.server.name])))
124
+ : [];
125
+ const found = missing.length === 0
126
+ ? undefined
127
+ : await pool.discover({
128
+ sessionId,
129
+ manifest,
130
+ manifestHash: hashManifest(manifest),
131
+ maxInstructionBytes: run.start.limits.recordEntryBytes,
132
+ signal: run.signal,
133
+ servers: missing,
134
+ });
135
+ const all = [...diagnostics, ...(found?.diagnostics ?? [])];
136
+ if (all.length === 0)
118
137
  return undefined;
119
- answer = await record({ diagnostics });
138
+ answer = await record({
139
+ ...(found && found.snapshot.servers.length > 0 ? { snapshot: found.snapshot } : {}),
140
+ diagnostics: all,
141
+ });
120
142
  }
121
143
  return answer.mcp === undefined ? undefined : { mcp: answer.mcp };
122
144
  }
@@ -195,7 +195,8 @@ export async function recordIntent(scope, effect, requestHash) {
195
195
  * - `mcp`: the tool `{server, tool}` of an MCP server its agent declares, as the session
196
196
  * discovered it (`tool.unknown` otherwise), left enabled by the turn's manifest (R2b C9,
197
197
  * `tool.disabled`);
198
- * - `http`: an HTTP tool its agent declares under that name (R2 M3), which crosses the Tool Gate;
198
+ * - `http`: an HTTP tool its agent declares under that name in the turn's manifest (R2 M3), which
199
+ * crosses the Tool Gate;
199
200
  * - `sandbox`: one of the sandbox operations, in a session with a sandbox;
200
201
  * - `harness`: a tool the harness runs in its own process; its egress gate is its only control.
201
202
  * - `runtime`: served by `recordIntent` itself, before this (`schedule_wake`, track T4).
@@ -222,7 +223,8 @@ function refusal(s, request) {
222
223
  return undefined;
223
224
  }
224
225
  case "http":
225
- return isHttpToolCall(s.manifest, request) ? undefined : unrunnableTool(request);
226
+ // Declared in the turn's manifest: a variant or `removeTools` may have removed it.
227
+ return isHttpToolCall(turnManifestOf(s), request) ? undefined : unrunnableTool(request);
226
228
  case "sandbox":
227
229
  return sessionSandboxSpec(s) !== undefined &&
228
230
  request.toolName !== undefined &&
@@ -266,9 +266,11 @@ export function createHarnessApiServer(ctx, options = {}) {
266
266
  /**
267
267
  * What a harness found readying the run's MCP servers: the first snapshot recorded for the
268
268
  * session wins, pinned as it is within the Tenant's Runtime limit `mcpSnapshotBytes` (a server
269
- * past it is left out, with a diagnostic: `fitSnapshot`), with `mcp.discovered` in its log;
270
- * diagnostics replace those of the same server. Answers the session's facts as the run's turn
271
- * sees them (protocol 15).
269
+ * past it is left out, with a diagnostic: `fitSnapshot`), with `mcp.discovered` in its log.
270
+ * A later snapshot adds only servers the session's lacks (servers that failed at its first
271
+ * discovery and answer now, `prepareMcp`), within the limit, with `mcp.discovered` naming them
272
+ * (`rejoined`). Diagnostics replace those of the same server. Answers the session's facts as the
273
+ * run's turn sees them (protocol 15).
272
274
  */
273
275
  const recordMcp = async (run, params) => {
274
276
  const snapshot = params.snapshot;
@@ -286,17 +288,39 @@ export function createHarnessApiServer(ctx, options = {}) {
286
288
  await t.put("sessions", current.id, current);
287
289
  await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics, current.manifest), run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
288
290
  }
289
- else if (diagnostics.length > 0) {
290
- const prior = [...(current.mcpDiagnostics ?? [])];
291
- for (const item of diagnostics) {
292
- const index = prior.findIndex((existing) => (existing.agentId ?? null) === (item.agentId ?? null) && existing.serverName === item.serverName);
293
- if (index >= 0)
294
- prior[index] = item;
295
- else
296
- prior.push(item);
291
+ else {
292
+ const key = (agentId, name) => JSON.stringify([agentId ?? null, name]);
293
+ // Servers the session's snapshot lacks, which answered now: they join it.
294
+ let added = new Set();
295
+ if (snapshot && current.mcpSnapshot) {
296
+ const pinned = current.mcpSnapshot.servers;
297
+ const has = new Set(pinned.map((server) => key(server.agentId, server.name)));
298
+ const fresh = snapshot.servers.filter((server) => !has.has(key(server.agentId, server.name)));
299
+ if (fresh.length > 0) {
300
+ const fitted = fitSnapshot({ ...current.mcpSnapshot, servers: [...pinned, ...fresh] }, diagnostics, (await readRuntimeLimits(t)).mcpSnapshotBytes, pinned.length);
301
+ diagnostics = fitted.diagnostics;
302
+ current.mcpSnapshot = fitted.snapshot;
303
+ added = new Set(fitted.snapshot.servers.slice(pinned.length).map((server) => key(server.agentId, server.name)));
304
+ }
297
305
  }
298
- current.mcpDiagnostics = prior;
299
- await t.put("sessions", current.id, current);
306
+ if (diagnostics.length > 0) {
307
+ const prior = [...(current.mcpDiagnostics ?? [])];
308
+ for (const item of diagnostics) {
309
+ const index = prior.findIndex((existing) => key(existing.agentId, existing.serverName) === key(item.agentId, item.serverName));
310
+ if (index >= 0)
311
+ prior[index] = item;
312
+ else
313
+ prior.push(item);
314
+ }
315
+ current.mcpDiagnostics = prior;
316
+ }
317
+ if (diagnostics.length > 0 || added.size > 0)
318
+ await t.put("sessions", current.id, current);
319
+ if (added.size > 0 && current.mcpSnapshot)
320
+ await t.event(current.id, run.grant.turnId, "mcp.discovered", {
321
+ ...mcpDiscovered(current.mcpSnapshot, diagnostics.filter((item) => added.has(key(item.agentId, item.serverName))), current.manifest),
322
+ rejoined: true,
323
+ }, run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
300
324
  }
301
325
  // The turn's manifest: a variant may have disabled tools.
302
326
  return current.mcpSnapshot ? { mcp: mcpFactsOf(current.mcpSnapshot, turnManifestOf(current)) } : {};
@@ -18,6 +18,7 @@ import { sandboxWorkspaceOf } from "../sandbox/share.js";
18
18
  import { ownedTx } from "../store/ownership.js";
19
19
  import { harnessOf } from "../tenant/context.js";
20
20
  import { repinTurn } from "../tenant/agent-versions.js";
21
+ import { noteSessionModel, sessionModelRecordOf } from "../tenant/session-model.js";
21
22
  import { HARNESS_ID } from "../sandbox/placement.js";
22
23
  import { checkParity, foldTranscriptWithCursor, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "../tenant/history.js";
23
24
  import { usesFixtureModel } from "../tenant/model-setting.js";
@@ -45,6 +46,11 @@ options = {}) {
45
46
  // A turn's first segment runs the version its Agent runs now (T3, D73).
46
47
  if (options.harness && (await repinTurn(t, current)))
47
48
  await t.put("sessions", id, current);
49
+ // The model the segment's calls use, and why: `session.model` when it changed since.
50
+ if (options.harness &&
51
+ !isWorkflowManifest(current.manifest) &&
52
+ (await noteSessionModel(t, current, await sessionModelRecordOf(t, current))))
53
+ await t.put("sessions", id, current);
48
54
  // A transcript left on our engine's state by an older Runtime is recorded once; another
49
55
  // harness's state is its own (R4.2).
50
56
  const limits = await readRuntimeLimits(t);
@@ -70,7 +76,10 @@ options = {}) {
70
76
  ...(current.sandbox === undefined ? {} : { spec: current.sandbox }),
71
77
  };
72
78
  const turnId = current.activeTurnId;
79
+ const noted = current.memoryIndex;
73
80
  const memory = isWorkflowManifest(current.manifest) ? undefined : await turnMemoryOf(t, current, limits);
81
+ if (current.memoryIndex !== noted)
82
+ await t.put("sessions", id, current);
74
83
  return {
75
84
  current,
76
85
  fixtureModel,
@@ -170,19 +179,48 @@ export function buildTurnStart(ctx, segment) {
170
179
  /**
171
180
  * The Agent's memory as the segment starts (track T5, D72), when the version the session runs sets
172
181
  * `harness.memory`: its access (the same in every turn), the agent's instructions, and the index of
173
- * its memories, newest first, within the Tenant's index limits.
182
+ * its memories, newest first, within the Tenant's index limits; no index when it sets
183
+ * `harness.memory.index: false`. `refresh` at a turn's first segment when someone other than the
184
+ * session wrote the memory since its last turn started (`memoryRefreshOf`); the harness then pins
185
+ * this index in place of the one it pinned before. Only our harness is sent it: another v5
186
+ * harness's schema may not know the field.
174
187
  */
175
188
  async function turnMemoryOf(t, s, limits) {
176
189
  const settings = await memorySettingsOf(t, s);
177
190
  if (!settings)
178
191
  return undefined;
179
- const { index, truncated } = await renderMemoryIndex(t, s.agentId, limits);
180
- return {
192
+ const base = {
181
193
  access: settings.access ?? "read_write",
182
194
  ...(settings.instructions === undefined ? {} : { instructions: settings.instructions }),
183
- index,
184
- truncated,
185
195
  };
196
+ // A version that sets `harness.memory.index: false` (ADR-100) pins no index: none is rendered.
197
+ if (settings.index === false)
198
+ return { ...base, truncated: false };
199
+ const { index, truncated } = await renderMemoryIndex(t, s.agentId, limits);
200
+ // Within Harness API 5 only our harness, the Runtime's own version, is sent `refresh`.
201
+ const refresh = (await memoryRefreshOf(t, s)) && harnessOf(s) === HARNESS_ID;
202
+ return { ...base, index, truncated, ...(refresh ? { refresh: true } : {}) };
203
+ }
204
+ /**
205
+ * Whether the harness pins the memory index again at this segment: only at a turn's first segment
206
+ * (never mid-turn), when a memory version newer than the one the session's last turn saw was
207
+ * written other than by this session (another session of the Agent, or the API). Notes the newest
208
+ * version on the session (`Session.memoryIndex`; the caller saves it), and answers a first segment
209
+ * that runs again as it was answered. The session's first turn pins the index anyway.
210
+ */
211
+ async function memoryRefreshOf(t, s) {
212
+ const checkpoint = s.checkpoint;
213
+ const turnId = s.activeTurnId;
214
+ if (turnId === null || checkpoint?.segment !== 0 || checkpoint.turnId !== turnId)
215
+ return false;
216
+ const before = s.memoryIndex;
217
+ if (before?.turnId === turnId)
218
+ return before.refresh;
219
+ const writes = await t.memoryWritesSince(s.agentId, before?.seen, s.id);
220
+ const refresh = before !== undefined && writes.others;
221
+ const seen = writes.newest ?? before?.seen;
222
+ s.memoryIndex = { turnId, ...(seen === undefined ? {} : { seen }), refresh };
223
+ return refresh;
186
224
  }
187
225
  /**
188
226
  * The facts of the Agent a session runs on (Harness API 5): its id and kind, and, with the
@@ -2,7 +2,7 @@ import type { AgentSecretView, HostModelView, McpPreview, McpPreviewRequest, Put
2
2
  import type { SessionStore } from "../store/types.js";
3
3
  import type { OutboundPolicy } from "../tenant/outbound.js";
4
4
  import { type SigningKeys } from "../tenant/signing-keys.js";
5
- import type { SealedSecretCopies, VaultService } from "../vault/service.js";
5
+ import type { SealedSecretCopies, SecretWriteOptions, VaultService } from "../vault/service.js";
6
6
  /** One JWT to sign: its `typ` and every claim, `iss`, `aud`, `sub`, `iat`, `exp` and `jti` included. */
7
7
  export interface SignRequest {
8
8
  readonly typ: string;
@@ -15,8 +15,11 @@ export interface Signed {
15
15
  }
16
16
  import { type SealedWebhookSecret, type WebhookDelivery } from "./webhooks.js";
17
17
  export interface Keys {
18
- /** `PUT /v1/agents/{id}/secrets/{name}` (`VaultService.putEnvironmentSecret`). */
19
- putEnvironmentSecret(environmentId: string, name: string, body: PutAgentSecretRequest, actor: string): Promise<AgentSecretView>;
18
+ /**
19
+ * `PUT /v1/agents/{id}/secrets/{name}` (`VaultService.putEnvironmentSecret`); `options.viaFromSlot`
20
+ * when `body.via` is the slot's, not the write's own (`secret-routing`).
21
+ */
22
+ putEnvironmentSecret(environmentId: string, name: string, body: PutAgentSecretRequest, actor: string, options?: SecretWriteOptions | null): Promise<AgentSecretView>;
20
23
  putHostModel(body: PutHostModelRequest): Promise<HostModelView>;
21
24
  selectHostModel(body: SelectHostModelRequest): Promise<HostModelView>;
22
25
  /**
package/dist/keys/keys.js CHANGED
@@ -39,7 +39,7 @@ export function inProcessKeys(options) {
39
39
  const { store, vault, signingKeys, kek } = options;
40
40
  const egress = new EgressAuthority(store, kek);
41
41
  return {
42
- putEnvironmentSecret: (environmentId, name, body, actor) => vault.putEnvironmentSecret(environmentId, name, body, actor),
42
+ putEnvironmentSecret: (environmentId, name, body, actor, options) => vault.putEnvironmentSecret(environmentId, name, body, actor, options),
43
43
  putHostModel: (body) => vault.putHostModel(body),
44
44
  selectHostModel: (body) => vault.selectHostModel(body),
45
45
  sealEnvironmentSecretCopies: (from, vaultId, slots) => vault.sealEnvironmentSecretCopies(from, vaultId, slots),
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Management API",
5
- "version": "0.38.0-beta",
5
+ "version": "0.40.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
@@ -40,7 +40,9 @@
40
40
  "model-host-identity",
41
41
  "credential-provider",
42
42
  "turns",
43
- "agent-model"
43
+ "agent-model",
44
+ "secret-routing",
45
+ "remove-tools"
44
46
  ]
45
47
  },
46
48
  "description": "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun tenant keys put <id> --management`) or from `NYLORUN_MANAGEMENT_KEY_FILE`."
@@ -2565,7 +2567,7 @@
2565
2567
  "agent": {
2566
2568
  "type": "string",
2567
2569
  "minLength": 1,
2568
- "description": "The agent that calls the tool: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id. `*` matches any run of characters"
2570
+ "description": "The agent that calls the tool: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id. The session's root agent's name also matches the calls of the agents it uses as tools. `*` matches any run of characters"
2569
2571
  },
2570
2572
  "when": {
2571
2573
  "type": "object",
@@ -2667,7 +2669,7 @@
2667
2669
  "agent": {
2668
2670
  "type": "string",
2669
2671
  "minLength": 1,
2670
- "description": "The agent that calls the tool: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id. `*` matches any run of characters"
2672
+ "description": "The agent that calls the tool: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id. The session's root agent's name also matches the calls of the agents it uses as tools. `*` matches any run of characters"
2671
2673
  },
2672
2674
  "when": {
2673
2675
  "type": "object",
@@ -34,6 +34,11 @@ export declare class McpPool {
34
34
  manifestHash: string;
35
35
  maxInstructionBytes: number;
36
36
  signal?: AbortSignal;
37
+ /**
38
+ * Only these of its declared servers (default all): the ones a later turn tries again because
39
+ * they are not in the session's snapshot (`prepareMcp`).
40
+ */
41
+ servers?: readonly DeclaredServer[];
37
42
  }): Promise<{
38
43
  snapshot: McpSnapshot;
39
44
  diagnostics: McpDiagnostic[];
package/dist/mcp/pool.js CHANGED
@@ -22,7 +22,7 @@ export class McpPool {
22
22
  async discover(input) {
23
23
  const servers = [];
24
24
  const diagnostics = [];
25
- for (const declared of serversOf(input.manifest)) {
25
+ for (const declared of input.servers ?? serversOf(input.manifest)) {
26
26
  input.signal?.throwIfAborted();
27
27
  const opened = await this.connectDeclared(input, declared);
28
28
  if (!opened.ok) {
@@ -49,7 +49,9 @@ export declare function manifestFor(manifest: AgentManifest, agent: HostEffect["
49
49
  * out, so the session never offers its tools, and its diagnostic says so (`failed`, replacing
50
50
  * the one it had). Unchanged when the whole snapshot fits.
51
51
  */
52
- export declare function fitSnapshot(snapshot: McpSnapshot, diagnostics: readonly McpDiagnostic[], limitBytes: number): {
52
+ export declare function fitSnapshot(snapshot: McpSnapshot, diagnostics: readonly McpDiagnostic[], limitBytes: number,
53
+ /** How many of its first servers stay whatever their size: those the session pinned already. */
54
+ keep?: number): {
53
55
  snapshot: McpSnapshot;
54
56
  diagnostics: McpDiagnostic[];
55
57
  };
@@ -86,17 +86,19 @@ export function manifestFor(manifest, agent) {
86
86
  * out, so the session never offers its tools, and its diagnostic says so (`failed`, replacing
87
87
  * the one it had). Unchanged when the whole snapshot fits.
88
88
  */
89
- export function fitSnapshot(snapshot, diagnostics, limitBytes) {
89
+ export function fitSnapshot(snapshot, diagnostics, limitBytes,
90
+ /** How many of its first servers stay whatever their size: those the session pinned already. */
91
+ keep = 0) {
90
92
  const bytes = (value) => Buffer.byteLength(JSON.stringify(value));
91
93
  if (bytes(snapshot) <= limitBytes)
92
94
  return { snapshot, diagnostics: [...diagnostics] };
93
95
  const kept = [];
94
96
  const dropped = new Map();
95
97
  const key = (agentId, serverName) => JSON.stringify([agentId ?? null, serverName]);
96
- for (const server of snapshot.servers) {
98
+ for (const [index, server] of snapshot.servers.entries()) {
97
99
  const next = { ...snapshot, servers: [...kept, server] };
98
100
  const size = bytes(next);
99
- if (size <= limitBytes)
101
+ if (index < keep || size <= limitBytes)
100
102
  kept.push(server);
101
103
  else
102
104
  dropped.set(key(server.agentId, server.name), bytes(server));