@nylorun/runtime 0.22.0-beta → 0.24.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 +161 -0
- package/README.md +95 -30
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +78 -84
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/adapters/streams/s2.js +1 -13
- package/dist/api/a2a/endpoint.js +5 -1
- package/dist/api/a2a/routes.d.ts +2 -0
- package/dist/api/a2a/routes.js +2 -2
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +4 -2
- package/dist/api/ag-ui/history.js +4 -1
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +11 -10
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +4953 -1204
- package/dist/api/components.js +25 -16
- package/dist/api/gate/keys.js +7 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +39 -3
- package/dist/api/http/app.js +4 -4
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +283 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +45 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +11 -6
- package/dist/api/http/routes/tenant.js +32 -10
- package/dist/api/openapi.js +31 -26
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +563 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +1 -1
- package/dist/artifacts/workspace.js +1 -1
- package/dist/core/flow-host.d.ts +3 -22
- package/dist/core/flow-host.js +11 -37
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +10 -43
- package/dist/execution/types.d.ts +11 -9
- package/dist/execution/types.js +5 -7
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +28 -1
- package/dist/gates/egress.js +115 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +6 -2
- package/dist/gates/http-tool.js +3 -5
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +52 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +134 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +10 -68
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +12 -5
- package/dist/harness/calls.js +21 -12
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +18 -5
- package/dist/harness/pod.d.ts +34 -0
- package/dist/harness/pod.js +41 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +13 -5
- package/dist/harness/setup.d.ts +27 -0
- package/dist/harness/setup.js +150 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +174 -44
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +8 -0
- package/dist/harness-api/server.js +93 -13
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +96 -0
- package/dist/harness-api/tool-journal.js +102 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +6 -3
- package/dist/harness-api/workspace.js +27 -11
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +64 -21
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +70 -0
- package/dist/host/stack-config.js +67 -1
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +5 -5
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +999 -2211
- package/dist/mcp/connect.d.ts +25 -20
- package/dist/mcp/connect.js +29 -30
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -20
- package/dist/mcp/snapshot.d.ts +5 -11
- package/dist/mcp/snapshot.js +6 -7
- package/dist/model/pi-model.js +8 -0
- package/dist/openapi.json +6955 -2526
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -20
- package/dist/sandbox/manager.d.ts +16 -8
- package/dist/sandbox/manager.js +33 -17
- package/dist/sandbox/placement.d.ts +13 -12
- package/dist/sandbox/placement.js +29 -12
- package/dist/sandbox/pods/client.d.ts +6 -1
- package/dist/sandbox/pods/lifecycle.d.ts +28 -27
- package/dist/sandbox/pods/lifecycle.js +14 -26
- package/dist/sandbox/pods/reconcile.js +21 -15
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +16 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +35 -27
- package/dist/sandbox/resolve.js +66 -41
- package/dist/sandbox/session-sandbox.js +1 -1
- package/dist/sandbox/setup.d.ts +61 -0
- package/dist/sandbox/setup.js +123 -0
- package/dist/sandbox/share.d.ts +11 -21
- package/dist/sandbox/share.js +12 -41
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +117 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1213 -779
- package/dist/store/postgres/schema.js +136 -76
- package/dist/store/postgres/store.js +283 -117
- package/dist/store/types.d.ts +206 -88
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +121 -49
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +94 -14
- package/dist/tenant/context.d.ts +67 -33
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +37 -20
- package/dist/tenant/environments.d.ts +167 -0
- package/dist/tenant/environments.js +823 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +30 -4
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +13 -13
- package/dist/tenant/sessions.js +61 -138
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +168 -63
- package/dist/tenant/transcript.d.ts +210 -20
- package/dist/tenant/transcript.js +62 -11
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +42 -0
- package/dist/traces/ids.js +85 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +317 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +64 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/service.d.ts +47 -48
- package/dist/vault/service.js +321 -382
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +14 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/tenant/sandboxes.d.ts +0 -122
- package/dist/tenant/sandboxes.js +0 -562
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isEnvironmentId, PutEnvironmentRequestSchema, PutEnvironmentSecretRequestSchema, RestoreEnvironmentRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { deleteEnvironmentVersion, environmentVersions, restoreEnvironment, snapshotEnvironment, } from "../../../artifacts/disk.js";
|
|
4
|
+
import { DeleteEnvironmentResponse, DeletedResponse, EnvironmentPage, EnvironmentRestore, EnvironmentSnapshot, EnvironmentVersions, RestoreEnvironmentRequest, EnvironmentSecretView, EnvironmentView, ListEnvironmentEventsResponse, ListEnvironmentSecretsResponse, ListEnvironmentsResponse, PutEnvironmentRequest, PutEnvironmentSecretRequest, } from "../../components.js";
|
|
5
|
+
import { fail } from "../../../tenant/http.js";
|
|
6
|
+
import { actorOf, assertReaches, deleteEnvironment, environmentEventsOf, getEnvironment, listEnvironments, putEnvironment, resetEnvironment, stopEnvironment, } from "../../../tenant/environments.js";
|
|
7
|
+
import { readJson } from "../body.js";
|
|
8
|
+
import { tenantRoute } from "../define.js";
|
|
9
|
+
import { environmentPage, pageQuery } from "./reads.js";
|
|
10
|
+
import { jsonResponse } from "../respond.js";
|
|
11
|
+
/** Reading an Environment: any caller that may use sessions, or change Environments. */
|
|
12
|
+
const READ = {
|
|
13
|
+
credentials: ["application", "subject", "token"],
|
|
14
|
+
scopes: ["sessions:own", "environments:write"],
|
|
15
|
+
};
|
|
16
|
+
/** Creating, changing or deleting one, and writing its secrets. */
|
|
17
|
+
const WRITE = {
|
|
18
|
+
credentials: ["application", "subject", "token"],
|
|
19
|
+
scopes: ["environments:write"],
|
|
20
|
+
};
|
|
21
|
+
const json = (schema, description) => ({
|
|
22
|
+
description,
|
|
23
|
+
content: { "application/json": { schema } },
|
|
24
|
+
});
|
|
25
|
+
const environmentId = z.object({
|
|
26
|
+
environmentId: z
|
|
27
|
+
.string()
|
|
28
|
+
.meta({ description: "The Environment id, percent-encoded: `users%2Falice%2Fweb` for `users/alice/web`" }),
|
|
29
|
+
});
|
|
30
|
+
const secretPath = environmentId.extend({
|
|
31
|
+
name: z.string().meta({
|
|
32
|
+
description: "The secret's name: the variable for `environment_secret` and `environment_variable`, a label for `bearer` and `headers`",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
/** The path's Environment id, or a 400 naming the rule. */
|
|
36
|
+
function idOf(value) {
|
|
37
|
+
if (!isEnvironmentId(value))
|
|
38
|
+
return fail(400, "An Environment id is up to 200 characters: /-separated segments of letters, digits, '.', '_' and '-'");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/** `label=k=v`, repeated: every one must match. */
|
|
42
|
+
function labelsOf(values) {
|
|
43
|
+
const labels = {};
|
|
44
|
+
for (const value of values ?? []) {
|
|
45
|
+
const at = value.indexOf("=");
|
|
46
|
+
if (at <= 0)
|
|
47
|
+
fail(400, `label must be key=value, not ${value}`);
|
|
48
|
+
labels[value.slice(0, at)] = value.slice(at + 1);
|
|
49
|
+
}
|
|
50
|
+
return labels;
|
|
51
|
+
}
|
|
52
|
+
export function environmentRoutes(api) {
|
|
53
|
+
tenantRoute(api, READ, {
|
|
54
|
+
method: "get",
|
|
55
|
+
path: "/v1/environments",
|
|
56
|
+
tags: ["Environments"],
|
|
57
|
+
summary: "List Environments",
|
|
58
|
+
description: "Every Environment with all the labels asked for. A token caller sees only the Environments its grants reach.",
|
|
59
|
+
request: {
|
|
60
|
+
query: pageQuery.extend({
|
|
61
|
+
label: z
|
|
62
|
+
.union([z.string(), z.array(z.string())])
|
|
63
|
+
.optional()
|
|
64
|
+
.meta({ description: "`key=value`; repeat it to require several labels" }),
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
responses: {
|
|
68
|
+
200: json(z.union([ListEnvironmentsResponse, EnvironmentPage]), "The Environments; limit opts into pagination"),
|
|
69
|
+
},
|
|
70
|
+
}, async (c) => jsonResponse(200, c.req.query("limit") !== undefined
|
|
71
|
+
? await environmentPage(c.env.tenant, c.get("scope"), c.req.query(), labelsOf(c.req.queries("label")))
|
|
72
|
+
: await listEnvironments(c.env.tenant, labelsOf(c.req.queries("label")), c.get("scope"))));
|
|
73
|
+
tenantRoute(api, WRITE, {
|
|
74
|
+
method: "put",
|
|
75
|
+
path: "/v1/environments/{environmentId}",
|
|
76
|
+
tags: ["Environments"],
|
|
77
|
+
summary: "Create, find or change an Environment",
|
|
78
|
+
description: "Creates the Environment and its vault within the Tenant's limits, or answers the one with this id, so get-or-create is one call. On an existing one the fields sent are compared: `kind` and `storage` are fixed (`409`), labels replace, `ownerUserId` sets or (with `null`) clears the owner guard, and any other change writes the recipe and bumps `version`. A pod's image and resources apply when it is next started from stopped (`appliedVersion`); `network.allowedHosts`, `harness` and `lifecycle` apply at once. Kind `pod` needs sandbox pods (`nylorun sandbox enable`) and is created at once.",
|
|
79
|
+
request: {
|
|
80
|
+
params: environmentId,
|
|
81
|
+
body: { required: true, content: { "application/json": { schema: PutEnvironmentRequest } } },
|
|
82
|
+
},
|
|
83
|
+
responses: {
|
|
84
|
+
200: json(EnvironmentView, "The Environment"),
|
|
85
|
+
409: {
|
|
86
|
+
description: "An Environment with this id has another kind or storage, the Tenant holds as many Environments as it allows (`limit_exceeded`), kind pod without sandbox pods (`environment_unavailable`), or it is being deleted (`environment_busy`)",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}, async (c) => {
|
|
90
|
+
const id = idOf(c.req.param("environmentId"));
|
|
91
|
+
const body = PutEnvironmentRequestSchema.parse(await readJson(c.req.raw));
|
|
92
|
+
return jsonResponse(200, await putEnvironment(c.env.tenant, id, body, c.get("scope")));
|
|
93
|
+
});
|
|
94
|
+
tenantRoute(api, READ, {
|
|
95
|
+
method: "get",
|
|
96
|
+
path: "/v1/environments/{environmentId}",
|
|
97
|
+
tags: ["Environments"],
|
|
98
|
+
summary: "Get an Environment",
|
|
99
|
+
description: "Its recipe, versions, labels, owner guard, state and the sessions that name it (acting for a person, only theirs).",
|
|
100
|
+
request: { params: environmentId },
|
|
101
|
+
responses: { 200: json(EnvironmentView, "The Environment") },
|
|
102
|
+
}, async (c) => jsonResponse(200, await getEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
103
|
+
tenantRoute(api, READ, {
|
|
104
|
+
method: "get",
|
|
105
|
+
path: "/v1/environments/{environmentId}/events",
|
|
106
|
+
tags: ["Environments"],
|
|
107
|
+
summary: "Read an Environment's lifecycle events",
|
|
108
|
+
description: "`environment.created`, `.attached`, `.detached`, `.deleted` and a pod's `.running`, `.suspended`, `.relaunched`, `.lost`, `.reset` and `.failed`, in order, numbered from 0.",
|
|
109
|
+
request: {
|
|
110
|
+
params: environmentId,
|
|
111
|
+
query: z.object({
|
|
112
|
+
from: z.string().optional().meta({ description: "The first `seq` to return" }),
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
responses: { 200: json(ListEnvironmentEventsResponse, "The events") },
|
|
116
|
+
}, async (c) => {
|
|
117
|
+
const from = c.req.query("from");
|
|
118
|
+
if (from !== undefined && !/^\d+$/.test(from))
|
|
119
|
+
fail(400, "from must be a seq");
|
|
120
|
+
return jsonResponse(200, await environmentEventsOf(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), from === undefined ? undefined : Number(from)));
|
|
121
|
+
});
|
|
122
|
+
tenantRoute(api, WRITE, {
|
|
123
|
+
method: "delete",
|
|
124
|
+
path: "/v1/environments/{environmentId}",
|
|
125
|
+
tags: ["Environments"],
|
|
126
|
+
summary: "Delete an Environment",
|
|
127
|
+
description: "Deletes the Environment, its vault with every secret, and its files. Sessions that name it keep its id; their next turn is refused (`environment_unavailable`) until an Environment with this id exists again.",
|
|
128
|
+
request: { params: environmentId },
|
|
129
|
+
responses: {
|
|
130
|
+
200: json(DeleteEnvironmentResponse, "Deleted, or there was none"),
|
|
131
|
+
409: { description: "A session that names it has a turn running (`environment_busy`)" },
|
|
132
|
+
},
|
|
133
|
+
}, async (c) => jsonResponse(200, await deleteEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
134
|
+
for (const [action, summary, description, run] of [
|
|
135
|
+
[
|
|
136
|
+
"stop",
|
|
137
|
+
"Stop a pod Environment",
|
|
138
|
+
"Suspends the pod: its volume is kept, and the next turn of a session that names it starts it again. Refused during a turn (`environment_busy`).",
|
|
139
|
+
stopEnvironment,
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"reset",
|
|
143
|
+
"Reset a pod Environment",
|
|
144
|
+
"A new pod on a new, empty volume with the recipe applied; the old pod and volume are deleted. The way out of `environment_lost`. Refused during a turn (`environment_busy`).",
|
|
145
|
+
resetEnvironment,
|
|
146
|
+
],
|
|
147
|
+
])
|
|
148
|
+
tenantRoute(api, WRITE, {
|
|
149
|
+
method: "post",
|
|
150
|
+
path: `/v1/environments/{environmentId}/${action}`,
|
|
151
|
+
tags: ["Environments"],
|
|
152
|
+
summary,
|
|
153
|
+
description,
|
|
154
|
+
request: { params: environmentId },
|
|
155
|
+
responses: {
|
|
156
|
+
200: json(EnvironmentView, "The Environment"),
|
|
157
|
+
400: { description: "The Environment is not of kind pod" },
|
|
158
|
+
409: {
|
|
159
|
+
description: "A turn holds it (`environment_busy`), or no sandbox pods (`environment_unavailable`)",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
163
|
+
tenantRoute(api, WRITE, {
|
|
164
|
+
method: "post",
|
|
165
|
+
path: "/v1/environments/{environmentId}/snapshot",
|
|
166
|
+
tags: ["Environments"],
|
|
167
|
+
summary: "Snapshot an Environment's disk",
|
|
168
|
+
description: "Between turns, a new version of the Environment's disk (`/workspace`, and a pod's `/harness` but the engine's own files): files with their modes and modification times, links and empty directories, as a version of the folder artifact the Environment owns. Unchanged files are stored once; an unchanged disk makes no version (`unchanged`). A stopped pod is started first. Refused during a turn or another copy (`environment_busy`).",
|
|
169
|
+
request: { params: environmentId },
|
|
170
|
+
responses: {
|
|
171
|
+
201: json(EnvironmentSnapshot, "The version"),
|
|
172
|
+
400: { description: "The Environment is of kind none" },
|
|
173
|
+
409: { description: "A turn or another copy holds the disk (`environment_busy`), or the pod could not start (`environment_unavailable`)" },
|
|
174
|
+
413: { description: "Past the entries, bytes or Tenant total a version may hold (`limit_exceeded`)" },
|
|
175
|
+
},
|
|
176
|
+
}, async (c) => jsonResponse(201, await snapshotEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
177
|
+
tenantRoute(api, WRITE, {
|
|
178
|
+
method: "post",
|
|
179
|
+
path: "/v1/environments/{environmentId}/restore",
|
|
180
|
+
tags: ["Environments"],
|
|
181
|
+
summary: "Restore a version of an Environment's disk",
|
|
182
|
+
description: "Puts disk version `version` (or `latest`) back. A virtual Environment's workspace is rewritten at once (`done`); a pod gets a new volume seeded from the version before any turn (`pending`, with `disk.operation` in the view until it ends). The Environment keeps its id, vault and sessions. Refused during a turn or another copy (`environment_busy`).",
|
|
183
|
+
request: {
|
|
184
|
+
params: environmentId,
|
|
185
|
+
body: { required: true, content: { "application/json": { schema: RestoreEnvironmentRequest } } },
|
|
186
|
+
},
|
|
187
|
+
responses: {
|
|
188
|
+
200: json(EnvironmentRestore, "Restored, or being restored"),
|
|
189
|
+
404: { description: "No such Environment, or no such version" },
|
|
190
|
+
409: { description: "A turn or another copy holds the disk (`environment_busy`)" },
|
|
191
|
+
},
|
|
192
|
+
}, async (c) => {
|
|
193
|
+
const body = RestoreEnvironmentRequestSchema.parse(await readJson(c.req.raw));
|
|
194
|
+
return jsonResponse(200, await restoreEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), body.version));
|
|
195
|
+
});
|
|
196
|
+
tenantRoute(api, READ, {
|
|
197
|
+
method: "get",
|
|
198
|
+
path: "/v1/environments/{environmentId}/versions",
|
|
199
|
+
tags: ["Environments"],
|
|
200
|
+
summary: "List an Environment's disk versions",
|
|
201
|
+
description: "Newest first. Read a version's tree, files, diff and zip through `/v1/artifacts/{artifactId}/versions/{version}/…`.",
|
|
202
|
+
request: { params: environmentId },
|
|
203
|
+
responses: { 200: json(EnvironmentVersions, "The versions") },
|
|
204
|
+
}, async (c) => jsonResponse(200, await environmentVersions(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
205
|
+
tenantRoute(api, WRITE, {
|
|
206
|
+
method: "delete",
|
|
207
|
+
path: "/v1/environments/{environmentId}/versions/{version}",
|
|
208
|
+
tags: ["Environments"],
|
|
209
|
+
summary: "Delete an Environment's disk version",
|
|
210
|
+
description: "Deletes the version, and the stored files no other version names. Refused while a seed reads it.",
|
|
211
|
+
request: { params: environmentId.extend({ version: z.string() }) },
|
|
212
|
+
responses: {
|
|
213
|
+
200: json(z.object({ environmentId: z.string(), version: z.number(), deleted: z.boolean() }), "Deleted, or there was none"),
|
|
214
|
+
409: { description: "A new volume is being seeded from it (`environment_busy`)" },
|
|
215
|
+
},
|
|
216
|
+
}, async (c) => {
|
|
217
|
+
const version = c.req.param("version") ?? "";
|
|
218
|
+
if (!/^[1-9]\d{0,8}$/.test(version))
|
|
219
|
+
fail(400, "version must be a version number");
|
|
220
|
+
return jsonResponse(200, await deleteEnvironmentVersion(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), Number(version)));
|
|
221
|
+
});
|
|
222
|
+
tenantRoute(api, READ, {
|
|
223
|
+
method: "get",
|
|
224
|
+
path: "/v1/environments/{environmentId}/secrets",
|
|
225
|
+
tags: ["Environments"],
|
|
226
|
+
summary: "List an Environment's secrets",
|
|
227
|
+
description: "Each secret's name, type and binding (its URL, or its hosts and header). Never a value.",
|
|
228
|
+
request: { params: environmentId },
|
|
229
|
+
responses: { 200: json(ListEnvironmentSecretsResponse, "The secrets") },
|
|
230
|
+
}, async (c) => {
|
|
231
|
+
const id = idOf(c.req.param("environmentId"));
|
|
232
|
+
assertReaches(c.get("scope"), id);
|
|
233
|
+
return jsonResponse(200, { secrets: await c.env.tenant.vault.listEnvironmentSecrets(id) });
|
|
234
|
+
});
|
|
235
|
+
tenantRoute(api, READ, {
|
|
236
|
+
method: "get",
|
|
237
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
238
|
+
tags: ["Environments"],
|
|
239
|
+
summary: "Get an Environment's secret",
|
|
240
|
+
description: "Its name, type and binding. Never its value.",
|
|
241
|
+
request: { params: secretPath },
|
|
242
|
+
responses: { 200: json(EnvironmentSecretView, "The secret") },
|
|
243
|
+
}, async (c) => {
|
|
244
|
+
const id = idOf(c.req.param("environmentId"));
|
|
245
|
+
assertReaches(c.get("scope"), id);
|
|
246
|
+
return jsonResponse(200, await c.env.tenant.vault.getEnvironmentSecret(id, c.req.param("name") ?? ""));
|
|
247
|
+
});
|
|
248
|
+
tenantRoute(api, WRITE, {
|
|
249
|
+
method: "put",
|
|
250
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
251
|
+
tags: ["Environments"],
|
|
252
|
+
summary: "Write an Environment's secret",
|
|
253
|
+
description: "Creates or replaces one credential of the Environment's vault: an `environment_secret` (the variable `name` reads the sentinel `nylorun-managed` in the sandbox, and egress-gate sets the value as a header on requests to `allowedHosts`), an `environment_variable` (a plain variable), or a `bearer` token or `headers` map for the MCP servers and HTTP tools at `url`. The same name and type re-seals the value and binding; another type replaces it. A rotated value is used from the next request. A second credential for a host or a URL is `409 credential_conflict`.",
|
|
254
|
+
request: {
|
|
255
|
+
params: secretPath,
|
|
256
|
+
body: { required: true, content: { "application/json": { schema: PutEnvironmentSecretRequest } } },
|
|
257
|
+
},
|
|
258
|
+
responses: {
|
|
259
|
+
200: json(EnvironmentSecretView, "The secret, without its value"),
|
|
260
|
+
409: { description: "Another credential is bound to one of its hosts or to its URL (`credential_conflict`)" },
|
|
261
|
+
},
|
|
262
|
+
}, async (c) => {
|
|
263
|
+
const id = idOf(c.req.param("environmentId"));
|
|
264
|
+
const scope = c.get("scope");
|
|
265
|
+
assertReaches(scope, id);
|
|
266
|
+
const body = PutEnvironmentSecretRequestSchema.parse(await readJson(c.req.raw));
|
|
267
|
+
return jsonResponse(200, await c.env.tenant.keys.putEnvironmentSecret(id, c.req.param("name") ?? "", body, actorOf(scope)));
|
|
268
|
+
});
|
|
269
|
+
tenantRoute(api, WRITE, {
|
|
270
|
+
method: "delete",
|
|
271
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
272
|
+
tags: ["Environments"],
|
|
273
|
+
summary: "Delete an Environment's secret",
|
|
274
|
+
description: "The next request that would have used it gets none.",
|
|
275
|
+
request: { params: secretPath },
|
|
276
|
+
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
277
|
+
}, async (c) => {
|
|
278
|
+
const id = idOf(c.req.param("environmentId"));
|
|
279
|
+
const scope = c.get("scope");
|
|
280
|
+
assertReaches(scope, id);
|
|
281
|
+
return jsonResponse(200, await c.env.tenant.vault.deleteEnvironmentSecret(id, c.req.param("name") ?? "", actorOf(scope)));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A remote MCP server's tools before an agent names it (`POST /v1/tenant/mcp/preview`, R2b C12,
|
|
3
3
|
* the Management API): a management key (or Studio's key, acting as itself), as the vault routes
|
|
4
|
-
* take. The keys service runs it (`Keys.previewMcp`), since it sends an
|
|
4
|
+
* take. The keys service runs it (`Keys.previewMcp`), since it sends an Environment's
|
|
5
5
|
* credential; this process never reads the plaintext.
|
|
6
6
|
*/
|
|
7
7
|
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
@@ -16,13 +16,13 @@ export function mcpRoutes(api) {
|
|
|
16
16
|
path: "/v1/tenant/mcp/preview",
|
|
17
17
|
tags: ["MCP servers"],
|
|
18
18
|
summary: "Preview an MCP server's tools",
|
|
19
|
-
description: "Connects to the remote MCP server at `url` with the
|
|
19
|
+
description: "Connects to the remote MCP server at `url` with the credential for that URL in the vault of `environmentId`, if given (its headers and `via`; never an identity header), under the Host's address policy, lists its tools within 15 s and closes the connection. It never calls a tool. Each tool comes with the name the model would call it (`modelName`, made with `name`), its annotations and its input schema's size. A server that answers `401` is `authRequired`, with its RFC 9728 protected-resource metadata when it publishes any: it needs a person's sign-in, so reach it through a gateway (`via`), or with a key. No credential value is in the answer.",
|
|
20
20
|
request: {
|
|
21
21
|
body: { required: true, content: { "application/json": { schema: McpPreviewRequest } } },
|
|
22
22
|
},
|
|
23
23
|
responses: {
|
|
24
24
|
200: json(McpPreview, "The server's tools, or that it needs a credential (`authRequired`)"),
|
|
25
|
-
409: json(Rejected, "
|
|
25
|
+
409: json(Rejected, "The Environment's credential for the URL cannot be used"),
|
|
26
26
|
502: json(Rejected, "The server could not be listed (`mcp_preview_failed`): `details.failure` is the code a tool call would have failed with, such as `mcp.unreachable` for an address the Host refuses"),
|
|
27
27
|
},
|
|
28
28
|
}, async (c) => {
|
|
@@ -19,7 +19,7 @@ export function describeCaller(scope) {
|
|
|
19
19
|
subject: scope.subject,
|
|
20
20
|
scopes: ordered(scope.scopes),
|
|
21
21
|
agents: scope.agents === "*" ? "*" : [...scope.agents].sort(),
|
|
22
|
-
|
|
22
|
+
environments: [...(scope.environments ?? [])],
|
|
23
23
|
via: `issuer:${scope.issuer}`,
|
|
24
24
|
};
|
|
25
25
|
case "management":
|
|
@@ -21,7 +21,7 @@ export declare const sessionPageQuery: z.ZodObject<{
|
|
|
21
21
|
uncertain: "uncertain";
|
|
22
22
|
waiting: "waiting";
|
|
23
23
|
}>>;
|
|
24
|
-
|
|
24
|
+
environmentId: z.ZodOptional<z.ZodString>;
|
|
25
25
|
ownerUserId: z.ZodOptional<z.ZodString>;
|
|
26
26
|
}, z.core.$strict>;
|
|
27
27
|
export declare function parseQuery<T>(schema: z.ZodType<T>, query: unknown): T;
|
|
@@ -33,27 +33,63 @@ export declare function sessionPage(ctx: TenantEnv["Bindings"]["tenant"], scope:
|
|
|
33
33
|
status: "cancelled" | "completed" | "failed" | "idle" | "paused" | "runnable" | "running" | "uncertain" | "waiting";
|
|
34
34
|
activeTurnId: string | null;
|
|
35
35
|
lastTurnId: string | null;
|
|
36
|
-
|
|
36
|
+
environmentId: string | null;
|
|
37
37
|
createdAt: string | null;
|
|
38
38
|
lastEventAt: string | null;
|
|
39
39
|
}[];
|
|
40
40
|
nextCursor: string | null;
|
|
41
41
|
}>;
|
|
42
|
-
export declare function
|
|
43
|
-
|
|
42
|
+
export declare function environmentPage(ctx: TenantEnv["Bindings"]["tenant"], scope: TenantEnv["Variables"]["scope"], query: Record<string, string>, labels: Record<string, string>): Promise<{
|
|
43
|
+
environments: {
|
|
44
44
|
id: string;
|
|
45
|
-
kind: "pod" | "virtual";
|
|
45
|
+
kind: "none" | "pod" | "virtual";
|
|
46
46
|
labels: Record<string, string>;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
recipe: {
|
|
48
|
+
image?: string | undefined;
|
|
49
|
+
resources?: {
|
|
50
|
+
cpus: number;
|
|
51
|
+
memory: string;
|
|
52
|
+
} | undefined;
|
|
53
|
+
storage?: string | undefined;
|
|
54
|
+
network?: {
|
|
55
|
+
allowedHosts: string[];
|
|
56
|
+
} | undefined;
|
|
57
|
+
harness: string;
|
|
58
|
+
lifecycle: {
|
|
59
|
+
idle?: string | undefined;
|
|
60
|
+
deleteAfter?: string | undefined;
|
|
61
|
+
};
|
|
62
|
+
setup?: {
|
|
63
|
+
command: string;
|
|
64
|
+
cwd?: string | undefined;
|
|
65
|
+
timeout?: string | undefined;
|
|
66
|
+
}[] | undefined;
|
|
67
|
+
files?: {
|
|
68
|
+
artifactId: string;
|
|
69
|
+
version: number;
|
|
70
|
+
path: string;
|
|
71
|
+
}[] | undefined;
|
|
72
|
+
};
|
|
73
|
+
version: number;
|
|
74
|
+
appliedVersion: number;
|
|
75
|
+
ownerUserId: string | null;
|
|
76
|
+
state: string;
|
|
49
77
|
pod?: {
|
|
50
78
|
desired: "deleted" | "running" | "suspended";
|
|
51
|
-
observed: "creating" | "deleting" | "
|
|
79
|
+
observed: "creating" | "deleting" | "failed" | "lost" | "running" | "suspended";
|
|
52
80
|
volumeGeneration: number;
|
|
53
81
|
hostEpoch: number;
|
|
54
|
-
expiresAt?: string | undefined;
|
|
55
82
|
reason?: string | undefined;
|
|
56
83
|
} | undefined;
|
|
84
|
+
lastUsedAt: string;
|
|
85
|
+
disk?: {
|
|
86
|
+
latestVersion: number | null;
|
|
87
|
+
operation?: {
|
|
88
|
+
kind: "restore" | "seed" | "snapshot";
|
|
89
|
+
version?: number | undefined;
|
|
90
|
+
startedAt: string;
|
|
91
|
+
} | undefined;
|
|
92
|
+
} | undefined;
|
|
57
93
|
sessions: {
|
|
58
94
|
id: string;
|
|
59
95
|
activeTurnId: string | null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SESSION_STATUSES } from "@nylorun/core/contracts";
|
|
3
|
-
import { SessionManifestView, SessionUsageTotals, ModelCallsPage, ModelCallExportPage, } from "../../components.js";
|
|
3
|
+
import { SessionManifestView, SessionUsageTotals, ModelCallsPage, ModelCallExportPage, ToolCallsPage, TurnTrace, } from "../../components.js";
|
|
4
|
+
import { turnTrace } from "../../../traces/read.js";
|
|
4
5
|
import { readAccess, readStoreOf } from "../../../reads/access.js";
|
|
5
6
|
import { requireApplication, requirePrincipal } from "../../../tenant/auth.js";
|
|
6
7
|
import { fail } from "../../../tenant/http.js";
|
|
7
|
-
import {
|
|
8
|
+
import { environmentGrantsOf } from "../../../tenant/environments.js";
|
|
8
9
|
import { tenantRoute } from "../define.js";
|
|
9
10
|
import { jsonResponse } from "../respond.js";
|
|
10
11
|
export const pageQuery = z.object({
|
|
@@ -15,7 +16,7 @@ export const sessionPageQuery = pageQuery
|
|
|
15
16
|
.extend({
|
|
16
17
|
agentId: z.string().optional(),
|
|
17
18
|
status: z.enum(SESSION_STATUSES).optional(),
|
|
18
|
-
|
|
19
|
+
environmentId: z.string().optional(),
|
|
19
20
|
ownerUserId: z.string().optional(),
|
|
20
21
|
})
|
|
21
22
|
.strict();
|
|
@@ -31,11 +32,11 @@ export async function sessionPage(ctx, scope, query) {
|
|
|
31
32
|
requireApplication(scope);
|
|
32
33
|
return readStoreOf(ctx).sessions(filters, { limit, cursor }, readAccess(scope));
|
|
33
34
|
}
|
|
34
|
-
export async function
|
|
35
|
+
export async function environmentPage(ctx, scope, query, labels) {
|
|
35
36
|
const { limit, cursor } = parseQuery(pageQuery.extend({ label: z.string().optional() }).strict(), query);
|
|
36
37
|
if (limit === undefined)
|
|
37
38
|
return fail(400, "Pagination requires limit");
|
|
38
|
-
return readStoreOf(ctx).
|
|
39
|
+
return readStoreOf(ctx).environments(labels, { limit, cursor }, readAccess(scope), environmentGrantsOf(scope));
|
|
39
40
|
}
|
|
40
41
|
export function readRoutes(api) {
|
|
41
42
|
const sessionId = z.object({ sessionId: z.string() });
|
|
@@ -101,6 +102,54 @@ export function readRoutes(api) {
|
|
|
101
102
|
return jsonResponse(200, await store.modelCalls(id, q.turnId, q, access));
|
|
102
103
|
});
|
|
103
104
|
}
|
|
105
|
+
const toolCallsQuery = turnQuery
|
|
106
|
+
.extend({
|
|
107
|
+
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
108
|
+
cursor: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
.strict();
|
|
111
|
+
// What ran, with what input, how many sends and how it ended (R5.2): the effect journal's tool
|
|
112
|
+
// rows, readable as the model calls are.
|
|
113
|
+
tenantRoute(api, { credentials: ["application"], scopes: "never" }, {
|
|
114
|
+
method: "get",
|
|
115
|
+
path: "/v1/sessions/{sessionId}/calls/tool",
|
|
116
|
+
tags: ["Sessions"],
|
|
117
|
+
summary: "Page a session's tool calls",
|
|
118
|
+
request: { params: sessionId, query: toolCallsQuery },
|
|
119
|
+
responses: {
|
|
120
|
+
200: {
|
|
121
|
+
description: "Each tool call in the order it was first sent, with its status and sends",
|
|
122
|
+
content: { "application/json": { schema: ToolCallsPage } },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}, async (c) => {
|
|
126
|
+
requirePrincipal(c.get("scope"));
|
|
127
|
+
const q = parseQuery(toolCallsQuery, c.req.query());
|
|
128
|
+
return jsonResponse(200, await readStoreOf(c.env.tenant).toolCalls(c.req.param("sessionId"), q.turnId, q, readAccess(c.get("scope"))));
|
|
129
|
+
});
|
|
130
|
+
// A turn's spans (R7.5): the same mapping the trace projector posts, from the turn's log rows,
|
|
131
|
+
// with the installation's content opt-ins. Spans carry cost: the application's, as /calls are.
|
|
132
|
+
tenantRoute(api, { credentials: ["application"], scopes: "never" }, {
|
|
133
|
+
method: "get",
|
|
134
|
+
path: "/v1/sessions/{sessionId}/turns/{turnId}/trace",
|
|
135
|
+
tags: ["Sessions"],
|
|
136
|
+
summary: "Read a turn's trace",
|
|
137
|
+
description: "The turn's spans as OTLP/HTTP JSON (`resourceSpans`), as the Runtime posts them to an OTLP collector: a root span for the turn, a span for each model, tool and egress crossing, ids derived so every read agrees. Post it to a collector's `/v1/traces` to backfill. A turn still running has no root span yet; a turn the session never had has no spans.",
|
|
138
|
+
request: { params: sessionId.extend({ turnId: z.string() }) },
|
|
139
|
+
responses: {
|
|
140
|
+
200: {
|
|
141
|
+
description: "The turn's spans",
|
|
142
|
+
content: { "application/json": { schema: TurnTrace } },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}, async (c) => {
|
|
146
|
+
requirePrincipal(c.get("scope"));
|
|
147
|
+
const ctx = c.env.tenant;
|
|
148
|
+
const id = c.req.param("sessionId");
|
|
149
|
+
const turnId = c.req.param("turnId");
|
|
150
|
+
const events = await readStoreOf(ctx).turnEvents(id, turnId, readAccess(c.get("scope")));
|
|
151
|
+
return jsonResponse(200, await turnTrace(ctx, id, turnId, events));
|
|
152
|
+
});
|
|
104
153
|
const exportQuery = z
|
|
105
154
|
.object({
|
|
106
155
|
after: z.string().optional(),
|
|
@@ -2,16 +2,17 @@ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { PutAgentRequestSchema, PutSessionRequestSchema, SessionCommandSchema, } from "@nylorun/core/contracts";
|
|
4
4
|
import { handleSessionSandboxTool, SandboxRouteError } from "../../../core/sandbox-routes.js";
|
|
5
|
+
import { traceContextOf } from "../../../traces/ids.js";
|
|
5
6
|
import { AcceptedResponse, ListAgentsResponse, ListPublicAgentsResponse, ListSessionsResponse, SessionPage, HistoryPage, SessionEvent, PutAgentRequest, PutAgentResponse, PutSessionRequest, SandboxToolOutcome, SessionCommand, SessionItemsResponse, SessionView, StreamClosedFrame, } from "../../components.js";
|
|
6
7
|
import { accessOf, requireApplication } from "../../../tenant/auth.js";
|
|
7
8
|
import { command } from "../../../tenant/commands.js";
|
|
8
9
|
import { loadSession, sessionOf } from "../../../tenant/context.js";
|
|
9
10
|
import { fail } from "../../../tenant/http.js";
|
|
10
|
-
import {
|
|
11
|
+
import { environmentGrantsOf, sandboxRouteDeps } from "../../../tenant/environments.js";
|
|
11
12
|
import { readHistory, requestCursor, streamHolderOf, streamSessionEvents } from "../../../tenant/session-streams.js";
|
|
12
13
|
import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
|
|
13
14
|
import { readJson } from "../body.js";
|
|
14
|
-
import { tenantRoute } from "../define.js";
|
|
15
|
+
import { TraceHeaders, tenantRoute } from "../define.js";
|
|
15
16
|
import { pageQuery, sessionPage, sessionPageQuery, parseQuery } from "./reads.js";
|
|
16
17
|
import { jsonResponse } from "../respond.js";
|
|
17
18
|
const OWN_SESSIONS = {
|
|
@@ -52,12 +53,16 @@ export function sessionRoutes(api) {
|
|
|
52
53
|
tags: ["Sessions"],
|
|
53
54
|
summary: "Send a command to a session",
|
|
54
55
|
description: "A message starts a turn; the others answer what the session waits on, or cancel. Idempotent on the command's `idempotencyKey`.",
|
|
55
|
-
request: { params: sessionId, body: body(SessionCommand) },
|
|
56
|
+
request: { params: sessionId, body: body(SessionCommand), headers: TraceHeaders },
|
|
56
57
|
responses: {
|
|
57
58
|
200: json(AcceptedResponse, "The command, accepted"),
|
|
58
59
|
409: { description: "A turn is running, or the session does not wait on this" },
|
|
59
60
|
},
|
|
60
|
-
}, async (c) =>
|
|
61
|
+
}, async (c) => {
|
|
62
|
+
// A `traceparent` joins the turn to the caller's trace (R7.4); a malformed one is ignored.
|
|
63
|
+
const trace = traceContextOf(c.req.raw.headers);
|
|
64
|
+
return jsonResponse(200, await command(c.env.tenant, c.req.param("sessionId"), SessionCommandSchema.parse(await readJson(c.req.raw)), c.get("scope"), trace ? { trace } : {}));
|
|
65
|
+
});
|
|
61
66
|
tenantRoute(api, {
|
|
62
67
|
credentials: ["application", "subject", "token"],
|
|
63
68
|
scopes: ["agents:read", "agents:write"],
|
|
@@ -119,8 +124,8 @@ export function sessionRoutes(api) {
|
|
|
119
124
|
// Agent code may trust `info`: only an app server sets it.
|
|
120
125
|
if (scope.kind === "token" && request.info !== undefined)
|
|
121
126
|
fail(403, "A token caller cannot set session info", { code: "scope_required" });
|
|
122
|
-
const grants =
|
|
123
|
-
const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : {
|
|
127
|
+
const grants = environmentGrantsOf(scope);
|
|
128
|
+
const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { environmentGrants: grants });
|
|
124
129
|
return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
|
|
125
130
|
});
|
|
126
131
|
tenantRoute(api, OWN_SESSIONS, {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ModelUsageQuerySchema, PutModelBudgetsRequestSchema, PutHostModelRequestSchema,
|
|
3
|
-
import { HostModelCatalog, HostModelView, ListProvidersResponse, ModelBudgets, ModelUsageTotals, PutModelBudgetsRequest, PutHostModelRequest,
|
|
2
|
+
import { ModelUsageQuerySchema, PutModelBudgetsRequestSchema, PutToolPolicyRequestSchema, PutHostModelRequestSchema, PutTenantEnvironmentsRequestSchema, ResetTenantRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { HostModelCatalog, HostModelView, ListProvidersResponse, ModelBudgets, ModelUsageTotals, PutModelBudgetsRequest, PutToolPolicyRequest, ToolPolicy, PutHostModelRequest, PutTenantEnvironmentsRequest, ResetTenantRequest, ResetTenantResponse, SeedTenantConfigRequest, SeedTenantConfigResponse, SelectHostModelRequest, TenantSandboxView, TenantStatus, } from "../../components.js";
|
|
4
4
|
import { periodStart } from "../../../gates/meter.js";
|
|
5
|
+
import { readToolPolicy, TOOL_POLICY_SETTING } from "../../../gates/policy.js";
|
|
5
6
|
import { hostModelCatalog } from "../../../model/catalog.js";
|
|
6
7
|
import { sandboxConfigErrors } from "../../../sandbox/resolve.js";
|
|
7
8
|
import { effectiveSandboxConfig, readSandboxConfig, writeSandboxConfig, } from "../../../sandbox/tenant-config.js";
|
|
@@ -15,7 +16,7 @@ import { tenantReset, streamsStatus, } from "../../../tenant/streams.js";
|
|
|
15
16
|
import { readJson } from "../body.js";
|
|
16
17
|
import { pathSegments, tenantRoute } from "../define.js";
|
|
17
18
|
import { jsonResponse } from "../respond.js";
|
|
18
|
-
import { rearmPodTimers } from "../../../tenant/
|
|
19
|
+
import { rearmPodTimers } from "../../../tenant/environments.js";
|
|
19
20
|
/** Every Management API route (`/v1/tenant/*`): a management key, acting as itself (protocol 8). */
|
|
20
21
|
const MANAGEMENT = { credentials: ["management"], scopes: "never" };
|
|
21
22
|
const json = (schema, description) => ({
|
|
@@ -105,7 +106,7 @@ export function tenantRoutes(api) {
|
|
|
105
106
|
path: "/v1/tenant/reset",
|
|
106
107
|
tags: ["Tenant"],
|
|
107
108
|
summary: "Reset the Tenant",
|
|
108
|
-
description: "Deletes its sessions, its
|
|
109
|
+
description: "Deletes its sessions, its Environments (with their vaults), or both, after draining or cancelling the turns in progress. The Tenant stays open.",
|
|
109
110
|
request: { body: body(ResetTenantRequest) },
|
|
110
111
|
responses: { 200: json(ResetTenantResponse, "Reset") },
|
|
111
112
|
}, async (c) => {
|
|
@@ -114,7 +115,7 @@ export function tenantRoutes(api) {
|
|
|
114
115
|
await drain(ctx, request.activeWork, 30_000);
|
|
115
116
|
// A sessions reset moves the Tenant to a new basin generation, so a session created
|
|
116
117
|
// again with the same id, during or after the reset, starts in an empty basin at 0.
|
|
117
|
-
const sessionsReset = request.scope !== "
|
|
118
|
+
const sessionsReset = request.scope !== "environments";
|
|
118
119
|
await resetTenant({
|
|
119
120
|
store: ctx.store,
|
|
120
121
|
sandbox: ctx.sandbox,
|
|
@@ -158,20 +159,20 @@ export function tenantRoutes(api) {
|
|
|
158
159
|
method: "get",
|
|
159
160
|
path: "/v1/tenant/sandbox",
|
|
160
161
|
tags: ["Tenant"],
|
|
161
|
-
summary: "Get the Tenant's sandbox
|
|
162
|
+
summary: "Get the Tenant's sandbox backend and Environment settings",
|
|
162
163
|
responses: { 200: json(TenantSandboxView, "The backend in use and the configuration in force") },
|
|
163
164
|
}, async (c) => jsonResponse(200, await sandboxView(c.env.tenant)));
|
|
164
165
|
tenantRoute(api, MANAGEMENT, {
|
|
165
166
|
method: "put",
|
|
166
167
|
path: "/v1/tenant/sandbox",
|
|
167
168
|
tags: ["Tenant"],
|
|
168
|
-
summary: "Set the Tenant's
|
|
169
|
-
description: "
|
|
170
|
-
request: { body: body(
|
|
169
|
+
summary: "Set the Tenant's Environment settings",
|
|
170
|
+
description: "The limits every Environment's recipe must fit, the pods' stop grace and where harnesses may run (Tenant setting `environments.config`).",
|
|
171
|
+
request: { body: body(PutTenantEnvironmentsRequest) },
|
|
171
172
|
responses: { 200: json(TenantSandboxView, "The configuration now in force") },
|
|
172
173
|
}, async (c) => {
|
|
173
174
|
const ctx = c.env.tenant;
|
|
174
|
-
const { requestId: _requestId, ...config } =
|
|
175
|
+
const { requestId: _requestId, ...config } = PutTenantEnvironmentsRequestSchema.parse(await readJson(c.req.raw));
|
|
175
176
|
const errors = sandboxConfigErrors(effectiveSandboxConfig(config));
|
|
176
177
|
if (errors.length > 0)
|
|
177
178
|
fail(400, errors.join(" "));
|
|
@@ -236,6 +237,27 @@ export function tenantRoutes(api) {
|
|
|
236
237
|
});
|
|
237
238
|
return jsonResponse(200, { budgets: rows.map(budgetView) });
|
|
238
239
|
});
|
|
240
|
+
tenantRoute(api, MANAGEMENT, {
|
|
241
|
+
method: "get",
|
|
242
|
+
path: "/v1/tenant/tool-policy",
|
|
243
|
+
tags: ["Tenant"],
|
|
244
|
+
summary: "Get the Tenant's tool policy",
|
|
245
|
+
responses: { 200: json(ToolPolicy, "The rules the Tool Gate applies to every MCP and HTTP tool call") },
|
|
246
|
+
}, async (c) => jsonResponse(200, await c.env.tenant.store.tx((t) => readToolPolicy(t))));
|
|
247
|
+
tenantRoute(api, MANAGEMENT, {
|
|
248
|
+
method: "put",
|
|
249
|
+
path: "/v1/tenant/tool-policy",
|
|
250
|
+
tags: ["Tenant"],
|
|
251
|
+
summary: "Set the Tenant's tool policy",
|
|
252
|
+
description: "Replaces every rule. The Tool Gate applies them to each MCP and HTTP tool call of every harness, before it sends the call: the strictest rule that matches decides. `deny` refuses the call (the model gets `tool.denied`), `ask` holds it and pauses the turn until a person answers it with an `approve` command on the session, `allow` runs it. A call no rule matches runs. A rule matches when every selector it sets matches: `tool` (the name the model calls it by; `*` matches any run of characters), `server` (an MCP server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it) and `when` (fields of the call's input by dotted path, each tested with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). Sandbox tools never cross the gate. An empty list allows every call.",
|
|
253
|
+
request: { body: body(PutToolPolicyRequest) },
|
|
254
|
+
responses: { 200: json(ToolPolicy, "The policy now in force") },
|
|
255
|
+
}, async (c) => {
|
|
256
|
+
const { rules } = PutToolPolicyRequestSchema.parse(await readJson(c.req.raw));
|
|
257
|
+
const policy = { rules, updatedAt: new Date().toISOString() };
|
|
258
|
+
await c.env.tenant.store.tx((t) => t.putSetting(TOOL_POLICY_SETTING, JSON.stringify(policy)));
|
|
259
|
+
return jsonResponse(200, policy);
|
|
260
|
+
});
|
|
239
261
|
tenantRoute(api, MANAGEMENT, {
|
|
240
262
|
method: "get",
|
|
241
263
|
path: "/v1/tenant/providers",
|