Compare commits

...

9 Commits

6 changed files with 18 additions and 52 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ Everything else is platform-managed or derived: instance/Organization id,
server, SSH settings, release, resource ceilings, database coordinates and server, SSH settings, release, resource ceilings, database coordinates and
generated password, domain, port, workspace, provider/base URL, model/role, generated password, domain, port, workspace, provider/base URL, model/role,
curated skills, concurrency, request/file limits and the managed Mihomo proxy curated skills, concurrency, request/file limits and the managed Mihomo proxy
environment. environment. `NODE_USE_ENV_PROXY=1` is required on Node.js 24 so Hub's built-in
`fetch` actually uses that proxy; merely setting `HTTP_PROXY`/`HTTPS_PROXY` is
not sufficient.
The Feishu app is scoped to this Silo. OAuth users authenticated by that app are The Feishu app is scoped to this Silo. OAuth users authenticated by that app are
automatically admitted to this Organization; OWNER remains the initial automatically admitted to this Organization; OWNER remains the initial
+2
View File
@@ -117,6 +117,8 @@ const platformEnv = [
envLine("HTTPS_PROXY", "http://127.0.0.1:7890"), envLine("HTTPS_PROXY", "http://127.0.0.1:7890"),
envLine("ALL_PROXY", "socks5h://127.0.0.1:7890"), envLine("ALL_PROXY", "socks5h://127.0.0.1:7890"),
envLine("NO_PROXY", "127.0.0.1,localhost,::1"), envLine("NO_PROXY", "127.0.0.1,localhost,::1"),
envLine("NODE_USE_ENV_PROXY", "1"),
envLine("ANTHROPIC_DEFAULT_SONNET_MODEL", "anthropic/claude-sonnet-5"),
envLine("CPH_SANDBOX_EXTRA_DENY_READ", `${envPath}:${keyringPath}`), envLine("CPH_SANDBOX_EXTRA_DENY_READ", `${envPath}:${keyringPath}`),
"", "",
].join("\n"); ].join("\n");
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@paradigm/hub", "name": "@paradigm/hub",
"version": "0.0.16", "version": "0.0.20",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@paradigm/hub", "name": "@paradigm/hub",
"version": "0.0.16", "version": "0.0.20",
"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
View File
@@ -1,6 +1,6 @@
{ {
"name": "@paradigm/hub", "name": "@paradigm/hub",
"version": "0.0.16", "version": "0.0.20",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
+6 -17
View File
@@ -80,8 +80,8 @@ export async function openProviderProxyLease(
return { return {
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: "", ANTHROPIC_AUTH_TOKEN: capability,
ANTHROPIC_API_KEY: capability, ANTHROPIC_API_KEY: "",
}, },
sensitiveValues: [capability], sensitiveValues: [capability],
async close(): Promise<void> { async close(): Promise<void> {
@@ -102,7 +102,7 @@ async function forwardProviderRequest(
upstream: ProviderUpstreamCredential, upstream: ProviderUpstreamCredential,
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, capability)) {
options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" }); options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" });
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");
@@ -165,24 +165,13 @@ async function forwardProviderRequest(
} }
} }
function authorized( function authorized(value: string | undefined, capability: string): boolean {
authorization: string | undefined, if (value === undefined || !value.startsWith("Bearer ")) return false;
apiKey: string | undefined, const supplied = Buffer.from(value.slice("Bearer ".length));
capability: string,
): boolean {
const value = authorization?.startsWith("Bearer ")
? authorization.slice("Bearer ".length)
: apiKey;
if (value === undefined) return false;
const supplied = Buffer.from(value);
const expected = Buffer.from(capability); const expected = Buffer.from(capability);
return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected); return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected);
} }
function header(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
function forwardedRequestHeaders(source: IncomingHttpHeaders): Headers { function forwardedRequestHeaders(source: IncomingHttpHeaders): Headers {
const result = new Headers(); const result = new Headers();
for (const [name, value] of Object.entries(source)) { for (const [name, value] of Object.entries(source)) {
+4 -31
View File
@@ -52,14 +52,14 @@ describe("run-scoped provider proxy", () => {
expect(serializedAgentEnv).not.toContain(String(address.port)); expect(serializedAgentEnv).not.toContain(String(address.port));
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: "", ANTHROPIC_AUTH_TOKEN: expect.any(String),
ANTHROPIC_API_KEY: expect.any(String), 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: {
"x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "", authorization: `Bearer ${lease.sdkEnv.ANTHROPIC_AUTH_TOKEN}`,
"content-type": "application/json", "content-type": "application/json",
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
}, },
@@ -107,33 +107,6 @@ describe("run-scoped provider proxy", () => {
expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]); expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]);
}); });
it("accepts the run capability in the Anthropic x-api-key header", async () => {
let upstreamRequests = 0;
const upstream = createServer((_request, response) => {
upstreamRequests += 1;
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
});
upstreamServers.push(upstream);
upstream.listen(0, "127.0.0.1");
await once(upstream, "listening");
const address = upstream.address();
if (address === null || typeof address === "string") throw new Error("expected upstream TCP address");
const lease = await openProviderProxyLease({
baseUrl: `http://127.0.0.1:${address.port}`,
authToken: "customer-secret",
anthropicApiKey: "",
});
leases.push(lease);
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "" },
});
expect(response.status).toBe(200);
expect(upstreamRequests).toBe(1);
});
it("does not forward provider credentials across an upstream redirect", async () => { it("does not forward provider credentials across an upstream redirect", async () => {
let redirectedRequests = 0; let redirectedRequests = 0;
const diagnostics: unknown[] = []; const diagnostics: unknown[] = [];
@@ -167,7 +140,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_API_KEY ?? "" }, headers: { authorization: `Bearer ${lease.sdkEnv.ANTHROPIC_AUTH_TOKEN}` },
redirect: "manual", redirect: "manual",
}); });