Harden provider security and build dependencies

This commit is contained in:
ZacharyZhang-NY committed 2026-06-07 14:04:32 -04:00
1 parent 67d2cf5649
commit abe578b1a0
10 files changed
+783 -285

No files matched your search

+3 -9
View File
@@ -43,7 +43,8 @@
"@types/react-dom": "19.2.2",
"@vitejs/plugin-react": "5.0.0",
"electron": "39.8.5",
"electron-builder": "26.8.1",
"electron-builder": "26.15.0",
"electron-builder-squirrel-windows": "26.15.0",
"electron-vite": "5.0.0",
"eslint": "9.38.0",
"eslint-plugin-react-hooks": "7.0.0",
@@ -58,12 +59,5 @@
"node": ">=22.0.0",
"pnpm": ">=10.0.0"
},
"packageManager": "pnpm@10.24.0",
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"electron-winstaller",
"esbuild"
]
}
"packageManager": "pnpm@10.24.0"
}
+737 -238
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -0,0 +1,13 @@
packages:
- .
onlyBuiltDependencies:
- electron
- electron-winstaller
- esbuild
overrides:
app-builder-lib: 26.15.0
dmg-builder: 26.15.0
electron-builder-squirrel-windows: 26.15.0
tmp: 0.2.6
@@ -9,7 +9,7 @@ import { generatePkce } from "./pkce";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
const CLIENT_SECRET = "***REMOVED***";
const CLIENT_SECRET_ENV = "AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET";
const CALLBACK_PORT = 51121;
const CALLBACK_PATH = "/oauth-callback";
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
@@ -22,9 +22,14 @@ const SCOPES = [
];
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
const CODE_ASSIST_ENDPOINTS = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"];
export function resolveAntigravityClientSecret(env: NodeJS.ProcessEnv = process.env): string {
const secret = env[CLIENT_SECRET_ENV]?.trim();
if (!secret) throw new Error(`${CLIENT_SECRET_ENV} is required for Google Antigravity OAuth.`);
return secret;
}
function buildAuthorizeUrl(challenge: string, state: string): string {
const params = new URLSearchParams({
client_id: CLIENT_ID,
@@ -46,7 +51,7 @@ async function exchangeCode(code: string, verifier: string): Promise<{ access: s
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
client_secret: resolveAntigravityClientSecret(),
code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
@@ -96,7 +101,7 @@ async function discoverProject(accessToken: string): Promise<string> {
// Try the next endpoint.
}
}
return DEFAULT_PROJECT_ID;
throw new Error("Antigravity project discovery failed.");
}
/** Refresh an expired Antigravity access token, retaining the project id. */
@@ -104,7 +109,12 @@ export async function refreshAntigravity(refreshToken: string, projectId: string
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, refresh_token: refreshToken, grant_type: "refresh_token" }),
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: resolveAntigravityClientSecret(),
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Antigravity token refresh failed: ${response.status} ${text}`);
@@ -66,13 +66,10 @@ function errorText(data: JsonRecord): string {
/**
* Accumulate a Responses SSE stream (Codex/Copilot) into the final result. The
* authoritative output arrives on the completion event's `response.output`; the
* incremental events are accumulated only as a fallback.
* result is accepted from the completion event's `response.output`.
*/
export async function accumulateResponsesStream(messages: AsyncIterable<ProviderEventStreamMessage>): Promise<AgentTurnResult> {
const raw: string[] = [];
const textParts: string[] = [];
const calls = new Map<string, { id: string; name: string; args: string }>();
for await (const message of messages) {
raw.push(message.rawData);
const type = eventType(message);
@@ -84,23 +81,8 @@ export async function accumulateResponsesStream(messages: AsyncIterable<Provider
const parsed = parseResponsesOutput(response);
return { text: parsed.text, toolCalls: parsed.toolCalls, rawResponseJson: JSON.stringify(raw) };
}
break;
}
if (type === "response.output_text.delta" && typeof message.data.delta === "string") {
textParts.push(message.data.delta);
} else if (type === "response.output_item.added" && isRecord(message.data.item) && message.data.item.type === "function_call") {
const item = message.data.item;
const key = typeof item.id === "string" ? item.id : typeof item.call_id === "string" ? item.call_id : "";
const id = typeof item.call_id === "string" ? item.call_id : key;
if (key) calls.set(key, { id, name: typeof item.name === "string" ? item.name : "", args: typeof item.arguments === "string" ? item.arguments : "" });
} else if (type === "response.function_call_arguments.delta" && typeof message.data.item_id === "string" && typeof message.data.delta === "string") {
const entry = calls.get(message.data.item_id);
if (entry) entry.args += message.data.delta;
} else if (type === "response.function_call_arguments.done" && typeof message.data.item_id === "string" && typeof message.data.arguments === "string") {
const entry = calls.get(message.data.item_id);
if (entry) entry.args = message.data.arguments;
throw new Error("Responses completion event was missing response.output.");
}
}
const toolCalls = [...calls.values()].filter((entry) => entry.name).map((entry) => ({ id: entry.id, name: entry.name, arguments: parseArguments(entry.args) }));
return { text: textParts.join("").trim(), toolCalls, rawResponseJson: JSON.stringify(raw) };
throw new Error("Responses stream ended before a completion event.");
}
@@ -254,7 +254,7 @@ function CompanyStep({ draft, onChange, onBack, onNext }: StepProps) {
</Field>
<Field label="Workspace path (optional)">
<div className="row" style={{ gap: 6 }}>
<input className="input" value={draft.workspacePath} onChange={(event) => onChange({ workspacePath: event.target.value })} placeholder="Choose a folder for agent artifacts" />
<input className="input" value={draft.workspacePath} onChange={(event) => onChange({ workspacePath: event.target.value })} />
<Button tone="ghost" onClick={chooseWorkspace}><Icon name="folder" size={13} /> Choose</Button>
</div>
</Field>
@@ -127,7 +127,6 @@ function ProviderCredentialFields({
<input
className="input"
value={draft.providerBaseUrl}
placeholder={selected.customEndpoint === "openai" ? "https://host/v1" : "https://host"}
onChange={(event) => onChange({ providerBaseUrl: event.target.value })}
/>
</Field>
@@ -174,7 +173,6 @@ function ProviderCredentialFields({
<input
className="input"
value={draft.providerModel}
placeholder={modelPlaceholder(live, isOAuthLike, loading)}
onChange={(event) => onChange({ providerModel: event.target.value })}
/>
)}
@@ -193,12 +191,6 @@ function Actions({ onBack, onNext }: { onBack: () => void; onNext: () => void })
);
}
function modelPlaceholder(live: boolean, isOAuthLike: boolean, loading: boolean): string {
if (!live) return "Enter the model id";
if (loading || isOAuthLike) return "Loading...";
return "Click Get models, or type a model id";
}
function authBadge(authType: string): string {
if (authType === "api_key") return "API key";
if (authType === "oauth_pkce") return "OAuth";
@@ -150,7 +150,6 @@ function ConversationHeader({
className="input"
autoFocus
aria-label="Search messages"
placeholder="Search messages"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
onKeyDown={(event) => { if (event.key === "Escape") toggleSearch(); }}
@@ -83,7 +83,6 @@ function CompatFieldsForm({ entry, provider }: { entry: ProviderCatalogEntry; pr
className="input"
name="cloudBaseUrl"
value={baseUrl}
placeholder={entry.customEndpoint === "openai" ? "https://host/v1" : "https://host"}
onChange={(event) => setBaseUrl(event.target.value)}
/>
</Field>
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { describe, expect, test } from "vitest";
import { interpretPollResponse } from "../../src/main/services/oauth/github-copilot-flow";
import { extractAccountId } from "../../src/main/services/oauth/openai-codex-flow";
import { readProjectId } from "../../src/main/services/oauth/google-antigravity-flow";
import { readProjectId, resolveAntigravityClientSecret } from "../../src/main/services/oauth/google-antigravity-flow";
import { generatePkce } from "../../src/main/services/oauth/pkce";
import { parseAccessToken, parseAccountToken } from "../../src/main/services/provider-auth-profiles";
@@ -57,6 +57,16 @@ describe("antigravity project discovery", () => {
});
});
describe("antigravity oauth configuration", () => {
test("reads the client secret from runtime environment", () => {
expect(resolveAntigravityClientSecret({ AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET: " live-secret " })).toBe("live-secret");
});
test("requires an explicit client secret", () => {
expect(() => resolveAntigravityClientSecret({})).toThrow("AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET");
});
});
describe("pkce generation", () => {
test("produces an S256 challenge for the verifier", () => {
const { verifier, challenge } = generatePkce();