@vellumai/assistant 0.12.5-dev.202609252115.c8e1713 → 0.12.5-dev.202609252314.2f8433a

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.
Files changed (38) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  12. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  13. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  14. package/package.json +1 -1
  15. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  16. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  17. package/src/cli/commands/browser.ts +1 -2
  18. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  19. package/src/cli/lib/plugin-artifact.ts +3 -2
  20. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  21. package/src/config/feature-flag-registry.json +1 -1
  22. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  23. package/src/inbound/oauth-callback-url.ts +30 -1
  24. package/src/inbound/public-ingress-urls.ts +12 -2
  25. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  26. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  27. package/src/mcp/mcp-oauth-provider.ts +66 -49
  28. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +262 -0
  29. package/src/monitoring/oom-kill-reporter.ts +345 -0
  30. package/src/monitoring/resource-sampler.ts +8 -0
  31. package/src/runtime/routes/notification-routes.ts +1 -2
  32. package/src/runtime/verification-templates.ts +1 -2
  33. package/src/tools/browser/browser-execution.ts +6 -5
  34. package/src/tools/executor.ts +1 -2
  35. package/src/tools/network/web-fetch.ts +2 -4
  36. package/src/tools/network/web-search.ts +11 -8
  37. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  38. package/src/tools/ui-surface/visual-validation.ts +1 -2
package/ARCHITECTURE.md CHANGED
@@ -49,6 +49,8 @@ Resource pressure monitoring reports sustained CPU/memory pressure on platform-h
49
49
 
