@nylorun/runtime 0.16.0-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 +58 -0
- package/README.md +39 -27
- package/dist/admin-openapi.json +307 -7
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +10 -12
- package/dist/api/components.d.ts +54 -116
- package/dist/api/components.js +8 -11
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +9 -12
- package/dist/api/http/define.js +11 -26
- 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.js +1 -3
- 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 +2 -2
- package/dist/api/http/routes/sandboxes.js +1 -3
- package/dist/api/http/routes/sessions.js +6 -12
- 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/links.js +6 -6
- package/dist/artifacts/service.js +1 -1
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- 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 +13 -23
- 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.js +6 -1
- package/dist/host/main.js +32 -10
- package/dist/host/stack-config.d.ts +11 -8
- package/dist/host/stack-config.js +23 -12
- 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/openapi.json +2359 -4096
- 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 +21 -0
- package/dist/store/postgres/schema.d.ts +194 -170
- package/dist/store/postgres/schema.js +46 -28
- package/dist/store/postgres/store.js +51 -100
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +24 -30
- package/dist/streams/control.d.ts +0 -5
- package/dist/streams/control.js +0 -8
- package/dist/streams/types.d.ts +2 -11
- package/dist/streams/types.js +1 -1
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +3 -7
- package/dist/tenant/context.d.ts +20 -22
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +5 -1
- package/dist/tenant/effects.js +5 -16
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/host-token.js +5 -5
- 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/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/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -0
- package/dist/tenant/runtime.js +14 -1
- package/dist/tenant/sandboxes.d.ts +5 -5
- package/dist/tenant/sandboxes.js +5 -5
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/sessions.d.ts +2 -2
- package/dist/tenant/sessions.js +1 -1
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/streams.d.ts +2 -3
- package/dist/tenant/streams.js +1 -5
- package/dist/tenant/types.d.ts +21 -0
- 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 +4 -3
- 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 -187
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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";
|
|
2
|
+
import { RevokeSigningKeyRequestSchema, RotateSigningKeysRequestSchema, TOKEN_TTL_MAX_SECONDS, } from "@nylorun/core/contracts";
|
|
3
|
+
import { Jwks, RevokeSigningKeyRequest, RotateSigningKeysRequest, SigningKeyList, SigningKeyView, } from "../../components.js";
|
|
8
4
|
import { requireApplication } from "../../../tenant/auth.js";
|
|
9
|
-
import { fail } from "../../../tenant/http.js";
|
|
10
5
|
import { publicJwk, signingKeyView } from "../../../tenant/signing-keys.js";
|
|
11
|
-
import { mintToken } from "../../../tenant/tokens.js";
|
|
12
6
|
import { readJson } from "../body.js";
|
|
13
7
|
import { tenantRoute } from "../define.js";
|
|
14
8
|
import { jsonResponse } from "../respond.js";
|
|
@@ -21,21 +15,19 @@ const body = (schema) => ({
|
|
|
21
15
|
required: true,
|
|
22
16
|
content: { "application/json": { schema } },
|
|
23
17
|
});
|
|
24
|
-
const keyId = z.object({ keyId: z.string() });
|
|
25
18
|
export function accessRoutes(api) {
|
|
26
19
|
tenantRoute(api, {
|
|
27
|
-
credentials: ["application", "subject", "token"
|
|
20
|
+
credentials: ["application", "subject", "token"],
|
|
28
21
|
scopes: "any",
|
|
29
|
-
browser: true,
|
|
30
22
|
// Public keys: an Action endpoint verifies delivery tokens with them and holds no key.
|
|
31
23
|
anonymous: true,
|
|
32
24
|
}, {
|
|
33
25
|
method: "get",
|
|
34
26
|
path: "/v1/access/jwks",
|
|
35
27
|
tags: ["Access"],
|
|
36
|
-
summary: "Get the public keys
|
|
37
|
-
description: "A JSON Web Key Set, to verify a
|
|
38
|
-
"
|
|
28
|
+
summary: "Get the public keys delivery tokens are signed with",
|
|
29
|
+
description: "A JSON Web Key Set, to verify a delivery token without calling the Runtime. No " +
|
|
30
|
+
"credential is needed.",
|
|
39
31
|
responses: { 200: json(Jwks, "The public keys") },
|
|
40
32
|
}, async (c) => {
|
|
41
33
|
// Anonymous: reads the public keys. Only when the current or standby key is missing does
|
|
@@ -49,41 +41,6 @@ export function accessRoutes(api) {
|
|
|
49
41
|
}
|
|
50
42
|
return jsonResponse(200, { keys: rows.map(publicJwk) });
|
|
51
43
|
});
|
|
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
44
|
tenantRoute(api, APPLICATION, {
|
|
88
45
|
method: "get",
|
|
89
46
|
path: "/v1/access/signing-keys",
|
|
@@ -106,7 +63,7 @@ export function accessRoutes(api) {
|
|
|
106
63
|
path: "/v1/access/signing-keys/rotate",
|
|
107
64
|
tags: ["Access"],
|
|
108
65
|
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.",
|
|
66
|
+
description: "The standby key signs from now on; the current one still verifies tokens it signed. `force` also ends every outstanding delivery token and capability link.",
|
|
110
67
|
request: { body: body(RotateSigningKeysRequest) },
|
|
111
68
|
responses: {
|
|
112
69
|
200: json(SigningKeyList, "The keys after rotation"),
|
|
@@ -116,8 +73,11 @@ export function accessRoutes(api) {
|
|
|
116
73
|
requireApplication(c.get("scope"));
|
|
117
74
|
const ctx = c.env.tenant;
|
|
118
75
|
const request = RotateSigningKeysRequestSchema.parse(await readJson(c.req.raw));
|
|
119
|
-
|
|
120
|
-
const keys = await ctx.keys.rotateSigningKeys({
|
|
76
|
+
// The longest any token the Runtime signs lives (delivery tokens, capability links).
|
|
77
|
+
const keys = await ctx.keys.rotateSigningKeys({
|
|
78
|
+
maxTtlSeconds: TOKEN_TTL_MAX_SECONDS,
|
|
79
|
+
force: request.force === true,
|
|
80
|
+
});
|
|
121
81
|
ctx.config.logger.info("signing keys rotated", { force: request.force === true });
|
|
122
82
|
return jsonResponse(200, { keys });
|
|
123
83
|
});
|
|
@@ -139,126 +99,6 @@ export function accessRoutes(api) {
|
|
|
139
99
|
ctx.config.logger.info("signing key revoked", { kid: row.id });
|
|
140
100
|
return jsonResponse(200, signingKeyView(row));
|
|
141
101
|
});
|
|
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
102
|
}
|
|
263
103
|
/** True when the Tenant has a current and a standby signing key. */
|
|
264
104
|
function complete(rows) {
|
|
@@ -32,15 +32,13 @@ import { jsonResponse } from "../respond.js";
|
|
|
32
32
|
const OWN = {
|
|
33
33
|
credentials: ["application", "subject", "token"],
|
|
34
34
|
scopes: ["sessions:own"],
|
|
35
|
-
browser: true,
|
|
36
35
|
};
|
|
37
36
|
/** An upload: the same callers, with the file's bytes as the body. */
|
|
38
37
|
const UPLOAD = { ...OWN, bytes: true };
|
|
39
38
|
/** A capability link: no credential, no protocol header; the token in the path is the grant. */
|
|
40
39
|
const LINK = {
|
|
41
|
-
credentials: ["application", "subject", "token"
|
|
40
|
+
credentials: ["application", "subject", "token"],
|
|
42
41
|
scopes: "any",
|
|
43
|
-
browser: true,
|
|
44
42
|
anonymous: true,
|
|
45
43
|
unversioned: true,
|
|
46
44
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /v1/me` (Host feature `trusted-issuers`): who the Runtime takes the caller to be, for any
|
|
3
|
+
* credential that acts on the Tenant. It shows what a trusted issuer's token renders to (its
|
|
4
|
+
* subject, scopes, agents and sandbox grants), so an operator can check an identity file, and
|
|
5
|
+
* Studio can check a forwarded token for the `studio` scope.
|
|
6
|
+
*/
|
|
7
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
8
|
+
import { type MeResponse as MeBody } from "@nylorun/core/contracts";
|
|
9
|
+
import type { AuthScope } from "../../../tenant/context.js";
|
|
10
|
+
import type { TenantEnv } from "../app.js";
|
|
11
|
+
/** The caller as `GET /v1/me` reports it. */
|
|
12
|
+
export declare function describeCaller(scope: AuthScope): MeBody;
|
|
13
|
+
export declare function meRoutes(api: OpenAPIHono<TenantEnv>): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CALLER_SCOPES, SUBJECT_SCOPES, } from "@nylorun/core/contracts";
|
|
2
|
+
import { MeResponse } from "../../components.js";
|
|
3
|
+
import { fail } from "../../../tenant/http.js";
|
|
4
|
+
import { tenantRoute } from "../define.js";
|
|
5
|
+
import { jsonResponse } from "../respond.js";
|
|
6
|
+
function ordered(scopes) {
|
|
7
|
+
const held = new Set(scopes);
|
|
8
|
+
return CALLER_SCOPES.filter((scope) => held.has(scope));
|
|
9
|
+
}
|
|
10
|
+
/** The caller as `GET /v1/me` reports it. */
|
|
11
|
+
export function describeCaller(scope) {
|
|
12
|
+
switch (scope.kind) {
|
|
13
|
+
case "application":
|
|
14
|
+
return { scopes: [...SUBJECT_SCOPES], agents: "*", via: `application:${scope.principalId}` };
|
|
15
|
+
case "subject":
|
|
16
|
+
return { subject: scope.subject, scopes: ordered(scope.scopes), agents: "*", via: "subject" };
|
|
17
|
+
case "token":
|
|
18
|
+
return {
|
|
19
|
+
subject: scope.subject,
|
|
20
|
+
scopes: ordered(scope.scopes),
|
|
21
|
+
agents: scope.agents === "*" ? "*" : [...scope.agents].sort(),
|
|
22
|
+
sandboxes: [...(scope.sandboxes ?? [])],
|
|
23
|
+
via: `issuer:${scope.issuer}`,
|
|
24
|
+
};
|
|
25
|
+
default:
|
|
26
|
+
return fail(403, "This credential acts on no Tenant resource");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function meRoutes(api) {
|
|
30
|
+
tenantRoute(api, { credentials: ["application", "subject", "token"], scopes: "any" }, {
|
|
31
|
+
method: "get",
|
|
32
|
+
path: "/v1/me",
|
|
33
|
+
tags: ["Access"],
|
|
34
|
+
summary: "Get who the caller is",
|
|
35
|
+
description: "The subject, scopes, agents and sandbox grants of the credential that sends it: an " +
|
|
36
|
+
"application key (alone or acting for a subject), or a trusted issuer's token " +
|
|
37
|
+
"(`via: issuer:<name>`).",
|
|
38
|
+
responses: {
|
|
39
|
+
200: {
|
|
40
|
+
description: "The caller",
|
|
41
|
+
content: { "application/json": { schema: MeResponse } },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}, (c) => jsonResponse(200, describeCaller(c.get("scope"))));
|
|
45
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sandboxes as a resource (`/v1/sandboxes/**`, Host feature `sandboxes`): create or find one by
|
|
3
3
|
* id, read it, list them by label, read its lifecycle stream, and delete it. Ids may hold `/`,
|
|
4
|
-
* sent percent-encoded as one path segment (`team-a%2Fproj-42`). A
|
|
5
|
-
* the ids its
|
|
4
|
+
* sent percent-encoded as one path segment (`team-a%2Fproj-42`). A token caller reaches only
|
|
5
|
+
* the ids its sandbox grants match, and changes them only with `sandboxes:write`.
|
|
6
6
|
*/
|
|
7
7
|
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
8
8
|
import type { TenantEnv } from "../app.js";
|
|
@@ -10,13 +10,11 @@ import { jsonResponse } from "../respond.js";
|
|
|
10
10
|
const READ = {
|
|
11
11
|
credentials: ["application", "subject", "token"],
|
|
12
12
|
scopes: ["sessions:own", "sandboxes:write"],
|
|
13
|
-
browser: true,
|
|
14
13
|
};
|
|
15
14
|
/** Creating or deleting one. */
|
|
16
15
|
const WRITE = {
|
|
17
16
|
credentials: ["application", "subject", "token"],
|
|
18
17
|
scopes: ["sandboxes:write"],
|
|
19
|
-
browser: true,
|
|
20
18
|
};
|
|
21
19
|
const json = (schema, description) => ({
|
|
22
20
|
description,
|
|
@@ -50,7 +48,7 @@ export function sandboxRoutes(api) {
|
|
|
50
48
|
path: "/v1/sandboxes",
|
|
51
49
|
tags: ["Sandboxes"],
|
|
52
50
|
summary: "List sandboxes",
|
|
53
|
-
description: "Every sandbox with all the labels asked for. A
|
|
51
|
+
description: "Every sandbox with all the labels asked for. A token caller sees only the sandboxes its grants reach.",
|
|
54
52
|
request: {
|
|
55
53
|
query: z.object({
|
|
56
54
|
label: z
|
|
@@ -8,7 +8,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
10
|
import { sandboxGrantsOf } from "../../../tenant/sandboxes.js";
|
|
11
|
-
import { readHistory, requestCursor, streamSessionEvents } from "../../../tenant/session-streams.js";
|
|
11
|
+
import { readHistory, requestCursor, streamHolderOf, streamSessionEvents } from "../../../tenant/session-streams.js";
|
|
12
12
|
import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
|
|
13
13
|
import { readJson } from "../body.js";
|
|
14
14
|
import { tenantRoute } from "../define.js";
|
|
@@ -16,7 +16,6 @@ import { jsonResponse } from "../respond.js";
|
|
|
16
16
|
const OWN_SESSIONS = {
|
|
17
17
|
credentials: ["application", "subject", "token"],
|
|
18
18
|
scopes: ["sessions:own"],
|
|
19
|
-
browser: true,
|
|
20
19
|
};
|
|
21
20
|
const json = (schema, description) => ({
|
|
22
21
|
description,
|
|
@@ -47,23 +46,20 @@ async function sessionBelow(ctx, incoming, id, access) {
|
|
|
47
46
|
}
|
|
48
47
|
export function sessionRoutes(api) {
|
|
49
48
|
tenantRoute(api, {
|
|
50
|
-
credentials: ["application", "subject", "token"
|
|
49
|
+
credentials: ["application", "subject", "token"],
|
|
51
50
|
scopes: ["agents:read", "agents:write"],
|
|
52
|
-
browser: true,
|
|
53
51
|
}, {
|
|
54
52
|
method: "get",
|
|
55
53
|
path: "/v1/agents",
|
|
56
54
|
tags: ["Agents"],
|
|
57
55
|
summary: "List agents",
|
|
58
|
-
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.",
|
|
59
57
|
responses: {
|
|
60
58
|
200: json(z.union([ListAgentsResponse, ListPublicAgentsResponse]), "The agents"),
|
|
61
59
|
},
|
|
62
60
|
}, async (c) => {
|
|
63
61
|
const scope = c.get("scope");
|
|
64
62
|
const ctx = c.env.tenant;
|
|
65
|
-
if (scope.kind === "publishable")
|
|
66
|
-
return jsonResponse(200, await listAgentsPublic(ctx, scope.agents));
|
|
67
63
|
return jsonResponse(200, scope.kind === "token"
|
|
68
64
|
? await listAgentsPublic(ctx, scope.agents)
|
|
69
65
|
: await listDefinitions(ctx));
|
|
@@ -107,7 +103,7 @@ export function sessionRoutes(api) {
|
|
|
107
103
|
const request = PutSessionRequestSchema.parse(await readJson(c.req.raw));
|
|
108
104
|
// Agent code may trust `info`: only an app server sets it.
|
|
109
105
|
if (scope.kind === "token" && request.info !== undefined)
|
|
110
|
-
fail(403, "A
|
|
106
|
+
fail(403, "A token caller cannot set session info", { code: "scope_required" });
|
|
111
107
|
const grants = sandboxGrantsOf(scope);
|
|
112
108
|
const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { sandboxGrants: grants });
|
|
113
109
|
return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
|
|
@@ -152,7 +148,7 @@ export function sessionRoutes(api) {
|
|
|
152
148
|
path: "/v1/sessions/{sessionId}/events",
|
|
153
149
|
tags: ["Sessions"],
|
|
154
150
|
summary: "Follow a session's events",
|
|
155
|
-
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.",
|
|
156
152
|
request: { params: sessionId, query: resume },
|
|
157
153
|
responses: {
|
|
158
154
|
200: {
|
|
@@ -170,9 +166,7 @@ export function sessionRoutes(api) {
|
|
|
170
166
|
const { incoming, outgoing } = c.env;
|
|
171
167
|
const id = c.req.param("sessionId");
|
|
172
168
|
const cursor = await sessionBelow(ctx, incoming, id, accessOf(scope));
|
|
173
|
-
await streamSessionEvents(ctx, incoming, outgoing, id, cursor, scope
|
|
174
|
-
? { subject: scope.subject, epoch: scope.epoch, expiresAt: scope.expiresAt }
|
|
175
|
-
: undefined);
|
|
169
|
+
await streamSessionEvents(ctx, incoming, outgoing, id, cursor, streamHolderOf(scope));
|
|
176
170
|
return RESPONSE_ALREADY_SENT;
|
|
177
171
|
});
|
|
178
172
|
tenantRoute(api, { credentials: ["application"], scopes: "never" }, {
|
|
@@ -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;
|