Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/desktop-signing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ jobs:
- name: Build and sign
env:
WINDOWS_SIGNING: keyvault
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID }}
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET: ${{ secrets.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET }}
OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT }}
run: bun run tauri build --config src-tauri/tauri.windows-signing.conf.json --config src-tauri/tauri.build-version.conf.json --bundles nsis
working-directory: desktop
# Tauri restores the unsigned build output after bundling. Verify the app
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ on:
branches: [main]
paths: ["desktop/**", "package.json", ".github/workflows/desktop.yml"]
workflow_call:
secrets:
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET:
required: false

permissions:
contents: read
Expand Down Expand Up @@ -101,6 +104,9 @@ jobs:
working-directory: desktop
env:
APPLE_SIGNING_IDENTITY: ${{ matrix.platform.name == 'macos' && '-' || '' }}
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID }}
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET: ${{ secrets.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET }}
OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT }}
- name: Verify packaged Mac version and ad-hoc signature
if: matrix.platform.name == 'macos'
run: |
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### Desktop setup shows progress and works around occupied local ports

Downloads show transferred bytes and elapsed time, Back preserves saved connections, and repair
stays under Installation options after setup. OpenBot selects and remembers usable local ports,
including when Windows reserves a default port. Setup can create a CopilotKit project and offers
Google Gemini and xAI API-key choices, plus OAuth sign-in with automatic token refresh. Google
OAuth requires the distributor's desktop client and quota-project configuration. Unsupported Bun
installations are replaced with the pinned runtime; startup errors retain useful details and provide a configurable setup-help link.
Docker image downloads can find the credential helper bundled beside Docker even when it is
missing from the desktop app's PATH. Existing Docker credentials and helper preferences are preserved.

## 0.0.14

### A tool cannot be granted for an app this deployment has not added
Expand Down
134 changes: 134 additions & 0 deletions agent-langgraph/tests/google-oauth-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { expect, test } from "bun:test";
import { RunAgentInputSchema } from "@ag-ui/core";
import type { AIMessageChunk } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import {
googleRequest,
googleResponse,
} from "../../server/src/google-oauth-transport";
import { toLangChainMessages } from "../src/history";

test("LangGraph streams and replays Gemini tool signatures through its actual SDK and AG-UI history", async () => {
const signature = `${"aBc012+/".repeat(512)}==`;
let calls = 0;
const nativeBodies: ReturnType<typeof googleRequest>["body"][] = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
const converted = googleRequest(await request.json());
nativeBodies.push(converted.body);
calls++;
const parts =
calls === 1
? [
{
functionCall: {
id: "native_browser_1",
name: "browser",
args: { url: "https://example.com" },
},
thoughtSignature: signature,
},
]
: [{ text: "Page loaded." }];
return googleResponse(
new Response(
`data: ${JSON.stringify({ candidates: [{ content: { parts }, finishReason: "STOP" }] })}\n\n`,
{ headers: { "content-type": "text/event-stream" } },
),
converted.model,
true,
);
},
});
try {
const model = new ChatOpenAI({
model: "gemini-3.6-flash",
apiKey: "local-fixture-token",
configuration: { baseURL: `${server.url}v1` },
useResponsesApi: false,
maxRetries: 0,
});
const tools = [
{
type: "function" as const,
function: {
name: "browser",
parameters: {
type: "object",
properties: { url: { type: "string" } },
required: ["url"],
},
},
},
];
let message: AIMessageChunk | undefined;
for await (const chunk of await model
.bindTools(tools)
.stream("Open example.com"))
message = message ? message.concat(chunk) : chunk;
const call = message?.tool_calls?.[0];
expect(call?.id).toBe(`native_browser_1__thought__${signature}`);
const history = RunAgentInputSchema.parse({
threadId: "fixture-thread",
runId: "fixture-run",
state: {},
tools: [],
context: [],
forwardedProps: {},
messages: [
{ id: "user-1", role: "user", content: "Open example.com" },
{
id: "assistant-1",
role: "assistant",
content: "",
toolCalls: [
{
id: call?.id,
type: "function",
function: {
name: call?.name,
arguments: JSON.stringify(call?.args),
},
},
],
},
{
id: "result-1",
role: "tool",
toolCallId: call?.id,
content: "The page loaded",
},
],
});
let answer = "";
for await (const chunk of await model
.bindTools(tools)
.stream(toLangChainMessages(history)))
answer += chunk.content;
expect(answer).toBe("Page loaded.");
expect(nativeBodies[1].contents[1].parts).toEqual([
{
functionCall: {
id: "native_browser_1",
name: "browser",
args: { url: "https://example.com" },
},
thoughtSignature: signature,
},
]);
expect(nativeBodies[1].contents[2].parts).toEqual([
{
functionResponse: {
id: "native_browser_1",
name: "browser",
response: { result: "The page loaded" },
},
},
]);
expect(calls).toBe(2);
} finally {
server.stop(true);
}
});
69 changes: 69 additions & 0 deletions desktop/PROVIDER_OAUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Model provider OAuth

