diff --git a/hub/package-lock.json b/hub/package-lock.json index f569594..7f2ae60 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -1,12 +1,12 @@ { "name": "@paradigm/hub", - "version": "0.0.18", + "version": "0.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@paradigm/hub", - "version": "0.0.18", + "version": "0.0.19", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", diff --git a/hub/package.json b/hub/package.json index 6126130..ce580e1 100644 --- a/hub/package.json +++ b/hub/package.json @@ -1,6 +1,6 @@ { "name": "@paradigm/hub", - "version": "0.0.18", + "version": "0.0.19", "private": true, "type": "module", "engines": { diff --git a/hub/src/agent/security.ts b/hub/src/agent/security.ts index 7601c3e..89d0118 100644 --- a/hub/src/agent/security.ts +++ b/hub/src/agent/security.ts @@ -8,6 +8,7 @@ const PROVIDER_ENV_KEYS = new Set([ "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY", + "ANTHROPIC_CUSTOM_HEADERS", ]); const SAFE_HOST_ENV_KEYS = [ diff --git a/hub/src/connections/providerProxy.ts b/hub/src/connections/providerProxy.ts index ab89e8f..abc70e6 100644 --- a/hub/src/connections/providerProxy.ts +++ b/hub/src/connections/providerProxy.ts @@ -20,6 +20,7 @@ const REPLACED_REQUEST_HEADERS = new Set([ "content-length", "host", "x-api-key", + "x-cph-run-capability", ]); export interface ProviderUpstreamCredential { @@ -44,6 +45,7 @@ export interface ProviderProxyDiagnostic { readonly authShape?: { readonly bearerLength: number; readonly apiKeyLength: number; + readonly customCapabilityLength: number; readonly expectedLength: number; }; } @@ -87,6 +89,7 @@ export async function openProviderProxyLease( ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`, ANTHROPIC_AUTH_TOKEN: capability, ANTHROPIC_API_KEY: capability, + ANTHROPIC_CUSTOM_HEADERS: `X-CPH-Run-Capability: ${capability}`, }, sensitiveValues: [capability], async close(): Promise { @@ -107,7 +110,12 @@ async function forwardProviderRequest( upstream: ProviderUpstreamCredential, options: ProviderProxyOptions, ): Promise { - if (!authorized(request.headers.authorization, header(request.headers["x-api-key"]), capability)) { + if (!authorized( + request.headers.authorization, + header(request.headers["x-api-key"]), + header(request.headers["x-cph-run-capability"]), + capability, + )) { options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization", @@ -116,6 +124,7 @@ async function forwardProviderRequest( ? request.headers.authorization.length - "Bearer ".length : 0, apiKeyLength: header(request.headers["x-api-key"])?.length ?? 0, + customCapabilityLength: header(request.headers["x-cph-run-capability"])?.length ?? 0, expectedLength: capability.length, }, }); @@ -183,11 +192,16 @@ async function forwardProviderRequest( function authorized( authorization: string | undefined, apiKey: string | undefined, + customCapability: string | undefined, capability: string, ): boolean { - const value = authorization?.startsWith("Bearer ") + const bearer = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length) - : apiKey; + : undefined; + return [bearer, apiKey, customCapability].some((value) => matchesCapability(value, capability)); +} + +function matchesCapability(value: string | undefined, capability: string): boolean { if (value === undefined) return false; const supplied = Buffer.from(value); const expected = Buffer.from(capability); diff --git a/hub/test/unit/provider-proxy.test.ts b/hub/test/unit/provider-proxy.test.ts index bf4e6b7..91a10b0 100644 --- a/hub/test/unit/provider-proxy.test.ts +++ b/hub/test/unit/provider-proxy.test.ts @@ -54,6 +54,7 @@ describe("run-scoped provider proxy", () => { ANTHROPIC_BASE_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+$/), ANTHROPIC_AUTH_TOKEN: expect.any(String), ANTHROPIC_API_KEY: expect.any(String), + ANTHROPIC_CUSTOM_HEADERS: expect.stringMatching(/^X-CPH-Run-Capability: /), }); expect(lease.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(lease.sdkEnv.ANTHROPIC_API_KEY); @@ -108,7 +109,7 @@ describe("run-scoped provider proxy", () => { expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization", - authShape: { bearerLength: 0, apiKeyLength: 0, expectedLength: 43 }, + authShape: { bearerLength: 0, apiKeyLength: 0, customCapabilityLength: 0, expectedLength: 43 }, }]); });