Compare commits

..

2 Commits

Author SHA1 Message Date
hongjr03 3087132083 fix: carry provider capability in dedicated header 2026-07-11 15:02:51 +08:00
hongjr03 63c86322de fix: preserve run provider capability 2026-07-11 15:00:34 +08:00
7 changed files with 28 additions and 21 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@paradigm/hub",
"version": "0.0.17",
"version": "0.0.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@paradigm/hub",
"version": "0.0.17",
"version": "0.0.19",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
"@fastify/cookie": "^11.0.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@paradigm/hub",
"version": "0.0.17",
"version": "0.0.19",
"private": true,
"type": "module",
"engines": {
+4 -6
View File
@@ -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 = [
@@ -23,11 +24,6 @@ const SAFE_HOST_ENV_KEYS = [
"CPH_BIN",
] as const;
const SANDBOX_HIDDEN_ENV_KEYS = [
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
] as const;
// Linux sockaddr_un.sun_path is 108 bytes including the terminator. Claude's
// sandbox appends its own user directory and randomized bridge socket names,
// so keep our prefix well below that hard limit.
@@ -156,7 +152,9 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
},
credentials: {
files: sensitiveReadPaths.map((path) => ({ path, mode: "deny" as const })),
envVars: SANDBOX_HIDDEN_ENV_KEYS.map((name) => ({ name, mode: "deny" as const })),
// These values are short-lived loopback capabilities, not Organization
// provider secrets. Denying them also strips Claude's own request auth.
envVars: [],
},
},
};
+17 -3
View File
@@ -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<void> {
@@ -107,7 +110,12 @@ async function forwardProviderRequest(
upstream: ProviderUpstreamCredential,
options: ProviderProxyOptions,
): Promise<void> {
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);
+1 -4
View File
@@ -66,10 +66,7 @@ describe("agent subprocess security policy", () => {
allowRead: expect.arrayContaining([canonicalWorkspace, "/usr/bin"]),
},
credentials: {
envVars: expect.arrayContaining([
{ name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
{ name: "ANTHROPIC_API_KEY", mode: "deny" },
]),
envVars: [],
},
});
});
+2 -1
View File
@@ -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 },
}]);
});
+1 -4
View File
@@ -288,10 +288,7 @@ describe("runAgent", () => {
},
sandbox: {
credentials: {
envVars: expect.arrayContaining([
{ name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
{ name: "ANTHROPIC_API_KEY", mode: "deny" },
]),
envVars: [],
},
},
},