@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
3
|
+
import { authenticate, requireScopes } from "../../tenant/auth.js";
|
|
4
|
+
import { identifyClient } from "../../tenant/browser.js";
|
|
5
|
+
import { fail } from "../../tenant/http.js";
|
|
6
|
+
import { ProtocolRejected, Rejected } from "../components.js";
|
|
7
|
+
const SCHEMES = {
|
|
8
|
+
application: "applicationKey",
|
|
9
|
+
subject: "applicationKey",
|
|
10
|
+
token: "subjectToken",
|
|
11
|
+
publishable: "publishableKey",
|
|
12
|
+
delivery: "deliveryToken",
|
|
13
|
+
};
|
|
14
|
+
const rejected = (description) => ({
|
|
15
|
+
description,
|
|
16
|
+
content: { "application/json": { schema: Rejected } },
|
|
17
|
+
});
|
|
18
|
+
/** The route's path segments, decoded, as the Tenant's checks read them. */
|
|
19
|
+
export function pathSegments(incoming) {
|
|
20
|
+
return new URL(incoming.url ?? "/", "http://runtime").pathname
|
|
21
|
+
.split("/")
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((segment) => {
|
|
24
|
+
try {
|
|
25
|
+
return decodeURIComponent(segment);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return fail(400, "Malformed path");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Who is calling: the browser client and the bearer, as every Tenant request checks them. */
|
|
33
|
+
export async function authenticateCaller(c,
|
|
34
|
+
/** The route serves public data (`RouteAccess.anonymous`): no credential is needed. */
|
|
35
|
+
anonymous = false) {
|
|
36
|
+
const { tenant, incoming, outgoing } = c.env;
|
|
37
|
+
// The client app first: a browser's origin is checked, and CORS headers set, before the
|
|
38
|
+
// bearer is looked at, so every answer from here on is readable by an allowed page.
|
|
39
|
+
const client = await identifyClient(tenant, incoming, outgoing);
|
|
40
|
+
if (anonymous && !client && incoming.headers.authorization === undefined)
|
|
41
|
+
return { kind: "anonymous" };
|
|
42
|
+
return await authenticate(tenant, incoming, client);
|
|
43
|
+
}
|
|
44
|
+
const declared = [];
|
|
45
|
+
/** The declared route `method path` is, if any. `{param}` segments match any one segment. */
|
|
46
|
+
export function declaredRoute(method, segments) {
|
|
47
|
+
return declared.find((route) => route.method === method.toUpperCase() &&
|
|
48
|
+
route.segments.length === segments.length &&
|
|
49
|
+
route.segments.every((segment, index) => segment === segments[index] ||
|
|
50
|
+
(segment.startsWith("{") && segments[index] !== "")))?.access;
|
|
51
|
+
}
|
|
52
|
+
function authenticated(access) {
|
|
53
|
+
return async (c, next) => {
|
|
54
|
+
const scope = await authenticateCaller(c, access.anonymous === true);
|
|
55
|
+
requireScopes(scope, access.scopes);
|
|
56
|
+
if (scope.kind === "publishable" && !access.credentials.includes("publishable"))
|
|
57
|
+
fail(403, "A publishable key alone reaches only the agent list", {
|
|
58
|
+
code: "scope_required",
|
|
59
|
+
});
|
|
60
|
+
// Handlers tell callers apart by kind, and a delivery token is none of theirs: only the
|
|
61
|
+
// routes that list it may see one.
|
|
62
|
+
if (scope.kind === "delivery" && !access.credentials.includes("delivery"))
|
|
63
|
+
fail(403, "A delivery token reaches only its Action's callbacks");
|
|
64
|
+
c.set("scope", scope);
|
|
65
|
+
await next();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function tenantRoute(api, access, route, handler) {
|
|
69
|
+
const takes = (credential) => access.credentials.includes(credential);
|
|
70
|
+
const schemes = [...new Set(access.credentials.map((credential) => SCHEMES[credential]))];
|
|
71
|
+
const headers = z.object({
|
|
72
|
+
"Nylorun-Protocol": z.string().meta({ description: `The protocol version, \`${PROTOCOL_VERSION}\`` }),
|
|
73
|
+
...(takes("subject")
|
|
74
|
+
? {
|
|
75
|
+
"Nylorun-Subject": z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.meta({ description: "The person an application key acts for" }),
|
|
79
|
+
"Nylorun-Scopes": z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.meta({ description: "The subject's space-separated scopes; required with a subject" }),
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
85
|
+
...(takes("publishable")
|
|
86
|
+
? {
|
|
87
|
+
"Nylorun-Key": z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.meta({ description: "A publishable key, from a browser page on an allowed origin" }),
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
});
|
|
94
|
+
api.openAPIRegistry.registerPath({
|
|
95
|
+
...route,
|
|
96
|
+
// `{}`: no credential needed (OpenAPI's optional security).
|
|
97
|
+
security: [
|
|
98
|
+
...schemes.map((scheme) => ({ [scheme]: [] })),
|
|
99
|
+
...(access.anonymous ? [{}] : []),
|
|
100
|
+
],
|
|
101
|
+
request: { ...route.request, headers },
|
|
102
|
+
responses: {
|
|
103
|
+
400: rejected("Invalid headers, path, body or cursor"),
|
|
104
|
+
...(takes("token") ? { 401: rejected("The subject token expired or was revoked (`token_expired`)") } : {}),
|
|
105
|
+
403: rejected("Not allowed for this credential, subject scope or origin"),
|
|
106
|
+
404: rejected("Not found, or a credential the Tenant does not know (`not_found`)"),
|
|
107
|
+
426: { description: "`Nylorun-Protocol` missing or unsupported", content: { "application/json": { schema: ProtocolRejected } } },
|
|
108
|
+
...(takes("token") ? { 429: rejected("The subject's turn limits are reached (`limit_exceeded`)") } : {}),
|
|
109
|
+
503: rejected("The Tenant's storage or streams are unavailable"),
|
|
110
|
+
...route.responses,
|
|
111
|
+
},
|
|
112
|
+
"x-nylorun-credentials": access.credentials,
|
|
113
|
+
"x-nylorun-scopes": access.scopes,
|
|
114
|
+
"x-nylorun-browser": access.browser === true,
|
|
115
|
+
});
|
|
116
|
+
declared.push({
|
|
117
|
+
method: route.method.toUpperCase(),
|
|
118
|
+
segments: route.path.split("/").filter(Boolean),
|
|
119
|
+
access,
|
|
120
|
+
});
|
|
121
|
+
api.on(route.method.toUpperCase(), route.path.replaceAll(/\/{(.+?)}/g, "/:$1"), authenticated(access), handler);
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A JSON answer. CORS headers a browser needs are already on the Node response. */
|
|
2
|
+
export declare function jsonResponse(status: number, body: unknown, headers?: Readonly<Record<string, string>>): Response;
|
|
3
|
+
export interface Rejection {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
/** What a Tenant route that threw `error` answers. */
|
|
9
|
+
export declare function rejectionOf(error: unknown): Rejection;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the Tenant API answers: JSON with an explicit length, and the one mapping from what a
|
|
3
|
+
* route throws to the rejection a client sees. The Hono routes and the router they replace
|
|
4
|
+
* (`routes.ts`) both answer through here, so a route answers the same wherever it runs.
|
|
5
|
+
*/
|
|
6
|
+
import { SandboxRouteError } from "../../core/sandbox-routes.js";
|
|
7
|
+
import { HttpError, OpaqueAuthError } from "../../tenant/http.js";
|
|
8
|
+
import { VaultError } from "../../vault/error.js";
|
|
9
|
+
/** A JSON answer. CORS headers a browser needs are already on the Node response. */
|
|
10
|
+
export function jsonResponse(status, body, headers = {}) {
|
|
11
|
+
const payload = JSON.stringify(body);
|
|
12
|
+
return new Response(payload, {
|
|
13
|
+
status,
|
|
14
|
+
headers: {
|
|
15
|
+
...headers,
|
|
16
|
+
"content-type": "application/json",
|
|
17
|
+
"content-length": String(Buffer.byteLength(payload)),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** What a Tenant route that threw `error` answers. */
|
|
22
|
+
export function rejectionOf(error) {
|
|
23
|
+
if (error instanceof OpaqueAuthError)
|
|
24
|
+
return { status: error.status, body: error.body, headers: {} };
|
|
25
|
+
const status = error instanceof HttpError ||
|
|
26
|
+
error instanceof VaultError ||
|
|
27
|
+
error instanceof SandboxRouteError
|
|
28
|
+
? error.status
|
|
29
|
+
: error?.name === "ZodError" ||
|
|
30
|
+
error?.message === "Invalid cursor"
|
|
31
|
+
? 400
|
|
32
|
+
: 500;
|
|
33
|
+
const rejection = error instanceof HttpError ? error.rejection : {};
|
|
34
|
+
return {
|
|
35
|
+
status,
|
|
36
|
+
body: {
|
|
37
|
+
status: "rejected",
|
|
38
|
+
code: status === 500 ? "internal_error" : (rejection.code ?? "request_rejected"),
|
|
39
|
+
message: status === 500 ? "Runtime request failed" : error.message,
|
|
40
|
+
...(rejection.details === undefined ? {} : { details: rejection.details }),
|
|
41
|
+
},
|
|
42
|
+
headers: error instanceof HttpError ? error.headers : {},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access management (Host feature `subject-tokens`), for the application key only: subject
|
|
3
|
+
* tokens, the access policy, signing keys, publishable keys and subject revocation; and the
|
|
4
|
+
* public keys, for any caller that reached the Tenant.
|
|
5
|
+
*
|
|
6
|
+
* Subjects travel in bodies, not paths, because they may be email addresses and request paths
|
|
7
|
+
* are logged.
|
|
8
|
+
*/
|
|
9
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
10
|
+
import type { PublishableKey as PublishableKeyBody } from "@nylorun/core/contracts";
|
|
11
|
+
import type { PublishableKeyRow } from "../../../store/types.js";
|
|
12
|
+
import type { TenantContext } from "../../../tenant/context.js";
|
|
13
|
+
import type { TenantEnv } from "../app.js";
|
|
14
|
+
export declare function accessRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
15
|
+
/** A publishable key as the API shows it. */
|
|
16
|
+
export declare function publishableKeyView(row: PublishableKeyRow): PublishableKeyBody;
|
|
17
|
+
/**
|
|
18
|
+
* Ends every token of `subject` minted so far: bumps its epoch, then ends its open streams
|
|
19
|
+
* here and, through `tenant/control`, on every other process. Running turns continue.
|
|
20
|
+
*/
|
|
21
|
+
export declare function revokeSubject(ctx: TenantContext, subject: string): Promise<{
|
|
22
|
+
subject: string;
|
|
23
|
+
epoch: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { newPublishableKey, newPublishableKeyId } from "@nylorun/core/compatibility";
|
|
3
|
+
import { CreatePublishableKeyRequestSchema, CreateTokenRequestSchema, PutAccessPolicyRequestSchema, RevokeSigningKeyRequestSchema, RevokeSubjectRequestSchema, RotateSigningKeysRequestSchema, UpdatePublishableKeyRequestSchema, } from "@nylorun/core/contracts";
|
|
4
|
+
import { AccessPolicyResponse, CreatePublishableKeyRequest, CreateTokenRequest, CreateTokenResponse, Jwks, ListPublishableKeysResponse, PublishableKey, PutAccessPolicyRequest, RevokeSigningKeyRequest, RevokeSubjectRequest, RevokeSubjectResponse, RotateSigningKeysRequest, SigningKeyList, SigningKeyView, UpdatePublishableKeyRequest, } from "../../components.js";
|
|
5
|
+
import { signalSubjectRevoked } from "../../../streams/control.js";
|
|
6
|
+
import { readPolicy, writePolicy } from "../../../tenant/access-policy.js";
|
|
7
|
+
import { currentBasin, endSubjectStreams } from "../../../tenant/session-streams.js";
|
|
8
|
+
import { requireApplication } from "../../../tenant/auth.js";
|
|
9
|
+
import { fail } from "../../../tenant/http.js";
|
|
10
|
+
import { publicJwk, signingKeyView } from "../../../tenant/signing-keys.js";
|
|
11
|
+
import { mintToken } from "../../../tenant/tokens.js";
|
|
12
|
+
import { readJson } from "../body.js";
|
|
13
|
+
import { tenantRoute } from "../define.js";
|
|
14
|
+
import { jsonResponse } from "../respond.js";
|
|
15
|
+
const APPLICATION = { credentials: ["application"], scopes: "never" };
|
|
16
|
+
const json = (schema, description) => ({
|
|
17
|
+
description,
|
|
18
|
+
content: { "application/json": { schema } },
|
|
19
|
+
});
|
|
20
|
+
const body = (schema) => ({
|
|
21
|
+
required: true,
|
|
22
|
+
content: { "application/json": { schema } },
|
|
23
|
+
});
|
|
24
|
+
const keyId = z.object({ keyId: z.string() });
|
|
25
|
+
export function accessRoutes(api) {
|
|
26
|
+
tenantRoute(api, {
|
|
27
|
+
credentials: ["application", "subject", "token", "publishable"],
|
|
28
|
+
scopes: "any",
|
|
29
|
+
browser: true,
|
|
30
|
+
// Public keys: an Action endpoint verifies delivery tokens with them and holds no key.
|
|
31
|
+
anonymous: true,
|
|
32
|
+
}, {
|
|
33
|
+
method: "get",
|
|
34
|
+
path: "/v1/access/jwks",
|
|
35
|
+
tags: ["Access"],
|
|
36
|
+
summary: "Get the public keys subject and delivery tokens are signed with",
|
|
37
|
+
description: "A JSON Web Key Set, to verify a subject token or a delivery token without calling the " +
|
|
38
|
+
"Runtime. No credential is needed.",
|
|
39
|
+
responses: { 200: json(Jwks, "The public keys") },
|
|
40
|
+
}, async (c) => {
|
|
41
|
+
// Anonymous: reads the public keys. Only when the current or standby key is missing does
|
|
42
|
+
// it ask the keys service to create it, so a verifier never caches a set without them.
|
|
43
|
+
const ctx = c.env.tenant;
|
|
44
|
+
const read = () => ctx.store.tx((t) => t.signingKeys(["standby", "current", "previous"]));
|
|
45
|
+
let rows = await read();
|
|
46
|
+
if (!complete(rows)) {
|
|
47
|
+
await ctx.keys.ensureSigningKeys();
|
|
48
|
+
rows = await read();
|
|
49
|
+
}
|
|
50
|
+
return jsonResponse(200, { keys: rows.map(publicJwk) });
|
|
51
|
+
});
|
|
52
|
+
tenantRoute(api, APPLICATION, {
|
|
53
|
+
method: "post",
|
|
54
|
+
path: "/v1/tokens",
|
|
55
|
+
tags: ["Access"],
|
|
56
|
+
summary: "Mint a subject token",
|
|
57
|
+
description: "A token for one person and one role of the access policy, for their browser or app to call the Runtime with. At most 15 minutes.",
|
|
58
|
+
request: { body: body(CreateTokenRequest) },
|
|
59
|
+
responses: { 200: json(CreateTokenResponse, "The token, its scopes and expiry") },
|
|
60
|
+
}, async (c) => {
|
|
61
|
+
requireApplication(c.get("scope"));
|
|
62
|
+
return jsonResponse(200, await mintToken(c.env.tenant, CreateTokenRequestSchema.parse(await readJson(c.req.raw))));
|
|
63
|
+
});
|
|
64
|
+
tenantRoute(api, APPLICATION, {
|
|
65
|
+
method: "get",
|
|
66
|
+
path: "/v1/access/policy",
|
|
67
|
+
tags: ["Access"],
|
|
68
|
+
summary: "Get the access policy",
|
|
69
|
+
responses: { 200: json(AccessPolicyResponse, "Roles, the anonymous grant and token lifetime") },
|
|
70
|
+
}, async (c) => {
|
|
71
|
+
requireApplication(c.get("scope"));
|
|
72
|
+
return jsonResponse(200, { policy: await c.env.tenant.store.tx((t) => readPolicy(t)) });
|
|
73
|
+
});
|
|
74
|
+
tenantRoute(api, APPLICATION, {
|
|
75
|
+
method: "put",
|
|
76
|
+
path: "/v1/access/policy",
|
|
77
|
+
tags: ["Access"],
|
|
78
|
+
summary: "Set the access policy",
|
|
79
|
+
request: { body: body(PutAccessPolicyRequest) },
|
|
80
|
+
responses: { 200: json(AccessPolicyResponse, "The policy now in force") },
|
|
81
|
+
}, async (c) => {
|
|
82
|
+
requireApplication(c.get("scope"));
|
|
83
|
+
const request = PutAccessPolicyRequestSchema.parse(await readJson(c.req.raw));
|
|
84
|
+
await c.env.tenant.store.tx((t) => writePolicy(t, request.policy));
|
|
85
|
+
return jsonResponse(200, { policy: request.policy });
|
|
86
|
+
});
|
|
87
|
+
tenantRoute(api, APPLICATION, {
|
|
88
|
+
method: "get",
|
|
89
|
+
path: "/v1/access/signing-keys",
|
|
90
|
+
tags: ["Access"],
|
|
91
|
+
summary: "List signing keys",
|
|
92
|
+
responses: { 200: json(SigningKeyList, "Standby, current, previous and revoked keys") },
|
|
93
|
+
}, async (c) => {
|
|
94
|
+
requireApplication(c.get("scope"));
|
|
95
|
+
const ctx = c.env.tenant;
|
|
96
|
+
const read = () => ctx.store.tx((t) => t.signingKeys());
|
|
97
|
+
let rows = await read();
|
|
98
|
+
if (!complete(rows)) {
|
|
99
|
+
await ctx.keys.ensureSigningKeys();
|
|
100
|
+
rows = await read();
|
|
101
|
+
}
|
|
102
|
+
return jsonResponse(200, { keys: rows.map(signingKeyView) });
|
|
103
|
+
});
|
|
104
|
+
tenantRoute(api, APPLICATION, {
|
|
105
|
+
method: "post",
|
|
106
|
+
path: "/v1/access/signing-keys/rotate",
|
|
107
|
+
tags: ["Access"],
|
|
108
|
+
summary: "Rotate the signing keys",
|
|
109
|
+
description: "The standby key signs from now on; the current one still verifies tokens it signed. `force` also ends every outstanding token.",
|
|
110
|
+
request: { body: body(RotateSigningKeysRequest) },
|
|
111
|
+
responses: {
|
|
112
|
+
200: json(SigningKeyList, "The keys after rotation"),
|
|
113
|
+
409: { description: "The previous key may still verify outstanding tokens" },
|
|
114
|
+
},
|
|
115
|
+
}, async (c) => {
|
|
116
|
+
requireApplication(c.get("scope"));
|
|
117
|
+
const ctx = c.env.tenant;
|
|
118
|
+
const request = RotateSigningKeysRequestSchema.parse(await readJson(c.req.raw));
|
|
119
|
+
const maxTtlSeconds = await ctx.store.tx(async (t) => (await readPolicy(t)).tokens.maxTtlSeconds);
|
|
120
|
+
const keys = await ctx.keys.rotateSigningKeys({ maxTtlSeconds, force: request.force === true });
|
|
121
|
+
ctx.config.logger.info("signing keys rotated", { force: request.force === true });
|
|
122
|
+
return jsonResponse(200, { keys });
|
|
123
|
+
});
|
|
124
|
+
tenantRoute(api, APPLICATION, {
|
|
125
|
+
method: "post",
|
|
126
|
+
path: "/v1/access/signing-keys/{kid}/revoke",
|
|
127
|
+
tags: ["Access"],
|
|
128
|
+
summary: "Revoke a signing key",
|
|
129
|
+
request: { params: z.object({ kid: z.string() }), body: body(RevokeSigningKeyRequest) },
|
|
130
|
+
responses: {
|
|
131
|
+
200: json(SigningKeyView, "The key, revoked"),
|
|
132
|
+
409: { description: "Rotate before revoking the current key" },
|
|
133
|
+
},
|
|
134
|
+
}, async (c) => {
|
|
135
|
+
requireApplication(c.get("scope"));
|
|
136
|
+
const ctx = c.env.tenant;
|
|
137
|
+
RevokeSigningKeyRequestSchema.parse(await readJson(c.req.raw));
|
|
138
|
+
const row = await ctx.store.tx((t) => ctx.signingKeys.revoke(t, c.req.param("kid")));
|
|
139
|
+
ctx.config.logger.info("signing key revoked", { kid: row.id });
|
|
140
|
+
return jsonResponse(200, signingKeyView(row));
|
|
141
|
+
});
|
|
142
|
+
tenantRoute(api, APPLICATION, {
|
|
143
|
+
method: "get",
|
|
144
|
+
path: "/v1/access/publishable-keys",
|
|
145
|
+
tags: ["Access"],
|
|
146
|
+
summary: "List publishable keys",
|
|
147
|
+
responses: { 200: json(ListPublishableKeysResponse, "The keys, revoked ones included") },
|
|
148
|
+
}, async (c) => {
|
|
149
|
+
requireApplication(c.get("scope"));
|
|
150
|
+
return jsonResponse(200, {
|
|
151
|
+
keys: (await c.env.tenant.store.tx((t) => t.publishableKeys())).map(publishableKeyView),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
tenantRoute(api, APPLICATION, {
|
|
155
|
+
method: "post",
|
|
156
|
+
path: "/v1/access/publishable-keys",
|
|
157
|
+
tags: ["Access"],
|
|
158
|
+
summary: "Create a publishable key",
|
|
159
|
+
description: "A key a browser page on one of the origins sends as `Nylorun-Key`. Public by design; it grants the policy's anonymous scopes.",
|
|
160
|
+
request: { body: body(CreatePublishableKeyRequest) },
|
|
161
|
+
responses: {
|
|
162
|
+
200: json(PublishableKey, "The key"),
|
|
163
|
+
409: { description: "A key with this name exists" },
|
|
164
|
+
},
|
|
165
|
+
}, async (c) => {
|
|
166
|
+
requireApplication(c.get("scope"));
|
|
167
|
+
const ctx = c.env.tenant;
|
|
168
|
+
const request = CreatePublishableKeyRequestSchema.parse(await readJson(c.req.raw));
|
|
169
|
+
const row = {
|
|
170
|
+
id: newPublishableKeyId(),
|
|
171
|
+
key: newPublishableKey(ctx.config.tenantId),
|
|
172
|
+
name: request.name,
|
|
173
|
+
originsJson: JSON.stringify(request.origins),
|
|
174
|
+
createdAt: new Date().toISOString(),
|
|
175
|
+
revokedAt: null,
|
|
176
|
+
};
|
|
177
|
+
await ctx.store.tx(async (t) => {
|
|
178
|
+
if ((await t.publishableKeys()).some((other) => other.name === request.name))
|
|
179
|
+
fail(409, `A publishable key named ${request.name} exists`);
|
|
180
|
+
await t.insertPublishableKey(row);
|
|
181
|
+
});
|
|
182
|
+
ctx.config.logger.info("publishable key created", { keyId: row.id });
|
|
183
|
+
return jsonResponse(200, publishableKeyView(row));
|
|
184
|
+
});
|
|
185
|
+
for (const method of ["put", "delete"])
|
|
186
|
+
tenantRoute(api, APPLICATION, {
|
|
187
|
+
method,
|
|
188
|
+
path: "/v1/access/publishable-keys/{keyId}",
|
|
189
|
+
tags: ["Access"],
|
|
190
|
+
summary: method === "put" ? "Set a publishable key's origins" : "Revoke a publishable key",
|
|
191
|
+
request: {
|
|
192
|
+
params: keyId,
|
|
193
|
+
...(method === "put" ? { body: body(UpdatePublishableKeyRequest) } : {}),
|
|
194
|
+
},
|
|
195
|
+
responses: {
|
|
196
|
+
200: json(PublishableKey, method === "put" ? "The key" : "The key, revoked (again: unchanged)"),
|
|
197
|
+
},
|
|
198
|
+
}, async (c) => {
|
|
199
|
+
requireApplication(c.get("scope"));
|
|
200
|
+
const ctx = c.env.tenant;
|
|
201
|
+
const id = c.req.param("keyId");
|
|
202
|
+
const patch = method === "put"
|
|
203
|
+
? {
|
|
204
|
+
originsJson: JSON.stringify(UpdatePublishableKeyRequestSchema.parse(await readJson(c.req.raw)).origins),
|
|
205
|
+
}
|
|
206
|
+
: { revokedAt: new Date().toISOString() };
|
|
207
|
+
const row = await ctx.store.tx(async (t) => {
|
|
208
|
+
const existing = (await t.publishableKey(id)) ?? fail(404, "Publishable key not found");
|
|
209
|
+
// Revoking twice keeps the first time.
|
|
210
|
+
if (method === "delete" && existing.revokedAt !== null)
|
|
211
|
+
return existing;
|
|
212
|
+
await t.updatePublishableKey(id, patch);
|
|
213
|
+
return { ...existing, ...patch };
|
|
214
|
+
});
|
|
215
|
+
ctx.config.logger.info(method === "put" ? "publishable key updated" : "publishable key revoked", { keyId: row.id });
|
|
216
|
+
return jsonResponse(200, publishableKeyView(row));
|
|
217
|
+
});
|
|
218
|
+
tenantRoute(api, APPLICATION, {
|
|
219
|
+
method: "post",
|
|
220
|
+
path: "/v1/access/revocations",
|
|
221
|
+
tags: ["Access"],
|
|
222
|
+
summary: "Revoke a subject",
|
|
223
|
+
description: "Ends every token minted for the subject so far, and their open streams on every process. Turns in progress continue.",
|
|
224
|
+
request: { body: body(RevokeSubjectRequest) },
|
|
225
|
+
responses: { 200: json(RevokeSubjectResponse, "The subject's new epoch") },
|
|
226
|
+
}, async (c) => {
|
|
227
|
+
requireApplication(c.get("scope"));
|
|
228
|
+
const request = RevokeSubjectRequestSchema.parse(await readJson(c.req.raw));
|
|
229
|
+
return jsonResponse(200, await revokeSubject(c.env.tenant, request.subject));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/** A publishable key as the API shows it. */
|
|
233
|
+
export function publishableKeyView(row) {
|
|
234
|
+
return {
|
|
235
|
+
id: row.id,
|
|
236
|
+
name: row.name,
|
|
237
|
+
key: row.key,
|
|
238
|
+
origins: JSON.parse(row.originsJson),
|
|
239
|
+
createdAt: row.createdAt,
|
|
240
|
+
revokedAt: row.revokedAt,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Ends every token of `subject` minted so far: bumps its epoch, then ends its open streams
|
|
245
|
+
* here and, through `tenant/control`, on every other process. Running turns continue.
|
|
246
|
+
*/
|
|
247
|
+
export async function revokeSubject(ctx, subject) {
|
|
248
|
+
const epoch = await ctx.store.tx(async (t) => {
|
|
249
|
+
const next = await t.bumpSubjectEpoch(subject, new Date().toISOString());
|
|
250
|
+
t.afterCommit(async () => {
|
|
251
|
+
endSubjectStreams(ctx.sessionStreams, subject, next);
|
|
252
|
+
const streams = ctx.sessionStreams.wiring?.streams;
|
|
253
|
+
if (streams)
|
|
254
|
+
await signalSubjectRevoked(streams, currentBasin(ctx), subject, next).catch((error) => ctx.config.logger.warn("subject revocation signal failed", {
|
|
255
|
+
message: error instanceof Error ? error.message : String(error),
|
|
256
|
+
}));
|
|
257
|
+
});
|
|
258
|
+
return next;
|
|
259
|
+
});
|
|
260
|
+
ctx.config.logger.info("subject revoked", { epoch });
|
|
261
|
+
return { subject, epoch };
|
|
262
|
+
}
|
|
263
|
+
/** True when the Tenant has a current and a standby signing key. */
|
|
264
|
+
function complete(rows) {
|
|
265
|
+
return rows.some((row) => row.state === "current") && rows.some((row) => row.state === "standby");
|
|
266
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session commands, and the callbacks an Action endpoint makes about the Action it was
|
|
3
|
+
* delivered: heartbeats and sandbox tools, with the Action's delivery token (Action endpoints).
|
|
4
|
+
* The background result is in `endpoints.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
7
|
+
import type { TenantEnv } from "../app.js";
|
|
8
|
+
export declare function actionRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SessionCommandSchema } from "@nylorun/core/contracts";
|
|
3
|
+
import { AcceptedResponse, DeliveryHeartbeatResponse, SandboxToolOutcome, SessionCommand, } from "../../components.js";
|
|
4
|
+
import { actionSandboxTool } from "../../../tenant/actions.js";
|
|
5
|
+
import { command } from "../../../tenant/commands.js";
|
|
6
|
+
import { deliveryHeartbeat } from "../../../tenant/delivery.js";
|
|
7
|
+
import { fail } from "../../../tenant/http.js";
|
|
8
|
+
import { readJson } from "../body.js";
|
|
9
|
+
import { tenantRoute } from "../define.js";
|
|
10
|
+
import { jsonResponse } from "../respond.js";
|
|
11
|
+
/** An Action's callbacks: only the delivery token of its current delivery. */
|
|
12
|
+
const CALLBACK = { credentials: ["delivery"], scopes: "never" };
|
|
13
|
+
const json = (schema, description) => ({
|
|
14
|
+
description,
|
|
15
|
+
content: { "application/json": { schema } },
|
|
16
|
+
});
|
|
17
|
+
const body = (schema) => ({
|
|
18
|
+
required: true,
|
|
19
|
+
content: { "application/json": { schema } },
|
|
20
|
+
});
|
|
21
|
+
const actionId = z.object({ actionId: z.string() });
|
|
22
|
+
const SANDBOX_TOOLS = ["bash", "read", "write", "edit", "grep", "glob"];
|
|
23
|
+
function deliveryOf(scope) {
|
|
24
|
+
if (scope.kind !== "delivery")
|
|
25
|
+
return fail(403, "A delivery token is required");
|
|
26
|
+
return scope;
|
|
27
|
+
}
|
|
28
|
+
export function actionRoutes(api) {
|
|
29
|
+
tenantRoute(api, CALLBACK, {
|
|
30
|
+
method: "post",
|
|
31
|
+
path: "/v1/actions/{actionId}/sandbox/{tool}",
|
|
32
|
+
tags: ["Action endpoints"],
|
|
33
|
+
summary: "Run a sandbox tool for an Action being delivered",
|
|
34
|
+
description: "Called by the Action endpoint with the Action's delivery token. The body is the tool's input.",
|
|
35
|
+
request: {
|
|
36
|
+
params: actionId.extend({ tool: z.enum(SANDBOX_TOOLS) }),
|
|
37
|
+
body: body(z.looseObject({}).meta({ description: "The tool's input" })),
|
|
38
|
+
},
|
|
39
|
+
responses: {
|
|
40
|
+
200: json(SandboxToolOutcome, "The tool's output, or why it failed"),
|
|
41
|
+
409: { description: "The delivery was cancelled, lost or sent again" },
|
|
42
|
+
},
|
|
43
|
+
}, async (c) => jsonResponse(200, await actionSandboxTool(c.env.tenant, deliveryOf(c.get("scope")), c.req.param("actionId"), c.req.param("tool"), () => readJson(c.req.raw), c.req.raw.signal)));
|
|
44
|
+
tenantRoute(api, CALLBACK, {
|
|
45
|
+
method: "post",
|
|
46
|
+
path: "/v1/actions/{actionId}/heartbeat",
|
|
47
|
+
tags: ["Action endpoints"],
|
|
48
|
+
summary: "Keep a background delivery alive",
|
|
49
|
+
description: "Called by an Action endpoint that answered a delivery with 202: extends the delivery by a " +
|
|
50
|
+
"lease and returns a fresh delivery token.",
|
|
51
|
+
request: { params: actionId },
|
|
52
|
+
responses: {
|
|
53
|
+
200: json(DeliveryHeartbeatResponse, "The delivery's new deadline, and a fresh token"),
|
|
54
|
+
409: { description: "The delivery was cancelled, lost or sent again" },
|
|
55
|
+
},
|
|
56
|
+
}, async (c) => jsonResponse(200, await deliveryHeartbeat(c.env.tenant, deliveryOf(c.get("scope")), c.req.param("actionId"))));
|
|
57
|
+
tenantRoute(api, {
|
|
58
|
+
credentials: ["application", "subject", "token"],
|
|
59
|
+
scopes: ["sessions:own"],
|
|
60
|
+
browser: true,
|
|
61
|
+
}, {
|
|
62
|
+
method: "post",
|
|
63
|
+
path: "/v1/sessions/{sessionId}/commands",
|
|
64
|
+
tags: ["Sessions"],
|
|
65
|
+
summary: "Send a command to a session",
|
|
66
|
+
description: "A message starts a turn; the others answer what the session waits on, or cancel. Idempotent on the command's `idempotencyKey`.",
|
|
67
|
+
request: {
|
|
68
|
+
params: z.object({ sessionId: z.string() }),
|
|
69
|
+
body: body(SessionCommand),
|
|
70
|
+
},
|
|
71
|
+
responses: {
|
|
72
|
+
200: json(AcceptedResponse, "The command, accepted"),
|
|
73
|
+
409: { description: "A turn is running, or the session does not wait on this" },
|
|
74
|
+
},
|
|
75
|
+
}, async (c) => jsonResponse(200, await command(c.env.tenant, c.req.param("sessionId"), SessionCommandSchema.parse(await readJson(c.req.raw)), c.get("scope"))));
|
|
76
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action endpoints: where the Runtime delivers each agent's Actions (design: Action endpoints
|
|
3
|
+
* §4.2). Registered, listed and removed with the application key; no subject reaches them.
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
6
|
+
import type { TenantEnv } from "../app.js";
|
|
7
|
+
export declare function endpointRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PutEndpointsRequestSchema } from "@nylorun/core/contracts";
|
|
3
|
+
import { DeleteEndpointResponse, EndpointPingResponse, ListEndpointsResponse, PutEndpointsRequest, } from "../../components.js";
|
|
4
|
+
import { ActionOutcomeSchema } from "@nylorun/core/contracts";
|
|
5
|
+
import { ActionOutcome, ActionResultReceipt } from "../../components.js";
|
|
6
|
+
import { deliveryResult, pingEndpoint } from "../../../tenant/delivery.js";
|
|
7
|
+
import { requireApplication } from "../../../tenant/auth.js";
|
|
8
|
+
import { fail } from "../../../tenant/http.js";
|
|
9
|
+
import { deleteEndpoint, listEndpoints, putEndpoints, } from "../../../tenant/endpoints.js";
|
|
10
|
+
import { readJson } from "../body.js";
|
|
11
|
+
import { tenantRoute } from "../define.js";
|
|
12
|
+
import { jsonResponse } from "../respond.js";
|
|
13
|
+
const APPLICATION = { credentials: ["application"], scopes: "never" };
|
|
14
|
+
const json = (schema, description) => ({
|
|
15
|
+
description,
|
|
16
|
+
content: { "application/json": { schema } },
|
|
17
|
+
});
|
|
18
|
+
export function endpointRoutes(api) {
|
|
19
|
+
tenantRoute(api, APPLICATION, {
|
|
20
|
+
method: "get",
|
|
21
|
+
path: "/v1/endpoints",
|
|
22
|
+
tags: ["Action endpoints"],
|
|
23
|
+
summary: "List Action endpoints",
|
|
24
|
+
description: "Each agent's Action endpoint, with what recent deliveries say about it.",
|
|
25
|
+
responses: { 200: json(ListEndpointsResponse, "The endpoints") },
|
|
26
|
+
}, async (c) => {
|
|
27
|
+
requireApplication(c.get("scope"));
|
|
28
|
+
return jsonResponse(200, await listEndpoints(c.env.tenant));
|
|
29
|
+
});
|
|
30
|
+
tenantRoute(api, APPLICATION, {
|
|
31
|
+
method: "put",
|
|
32
|
+
path: "/v1/endpoints",
|
|
33
|
+
tags: ["Action endpoints"],
|
|
34
|
+
summary: "Register Action endpoints",
|
|
35
|
+
description: "Points the Runtime at the URL that runs each agent's Actions. A new URL starts with no " +
|
|
36
|
+
"health.",
|
|
37
|
+
request: {
|
|
38
|
+
body: { required: true, content: { "application/json": { schema: PutEndpointsRequest } } },
|
|
39
|
+
},
|
|
40
|
+
responses: { 200: json(ListEndpointsResponse, "The registered endpoints") },
|
|
41
|
+
}, async (c) => {
|
|
42
|
+
const principalId = requireApplication(c.get("scope"));
|
|
43
|
+
return jsonResponse(200, await putEndpoints(c.env.tenant, principalId, PutEndpointsRequestSchema.parse(await readJson(c.req.raw))));
|
|
44
|
+
});
|
|
45
|
+
tenantRoute(api, APPLICATION, {
|
|
46
|
+
method: "delete",
|
|
47
|
+
path: "/v1/endpoints/{agentId}",
|
|
48
|
+
tags: ["Action endpoints"],
|
|
49
|
+
summary: "Remove an agent's Action endpoint",
|
|
50
|
+
description: "The agent's pending Actions wait until an endpoint is registered again.",
|
|
51
|
+
request: { params: z.object({ agentId: z.string() }) },
|
|
52
|
+
responses: {
|
|
53
|
+
200: json(DeleteEndpointResponse, "The endpoint was removed"),
|
|
54
|
+
404: { description: "The agent has no endpoint" },
|
|
55
|
+
},
|
|
56
|
+
}, async (c) => {
|
|
57
|
+
requireApplication(c.get("scope"));
|
|
58
|
+
return jsonResponse(200, await deleteEndpoint(c.env.tenant, c.req.param("agentId")));
|
|
59
|
+
});
|
|
60
|
+
tenantRoute(api, APPLICATION, {
|
|
61
|
+
method: "post",
|
|
62
|
+
path: "/v1/endpoints/{agentId}/ping",
|
|
63
|
+
tags: ["Action endpoints"],
|
|
64
|
+
summary: "Ping an agent's Action endpoint",
|
|
65
|
+
description: "Sends a signed ping through the endpoint and records what it answers it serves. A " +
|
|
66
|
+
"wrong URL, a tunnel that is down or a handler that does not serve the agent answers 502.",
|
|
67
|
+
request: { params: z.object({ agentId: z.string() }) },
|
|
68
|
+
responses: {
|
|
69
|
+
200: json(EndpointPingResponse, "What the endpoint serves"),
|
|
70
|
+
404: { description: "The agent has no endpoint" },
|
|
71
|
+
502: { description: "The endpoint did not answer the ping" },
|
|
72
|
+
},
|
|
73
|
+
}, async (c) => {
|
|
74
|
+
requireApplication(c.get("scope"));
|
|
75
|
+
return jsonResponse(200, await pingEndpoint(c.env.tenant, c.req.param("agentId")));
|
|
76
|
+
});
|
|
77
|
+
tenantRoute(api, { credentials: ["delivery"], scopes: "never" }, {
|
|
78
|
+
method: "post",
|
|
79
|
+
path: "/v1/actions/{actionId}/result",
|
|
80
|
+
tags: ["Action endpoints"],
|
|
81
|
+
summary: "Post the outcome of an Action answered with 202",
|
|
82
|
+
description: "Called by an Action endpoint with the Action's delivery token after it answered the " +
|
|
83
|
+
"delivery with 202. The same result again returns the first receipt.",
|
|
84
|
+
request: {
|
|
85
|
+
params: z.object({ actionId: z.string() }),
|
|
86
|
+
body: { required: true, content: { "application/json": { schema: ActionOutcome } } },
|
|
87
|
+
},
|
|
88
|
+
responses: {
|
|
89
|
+
200: json(ActionResultReceipt, "The outcome was recorded"),
|
|
90
|
+
409: {
|
|
91
|
+
description: "The delivery was cancelled, lost or sent again, or the Action has another result",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}, async (c) => {
|
|
95
|
+
const scope = c.get("scope");
|
|
96
|
+
if (scope.kind !== "delivery")
|
|
97
|
+
return fail(403, "A delivery token is required");
|
|
98
|
+
return jsonResponse(200, await deliveryResult(c.env.tenant, scope, c.req.param("actionId"), ActionOutcomeSchema.parse(await readJson(c.req.raw))));
|
|
99
|
+
});
|
|
100
|
+
}
|