50
50
  When the container hits its memory limit, the kernel kills the process with the highest `oom_score` (memory share plus `oom_score_adj`). `src/util/oom-priority.ts` steers that choice so the assistant is never the victim of a runaway tool: the daemon sets itself to -700 as the first thing in `runDaemon`, before any spawn. Children inherit the parent's value at fork, so every spawn path resets its child: shell children get +1000 from the Linux prefix in `buildShellInvocation` (`packages/environments`), desktop children and ACP agents and terminals get +1000 from `withOomScoreAdj`, an `sh -c ... exec` wrapper that keeps pid and process group, and workers, the embedding worker, and Qdrant get 0 from a parent-side write right after spawn (the resource monitor gets -500 so it outlives everything but the daemon and records the kill). The monitor also sweeps the daemon's descendants every few seconds (`src/monitoring/oom-inheritance-sweep.ts`) and writes 0 over any negative value it finds, logging the comm, so a spawn path that forgets the reset degrades to a few seconds of inherited protection instead of a protected runaway. Bundled plugins (the memory plugin's rerank worker, for one) rely on the sweep rather than importing the helper, since the plugin import boundary forbids reaching outside the plugin. Raising never needs a capability; lowering needs `CAP_SYS_RESOURCE`, which the platform grants and self-hosted Docker does not, where the daemon simply stays at 0. Nothing here caps a single process or acts early: it only decides who dies once the kernel has to kill.
51
51
 
52
+ **Verification telemetry:** the resource monitor reports kernel OOM kills in the container as a `watchdog` event with `check_name` `oom_kill` (`src/monitoring/oom-kill-reporter.ts`). It reads the kernel log through `dmesg` (the `/dev/kmsg` device is not mounted in the container; `dmesg` uses the syslog syscall, which the Kata profile permits) when the cgroup `memory.events` kill counter moves, on its first scan after start (the kill that took the daemon down restarts the monitor with it), and once a minute as a fallback. The kernel log is shared by every container on that kernel and cgroup paths read `/` from inside, so attribution is by the counter: a move of N claims the N most recent new kills; the first scan and hosts without the counter claim every new kill and say so with `attribution: "kernel_log"`. The `detail` bag lists each victim's comm, `oom_score_adj` and resident size, the cgroup memory figures at the time, and `killed_daemon`, detected by the daemon's -700 score rather than a pid the restart has since replaced; where the log is unreadable a counter move still yields a victimless event. A cursor (kernel boot id plus last log timestamp) on the monitor data dir advances only after the event is queued, so a telemetry store outage delays a report rather than losing one, and resets when the kernel reboots.
53
+
52
54
  ### Single-Header JWT Auth Model
53
55
 
54
56
  All HTTP API requests use a single `Authorization: Bearer <jwt>` header for authentication. The JWT carries identity, permissions, and policy versioning in a unified token.
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";
@@ -0,0 +1,60 @@
1
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
2
+
3
+ export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
4
+ export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
5
+ export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
6
+
7
+ export interface McpOAuthClientMetadata {
8
+ client_id?: string;
9
+ redirect_uris: string[];
10
+ token_endpoint_auth_method: "none";
11
+ grant_types: ["authorization_code", "refresh_token"];
12
+ response_types: ["code"];
13
+ client_name: string;
14
+ logo_uri: string;
15
+ software_id?: string;
16
+ software_version?: string;
17
+ }
18
+
19
+ export interface BuildMcpOAuthClientMetadataOptions {
20
+ clientId?: string;
21
+ redirectUris: string[];
22
+ clientName?: string;
23
+ softwareId?: string;
24
+ softwareVersion?: string;
25
+ }
26
+
27
+ export function buildMcpOAuthClientMetadata(
28
+ options: BuildMcpOAuthClientMetadataOptions,
29
+ ): McpOAuthClientMetadata {
30
+ return {
31
+ ...(options.clientId && { client_id: options.clientId }),
32
+ redirect_uris: options.redirectUris,
33
+ token_endpoint_auth_method: "none",
34
+ grant_types: ["authorization_code", "refresh_token"],
35
+ response_types: ["code"],
36
+ client_name: options.clientName ?? "Vellum Assistant",
37
+ logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
38
+ ...(options.softwareId && { software_id: options.softwareId }),
39
+ ...(options.softwareVersion && {
40
+ software_version: options.softwareVersion,
41
+ }),
42
+ };
43
+ }
44
+
45
+ export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
46
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
47
+ }
48
+
49
+ export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
50
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
51
+ }
52
+
53
+ function normalizeBaseUrl(publicBaseUrl: string): string {
54
+ const normalized =
55
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
56
+ if (!normalized) {
57
+ throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
58
+ }
59
+ return normalized;
60
+ }
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";
@@ -0,0 +1,60 @@
1
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
2
+
3
+ export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
4
+ export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
5
+ export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
6
+
7
+ export interface McpOAuthClientMetadata {
8
+ client_id?: string;
9
+ redirect_uris: string[];
10
+ token_endpoint_auth_method: "none";
11
+ grant_types: ["authorization_code", "refresh_token"];
12
+ response_types: ["code"];
13
+ client_name: string;
14
+ logo_uri: string;
15
+ software_id?: string;
16
+ software_version?: string;
17
+ }
18
+
19
+ export interface BuildMcpOAuthClientMetadataOptions {
20
+ clientId?: string;
21
+ redirectUris: string[];
22
+ clientName?: string;
23
+ softwareId?: string;
24
+ softwareVersion?: string;
25
+ }
26
+
27
+ export function buildMcpOAuthClientMetadata(
28
+ options: BuildMcpOAuthClientMetadataOptions,
29
+ ): McpOAuthClientMetadata {
30
+ return {
31
+ ...(options.clientId && { client_id: options.clientId }),
32
+ redirect_uris: options.redirectUris,
33
+ token_endpoint_auth_method: "none",
34
+ grant_types: ["authorization_code", "refresh_token"],
35
+ response_types: ["code"],
36
+ client_name: options.clientName ?? "Vellum Assistant",
37
+ logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
38
+ ...(options.softwareId && { software_id: options.softwareId }),
39
+ ...(options.softwareVersion && {
40
+ software_version: options.softwareVersion,
41
+ }),
42
+ };
43
+ }
44
+
45
+ export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
46
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
47
+ }
48
+
49
+ export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
50
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
51
+ }
52
+
53
+ function normalizeBaseUrl(publicBaseUrl: string): string {
54
+ const normalized =
55
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
56
+ if (!normalized) {
57
+ throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
58
+ }
59
+ return normalized;
60
+ }
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";
@@ -0,0 +1,60 @@
1
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
2
+
3
+ export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
4
+ export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
5
+ export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
6
+
7
+ export interface McpOAuthClientMetadata {
8
+ client_id?: string;
9
+ redirect_uris: string[];
10
+ token_endpoint_auth_method: "none";
11
+ grant_types: ["authorization_code", "refresh_token"];
12
+ response_types: ["code"];
13
+ client_name: string;
14
+ logo_uri: string;
15
+ software_id?: string;
16
+ software_version?: string;
17
+ }
18
+
19
+ export interface BuildMcpOAuthClientMetadataOptions {
20
+ clientId?: string;
21
+ redirectUris: string[];
22
+ clientName?: string;
23
+ softwareId?: string;
24
+ softwareVersion?: string;
25
+ }
26
+
27
+ export function buildMcpOAuthClientMetadata(
28
+ options: BuildMcpOAuthClientMetadataOptions,
29
+ ): McpOAuthClientMetadata {
30
+ return {
31
+ ...(options.clientId && { client_id: options.clientId }),
32
+ redirect_uris: options.redirectUris,
33
+ token_endpoint_auth_method: "none",
34
+ grant_types: ["authorization_code", "refresh_token"],
35
+ response_types: ["code"],
36
+ client_name: options.clientName ?? "Vellum Assistant",
37
+ logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
38
+ ...(options.softwareId && { software_id: options.softwareId }),
39
+ ...(options.softwareVersion && {
40
+ software_version: options.softwareVersion,
41
+ }),
42
+ };
43
+ }
44
+
45
+ export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
46
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
47
+ }
48
+
49
+ export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
50
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
51
+ }
52
+
53
+ function normalizeBaseUrl(publicBaseUrl: string): string {
54
+ const normalized =
55
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
56
+ if (!normalized) {
57
+ throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
58
+ }
59
+ return normalized;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.5-dev.202609252115.c8e1713",
3
+ "version": "0.12.5-dev.202609252314.2f8433a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,6 +48,8 @@ let mockOAuthCallbackUrl = "";
48
48
  // getPublicBaseUrl.
49
49
  mock.module("../inbound/public-ingress-urls.js", () => ({
50
50
  getOAuthCallbackUrl: () => mockOAuthCallbackUrl,
51
+ getMcpOAuthClientMetadataUrl: () =>
52
+ "http://localhost/oauth/client-metadata.json",
51
53
  getPublicBaseUrl: (config?: { ingress?: { publicBaseUrl?: string } }) => {
52
54
  const url = config?.ingress?.publicBaseUrl ?? "";
53
55
  if (!url) {
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
6
  import { setIngressPublicBaseUrl } from "../config/env.js";
7
7
  import { IngressConfigSchema } from "../config/schemas/ingress.js";
8
8
  import {
9
+ getMcpOAuthClientMetadataUrl,
9
10
  getOAuthCallbackUrl,
10
11
  getPublicBaseUrl,
11
12
  getTelegramWebhookUrl,
@@ -307,6 +308,31 @@ describe("getOAuthCallbackUrl", () => {
307
308
  });
308
309
  expect(url).toBe("https://example.com/webhooks/oauth/callback");
309
310
  });
311
+
312
+ test("canonicalizes the base before building OAuth URLs", () => {
313
+ const config = {
314
+ ingress: {
315
+ publicBaseUrl: "https://EXAMPLE.com:443/a/../assistant-123///",
316
+ },
317
+ };
318
+
319
+ expect(getOAuthCallbackUrl(config)).toBe(
320
+ "https://example.com/assistant-123/webhooks/oauth/callback",
321
+ );
322
+ expect(getMcpOAuthClientMetadataUrl(config)).toBe(
323
+ "https://example.com/assistant-123/oauth/client-metadata.json",
324
+ );
325
+ });
326
+ });
327
+
328
+ describe("getMcpOAuthClientMetadataUrl", () => {
329
+ test("builds the public metadata URL", () => {
330
+ expect(
331
+ getMcpOAuthClientMetadataUrl({
332
+ ingress: { publicBaseUrl: "https://example.com/assistant-123/" },
333
+ }),
334
+ ).toBe("https://example.com/assistant-123/oauth/client-metadata.json");
335
+ });
310
336
  });
311
337
 
312
338
  // ---------------------------------------------------------------------------
@@ -214,8 +214,7 @@ function attachOperationAction(
214
214
  opts.output &&
215
215
  result.screenshots?.length
216
216
  ) {
217
- const screenshot = result.screenshots[0];
218
- const buffer = Buffer.from(screenshot.data, "base64");
217
+ const buffer = Buffer.from(result.screenshots[0].data, "base64");
219
218
  try {
220
219
  writeFileSync(String(opts.output), buffer);
221
220
  } catch (err) {
@@ -33,6 +33,7 @@ describe("filterPluginCatalogByFeatureFlags", () => {
33
33
  "ramp",
34
34
  "semrush",
35
35
  "typeform",
36
+ "upwork",
36
37
  "notion",
37
38
  ]);
38
39
 
@@ -56,6 +57,7 @@ describe("filterPluginCatalogByFeatureFlags", () => {
56
57
  "ramp",
57
58
  "semrush",
58
59
  "typeform",
60
+ "upwork",
59
61
  ]);
60
62
 
61
63
  const result = filterPluginCatalogByFeatureFlags(input, () => true);
@@ -89,8 +89,9 @@ export function parsePluginArtifact(
89
89
  if (typeof vellum !== "object" || vellum === null || Array.isArray(vellum)) {
90
90
  return null;
91
91
  }
92
- const artifact = (vellum as Record<string, unknown>).artifact;
93
- const parsed = PluginArtifactSchema.safeParse(artifact);
92
+ const parsed = PluginArtifactSchema.safeParse(
93
+ (vellum as Record<string, unknown>).artifact,
94
+ );
94
95
  if (!parsed.success) {
95
96
  return null;
96
97
  }
@@ -10,6 +10,7 @@ const MCP_CATALOG_QA_INTEGRATION_NAMES = new Set([
10
10
  "ramp",
11
11
  "semrush",
12
12
  "typeform",
13
+ "upwork",
13
14
  ]);
14
15
 
15
16
  export type PluginCatalogFeatureFlagResolver = (key: string) => boolean;
@@ -343,7 +343,7 @@
343
343
  "scope": "assistant",
344
344
  "key": "mcp-catalog-qa-integrations",
345
345
  "label": "MCP Catalog QA Integrations",
346
- "description": "Show Gamma, Guru, Intercom, Ramp, Semrush, and Typeform in the installable plugin catalog while their OAuth flows are validated. When off, catalog search, detail, and install-by-name cannot resolve them. Existing installed connections remain available.",
346
+ "description": "Show Gamma, Guru, Intercom, Ramp, Semrush, Typeform, and Upwork in the installable plugin catalog while their OAuth flows are validated. When off, catalog search, detail, and install-by-name cannot resolve them. Existing installed connections remain available.",
347
347
  "defaultEnabled": false
348
348
  },
349
349
  {
@@ -21,19 +21,42 @@ mock.module("../platform-callback-registration.js", () => ({
21
21
 
22
22
  mock.module("../public-ingress-urls.js", () => ({
23
23
  getOAuthCallbackUrl: () => "https://direct.example/webhooks/oauth/callback",
24
+ getMcpOAuthClientMetadataUrl: () =>
25
+ "https://direct.example/oauth/client-metadata.json",
24
26
  }));
25
27
 
26
28
  mock.module("../../config/loader.js", () => ({
27
29
  loadConfig: () => ({}),
28
30
  }));
29
31
 
30
- const { OAUTH_CALLBACK_PATH, resolveOauthCallbackUrl } =
31
- await import("../oauth-callback-url.js");
32
+ const {
33
+ OAUTH_CALLBACK_PATH,
34
+ resolveMcpOAuthClientMetadataUrl,
35
+ resolveOauthCallbackUrl,
36
+ } = await import("../oauth-callback-url.js");
32
37
 
33
38
  afterEach(() => {
34
39
  resolveCallbackUrl.mockClear();
35
40
  });
36
41
 
42
+ describe("resolveMcpOAuthClientMetadataUrl", () => {
43
+ test("returns the metadata URL for the exact direct HTTPS callback", () => {
44
+ expect(
45
+ resolveMcpOAuthClientMetadataUrl(
46
+ "https://direct.example/webhooks/oauth/callback",
47
+ ),
48
+ ).toBe("https://direct.example/oauth/client-metadata.json");
49
+ });
50
+
51
+ test("returns undefined for a callback served somewhere else", () => {
52
+ expect(
53
+ resolveMcpOAuthClientMetadataUrl(
54
+ "https://platform.example/v1/gateway/callbacks/assistant-123/webhooks/oauth/callback",
55
+ ),
56
+ ).toBeUndefined();
57
+ });
58
+ });
59
+
37
60
  describe("resolveOauthCallbackUrl", () => {
38
61
  test("returns whatever the shared resolver decides", async () => {
39
62
  await expect(resolveOauthCallbackUrl()).resolves.toEqual(
@@ -18,7 +18,10 @@
18
18
 
19
19
  import { loadConfig } from "../config/loader.js";
20
20
  import { resolveCallbackUrl } from "./platform-callback-registration.js";
21
- import { getOAuthCallbackUrl } from "./public-ingress-urls.js";
21
+ import {
22
+ getMcpOAuthClientMetadataUrl,
23
+ getOAuthCallbackUrl,
24
+ } from "./public-ingress-urls.js";
22
25
 
23
26
  /**
24
27
  * Path the gateway serves the shared OAuth callback on.
@@ -59,3 +62,29 @@ export async function resolveOauthCallbackUrl(): Promise<string> {
59
62
  OAUTH_REGISTRATION_TYPE,
60
63
  );
61
64
  }
65
+
66
+ /**
67
+ * Return the public Client ID Metadata Document URL when it is compatible
68
+ * with the callback selected for this flow.
69
+ *
70
+ * A URL-based client id must be HTTPS, and its document must publish the exact
71
+ * redirect URI used by the authorization request. The gateway document is
72
+ * derived from public ingress, so a platform-relayed callback cannot use it.
73
+ * Returning undefined preserves Dynamic Client Registration as the SDK's
74
+ * fallback in either case.
75
+ */
76
+ export function resolveMcpOAuthClientMetadataUrl(
77
+ redirectUrl: string,
78
+ ): string | undefined {
79
+ try {
80
+ const config = loadConfig();
81
+ if (getOAuthCallbackUrl(config) !== redirectUrl) {
82
+ return undefined;
83
+ }
84
+
85
+ const metadataUrl = getMcpOAuthClientMetadataUrl(config);
86
+ return new URL(metadataUrl).protocol === "https:" ? metadataUrl : undefined;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
@@ -27,6 +27,10 @@
27
27
  * All public-facing ingress URL construction is centralized here.
28
28
  */
29
29
 
30
+ import {
31
+ buildMcpOAuthCallbackUrl,
32
+ buildMcpOAuthClientMetadataUrl,
33
+ } from "@vellumai/service-contracts/mcp-oauth";
30
34
  import {
31
35
  buildTwilioMediaStreamUrl,
32
36
  buildTwilioStatusWebhookUrl,
@@ -160,8 +164,14 @@ export function getTwilioMediaStreamUrl(config: IngressConfig): string {
160
164
  * Build the OAuth callback URL.
161
165
  */
162
166
  export function getOAuthCallbackUrl(config: IngressConfig): string {
163
- const base = getPublicBaseUrl(config);
164
- return `${base}/webhooks/oauth/callback`;
167
+ return buildMcpOAuthCallbackUrl(getPublicBaseUrl(config));
168
+ }
169
+
170
+ /**
171
+ * Build the public Client ID Metadata Document URL for MCP OAuth.
172
+ */
173
+ export function getMcpOAuthClientMetadataUrl(config: IngressConfig): string {
174
+ return buildMcpOAuthClientMetadataUrl(getPublicBaseUrl(config));
165
175
  }
166
176
 
167
177
  /**