Harden provider security and build dependencies
This commit is contained in:
1 parent
67d2cf5649
commit
abe578b1a0
10 files changed
+783
-285
No files matched your search
@@ -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"
|
||||
}
|
||||
Generated
+737
-238
File diff suppressed because it is too large.
Load diff
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user