@nylorun/runtime 0.27.0-beta → 0.29.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 (160) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/dist/api/a2a/card.d.ts +4 -3
  4. package/dist/api/a2a/card.js +3 -3
  5. package/dist/api/a2a/routes.js +18 -7
  6. package/dist/api/ag-ui/routes.js +4 -7
  7. package/dist/api/components.d.ts +5854 -2718
  8. package/dist/api/components.js +39 -30
  9. package/dist/api/http/app.js +9 -4
  10. package/dist/api/http/body.d.ts +2 -0
  11. package/dist/api/http/body.js +20 -0
  12. package/dist/api/http/define.d.ts +4 -0
  13. package/dist/api/http/define.js +20 -9
  14. package/dist/api/http/routes/agent-manifests.d.ts +10 -0
  15. package/dist/api/http/routes/agent-manifests.js +135 -0
  16. package/dist/api/http/routes/agents.d.ts +11 -0
  17. package/dist/api/http/routes/agents.js +418 -0
  18. package/dist/api/http/routes/artifacts.js +2 -2
  19. package/dist/api/http/routes/coverage.d.ts +4 -4
  20. package/dist/api/http/routes/coverage.js +12 -6
  21. package/dist/api/http/routes/files.js +1 -1
  22. package/dist/api/http/routes/mcp.js +2 -2
  23. package/dist/api/http/routes/me.d.ts +1 -1
  24. package/dist/api/http/routes/me.js +1 -2
  25. package/dist/api/http/routes/memories.d.ts +9 -0
  26. package/dist/api/http/routes/memories.js +152 -0
  27. package/dist/api/http/routes/reads.d.ts +1 -137
  28. package/dist/api/http/routes/reads.js +31 -12
  29. package/dist/api/http/routes/sessions.js +21 -50
  30. package/dist/api/http/routes/tenant.js +13 -10
  31. package/dist/api/http/routes/triggers.d.ts +10 -0
  32. package/dist/api/http/routes/triggers.js +183 -0
  33. package/dist/api/openapi.js +69 -47
  34. package/dist/artifacts/disk.d.ts +10 -10
  35. package/dist/artifacts/disk.js +27 -27
  36. package/dist/artifacts/quota.d.ts +1 -1
  37. package/dist/artifacts/quota.js +2 -2
  38. package/dist/artifacts/service.js +2 -2
  39. package/dist/core/flow-host.d.ts +0 -1
  40. package/dist/gates/egress-credentials.js +1 -1
  41. package/dist/gates/egress.d.ts +5 -5
  42. package/dist/gates/egress.js +6 -6
  43. package/dist/gates/http-tool.js +2 -2
  44. package/dist/gates/meter.js +22 -5
  45. package/dist/gates/policy.d.ts +8 -3
  46. package/dist/gates/policy.js +7 -2
  47. package/dist/gates/tenant-vaults.d.ts +1 -1
  48. package/dist/harness/pod.d.ts +2 -0
  49. package/dist/harness/setup.js +17 -6
  50. package/dist/harness-api/record.js +11 -0
  51. package/dist/harness-api/server.js +4 -0
  52. package/dist/harness-api/start.d.ts +11 -7
  53. package/dist/harness-api/start.js +49 -5
  54. package/dist/harness-api/workspace.js +2 -2
  55. package/dist/host/config-for.d.ts +2 -0
  56. package/dist/host/config-for.js +1 -0
  57. package/dist/host/http.d.ts +2 -2
  58. package/dist/host/http.js +5 -2
  59. package/dist/host/main.js +1 -0
  60. package/dist/keys/keys.d.ts +15 -4
  61. package/dist/keys/keys.js +16 -0
  62. package/dist/keys/webhooks.d.ts +25 -0
  63. package/dist/keys/webhooks.js +62 -0
  64. package/dist/management-openapi.json +313 -133
  65. package/dist/mcp/connect.d.ts +2 -2
  66. package/dist/mcp/connect.js +3 -3
  67. package/dist/mcp/preview.js +7 -7
  68. package/dist/memory/service.d.ts +97 -0
  69. package/dist/memory/service.js +335 -0
  70. package/dist/memory/tool.d.ts +36 -0
  71. package/dist/memory/tool.js +84 -0
  72. package/dist/model/pi-model.js +7 -0
  73. package/dist/openapi.json +13120 -9034
  74. package/dist/reads/types.d.ts +3 -3
  75. package/dist/record/agent.d.ts +27 -0
  76. package/dist/record/agent.js +46 -0
  77. package/dist/record/index.d.ts +1 -1
  78. package/dist/record/index.js +1 -1
  79. package/dist/sandbox/join.js +5 -5
  80. package/dist/sandbox/placement.d.ts +1 -1
  81. package/dist/sandbox/placement.js +3 -3
  82. package/dist/sandbox/pods/lifecycle.d.ts +6 -6
  83. package/dist/sandbox/pods/lifecycle.js +6 -6
  84. package/dist/sandbox/pods/reconcile.js +10 -2
  85. package/dist/sandbox/resolve.d.ts +3 -3
  86. package/dist/sandbox/resolve.js +7 -7
  87. package/dist/sandbox/setup.d.ts +3 -1
  88. package/dist/sandbox/setup.js +14 -4
  89. package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
  90. package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
  91. package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
  92. package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
  93. package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
  94. package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
  95. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  96. package/dist/store/postgres/reads.js +44 -87
  97. package/dist/store/postgres/record-writer.d.ts +4 -4
  98. package/dist/store/postgres/record-writer.js +9 -9
  99. package/dist/store/postgres/schema.d.ts +1408 -123
  100. package/dist/store/postgres/schema.js +194 -38
  101. package/dist/store/postgres/store.js +329 -79
  102. package/dist/store/postgres/tenant.js +1 -2
  103. package/dist/store/types.d.ts +155 -50
  104. package/dist/store/types.js +0 -1
  105. package/dist/tenant/advance.js +20 -8
  106. package/dist/tenant/agent-manifests.d.ts +72 -0
  107. package/dist/tenant/agent-manifests.js +359 -0
  108. package/dist/tenant/agent-versions.d.ts +126 -0
  109. package/dist/tenant/agent-versions.js +389 -0
  110. package/dist/tenant/agents.d.ts +230 -0
  111. package/dist/tenant/{environments.js → agents.js} +464 -320
  112. package/dist/tenant/auth.d.ts +1 -1
  113. package/dist/tenant/auth.js +3 -3
  114. package/dist/tenant/commands.d.ts +63 -2
  115. package/dist/tenant/commands.js +240 -83
  116. package/dist/tenant/context.d.ts +46 -19
  117. package/dist/tenant/context.js +6 -2
  118. package/dist/tenant/effects.js +7 -16
  119. package/dist/tenant/identity-file.d.ts +6 -6
  120. package/dist/tenant/identity-file.js +12 -10
  121. package/dist/tenant/issuers.js +3 -3
  122. package/dist/tenant/limits.d.ts +0 -3
  123. package/dist/tenant/limits.js +0 -3
  124. package/dist/tenant/memories.d.ts +38 -0
  125. package/dist/tenant/memories.js +119 -0
  126. package/dist/tenant/reset.js +2 -2
  127. package/dist/tenant/run-grants.d.ts +6 -0
  128. package/dist/tenant/run-grants.js +4 -0
  129. package/dist/tenant/runtime.d.ts +1 -1
  130. package/dist/tenant/runtime.js +8 -0
  131. package/dist/tenant/sessions.d.ts +28 -45
  132. package/dist/tenant/sessions.js +85 -208
  133. package/dist/tenant/status.js +6 -7
  134. package/dist/tenant/sweep.js +12 -3
  135. package/dist/tenant/transcript.d.ts +16 -16
  136. package/dist/tenant/triggers.d.ts +143 -0
  137. package/dist/tenant/triggers.js +715 -0
  138. package/dist/tenant/turn-timing.d.ts +11 -1
  139. package/dist/tenant/turn-timing.js +3 -1
  140. package/dist/tenant/types.d.ts +5 -0
  141. package/dist/tenant/worker.d.ts +2 -0
  142. package/dist/tenant/worker.js +5 -0
  143. package/dist/traces/map.d.ts +1 -1
  144. package/dist/traces/map.js +7 -1
  145. package/dist/tuning.d.ts +7 -0
  146. package/dist/tuning.js +19 -0
  147. package/dist/vault/service.d.ts +13 -8
  148. package/dist/vault/service.js +61 -17
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/package.json +13 -12
  152. package/dist/api/http/routes/environment-manifests.d.ts +0 -9
  153. package/dist/api/http/routes/environment-manifests.js +0 -118
  154. package/dist/api/http/routes/environments.d.ts +0 -11
  155. package/dist/api/http/routes/environments.js +0 -332
  156. package/dist/record/environment.d.ts +0 -27
  157. package/dist/record/environment.js +0 -46
  158. package/dist/tenant/environment-manifests.d.ts +0 -57
  159. package/dist/tenant/environment-manifests.js +0 -208
  160. package/dist/tenant/environments.d.ts +0 -172
