@nylorun/runtime 0.23.0-beta → 0.25.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 +252 -0
- package/README.md +87 -28
- package/dist/adapters/execution/restate.js +1 -1
- 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/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +9 -3
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +6 -1
- package/dist/api/a2a/routes.js +67 -9
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +8 -3
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +39 -3
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +63 -20
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +5327 -1091
- package/dist/api/components.js +40 -16
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +14 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +52 -3
- package/dist/api/http/app.js +17 -6
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- 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/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +332 -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 +94 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +24 -9
- package/dist/api/http/routes/tenant.js +65 -15
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +48 -28
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +569 -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/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +5 -5
- package/dist/artifacts/workspace.js +7 -19
- package/dist/core/flow-host.d.ts +3 -4
- package/dist/core/flow-host.js +5 -2
- 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 +11 -44
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/execution/types.d.ts +1 -1
- 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 +22 -1
- package/dist/gates/egress.js +116 -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 +9 -6
- package/dist/gates/http-tool.js +11 -17
- 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 +51 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +7 -4
- 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 +132 -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 +11 -69
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +14 -7
- package/dist/harness/calls.js +30 -24
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +24 -5
- package/dist/harness/pod.d.ts +39 -0
- package/dist/harness/pod.js +46 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +17 -9
- package/dist/harness/setup.d.ts +29 -0
- package/dist/harness/setup.js +174 -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 +180 -50
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +35 -4
- package/dist/harness-api/server.js +130 -17
- 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 +105 -0
- package/dist/harness-api/tool-journal.js +132 -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 +12 -7
- package/dist/harness-api/workspace.js +47 -25
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +62 -2
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +13 -6
- package/dist/keys/keys.js +9 -8
- package/dist/management-openapi.json +1264 -2171
- package/dist/mcp/connect.d.ts +26 -22
- package/dist/mcp/connect.js +30 -34
- package/dist/mcp/pool.d.ts +3 -6
- package/dist/mcp/pool.js +14 -28
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -21
- package/dist/mcp/snapshot.d.ts +16 -24
- package/dist/mcp/snapshot.js +33 -45
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +114 -94
- package/dist/openapi.json +12822 -3943
- 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/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 -15
- package/dist/sandbox/manager.d.ts +47 -21
- package/dist/sandbox/manager.js +162 -107
- package/dist/sandbox/placement.d.ts +14 -13
- package/dist/sandbox/placement.js +31 -14
- package/dist/sandbox/pods/client.d.ts +9 -2
- package/dist/sandbox/pods/lifecycle.d.ts +36 -27
- package/dist/sandbox/pods/lifecycle.js +29 -26
- package/dist/sandbox/pods/reconcile.js +18 -12
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +17 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +37 -27
- package/dist/sandbox/resolve.js +73 -41
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +64 -0
- package/dist/sandbox/setup.js +160 -0
- package/dist/sandbox/share.d.ts +21 -26
- package/dist/sandbox/share.js +36 -63
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- 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/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/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
- package/dist/store/postgres/reads.js +123 -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 +1126 -736
- package/dist/store/postgres/schema.js +136 -73
- package/dist/store/postgres/store.js +195 -67
- package/dist/store/types.d.ts +165 -50
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +118 -42
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +46 -26
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +115 -14
- package/dist/tenant/context.d.ts +56 -24
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +44 -29
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +174 -0
- package/dist/tenant/environments.js +1044 -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/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- 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 +7 -1
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +18 -14
- package/dist/tenant/sessions.js +99 -143
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/sweep.js +13 -7
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +245 -36
- package/dist/tenant/transcript.js +64 -12
- 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 +41 -0
- package/dist/traces/ids.js +81 -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 +289 -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 +62 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +92 -48
- package/dist/vault/service.js +462 -376
- 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 +13 -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/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
- package/dist/tenant/sandboxes.d.ts +0 -123
- package/dist/tenant/sandboxes.js +0 -566
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isEnvironmentManifestName, PutEnvironmentManifestRequestSchema } from "@nylorun/core/contracts";
|
|
3
|
+
import { DeleteEnvironmentManifestResponse, EnvironmentManifestPlan, EnvironmentManifestVersions, EnvironmentManifestView, ListEnvironmentManifestsResponse, PutEnvironmentManifestRequest, SaveEnvironmentManifestResponse, } from "../../components.js";
|
|
4
|
+
import { fail } from "../../../tenant/http.js";
|
|
5
|
+
import { deleteEnvironmentManifest, environmentManifestVersions, getEnvironmentManifest, listEnvironmentManifests, saveEnvironmentManifest, } from "../../../tenant/environment-manifests.js";
|
|
6
|
+
import { readJson } from "../body.js";
|
|
7
|
+
import { tenantRoute } from "../define.js";
|
|
8
|
+
import { jsonResponse } from "../respond.js";
|
|
9
|
+
const READ = {
|
|
10
|
+
credentials: ["application", "subject", "token"],
|
|
11
|
+
scopes: ["sessions:own", "environments:write"],
|
|
12
|
+
};
|
|
13
|
+
const WRITE = {
|
|
14
|
+
credentials: ["application", "subject", "token"],
|
|
15
|
+
scopes: ["environments:write"],
|
|
16
|
+
};
|
|
17
|
+
const json = (schema, description) => ({
|
|
18
|
+
description,
|
|
19
|
+
content: { "application/json": { schema } },
|
|
20
|
+
});
|
|
21
|
+
const namePath = z.object({
|
|
22
|
+
name: z.string().meta({ description: "The environment manifest's name, such as `analyst-box`" }),
|
|
23
|
+
});
|
|
24
|
+
function nameOf(value) {
|
|
25
|
+
if (!isEnvironmentManifestName(value))
|
|
26
|
+
return fail(400, "An environment manifest's name is 1-64 letters, digits, '.', '_' and '-', starting with a letter or digit");
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function versionOf(value) {
|
|
30
|
+
const version = Number(value);
|
|
31
|
+
if (!Number.isInteger(version) || version < 1)
|
|
32
|
+
return fail(400, "version is a positive integer");
|
|
33
|
+
return version;
|
|
34
|
+
}
|
|
35
|
+
/** `If-Match: <version>` (or `"<version>"`), when sent. */
|
|
36
|
+
function ifMatchOf(value) {
|
|
37
|
+
if (value === undefined || value.trim() === "")
|
|
38
|
+
return undefined;
|
|
39
|
+
const version = Number(value.trim().replace(/^W\//, "").replace(/^"|"$/g, ""));
|
|
40
|
+
if (!Number.isInteger(version) || version < 0)
|
|
41
|
+
return fail(400, "If-Match is the version you read: a whole number, 0 for none yet");
|
|
42
|
+
return version;
|
|
43
|
+
}
|
|
44
|
+
export function environmentManifestRoutes(api) {
|
|
45
|
+
tenantRoute(api, READ, {
|
|
46
|
+
method: "get",
|
|
47
|
+
path: "/v1/environment-manifests",
|
|
48
|
+
tags: ["Environment manifests"],
|
|
49
|
+
summary: "List environment manifests",
|
|
50
|
+
description: "Each manifest's latest version, by name, with how many Environments were made from it.",
|
|
51
|
+
responses: { 200: json(ListEnvironmentManifestsResponse, "The manifests") },
|
|
52
|
+
}, async (c) => jsonResponse(200, await listEnvironmentManifests(c.env.tenant)));
|
|
53
|
+
tenantRoute(api, WRITE, {
|
|
54
|
+
method: "put",
|
|
55
|
+
path: "/v1/environment-manifests/{name}",
|
|
56
|
+
tags: ["Environment manifests"],
|
|
57
|
+
summary: "Save an environment manifest",
|
|
58
|
+
description: "Saves the manifest as its next version, or answers the latest unchanged when nothing differs. No Environment changes: an Environment keeps the version it was made from, and a fork moves it to another (D63). `If-Match: <version>` refuses the save with `412 manifest_changed` when the latest version is no longer the one you read (`0` for a manifest that does not exist yet). With `?dryRun=true` the answer is the plan: the changes, every reason a save would be refused (`errors`), warnings, and how many Environments stay on older versions; nothing is saved. Clients drop a file's `$schema` before they send it.",
|
|
59
|
+
request: {
|
|
60
|
+
params: namePath,
|
|
61
|
+
query: z.object({
|
|
62
|
+
dryRun: z.enum(["true", "false"]).optional().meta({ description: "`true`: answer the plan and save nothing" }),
|
|
63
|
+
}),
|
|
64
|
+
body: { required: true, content: { "application/json": { schema: PutEnvironmentManifestRequest } } },
|
|
65
|
+
},
|
|
66
|
+
responses: {
|
|
67
|
+
200: json(z.union([SaveEnvironmentManifestResponse, EnvironmentManifestPlan]), "The version saved, or the plan with dryRun"),
|
|
68
|
+
400: { description: "The manifest breaks the Tenant's limits or names files the Runtime cannot use" },
|
|
69
|
+
412: { description: "If-Match named a version that is no longer the latest (`manifest_changed`)" },
|
|
70
|
+
},
|
|
71
|
+
}, async (c) => {
|
|
72
|
+
const name = nameOf(c.req.param("name"));
|
|
73
|
+
const body = PutEnvironmentManifestRequestSchema.parse(await readJson(c.req.raw));
|
|
74
|
+
const ifMatch = ifMatchOf(c.req.header("if-match"));
|
|
75
|
+
const options = ifMatch === undefined ? {} : { ifMatch };
|
|
76
|
+
return jsonResponse(200, c.req.query("dryRun") === "true"
|
|
77
|
+
? await saveEnvironmentManifest(c.env.tenant, name, body, c.get("scope"), { ...options, dryRun: true })
|
|
78
|
+
: await saveEnvironmentManifest(c.env.tenant, name, body, c.get("scope"), options));
|
|
79
|
+
});
|
|
80
|
+
tenantRoute(api, READ, {
|
|
81
|
+
method: "get",
|
|
82
|
+
path: "/v1/environment-manifests/{name}",
|
|
83
|
+
tags: ["Environment manifests"],
|
|
84
|
+
summary: "Get an environment manifest",
|
|
85
|
+
description: "Its latest version.",
|
|
86
|
+
request: { params: namePath },
|
|
87
|
+
responses: { 200: json(EnvironmentManifestView, "The latest version") },
|
|
88
|
+
}, async (c) => jsonResponse(200, await getEnvironmentManifest(c.env.tenant, nameOf(c.req.param("name")))));
|
|
89
|
+
tenantRoute(api, READ, {
|
|
90
|
+
method: "get",
|
|
91
|
+
path: "/v1/environment-manifests/{name}/versions",
|
|
92
|
+
tags: ["Environment manifests"],
|
|
93
|
+
summary: "List an environment manifest's versions",
|
|
94
|
+
description: "Every version, newest first, with who saved it and how many Environments were made from it.",
|
|
95
|
+
request: { params: namePath },
|
|
96
|
+
responses: { 200: json(EnvironmentManifestVersions, "The versions") },
|
|
97
|
+
}, async (c) => jsonResponse(200, await environmentManifestVersions(c.env.tenant, nameOf(c.req.param("name")))));
|
|
98
|
+
tenantRoute(api, READ, {
|
|
99
|
+
method: "get",
|
|
100
|
+
path: "/v1/environment-manifests/{name}/versions/{version}",
|
|
101
|
+
tags: ["Environment manifests"],
|
|
102
|
+
summary: "Get one version of an environment manifest",
|
|
103
|
+
request: { params: namePath.extend({ version: z.string().meta({ description: "The version number" }) }) },
|
|
104
|
+
responses: { 200: json(EnvironmentManifestView, "The version") },
|
|
105
|
+
}, async (c) => jsonResponse(200, await getEnvironmentManifest(c.env.tenant, nameOf(c.req.param("name")), versionOf(c.req.param("version")))));
|
|
106
|
+
tenantRoute(api, WRITE, {
|
|
107
|
+
method: "delete",
|
|
108
|
+
path: "/v1/environment-manifests/{name}",
|
|
109
|
+
tags: ["Environment manifests"],
|
|
110
|
+
summary: "Delete an environment manifest",
|
|
111
|
+
description: "Deletes every version. Refused while any Environment was made from one (`409 manifest_in_use`).",
|
|
112
|
+
request: { params: namePath },
|
|
113
|
+
responses: {
|
|
114
|
+
200: json(DeleteEnvironmentManifestResponse, "Deleted, or there was none"),
|
|
115
|
+
409: { description: "An Environment was made from one of its versions (`manifest_in_use`)" },
|
|
116
|
+
},
|
|
117
|
+
}, async (c) => jsonResponse(200, await deleteEnvironmentManifest(c.env.tenant, nameOf(c.req.param("name")))));
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environments (`/v1/environments/**`, protocol 11, D55, D56): create, find or change one by id,
|
|
3
|
+
* read it, list them by label, read its lifecycle stream, stop, reset and delete it, and write
|
|
4
|
+
* its vault's secrets. Ids may hold `/`, sent percent-encoded as one path segment
|
|
5
|
+
* (`users%2Falice%2Fweb`). A token caller reaches only the ids its Environment grants match,
|
|
6
|
+
* and changes them only with `environments:write`. Secret values are write-only: no route reads
|
|
7
|
+
* one back.
|
|
8
|
+
*/
|
|
9
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
10
|
+
import type { TenantEnv } from "../app.js";
|
|
11
|
+
export declare function environmentRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ENVIRONMENT_EVENT_TYPES, ForkEnvironmentRequestSchema, isEnvironmentId, PutEnvironmentRequestSchema, RestoreEnvironmentRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { deleteEnvironmentVersion, environmentVersions, restoreEnvironment, snapshotEnvironment, } from "../../../artifacts/disk.js";
|
|
4
|
+
import { DeleteEnvironmentResponse, DeletedResponse, EnvironmentPage, EnvironmentRestore, EnvironmentSnapshot, EnvironmentVersions, ForkEnvironmentRequest, ForkEnvironmentResponse, RestoreEnvironmentRequest, EnvironmentSecretView, EnvironmentView, ListEnvironmentEventsResponse, ListEnvironmentSecretsResponse, ListEnvironmentsResponse, PutEnvironmentRequest, PutEnvironmentSecretRequest, PutEnvironmentSecretValueRequest, } from "../../components.js";
|
|
5
|
+
import { fail } from "../../../tenant/http.js";
|
|
6
|
+
import { actorOf, assertReaches, deleteEnvironment, environmentEventsOf, forkEnvironment, getEnvironment, listEnvironments, putEnvironment, resetEnvironment, secretRequestFor, 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. It is made from one version of an environment manifest (`manifest: \"name\"` takes the latest, `\"name@n\"` version n) or from the inline fields, and never changes what it runs after that (protocol 14, D63): on an existing one, labels replace, `ownerUserId` sets or (with `null`) clears the owner guard, fields that match what it runs are accepted, and anything else is `409 environment_immutable`, which names the fork that makes a new Environment instead. Kind `pod` needs sandbox pods (`nylorun local 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 runs something else (`environment_immutable`), 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 events",
|
|
108
|
+
description: "`environment.created`, `.attached`, `.detached`, `.deleted`, a pod's `.running`, `.suspended`, `.relaunched`, `.lost`, `.reset` and `.failed`, and `environment.egress` for every connection its sandbox opens through egress-gate, naming the turns that were running (D67), in order, numbered from 0, at most 1000 per call.",
|
|
109
|
+
request: {
|
|
110
|
+
params: environmentId,
|
|
111
|
+
query: z.object({
|
|
112
|
+
from: z.string().optional().meta({ description: "The first `seq` to return" }),
|
|
113
|
+
types: z.string().optional().meta({ description: "Only these event types, comma-separated (`environment.egress`)" }),
|
|
114
|
+
since: z.string().optional().meta({ description: "Only events recorded at or after this ISO time" }),
|
|
115
|
+
until: z.string().optional().meta({ description: "Only events recorded before this ISO time" }),
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
responses: { 200: json(ListEnvironmentEventsResponse, "The events") },
|
|
119
|
+
}, async (c) => {
|
|
120
|
+
const from = c.req.query("from");
|
|
121
|
+
if (from !== undefined && !/^\d+$/.test(from))
|
|
122
|
+
fail(400, "from must be a seq");
|
|
123
|
+
const types = c.req.query("types")?.split(",").map((type) => type.trim()).filter(Boolean);
|
|
124
|
+
const unknown = types?.find((type) => !ENVIRONMENT_EVENT_TYPES.includes(type));
|
|
125
|
+
if (unknown !== undefined)
|
|
126
|
+
fail(400, `types: ${unknown} is not an Environment event type`);
|
|
127
|
+
const time = (name) => {
|
|
128
|
+
const value = c.req.query(name);
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
const at = Date.parse(value);
|
|
132
|
+
if (!Number.isFinite(at))
|
|
133
|
+
fail(400, `${name} must be an ISO time`);
|
|
134
|
+
return new Date(at).toISOString();
|
|
135
|
+
};
|
|
136
|
+
const since = time("since");
|
|
137
|
+
const until = time("until");
|
|
138
|
+
return jsonResponse(200, await environmentEventsOf(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), {
|
|
139
|
+
...(from === undefined ? {} : { fromSeq: Number(from) }),
|
|
140
|
+
...(types === undefined || types.length === 0 ? {} : { types }),
|
|
141
|
+
...(since === undefined ? {} : { since }),
|
|
142
|
+
...(until === undefined ? {} : { until }),
|
|
143
|
+
}));
|
|
144
|
+
});
|
|
145
|
+
tenantRoute(api, WRITE, {
|
|
146
|
+
method: "delete",
|
|
147
|
+
path: "/v1/environments/{environmentId}",
|
|
148
|
+
tags: ["Environments"],
|
|
149
|
+
summary: "Delete an Environment",
|
|
150
|
+
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.",
|
|
151
|
+
request: { params: environmentId },
|
|
152
|
+
responses: {
|
|
153
|
+
200: json(DeleteEnvironmentResponse, "Deleted, or there was none"),
|
|
154
|
+
409: { description: "A session that names it has a turn running (`environment_busy`)" },
|
|
155
|
+
},
|
|
156
|
+
}, async (c) => jsonResponse(200, await deleteEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
157
|
+
for (const [action, summary, description, run] of [
|
|
158
|
+
[
|
|
159
|
+
"stop",
|
|
160
|
+
"Stop a pod Environment",
|
|
161
|
+
"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`).",
|
|
162
|
+
stopEnvironment,
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"reset",
|
|
166
|
+
"Reset a pod Environment",
|
|
167
|
+
"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`).",
|
|
168
|
+
resetEnvironment,
|
|
169
|
+
],
|
|
170
|
+
])
|
|
171
|
+
tenantRoute(api, WRITE, {
|
|
172
|
+
method: "post",
|
|
173
|
+
path: `/v1/environments/{environmentId}/${action}`,
|
|
174
|
+
tags: ["Environments"],
|
|
175
|
+
summary,
|
|
176
|
+
description,
|
|
177
|
+
request: { params: environmentId },
|
|
178
|
+
responses: {
|
|
179
|
+
200: json(EnvironmentView, "The Environment"),
|
|
180
|
+
400: { description: "The Environment is not of kind pod" },
|
|
181
|
+
409: {
|
|
182
|
+
description: "A turn holds it (`environment_busy`), or no sandbox pods (`environment_unavailable`)",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
186
|
+
tenantRoute(api, WRITE, {
|
|
187
|
+
method: "post",
|
|
188
|
+
path: "/v1/environments/{environmentId}/snapshot",
|
|
189
|
+
tags: ["Environments"],
|
|
190
|
+
summary: "Snapshot an Environment's disk",
|
|
191
|
+
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`).",
|
|
192
|
+
request: { params: environmentId },
|
|
193
|
+
responses: {
|
|
194
|
+
201: json(EnvironmentSnapshot, "The version"),
|
|
195
|
+
400: { description: "The Environment is of kind none" },
|
|
196
|
+
409: { description: "A turn or another copy holds the disk (`environment_busy`), or the pod could not start (`environment_unavailable`)" },
|
|
197
|
+
413: { description: "Past the entries, bytes or Tenant total a version may hold (`limit_exceeded`)" },
|
|
198
|
+
},
|
|
199
|
+
}, async (c) => jsonResponse(201, await snapshotEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
200
|
+
tenantRoute(api, WRITE, {
|
|
201
|
+
method: "post",
|
|
202
|
+
path: "/v1/environments/{environmentId}/restore",
|
|
203
|
+
tags: ["Environments"],
|
|
204
|
+
summary: "Restore a version of an Environment's disk",
|
|
205
|
+
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`).",
|
|
206
|
+
request: {
|
|
207
|
+
params: environmentId,
|
|
208
|
+
body: { required: true, content: { "application/json": { schema: RestoreEnvironmentRequest } } },
|
|
209
|
+
},
|
|
210
|
+
responses: {
|
|
211
|
+
200: json(EnvironmentRestore, "Restored, or being restored"),
|
|
212
|
+
404: { description: "No such Environment, or no such version" },
|
|
213
|
+
409: { description: "A turn or another copy holds the disk (`environment_busy`)" },
|
|
214
|
+
},
|
|
215
|
+
}, async (c) => {
|
|
216
|
+
const body = RestoreEnvironmentRequestSchema.parse(await readJson(c.req.raw));
|
|
217
|
+
return jsonResponse(200, await restoreEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), body.version));
|
|
218
|
+
});
|
|
219
|
+
tenantRoute(api, WRITE, {
|
|
220
|
+
method: "post",
|
|
221
|
+
path: "/v1/environments/{environmentId}/fork",
|
|
222
|
+
tags: ["Environments"],
|
|
223
|
+
summary: "Fork an Environment",
|
|
224
|
+
description: "How an Environment gets a new manifest version (Q15): a new Environment `id`, made from `manifest` (default the source's manifest at its latest version), its disk started from a version of this one's (`disk`: `latest`, the default, takes a snapshot first; a version number; or `empty`) and its secrets copied inside the Runtime (`secrets`: `copy`, the default, copies those the new manifest declares; `none`). On a fork onto other setup the new manifest's setup runs on the copied disk before its first turn. This Environment keeps what it runs, its disk and its sessions. `dryRun` answers the plan and makes nothing.",
|
|
225
|
+
request: {
|
|
226
|
+
params: environmentId,
|
|
227
|
+
body: { required: true, content: { "application/json": { schema: ForkEnvironmentRequest } } },
|
|
228
|
+
},
|
|
229
|
+
responses: {
|
|
230
|
+
200: json(ForkEnvironmentResponse, "The plan (`dryRun`)"),
|
|
231
|
+
201: json(ForkEnvironmentResponse, "The new Environment, and the plan it ran"),
|
|
232
|
+
400: { description: "The source was made from inline fields and no manifest is named, or the manifest does not resolve" },
|
|
233
|
+
404: { description: "No such Environment, manifest version (`manifest_not_found`) or disk version" },
|
|
234
|
+
409: { description: "The id exists, or a turn or another copy holds the source's disk (`environment_busy`)" },
|
|
235
|
+
},
|
|
236
|
+
}, async (c) => {
|
|
237
|
+
const body = ForkEnvironmentRequestSchema.parse(await readJson(c.req.raw));
|
|
238
|
+
const answer = await forkEnvironment(c.env.tenant, idOf(c.req.param("environmentId")), body, c.get("scope"));
|
|
239
|
+
return jsonResponse("dryRun" in answer ? 200 : 201, answer);
|
|
240
|
+
});
|
|
241
|
+
tenantRoute(api, READ, {
|
|
242
|
+
method: "get",
|
|
243
|
+
path: "/v1/environments/{environmentId}/versions",
|
|
244
|
+
tags: ["Environments"],
|
|
245
|
+
summary: "List an Environment's disk versions",
|
|
246
|
+
description: "Newest first. Read a version's tree, files, diff and zip through `/v1/artifacts/{artifactId}/versions/{version}/…`.",
|
|
247
|
+
request: { params: environmentId },
|
|
248
|
+
responses: { 200: json(EnvironmentVersions, "The versions") },
|
|
249
|
+
}, async (c) => jsonResponse(200, await environmentVersions(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"))));
|
|
250
|
+
tenantRoute(api, WRITE, {
|
|
251
|
+
method: "delete",
|
|
252
|
+
path: "/v1/environments/{environmentId}/versions/{version}",
|
|
253
|
+
tags: ["Environments"],
|
|
254
|
+
summary: "Delete an Environment's disk version",
|
|
255
|
+
description: "Deletes the version, and the stored files no other version names. Refused while a seed reads it.",
|
|
256
|
+
request: { params: environmentId.extend({ version: z.string() }) },
|
|
257
|
+
responses: {
|
|
258
|
+
200: json(z.object({ environmentId: z.string(), version: z.number(), deleted: z.boolean() }), "Deleted, or there was none"),
|
|
259
|
+
409: { description: "A new volume is being seeded from it (`environment_busy`)" },
|
|
260
|
+
},
|
|
261
|
+
}, async (c) => {
|
|
262
|
+
const version = c.req.param("version") ?? "";
|
|
263
|
+
if (!/^[1-9]\d{0,8}$/.test(version))
|
|
264
|
+
fail(400, "version must be a version number");
|
|
265
|
+
return jsonResponse(200, await deleteEnvironmentVersion(c.env.tenant, idOf(c.req.param("environmentId")), c.get("scope"), Number(version)));
|
|
266
|
+
});
|
|
267
|
+
tenantRoute(api, READ, {
|
|
268
|
+
method: "get",
|
|
269
|
+
path: "/v1/environments/{environmentId}/secrets",
|
|
270
|
+
tags: ["Environments"],
|
|
271
|
+
summary: "List an Environment's secrets",
|
|
272
|
+
description: "Each secret's name, type and binding (its URL, or its hosts and header). Never a value.",
|
|
273
|
+
request: { params: environmentId },
|
|
274
|
+
responses: { 200: json(ListEnvironmentSecretsResponse, "The secrets") },
|
|
275
|
+
}, async (c) => {
|
|
276
|
+
const id = idOf(c.req.param("environmentId"));
|
|
277
|
+
assertReaches(c.get("scope"), id);
|
|
278
|
+
return jsonResponse(200, { secrets: await c.env.tenant.vault.listEnvironmentSecrets(id) });
|
|
279
|
+
});
|
|
280
|
+
tenantRoute(api, READ, {
|
|
281
|
+
method: "get",
|
|
282
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
283
|
+
tags: ["Environments"],
|
|
284
|
+
summary: "Get an Environment's secret",
|
|
285
|
+
description: "Its name, type and binding. Never its value.",
|
|
286
|
+
request: { params: secretPath },
|
|
287
|
+
responses: { 200: json(EnvironmentSecretView, "The secret") },
|
|
288
|
+
}, async (c) => {
|
|
289
|
+
const id = idOf(c.req.param("environmentId"));
|
|
290
|
+
assertReaches(c.get("scope"), id);
|
|
291
|
+
return jsonResponse(200, await c.env.tenant.vault.getEnvironmentSecret(id, c.req.param("name") ?? ""));
|
|
292
|
+
});
|
|
293
|
+
tenantRoute(api, WRITE, {
|
|
294
|
+
method: "put",
|
|
295
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
296
|
+
tags: ["Environments"],
|
|
297
|
+
summary: "Write an Environment's secret",
|
|
298
|
+
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`. When what the Environment runs declares secret slots (protocol 14, D64), send only the value (`{ value }`, or `{ headers }` for a `headers` slot): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`.",
|
|
299
|
+
request: {
|
|
300
|
+
params: secretPath,
|
|
301
|
+
body: {
|
|
302
|
+
required: true,
|
|
303
|
+
content: { "application/json": { schema: z.union([PutEnvironmentSecretRequest, PutEnvironmentSecretValueRequest]) } },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
responses: {
|
|
307
|
+
200: json(EnvironmentSecretView, "The secret, without its value"),
|
|
308
|
+
409: { description: "Another credential is bound to one of its hosts or to its URL (`credential_conflict`)" },
|
|
309
|
+
},
|
|
310
|
+
}, async (c) => {
|
|
311
|
+
const id = idOf(c.req.param("environmentId"));
|
|
312
|
+
const scope = c.get("scope");
|
|
313
|
+
assertReaches(scope, id);
|
|
314
|
+
const name = c.req.param("name") ?? "";
|
|
315
|
+
const body = await secretRequestFor(c.env.tenant, id, name, await readJson(c.req.raw));
|
|
316
|
+
return jsonResponse(200, await c.env.tenant.keys.putEnvironmentSecret(id, name, body, actorOf(scope)));
|
|
317
|
+
});
|
|
318
|
+
tenantRoute(api, WRITE, {
|
|
319
|
+
method: "delete",
|
|
320
|
+
path: "/v1/environments/{environmentId}/secrets/{name}",
|
|
321
|
+
tags: ["Environments"],
|
|
322
|
+
summary: "Delete an Environment's secret",
|
|
323
|
+
description: "The next request that would have used it gets none.",
|
|
324
|
+
request: { params: secretPath },
|
|
325
|
+
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
326
|
+
}, async (c) => {
|
|
327
|
+
const id = idOf(c.req.param("environmentId"));
|
|
328
|
+
const scope = c.get("scope");
|
|
329
|
+
assertReaches(scope, id);
|
|
330
|
+
return jsonResponse(200, await c.env.tenant.vault.deleteEnvironmentSecret(id, c.req.param("name") ?? "", actorOf(scope)));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -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,26 +33,111 @@ 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
|
+
manifest: {
|
|
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
|
+
} | {
|
|
72
|
+
path: string;
|
|
73
|
+
files: Record<string, string>;
|
|
74
|
+
})[] | undefined;
|
|
75
|
+
secrets?: Record<string, {
|
|
76
|
+
required?: boolean | undefined;
|
|
77
|
+
description?: string | undefined;
|
|
78
|
+
type: "environment_secret";
|
|
79
|
+
allowedHosts: string[];
|
|
80
|
+
inject?: {
|
|
81
|
+
header: string;
|
|
82
|
+
format: string;
|
|
83
|
+
} | undefined;
|
|
84
|
+
} | {
|
|
85
|
+
required?: boolean | undefined;
|
|
86
|
+
description?: string | undefined;
|
|
87
|
+
type: "environment_variable";
|
|
88
|
+
} | {
|
|
89
|
+
via?: string | undefined;
|
|
90
|
+
identity?: {
|
|
91
|
+
header: string;
|
|
92
|
+
} | undefined;
|
|
93
|
+
required?: boolean | undefined;
|
|
94
|
+
description?: string | undefined;
|
|
95
|
+
type: "bearer";
|
|
96
|
+
url: string;
|
|
97
|
+
} | {
|
|
98
|
+
via?: string | undefined;
|
|
99
|
+
identity?: {
|
|
100
|
+
header: string;
|
|
101
|
+
} | undefined;
|
|
102
|
+
required?: boolean | undefined;
|
|
103
|
+
description?: string | undefined;
|
|
104
|
+
type: "headers";
|
|
105
|
+
url: string;
|
|
106
|
+
}> | undefined;
|
|
107
|
+
};
|
|
108
|
+
manifestName?: string | undefined;
|
|
109
|
+
manifestVersion?: number | undefined;
|
|
110
|
+
manifestHash?: string | undefined;
|
|
111
|
+
forkedFrom?: {
|
|
112
|
+
environment: string;
|
|
113
|
+
disk: number | null;
|
|
114
|
+
} | undefined;
|
|
115
|
+
slots?: {
|
|
116
|
+
name: string;
|
|
117
|
+
type: string;
|
|
118
|
+
required: boolean;
|
|
119
|
+
set: boolean;
|
|
120
|
+
}[] | undefined;
|
|
121
|
+
version: number;
|
|
122
|
+
appliedVersion: number;
|
|
123
|
+
ownerUserId: string | null;
|
|
124
|
+
state: string;
|
|
49
125
|
pod?: {
|
|
50
126
|
desired: "deleted" | "running" | "suspended";
|
|
51
|
-
observed: "creating" | "deleting" | "
|
|
127
|
+
observed: "creating" | "deleting" | "failed" | "lost" | "running" | "suspended";
|
|
52
128
|
volumeGeneration: number;
|
|
53
129
|
hostEpoch: number;
|
|
54
|
-
expiresAt?: string | undefined;
|
|
55
130
|
reason?: string | undefined;
|
|
131
|
+
imageDigest?: string | undefined;
|
|
132
|
+
} | undefined;
|
|
133
|
+
lastUsedAt: string;
|
|
134
|
+
disk?: {
|
|
135
|
+
latestVersion: number | null;
|
|
136
|
+
operation?: {
|
|
137
|
+
kind: "restore" | "seed" | "snapshot";
|
|
138
|
+
version?: number | undefined;
|
|
139
|
+
startedAt: string;
|
|
140
|
+
} | undefined;
|
|
56
141
|
} | undefined;
|
|
57
142
|
sessions: {
|
|
58
143
|
id: string;
|