@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
package/dist/tenant/auth.js
CHANGED
|
@@ -2,10 +2,9 @@ import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
|
|
|
2
2
|
import { DELIVERY_TOKEN_TYPE, parseSubjectHeaders, } from "@nylorun/core/contracts";
|
|
3
3
|
import { hashToken } from "../core/bearer.js";
|
|
4
4
|
import { fail, failOpaque } from "./http.js";
|
|
5
|
-
import { looksLikeToken, verifySubjectToken } from "./tokens.js";
|
|
6
5
|
import { verifyDeliveryToken } from "./delivery-token.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { verifyIssuerToken } from "./issuers.js";
|
|
7
|
+
import { looksLikeToken, tokenType } from "./jwt.js";
|
|
9
8
|
const SUBJECT_INVALID = { code: "subject_invalid" };
|
|
10
9
|
/** One header's value, or `undefined`; a header sent twice is refused. */
|
|
11
10
|
function singleHeader(request, name) {
|
|
@@ -18,20 +17,10 @@ function singleHeader(request, name) {
|
|
|
18
17
|
return fail(400, `${name} must be sent once`, SUBJECT_INVALID);
|
|
19
18
|
return values[0];
|
|
20
19
|
}
|
|
21
|
-
export async function authenticate(ctx, request
|
|
20
|
+
export async function authenticate(ctx, request) {
|
|
22
21
|
const header = request.headers.authorization;
|
|
23
22
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
24
23
|
if (!token || !header?.startsWith("Bearer ")) {
|
|
25
|
-
// A publishable key alone: what the policy grants `anon` (nothing by default).
|
|
26
|
-
if (client && header === undefined) {
|
|
27
|
-
const { anon } = await ctx.store.tx((t) => readPolicy(t));
|
|
28
|
-
return {
|
|
29
|
-
kind: "publishable",
|
|
30
|
-
keyId: client.keyId,
|
|
31
|
-
scopes: new Set(anon.scopes),
|
|
32
|
-
agents: anon.agents === "*" ? "*" : new Set(anon.agents),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
24
|
ctx.config.logger.warn("credential rejected", {
|
|
36
25
|
reason: "missing_bearer",
|
|
37
26
|
});
|
|
@@ -47,24 +36,27 @@ export async function authenticate(ctx, request, client) {
|
|
|
47
36
|
fail(403, "A delivery token cannot act for a subject");
|
|
48
37
|
return scope;
|
|
49
38
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
// A trusted issuer's token (Host feature `trusted-issuers`), from a server or a browser: its
|
|
40
|
+
// unverified `iss` names an issuer of the identity file, and only that issuer verifies it.
|
|
41
|
+
const issuer = ctx.config.issuers?.claimed(token);
|
|
42
|
+
if (issuer) {
|
|
43
|
+
const scope = await verifyIssuerToken(ctx, issuer, token);
|
|
53
44
|
if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
|
|
54
45
|
singleHeader(request, SCOPES_HEADER) !== undefined)
|
|
55
|
-
fail(403, "
|
|
46
|
+
fail(403, "An issuer token cannot act for another subject");
|
|
56
47
|
return scope;
|
|
57
48
|
}
|
|
49
|
+
// Any other JWT: no issuer of this Host signed it (subject tokens are gone, protocol 7).
|
|
50
|
+
if (looksLikeToken(token)) {
|
|
51
|
+
ctx.config.logger.warn("credential rejected", { reason: "token_unknown_issuer" });
|
|
52
|
+
return failOpaque();
|
|
53
|
+
}
|
|
58
54
|
// Application keys never come from a browser or a shipped app: refused before they are even
|
|
59
55
|
// looked up.
|
|
60
56
|
if (request.headers.origin !== undefined)
|
|
61
57
|
fail(403, "Application keys are not accepted from browsers", {
|
|
62
58
|
code: "origin_rejected",
|
|
63
59
|
});
|
|
64
|
-
if (client)
|
|
65
|
-
fail(400, "Nylorun-Key is for browser and mobile clients, not with a Tenant key", {
|
|
66
|
-
code: "invalid_request",
|
|
67
|
-
});
|
|
68
60
|
const tokenHash = hashToken(token);
|
|
69
61
|
const principal = await ctx.store.tx((t) => t.principalByTokenHash(tokenHash));
|
|
70
62
|
if (!principal) {
|
|
@@ -92,13 +84,11 @@ export async function authenticate(ctx, request, client) {
|
|
|
92
84
|
};
|
|
93
85
|
}
|
|
94
86
|
/**
|
|
95
|
-
* A subject
|
|
96
|
-
*
|
|
87
|
+
* A subject or a token caller reaches a route only with one of the scopes it declares:
|
|
88
|
+
* `403 scope_required`, decided from the route alone before anything is read.
|
|
97
89
|
*/
|
|
98
90
|
export function requireScopes(scope, access) {
|
|
99
|
-
if (scope.kind !== "subject" &&
|
|
100
|
-
scope.kind !== "token" &&
|
|
101
|
-
scope.kind !== "publishable")
|
|
91
|
+
if (scope.kind !== "subject" && scope.kind !== "token")
|
|
102
92
|
return;
|
|
103
93
|
if (access === "any")
|
|
104
94
|
return;
|
|
@@ -107,7 +97,8 @@ export function requireScopes(scope, access) {
|
|
|
107
97
|
code: "scope_required",
|
|
108
98
|
details: { scopes: [] },
|
|
109
99
|
});
|
|
110
|
-
|
|
100
|
+
const held = scope.scopes;
|
|
101
|
+
if (!access.some((name) => held.has(name)))
|
|
111
102
|
fail(403, `Scope ${access.join(" or ")} required`, {
|
|
112
103
|
code: "scope_required",
|
|
113
104
|
details: { scopes: access },
|
|
@@ -124,9 +115,7 @@ export function accessOf(scope) {
|
|
|
124
115
|
owner: scope.subject,
|
|
125
116
|
...(scope.agents === "*" ? {} : { agents: scope.agents }),
|
|
126
117
|
};
|
|
127
|
-
// A
|
|
128
|
-
case "publishable":
|
|
129
|
-
// Nor does a request with no credential, on a route that serves public data.
|
|
118
|
+
// A request with no credential, on a route that serves public data, owns nothing.
|
|
130
119
|
case "anonymous":
|
|
131
120
|
return fail(404, "Not found");
|
|
132
121
|
// A delivery token reaches its Action's callbacks, never a session or vault.
|
|
@@ -138,7 +127,7 @@ export function accessOf(scope) {
|
|
|
138
127
|
}
|
|
139
128
|
}
|
|
140
129
|
}
|
|
141
|
-
/** The subject whose sessions
|
|
130
|
+
/** The subject whose sessions the request is limited to, if it acts for one. */
|
|
142
131
|
export function ownerOf(scope) {
|
|
143
132
|
return accessOf(scope)?.owner;
|
|
144
133
|
}
|
|
@@ -159,6 +148,10 @@ export function scoped(scope, action) {
|
|
|
159
148
|
return;
|
|
160
149
|
fail(403, "This delivery token is for another Action");
|
|
161
150
|
}
|
|
151
|
+
/** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
|
|
152
|
+
export function mayUseAgent(scope, agentId) {
|
|
153
|
+
return scope.kind !== "token" || scope.agents === "*" || scope.agents.has(agentId);
|
|
154
|
+
}
|
|
162
155
|
export function requireApplication(scope) {
|
|
163
156
|
if (scope.kind === "application")
|
|
164
157
|
return scope.principalId;
|
package/dist/tenant/commands.js
CHANGED
|
@@ -24,7 +24,6 @@ import { canonical } from "../store/canonical.js";
|
|
|
24
24
|
import { lockedSession, } from "./context.js";
|
|
25
25
|
import { fail } from "./http.js";
|
|
26
26
|
import { accessOf } from "./auth.js";
|
|
27
|
-
import { chargeTurn } from "./subject-limits.js";
|
|
28
27
|
import { checkSandboxTurn } from "./sandboxes.js";
|
|
29
28
|
import { actionTarget, rebaseSessionState, turnManifestOf, variantStore, } from "./session.js";
|
|
30
29
|
import { signalActionOutcome, signalSessionCancel } from "./streams.js";
|
|
@@ -97,7 +96,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
97
96
|
if (scope.kind === "token" &&
|
|
98
97
|
command.type === "message" &&
|
|
99
98
|
command.manifest !== undefined)
|
|
100
|
-
fail(403, "A
|
|
99
|
+
fail(403, "A token caller cannot send message.manifest", {
|
|
101
100
|
code: "scope_required",
|
|
102
101
|
});
|
|
103
102
|
const key = commandKey(id, command.idempotencyKey);
|
|
@@ -180,12 +179,9 @@ export async function command(ctx, id, input, scope) {
|
|
|
180
179
|
if (command.type === "message") {
|
|
181
180
|
if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
|
|
182
181
|
fail(409, "Session has active or unresolved work");
|
|
183
|
-
// Every turn start on a sandbox: it exists, the token's
|
|
184
|
-
// session's turn holds it.
|
|
182
|
+
// Every turn start on a sandbox: it exists, the token's grants reach it, and no other
|
|
183
|
+
// session's turn holds it.
|
|
185
184
|
await checkSandboxTurn(t, s, scope, ctx);
|
|
186
|
-
// After the replay check above, so a retried message is never charged twice.
|
|
187
|
-
if (scope.kind === "token" && scope.limits)
|
|
188
|
-
await chargeTurn(t, scope.subject, scope.limits);
|
|
189
185
|
s.turnStartState = s.state;
|
|
190
186
|
// A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
|
|
191
187
|
if (s.history?.snapshot !== undefined)
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* under ownership (§10.6); `abortLocal` aborts an advance running on this process.
|
|
15
15
|
*/
|
|
16
16
|
import type { SessionHistory } from "./history.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type { IssuerScope, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
|
|
18
18
|
import type { DurableCheckpoint, FlowCheckpoint } from "@nylorun/harness/run";
|
|
19
19
|
import type { AgentManifest, JsonValue, SandboxManifest } from "@nylorun/core/define";
|
|
20
20
|
import type { CredentialSelection } from "@nylorun/core/contracts";
|
|
@@ -25,6 +25,7 @@ import type { ModelGate } from "../gates/model-gate.js";
|
|
|
25
25
|
import type { ToolGate } from "../gates/tool-gate.js";
|
|
26
26
|
import type { Keys } from "../keys/keys.js";
|
|
27
27
|
import type { VaultService } from "../vault/service.js";
|
|
28
|
+
import type { CredentialSources } from "../vault/sources.js";
|
|
28
29
|
import type { McpPool } from "../mcp/pool.js";
|
|
29
30
|
import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
|
|
30
31
|
import type { HarnessApiServer } from "../harness-api/server.js";
|
|
@@ -95,22 +96,24 @@ export type AuthScope = {
|
|
|
95
96
|
principalId: string;
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
|
-
* A
|
|
99
|
-
*
|
|
99
|
+
* A trusted issuer's token (Host feature `trusted-issuers`, F9-D12): one subject, with the
|
|
100
|
+
* issuer's scopes, agents and sandbox grants, until `expiresAt` (ms). Only its expiry ends it.
|
|
100
101
|
*/
|
|
101
102
|
| {
|
|
102
103
|
kind: "token";
|
|
104
|
+
/** The identity file's issuer that signed it. */
|
|
105
|
+
issuer: string;
|
|
103
106
|
subject: string;
|
|
104
|
-
scopes
|
|
107
|
+
/** The issuer's scopes the token holds, which may add `studio`. */
|
|
108
|
+
scopes: ReadonlySet<IssuerScope>;
|
|
105
109
|
agents: ReadonlySet<string> | "*";
|
|
106
|
-
|
|
107
|
-
limits?: RoleLimits;
|
|
108
|
-
/** The token's `sbx` grants: the sandboxes it reaches. Absent reaches none. */
|
|
110
|
+
/** The sandboxes it reaches: exact ids or `p/*` prefixes. Absent or empty reaches none. */
|
|
109
111
|
sandboxes?: readonly string[];
|
|
110
|
-
epoch: number;
|
|
111
112
|
expiresAt: number;
|
|
113
|
+
/** The token's `jti`, or a hash of it. */
|
|
112
114
|
tokenId: string;
|
|
113
|
-
|
|
115
|
+
/** The issuer key's `kid`, when it has one. */
|
|
116
|
+
keyId?: string;
|
|
114
117
|
}
|
|
115
118
|
/** An application principal acting for `subject` (`Nylorun-Subject`), narrowed to `scopes`. */
|
|
116
119
|
| {
|
|
@@ -135,22 +138,17 @@ export type AuthScope = {
|
|
|
135
138
|
expiresAt: number;
|
|
136
139
|
tokenId: string;
|
|
137
140
|
keyId: string;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* A publishable key with no bearer (Host feature `browser-access`): what the policy grants
|
|
141
|
-
* `anon`, at most `agents:read`. It owns no session or vault.
|
|
142
|
-
*/
|
|
143
|
-
| {
|
|
144
|
-
kind: "publishable";
|
|
145
|
-
keyId: string;
|
|
146
|
-
scopes: ReadonlySet<SubjectScope>;
|
|
147
|
-
agents: ReadonlySet<string> | "*";
|
|
148
141
|
};
|
|
149
142
|
export interface TenantContext {
|
|
150
143
|
readonly config: TenantConfig;
|
|
151
144
|
readonly envelope: TenantEnvelope;
|
|
152
145
|
readonly store: SessionStore;
|
|
153
146
|
readonly vault: VaultService;
|
|
147
|
+
/**
|
|
148
|
+
* A session's MCP credentials, for the in-process MCP pool: its attached vaults, then the
|
|
149
|
+
* operator's credential resolver (`TenantConfig.resolver`, F9 C1).
|
|
150
|
+
*/
|
|
151
|
+
readonly credentials: CredentialSources;
|
|
154
152
|
/**
|
|
155
153
|
* The MCP pool of the Tenant's in-process harness. Absent when harnesses run elsewhere
|
|
156
154
|
* (`NYLORUN_HARNESS=remote`): each keeps its own (F6.2).
|
|
@@ -208,7 +206,7 @@ export interface TenantContext {
|
|
|
208
206
|
readonly work: WorkState;
|
|
209
207
|
/** Live delivery over Durable Streams: one `SessionStream` per observed session, and the streams wiring. */
|
|
210
208
|
readonly sessionStreams: SessionStreams;
|
|
211
|
-
/** The Tenant's signing keys
|
|
209
|
+
/** The Tenant's signing keys: delivery tokens, capability links, run and host tokens. */
|
|
212
210
|
readonly signingKeys: SigningKeys;
|
|
213
211
|
/** The Worker id this process writes as session `owner` (§10.6). */
|
|
214
212
|
readonly workerId: string;
|
|
@@ -248,8 +246,8 @@ export interface TenantContext {
|
|
|
248
246
|
onSweep(hook: () => Promise<void>): () => void;
|
|
249
247
|
}
|
|
250
248
|
/**
|
|
251
|
-
* What a request acting for a person may reach: that person's sessions
|
|
252
|
-
*
|
|
249
|
+
* What a request acting for a person may reach: that person's sessions, and, for a token
|
|
250
|
+
* caller, only sessions of the agents its issuer allows. Undefined means the whole Tenant.
|
|
253
251
|
*/
|
|
254
252
|
export interface SessionAccess {
|
|
255
253
|
readonly owner: string;
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* Tenant's public keys, and comes back as the bearer of that Action's callbacks (a background
|
|
6
6
|
* result, heartbeats, sandbox tools).
|
|
7
7
|
*
|
|
8
|
-
* A token lives at most `DELIVERY_TOKEN_MAX_TTL_SECONDS
|
|
8
|
+
* A token lives at most `DELIVERY_TOKEN_MAX_TTL_SECONDS` (`TOKEN_TTL_MAX_SECONDS`), and key
|
|
9
9
|
* rotation waits at least that long before it revokes the previous key
|
|
10
10
|
* (`signing-keys.ts`), so a rotation never revokes a live token.
|
|
11
11
|
*/
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { errors, jwtVerify } from "jose";
|
|
14
|
-
import { DELIVERY_TOKEN_MAX_TTL_SECONDS, DELIVERY_TOKEN_TYPE,
|
|
14
|
+
import { DELIVERY_TOKEN_MAX_TTL_SECONDS, DELIVERY_TOKEN_TYPE, tenantTokenIssuer, } from "@nylorun/core/contracts";
|
|
15
15
|
import { failOpaque, HttpError } from "./http.js";
|
|
16
16
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
17
17
|
/** base64url SHA-256 of a request body: the token's `bdy` claim. */
|
|
@@ -34,7 +34,7 @@ export async function mintDeliveryToken(ctx, options) {
|
|
|
34
34
|
agt: delivery.agentId,
|
|
35
35
|
gen: delivery.kind === "action" ? delivery.generation : 0,
|
|
36
36
|
bdy: bodyHash(options.body),
|
|
37
|
-
iss:
|
|
37
|
+
iss: tenantTokenIssuer(ctx.config.tenantId),
|
|
38
38
|
aud: options.audience,
|
|
39
39
|
sub: delivery.kind === "action" ? delivery.actionId : "ping",
|
|
40
40
|
iat,
|
|
@@ -62,7 +62,7 @@ export async function verifyDeliveryToken(ctx, raw) {
|
|
|
62
62
|
try {
|
|
63
63
|
const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
|
|
64
64
|
algorithms: ["ES256"],
|
|
65
|
-
issuer:
|
|
65
|
+
issuer: tenantTokenIssuer(ctx.config.tenantId),
|
|
66
66
|
typ: DELIVERY_TOKEN_TYPE,
|
|
67
67
|
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
68
68
|
requiredClaims: ["sub", "aud", "iat", "exp", "jti"],
|
package/dist/tenant/effects.d.ts
CHANGED
|
@@ -38,8 +38,12 @@ export declare function isRemoteMcpEffect(s: Session, request: HostEffect): bool
|
|
|
38
38
|
export declare function recoversModelCalls(ctx: TenantContext): boolean;
|
|
39
39
|
/** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
|
|
40
40
|
export declare function resolveNewFlowEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease): Promise<EffectResolution>;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The credential of an MCP server request made on behalf of a session: from its attached vaults,
|
|
43
|
+
* else the operator's credential resolver (`vault/sources.ts`).
|
|
44
|
+
*/
|
|
42
45
|
export declare function authorize(ctx: TenantContext, sessionId: string, request: {
|
|
43
46
|
url: string;
|
|
44
47
|
serverName?: string;
|
|
48
|
+
agentId?: string;
|
|
45
49
|
}): Promise<AuthorizeResult>;
|
package/dist/tenant/effects.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { embeddedAgent, flowDelegateManifest, hashManifest, isWorkflowManifestV2, } from "@nylorun/core/define";
|
|
2
2
|
import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowToolEffect, isWorkflowManifest, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
|
|
3
3
|
import { mayDispatchMore } from "../core/limits.js";
|
|
4
|
-
import { scrub } from "../redact.js";
|
|
5
4
|
import { isRemoteMcpCall } from "../harness/calls.js";
|
|
6
5
|
import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
7
6
|
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
@@ -286,23 +285,13 @@ function inheritedSandbox(owner, definition) {
|
|
|
286
285
|
throw new Error(sandboxed.message);
|
|
287
286
|
return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
|
|
288
287
|
}
|
|
289
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* The credential of an MCP server request made on behalf of a session: from its attached vaults,
|
|
290
|
+
* else the operator's credential resolver (`vault/sources.ts`).
|
|
291
|
+
*/
|
|
290
292
|
export async function authorize(ctx, sessionId, request) {
|
|
291
293
|
const s = await loadSession(ctx, sessionId);
|
|
292
|
-
|
|
293
|
-
sessionId,
|
|
294
|
-
vaultIds: s.vaultIds ?? [],
|
|
295
|
-
credentialSelections: s.credentialSelections ?? [],
|
|
296
|
-
url: request.url,
|
|
297
|
-
serverName: request.serverName,
|
|
298
|
-
});
|
|
299
|
-
if (result.status === "authorized") {
|
|
300
|
-
const token = result.headers.authorization.slice("Bearer ".length);
|
|
301
|
-
scrub({ authorization: result.headers.authorization, url: result.url }, [
|
|
302
|
-
token,
|
|
303
|
-
]);
|
|
304
|
-
}
|
|
305
|
-
return result;
|
|
294
|
+
return ctx.credentials.authorize({ ...s, id: sessionId }, request);
|
|
306
295
|
}
|
|
307
296
|
/**
|
|
308
297
|
* The definition a flow leaf's session runs. A v2 workflow embeds its leaves, so they
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type PostgresClient } from "../store/postgres/connect.js";
|
|
2
|
+
import type { TrustedIssuerConfig } from "./identity-file.js";
|
|
3
|
+
import { type TrustedIssuers } from "./issuers.js";
|
|
2
4
|
import type { Logger, TenantModelConfig } from "./types.js";
|
|
5
|
+
import type { ResolverConfig } from "../vault/sources.js";
|
|
3
6
|
export interface StartEphemeralRuntimeOptions {
|
|
4
7
|
/**
|
|
5
8
|
* Absolute Host root for `host.json`, the admin key and the Tenant's files (vault key,
|
|
@@ -25,10 +28,19 @@ export interface StartEphemeralRuntimeOptions {
|
|
|
25
28
|
sandboxBackend?: "auto" | "virtual";
|
|
26
29
|
/** When true, close() leaves hostRoot on disk. */
|
|
27
30
|
retainRoot?: boolean;
|
|
28
|
-
/** Allow browser requests (an `Origin` with a publishable key). Default off. */
|
|
29
|
-
browserAccess?: boolean;
|
|
30
31
|
/** Serve the Admin API on its own loopback listener (`adminUrl`). Default off. */
|
|
31
32
|
operatorListener?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Trusted issuers whose JWTs the Tenant API accepts (Host feature `trusted-issuers`): the
|
|
35
|
+
* issuers of an identity file (`parseIdentityFile`), or ones already built with
|
|
36
|
+
* `createTrustedIssuers`. Default none.
|
|
37
|
+
*/
|
|
38
|
+
issuers?: readonly TrustedIssuerConfig[] | TrustedIssuers;
|
|
39
|
+
/**
|
|
40
|
+
* The operator's credential resolver (F9 C1): asked for a person's MCP credential when the
|
|
41
|
+
* session's vaults hold none. Default: none (vaults only).
|
|
42
|
+
*/
|
|
43
|
+
resolver?: ResolverConfig;
|
|
32
44
|
logger?: Logger;
|
|
33
45
|
/**
|
|
34
46
|
* The Postgres database of the Tenant (one Tenant per database): a pool, which the caller
|
package/dist/tenant/ephemeral.js
CHANGED
|
@@ -24,6 +24,7 @@ import { createTenantLogger } from "./logger.js";
|
|
|
24
24
|
import { hostPaths, tenantPaths } from "./paths.js";
|
|
25
25
|
import { hostPrincipals } from "./principals.js";
|
|
26
26
|
import { openTenantRuntime } from "./runtime.js";
|
|
27
|
+
import { createTrustedIssuers } from "./issuers.js";
|
|
27
28
|
const nodeRequire = createRequire(import.meta.url);
|
|
28
29
|
function coreVersion() {
|
|
29
30
|
try {
|
|
@@ -66,6 +67,11 @@ export async function startEphemeralRuntime(options) {
|
|
|
66
67
|
};
|
|
67
68
|
const baseline = options.baseline ?? {};
|
|
68
69
|
const model = options.model ?? { kind: "scripted", output: "ok" };
|
|
70
|
+
const issuers = options.issuers === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: Array.isArray(options.issuers)
|
|
73
|
+
? createTrustedIssuers(options.issuers)
|
|
74
|
+
: options.issuers;
|
|
69
75
|
const configFor = (tenantId) => {
|
|
70
76
|
const tenant = tenantPaths(hostRoot);
|
|
71
77
|
// A seeded `sandbox.backend` setting overrides this when the Tenant opens.
|
|
@@ -76,6 +82,8 @@ export async function startEphemeralRuntime(options) {
|
|
|
76
82
|
paths: tenant,
|
|
77
83
|
sandbox: { backend: sandboxBackend },
|
|
78
84
|
model,
|
|
85
|
+
...(issuers ? { issuers } : {}),
|
|
86
|
+
...(options.resolver ? { resolver: options.resolver } : {}),
|
|
79
87
|
childEnv: Object.freeze({
|
|
80
88
|
...baseline,
|
|
81
89
|
HOME: tenant.home,
|
|
@@ -142,7 +150,6 @@ export async function startEphemeralRuntime(options) {
|
|
|
142
150
|
credentials,
|
|
143
151
|
logger,
|
|
144
152
|
coreVersion: coreVersion(),
|
|
145
|
-
browserAccess: options.browserAccess === true,
|
|
146
153
|
...(options.operatorListener
|
|
147
154
|
? { operator: { host: "127.0.0.1", port: 0 } }
|
|
148
155
|
: {}),
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* The host token is accepted only by the Harness API listener (a connection serving that one
|
|
18
18
|
* sandbox, and `host.renew`); the egress token only by egress-gate, as the proxy credential
|
|
19
19
|
* (`Proxy-Authorization`). Neither is a bearer anywhere else: the Tenant API's bearer check
|
|
20
|
-
* accepts
|
|
20
|
+
* accepts trusted issuers' and delivery tokens only, and the gates run tokens only.
|
|
21
21
|
*
|
|
22
22
|
* Verification reads the key row on every call (a revoked key refuses at once) and, with
|
|
23
23
|
* `currentHost`, the sandbox's row (one indexed read): the epoch must be the sandbox's, the
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
27
|
import { errors, importJWK, jwtVerify } from "jose";
|
|
28
|
-
import {
|
|
28
|
+
import { tenantTokenIssuer } from "@nylorun/core/contracts";
|
|
29
29
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
30
30
|
export const HOST_TOKEN_TYP = "nylorun-host+jwt";
|
|
31
31
|
export const HOST_TOKEN_AUD = "nylorun-harness-api";
|
|
@@ -52,7 +52,7 @@ export async function mintEgressToken(signer, request) {
|
|
|
52
52
|
const { token } = await signer.keys.sign({
|
|
53
53
|
typ: EGRESS_TOKEN_TYP,
|
|
54
54
|
claims: {
|
|
55
|
-
iss:
|
|
55
|
+
iss: tenantTokenIssuer(tenantId),
|
|
56
56
|
aud: EGRESS_TOKEN_AUD,
|
|
57
57
|
sbx: sandboxId,
|
|
58
58
|
epc: epoch,
|
|
@@ -73,7 +73,7 @@ export async function mintHostTokens(signer, host) {
|
|
|
73
73
|
signer.keys.sign({
|
|
74
74
|
typ: HOST_TOKEN_TYP,
|
|
75
75
|
claims: {
|
|
76
|
-
iss:
|
|
76
|
+
iss: tenantTokenIssuer(tenantId),
|
|
77
77
|
aud: HOST_TOKEN_AUD,
|
|
78
78
|
sub: host.sandboxId,
|
|
79
79
|
sbx: host.sandboxId,
|
|
@@ -126,7 +126,7 @@ export async function verifyHostToken(store, tenantId, raw, kind, cache = new Ma
|
|
|
126
126
|
try {
|
|
127
127
|
const verified = await jwtVerify(raw, key, {
|
|
128
128
|
algorithms: ["ES256"],
|
|
129
|
-
issuer:
|
|
129
|
+
issuer: tenantTokenIssuer(tenantId),
|
|
130
130
|
audience: aud,
|
|
131
131
|
typ,
|
|
132
132
|
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity file (Host feature `trusted-issuers`, F9 I2): the YAML file
|
|
3
|
+
* `NYLORUN_IDENTITY_FILE` names, listing the issuers whose JWTs the Tenant API accepts as
|
|
4
|
+
* bearers. Read once at boot (`host/main.ts`); a change takes a restart. A malformed file stops
|
|
5
|
+
* the boot with a message naming the issuer and the field.
|
|
6
|
+
*
|
|
7
|
+
* ```yaml
|
|
8
|
+
* issuers:
|
|
9
|
+
* - name: keycloak
|
|
10
|
+
* issuer: https://sso.acme.dev/realms/eng
|
|
11
|
+
* audience: nylorun
|
|
12
|
+
* jwks: https://sso.acme.dev/realms/eng/protocol/openid-connect/certs # or keys: [<PEM>]
|
|
13
|
+
* subject: "u:{sub}" # scalar claims only
|
|
14
|
+
* scopes: { claim: nylorun_scopes } # or { fixed: [sessions:own, agents:read] }
|
|
15
|
+
* allowedScopes: [agents:read, sessions:own, sandboxes:write, studio]
|
|
16
|
+
* agents: [support] # optional allowlist; absent means all
|
|
17
|
+
* sandboxes: ["{org_id}/*"] # optional grants; absent reaches none
|
|
18
|
+
* maxLifetime: 15m
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { type KeyObject } from "node:crypto";
|
|
22
|
+
import { type IssuerScope } from "@nylorun/core/contracts";
|
|
23
|
+
/** The JWT algorithms an issuer may sign with. */
|
|
24
|
+
export declare const ISSUER_ALGORITHMS: readonly ["RS256", "ES256", "EdDSA"];
|
|
25
|
+
export type IssuerAlgorithm = (typeof ISSUER_ALGORITHMS)[number];
|
|
26
|
+
/** A public key an issuer signs with, and the one algorithm it verifies. */
|
|
27
|
+
export interface IssuerKey {
|
|
28
|
+
readonly key: KeyObject;
|
|
29
|
+
readonly alg: IssuerAlgorithm;
|
|
30
|
+
readonly kid?: string;
|
|
31
|
+
}
|
|
32
|
+
/** One trusted issuer, as the identity file declares it. */
|
|
33
|
+
export interface TrustedIssuerConfig {
|
|
34
|
+
/** Its name: `issuer:<name>` in `GET /v1/me` and the token's role. */
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** The `iss` its tokens carry, exactly. */
|
|
37
|
+
readonly issuer: string;
|
|
38
|
+
/** The `aud` its tokens must carry. */
|
|
39
|
+
readonly audience: string;
|
|
40
|
+
/** Where its public keys are fetched (a JWKS URL); or `keys`. */
|
|
41
|
+
readonly jwks?: string;
|
|
42
|
+
/** Its public keys, given in the file (PEM); or `jwks`. */
|
|
43
|
+
readonly keys?: readonly IssuerKey[];
|
|
44
|
+
/** The subject template, such as `u:{sub}`: at least one claim reference. */
|
|
45
|
+
readonly subject: string;
|
|
46
|
+
/** Where its tokens' scopes come from: a claim, or a fixed list. */
|
|
47
|
+
readonly scopes: {
|
|
48
|
+
readonly claim: string;
|
|
49
|
+
} | {
|
|
50
|
+
readonly fixed: readonly IssuerScope[];
|
|
51
|
+
};
|
|
52
|
+
/** The scopes its tokens may carry; others are dropped. */
|
|
53
|
+
readonly allowedScopes: readonly IssuerScope[];
|
|
54
|
+
/** The agents its tokens reach; absent reaches all. */
|
|
55
|
+
readonly agents?: readonly string[];
|
|
56
|
+
/** Sandbox grant templates, such as `{org_id}/*`; absent reaches no sandbox. */
|
|
57
|
+
readonly sandboxes?: readonly string[];
|
|
58
|
+
/** The longest `exp - iat` it may sign, in seconds. */
|
|
59
|
+
readonly maxLifetimeSeconds: number;
|
|
60
|
+
}
|
|
61
|
+
/** A malformed identity file: the message names the issuer and the field. */
|
|
62
|
+
export declare class IdentityFileError extends Error {
|
|
63
|
+
constructor(message: string);
|
|
64
|
+
}
|
|
65
|
+
/** The claim names a template references. */
|
|
66
|
+
export declare function claimReferences(template: string): string[];
|
|
67
|
+
/** `template` with every reference replaced by `value(claim)`; undefined when one has none. */
|
|
68
|
+
export declare function renderTemplate(template: string, value: (claim: string) => string | undefined): string | undefined;
|
|
69
|
+
/** A PEM public key an issuer may sign with, and its algorithm; throws a message otherwise. */
|
|
70
|
+
export declare function issuerKey(key: KeyObject, kid?: string): IssuerKey;
|
|
71
|
+
/**
|
|
72
|
+
* The issuers of an identity file's text. Throws `IdentityFileError` naming the file, the issuer
|
|
73
|
+
* and the field when the file is malformed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseIdentityFile(text: string, source?: string): TrustedIssuerConfig[];
|