@@ -0,0 +1,389 @@
1
+ import { hashManifest } from "@nylorun/core/compatibility";
2
+ import { loopManifestOf, runtimeOf } from "@nylorun/core/define";
3
+ import { isWorkflowManifest } from "../core/flow-host.js";
4
+ import { canonical } from "../store/canonical.js";
5
+ import { resolveRecipe, sandboxSpecOf } from "../sandbox/resolve.js";
6
+ import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
7
+ import { HARNESS_ID } from "../sandbox/placement.js";
8
+ import { slotHolds } from "../vault/service.js";
9
+ import { manifestRecipeRequest, sectionChanges } from "./agent-manifests.js";
10
+ import { podLifecycleConfig } from "../sandbox/pods/spec.js";
11
+ import { hostTuning } from "../tuning.js";
12
+ import { harnessOf } from "./context.js";
13
+ import { fail } from "./http.js";
14
+ import { limitIssues, manifestShapeIssues, readTenantLimits } from "./limits.js";
15
+ import { readRuntimeLimits } from "./runtime-limits.js";
16
+ import { mergeAgentTriggers } from "./triggers.js";
17
+ /**
18
+ * The pin of the version Agent `agent` runs: its manifest version's loop manifest, and the
19
+ * harness and sandbox spec of the runtime its compute applies (`applied`). `409 agent_unavailable`
20
+ * when that version is gone.
21
+ */
22
+ export async function pinOf(t, agent) {
23
+ const version = (await t.agentManifest(agent.manifestName, agent.manifestVersion)) ??
24
+ fail(409, `Agent ${agent.id}'s manifest ${agent.manifestName}@${agent.manifestVersion} is gone.`, {
25
+ code: "agent_unavailable",
26
+ });
27
+ const manifest = loopManifestOf(version.body);
28
+ const config = await readSandboxConfig(t);
29
+ const loop = version.body.harness.loop;
30
+ const sandbox = agent.kind === "none" ? undefined : sandboxSpecOf(agent.applied, effectiveSandboxConfig(config).limits.idle);
31
+ return {
32
+ version,
33
+ pin: {
34
+ manifest,
35
+ manifestHash: hashManifest(manifest),
36
+ definition: { name: version.name, version: version.version },
37
+ ...(loop === undefined ? {} : { loop }),
38
+ harness: isWorkflowManifest(manifest) ? HARNESS_ID : agent.applied.harness,
39
+ ...(sandbox === undefined ? {} : { sandbox }),
40
+ },
41
+ };
42
+ }
43
+ /** Writes `pin` onto `session` (the caller saves it). */
44
+ export function applyPin(session, pin) {
45
+ session.manifest = pin.manifest;
46
+ session.manifestHash = pin.manifestHash;
47
+ session.definition = { ...pin.definition };
48
+ if (pin.loop === undefined)
49
+ delete session.loop;
50
+ else
51
+ session.loop = pin.loop;
52
+ session.harness = pin.harness;
53
+ if (pin.sandbox === undefined)
54
+ delete session.sandbox;
55
+ else
56
+ session.sandbox = pin.sandbox;
57
+ }
58
+ /**
59
+ * Re-pins `session` (in the segment's owned transaction) to the version its Agent runs, when its
60
+ * active turn is about to run its first segment and nothing of the turn has run yet: the turn
61
+ * then runs that version. Returns whether it changed the session (the caller saves it).
62
+ *
63
+ * Leaves the session as it is for a flow and a flow's linked session, a turn whose message named
64
+ * a variant, a later segment, a turn with any journaled effect, and a version whose harness is
65
+ * not the session's (an Agent's harness is fixed for its life, D73).
66
+ */
67
+ export async function repinTurn(t, session) {
68
+ const id = session.environmentId;
69
+ const checkpoint = session.checkpoint;
70
+ if (id === undefined || session.definition === undefined || session.flowRootId !== undefined)
71
+ return false;
72
+ if (isWorkflowManifest(session.manifest) || session.activeTurnId === null)
73
+ return false;
74
+ if (!checkpoint || checkpoint.segment !== 0 || checkpoint.turnId !== session.activeTurnId)
75
+ return false;
76
+ // A variant the message named (a turn-manifest variant) is validated against the old pin.
77
+ if (checkpoint.manifestHash !== session.manifestHash)
78
+ return false;
79
+ const agent = await t.environment(id);
80
+ if (!agent)
81
+ return false;
82
+ if (session.definition.name === agent.manifestName && session.definition.version === agent.manifestVersion)
83
+ return false;
84
+ if ((await t.effectsForTurn(session.id, session.activeTurnId)).length > 0)
85
+ return false;
86
+ const { pin } = await pinOf(t, agent);
87
+ if (pin.harness !== harnessOf(session))
88
+ return false;
89
+ const changed = pin.manifestHash !== session.manifestHash;
90
+ applyPin(session, pin);
91
+ session.environmentVersion = agent.version;
92
+ session.checkpoint = { ...session.checkpoint, manifestHash: pin.manifestHash };
93
+ // MCP discovery is per loop manifest: the next one is the new version's.
94
+ if (changed) {
95
+ delete session.mcpSnapshot;
96
+ delete session.mcpDiagnostics;
97
+ }
98
+ return true;
99
+ }
100
+ /** The runtime fields a pod applies only when it starts: a change waits for its next start. */
101
+ const START_FIELDS = ["image", "resources", "network", "setup", "files"];
102
+ /** Whether pod Agent `agent` applies `recipe` only at its next start. */
103
+ function needsStart(agent, recipe) {
104
+ return agent.kind === "pod" && START_FIELDS.some((field) => canonical(recipe[field]) !== canonical(agent.applied[field]));
105
+ }
106
+ /**
107
+ * The plan of moving `agent` to version `target` of its manifest, named `tracking` after it:
108
+ * the target's runtime resolved against the Tenant's limits (a `400` when it no longer fits),
109
+ * what it changes, the fixed fields it would change, and the required secrets it would wait for.
110
+ */
111
+ export async function planMove(t, agent, target, tracking) {
112
+ const runtime = runtimeOf(target.body);
113
+ const recipe = target.version === agent.manifestVersion ? agent.applied : await resolvedRuntime(t, target);
114
+ const refusals = [];
115
+ const fixed = (field, from, to) => {
116
+ if (canonical(from) !== canonical(to))
117
+ refusals.push({ field, ...(from === undefined ? {} : { from }), ...(to === undefined ? {} : { to }) });
118
+ };
119
+ fixed("kind", agent.kind, runtime.kind);
120
+ fixed("harness", agent.applied.harness, recipe.harness);
121
+ fixed("storage", agent.applied.storage, recipe.storage);
122
+ const { missing, dormant } = await secretsOf(t, agent.id, recipe.secrets);
123
+ const current = await t.agentManifest(agent.manifestName, agent.manifestVersion);
124
+ const from = agent.manifestVersion;
125
+ const applies = refusals.length > 0
126
+ ? "refused"
127
+ : target.version === from
128
+ ? "none"
129
+ : missing.length > 0
130
+ ? "secrets"
131
+ : needsStart(agent, recipe)
132
+ ? "next_start"
133
+ : "now";
134
+ return {
135
+ target,
136
+ recipe,
137
+ plan: {
138
+ agent: agent.id,
139
+ manifest: tracking === "tracking" ? target.name : `${target.name}@${target.version}`,
140
+ from,
141
+ to: target.version,
142
+ tracking,
143
+ applies,
144
+ changes: target.version === from ? [] : sectionChanges(current?.body, target.body),
145
+ missingSecrets: missing,
146
+ dormantSecrets: dormant,
147
+ refusals,
148
+ sessions: (await t.sessionsOnEnvironment(agent.id)).length,
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Version `target`'s runtime resolved against the Tenant's limits now, as a create resolves it:
154
+ * `400` when it no longer fits them. (What an Agent runs already stays, whatever its limits.)
155
+ */
156
+ async function resolvedRuntime(t, target) {
157
+ const runtime = runtimeOf(target.body);
158
+ const resolved = resolveRecipe(runtime.kind, manifestRecipeRequest(target.body), effectiveSandboxConfig(await readSandboxConfig(t)));
159
+ const errors = [
160
+ ...(resolved.kind === "error" ? resolved.errors : []),
161
+ ...limitIssues(runtime.limits, await readTenantLimits(t)),
162
+ ...manifestShapeIssues(runtime, await readRuntimeLimits(t)),
163
+ ];
164
+ if (resolved.kind === "error" || errors.length > 0)
165
+ return fail(400, `${target.name}@${target.version} does not fit this Tenant's limits now: ${errors.join(" ")}`);
166
+ return resolved.recipe;
167
+ }
168
+ /**
169
+ * Of Agent `id`'s vault and the secret slots `slots` of a version: each required slot with no
170
+ * value that fills it (`slotHolds`), and each secret that fills none, which stays dormant.
171
+ */
172
+ async function secretsOf(t, id, slots) {
173
+ const vault = await t.vaultOfEnvironment(id);
174
+ const rows = vault ? (await t.credentialsForVault(vault.id)).filter((row) => row.type !== "model") : [];
175
+ if (slots === undefined)
176
+ return { missing: [], dormant: [] };
177
+ const fills = (name) => rows.some((row) => row.name === name && slotHolds(slots[name], row));
178
+ return {
179
+ missing: Object.entries(slots)
180
+ .filter(([name, slot]) => slot.required === true && !fills(name))
181
+ .map(([name]) => name)
182
+ .sort(),
183
+ dormant: rows
184
+ .filter((row) => slots[row.name] === undefined || !slotHolds(slots[row.name], row))
185
+ .map((row) => row.name)
186
+ .sort(),
187
+ };
188
+ }
189
+ /** The `409 agent_fixed_field` of a move that would change what is fixed for an Agent's life. */
190
+ export function refuseFixed(agentId, plan) {
191
+ return fail(409, `Agent ${agentId}'s ${plan.refusals.map((refusal) => refusal.field).join(", ")} ${plan.refusals.length === 1 ? "is" : "are"} fixed for its life, and version ${plan.to} changes ${plan.refusals.length === 1 ? "it" : "them"}: fork it into a new Agent with POST /v1/agents/${encodeURIComponent(agentId)}/fork.`, { code: "agent_fixed_field", details: { fields: plan.refusals, from: plan.from, to: plan.to } });
192
+ }
193
+ /**
194
+ * Moves `agent` (locked, live) per `move` (`planMove`), in the caller's transaction. A move
195
+ * replaces a pending one: the runtime it waited to apply is dropped first. `none` (back to what it
196
+ * runs) only clears the target; `now` switches; `next_start` keeps the target's runtime in
197
+ * `recipe` for the pod's next start (`switchTo`); `secrets` names the target and waits.
198
+ * Returns when it applies.
199
+ */
200
+ export async function moveAgent(t, agent, move, now) {
201
+ const { plan, target, recipe } = move;
202
+ if (plan.applies === "refused")
203
+ refuseFixed(agent.id, plan);
204
+ // Asked again while it waits for secrets: nothing new.
205
+ if (plan.applies === "secrets" && agent.targetVersion === plan.to && agent.appliedVersion === agent.version) {
206
+ if (agent.manifestRef !== plan.tracking)
207
+ await t.updateEnvironment(agent.id, { manifestRef: plan.tracking }, now);
208
+ return plan.applies;
209
+ }
210
+ const pending = agent.appliedVersion !== agent.version;
211
+ // What it runs, with no pending runtime: the base every move starts from.
212
+ const version = pending ? agent.version + 1 : agent.version;
213
+ const base = pending ? { recipe: agent.applied, version, appliedVersion: version } : {};
214
+ const ref = { manifestRef: plan.tracking };
215
+ switch (plan.applies) {
216
+ case "none":
217
+ await t.updateEnvironment(agent.id, { ...base, ...ref, targetVersion: null }, now);
218
+ break;
219
+ case "secrets":
220
+ await t.updateEnvironment(agent.id, { ...base, ...ref, targetVersion: plan.to }, now);
221
+ await t.agentEvent(agent.id, "agent.outdated", { from: plan.from, to: plan.to, waitingFor: "secrets" });
222
+ break;
223
+ case "next_start":
224
+ // `applied` stays what runs: `recipe` differs from it until the start applies it.
225
+ await t.updateEnvironment(agent.id, { ...ref, recipe, version: agent.version + 1, targetVersion: plan.to }, now);
226
+ await t.agentEvent(agent.id, "agent.outdated", { from: plan.from, to: plan.to, waitingFor: "start" });
227
+ // A running pod stops sooner once idle, so its next start applies the version.
228
+ if (agent.pod?.desired === "running") {
229
+ const idleMs = Math.min(podLifecycleConfig(await readSandboxConfig(t), recipe).idleMs, hostTuning().tenant.upgradeIdleMs);
230
+ const at = Math.max(Date.parse(agent.pod.lastActiveAt ?? now) + idleMs, Date.parse(now));
231
+ await t.signalSandbox(agent.id, { kind: "arm", timer: "idle", at });
232
+ }
233
+ break;
234
+ case "now":
235
+ await t.updateEnvironment(agent.id, {
236
+ ...ref,
237
+ recipe,
238
+ applied: recipe,
239
+ version: version + 1,
240
+ appliedVersion: version + 1,
241
+ manifestVersion: target.version,
242
+ manifestHash: target.hash,
243
+ targetVersion: null,
244
+ failedVersion: null,
245
+ }, now);
246
+ await t.agentEvent(agent.id, "agent.version_changed", { from: plan.from, to: plan.to });
247
+ await mergeAgentTriggers(t, { ...agent, manifestVersion: target.version }, agent.manifestVersion);
248
+ break;
249
+ }
250
+ return plan.applies;
251
+ }
252
+ /**
253
+ * After a secret write: an Agent that waits for its secrets moves on once the target's required
254
+ * slots are filled (`moveAgent` again, to the same target). In the caller's transaction.
255
+ */
256
+ export async function resumeMove(t, agent, now) {
257
+ if (agent.targetVersion === undefined || agent.appliedVersion !== agent.version)
258
+ return;
259
+ const target = await t.agentManifest(agent.manifestName, agent.targetVersion);
260
+ if (!target)
261
+ return void (await t.updateEnvironment(agent.id, { targetVersion: null }, now));
262
+ const move = await planMove(t, agent, target, agent.manifestRef);
263
+ if (move.plan.applies === "refused")
264
+ return;
265
+ await moveAgent(t, agent, move, now);
266
+ }
267
+ /**
268
+ * At a pod Agent's start (`wantRunning`, `newVolume`): switches `agent` (locked) to the version it
269
+ * moves to when its runtime waits for this start. Applies the target's runtime and version, drops
270
+ * the pinned image digest when the image changed, and records what ran before (`startCheck`)
271
+ * until the new pod's harness joins. Returns whether it switched.
272
+ */
273
+ export async function switchTo(t, agent, now) {
274
+ if (agent.targetVersion === undefined || agent.appliedVersion === agent.version)
275
+ return false;
276
+ const target = await t.agentManifest(agent.manifestName, agent.targetVersion);
277
+ if (!target)
278
+ return false;
279
+ // A start that replaces one still being checked goes back as far as that one would.
280
+ const from = agent.startCheck?.from ?? {
281
+ manifestVersion: agent.manifestVersion,
282
+ manifestHash: agent.manifestHash,
283
+ applied: agent.applied,
284
+ ...(agent.pod?.imageDigest === undefined ? {} : { imageDigest: agent.pod.imageDigest }),
285
+ };
286
+ await t.updateEnvironment(agent.id, {
287
+ applied: agent.recipe,
288
+ appliedVersion: agent.version,
289
+ manifestVersion: target.version,
290
+ manifestHash: target.hash,
291
+ targetVersion: null,
292
+ ...(agent.pod ? { startCheck: { from, startedAt: now } } : {}),
293
+ }, now);
294
+ if (agent.pod?.imageDigest !== undefined && canonical(agent.recipe.image) !== canonical(agent.applied.image))
295
+ await t.updateEnvironmentPod(agent.id, { imageDigest: null }, now);
296
+ await t.agentEvent(agent.id, "agent.version_changed", { from: agent.manifestVersion, to: target.version });
297
+ await mergeAgentTriggers(t, { ...agent, manifestVersion: target.version }, agent.manifestVersion);
298
+ return true;
299
+ }
300
+ /** What an outdated Agent waits for: its secrets, or its pod's next start; `null` when it is not. */
301
+ export function waitingFor(agent) {
302
+ if (agent.targetVersion === undefined)
303
+ return null;
304
+ return agent.appliedVersion === agent.version ? "secrets" : "start";
305
+ }
306
+ /** How long pod Agent `agent` may idle before it stops: shorter while a start would apply a new version. */
307
+ export function idleMsOf(config, agent) {
308
+ const idleMs = podLifecycleConfig(config, agent.recipe).idleMs;
309
+ return waitingFor(agent) === "start" ? Math.min(idleMs, hostTuning().tenant.upgradeIdleMs) : idleMs;
310
+ }
311
+ /**
312
+ * The harness of pod Agent `id` joined at host epoch `epoch` (its `hello`): a start on a new
313
+ * version succeeded, setup included. Clears the start check and the failed version.
314
+ */
315
+ export async function passStartCheck(ctx, id, epoch) {
316
+ await ctx.store.tx(async (t) => {
317
+ const agent = await t.environment(id, { lock: true });
318
+ if (!agent?.startCheck || agent.pod?.hostEpoch !== epoch)
319
+ return;
320
+ await t.updateEnvironment(id, { startCheck: null, failedVersion: null }, new Date().toISOString());
321
+ });
322
+ }
323
+ /**
324
+ * Pod Agent `agent` (locked) did not start on the version it switched to (`reason`): back to what
325
+ * it ran before (`startCheck.from`), its definition and not its data, with that version as its
326
+ * `failedVersion`, and its pod asked to run again. A turn waiting for the start runs on the
327
+ * version it went back to. Returns whether there was a start to go back from.
328
+ */
329
+ export async function revertStart(t, agent, reason, now) {
330
+ const check = agent.startCheck;
331
+ const pod = agent.pod;
332
+ if (!check || !pod)
333
+ return false;
334
+ const version = agent.version + 1;
335
+ await t.updateEnvironment(agent.id, {
336
+ recipe: check.from.applied,
337
+ applied: check.from.applied,
338
+ version,
339
+ appliedVersion: version,
340
+ manifestVersion: check.from.manifestVersion,
341
+ manifestHash: check.from.manifestHash,
342
+ targetVersion: null,
343
+ failedVersion: agent.manifestVersion,
344
+ startCheck: null,
345
+ }, now);
346
+ await t.updateEnvironmentPod(agent.id, {
347
+ imageDigest: check.from.imageDigest ?? null,
348
+ desired: "running",
349
+ rev: pod.rev + 1,
350
+ observed: "creating",
351
+ startedAt: now,
352
+ lastActiveAt: now,
353
+ reason: null,
354
+ }, now);
355
+ await t.agentEvent(agent.id, "agent.start_failed", {
356
+ from: check.from.manifestVersion,
357
+ to: agent.manifestVersion,
358
+ reason,
359
+ reverted: true,
360
+ });
361
+ await t.agentEvent(agent.id, "agent.version_changed", { from: agent.manifestVersion, to: check.from.manifestVersion });
362
+ await mergeAgentTriggers(t, { ...agent, manifestVersion: check.from.manifestVersion }, agent.manifestVersion);
363
+ await t.signalSandbox(agent.id, { kind: "reconcile" });
364
+ return true;
365
+ }
366
+ /**
367
+ * The Tenant sweep: each pod Agent whose start on a new version has taken longer than the Host's
368
+ * `tenant.upgradeStartMs` goes back to the version it ran (`revertStart`).
369
+ */
370
+ export async function sweepStartChecks(ctx, now = Date.now()) {
371
+ const limit = hostTuning().tenant.upgradeStartMs;
372
+ const due = (agent) => agent.startCheck !== undefined && Date.parse(agent.startCheck.startedAt) + limit <= now;
373
+ for (const candidate of (await ctx.store.tx((t) => t.listEnvironments())).filter(due)) {
374
+ try {
375
+ await ctx.store.tx(async (t) => {
376
+ const agent = await t.environment(candidate.id, { lock: true });
377
+ if (!agent || !due(agent))
378
+ return;
379
+ await revertStart(t, agent, `The pod did not start within ${Math.round(limit / 1000)} s`, new Date(now).toISOString());
380
+ });
381
+ }
382
+ catch (error) {
383
+ ctx.config.logger.warn("agent start check not swept", {
384
+ environmentId: candidate.id,
385
+ message: error instanceof Error ? error.message : String(error),
386
+ });
387
+ }
388
+ }
389
+ }
@@ -0,0 +1,230 @@
1
+ import { type AgentAttention, type AgentMovePlan, type AgentState, type EnvironmentRecipe, type AgentView, type ForkAgentRequest, type ForkAgentResponse, type PutAgentRequest, type PutAgentSecretRequest } from "@nylorun/core/contracts";
2
+ import type { Environment, AgentEventsQuery, Tx } from "../store/types.js";
3
+ import type { SandboxRouteDeps } from "../core/sandbox-routes.js";
4
+ import { type AuthScope, type Session, type TenantContext } from "./context.js";
5
+ import type { SealedSecretCopies } from "../vault/service.js";
6
+ /** The Agent grants that limit `scope`, or undefined when it reaches every Agent. */
7
+ export declare function agentGrantsOf(scope: AuthScope): readonly string[] | undefined;
8
+ /** Who the vault's audit rows name for a request: the subject, else the application. */
9
+ export declare function actorOf(scope: AuthScope): string;
10
+ /** Refuses (the 404 of a missing one) Agent `id` when the caller's grants miss it. */
11
+ export declare function assertReaches(scope: AuthScope, id: string): void;
12
+ /** An Agent's row, unless it is gone or being deleted. */
13
+ export declare function liveEnvironment(t: Tx, id: string, options?: {
14
+ lock?: boolean;
15
+ }): Promise<Environment | undefined>;
16
+ /**
17
+ * An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
18
+ * every state that holds with the session behind it (`attention`), highest first. A session waits
19
+ * on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
20
+ * a move waits for its required secrets (track T3); a session's last turn
21
+ * failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
22
+ */
23
+ export declare function stateOf(agent: Pick<Environment, "desiredState" | "pod"> & Partial<Pick<Environment, "targetVersion" | "appliedVersion" | "version" | "failedVersion">>, sessions: readonly Pick<Session, "id" | "status">[]): {
24
+ state: AgentState;
25
+ attention: AgentAttention[];
26
+ };
27
+ /** How an Agent names its manifest: `name` when it tracks it, `name@n` when pinned. */
28
+ export declare function manifestRefOf(agent: Pick<Environment, "manifestName" | "manifestVersion" | "manifestRef">): string;
29
+ /**
30
+ * Why input files cannot be used: each that is not a version of a Tenant-wide artifact (a
31
+ * session's artifacts go with the session). Pins nothing else: the version is the one named.
32
+ */
33
+ export declare function fileErrors(t: Tx, files: EnvironmentRecipe["files"]): Promise<string[]>;
34
+ /**
35
+ * `PUT /v1/agents/{id}`: creates the Agent and its vault (within the Tenant's limit on their
36
+ * number) from a saved agent manifest's version, or finds the one with this id, so "get or
37
+ * create" is one call. On an existing one, its labels and owner guard may change, and `manifest`
38
+ * moves it to the version it names (track T3, `moveAgent`): `name` tracks the latest (unless that
39
+ * failed to start), `name@n` pins `n`. Another manifest name is `409 agent_fixed_field`. Every
40
+ * `PUT` counts as the Agent's use.
41
+ */
42
+ export declare function putAgent(ctx: TenantContext, id: string, body: PutAgentRequest, scope: AuthScope, options?: {
43
+ fork?: Fork;
44
+ }): Promise<AgentView>;
45
+ /**
46
+ * `PUT /v1/agents/{id}?dryRun=true` (track T3): what moving existing Agent `id` to the version
47
+ * `body.manifest` names would do (`AgentMovePlan`), with nothing changed.
48
+ */
49
+ export declare function planAgentPut(ctx: TenantContext, id: string, body: PutAgentRequest, scope: AuthScope): Promise<{
50
+ dryRun: true;
51
+ plan: AgentMovePlan;
52
+ }>;
53
+ /**
54
+ * After a secret write on Agent `id`: a move that waited for its required secrets goes on once
55
+ * they are set (track T3). Best effort: the write stands either way.
56
+ */
57
+ export declare function secretWritten(ctx: TenantContext, id: string): Promise<void>;
58
+ /**
59
+ * The secret write `raw` asks of Environment `id` (D64). When what it runs declares secret
60
+ * slots, the name must be one (`409 secret_not_in_manifest`), and the body is `{ value }` (or
61
+ * `{ headers }` for a `headers` slot) with the binding from the slot; a full typed body is
62
+ * accepted only when its binding is the slot's. Without slots, today's typed body.
63
+ */
64
+ export declare function secretRequestFor(ctx: TenantContext, id: string, name: string, raw: unknown): Promise<PutAgentSecretRequest>;
65
+ /** What a create does for a fork (`POST .../fork`); `copied` is filled in by the create. */
66
+ interface Fork {
67
+ source: string;
68
+ /** The manifest version the plan resolved, which the create makes the fork from. */
69
+ version: number;
70
+ setup: boolean;
71
+ /**
72
+ * The source's secrets sealed by keys for the new vault `vaultId` (F4.2), before the create's
73
+ * transaction, which inserts them; absent with `secrets: none`.
74
+ */
75
+ sealed?: {
76
+ vaultId: string;
77
+ copies: SealedSecretCopies;
78
+ };
79
+ copied?: {
80
+ copied: string[];
81
+ dropped: string[];
82
+ };
83
+ }
84
+ /**
85
+ * `POST /v1/agents/{id}/fork` (Q15): copies an Agent into a new one, which is how an Agent changes
86
+ * what is fixed for its life (its manifest, kind, harness or storage). A new Agent `body.id` is made from the manifest
87
+ * (default the source's manifest name, tracking, at its latest version), its disk started from a
88
+ * version of the source's (`latest` takes a snapshot first, refused during a turn), and its
89
+ * secrets copied inside the Runtime. A fork onto other setup runs that setup on the copied disk
90
+ * before its first turn (Q17). The source keeps what it runs, its disk and its sessions. With
91
+ * `dryRun`, the plan only.
92
+ */
93
+ export declare function forkAgent(ctx: TenantContext, sourceId: string, body: ForkAgentRequest, scope: AuthScope): Promise<ForkAgentResponse>;
94
+ /** `GET /v1/agents/{id}`. */
95
+ export declare function getAgent(ctx: TenantContext, id: string, scope: AuthScope): Promise<AgentView>;
96
+ /**
97
+ * What `GET /v1/agents` filters on: labels (all of them), the derived state, the manifest's name
98
+ * and whether it is outdated.
99
+ */
100
+ export interface AgentFilters {
101
+ labels: Record<string, string>;
102
+ state?: string;
103
+ manifest?: string;
104
+ /** Only Agents moving to another version (track T3): waiting for their secrets or their pod's start. */
105
+ outdated?: boolean;
106
+ }
107
+ /**
108
+ * `GET /v1/agents?label=k=v&state=s&manifest=m`: every Agent with all the labels, in the state
109
+ * and made from the manifest, that the caller reaches, by id. With `page`, at most `limit` after
110
+ * the Agent id `after` (the cursor), and the id to continue from.
111
+ */
112
+ export declare function listAgents(ctx: TenantContext, filters: AgentFilters, scope: AuthScope, page?: {
113
+ limit: number;
114
+ after?: string;
115
+ }): Promise<{
116
+ agents: AgentView[];
117
+ next: string | null;
118
+ }>;
119
+ /**
120
+ * Asks pod Environment `id` to run when it is stopped or failed (a disk copy needs its engine),
121
+ * with its recipe applied, as a turn start would.
122
+ */
123
+ export declare function wakePod(ctx: TenantContext, id: string): Promise<void>;
124
+ /** Whether a disk lease (R2d step 3) holds `environment` now. */
125
+ export declare function diskHeld(environment: Environment, now?: number): boolean;
126
+ /** Refused while a snapshot or restore copies the disk (a pending seed does not count). */
127
+ export declare function refuseDiskCopy(environment: Environment): void;
128
+ /**
129
+ * `DELETE /v1/agents/{id}`: deletes the Environment, its vault and its workspace. Refused
130
+ * while a session that names it has an active turn. Sessions keep its id: their next turn is
131
+ * refused (`agent_unavailable`) unless an Environment with this id exists again.
132
+ */
133
+ export declare function deleteAgent(ctx: TenantContext, id: string, scope: AuthScope): Promise<{
134
+ id: string;
135
+ deleted: boolean;
136
+ }>;
137
+ /**
138
+ * Deletes every Environment unused for its `lifecycle.deleteAfter` (Q13): no turn, recipe
139
+ * change, secret write, stop or reset since. One with a turn running waits for the next sweep.
140
+ * Run by the Tenant sweep.
141
+ */
142
+ export declare function sweepUnusedAgents(ctx: TenantContext, now?: number): Promise<void>;
143
+ /**
144
+ * `POST /v1/agents/{id}/stop`: suspends a pod Environment. Its volume is kept, and the next
145
+ * turn of a session that names it starts it again. Refused during a turn.
146
+ */
147
+ export declare function stopAgent(ctx: TenantContext, id: string, scope: AuthScope): Promise<AgentView>;
148
+ /**
149
+ * `POST /v1/agents/{id}/restart` (track T3): applies a pending version now, refused during a
150
+ * turn. With `retry`, it first moves the Agent to the version that failed to start again. A
151
+ * running pod stops, so the next turn starts it on the version; a stopped or failed one is asked
152
+ * to run at once. Kinds `virtual` and `none` apply a version at once anyway.
153
+ */
154
+ export declare function restartAgent(ctx: TenantContext, id: string, options: {
155
+ retry?: boolean;
156
+ }, scope: AuthScope): Promise<AgentView>;
157
+ /**
158
+ * `POST /v1/agents/{id}/reset`: a new pod on a new, empty volume (the next volume
159
+ * generation), with a new join token and the recipe applied; the old Sandbox and volume are
160
+ * deleted. Leaves `lost` and `failed`. Refused during a turn, and while an earlier reset's
161
+ * Sandbox is still being deleted.
162
+ */
163
+ export declare function resetAgent(ctx: TenantContext, id: string, scope: AuthScope): Promise<AgentView>;
164
+ /**
165
+ * Gives pod Environment `environment` (locked) a new pod on a new, empty volume, the next volume
166
+ * generation, with its recipe applied: the old Sandbox and volume are retired. With `seed`, the
167
+ * new volume starts from that disk version before any turn (R2d step 3): a restore, or the
168
+ * recovery of a lost volume.
169
+ */
170
+ export declare function newVolume(ctx: TenantContext, t: Tx, environment: Environment, options?: {
171
+ seed?: {
172
+ artifactId: string;
173
+ version: number;
174
+ };
175
+ reason?: "restore" | "lost";
176
+ }): Promise<number>;
177
+ /** `GET /v1/agents/{id}/events`: the Environment's lifecycle stream. */
178
+ export declare function agentEventsOf(ctx: TenantContext, id: string, scope: AuthScope, query?: AgentEventsQuery): Promise<{
179
+ events: {
180
+ schema: "nylorun.agent-event/1";
181
+ eventId: string;
182
+ tenantId: string;
183
+ agentId: string;
184
+ seq: number;
185
+ time: string;
186
+ type: "agent.attached" | "agent.created" | "agent.deleted" | "agent.detached" | "agent.egress" | "agent.failed" | "agent.forked" | "agent.lost" | "agent.memory_updated" | "agent.outdated" | "agent.relaunched" | "agent.reset" | "agent.run" | "agent.running" | "agent.start_failed" | "agent.suspended" | "agent.trigger_paused" | "agent.trigger_resumed" | "agent.trigger_secret_rotated" | "agent.version_changed";
187
+ payload: Record<string, unknown>;
188
+ }[];
189
+ }>;
190
+ /**
191
+ * Opens a new session on Environment `id` (`PutSessionRequest.environment`): it must exist, be
192
+ * reachable with `grants` and pass its owner guard. Runs in the session's transaction, after
193
+ * its session row lock; holds the Environment's row lock.
194
+ */
195
+ export declare function openAgent(t: Tx, id: string, options: {
196
+ grants?: readonly string[];
197
+ ownerUserId: string;
198
+ opaque: boolean;
199
+ }): Promise<Environment>;
200
+ /** Records the attachment on the session's log and on the Environment's stream. */
201
+ export declare function recordAttachment(t: Tx, sessionId: string, agentId: string): Promise<void>;
202
+ /**
203
+ * Checks a turn start on a session that names an Environment, in the command's transaction
204
+ * after the session row lock: the Environment still exists, a token caller's grants reach it,
205
+ * its owner guard admits the session's owner, and no disk copy holds it. Other sessions' turns
206
+ * do not stop it (D66). Holds the Environment row's lock until the transaction ends, so a turn
207
+ * start and disk work on one Environment are decided one after the other.
208
+ *
209
+ * A pod Environment must also be usable: not lost (`agent_lost`), on a Runtime with
210
+ * sandbox pods (`agent_unavailable`). A stopped or failed pod is asked to run again, with
211
+ * its recipe applied, after the commit.
212
+ */
213
+ export declare function checkAgentTurn(t: Tx, session: Session, scope: AuthScope, ctx?: TenantContext): Promise<void>;
214
+ /** A sessions reset detaches every session from its Environment: each one's stream says so. */
215
+ export declare function detachAllSessions(t: Tx): Promise<void>;
216
+ /**
217
+ * Marks the end of a turn's segment on Environment `id`: it was used now (`deleteAfter` counts
218
+ * from here), and for a running pod idle counts from now and the idle timer is armed (D34).
219
+ * Best effort.
220
+ */
221
+ export declare function touchAgent(ctx: TenantContext, id: string): Promise<void>;
222
+ /**
223
+ * Re-arms the idle timer of every running pod after the Tenant's settings change, so a shorter
224
+ * `limits.idle` applies to pods already idle (D36: the settings are read whenever a timer is
225
+ * set). Best effort.
226
+ */
227
+ export declare function rearmPodTimers(ctx: TenantContext): Promise<void>;
228
+ /** Session lookups for the sandbox route handlers (`core/sandbox-routes.ts`). */
229
+ export declare function sandboxRouteDeps(ctx: TenantContext): SandboxRouteDeps;
230
+ export {};