@stigmer/server 3.18.0 → 3.19.0
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/README.md +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +42 -6
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +6 -2
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js.map +1 -1
- package/dist/boot/oauth-redirect-uri.d.ts +54 -0
- package/dist/boot/oauth-redirect-uri.d.ts.map +1 -0
- package/dist/boot/oauth-redirect-uri.js +37 -0
- package/dist/boot/oauth-redirect-uri.js.map +1 -0
- package/dist/domain/agentexecution/lifecycle.js +1 -1
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/mcpserver/complete-endpoint-auth.d.ts +16 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.d.ts.map +1 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.js +260 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.js.map +1 -0
- package/dist/domain/mcpserver/complete-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.js +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/connect.d.ts +25 -0
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +8 -23
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +12 -0
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.js +38 -14
- package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/oauth/dcr.d.ts +10 -1
- package/dist/domain/mcpserver/oauth/dcr.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/dcr.js +1 -9
- package/dist/domain/mcpserver/oauth/dcr.js.map +1 -1
- package/dist/domain/mcpserver/oauth/discovery.d.ts +43 -20
- package/dist/domain/mcpserver/oauth/discovery.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/discovery.js +69 -54
- package/dist/domain/mcpserver/oauth/discovery.js.map +1 -1
- package/dist/domain/mcpserver/oauth/preflight.d.ts +27 -1
- package/dist/domain/mcpserver/oauth/preflight.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/preflight.js +1 -26
- package/dist/domain/mcpserver/oauth/preflight.js.map +1 -1
- package/dist/domain/mcpserver/oauth/refresh.d.ts +2 -1
- package/dist/domain/mcpserver/oauth/refresh.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/refresh.js +1 -1
- package/dist/domain/mcpserver/oauth/refresh.js.map +1 -1
- package/dist/domain/mcpserver/oauth/token.d.ts +9 -2
- package/dist/domain/mcpserver/oauth/token.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/token.js +2 -8
- package/dist/domain/mcpserver/oauth/token.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +1 -1
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/extensions/drivers.d.ts +17 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/outbound-egress.d.ts +49 -0
- package/dist/extensions/outbound-egress.d.ts.map +1 -0
- package/dist/extensions/outbound-egress.js +51 -0
- package/dist/extensions/outbound-egress.js.map +1 -0
- package/dist/extensions/registry.d.ts +6 -0
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +10 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-labels.d.ts +14 -0
- package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
- package/dist/pipeline/apiresource-labels.js +14 -0
- package/dist/pipeline/apiresource-labels.js.map +1 -1
- package/dist/sandbox/kubernetes.d.ts.map +1 -1
- package/dist/sandbox/kubernetes.js +8 -1
- package/dist/sandbox/kubernetes.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts +20 -5
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sandbox/steps.d.ts +22 -8
- package/dist/sandbox/steps.d.ts.map +1 -1
- package/dist/sandbox/steps.js +17 -12
- package/dist/sandbox/steps.js.map +1 -1
- package/package.json +6 -5
- package/src/boot/__tests__/oauth-redirect-uri.test.ts +38 -0
- package/src/boot/compose.ts +47 -8
- package/src/boot/config.ts +6 -2
- package/src/boot/oauth-redirect-uri.ts +53 -0
- package/src/domain/agentexecution/lifecycle.ts +1 -1
- package/src/domain/mcpserver/__tests__/complete-endpoint-auth.test.ts +270 -0
- package/src/domain/mcpserver/__tests__/connect-authorization.test.ts +3 -0
- package/src/domain/mcpserver/__tests__/connect.test.ts +31 -0
- package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +1 -1
- package/src/domain/mcpserver/complete-endpoint-auth.ts +277 -0
- package/src/domain/mcpserver/complete-oauth-connect.ts +1 -0
- package/src/domain/mcpserver/connect.ts +21 -15
- package/src/domain/mcpserver/controller.ts +16 -0
- package/src/domain/mcpserver/initiate-oauth-connect.ts +37 -13
- package/src/domain/mcpserver/oauth/__tests__/discovery.test.ts +120 -91
- package/src/domain/mcpserver/oauth/__tests__/token-refresh.test.ts +3 -1
- package/src/domain/mcpserver/oauth/dcr.ts +2 -1
- package/src/domain/mcpserver/oauth/discovery.ts +120 -89
- package/src/domain/mcpserver/oauth/preflight.ts +2 -1
- package/src/domain/mcpserver/oauth/refresh.ts +2 -1
- package/src/domain/mcpserver/oauth/token.ts +4 -3
- package/src/domain/workflowexecution/lifecycle.ts +1 -1
- package/src/extensions/__tests__/outbound-egress-points.test.ts +58 -0
- package/src/extensions/drivers.ts +17 -0
- package/src/extensions/outbound-egress.ts +55 -0
- package/src/extensions/registry.ts +19 -0
- package/src/index.ts +10 -0
- package/src/pipeline/apiresource-labels.ts +16 -0
- package/src/sandbox/__tests__/docker.integration.test.ts +2 -0
- package/src/sandbox/__tests__/kubernetes.test.ts +10 -2
- package/src/sandbox/__tests__/local-process.integration.test.ts +3 -0
- package/src/sandbox/__tests__/steps.test.ts +72 -19
- package/src/sandbox/kubernetes.ts +8 -1
- package/src/sandbox/provisioner.ts +20 -5
- package/src/sandbox/steps.ts +29 -12
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the outboundEgress driver point in the single-instance shape the
|
|
3
|
+
* registry already enforces for policyGrantScope and licenseStatus:
|
|
4
|
+
*
|
|
5
|
+
* - `drivers.outboundEgress` — the address policy the composition root
|
|
6
|
+
* builds the McpServer slice's one guarded fetch from; single instance;
|
|
7
|
+
* absent = open source's relaxed posture installs at the compose.ts
|
|
8
|
+
* consumption site.
|
|
9
|
+
*
|
|
10
|
+
* And the two exported postures: relaxed refuses only the link-local
|
|
11
|
+
* (cloud metadata) range, strict refuses loopback, private and unspecified
|
|
12
|
+
* too, and neither owns a copy of the ranges (both read the shared table).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { relaxedEgressPolicy, strictEgressPolicy } from "../outbound-egress.js";
|
|
17
|
+
import { resolveExtensions } from "../registry.js";
|
|
18
|
+
|
|
19
|
+
describe("the outboundEgress driver point", () => {
|
|
20
|
+
it("is undefined with no extensions — the relaxed posture installs at the consumption site", () => {
|
|
21
|
+
expect(resolveExtensions([]).drivers.outboundEgress).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("carries the one registered instance through", () => {
|
|
25
|
+
const policy = strictEgressPolicy();
|
|
26
|
+
const resolved = resolveExtensions([{ name: "cloud-core", drivers: { outboundEgress: policy } }]);
|
|
27
|
+
expect(resolved.drivers.outboundEgress).toBe(policy);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("throws on a second registration, naming both units", () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
resolveExtensions([
|
|
33
|
+
{ name: "egress-a", drivers: { outboundEgress: strictEgressPolicy() } },
|
|
34
|
+
{ name: "egress-b", drivers: { outboundEgress: relaxedEgressPolicy() } },
|
|
35
|
+
]),
|
|
36
|
+
).toThrowError(/extension 'egress-b' registers an OutboundEgressPolicy, but 'egress-a' already did/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("the two postures", () => {
|
|
41
|
+
it("relaxed refuses only the link-local range, so a local MCP server stays reachable", () => {
|
|
42
|
+
const relaxed = relaxedEgressPolicy();
|
|
43
|
+
expect(relaxed.name).toBe("relaxed");
|
|
44
|
+
expect(relaxed.blockedReason("127.0.0.1")).toBeNull();
|
|
45
|
+
expect(relaxed.blockedReason("10.0.0.1")).toBeNull();
|
|
46
|
+
expect(relaxed.blockedReason("169.254.169.254")).toBe("link-local (cloud metadata)");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("strict refuses loopback, private, link-local and unspecified addresses", () => {
|
|
50
|
+
const strict = strictEgressPolicy();
|
|
51
|
+
expect(strict.name).toBe("strict");
|
|
52
|
+
expect(strict.blockedReason("127.0.0.1")).toBe("loopback");
|
|
53
|
+
expect(strict.blockedReason("10.0.0.1")).toBe("private (RFC 1918)");
|
|
54
|
+
expect(strict.blockedReason("169.254.169.254")).toBe("link-local (cloud metadata)");
|
|
55
|
+
expect(strict.blockedReason("0.0.0.0")).toBe("unspecified");
|
|
56
|
+
expect(strict.blockedReason("104.16.0.1")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
* port, which kinds an edition grants on, and the tuple-half query
|
|
32
32
|
* engine only an authorization backend can answer) — landed with
|
|
33
33
|
* 20260913.01, gate ruling Q-OR-10
|
|
34
|
+
* - outbound egress (which addresses the control plane may dial when
|
|
35
|
+
* it reaches a URL a user supplied: the MCP endpoint it probes at save
|
|
36
|
+
* time and the login server it reaches on Sign in) — landed with the
|
|
37
|
+
* save-time OAuth completion for URL-only MCP servers, 2026-09-19
|
|
34
38
|
*
|
|
35
39
|
* Merge rules (enforced by resolveExtensions, DD-006 §2b): the two
|
|
36
40
|
* provider kinds are single-instance points — a second declaring unit is
|
|
@@ -54,6 +58,7 @@ import type { SandboxProvisionerFactory } from "../sandbox/provisioner.js";
|
|
|
54
58
|
import type { AuthorizationQueryEngine } from "./authorization-queries.js";
|
|
55
59
|
import type { IdentityFederation } from "./identity-federation.js";
|
|
56
60
|
import type { LicenseStatusProvider } from "./license-status.js";
|
|
61
|
+
import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
57
62
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
58
63
|
import type { OrganizationDirectory } from "./organization-directory.js";
|
|
59
64
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
@@ -230,4 +235,16 @@ export interface ExtensionDrivers {
|
|
|
230
235
|
* way (extensions/license-status.ts carries the contract).
|
|
231
236
|
*/
|
|
232
237
|
readonly licenseStatus?: LicenseStatusProvider;
|
|
238
|
+
/**
|
|
239
|
+
* The outbound-egress policy (single-instance point): which addresses
|
|
240
|
+
* this edition's control plane may dial when it reaches a URL a user
|
|
241
|
+
* supplied. The composition root builds one guarded fetch from it and
|
|
242
|
+
* hands it to the McpServer connect slice, the only fetch its OAuth code
|
|
243
|
+
* and its save-time endpoint probe hold. When absent, open source's
|
|
244
|
+
* `relaxedEgressPolicy()` installs at the compose.ts consumption site:
|
|
245
|
+
* everything but the link-local (cloud metadata) range is allowed, so a
|
|
246
|
+
* server beside a self-hosted control plane keeps working
|
|
247
|
+
* (extensions/outbound-egress.ts carries the contract).
|
|
248
|
+
*/
|
|
249
|
+
readonly outboundEgress?: OutboundEgressPolicy;
|
|
233
250
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outbound-egress driver point: WHICH addresses this edition's control
|
|
3
|
+
* plane may dial when it reaches a URL a user supplied. Single instance,
|
|
4
|
+
* registered as `drivers.outboundEgress` (the policyGrantScope shape: a
|
|
5
|
+
* driver point whose absence is open source's own behaviour). Absent, the
|
|
6
|
+
* composition root installs `relaxedEgressPolicy()`: everything but the
|
|
7
|
+
* link-local range is allowed, because a server beside a self-hosted
|
|
8
|
+
* control plane (`http://localhost:3000/mcp`, the proto's own example) is
|
|
9
|
+
* the normal case and the link-local range, which carries the cloud
|
|
10
|
+
* metadata endpoint, costs a local user nothing to refuse. The cloud
|
|
11
|
+
* registers `strictEgressPolicy()`: loopback, private, link-local and
|
|
12
|
+
* unspecified refused, because its control plane sits on infrastructure
|
|
13
|
+
* with reach to internal services.
|
|
14
|
+
*
|
|
15
|
+
* Who reads it: nobody in the domains directly. The composition root builds
|
|
16
|
+
* ONE guarded fetch from the policy (`@stigmer/outbound/egress`
|
|
17
|
+
* `guardedFetch`, which judges the first URL and every redirect hop) and
|
|
18
|
+
* hands it to the McpServer connect slice as `outboundFetch`, the only
|
|
19
|
+
* fetch the McpServer domain's OAuth code and its save-time endpoint probe
|
|
20
|
+
* hold. Before this point existed every OAuth fetch (discovery, dynamic
|
|
21
|
+
* client registration, the authorize preflight, token exchange and
|
|
22
|
+
* refresh) defaulted to the raw global `fetch` in every edition, which is
|
|
23
|
+
* the exposure the point closes.
|
|
24
|
+
*
|
|
25
|
+
* The contract every policy is held to:
|
|
26
|
+
*
|
|
27
|
+
* - It judges ADDRESSES, not names: the check resolves a hostname and
|
|
28
|
+
* asks about every address it has, so a name with one public and one
|
|
29
|
+
* private record is refused. A policy that wanted a hostname allowlist
|
|
30
|
+
* would be a different seam.
|
|
31
|
+
* - It is TOTAL: an unparseable address is refused ("unrecognized"),
|
|
32
|
+
* never a throw.
|
|
33
|
+
* - It is SYNCHRONOUS: an edition's egress posture is a fact about where
|
|
34
|
+
* it runs, never I/O.
|
|
35
|
+
*
|
|
36
|
+
* Both constructors are thin over the shared classification in
|
|
37
|
+
* `@stigmer/outbound/egress`, so a composition registers a posture and owns
|
|
38
|
+
* no copy of the ranges; the runner's `web_fetch` guard reads the same
|
|
39
|
+
* table, so the two processes refuse the same addresses under the same
|
|
40
|
+
* posture name.
|
|
41
|
+
*/
|
|
42
|
+
import { egressPolicyForPosture, type EgressPolicy } from "@stigmer/outbound/egress";
|
|
43
|
+
|
|
44
|
+
/** The egress contract (single-instance point, ExtensionDrivers.outboundEgress). */
|
|
45
|
+
export type OutboundEgressPolicy = EgressPolicy;
|
|
46
|
+
|
|
47
|
+
/** Open source's own posture: only the link-local (cloud metadata) range is refused. */
|
|
48
|
+
export function relaxedEgressPolicy(): OutboundEgressPolicy {
|
|
49
|
+
return egressPolicyForPosture("relaxed");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The managed-cloud posture: loopback, private, link-local and unspecified addresses refused. */
|
|
53
|
+
export function strictEgressPolicy(): OutboundEgressPolicy {
|
|
54
|
+
return egressPolicyForPosture("strict");
|
|
55
|
+
}
|
|
@@ -54,6 +54,7 @@ import { V1_VERSION } from "../encryption/v1-codec.js";
|
|
|
54
54
|
import type { AuthorizationQueryEngine } from "./authorization-queries.js";
|
|
55
55
|
import type { IdentityFederation } from "./identity-federation.js";
|
|
56
56
|
import type { LicenseStatusProvider } from "./license-status.js";
|
|
57
|
+
import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
57
58
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
58
59
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
59
60
|
import type { ModelCatalogProvider } from "../domain/workflow/registry/model-catalog-provider.js";
|
|
@@ -278,6 +279,11 @@ export interface ResolvedExtensionDrivers {
|
|
|
278
279
|
* built-in `absent` answer, so every edition serves getLicenseStatus.
|
|
279
280
|
*/
|
|
280
281
|
readonly licenseStatus: LicenseStatusProvider | undefined;
|
|
282
|
+
/**
|
|
283
|
+
* The outbound-egress policy — undefined = compose.ts installs open
|
|
284
|
+
* source's relaxed posture (only the link-local range refused).
|
|
285
|
+
*/
|
|
286
|
+
readonly outboundEgress: OutboundEgressPolicy | undefined;
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
/**
|
|
@@ -333,6 +339,8 @@ export function resolveExtensions(
|
|
|
333
339
|
let authorizationQueriesDeclaredBy: string | undefined;
|
|
334
340
|
let licenseStatus: LicenseStatusProvider | undefined;
|
|
335
341
|
let licenseStatusDeclaredBy: string | undefined;
|
|
342
|
+
let outboundEgress: OutboundEgressPolicy | undefined;
|
|
343
|
+
let outboundEgressDeclaredBy: string | undefined;
|
|
336
344
|
const artifactStorageDrivers = new Map<
|
|
337
345
|
string,
|
|
338
346
|
ArtifactStorageDriverFactory
|
|
@@ -534,6 +542,16 @@ export function resolveExtensions(
|
|
|
534
542
|
licenseStatusDeclaredBy = unit.name;
|
|
535
543
|
}
|
|
536
544
|
|
|
545
|
+
if (unit.drivers?.outboundEgress !== undefined) {
|
|
546
|
+
if (outboundEgressDeclaredBy !== undefined) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`extension '${unit.name}' registers an OutboundEgressPolicy, but '${outboundEgressDeclaredBy}' already did — exactly one may be composed`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
outboundEgress = unit.drivers.outboundEgress;
|
|
552
|
+
outboundEgressDeclaredBy = unit.name;
|
|
553
|
+
}
|
|
554
|
+
|
|
537
555
|
if (unit.drivers?.artifactStorageDrivers !== undefined) {
|
|
538
556
|
for (const [name, factory] of unit.drivers.artifactStorageDrivers) {
|
|
539
557
|
if ((BUILT_IN_STORAGE_TYPES as ReadonlyArray<string>).includes(name)) {
|
|
@@ -660,6 +678,7 @@ export function resolveExtensions(
|
|
|
660
678
|
policyGrantScope,
|
|
661
679
|
authorizationQueries,
|
|
662
680
|
licenseStatus,
|
|
681
|
+
outboundEgress,
|
|
663
682
|
},
|
|
664
683
|
services,
|
|
665
684
|
workers,
|
package/src/index.ts
CHANGED
|
@@ -119,6 +119,16 @@ export type {
|
|
|
119
119
|
LicenseStatusProvider,
|
|
120
120
|
LicenseStatusReport,
|
|
121
121
|
} from "./extensions/license-status.js";
|
|
122
|
+
// The outbound-egress seam (drivers.outboundEgress): which addresses the
|
|
123
|
+
// control plane may dial when it reaches a URL a user supplied. Both
|
|
124
|
+
// postures are exported so a composition registers one line and owns no
|
|
125
|
+
// copy of the address ranges (the classification lives in
|
|
126
|
+
// @stigmer/outbound/egress, shared with the runner's web_fetch guard).
|
|
127
|
+
export type { OutboundEgressPolicy } from "./extensions/outbound-egress.js";
|
|
128
|
+
export {
|
|
129
|
+
relaxedEgressPolicy,
|
|
130
|
+
strictEgressPolicy,
|
|
131
|
+
} from "./extensions/outbound-egress.js";
|
|
122
132
|
// The 20260911.11 identity-account seams (Q-IA-9): the store PORT a
|
|
123
133
|
// composition drives the domain through (drivers.identityAccountStore;
|
|
124
134
|
// a driver throws DuplicateAccountError for a held id), the federation
|
|
@@ -82,6 +82,22 @@ export const PLUGIN_LABEL = `${RESERVED_LABEL_PREFIX}plugin`;
|
|
|
82
82
|
*/
|
|
83
83
|
export const PLUGIN_VERSION_LABEL = `${RESERVED_LABEL_PREFIX}plugin-version`;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* How an MCP server's `spec.auth` came to be. The one value, `endpoint`,
|
|
87
|
+
* says the server itself completed it: the author supplied a URL and
|
|
88
|
+
* nothing about authentication, the endpoint answered the save-time probe
|
|
89
|
+
* with an OAuth challenge, and the server wrote the token variable, its
|
|
90
|
+
* declaration and the Bearer header (domain/mcpserver/complete-endpoint-auth.ts).
|
|
91
|
+
* The label is provenance, not permission: it lets the next save tell a
|
|
92
|
+
* completion it may reuse from an author's own `auth` it must not touch.
|
|
93
|
+
* Stamped through the server-stamped mechanism; dropped when the author
|
|
94
|
+
* writes their own `auth`.
|
|
95
|
+
*/
|
|
96
|
+
export const MCP_AUTH_LABEL = `${RESERVED_LABEL_PREFIX}mcp-auth`;
|
|
97
|
+
|
|
98
|
+
/** The MCP_AUTH_LABEL value for a completion the endpoint's own challenge produced. */
|
|
99
|
+
export const MCP_AUTH_ENDPOINT = "endpoint";
|
|
100
|
+
|
|
85
101
|
/** The plugin id a resource's labels claim, or undefined when unlabelled. */
|
|
86
102
|
export function pluginIdOf(
|
|
87
103
|
metadata: ApiResourceMetadata | undefined,
|
|
@@ -68,6 +68,7 @@ describe.skipIf(!optedIn || !dockerAnswers())(
|
|
|
68
68
|
await driver.ensureSessionSandbox(sessionId, {
|
|
69
69
|
taskQueue: `session:${sessionId}`,
|
|
70
70
|
stigmerToken: "tok-docker-smoke",
|
|
71
|
+
callerClass: "user",
|
|
71
72
|
});
|
|
72
73
|
expect(await driver.probe("session", sessionId)).toBe("running");
|
|
73
74
|
|
|
@@ -91,6 +92,7 @@ describe.skipIf(!optedIn || !dockerAnswers())(
|
|
|
91
92
|
await driver.ensureSessionSandbox(sessionId, {
|
|
92
93
|
taskQueue: `session:${sessionId}`,
|
|
93
94
|
stigmerToken: "tok-docker-smoke",
|
|
95
|
+
callerClass: "user",
|
|
94
96
|
});
|
|
95
97
|
expect(await driver.probe("session", sessionId)).toBe("running");
|
|
96
98
|
|
|
@@ -89,7 +89,11 @@ function fakeGateway(
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const env = {
|
|
92
|
+
const env = {
|
|
93
|
+
taskQueue: "session:ses_1",
|
|
94
|
+
stigmerToken: "tok-1",
|
|
95
|
+
callerClass: "user",
|
|
96
|
+
};
|
|
93
97
|
|
|
94
98
|
describe("the ensure state machine", () => {
|
|
95
99
|
it("absent → Secret, PVC, Deployment — in that order, no readiness wait", async () => {
|
|
@@ -211,7 +215,11 @@ describe("the manifest shapes (the Java SandboxManifestFactory pins)", () => {
|
|
|
211
215
|
});
|
|
212
216
|
|
|
213
217
|
it("a token-less sandbox omits the token env but keeps the (empty) Secret", () => {
|
|
214
|
-
const tokenless = {
|
|
218
|
+
const tokenless = {
|
|
219
|
+
taskQueue: "session:ses_1",
|
|
220
|
+
stigmerToken: "",
|
|
221
|
+
callerClass: "user",
|
|
222
|
+
};
|
|
215
223
|
const deployment = buildSandboxDeployment(
|
|
216
224
|
"session",
|
|
217
225
|
"ses_1",
|
|
@@ -81,6 +81,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
81
81
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
82
82
|
taskQueue: "session:ses_smoke",
|
|
83
83
|
stigmerToken: "tok-smoke",
|
|
84
|
+
callerClass: "user",
|
|
84
85
|
});
|
|
85
86
|
await vi.waitFor(() => expect(spawnCount()).toBe(1), {
|
|
86
87
|
timeout: CHILD_SETTLE_TIMEOUT_MS,
|
|
@@ -91,6 +92,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
91
92
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
92
93
|
taskQueue: "session:ses_smoke",
|
|
93
94
|
stigmerToken: "tok-smoke",
|
|
95
|
+
callerClass: "user",
|
|
94
96
|
});
|
|
95
97
|
expect(spawnCount()).toBe(1);
|
|
96
98
|
|
|
@@ -124,6 +126,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
124
126
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
125
127
|
taskQueue: "session:ses_smoke",
|
|
126
128
|
stigmerToken: "",
|
|
129
|
+
callerClass: "user",
|
|
127
130
|
});
|
|
128
131
|
await vi.waitFor(() => expect(spawnCount()).toBe(2), {
|
|
129
132
|
timeout: CHILD_SETTLE_TIMEOUT_MS,
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* - the terminal observer deprovisions exactly on transitions INTO a
|
|
12
12
|
* terminal phase, fire-and-forget, and swallows (logs) teardown
|
|
13
13
|
* failures;
|
|
14
|
-
* - a disabled mint lane launches token-less rather than failing
|
|
14
|
+
* - a disabled mint lane launches token-less rather than failing;
|
|
15
|
+
* - the caller splits two ways on both lanes: the identity id reaches
|
|
16
|
+
* the credential mint, the caller class reaches the driver's
|
|
17
|
+
* environment unchanged (a composed lane such as `guest` included).
|
|
15
18
|
*/
|
|
16
19
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
20
|
import { tmpdir } from "node:os";
|
|
@@ -53,14 +56,25 @@ import {
|
|
|
53
56
|
ensureWorkflowSandboxForExecution,
|
|
54
57
|
newWorkflowSandboxTerminalObserver,
|
|
55
58
|
SANDBOX_PROVISIONING_FAILED_PREFIX,
|
|
59
|
+
type SandboxCaller,
|
|
56
60
|
} from "../steps.js";
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
|
-
* The caller
|
|
60
|
-
* (C4
|
|
61
|
-
*
|
|
63
|
+
* The caller the ensure bodies split two ways: the identity id into the
|
|
64
|
+
* credential mint (C4 — the OSS execution-scoped mint ignores it, so the
|
|
65
|
+
* token assertions stay binding-shaped) and the class onto the driver's
|
|
66
|
+
* environment. `user` is the plain lane; the class-propagation cases hand
|
|
67
|
+
* in a composed lane's word to prove the body copies, never normalises.
|
|
62
68
|
*/
|
|
63
69
|
const TEST_CALLER_IDENTITY_ID = "ida_test_caller";
|
|
70
|
+
const TEST_CALLER: SandboxCaller = {
|
|
71
|
+
identityId: TEST_CALLER_IDENTITY_ID,
|
|
72
|
+
callerClass: "user",
|
|
73
|
+
};
|
|
74
|
+
const GUEST_CALLER: SandboxCaller = {
|
|
75
|
+
identityId: "ida_guest_caller",
|
|
76
|
+
callerClass: "guest",
|
|
77
|
+
};
|
|
64
78
|
|
|
65
79
|
const silentLogger = createLogger({
|
|
66
80
|
level: "error",
|
|
@@ -235,7 +249,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
235
249
|
temporalConfig: sessionRoutingCloudDefault,
|
|
236
250
|
},
|
|
237
251
|
execution,
|
|
238
|
-
|
|
252
|
+
TEST_CALLER,
|
|
239
253
|
);
|
|
240
254
|
expect(provisioner.ensured).toEqual([
|
|
241
255
|
{
|
|
@@ -244,11 +258,32 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
244
258
|
env: {
|
|
245
259
|
taskQueue: `session:${sessionId}`,
|
|
246
260
|
stigmerToken: `tok-${execution.metadata?.id ?? ""}`,
|
|
261
|
+
callerClass: "user",
|
|
247
262
|
},
|
|
248
263
|
},
|
|
249
264
|
]);
|
|
250
265
|
});
|
|
251
266
|
|
|
267
|
+
it("hands the caller's class to the driver unchanged and the caller's id to the mint", async () => {
|
|
268
|
+
const provisioner = fakeProvisioner();
|
|
269
|
+
const credentials = capabilityCredentials();
|
|
270
|
+
const { execution } = await seed(ExecutionTarget.CLOUD);
|
|
271
|
+
await ensureSessionSandboxForExecution(
|
|
272
|
+
{
|
|
273
|
+
store,
|
|
274
|
+
logger: silentLogger,
|
|
275
|
+
lane: lane(provisioner, credentials),
|
|
276
|
+
temporalConfig: sessionRoutingCloudDefault,
|
|
277
|
+
},
|
|
278
|
+
execution,
|
|
279
|
+
GUEST_CALLER,
|
|
280
|
+
);
|
|
281
|
+
expect(provisioner.ensured[0]?.env.callerClass).toBe("guest");
|
|
282
|
+
expect(credentials.minted[0]?.callerIdentityId).toBe(
|
|
283
|
+
GUEST_CALLER.identityId,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
252
287
|
it("delegates the mint to the capability provider with the full provisioning context (C4)", async () => {
|
|
253
288
|
const provisioner = fakeProvisioner();
|
|
254
289
|
const credentials = capabilityCredentials();
|
|
@@ -277,7 +312,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
277
312
|
temporalConfig: sessionRoutingCloudDefault,
|
|
278
313
|
},
|
|
279
314
|
execution,
|
|
280
|
-
|
|
315
|
+
TEST_CALLER,
|
|
281
316
|
);
|
|
282
317
|
|
|
283
318
|
expect(credentials.minted).toEqual([
|
|
@@ -303,7 +338,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
303
338
|
temporalConfig: sessionRoutingCloudDefault,
|
|
304
339
|
},
|
|
305
340
|
execution,
|
|
306
|
-
|
|
341
|
+
TEST_CALLER,
|
|
307
342
|
);
|
|
308
343
|
expect(provisioner.ensured).toEqual([]);
|
|
309
344
|
});
|
|
@@ -327,7 +362,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
327
362
|
temporalConfig: sessionRoutingCloudDefault,
|
|
328
363
|
},
|
|
329
364
|
execution,
|
|
330
|
-
|
|
365
|
+
TEST_CALLER,
|
|
331
366
|
);
|
|
332
367
|
});
|
|
333
368
|
|
|
@@ -345,7 +380,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
345
380
|
temporalConfig: sessionRoutingCloudDefault,
|
|
346
381
|
},
|
|
347
382
|
execution,
|
|
348
|
-
|
|
383
|
+
TEST_CALLER,
|
|
349
384
|
);
|
|
350
385
|
expect(provisioner.ensured).toEqual([]);
|
|
351
386
|
});
|
|
@@ -366,7 +401,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
366
401
|
),
|
|
367
402
|
},
|
|
368
403
|
execution,
|
|
369
|
-
|
|
404
|
+
TEST_CALLER,
|
|
370
405
|
);
|
|
371
406
|
expect(provisioner.ensured).toEqual([]);
|
|
372
407
|
});
|
|
@@ -382,7 +417,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
382
417
|
temporalConfig: sessionRoutingCloudDefault,
|
|
383
418
|
},
|
|
384
419
|
execution,
|
|
385
|
-
|
|
420
|
+
TEST_CALLER,
|
|
386
421
|
);
|
|
387
422
|
expect(provisioner.ensured[0]?.env.stigmerToken).toBe("");
|
|
388
423
|
});
|
|
@@ -401,7 +436,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
401
436
|
temporalConfig: sessionRoutingCloudDefault,
|
|
402
437
|
},
|
|
403
438
|
execution,
|
|
404
|
-
|
|
439
|
+
TEST_CALLER,
|
|
405
440
|
);
|
|
406
441
|
const stamped = await store.getResource(
|
|
407
442
|
ApiResourceKind.agent_execution,
|
|
@@ -439,7 +474,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
439
474
|
temporalConfig: sessionRoutingCloudDefault,
|
|
440
475
|
},
|
|
441
476
|
execution,
|
|
442
|
-
|
|
477
|
+
TEST_CALLER,
|
|
443
478
|
);
|
|
444
479
|
const after = await store.getResource(
|
|
445
480
|
ApiResourceKind.agent_execution,
|
|
@@ -467,17 +502,35 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
467
502
|
temporalConfig: executionRoutingConfig,
|
|
468
503
|
},
|
|
469
504
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
470
|
-
|
|
505
|
+
TEST_CALLER,
|
|
471
506
|
);
|
|
472
507
|
expect(provisioner.ensured).toEqual([
|
|
473
508
|
{
|
|
474
509
|
scope: "workflow",
|
|
475
510
|
id: "wfx_sbx_1",
|
|
476
|
-
env: {
|
|
511
|
+
env: {
|
|
512
|
+
taskQueue: "wfexec:wfx_sbx_1",
|
|
513
|
+
stigmerToken: "tok-wfx_sbx_1",
|
|
514
|
+
callerClass: "user",
|
|
515
|
+
},
|
|
477
516
|
},
|
|
478
517
|
]);
|
|
479
518
|
});
|
|
480
519
|
|
|
520
|
+
it("hands the caller's class to the driver unchanged on the workflow lane", async () => {
|
|
521
|
+
const provisioner = fakeProvisioner();
|
|
522
|
+
await ensureWorkflowSandboxForExecution(
|
|
523
|
+
{
|
|
524
|
+
logger: silentLogger,
|
|
525
|
+
lane: lane(provisioner),
|
|
526
|
+
temporalConfig: executionRoutingConfig,
|
|
527
|
+
},
|
|
528
|
+
workflowExecution(ExecutionTarget.CLOUD),
|
|
529
|
+
GUEST_CALLER,
|
|
530
|
+
);
|
|
531
|
+
expect(provisioner.ensured[0]?.env.callerClass).toBe("guest");
|
|
532
|
+
});
|
|
533
|
+
|
|
481
534
|
it("delegates the mint to the capability provider on the workflow scope (C4)", async () => {
|
|
482
535
|
const provisioner = fakeProvisioner();
|
|
483
536
|
const credentials = capabilityCredentials();
|
|
@@ -491,7 +544,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
491
544
|
metadata: { id: "wfx_sbx_cap", name: "wfx_sbx_cap", org: "org-test" },
|
|
492
545
|
spec: { executionTarget: ExecutionTarget.CLOUD },
|
|
493
546
|
}),
|
|
494
|
-
|
|
547
|
+
TEST_CALLER,
|
|
495
548
|
);
|
|
496
549
|
expect(credentials.minted).toEqual([
|
|
497
550
|
{
|
|
@@ -514,7 +567,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
514
567
|
temporalConfig: executionRoutingConfig,
|
|
515
568
|
},
|
|
516
569
|
workflowExecution(ExecutionTarget.LOCAL),
|
|
517
|
-
|
|
570
|
+
TEST_CALLER,
|
|
518
571
|
);
|
|
519
572
|
await ensureWorkflowSandboxForExecution(
|
|
520
573
|
{
|
|
@@ -528,7 +581,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
528
581
|
),
|
|
529
582
|
},
|
|
530
583
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
531
|
-
|
|
584
|
+
TEST_CALLER,
|
|
532
585
|
);
|
|
533
586
|
expect(provisioner.ensured).toEqual([]);
|
|
534
587
|
});
|
|
@@ -545,7 +598,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
545
598
|
temporalConfig: executionRoutingConfig,
|
|
546
599
|
},
|
|
547
600
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
548
|
-
|
|
601
|
+
TEST_CALLER,
|
|
549
602
|
);
|
|
550
603
|
expect.unreachable("the workflow lane must throw on failure");
|
|
551
604
|
} catch (error) {
|
|
@@ -85,7 +85,14 @@ const RUNNER_RESOURCES = {
|
|
|
85
85
|
limits: { cpu: "2", memory: "2Gi" },
|
|
86
86
|
} as const;
|
|
87
87
|
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The persistent-workspace scopes (SandboxScope.persistentWorkspace).
|
|
90
|
+
* Durability here is decided by SCOPE alone: this driver reads neither the
|
|
91
|
+
* environment's caller class nor any per-session policy, so every session
|
|
92
|
+
* keeps its PVC whoever asked. A driver that gives some caller classes a
|
|
93
|
+
* workspace that dies with the conversation is a composition's choice
|
|
94
|
+
* built on SandboxEnvironment.callerClass, not a built-in behaviour.
|
|
95
|
+
*/
|
|
89
96
|
const PERSISTENT_SCOPES: ReadonlySet<SandboxScope> = new Set([
|
|
90
97
|
"session",
|
|
91
98
|
"workflow",
|
|
@@ -39,13 +39,16 @@
|
|
|
39
39
|
* runner process polls the queues.
|
|
40
40
|
*/
|
|
41
41
|
import type { Logger } from "../boot/logger.js";
|
|
42
|
+
import type { CallerClass } from "../extensions/identity.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
* sandboxed runner must poll
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* What is TRUE about one provisioning request, handed to a driver: which
|
|
46
|
+
* queue the sandboxed runner must poll, the credential it authenticates
|
|
47
|
+
* with, and the class of the caller whose request is being served. Facts
|
|
48
|
+
* only — what a driver DOES with them (the workspace's durability, the
|
|
49
|
+
* pod's shape, whether a warm pool is consulted) is driver policy and
|
|
50
|
+
* never travels here. Endpoints, images and resource shapes are driver
|
|
51
|
+
* configuration, not per-sandbox state.
|
|
49
52
|
*/
|
|
50
53
|
export interface SandboxEnvironment {
|
|
51
54
|
/** The Temporal task queue the sandboxed runner polls (session:{id} / wfexec:{id}). */
|
|
@@ -56,6 +59,18 @@ export interface SandboxEnvironment {
|
|
|
56
59
|
* and EC decrypt falls back to redaction, the oss#535 posture.
|
|
57
60
|
*/
|
|
58
61
|
readonly stigmerToken: string;
|
|
62
|
+
/**
|
|
63
|
+
* The class of the caller whose request provisioned this sandbox — the
|
|
64
|
+
* identity interceptor's word for it (`user`, `internal`, a composed
|
|
65
|
+
* verifier's own lane such as a guest or a scheduled fire). A driver
|
|
66
|
+
* that gives some session classes a workspace that does not outlive the
|
|
67
|
+
* conversation reads it here; the built-in drivers (`local-process`,
|
|
68
|
+
* `docker`, `kubernetes`) ignore it and keep every session persistent.
|
|
69
|
+
* Required rather than optional so a construction site that forgets it
|
|
70
|
+
* fails to compile instead of silently reading as a persistent user
|
|
71
|
+
* session to every driver.
|
|
72
|
+
*/
|
|
73
|
+
readonly callerClass: CallerClass;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/** One sandbox's observed live state (the Q5 probe result). */
|