@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.
- package/CHANGELOG.md +67 -0
- package/README.md +1 -1
- package/dist/api/a2a/card.d.ts +4 -3
- package/dist/api/a2a/card.js +3 -3
- package/dist/api/a2a/routes.js +18 -7
- package/dist/api/ag-ui/routes.js +4 -7
- package/dist/api/components.d.ts +5854 -2718
- package/dist/api/components.js +39 -30
- package/dist/api/http/app.js +9 -4
- package/dist/api/http/body.d.ts +2 -0
- package/dist/api/http/body.js +20 -0
- package/dist/api/http/define.d.ts +4 -0
- package/dist/api/http/define.js +20 -9
- package/dist/api/http/routes/agent-manifests.d.ts +10 -0
- package/dist/api/http/routes/agent-manifests.js +135 -0
- package/dist/api/http/routes/agents.d.ts +11 -0
- package/dist/api/http/routes/agents.js +418 -0
- package/dist/api/http/routes/artifacts.js +2 -2
- package/dist/api/http/routes/coverage.d.ts +4 -4
- package/dist/api/http/routes/coverage.js +12 -6
- package/dist/api/http/routes/files.js +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.d.ts +1 -1
- package/dist/api/http/routes/me.js +1 -2
- package/dist/api/http/routes/memories.d.ts +9 -0
- package/dist/api/http/routes/memories.js +152 -0
- package/dist/api/http/routes/reads.d.ts +1 -137
- package/dist/api/http/routes/reads.js +31 -12
- package/dist/api/http/routes/sessions.js +21 -50
- package/dist/api/http/routes/tenant.js +13 -10
- package/dist/api/http/routes/triggers.d.ts +10 -0
- package/dist/api/http/routes/triggers.js +183 -0
- package/dist/api/openapi.js +69 -47
- package/dist/artifacts/disk.d.ts +10 -10
- package/dist/artifacts/disk.js +27 -27
- package/dist/artifacts/quota.d.ts +1 -1
- package/dist/artifacts/quota.js +2 -2
- package/dist/artifacts/service.js +2 -2
- package/dist/core/flow-host.d.ts +0 -1
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +5 -5
- package/dist/gates/egress.js +6 -6
- package/dist/gates/http-tool.js +2 -2
- package/dist/gates/meter.js +22 -5
- package/dist/gates/policy.d.ts +8 -3
- package/dist/gates/policy.js +7 -2
- package/dist/gates/tenant-vaults.d.ts +1 -1
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/setup.js +17 -6
- package/dist/harness-api/record.js +11 -0
- package/dist/harness-api/server.js +4 -0
- package/dist/harness-api/start.d.ts +11 -7
- package/dist/harness-api/start.js +49 -5
- package/dist/harness-api/workspace.js +2 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +5 -2
- package/dist/host/main.js +1 -0
- package/dist/keys/keys.d.ts +15 -4
- package/dist/keys/keys.js +16 -0
- package/dist/keys/webhooks.d.ts +25 -0
- package/dist/keys/webhooks.js +62 -0
- package/dist/management-openapi.json +313 -133
- package/dist/mcp/connect.d.ts +2 -2
- package/dist/mcp/connect.js +3 -3
- package/dist/mcp/preview.js +7 -7
- package/dist/memory/service.d.ts +97 -0
- package/dist/memory/service.js +335 -0
- package/dist/memory/tool.d.ts +36 -0
- package/dist/memory/tool.js +84 -0
- package/dist/model/pi-model.js +7 -0
- package/dist/openapi.json +13120 -9034
- package/dist/reads/types.d.ts +3 -3
- package/dist/record/agent.d.ts +27 -0
- package/dist/record/agent.js +46 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/join.js +5 -5
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +3 -3
- package/dist/sandbox/pods/lifecycle.d.ts +6 -6
- package/dist/sandbox/pods/lifecycle.js +6 -6
- package/dist/sandbox/pods/reconcile.js +10 -2
- package/dist/sandbox/resolve.d.ts +3 -3
- package/dist/sandbox/resolve.js +7 -7
- package/dist/sandbox/setup.d.ts +3 -1
- package/dist/sandbox/setup.js +14 -4
- package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
- package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
- package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
- package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
- package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +44 -87
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/schema.d.ts +1408 -123
- package/dist/store/postgres/schema.js +194 -38
- package/dist/store/postgres/store.js +329 -79
- package/dist/store/postgres/tenant.js +1 -2
- package/dist/store/types.d.ts +155 -50
- package/dist/store/types.js +0 -1
- package/dist/tenant/advance.js +20 -8
- package/dist/tenant/agent-manifests.d.ts +72 -0
- package/dist/tenant/agent-manifests.js +359 -0
- package/dist/tenant/agent-versions.d.ts +126 -0
- package/dist/tenant/agent-versions.js +389 -0
- package/dist/tenant/agents.d.ts +230 -0
- package/dist/tenant/{environments.js → agents.js} +464 -320
- package/dist/tenant/auth.d.ts +1 -1
- package/dist/tenant/auth.js +3 -3
- package/dist/tenant/commands.d.ts +63 -2
- package/dist/tenant/commands.js +240 -83
- package/dist/tenant/context.d.ts +46 -19
- package/dist/tenant/context.js +6 -2
- package/dist/tenant/effects.js +7 -16
- package/dist/tenant/identity-file.d.ts +6 -6
- package/dist/tenant/identity-file.js +12 -10
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/limits.d.ts +0 -3
- package/dist/tenant/limits.js +0 -3
- package/dist/tenant/memories.d.ts +38 -0
- package/dist/tenant/memories.js +119 -0
- package/dist/tenant/reset.js +2 -2
- package/dist/tenant/run-grants.d.ts +6 -0
- package/dist/tenant/run-grants.js +4 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +8 -0
- package/dist/tenant/sessions.d.ts +28 -45
- package/dist/tenant/sessions.js +85 -208
- package/dist/tenant/status.js +6 -7
- package/dist/tenant/sweep.js +12 -3
- package/dist/tenant/transcript.d.ts +16 -16
- package/dist/tenant/triggers.d.ts +143 -0
- package/dist/tenant/triggers.js +715 -0
- package/dist/tenant/turn-timing.d.ts +11 -1
- package/dist/tenant/turn-timing.js +3 -1
- package/dist/tenant/types.d.ts +5 -0
- package/dist/tenant/worker.d.ts +2 -0
- package/dist/tenant/worker.js +5 -0
- package/dist/traces/map.d.ts +1 -1
- package/dist/traces/map.js +7 -1
- package/dist/tuning.d.ts +7 -0
- package/dist/tuning.js +19 -0
- package/dist/vault/service.d.ts +13 -8
- package/dist/vault/service.js +61 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -12
- package/dist/api/http/routes/environment-manifests.d.ts +0 -9
- package/dist/api/http/routes/environment-manifests.js +0 -118
- package/dist/api/http/routes/environments.d.ts +0 -11
- package/dist/api/http/routes/environments.js +0 -332
- package/dist/record/environment.d.ts +0 -27
- package/dist/record/environment.js +0 -46
- package/dist/tenant/environment-manifests.d.ts +0 -57
- package/dist/tenant/environment-manifests.js +0 -208
- 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 {};
|