OpenBot's Google and xAI model connections are separate from signing in to
OpenBot or CopilotKit. API keys remain available for both providers.

## Google Gemini

Google OAuth uses the Gemini Developer API and the selected Google Cloud
project's API quota. It does not use a personal Gemini subscription. OAuth
requests use Google's native generation API; the local server translates the
existing agents' Chat Completions requests, streamed replies, tool calls and
screenshots. API-key connections continue to use Google's compatibility API.

Before distributing a configured desktop build, register a **Desktop app** OAuth
client in a Google Cloud project with the Generative Language API enabled. Set
these variables when building the desktop app to include its defaults. Runtime
process environment variables can override those defaults:

- `OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID`: that desktop client's ID.
- `OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET`: the client secret, when issued.
- `OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT`: the project that supplies API quota.

For GitHub-built artifacts, configure repository Actions **variables** named
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID` and
`OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT`, plus an Actions **secret** named
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET` when the client has one. Both the
Desktop artifact workflow and the Windows signing workflow pass these settings
to the native build. They become defaults inside the distributed desktop app;
the desktop client credential is not a user access or refresh token.

When calling the Desktop workflow as a reusable workflow, pass its optional
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET` secret explicitly or use
`secrets: inherit`. GitHub does not provide repository secrets to fork pull
requests. Missing settings do not fail the build; API-key connections remain
available, and Google sign-in requires a configured build or runtime settings.

Do not reuse OpenBot's web SSO credentials (`GOOGLE_OAUTH_CLIENT_ID` and
`GOOGLE_OAUTH_CLIENT_SECRET`): their callback and permissions serve a different
purpose. Configure the consent screen and test users, and complete Google's
verification requirements before wider distribution.

References: [Gemini API OAuth](https://ai.google.dev/gemini-api/docs/oauth) and
[Google desktop OAuth](https://developers.google.com/identity/protocols/oauth2/native-app).

## xAI

xAI sign-in uses device authorization: OpenBot opens the provider's verification
page, the user approves the displayed code, and OpenBot receives refreshable
credentials. API calls use `https://api.x.ai/v1`.

The integration follows the public OAuth flow in xAI's endorsed OpenCode
integration. A distributor can set `OPENBOT_XAI_MODEL_OAUTH_CLIENT_ID` to an
alternative registered client. Account entitlements and provider limits still
apply.

References: [xAI's OpenCode integration](https://x.ai/news/grok-opencode) and
[xAI OAuth discovery](https://auth.x.ai/.well-known/openid-configuration).

## Credential lifecycle

The desktop's provider credentials remain in its local deployment; React does
not receive access or refresh tokens. The local server refreshes credentials
before model calls and saves rotated tokens. Agent processes receive a local
proxy credential instead of the provider's refresh token. Provider revocation
requires signing in again.

Do not commit deployment credential files or include their contents in support
reports. OAuth validation must include a real model call, refresh and restart,
and a Bot using its browser tool and rendering a graphical component.
8 changes: 7 additions & 1 deletion desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
"identifier": "default",
"description": "What the setup window is allowed to do. Commands this app defines are always callable; everything under core: is not, and has to be granted here.",
"windows": ["main"],
"permissions": ["core:event:default"]
"permissions": [
"core:event:default",
{
"identifier": "opener:allow-open-url",
"allow": [{ "url": "https://*" }, { "url": "http://*" }]
}
]
}
2 changes: 1 addition & 1 deletion desktop/src-tauri/gen/schemas/capabilities.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"default":{"identifier":"default","description":"What the setup window is allowed to do. Commands this app defines are always callable; everything under core: is not, and has to be granted here.","local":true,"windows":["main"],"permissions":["core:event:default"]}}
{"default":{"identifier":"default","description":"What the setup window is allowed to do. Commands this app defines are always callable; everything under core: is not, and has to be granted here.","local":true,"windows":["main"],"permissions":["core:event:default",{"identifier":"opener:allow-open-url","allow":[{"url":"https://*"},{"url":"http://*"}]}]}}
Loading
Loading