Compare commits

..

8 Commits

Author SHA1 Message Date
hongjr03 53998d2651 fix: enable proxy use in new silos 2026-07-11 15:07:28 +08:00
hongjr03 5b55cf18a8 Revert "fix: accept SDK provider capability headers"
This reverts commit e7ad5580ec.
2026-07-11 15:06:48 +08:00
hongjr03 b0d691d53f Revert "fix: pass provider capability as API key"
This reverts commit f065f9f978.
2026-07-11 15:06:48 +08:00
hongjr03 1f48c5b707 Revert "fix: provide capability for both SDK auth modes"
This reverts commit ebf870249f.
2026-07-11 15:06:48 +08:00
hongjr03 12a2f3117f Revert "fix: preserve run provider capability"
This reverts commit 63c86322de.
2026-07-11 15:06:48 +08:00
hongjr03 2ee84d9543 Revert "fix: carry provider capability in dedicated header"
This reverts commit 3087132083.
2026-07-11 15:06:47 +08:00
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 18 additions and 73 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
generated password, domain, port, workspace, provider/base URL, model/role,
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
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("ALL_PROXY", "socks5h://127.0.0.1:7890"),
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}`),
"",
].join("\n");
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@paradigm/hub",
"version": "0.0.17",
"version": "0.0.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@paradigm/hub",
"version": "0.0.17",
"version": "0.0.20",
"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.20",
"private": true,
"type": "module",
"engines": {
+6 -32
View File
@@ -41,11 +41,6 @@ export interface ProviderProxyDiagnostic {
| "provider_proxy_redirect_refused"
| "provider_proxy_upstream_failed";
readonly category: NetworkFailureCategory | "authorization" | "request_limit" | "redirect";
readonly authShape?: {
readonly bearerLength: number;
readonly apiKeyLength: number;
readonly expectedLength: number;
};
}
export interface ProviderProxyOptions {
@@ -86,7 +81,7 @@ export async function openProviderProxyLease(
sdkEnv: {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
ANTHROPIC_AUTH_TOKEN: capability,
ANTHROPIC_API_KEY: capability,
ANTHROPIC_API_KEY: "",
},
sensitiveValues: [capability],
async close(): Promise<void> {
@@ -107,18 +102,8 @@ async function forwardProviderRequest(
upstream: ProviderUpstreamCredential,
options: ProviderProxyOptions,
): Promise<void> {
if (!authorized(request.headers.authorization, header(request.headers["x-api-key"]), capability)) {
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,
},
});
if (!authorized(request.headers.authorization, capability)) {
options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" });
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
response.end("unauthorized");
return;
@@ -180,24 +165,13 @@ async function forwardProviderRequest(
}
}
function authorized(
authorization: string | undefined,
apiKey: string | undefined,
capability: string,
): boolean {
const value = authorization?.startsWith("Bearer ")
? authorization.slice("Bearer ".length)
: apiKey;
if (value === undefined) return false;
const supplied = Buffer.from(value);
function authorized(value: string | undefined, capability: string): boolean {
if (value === undefined || !value.startsWith("Bearer ")) return false;
const supplied = Buffer.from(value.slice("Bearer ".length));
const expected = Buffer.from(capability);
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 {
const result = new Headers();
for (const [name, value] of Object.entries(source)) {
-1
View File
@@ -73,7 +73,6 @@ export async function startHub(): Promise<void> {
providerId: context.providerId,
errorCode: diagnostic.code,
failureCategory: diagnostic.category,
authShape: diagnostic.authShape,
}, "provider proxy diagnostic");
},
}),
+4 -36
View File
@@ -53,14 +53,13 @@ describe("run-scoped provider proxy", () => {
expect(lease.sdkEnv).toMatchObject({
ANTHROPIC_BASE_URL: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+$/),
ANTHROPIC_AUTH_TOKEN: expect.any(String),
ANTHROPIC_API_KEY: expect.any(String),
ANTHROPIC_API_KEY: "",
});
expect(lease.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(lease.sdkEnv.ANTHROPIC_API_KEY);
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
method: "POST",
headers: {
"x-api-key": lease.sdkEnv.ANTHROPIC_API_KEY ?? "",
authorization: `Bearer ${lease.sdkEnv.ANTHROPIC_AUTH_TOKEN}`,
"content-type": "application/json",
"anthropic-version": "2023-06-01",
},
@@ -105,38 +104,7 @@ describe("run-scoped provider proxy", () => {
expect(response.status).toBe(401);
expect(upstreamRequests).toBe(0);
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 () => {
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);
expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]);
});
it("does not forward provider credentials across an upstream redirect", async () => {
@@ -172,7 +140,7 @@ describe("run-scoped provider proxy", () => {
leases.push(lease);
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",
});