forked from bai/curriculum-project-hub
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63c86322de | |||
| ebf870249f | |||
| f065f9f978 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.18",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.18",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.18",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ const SAFE_HOST_ENV_KEYS = [
|
|||||||
"CPH_BIN",
|
"CPH_BIN",
|
||||||
] as const;
|
] 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
|
// Linux sockaddr_un.sun_path is 108 bytes including the terminator. Claude's
|
||||||
// sandbox appends its own user directory and randomized bridge socket names,
|
// sandbox appends its own user directory and randomized bridge socket names,
|
||||||
// so keep our prefix well below that hard limit.
|
// so keep our prefix well below that hard limit.
|
||||||
@@ -156,7 +151,9 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
|||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
files: sensitiveReadPaths.map((path) => ({ path, mode: "deny" as const })),
|
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: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export interface ProviderProxyDiagnostic {
|
|||||||
| "provider_proxy_redirect_refused"
|
| "provider_proxy_redirect_refused"
|
||||||
| "provider_proxy_upstream_failed";
|
| "provider_proxy_upstream_failed";
|
||||||
readonly category: NetworkFailureCategory | "authorization" | "request_limit" | "redirect";
|
readonly category: NetworkFailureCategory | "authorization" | "request_limit" | "redirect";
|
||||||
|
readonly authShape?: {
|
||||||
|
readonly bearerLength: number;
|
||||||
|
readonly apiKeyLength: number;
|
||||||
|
readonly expectedLength: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderProxyOptions {
|
export interface ProviderProxyOptions {
|
||||||
@@ -81,7 +86,7 @@ export async function openProviderProxyLease(
|
|||||||
sdkEnv: {
|
sdkEnv: {
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||||
ANTHROPIC_AUTH_TOKEN: capability,
|
ANTHROPIC_AUTH_TOKEN: capability,
|
||||||
ANTHROPIC_API_KEY: "",
|
ANTHROPIC_API_KEY: capability,
|
||||||
},
|
},
|
||||||
sensitiveValues: [capability],
|
sensitiveValues: [capability],
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
@@ -103,7 +108,17 @@ async function forwardProviderRequest(
|
|||||||
options: ProviderProxyOptions,
|
options: ProviderProxyOptions,
|
||||||
): Promise<void> {
|
): 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"]), capability)) {
|
||||||
options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" });
|
options.onDiagnostic?.({
|
||||||
|
code: "provider_proxy_unauthorized",
|
||||||
|
category: "authorization",
|
||||||
|
authShape: {
|
||||||
|
bearerLength: request.headers.authorization?.startsWith("Bearer ")
|
||||||
|
? request.headers.authorization.length - "Bearer ".length
|
||||||
|
: 0,
|
||||||
|
apiKeyLength: header(request.headers["x-api-key"])?.length ?? 0,
|
||||||
|
expectedLength: capability.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
||||||
response.end("unauthorized");
|
response.end("unauthorized");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export async function startHub(): Promise<void> {
|
|||||||
providerId: context.providerId,
|
providerId: context.providerId,
|
||||||
errorCode: diagnostic.code,
|
errorCode: diagnostic.code,
|
||||||
failureCategory: diagnostic.category,
|
failureCategory: diagnostic.category,
|
||||||
|
authShape: diagnostic.authShape,
|
||||||
}, "provider proxy diagnostic");
|
}, "provider proxy diagnostic");
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -66,10 +66,7 @@ describe("agent subprocess security policy", () => {
|
|||||||
allowRead: expect.arrayContaining([canonicalWorkspace, "/usr/bin"]),
|
allowRead: expect.arrayContaining([canonicalWorkspace, "/usr/bin"]),
|
||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
envVars: expect.arrayContaining([
|
envVars: [],
|
||||||
{ name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
|
|
||||||
{ name: "ANTHROPIC_API_KEY", mode: "deny" },
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,13 +53,14 @@ describe("run-scoped provider proxy", () => {
|
|||||||
expect(lease.sdkEnv).toMatchObject({
|
expect(lease.sdkEnv).toMatchObject({
|
||||||
ANTHROPIC_BASE_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+$/),
|
ANTHROPIC_BASE_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+$/),
|
||||||
ANTHROPIC_AUTH_TOKEN: expect.any(String),
|
ANTHROPIC_AUTH_TOKEN: expect.any(String),
|
||||||
ANTHROPIC_API_KEY: "",
|
ANTHROPIC_API_KEY: expect.any(String),
|
||||||
});
|
});
|
||||||
|
expect(lease.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(lease.sdkEnv.ANTHROPIC_API_KEY);
|
||||||
|
|
||||||
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${lease.sdkEnv.ANTHROPIC_AUTH_TOKEN}`,
|
"x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": "2023-06-01",
|
||||||
},
|
},
|
||||||
@@ -104,7 +105,11 @@ describe("run-scoped provider proxy", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
expect(upstreamRequests).toBe(0);
|
expect(upstreamRequests).toBe(0);
|
||||||
expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]);
|
expect(diagnostics).toEqual([{
|
||||||
|
code: "provider_proxy_unauthorized",
|
||||||
|
category: "authorization",
|
||||||
|
authShape: { bearerLength: 0, apiKeyLength: 0, expectedLength: 43 },
|
||||||
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts the run capability in the Anthropic x-api-key header", async () => {
|
it("accepts the run capability in the Anthropic x-api-key header", async () => {
|
||||||
@@ -127,7 +132,7 @@ describe("run-scoped provider proxy", () => {
|
|||||||
leases.push(lease);
|
leases.push(lease);
|
||||||
|
|
||||||
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
||||||
headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_AUTH_TOKEN ?? "" },
|
headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -167,7 +172,7 @@ describe("run-scoped provider proxy", () => {
|
|||||||
leases.push(lease);
|
leases.push(lease);
|
||||||
|
|
||||||
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
||||||
headers: { authorization: `Bearer ${lease.sdkEnv.ANTHROPIC_AUTH_TOKEN}` },
|
headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "" },
|
||||||
redirect: "manual",
|
redirect: "manual",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -288,10 +288,7 @@ describe("runAgent", () => {
|
|||||||
},
|
},
|
||||||
sandbox: {
|
sandbox: {
|
||||||
credentials: {
|
credentials: {
|
||||||
envVars: expect.arrayContaining([
|
envVars: [],
|
||||||
{ name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
|
|
||||||
{ name: "ANTHROPIC_API_KEY", mode: "deny" },
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user