@nylorun/runtime 0.15.1-beta → 0.16.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 +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandboxes as a resource (blueprint D39, F7.1; Host feature `sandboxes`): `PUT`, `GET` and
|
|
3
|
+
* `DELETE /v1/sandboxes/{id}`, the list by label, sessions attaching with `sandbox: { id }`, and
|
|
4
|
+
* the checks every turn start makes on the sandbox its session is attached to.
|
|
5
|
+
*
|
|
6
|
+
* - **No scope.** The Runtime decides nothing about who shares a sandbox. It stores the
|
|
7
|
+
* sandbox's id, kind, spec and labels, and the session body names the sandbox it is attached
|
|
8
|
+
* to (`Session.sandboxId`); deleting a session (a sessions reset) only detaches it.
|
|
9
|
+
* - **Access.** An application key reaches every sandbox, and so does an application key acting
|
|
10
|
+
* for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A subject
|
|
11
|
+
* token reaches only the ids its `sbx` grants match, and changes them only with
|
|
12
|
+
* `sandboxes:write`; any other id is the 404 of a missing one.
|
|
13
|
+
* - **Serial turns.** One turn at a time per sandbox: a session whose sandbox another attached
|
|
14
|
+
* session's turn holds is refused (`409 sandbox_busy`), as the session sandbox route is
|
|
15
|
+
* refused while its session's turn runs. Commands within the workspace keep queueing in the
|
|
16
|
+
* Sandbox Manager.
|
|
17
|
+
* - **Kinds.** `virtual`: its workspace is the virtual backend's, keyed by the sandbox id
|
|
18
|
+
* (`SandboxManager.sandboxKeyOf`) and created on the first tool call. `pod` (F7.2, Host feature
|
|
19
|
+
* `sandbox-pods`): an agent-sandbox pod on the Tenant's cluster, created at once, whose engine
|
|
20
|
+
* runs the turns of the sessions attached to it. Its lifecycle (`sandbox/pods/lifecycle.ts`)
|
|
21
|
+
* runs in the `Sandbox` object: this module records what is wanted (`desired`, `rev`) and asks
|
|
22
|
+
* for a reconcile after the commit (`ctx.sandboxSignal`). Without sandbox pods kind `pod` is
|
|
23
|
+
* `409 sandbox_unavailable`. `POST .../stop` suspends a pod (the volume is kept; the next turn
|
|
24
|
+
* starts it), `POST .../reset` gives it a new pod on a new volume. An expired pod refuses
|
|
25
|
+
* turns (`sandbox_expired`) until a `PUT` sets a longer `lifecycle.ttl`; a lost one
|
|
26
|
+
* (`sandbox_lost`) until a reset. Deleting a pod sandbox keeps its row (`desired: deleted`,
|
|
27
|
+
* invisible here) until its Sandbox is gone.
|
|
28
|
+
* - **Lifecycle events** go to the sandbox's own stream in the record (`Tx.sandboxEvent`,
|
|
29
|
+
* `record/sandbox.ts`); the session's log records `sandbox.attached` too.
|
|
30
|
+
*/
|
|
31
|
+
import { sandboxGranted, } from "@nylorun/core/contracts";
|
|
32
|
+
import { canonical } from "../store/canonical.js";
|
|
33
|
+
import { resolveSandbox } from "../sandbox/resolve.js";
|
|
34
|
+
import { sandboxWorkspaceKey } from "../sandbox/records.js";
|
|
35
|
+
import { effectiveSandboxConfig, memoryMiB, readSandboxConfig, } from "../sandbox/tenant-config.js";
|
|
36
|
+
import { podName } from "../sandbox/pods/name.js";
|
|
37
|
+
import { expiresAtOf, podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
38
|
+
import { requirePods } from "../sandbox/placement.js";
|
|
39
|
+
import { parseSandboxDuration } from "@nylorun/core/define";
|
|
40
|
+
import { fail } from "./http.js";
|
|
41
|
+
/** The `sbx` grants that limit `scope`, or undefined when it reaches every sandbox. */
|
|
42
|
+
export function sandboxGrantsOf(scope) {
|
|
43
|
+
return scope.kind === "token" ? (scope.sandboxes ?? []) : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** The subject whose sessions a sandbox's view lists, when the request acts for one. */
|
|
46
|
+
function ownerOf(scope) {
|
|
47
|
+
return scope.kind === "token" || scope.kind === "subject" ? scope.subject : undefined;
|
|
48
|
+
}
|
|
49
|
+
const notFound = () => fail(404, "Sandbox not found");
|
|
50
|
+
/** The sandbox, when `grants` reach it; otherwise the 404 of a missing one. */
|
|
51
|
+
function reachable(grants, id) {
|
|
52
|
+
if (grants !== undefined && !sandboxGranted(grants, id))
|
|
53
|
+
notFound();
|
|
54
|
+
}
|
|
55
|
+
/** A pod sandbox being deleted is no longer a sandbox, here. */
|
|
56
|
+
function live(row) {
|
|
57
|
+
return row?.pod?.desired === "deleted" ? undefined : row;
|
|
58
|
+
}
|
|
59
|
+
/** A sandbox resource, unless it is gone or being deleted. */
|
|
60
|
+
export async function liveSandbox(t, id, options = {}) {
|
|
61
|
+
return live(await t.sandboxResource(id, options));
|
|
62
|
+
}
|
|
63
|
+
/** The compute state a pod's view reports. */
|
|
64
|
+
function podState(pod) {
|
|
65
|
+
if (pod.observed === "running")
|
|
66
|
+
return "running";
|
|
67
|
+
if (pod.observed === "creating")
|
|
68
|
+
return "creating";
|
|
69
|
+
return "stopped";
|
|
70
|
+
}
|
|
71
|
+
async function viewOf(ctx, t, sandbox, owner) {
|
|
72
|
+
const sessions = await t.sessionsOnSandbox(sandbox.id);
|
|
73
|
+
const record = sandbox.pod
|
|
74
|
+
? undefined
|
|
75
|
+
: await t.get("sandboxes", sandboxWorkspaceKey(ctx.config.tenantId, sandbox.id));
|
|
76
|
+
const pod = sandbox.pod;
|
|
77
|
+
return {
|
|
78
|
+
id: sandbox.id,
|
|
79
|
+
kind: sandbox.kind,
|
|
80
|
+
labels: sandbox.labels,
|
|
81
|
+
spec: sandbox.spec,
|
|
82
|
+
state: pod ? podState(pod) : (record?.state ?? "ready"),
|
|
83
|
+
...(pod
|
|
84
|
+
? {
|
|
85
|
+
pod: {
|
|
86
|
+
desired: pod.desired,
|
|
87
|
+
observed: pod.observed,
|
|
88
|
+
volumeGeneration: pod.volumeGen,
|
|
89
|
+
hostEpoch: pod.hostEpoch,
|
|
90
|
+
...(pod.expiresAt ? { expiresAt: pod.expiresAt } : {}),
|
|
91
|
+
...(pod.reason ? { reason: pod.reason } : {}),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
95
|
+
sessions: sessions
|
|
96
|
+
.filter((session) => owner === undefined || session.ownerUserId === owner)
|
|
97
|
+
.map((session) => ({ id: session.id, activeTurnId: session.activeTurnId })),
|
|
98
|
+
createdAt: sandbox.createdAt,
|
|
99
|
+
updatedAt: sandbox.updatedAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The spec a `PUT` asks for, resolved against the Tenant's limits. */
|
|
103
|
+
function resolveSpec(body, config, ttlLimit) {
|
|
104
|
+
const pod = body.kind === "pod";
|
|
105
|
+
if (!pod && (body.storage !== undefined || body.lifecycle !== undefined))
|
|
106
|
+
fail(400, "storage and lifecycle are for kind pod only");
|
|
107
|
+
const resolved = resolveSandbox({
|
|
108
|
+
request: {
|
|
109
|
+
...(body.image === undefined ? {} : { image: body.image }),
|
|
110
|
+
...(body.network === undefined ? {} : { network: body.network }),
|
|
111
|
+
...(body.resources === undefined ? {} : { resources: body.resources }),
|
|
112
|
+
},
|
|
113
|
+
config,
|
|
114
|
+
actingForSubject: false,
|
|
115
|
+
kind: pod ? "pod" : "virtual",
|
|
116
|
+
});
|
|
117
|
+
if (resolved.kind === "error")
|
|
118
|
+
return fail(resolved.status, resolved.errors.join(" "));
|
|
119
|
+
if (resolved.kind === "none")
|
|
120
|
+
return fail(400, "The sandbox has no spec");
|
|
121
|
+
if (!pod)
|
|
122
|
+
return resolved.spec;
|
|
123
|
+
checkTtl(body.lifecycle?.ttl, ttlLimit);
|
|
124
|
+
return {
|
|
125
|
+
...resolved.spec,
|
|
126
|
+
...(body.storage === undefined ? {} : { storage: body.storage }),
|
|
127
|
+
...(body.lifecycle?.ttl === undefined ? {} : { lifecycle: { ttl: body.lifecycle.ttl } }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Refuses a TTL longer than the Tenant's `limits.ttl`. */
|
|
131
|
+
function checkTtl(ttl, limit) {
|
|
132
|
+
if (ttl === undefined || limit === undefined)
|
|
133
|
+
return;
|
|
134
|
+
if ((parseSandboxDuration(ttl) ?? 0) > (parseSandboxDuration(limit) ?? Infinity))
|
|
135
|
+
fail(400, `lifecycle.ttl is ${ttl}; this Tenant allows at most ${limit}.`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Why an existing sandbox does not match what a `PUT` asks for, comparing only the fields the
|
|
139
|
+
* request sends: an `ensure` that repeats its spec keeps working after the Tenant's defaults or
|
|
140
|
+
* limits change. A pod's `lifecycle.ttl` is not part of the fixed spec.
|
|
141
|
+
*/
|
|
142
|
+
function specMismatch(existing, body) {
|
|
143
|
+
if (body.kind !== undefined && body.kind !== existing.kind)
|
|
144
|
+
return "kind";
|
|
145
|
+
if (body.image !== undefined && body.image !== existing.spec.image)
|
|
146
|
+
return "image";
|
|
147
|
+
if (body.network !== undefined) {
|
|
148
|
+
const asked = [...new Set((body.network.allow ?? []).map((host) => host.toLowerCase()))].sort();
|
|
149
|
+
const has = [...(existing.spec.network?.allow ?? [])].sort();
|
|
150
|
+
if (canonical(asked) !== canonical(has))
|
|
151
|
+
return "network";
|
|
152
|
+
}
|
|
153
|
+
const resources = existing.spec.resources;
|
|
154
|
+
if (body.resources?.cpus !== undefined && body.resources.cpus !== resources?.cpus)
|
|
155
|
+
return "resources";
|
|
156
|
+
if (body.resources?.memory !== undefined &&
|
|
157
|
+
(resources?.memory === undefined || memoryMiB(body.resources.memory) !== memoryMiB(resources.memory)))
|
|
158
|
+
return "resources";
|
|
159
|
+
if (body.storage !== undefined && body.storage !== existing.spec.storage)
|
|
160
|
+
return "storage";
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/** Asks for a pod's reconcile once the transaction commits. */
|
|
164
|
+
function reconcileAfter(ctx, t, id) {
|
|
165
|
+
t.afterCommit(() => ctx.sandboxSignal(id, { kind: "reconcile" }));
|
|
166
|
+
}
|
|
167
|
+
/** Asks the pod to run (again): a new `rev`, so the service applies it. */
|
|
168
|
+
async function wantRunning(t, sandbox, now) {
|
|
169
|
+
await t.updateSandboxPod(sandbox.id, {
|
|
170
|
+
desired: "running",
|
|
171
|
+
rev: sandbox.pod.rev + 1,
|
|
172
|
+
observed: "creating",
|
|
173
|
+
startedAt: now,
|
|
174
|
+
lastActiveAt: now,
|
|
175
|
+
reason: null,
|
|
176
|
+
}, now);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* `PUT /v1/sandboxes/{id}`: creates the sandbox (within the Tenant's limit on their number), or
|
|
180
|
+
* finds the one with this id, so "get or create" is one call. The spec is fixed once it exists;
|
|
181
|
+
* labels, when sent, replace its labels. On an existing pod sandbox it also starts a stopped or
|
|
182
|
+
* failed pod again, and a new `lifecycle.ttl` sets a new expiry (reviving an expired one).
|
|
183
|
+
*/
|
|
184
|
+
export async function putSandbox(ctx, id, body, scope) {
|
|
185
|
+
const grants = sandboxGrantsOf(scope);
|
|
186
|
+
reachable(grants, id);
|
|
187
|
+
if (body.kind === "pod")
|
|
188
|
+
await requirePods(ctx.pods, "A pod sandbox");
|
|
189
|
+
const conflict = (field) => fail(409, `Sandbox ${id} exists with another ${field}. Its spec is fixed: delete it to create it again.`);
|
|
190
|
+
return ctx.store.tx(async (t) => {
|
|
191
|
+
const now = new Date().toISOString();
|
|
192
|
+
const config = await readSandboxConfig(t);
|
|
193
|
+
const deleting = (row) => {
|
|
194
|
+
if (row && !live(row))
|
|
195
|
+
fail(409, `Sandbox ${id} is being deleted. Try again once it is gone.`, {
|
|
196
|
+
code: "sandbox_busy",
|
|
197
|
+
details: { retryAfterSeconds: 5 },
|
|
198
|
+
}, { "retry-after": "5" });
|
|
199
|
+
};
|
|
200
|
+
const found = async (existing) => {
|
|
201
|
+
const mismatch = specMismatch(existing, body);
|
|
202
|
+
if (mismatch)
|
|
203
|
+
conflict(mismatch);
|
|
204
|
+
let current = existing;
|
|
205
|
+
if (body.labels !== undefined && canonical(body.labels) !== canonical(existing.labels)) {
|
|
206
|
+
await t.updateSandboxLabels(id, body.labels, now);
|
|
207
|
+
current = { ...current, labels: body.labels, updatedAt: now };
|
|
208
|
+
}
|
|
209
|
+
const pod = current.pod;
|
|
210
|
+
if (pod) {
|
|
211
|
+
const spec = current.spec;
|
|
212
|
+
const ttl = body.lifecycle?.ttl;
|
|
213
|
+
let revive = false;
|
|
214
|
+
if (ttl !== undefined && ttl !== spec.lifecycle?.ttl) {
|
|
215
|
+
checkTtl(ttl, config.limits?.ttl);
|
|
216
|
+
const next = { ...spec, lifecycle: { ...spec.lifecycle, ttl } };
|
|
217
|
+
const expiresAt = expiresAtOf(current.createdAt, ttl);
|
|
218
|
+
await t.updateSandboxSpec(id, next, now);
|
|
219
|
+
await t.updateSandboxPod(id, { expiresAt: expiresAt ?? null }, now);
|
|
220
|
+
current = { ...current, spec: next, pod: { ...pod, ...(expiresAt ? { expiresAt } : {}) } };
|
|
221
|
+
revive = pod.observed === "expired" && expiresAt !== undefined && Date.parse(expiresAt) > Date.now();
|
|
222
|
+
}
|
|
223
|
+
if (pod.observed === "lost")
|
|
224
|
+
fail(409, `Sandbox ${id} was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, {
|
|
225
|
+
code: "sandbox_lost",
|
|
226
|
+
});
|
|
227
|
+
if (revive || pod.desired !== "running" || pod.observed === "failed") {
|
|
228
|
+
await wantRunning(t, current, now);
|
|
229
|
+
reconcileAfter(ctx, t, id);
|
|
230
|
+
}
|
|
231
|
+
return viewOf(ctx, t, (await t.sandboxResource(id)) ?? current, ownerOf(scope));
|
|
232
|
+
}
|
|
233
|
+
return viewOf(ctx, t, current, ownerOf(scope));
|
|
234
|
+
};
|
|
235
|
+
const existing = await t.sandboxResource(id, { lock: true });
|
|
236
|
+
deleting(existing);
|
|
237
|
+
if (existing)
|
|
238
|
+
return found(existing);
|
|
239
|
+
const effective = effectiveSandboxConfig(config);
|
|
240
|
+
const spec = resolveSpec(body, effective, config.limits?.ttl);
|
|
241
|
+
const kind = body.kind ?? "virtual";
|
|
242
|
+
const row = {
|
|
243
|
+
id,
|
|
244
|
+
kind,
|
|
245
|
+
spec,
|
|
246
|
+
labels: body.labels ?? {},
|
|
247
|
+
createdAt: now,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
...(kind === "pod"
|
|
250
|
+
? {
|
|
251
|
+
pod: {
|
|
252
|
+
k8sName: podName(ctx.config.tenantId, id, 0),
|
|
253
|
+
volumeGen: 0,
|
|
254
|
+
desired: "running",
|
|
255
|
+
observed: "creating",
|
|
256
|
+
hostEpoch: 0,
|
|
257
|
+
rev: 1,
|
|
258
|
+
startedAt: now,
|
|
259
|
+
lastActiveAt: now,
|
|
260
|
+
...(body.lifecycle?.ttl ? { expiresAt: expiresAtOf(now, body.lifecycle.ttl) } : {}),
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
: {}),
|
|
264
|
+
};
|
|
265
|
+
const created = await t.createSandboxResource(row, effective.limits.sandboxes);
|
|
266
|
+
if (created === "limit")
|
|
267
|
+
fail(409, `This Tenant allows at most ${effective.limits.sandboxes} sandboxes.`, {
|
|
268
|
+
code: "limit_exceeded",
|
|
269
|
+
details: { limit: "sandboxes", max: effective.limits.sandboxes },
|
|
270
|
+
});
|
|
271
|
+
if (created === "exists") {
|
|
272
|
+
// Created by another request since the read above: answer as for an existing one.
|
|
273
|
+
const again = (await t.sandboxResource(id, { lock: true })) ?? notFound();
|
|
274
|
+
deleting(again);
|
|
275
|
+
return found(again);
|
|
276
|
+
}
|
|
277
|
+
await t.sandboxEvent(id, "sandbox.created", { kind, labels: row.labels });
|
|
278
|
+
if (row.pod) {
|
|
279
|
+
// Created at once; idle from now if no turn comes.
|
|
280
|
+
reconcileAfter(ctx, t, id);
|
|
281
|
+
const idleAt = Date.parse(now) + podLifecycleConfig(config).idleMs;
|
|
282
|
+
t.afterCommit(() => ctx.sandboxSignal(id, { kind: "arm", timer: "idle", at: idleAt }));
|
|
283
|
+
}
|
|
284
|
+
return viewOf(ctx, t, row, ownerOf(scope));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** `GET /v1/sandboxes/{id}`. */
|
|
288
|
+
export async function getSandbox(ctx, id, scope) {
|
|
289
|
+
reachable(sandboxGrantsOf(scope), id);
|
|
290
|
+
return ctx.store.tx(async (t) => {
|
|
291
|
+
const sandbox = (await liveSandbox(t, id)) ?? notFound();
|
|
292
|
+
return viewOf(ctx, t, sandbox, ownerOf(scope));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** `GET /v1/sandboxes?label=k=v`: every sandbox with all the labels, that the caller reaches. */
|
|
296
|
+
export async function listSandboxes(ctx, labels, scope) {
|
|
297
|
+
const grants = sandboxGrantsOf(scope);
|
|
298
|
+
return ctx.store.tx(async (t) => {
|
|
299
|
+
const found = await t.listSandboxResources({ labels });
|
|
300
|
+
const views = [];
|
|
301
|
+
for (const sandbox of found)
|
|
302
|
+
if (live(sandbox) && (grants === undefined || sandboxGranted(grants, sandbox.id)))
|
|
303
|
+
views.push(await viewOf(ctx, t, sandbox, ownerOf(scope)));
|
|
304
|
+
return { sandboxes: views };
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** Refused while a session attached to sandbox `id` has a turn running. */
|
|
308
|
+
async function refuseBusy(t, id) {
|
|
309
|
+
const busy = (await t.sessionsOnSandbox(id)).some((session) => session.activeTurnId !== null);
|
|
310
|
+
if (busy)
|
|
311
|
+
fail(409, `Sandbox ${id} has a turn running. Cancel it, or wait for it to end.`, {
|
|
312
|
+
code: "sandbox_busy",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* `DELETE /v1/sandboxes/{id}`: deletes the sandbox and its workspace. Refused while a session
|
|
317
|
+
* attached to it has an active turn. Sessions stay attached by id: their next turn is refused
|
|
318
|
+
* (`sandbox_unavailable`) unless a sandbox with this id exists again. A pod sandbox's row stays
|
|
319
|
+
* (`desired: deleted`) until its Sandbox is gone.
|
|
320
|
+
*/
|
|
321
|
+
export async function deleteSandbox(ctx, id, scope) {
|
|
322
|
+
reachable(sandboxGrantsOf(scope), id);
|
|
323
|
+
const deleted = await ctx.store.tx(async (t) => {
|
|
324
|
+
const sandbox = await liveSandbox(t, id, { lock: true });
|
|
325
|
+
if (!sandbox)
|
|
326
|
+
return false;
|
|
327
|
+
await refuseBusy(t, id);
|
|
328
|
+
await t.sandboxEvent(id, "sandbox.deleted", {});
|
|
329
|
+
if (sandbox.pod) {
|
|
330
|
+
const epoch = sandbox.pod.hostEpoch + 1;
|
|
331
|
+
await t.updateSandboxPod(id, { desired: "deleted", observed: "deleting", rev: sandbox.pod.rev + 1, hostEpoch: epoch }, new Date().toISOString());
|
|
332
|
+
reconcileAfter(ctx, t, id);
|
|
333
|
+
t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
await t.deleteSandboxResource(id);
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
// After the commit: a turn that starts now finds no sandbox and is refused.
|
|
340
|
+
if (deleted)
|
|
341
|
+
await ctx.sandbox.removeSandbox(id);
|
|
342
|
+
return { id, deleted };
|
|
343
|
+
}
|
|
344
|
+
/** The pod sandbox `id` under the row lock, or a 404 or 400. */
|
|
345
|
+
async function podSandbox(t, id) {
|
|
346
|
+
const sandbox = (await liveSandbox(t, id, { lock: true })) ?? notFound();
|
|
347
|
+
if (!sandbox.pod)
|
|
348
|
+
return fail(400, `Sandbox ${id} is virtual: only a pod sandbox stops and resets`);
|
|
349
|
+
return sandbox;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* `POST /v1/sandboxes/{id}/stop`: suspends a pod sandbox. Its volume is kept, and the next turn
|
|
353
|
+
* of a session attached to it (or a `PUT`) starts it again. Refused during a turn.
|
|
354
|
+
*/
|
|
355
|
+
export async function stopSandbox(ctx, id, scope) {
|
|
356
|
+
reachable(sandboxGrantsOf(scope), id);
|
|
357
|
+
return ctx.store.tx(async (t) => {
|
|
358
|
+
const sandbox = await podSandbox(t, id);
|
|
359
|
+
await refuseBusy(t, id);
|
|
360
|
+
const pod = sandbox.pod;
|
|
361
|
+
if (pod.desired === "running" && pod.observed !== "lost" && pod.observed !== "expired") {
|
|
362
|
+
const now = new Date().toISOString();
|
|
363
|
+
const epoch = pod.hostEpoch + 1;
|
|
364
|
+
await t.updateSandboxPod(id, { desired: "suspended", rev: pod.rev + 1, hostEpoch: epoch }, now);
|
|
365
|
+
await t.sandboxEvent(id, "sandbox.suspended", { reason: "stop" });
|
|
366
|
+
reconcileAfter(ctx, t, id);
|
|
367
|
+
t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
|
|
368
|
+
}
|
|
369
|
+
return viewOf(ctx, t, (await t.sandboxResource(id)) ?? sandbox, ownerOf(scope));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* `POST /v1/sandboxes/{id}/reset`: a new pod on a new, empty volume (the next volume
|
|
374
|
+
* generation), with a new join token; the old Sandbox and volume are deleted. Leaves `lost` and
|
|
375
|
+
* `failed`; an expired sandbox stays expired. Refused during a turn, and while an earlier
|
|
376
|
+
* reset's Sandbox is still being deleted.
|
|
377
|
+
*/
|
|
378
|
+
export async function resetSandbox(ctx, id, scope) {
|
|
379
|
+
reachable(sandboxGrantsOf(scope), id);
|
|
380
|
+
await requirePods(ctx.pods, "Resetting a pod sandbox");
|
|
381
|
+
return ctx.store.tx(async (t) => {
|
|
382
|
+
const sandbox = await podSandbox(t, id);
|
|
383
|
+
await refuseBusy(t, id);
|
|
384
|
+
const pod = sandbox.pod;
|
|
385
|
+
if (pod.retiring !== undefined)
|
|
386
|
+
fail(409, `Sandbox ${id} is still deleting its previous volume. Try again shortly.`, {
|
|
387
|
+
code: "sandbox_busy",
|
|
388
|
+
details: { retryAfterSeconds: 5 },
|
|
389
|
+
}, { "retry-after": "5" });
|
|
390
|
+
const now = new Date().toISOString();
|
|
391
|
+
const volumeGen = pod.volumeGen + 1;
|
|
392
|
+
const epoch = pod.hostEpoch + 1;
|
|
393
|
+
const expired = pod.expiresAt !== undefined && Date.parse(pod.expiresAt) <= Date.now();
|
|
394
|
+
await t.updateSandboxPod(id, {
|
|
395
|
+
k8sName: podName(ctx.config.tenantId, id, volumeGen),
|
|
396
|
+
volumeGen,
|
|
397
|
+
retiring: pod.k8sName,
|
|
398
|
+
desired: "running",
|
|
399
|
+
observed: expired ? "expired" : "creating",
|
|
400
|
+
rev: pod.rev + 1,
|
|
401
|
+
hostEpoch: epoch,
|
|
402
|
+
joinTokenHash: null,
|
|
403
|
+
podUid: null,
|
|
404
|
+
volumeId: null,
|
|
405
|
+
startedAt: now,
|
|
406
|
+
lastActiveAt: now,
|
|
407
|
+
reason: null,
|
|
408
|
+
}, now);
|
|
409
|
+
await t.sandboxEvent(id, "sandbox.reset", { volumeGeneration: volumeGen });
|
|
410
|
+
reconcileAfter(ctx, t, id);
|
|
411
|
+
t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
|
|
412
|
+
return viewOf(ctx, t, (await t.sandboxResource(id)) ?? sandbox, ownerOf(scope));
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/** `GET /v1/sandboxes/{id}/events`: the sandbox's lifecycle stream. */
|
|
416
|
+
export async function sandboxEventsOf(ctx, id, scope, fromSeq) {
|
|
417
|
+
reachable(sandboxGrantsOf(scope), id);
|
|
418
|
+
return ctx.store.tx(async (t) => ({
|
|
419
|
+
events: await t.sandboxEvents(id, fromSeq === undefined ? {} : { fromSeq }),
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Attaches a new session to sandbox `id` (`PutSessionRequest.sandbox = { id }`): the sandbox
|
|
424
|
+
* must exist and be reachable with `grants`. Returns its spec, which the session pins. Runs in
|
|
425
|
+
* the session's transaction, after its session row lock.
|
|
426
|
+
*/
|
|
427
|
+
export async function attachSandbox(t, id, grants) {
|
|
428
|
+
reachable(grants, id);
|
|
429
|
+
return (await liveSandbox(t, id, { lock: true })) ?? notFound();
|
|
430
|
+
}
|
|
431
|
+
/** Records the attachment on the session's log and on the sandbox's stream. */
|
|
432
|
+
export async function recordAttachment(t, sessionId, sandboxId) {
|
|
433
|
+
await t.event(sessionId, null, "sandbox.attached", { sandboxId });
|
|
434
|
+
await t.sandboxEvent(sandboxId, "sandbox.attached", { sessionId });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Checks a turn start on a session attached to a sandbox, in the command's transaction after
|
|
438
|
+
* the session row lock: the sandbox still exists, a subject token's `sbx` grants reach it, and
|
|
439
|
+
* no other session attached to it has a turn running. Holds the sandbox row's lock until the
|
|
440
|
+
* transaction ends, so two turn starts on one sandbox are decided one after the other.
|
|
441
|
+
*
|
|
442
|
+
* A pod sandbox must also be usable: not lost (`sandbox_lost`) or expired (`sandbox_expired`),
|
|
443
|
+
* on a Runtime with sandbox pods (`sandbox_unavailable`). A stopped or failed pod is asked to
|
|
444
|
+
* run again, after the commit.
|
|
445
|
+
*/
|
|
446
|
+
export async function checkSandboxTurn(t, session, scope, ctx) {
|
|
447
|
+
const id = session.sandboxId;
|
|
448
|
+
if (id === undefined)
|
|
449
|
+
return;
|
|
450
|
+
const grants = sandboxGrantsOf(scope);
|
|
451
|
+
if (grants !== undefined && !sandboxGranted(grants, id))
|
|
452
|
+
fail(403, `This token does not reach sandbox ${id}.`, { code: "sandbox_not_granted" });
|
|
453
|
+
const sandbox = await liveSandbox(t, id, { lock: true });
|
|
454
|
+
if (!sandbox)
|
|
455
|
+
fail(409, `Sandbox ${id} was deleted. Create it again to continue this session.`, {
|
|
456
|
+
code: "sandbox_unavailable",
|
|
457
|
+
});
|
|
458
|
+
const holder = (await t.sessionsOnSandbox(id)).find((other) => other.id !== session.id && other.activeTurnId !== null);
|
|
459
|
+
if (holder)
|
|
460
|
+
fail(409, `Sandbox ${id} is running another session's turn. Turns are serial per sandbox.`, {
|
|
461
|
+
code: "sandbox_busy",
|
|
462
|
+
details: { retryAfterSeconds: 5 },
|
|
463
|
+
}, { "retry-after": "5" });
|
|
464
|
+
const pod = sandbox?.pod;
|
|
465
|
+
if (!pod)
|
|
466
|
+
return;
|
|
467
|
+
if (!ctx?.pods)
|
|
468
|
+
fail(409, `Sandbox ${id} is a pod sandbox, and this Runtime has no sandbox pods: run \`nylorun sandbox enable --context <name>\`.`, {
|
|
469
|
+
code: "sandbox_unavailable",
|
|
470
|
+
});
|
|
471
|
+
if (pod.observed === "lost")
|
|
472
|
+
fail(409, `Sandbox ${id} was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, {
|
|
473
|
+
code: "sandbox_lost",
|
|
474
|
+
});
|
|
475
|
+
if (pod.observed === "expired")
|
|
476
|
+
fail(409, `Sandbox ${id} expired. PUT it with a longer lifecycle.ttl to revive it.`, {
|
|
477
|
+
code: "sandbox_expired",
|
|
478
|
+
});
|
|
479
|
+
const now = new Date().toISOString();
|
|
480
|
+
if (pod.desired !== "running" || pod.observed === "failed") {
|
|
481
|
+
await wantRunning(t, sandbox, now);
|
|
482
|
+
reconcileAfter(ctx, t, id);
|
|
483
|
+
}
|
|
484
|
+
else
|
|
485
|
+
await t.updateSandboxPod(id, { lastActiveAt: now }, now);
|
|
486
|
+
}
|
|
487
|
+
/** A sessions reset detaches every session from its sandbox: each sandbox's stream says so. */
|
|
488
|
+
export async function detachAllSessions(t) {
|
|
489
|
+
for (const sandbox of await t.listSandboxResources()) {
|
|
490
|
+
const sessions = await t.sessionsOnSandbox(sandbox.id);
|
|
491
|
+
if (sessions.length === 0)
|
|
492
|
+
continue;
|
|
493
|
+
await t.sandboxResource(sandbox.id, { lock: true });
|
|
494
|
+
for (const session of sessions)
|
|
495
|
+
await t.sandboxEvent(sandbox.id, "sandbox.detached", {
|
|
496
|
+
sessionId: session.id,
|
|
497
|
+
reason: "reset",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Marks the end of a turn's segment on pod sandbox `id`: idle counts from now, and the idle
|
|
503
|
+
* timer is armed (D34). Best effort.
|
|
504
|
+
*/
|
|
505
|
+
export async function touchPodSandbox(ctx, id) {
|
|
506
|
+
try {
|
|
507
|
+
const at = await ctx.store.tx(async (t) => {
|
|
508
|
+
const sandbox = await t.sandboxResource(id, { lock: true });
|
|
509
|
+
if (!sandbox?.pod || sandbox.pod.desired !== "running")
|
|
510
|
+
return undefined;
|
|
511
|
+
const now = new Date();
|
|
512
|
+
await t.updateSandboxPod(id, { lastActiveAt: now.toISOString() }, now.toISOString());
|
|
513
|
+
return now.getTime() + podLifecycleConfig(await readSandboxConfig(t)).idleMs;
|
|
514
|
+
});
|
|
515
|
+
if (at !== undefined)
|
|
516
|
+
await ctx.sandboxSignal(id, { kind: "arm", timer: "idle", at });
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
ctx.config.logger.warn("sandbox idle timer not armed", {
|
|
520
|
+
sandboxId: id,
|
|
521
|
+
message: error instanceof Error ? error.message : String(error),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Re-arms the idle timer of every running pod sandbox after the Tenant's settings change, so a
|
|
527
|
+
* shorter `limits.idle` applies to pods already idle (D36: the settings are read whenever a
|
|
528
|
+
* timer is set). Best effort.
|
|
529
|
+
*/
|
|
530
|
+
export async function rearmPodTimers(ctx) {
|
|
531
|
+
if (!ctx.pods)
|
|
532
|
+
return;
|
|
533
|
+
try {
|
|
534
|
+
const { rows, idleMs } = await ctx.store.tx(async (t) => ({
|
|
535
|
+
rows: await t.listSandboxResources(),
|
|
536
|
+
idleMs: podLifecycleConfig(await readSandboxConfig(t)).idleMs,
|
|
537
|
+
}));
|
|
538
|
+
for (const row of rows)
|
|
539
|
+
if (row.pod?.desired === "running")
|
|
540
|
+
await ctx.sandboxSignal(row.id, {
|
|
541
|
+
kind: "arm",
|
|
542
|
+
timer: "idle",
|
|
543
|
+
at: Date.parse(row.pod.lastActiveAt ?? row.createdAt) + idleMs,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
ctx.config.logger.warn("sandbox idle timers not re-armed", {
|
|
548
|
+
message: error instanceof Error ? error.message : String(error),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
package/dist/tenant/session.d.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers over the session document: turn-manifest pins and variants, state rebasing,
|
|
3
|
-
* the tools of an MCP snapshot, and the event target of an Action. No store access and no I/O.
|
|
4
|
-
*
|
|
5
|
-
* Later waves: stable; the async store conversion (Wave 1 / A) does not change these.
|
|
6
|
-
*/
|
|
7
|
-
import { type DurableSessionTool, type HostEffect } from "@nylorun/harness/run";
|
|
8
1
|
import type { Action } from "@nylorun/core/contracts";
|
|
9
|
-
import {
|
|
2
|
+
import type { AgentManifest, JsonValue } from "@nylorun/core/define";
|
|
10
3
|
import { type TurnManifestStore } from "../core/turn-manifest.js";
|
|
11
|
-
import type { McpSnapshot, McpToolRecord } from "../mcp/snapshot.js";
|
|
12
4
|
import type { Session } from "./context.js";
|
|
13
|
-
export
|
|
5
|
+
export { manifestFor, sessionToolsOf } from "../mcp/snapshot.js";
|
|
14
6
|
/** Turn-manifest store backed by the session's `variants` map (no schema change). */
|
|
15
7
|
export declare function variantStore(session: Session): TurnManifestStore;
|
|
16
8
|
/** Manifest this turn's checkpoint is pinned to (session pin or a stored variant). */
|
|
@@ -18,9 +10,6 @@ export declare function turnManifestOf(session: Session): AgentManifest;
|
|
|
18
10
|
/** Wrap a linked agent turn so the Loop learns the turn's manifest (SD-I4 pin stays on session). */
|
|
19
11
|
export declare function linkedAgentOutput(session: Session, output: JsonValue | undefined): JsonValue;
|
|
20
12
|
export declare function rebaseSessionState(session: Session, turnHash: string): void;
|
|
21
|
-
export declare function mcpToolOf(session: Session, request: Pick<HostEffect, "agent" | "capabilityId" | "toolName">): McpToolRecord | undefined;
|
|
22
|
-
/** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
|
|
23
|
-
export declare function manifestFor(manifest: AgentManifest, agent: HostEffect["agent"]): AgentManifest | undefined;
|
|
24
13
|
export declare function pinnedTool(manifest: AgentManifest, capabilityId?: string, toolName?: string): import("@nylorun/core/define").ToolManifest | undefined;
|
|
25
14
|
/** What an action runs, for events: a tool name, or a hook point and its capabilities. */
|
|
26
15
|
export declare function actionTarget(action: Action): {
|
package/dist/tenant/session.js
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure helpers over the session document: turn-manifest pins and variants, state rebasing,
|
|
3
|
-
*
|
|
3
|
+
* and the event target of an Action. No store access and no I/O. (The tools of an MCP snapshot
|
|
4
|
+
* live in `mcp/snapshot.ts`.)
|
|
4
5
|
*
|
|
5
6
|
* Later waves: stable; the async store conversion (Wave 1 / A) does not change these.
|
|
6
7
|
*/
|
|
7
|
-
import { agentTurnValue
|
|
8
|
-
import { delegateManifest, } from "@nylorun/core/define";
|
|
8
|
+
import { agentTurnValue } from "@nylorun/harness/run";
|
|
9
9
|
import { isWorkflowManifest } from "../core/flow-host.js";
|
|
10
10
|
import { allowedManifestHashes, rebaseTurnState, } from "../core/turn-manifest.js";
|
|
11
|
-
export
|
|
12
|
-
if (!snapshot?.mcpTools.length)
|
|
13
|
-
return undefined;
|
|
14
|
-
return snapshot.mcpTools.map((tool) => ({
|
|
15
|
-
...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
|
|
16
|
-
capabilityId: tool.capabilityId,
|
|
17
|
-
name: tool.name,
|
|
18
|
-
...(tool.description === undefined
|
|
19
|
-
? {}
|
|
20
|
-
: { description: tool.description }),
|
|
21
|
-
inputSchema: tool.inputSchema,
|
|
22
|
-
...(tool.outputSchema === undefined
|
|
23
|
-
? {}
|
|
24
|
-
: { outputSchema: tool.outputSchema }),
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
11
|
+
export { manifestFor, sessionToolsOf } from "../mcp/snapshot.js";
|
|
27
12
|
/** Turn-manifest store backed by the session's `variants` map (no schema change). */
|
|
28
13
|
export function variantStore(session) {
|
|
29
14
|
return {
|
|
@@ -61,15 +46,6 @@ export function rebaseSessionState(session, turnHash) {
|
|
|
61
46
|
if (next !== session.state)
|
|
62
47
|
session.state = next;
|
|
63
48
|
}
|
|
64
|
-
export function mcpToolOf(session, request) {
|
|
65
|
-
return session.mcpSnapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
|
|
66
|
-
tool.capabilityId === request.capabilityId &&
|
|
67
|
-
tool.name === request.toolName);
|
|
68
|
-
}
|
|
69
|
-
/** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
|
|
70
|
-
export function manifestFor(manifest, agent) {
|
|
71
|
-
return agent ? delegateManifest(manifest, agent.id) : manifest;
|
|
72
|
-
}
|
|
73
49
|
export function pinnedTool(manifest, capabilityId, toolName) {
|
|
74
50
|
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
75
51
|
return capability?.tools?.find((tool) => tool.name === toolName);
|
|
@@ -59,6 +59,11 @@ export declare function putSession(ctx: TenantContext, id: string, body: PutSess
|
|
|
59
59
|
* session): an existing one must have the same owner and agent, or it is a 404.
|
|
60
60
|
*/
|
|
61
61
|
createOnly?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The sandboxes the caller reaches (a subject token's `sbx`, `sandboxGrantsOf`); undefined
|
|
64
|
+
* reaches every one. `sandbox: { id }` must name one of them.
|
|
65
|
+
*/
|
|
66
|
+
sandboxGrants?: readonly string[];
|
|
62
67
|
}): Promise<Session>;
|
|
63
68
|
/** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
|
|
64
69
|
export declare function sessionView(t: Tx, s: Session): Promise<unknown>;
|