@nylorun/runtime 0.38.0-beta → 0.39.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/dist/api/components.d.ts +1726 -80
- package/dist/api/http/routes/agents.js +3 -3
- package/dist/core/turn-manifest.d.ts +23 -0
- package/dist/core/turn-manifest.js +36 -1
- package/dist/gates/policy.d.ts +13 -3
- package/dist/gates/policy.js +17 -4
- package/dist/harness/executors.d.ts +4 -2
- package/dist/harness/executors.js +26 -4
- package/dist/harness-api/record.js +4 -2
- package/dist/harness-api/server.js +37 -13
- package/dist/harness-api/start.js +43 -5
- package/dist/keys/keys.d.ts +6 -3
- package/dist/keys/keys.js +1 -1
- package/dist/management-openapi.json +6 -4
- package/dist/mcp/pool.d.ts +5 -0
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/snapshot.d.ts +3 -1
- package/dist/mcp/snapshot.js +5 -3
- package/dist/openapi.json +1636 -563
- package/dist/store/postgres/store.js +14 -0
- package/dist/store/types.d.ts +9 -0
- package/dist/tenant/agent-versions.d.ts +13 -5
- package/dist/tenant/agent-versions.js +30 -6
- package/dist/tenant/agents.d.ts +14 -2
- package/dist/tenant/agents.js +51 -13
- package/dist/tenant/commands.js +16 -8
- package/dist/tenant/context.d.ts +30 -1
- package/dist/tenant/session-model.d.ts +35 -6
- package/dist/tenant/session-model.js +77 -9
- package/dist/tenant/sessions.js +13 -3
- package/dist/vault/service.d.ts +44 -3
- package/dist/vault/service.js +68 -27
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
399
|
-
const secret = await c.env.tenant.keys.putEnvironmentSecret(id, name,
|
|
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).
|
package/dist/gates/policy.d.ts
CHANGED
|
@@ -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
|
|
67
|
-
* (protocol 16: the agent manifest's name). Never the
|
|
68
|
-
* protocol 16 made the Agent the session runs on
|
|
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. */
|
package/dist/gates/policy.js
CHANGED
|
@@ -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
|
|
74
|
-
* (protocol 16: the agent manifest's name). Never the
|
|
75
|
-
* protocol 16 made the Agent the session runs on
|
|
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)
|
|
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.
|
|
25
|
-
*
|
|
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.
|
|
95
|
-
*
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
271
|
-
*
|
|
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
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
|
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
|
package/dist/keys/keys.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
19
|
-
|
|
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.
|
|
5
|
+
"version": "0.39.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",
|
package/dist/mcp/pool.d.ts
CHANGED
|
@@ -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) {
|
package/dist/mcp/snapshot.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/mcp/snapshot.js
CHANGED
|
@@ -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));
|