@nylorun/runtime 0.15.2-beta → 0.17.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 +134 -0
- package/README.md +68 -29
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +341 -7
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +15 -13
- package/dist/api/components.d.ts +2667 -711
- package/dist/api/components.js +24 -11
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +6 -0
- package/dist/api/http/define.d.ts +19 -12
- package/dist/api/http/define.js +13 -27
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +516 -0
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +150 -0
- package/dist/api/http/routes/sessions.js +9 -13
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +35 -24
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +52 -2
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +167 -37
- package/dist/host/stack-config.d.ts +112 -9
- package/dist/host/stack-config.js +222 -20
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +14084 -9426
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +1168 -250
- package/dist/store/postgres/schema.js +174 -28
- package/dist/store/postgres/store.js +339 -103
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +163 -32
- package/dist/streams/control.d.ts +2 -5
- package/dist/streams/control.js +5 -8
- package/dist/streams/types.d.ts +8 -8
- package/dist/streams/types.js +1 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +31 -10
- package/dist/tenant/context.d.ts +83 -26
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +24 -14
- package/dist/tenant/effects.js +10 -398
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +42 -0
- package/dist/tenant/runtime.js +134 -33
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +6 -1
- package/dist/tenant/sessions.js +29 -2
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +8 -2
- package/dist/tenant/streams.js +25 -6
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/types.d.ts +39 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -180
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSandboxId, PutSandboxRequestSchema } from "@nylorun/core/contracts";
|
|
3
|
+
import { DeleteSandboxResponse, ListSandboxEventsResponse, ListSandboxesResponse, PutSandboxRequest, SandboxView, } from "../../components.js";
|
|
4
|
+
import { fail } from "../../../tenant/http.js";
|
|
5
|
+
import { deleteSandbox, getSandbox, listSandboxes, putSandbox, resetSandbox, sandboxEventsOf, stopSandbox, } from "../../../tenant/sandboxes.js";
|
|
6
|
+
import { readJson } from "../body.js";
|
|
7
|
+
import { tenantRoute } from "../define.js";
|
|
8
|
+
import { jsonResponse } from "../respond.js";
|
|
9
|
+
/** Reading a sandbox: any caller that may use sessions, or change sandboxes. */
|
|
10
|
+
const READ = {
|
|
11
|
+
credentials: ["application", "subject", "token"],
|
|
12
|
+
scopes: ["sessions:own", "sandboxes:write"],
|
|
13
|
+
};
|
|
14
|
+
/** Creating or deleting one. */
|
|
15
|
+
const WRITE = {
|
|
16
|
+
credentials: ["application", "subject", "token"],
|
|
17
|
+
scopes: ["sandboxes:write"],
|
|
18
|
+
};
|
|
19
|
+
const json = (schema, description) => ({
|
|
20
|
+
description,
|
|
21
|
+
content: { "application/json": { schema } },
|
|
22
|
+
});
|
|
23
|
+
const sandboxId = z.object({
|
|
24
|
+
sandboxId: z
|
|
25
|
+
.string()
|
|
26
|
+
.meta({ description: "The sandbox id, percent-encoded: `team-a%2Fproj-42` for `team-a/proj-42`" }),
|
|
27
|
+
});
|
|
28
|
+
/** The path's sandbox id, or a 400 naming the rule. */
|
|
29
|
+
function idOf(value) {
|
|
30
|
+
if (!isSandboxId(value))
|
|
31
|
+
return fail(400, "A sandbox id is up to 200 characters: /-separated segments of letters, digits, '.', '_' and '-'");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/** `label=k=v`, repeated: every one must match. */
|
|
35
|
+
function labelsOf(values) {
|
|
36
|
+
const labels = {};
|
|
37
|
+
for (const value of values ?? []) {
|
|
38
|
+
const at = value.indexOf("=");
|
|
39
|
+
if (at <= 0)
|
|
40
|
+
fail(400, `label must be key=value, not ${value}`);
|
|
41
|
+
labels[value.slice(0, at)] = value.slice(at + 1);
|
|
42
|
+
}
|
|
43
|
+
return labels;
|
|
44
|
+
}
|
|
45
|
+
export function sandboxRoutes(api) {
|
|
46
|
+
tenantRoute(api, READ, {
|
|
47
|
+
method: "get",
|
|
48
|
+
path: "/v1/sandboxes",
|
|
49
|
+
tags: ["Sandboxes"],
|
|
50
|
+
summary: "List sandboxes",
|
|
51
|
+
description: "Every sandbox with all the labels asked for. A token caller sees only the sandboxes its grants reach.",
|
|
52
|
+
request: {
|
|
53
|
+
query: z.object({
|
|
54
|
+
label: z
|
|
55
|
+
.union([z.string(), z.array(z.string())])
|
|
56
|
+
.optional()
|
|
57
|
+
.meta({ description: "`key=value`; repeat it to require several labels" }),
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
responses: { 200: json(ListSandboxesResponse, "The sandboxes") },
|
|
61
|
+
}, async (c) => jsonResponse(200, await listSandboxes(c.env.tenant, labelsOf(c.req.queries("label")), c.get("scope"))));
|
|
62
|
+
tenantRoute(api, WRITE, {
|
|
63
|
+
method: "put",
|
|
64
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
65
|
+
tags: ["Sandboxes"],
|
|
66
|
+
summary: "Create or find a sandbox",
|
|
67
|
+
description: "Creates the sandbox within the Tenant's limits, or answers the one with this id, so get-or-create is one call. Its spec is fixed once it exists; labels, when sent, replace its labels. Kind `pod` needs sandbox pods (`nylorun sandbox enable`): it is created at once, and a PUT on an existing pod sandbox starts it again when it was stopped, or revives it with a longer `lifecycle.ttl`.",
|
|
68
|
+
request: {
|
|
69
|
+
params: sandboxId,
|
|
70
|
+
body: { required: true, content: { "application/json": { schema: PutSandboxRequest } } },
|
|
71
|
+
},
|
|
72
|
+
responses: {
|
|
73
|
+
200: json(SandboxView, "The sandbox"),
|
|
74
|
+
409: {
|
|
75
|
+
description: "A sandbox with this id has another spec, the Tenant holds as many sandboxes as it allows (`limit_exceeded`), kind pod without sandbox pods (`sandbox_unavailable`), or a lost pod sandbox (`sandbox_lost`)",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}, async (c) => {
|
|
79
|
+
const id = idOf(c.req.param("sandboxId"));
|
|
80
|
+
const body = PutSandboxRequestSchema.parse(await readJson(c.req.raw));
|
|
81
|
+
return jsonResponse(200, await putSandbox(c.env.tenant, id, body, c.get("scope")));
|
|
82
|
+
});
|
|
83
|
+
tenantRoute(api, READ, {
|
|
84
|
+
method: "get",
|
|
85
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
86
|
+
tags: ["Sandboxes"],
|
|
87
|
+
summary: "Get a sandbox",
|
|
88
|
+
description: "Its spec, labels, state and the sessions attached to it (acting for a person, only theirs).",
|
|
89
|
+
request: { params: sandboxId },
|
|
90
|
+
responses: { 200: json(SandboxView, "The sandbox") },
|
|
91
|
+
}, async (c) => jsonResponse(200, await getSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
92
|
+
tenantRoute(api, READ, {
|
|
93
|
+
method: "get",
|
|
94
|
+
path: "/v1/sandboxes/{sandboxId}/events",
|
|
95
|
+
tags: ["Sandboxes"],
|
|
96
|
+
summary: "Read a sandbox's lifecycle events",
|
|
97
|
+
description: "`sandbox.created`, `sandbox.attached`, `sandbox.detached` and `sandbox.deleted`, in order, numbered from 0.",
|
|
98
|
+
request: {
|
|
99
|
+
params: sandboxId,
|
|
100
|
+
query: z.object({
|
|
101
|
+
from: z.string().optional().meta({ description: "The first `seq` to return" }),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
responses: { 200: json(ListSandboxEventsResponse, "The events") },
|
|
105
|
+
}, async (c) => {
|
|
106
|
+
const from = c.req.query("from");
|
|
107
|
+
if (from !== undefined && !/^\d+$/.test(from))
|
|
108
|
+
fail(400, "from must be a seq");
|
|
109
|
+
return jsonResponse(200, await sandboxEventsOf(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"), from === undefined ? undefined : Number(from)));
|
|
110
|
+
});
|
|
111
|
+
tenantRoute(api, WRITE, {
|
|
112
|
+
method: "delete",
|
|
113
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
114
|
+
tags: ["Sandboxes"],
|
|
115
|
+
summary: "Delete a sandbox",
|
|
116
|
+
description: "Deletes the sandbox and its files. Sessions attached to it stay attached by id; their next turn is refused until a sandbox with this id exists again.",
|
|
117
|
+
request: { params: sandboxId },
|
|
118
|
+
responses: {
|
|
119
|
+
200: json(DeleteSandboxResponse, "Deleted, or there was none"),
|
|
120
|
+
409: { description: "A session attached to it has a turn running (`sandbox_busy`)" },
|
|
121
|
+
},
|
|
122
|
+
}, async (c) => jsonResponse(200, await deleteSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
123
|
+
for (const [action, summary, description, run] of [
|
|
124
|
+
[
|
|
125
|
+
"stop",
|
|
126
|
+
"Stop a pod sandbox",
|
|
127
|
+
"Suspends the pod: its volume is kept, and the next turn of a session attached to it (or a PUT) starts it again. Refused during a turn (`sandbox_busy`).",
|
|
128
|
+
stopSandbox,
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
"reset",
|
|
132
|
+
"Reset a pod sandbox",
|
|
133
|
+
"A new pod on a new, empty volume; the old pod and volume are deleted. The way out of `sandbox_lost`. Refused during a turn (`sandbox_busy`).",
|
|
134
|
+
resetSandbox,
|
|
135
|
+
],
|
|
136
|
+
])
|
|
137
|
+
tenantRoute(api, WRITE, {
|
|
138
|
+
method: "post",
|
|
139
|
+
path: `/v1/sandboxes/{sandboxId}/${action}`,
|
|
140
|
+
tags: ["Sandboxes"],
|
|
141
|
+
summary,
|
|
142
|
+
description,
|
|
143
|
+
request: { params: sandboxId },
|
|
144
|
+
responses: {
|
|
145
|
+
200: json(SandboxView, "The sandbox"),
|
|
146
|
+
400: { description: "The sandbox is virtual" },
|
|
147
|
+
409: { description: "A turn holds it (`sandbox_busy`), or no sandbox pods (`sandbox_unavailable`)" },
|
|
148
|
+
},
|
|
149
|
+
}, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
150
|
+
}
|
|
@@ -7,7 +7,8 @@ import { sandboxRouteDeps } from "../../../tenant/actions.js";
|
|
|
7
7
|
import { accessOf, requireApplication } from "../../../tenant/auth.js";
|
|
8
8
|
import { loadSession, sessionOf } from "../../../tenant/context.js";
|
|
9
9
|
import { fail } from "../../../tenant/http.js";
|
|
10
|
-
import {
|
|
10
|
+
import { sandboxGrantsOf } from "../../../tenant/sandboxes.js";
|
|
11
|
+
import { readHistory, requestCursor, streamHolderOf, streamSessionEvents } from "../../../tenant/session-streams.js";
|
|
11
12
|
import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
|
|
12
13
|
import { readJson } from "../body.js";
|
|
13
14
|
import { tenantRoute } from "../define.js";
|
|
@@ -15,7 +16,6 @@ import { jsonResponse } from "../respond.js";
|
|
|
15
16
|
const OWN_SESSIONS = {
|
|
16
17
|
credentials: ["application", "subject", "token"],
|
|
17
18
|
scopes: ["sessions:own"],
|
|
18
|
-
browser: true,
|
|
19
19
|
};
|
|
20
20
|
const json = (schema, description) => ({
|
|
21
21
|
description,
|
|
@@ -46,23 +46,20 @@ async function sessionBelow(ctx, incoming, id, access) {
|
|
|
46
46
|
}
|
|
47
47
|
export function sessionRoutes(api) {
|
|
48
48
|
tenantRoute(api, {
|
|
49
|
-
credentials: ["application", "subject", "token"
|
|
49
|
+
credentials: ["application", "subject", "token"],
|
|
50
50
|
scopes: ["agents:read", "agents:write"],
|
|
51
|
-
browser: true,
|
|
52
51
|
}, {
|
|
53
52
|
method: "get",
|
|
54
53
|
path: "/v1/agents",
|
|
55
54
|
tags: ["Agents"],
|
|
56
55
|
summary: "List agents",
|
|
57
|
-
description: "With an application key, every definition, manifest included. With a
|
|
56
|
+
description: "With an application key, every definition, manifest included. With a trusted issuer's token, only the agents it may use, by name.",
|
|
58
57
|
responses: {
|
|
59
58
|
200: json(z.union([ListAgentsResponse, ListPublicAgentsResponse]), "The agents"),
|
|
60
59
|
},
|
|
61
60
|
}, async (c) => {
|
|
62
61
|
const scope = c.get("scope");
|
|
63
62
|
const ctx = c.env.tenant;
|
|
64
|
-
if (scope.kind === "publishable")
|
|
65
|
-
return jsonResponse(200, await listAgentsPublic(ctx, scope.agents));
|
|
66
63
|
return jsonResponse(200, scope.kind === "token"
|
|
67
64
|
? await listAgentsPublic(ctx, scope.agents)
|
|
68
65
|
: await listDefinitions(ctx));
|
|
@@ -106,8 +103,9 @@ export function sessionRoutes(api) {
|
|
|
106
103
|
const request = PutSessionRequestSchema.parse(await readJson(c.req.raw));
|
|
107
104
|
// Agent code may trust `info`: only an app server sets it.
|
|
108
105
|
if (scope.kind === "token" && request.info !== undefined)
|
|
109
|
-
fail(403, "A
|
|
110
|
-
const
|
|
106
|
+
fail(403, "A token caller cannot set session info", { code: "scope_required" });
|
|
107
|
+
const grants = sandboxGrantsOf(scope);
|
|
108
|
+
const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { sandboxGrants: grants });
|
|
111
109
|
return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
|
|
112
110
|
});
|
|
113
111
|
tenantRoute(api, OWN_SESSIONS, {
|
|
@@ -150,7 +148,7 @@ export function sessionRoutes(api) {
|
|
|
150
148
|
path: "/v1/sessions/{sessionId}/events",
|
|
151
149
|
tags: ["Sessions"],
|
|
152
150
|
summary: "Follow a session's events",
|
|
153
|
-
description: "Server-sent events, each with its type as `event` and its cursor as `id`, from the cursor on. Events are on the `nylorun.event/2` envelope, typed by `SessionEvent`; a client ignores a type it does not know. A
|
|
151
|
+
description: "Server-sent events, each with its type as `event` and its cursor as `id`, from the cursor on. Events are on the `nylorun.event/2` envelope, typed by `SessionEvent`; a client ignores a type it does not know. A trusted issuer's token's stream ends with `event: nylorun.closed` when the token expires. A `: keepalive` comment every 15 seconds.",
|
|
154
152
|
request: { params: sessionId, query: resume },
|
|
155
153
|
responses: {
|
|
156
154
|
200: {
|
|
@@ -168,9 +166,7 @@ export function sessionRoutes(api) {
|
|
|
168
166
|
const { incoming, outgoing } = c.env;
|
|
169
167
|
const id = c.req.param("sessionId");
|
|
170
168
|
const cursor = await sessionBelow(ctx, incoming, id, accessOf(scope));
|
|
171
|
-
await streamSessionEvents(ctx, incoming, outgoing, id, cursor, scope
|
|
172
|
-
? { subject: scope.subject, epoch: scope.epoch, expiresAt: scope.expiresAt }
|
|
173
|
-
: undefined);
|
|
169
|
+
await streamSessionEvents(ctx, incoming, outgoing, id, cursor, streamHolderOf(scope));
|
|
174
170
|
return RESPONSE_ALREADY_SENT;
|
|
175
171
|
});
|
|
176
172
|
tenantRoute(api, { credentials: ["application"], scopes: "never" }, {
|
|
@@ -10,11 +10,12 @@ import { clearObservers } from "../../../tenant/session-streams.js";
|
|
|
10
10
|
import { usesFixtureModel } from "../../../tenant/model-setting.js";
|
|
11
11
|
import { resetTenant } from "../../../tenant/reset.js";
|
|
12
12
|
import { clearWork, drain } from "../../../tenant/scheduler.js";
|
|
13
|
-
import { buildTenantStatus, seedTenantConfig } from "../../../tenant/status.js";
|
|
13
|
+
import { buildTenantStatus, harnessStatusOf, seedTenantConfig } from "../../../tenant/status.js";
|
|
14
14
|
import { tenantReset, streamsStatus, } from "../../../tenant/streams.js";
|
|
15
15
|
import { readJson } from "../body.js";
|
|
16
16
|
import { pathSegments, tenantRoute } from "../define.js";
|
|
17
17
|
import { jsonResponse } from "../respond.js";
|
|
18
|
+
import { rearmPodTimers } from "../../../tenant/sandboxes.js";
|
|
18
19
|
const settings = (scopes) => ({
|
|
19
20
|
credentials: scopes === "never" ? ["application"] : ["application", "subject"],
|
|
20
21
|
scopes,
|
|
@@ -46,7 +47,36 @@ const budgetView = (row) => ({
|
|
|
46
47
|
/** `GET /v1/tenant/sandbox`: the backend report and the configuration with defaults applied. */
|
|
47
48
|
async function sandboxView(ctx) {
|
|
48
49
|
const config = await ctx.store.tx((t) => readSandboxConfig(t));
|
|
49
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
...(await ctx.sandbox.report()),
|
|
52
|
+
config: effectiveSandboxConfig(config),
|
|
53
|
+
cluster: await clusterOf(ctx),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The Tenant's cluster for pod sandboxes (F7.2), from the sandboxes service; null without one. */
|
|
57
|
+
async function clusterOf(ctx) {
|
|
58
|
+
if (!ctx.pods)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
const [info, ready] = await Promise.all([ctx.pods.client.info(), ctx.pods.client.ready()]);
|
|
62
|
+
return {
|
|
63
|
+
namespace: info.namespace,
|
|
64
|
+
context: info.context,
|
|
65
|
+
ready,
|
|
66
|
+
...(info.controllerVersion ? { controllerVersion: info.controllerVersion } : {}),
|
|
67
|
+
...(info.networkPolicy
|
|
68
|
+
? {
|
|
69
|
+
networkPolicy: {
|
|
70
|
+
enforced: info.networkPolicy.enforced === true,
|
|
71
|
+
...(info.networkPolicy.probedAt ? { probedAt: info.networkPolicy.probedAt } : {}),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
50
80
|
}
|
|
51
81
|
export function tenantRoutes(api) {
|
|
52
82
|
tenantRoute(api, SETTINGS, {
|
|
@@ -70,6 +100,7 @@ export function tenantRoutes(api) {
|
|
|
70
100
|
(await vault.getHostModel()).configured,
|
|
71
101
|
...(ctx.stuckInvocations ? { stuckInvocations: ctx.stuckInvocations } : {}),
|
|
72
102
|
streamsStatus: () => streamsStatus(ctx),
|
|
103
|
+
harness: harnessStatusOf(ctx),
|
|
73
104
|
}));
|
|
74
105
|
});
|
|
75
106
|
tenantRoute(api, settings("never"), {
|
|
@@ -91,6 +122,7 @@ export function tenantRoutes(api) {
|
|
|
91
122
|
store: ctx.store,
|
|
92
123
|
sandbox: ctx.sandbox,
|
|
93
124
|
paths: ctx.config.paths,
|
|
125
|
+
blobs: ctx.blobs,
|
|
94
126
|
clearSessionState: () => {
|
|
95
127
|
clearWork(ctx);
|
|
96
128
|
clearObservers(ctx.sessionStreams);
|
|
@@ -147,6 +179,8 @@ export function tenantRoutes(api) {
|
|
|
147
179
|
if (errors.length > 0)
|
|
148
180
|
fail(400, errors.join(" "));
|
|
149
181
|
await ctx.store.tx((t) => writeSandboxConfig(t, config));
|
|
182
|
+
// Pods' idle timers follow the new limits.idle (D36).
|
|
183
|
+
await rearmPodTimers(ctx);
|
|
150
184
|
return jsonResponse(200, await sandboxView(ctx));
|
|
151
185
|
});
|
|
152
186
|
tenantRoute(api, SETTINGS, {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vaults and their credentials (`/v1/vaults/**`): secrets a session's tools use
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Vaults and their credentials (`/v1/vaults/**`): secrets a session's tools use. Only an
|
|
3
|
+
* application key acting for no one (an operator key, Studio) reaches these routes (protocol 7):
|
|
4
|
+
* a request acting for a subject, or a trusted issuer's token, is `403 scope_required`. The
|
|
5
|
+
* installation's own vaults (`scope: "installation"`) attach to any session; a vault of one
|
|
6
|
+
* person (`ownerUserId`) attaches only to that person's sessions. A person's own credentials
|
|
7
|
+
* come from the operator's credential resolver (`vault/sources.ts`), not from these routes.
|
|
8
|
+
*
|
|
9
|
+
* MCP OAuth connect (F9 C2): an application key starts one into an installation vault
|
|
10
|
+
* (`/oauth/start`); the authorization server sends the browser back to the anonymous, unversioned
|
|
11
|
+
* `GET /v1/oauth/callback`. Both only route: the gateway's keys module does the OAuth (F9-D14).
|
|
5
12
|
*/
|
|
6
13
|
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
7
14
|
import type { TenantEnv } from "../app.js";
|
|
15
|
+
/** Where the authorization server sends the browser back. */
|
|
16
|
+
export declare const OAUTH_CALLBACK_PATH = "/v1/oauth/callback";
|
|
8
17
|
export declare function vaultRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
-
import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, VaultInfo, } from "../../components.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, StartOAuthRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, StartOAuthRequest, StartOAuthResponse, VaultInfo, } from "../../components.js";
|
|
4
|
+
import { HttpError } from "../../../tenant/http.js";
|
|
5
|
+
import { VaultError } from "../../../vault/error.js";
|
|
6
6
|
import { readJson } from "../body.js";
|
|
7
|
-
import {
|
|
7
|
+
import { tenantRoute } from "../define.js";
|
|
8
8
|
import { jsonResponse } from "../respond.js";
|
|
9
|
-
|
|
9
|
+
/** Every vault route, and starting an MCP OAuth connect: an application key acting for no one. */
|
|
10
|
+
const APPLICATION = { credentials: ["application"], scopes: "never" };
|
|
11
|
+
/**
|
|
12
|
+
* The OAuth callback: a browser sent back by the authorization server, with no credential and no
|
|
13
|
+
* protocol header. The `state` it carries is the grant.
|
|
14
|
+
*/
|
|
15
|
+
const CALLBACK = {
|
|
10
16
|
credentials: ["application", "subject", "token"],
|
|
11
|
-
scopes:
|
|
12
|
-
|
|
17
|
+
scopes: "any",
|
|
18
|
+
anonymous: true,
|
|
19
|
+
unversioned: true,
|
|
13
20
|
};
|
|
21
|
+
/** Where the authorization server sends the browser back. */
|
|
22
|
+
export const OAUTH_CALLBACK_PATH = "/v1/oauth/callback";
|
|
14
23
|
const json = (schema, description) => ({
|
|
15
24
|
description,
|
|
16
25
|
content: { "application/json": { schema } },
|
|
@@ -21,58 +30,45 @@ const body = (schema) => ({
|
|
|
21
30
|
});
|
|
22
31
|
const vaultId = z.object({ vaultId: z.string() });
|
|
23
32
|
const credentialId = vaultId.extend({ credentialId: z.string() });
|
|
24
|
-
/**
|
|
25
|
-
* Who a vault route acts for: the subject, or no one in particular (an application key). Below
|
|
26
|
-
* a vault, the vault must be the subject's before anything else.
|
|
27
|
-
*/
|
|
28
|
-
async function ownerFor(c) {
|
|
29
|
-
const { vault } = c.env.tenant;
|
|
30
|
-
const owner = ownerOf(c.get("scope"));
|
|
31
|
-
const id = c.req.param("vaultId");
|
|
32
|
-
if (id !== undefined && owner !== undefined)
|
|
33
|
-
await vault.assertOwner(id, owner);
|
|
34
|
-
return owner;
|
|
35
|
-
}
|
|
36
33
|
export function vaultRoutes(api) {
|
|
37
|
-
tenantRoute(api,
|
|
34
|
+
tenantRoute(api, APPLICATION, {
|
|
38
35
|
method: "post",
|
|
39
36
|
path: "/v1/vaults",
|
|
40
37
|
tags: ["Vaults"],
|
|
41
38
|
summary: "Create a vault",
|
|
39
|
+
description: "With `scope: \"installation\"` the installation's own vault, owned by `installation`, which any session may attach; or a vault of one person (`ownerUserId`), which only that person's sessions attach.",
|
|
42
40
|
request: { body: body(CreateVaultRequest) },
|
|
43
41
|
responses: {
|
|
44
42
|
200: json(VaultInfo, "The vault"),
|
|
45
43
|
409: { description: "The idempotency key was used for another vault" },
|
|
46
44
|
},
|
|
47
45
|
}, async (c) => {
|
|
48
|
-
const owner = await ownerFor(c);
|
|
49
46
|
const request = CreateVaultRequestSchema.parse(await readJson(c.req.raw));
|
|
50
|
-
if (owner !== undefined && request.ownerUserId !== owner)
|
|
51
|
-
fail(403, "ownerUserId must be the subject");
|
|
52
47
|
return jsonResponse(200, await c.env.tenant.vault.createVault(request));
|
|
53
48
|
});
|
|
54
|
-
tenantRoute(api,
|
|
49
|
+
tenantRoute(api, APPLICATION, {
|
|
55
50
|
method: "get",
|
|
56
51
|
path: "/v1/vaults",
|
|
57
52
|
tags: ["Vaults"],
|
|
58
|
-
summary: "List
|
|
53
|
+
summary: "List vaults",
|
|
54
|
+
description: "The installation vaults, after the vaults of `ownerUserId` when it names a person.",
|
|
59
55
|
request: {
|
|
60
56
|
query: z.object({
|
|
61
57
|
ownerUserId: z
|
|
62
58
|
.string()
|
|
63
59
|
.optional()
|
|
64
|
-
.meta({ description: "
|
|
60
|
+
.meta({ description: "Also list this person's vaults" }),
|
|
65
61
|
}),
|
|
66
62
|
},
|
|
67
63
|
responses: { 200: json(ListVaultsResponse, "The vaults") },
|
|
68
64
|
}, async (c) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
return jsonResponse(200, {
|
|
66
|
+
vaults: await c.env.tenant.vault.listVaults(c.req.query("ownerUserId"), {
|
|
67
|
+
installation: true,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
74
70
|
});
|
|
75
|
-
tenantRoute(api,
|
|
71
|
+
tenantRoute(api, APPLICATION, {
|
|
76
72
|
method: "get",
|
|
77
73
|
path: "/v1/vaults/{vaultId}",
|
|
78
74
|
tags: ["Vaults"],
|
|
@@ -80,10 +76,9 @@ export function vaultRoutes(api) {
|
|
|
80
76
|
request: { params: vaultId },
|
|
81
77
|
responses: { 200: json(VaultInfo, "The vault") },
|
|
82
78
|
}, async (c) => {
|
|
83
|
-
await ownerFor(c);
|
|
84
79
|
return jsonResponse(200, await c.env.tenant.vault.getVault(c.req.param("vaultId")));
|
|
85
80
|
});
|
|
86
|
-
tenantRoute(api,
|
|
81
|
+
tenantRoute(api, APPLICATION, {
|
|
87
82
|
method: "delete",
|
|
88
83
|
path: "/v1/vaults/{vaultId}",
|
|
89
84
|
tags: ["Vaults"],
|
|
@@ -91,30 +86,21 @@ export function vaultRoutes(api) {
|
|
|
91
86
|
request: { params: vaultId },
|
|
92
87
|
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
93
88
|
}, async (c) => {
|
|
94
|
-
await ownerFor(c);
|
|
95
89
|
return jsonResponse(200, await c.env.tenant.vault.deleteVault(c.req.param("vaultId")));
|
|
96
90
|
});
|
|
97
|
-
tenantRoute(api,
|
|
91
|
+
tenantRoute(api, APPLICATION, {
|
|
98
92
|
method: "post",
|
|
99
93
|
path: "/v1/vaults/{vaultId}/credentials",
|
|
100
94
|
tags: ["Vaults"],
|
|
101
95
|
summary: "Add a credential",
|
|
102
|
-
description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to.
|
|
96
|
+
description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to.",
|
|
103
97
|
request: { params: vaultId, body: body(CreateCredentialRequest) },
|
|
104
98
|
responses: { 200: json(CredentialInfo, "The credential, without its secret") },
|
|
105
99
|
}, async (c) => {
|
|
106
|
-
await ownerFor(c);
|
|
107
100
|
const request = CreateCredentialRequestSchema.parse(await readJson(c.req.raw));
|
|
108
|
-
// The Runtime calls a refresh credential's token endpoint itself: never for a browser.
|
|
109
|
-
if (c.get("scope").kind === "token" &&
|
|
110
|
-
request.auth.type === "oauth" &&
|
|
111
|
-
request.auth.refresh)
|
|
112
|
-
fail(403, "A subject token cannot store OAuth refresh credentials", {
|
|
113
|
-
code: "scope_required",
|
|
114
|
-
});
|
|
115
101
|
return jsonResponse(200, await c.env.tenant.keys.createCredential(c.req.param("vaultId"), request));
|
|
116
102
|
});
|
|
117
|
-
tenantRoute(api,
|
|
103
|
+
tenantRoute(api, APPLICATION, {
|
|
118
104
|
method: "get",
|
|
119
105
|
path: "/v1/vaults/{vaultId}/credentials",
|
|
120
106
|
tags: ["Vaults"],
|
|
@@ -122,12 +108,11 @@ export function vaultRoutes(api) {
|
|
|
122
108
|
request: { params: vaultId },
|
|
123
109
|
responses: { 200: json(ListCredentialsResponse, "The credentials, without their secrets") },
|
|
124
110
|
}, async (c) => {
|
|
125
|
-
await ownerFor(c);
|
|
126
111
|
return jsonResponse(200, {
|
|
127
112
|
credentials: await c.env.tenant.vault.listCredentials(c.req.param("vaultId")),
|
|
128
113
|
});
|
|
129
114
|
});
|
|
130
|
-
tenantRoute(api,
|
|
115
|
+
tenantRoute(api, APPLICATION, {
|
|
131
116
|
method: "get",
|
|
132
117
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
133
118
|
tags: ["Vaults"],
|
|
@@ -135,10 +120,9 @@ export function vaultRoutes(api) {
|
|
|
135
120
|
request: { params: credentialId },
|
|
136
121
|
responses: { 200: json(CredentialInfo, "The credential, without its secret") },
|
|
137
122
|
}, async (c) => {
|
|
138
|
-
await ownerFor(c);
|
|
139
123
|
return jsonResponse(200, await c.env.tenant.vault.getCredential(c.req.param("vaultId"), c.req.param("credentialId")));
|
|
140
124
|
});
|
|
141
|
-
tenantRoute(api,
|
|
125
|
+
tenantRoute(api, APPLICATION, {
|
|
142
126
|
method: "post",
|
|
143
127
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
144
128
|
tags: ["Vaults"],
|
|
@@ -149,11 +133,10 @@ export function vaultRoutes(api) {
|
|
|
149
133
|
409: { description: "A credential's type cannot change" },
|
|
150
134
|
},
|
|
151
135
|
}, async (c) => {
|
|
152
|
-
await ownerFor(c);
|
|
153
136
|
const request = RotateCredentialRequestSchema.parse(await readJson(c.req.raw));
|
|
154
137
|
return jsonResponse(200, await c.env.tenant.keys.rotateCredential(c.req.param("vaultId"), c.req.param("credentialId"), request));
|
|
155
138
|
});
|
|
156
|
-
tenantRoute(api,
|
|
139
|
+
tenantRoute(api, APPLICATION, {
|
|
157
140
|
method: "delete",
|
|
158
141
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
159
142
|
tags: ["Vaults"],
|
|
@@ -161,7 +144,92 @@ export function vaultRoutes(api) {
|
|
|
161
144
|
request: { params: credentialId },
|
|
162
145
|
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
163
146
|
}, async (c) => {
|
|
164
|
-
await ownerFor(c);
|
|
165
147
|
return jsonResponse(200, await c.env.tenant.vault.deleteCredential(c.req.param("vaultId"), c.req.param("credentialId")));
|
|
166
148
|
});
|
|
149
|
+
tenantRoute(api, APPLICATION, {
|
|
150
|
+
method: "post",
|
|
151
|
+
path: "/v1/vaults/{vaultId}/oauth/start",
|
|
152
|
+
tags: ["Vaults"],
|
|
153
|
+
summary: "Start an MCP OAuth connect",
|
|
154
|
+
description: "Signs the installation in to the remote MCP server at `url` (declared as `server`) and stores its OAuth credential, bound to `url`, in this installation vault. The Runtime discovers the server's authorization server (RFC 9728, RFC 8414), registers itself (RFC 7591) unless `clientId` names a registered client, and answers the URL to open in a browser; the sign-in returns to `GET /v1/oauth/callback` within `expiresAt`. The callback URL is `NYLORUN_PUBLIC_URL` + `/v1/oauth/callback`, or this request's own origin when the Host has no public URL. Application keys acting for no one only. `oauth_client_required` when the server offers no registration and no `clientId` was given.",
|
|
155
|
+
request: { params: vaultId, body: body(StartOAuthRequest) },
|
|
156
|
+
responses: {
|
|
157
|
+
200: json(StartOAuthResponse, "Where to send the browser"),
|
|
158
|
+
502: {
|
|
159
|
+
description: "The authorization server, or its discovery, failed or refused (`oauth_failed`)",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}, async (c) => {
|
|
163
|
+
const request = StartOAuthRequestSchema.parse(await readJson(c.req.raw));
|
|
164
|
+
const tenant = c.env.tenant;
|
|
165
|
+
const base = tenant.config.publicUrl ?? new URL(c.req.url).origin;
|
|
166
|
+
return jsonResponse(200, await tenant.keys.startOAuth({
|
|
167
|
+
vaultId: c.req.param("vaultId"),
|
|
168
|
+
server: request.server,
|
|
169
|
+
url: request.url,
|
|
170
|
+
...(request.clientId === undefined ? {} : { clientId: request.clientId }),
|
|
171
|
+
redirectUri: `${base}${OAUTH_CALLBACK_PATH}`,
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
tenantRoute(api, CALLBACK, {
|
|
175
|
+
method: "get",
|
|
176
|
+
path: OAUTH_CALLBACK_PATH,
|
|
177
|
+
tags: ["Vaults"],
|
|
178
|
+
summary: "Finish an MCP OAuth connect",
|
|
179
|
+
description: "Where the authorization server sends the browser back with `code` and `state` (or `error`). Exchanges the code once and stores the credential; answers a small HTML page. Needs no credential and no `Nylorun-Protocol`. A `state` is used once, for ten minutes (`oauth_state_invalid`).",
|
|
180
|
+
request: {
|
|
181
|
+
query: z.object({
|
|
182
|
+
code: z.string().optional(),
|
|
183
|
+
state: z.string().optional(),
|
|
184
|
+
error: z.string().optional(),
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
responses: {
|
|
188
|
+
200: { description: "Connected", content: { "text/html": { schema: z.string() } } },
|
|
189
|
+
502: { description: "The authorization server refused the code (`oauth_failed`)" },
|
|
190
|
+
},
|
|
191
|
+
}, async (c) => {
|
|
192
|
+
const state = c.req.query("state");
|
|
193
|
+
const code = c.req.query("code");
|
|
194
|
+
const error = c.req.query("error");
|
|
195
|
+
if (!state)
|
|
196
|
+
return page(400, "Sign-in failed", "The authorization server sent no state. Start the connect again.");
|
|
197
|
+
try {
|
|
198
|
+
await c.env.tenant.keys.finishOAuth({
|
|
199
|
+
state,
|
|
200
|
+
...(code === undefined ? {} : { code }),
|
|
201
|
+
...(error === undefined ? {} : { error }),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (failure) {
|
|
205
|
+
if (failure instanceof HttpError || failure instanceof VaultError)
|
|
206
|
+
return page(failure.status, "Sign-in failed", failure.message);
|
|
207
|
+
c.env.tenant.config.logger.error("oauth_callback_failed", {
|
|
208
|
+
error: failure instanceof Error ? failure.name : "unknown",
|
|
209
|
+
});
|
|
210
|
+
return page(500, "Sign-in failed", "The connect failed. Start it again.");
|
|
211
|
+
}
|
|
212
|
+
return page(200, "Connected", "Connected. You can close this tab.");
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
216
|
+
const escape = (text) => text.replace(/[&<>"']/g, (char) => ESCAPES[char]);
|
|
217
|
+
/** The callback's answer: a small page that loads nothing, is never cached and leaks no referrer. */
|
|
218
|
+
function page(status, title, message) {
|
|
219
|
+
const html = `<!doctype html>
|
|
220
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
221
|
+
<title>${escape(title)} · Nylorun</title>
|
|
222
|
+
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1rem;color:#1a1a1a;background:#fff}@media (prefers-color-scheme:dark){body{color:#eee;background:#111}}h1{font-size:1.25rem}</style>
|
|
223
|
+
</head><body><h1>${escape(title)}</h1><p>${escape(message)}</p></body></html>
|
|
224
|
+
`;
|
|
225
|
+
return new Response(html, {
|
|
226
|
+
status,
|
|
227
|
+
headers: {
|
|
228
|
+
"content-type": "text/html; charset=utf-8",
|
|
229
|
+
"cache-control": "no-store",
|
|
230
|
+
"referrer-policy": "no-referrer",
|
|
231
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
232
|
+
"x-content-type-options": "nosniff",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
167
235
|
}
|
package/dist/api/openapi.js
CHANGED
|
@@ -68,15 +68,9 @@ function hostRoutes() {
|
|
|
68
68
|
return registry;
|
|
69
69
|
}
|
|
70
70
|
function tenantSchemes(registry) {
|
|
71
|
-
registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant, or
|
|
72
|
-
registry.registerComponent("securitySchemes", "
|
|
71
|
+
registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant: an operator key (`PUT /v1/admin/keys/{id}`), or Studio's key, derived from the admin key. With `Nylorun-Subject` and `Nylorun-Scopes`, it acts for that person, narrowed to those scopes. Never accepted from a browser (`Origin`)."));
|
|
72
|
+
registry.registerComponent("securitySchemes", "issuerToken", bearer("A JWT from a trusted issuer of the Host's identity file (Host feature `trusted-issuers`): one person, with the issuer's scopes, agents and sandbox grants, until it expires. Accepted from servers and browsers alike; CORS is the operator's proxy's.", "JWT"));
|
|
73
73
|
registry.registerComponent("securitySchemes", "deliveryToken", bearer("The delivery token the Runtime signs each Action delivery with (`Nylorun-Signature`), sent back by the Action endpoint. It reaches only that Action's heartbeat, result and sandbox routes, while that delivery is current, for at most 15 minutes.", "JWT"));
|
|
74
|
-
registry.registerComponent("securitySchemes", "publishableKey", {
|
|
75
|
-
type: "apiKey",
|
|
76
|
-
in: "header",
|
|
77
|
-
name: "Nylorun-Key",
|
|
78
|
-
description: "A publishable key, sent by a browser page on one of its origins. Public by design: it grants the access policy's anonymous scopes.",
|
|
79
|
-
});
|
|
80
74
|
}
|
|
81
75
|
let tenant;
|
|
82
76
|
let admin;
|
|
@@ -94,7 +88,7 @@ export function tenantDocument() {
|
|
|
94
88
|
info: {
|
|
95
89
|
title: "Nylorun Runtime: Tenant API",
|
|
96
90
|
version: RUNTIME_VERSION,
|
|
97
|
-
description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials
|
|
91
|
+
description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields.",
|
|
98
92
|
"x-nylorun-protocol": PROTOCOL,
|
|
99
93
|
},
|
|
100
94
|
servers: [
|
|
@@ -122,7 +116,7 @@ export function adminDocument() {
|
|
|
122
116
|
info: {
|
|
123
117
|
title: "Nylorun Runtime: Admin API",
|
|
124
118
|
version: RUNTIME_VERSION,
|
|
125
|
-
description: "The Host's status, its Tenant's state and
|
|
119
|
+
description: "The Host's status, its Tenant's state, shutdown and the Tenant's operator keys, with the admin key. Served on the operator listener when the Host has one; the public listener answers these routes with the opaque 404.",
|
|
126
120
|
"x-nylorun-protocol": PROTOCOL,
|
|
127
121
|
},
|
|
128
122
|
servers: [
|