Skip to content

Commit 6014b39

Browse files
authored
fix(desktop): improve setup, local startup and provider OAuth (#633)
* fix(desktop): make installation progress and connection setup reliable * feat(desktop): add Google and xAI provider OAuth * fix(desktop): preserve OAuth storage across PowerShell environments * fix(desktop): find bundled Docker credential helpers * build(desktop): include configured Google OAuth defaults * fix: use native Gemini transport for Google OAuth * docs: explain native Google OAuth transport * test: use bounded blocking sockets in host result collector * test: read the framed host response without waiting for EOF * fix: make desktop OAuth callbacks and token rotation reliable * fix(desktop): recover leftover databases during fresh setup
1 parent 9c6a237 commit 6014b39

44 files changed

Lines changed: 7871 additions & 644 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/desktop-signing.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ jobs:
118118
- name: Build and sign
119119
env:
120120
WINDOWS_SIGNING: keyvault
121+
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID }}
122+
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET: ${{ secrets.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET }}
123+
OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT }}
121124
run: bun run tauri build --config src-tauri/tauri.windows-signing.conf.json --config src-tauri/tauri.build-version.conf.json --bundles nsis
122125
working-directory: desktop
123126
# Tauri restores the unsigned build output after bundling. Verify the app

‎.github/workflows/desktop.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ on:
1010
branches: [main]
1111
paths: ["desktop/**", "package.json", ".github/workflows/desktop.yml"]
1212
workflow_call:
13+
secrets:
14+
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET:
15+
required: false
1316

1417
permissions:
1518
contents: read
@@ -101,6 +104,9 @@ jobs:
101104
working-directory: desktop
102105
env:
103106
APPLE_SIGNING_IDENTITY: ${{ matrix.platform.name == 'macos' && '-' || '' }}
107+
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID }}
108+
OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET: ${{ secrets.OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET }}
109+
OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT: ${{ vars.OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT }}
104110
- name: Verify packaged Mac version and ad-hoc signature
105111
if: matrix.platform.name == 'macos'
106112
run: |

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

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

1324
### A tool cannot be granted for an app this deployment has not added
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { expect, test } from "bun:test";
2+
import { RunAgentInputSchema } from "@ag-ui/core";
3+
import type { AIMessageChunk } from "@langchain/core/messages";
4+
import { ChatOpenAI } from "@langchain/openai";
5+
import {
6+
googleRequest,
7+
googleResponse,
8+
} from "../../server/src/google-oauth-transport";
9+
import { toLangChainMessages } from "../src/history";
10+
11+
test("LangGraph streams and replays Gemini tool signatures through its actual SDK and AG-UI history", async () => {
12+
const signature = `${"aBc012+/".repeat(512)}==`;
13+
let calls = 0;
14+
const nativeBodies: ReturnType<typeof googleRequest>["body"][] = [];
15+
const server = Bun.serve({
16+
hostname: "127.0.0.1",
17+
port: 0,
18+
async fetch(request) {
19+
const converted = googleRequest(await request.json());
20+
nativeBodies.push(converted.body);
21+
calls++;
22+
const parts =
23+
calls === 1
24+
? [
25+
{
26+
functionCall: {
27+
id: "native_browser_1",
28+
name: "browser",
29+
args: { url: "https://example.com" },
30+
},
31+
thoughtSignature: signature,
32+
},
33+
]
34+
: [{ text: "Page loaded." }];
35+
return googleResponse(
36+
new Response(
37+
`data: ${JSON.stringify({ candidates: [{ content: { parts }, finishReason: "STOP" }] })}\n\n`,
38+
{ headers: { "content-type": "text/event-stream" } },
39+
),
40+
converted.model,
41+
true,
42+
);
43+
},
44+
});
45+
try {
46+
const model = new ChatOpenAI({
47+
model: "gemini-3.6-flash",
48+
apiKey: "local-fixture-token",
49+
configuration: { baseURL: `${server.url}v1` },
50+
useResponsesApi: false,
51+
maxRetries: 0,
52+
});
53+
const tools = [
54+
{
55+
type: "function" as const,
56+
function: {
57+
name: "browser",
58+
parameters: {
59+
type: "object",
60+
properties: { url: { type: "string" } },
61+
required: ["url"],
62+
},
63+
},
64+
},
65+
];
66+
let message: AIMessageChunk | undefined;
67+
for await (const chunk of await model
68+
.bindTools(tools)
69+
.stream("Open example.com"))
70+
message = message ? message.concat(chunk) : chunk;
71+
const call = message?.tool_calls?.[0];
72+
expect(call?.id).toBe(`native_browser_1__thought__${signature}`);
73+
const history = RunAgentInputSchema.parse({
74+
threadId: "fixture-thread",
75+
runId: "fixture-run",
76+
state: {},
77+
tools: [],
78+
context: [],
79+
forwardedProps: {},
80+
messages: [
81+
{ id: "user-1", role: "user", content: "Open example.com" },
82+
{
83+
id: "assistant-1",
84+
role: "assistant",
85+
content: "",
86+
toolCalls: [
87+
{
88+
id: call?.id,
89+
type: "function",
90+
function: {
91+
name: call?.name,
92+
arguments: JSON.stringify(call?.args),
93+
},
94+
},
95+
],
96+
},
97+
{
98+
id: "result-1",
99+
role: "tool",
100+
toolCallId: call?.id,
101+
content: "The page loaded",
102+
},
103+
],
104+
});
105+
let answer = "";
106+
for await (const chunk of await model
107+
.bindTools(tools)
108+
.stream(toLangChainMessages(history)))
109+
answer += chunk.content;
110+
expect(answer).toBe("Page loaded.");
111+
expect(nativeBodies[1].contents[1].parts).toEqual([
112+
{
113+
functionCall: {
114+
id: "native_browser_1",
115+
name: "browser",
116+
args: { url: "https://example.com" },
117+
},
118+
thoughtSignature: signature,
119+
},
120+
]);
121+
expect(nativeBodies[1].contents[2].parts).toEqual([
122+
{
123+
functionResponse: {
124+
id: "native_browser_1",
125+
name: "browser",
126+
response: { result: "The page loaded" },
127+
},
128+
},
129+
]);
130+
expect(calls).toBe(2);
131+
} finally {
132+
server.stop(true);
133+
}
134+
});

‎desktop/PROVIDER_OAUTH.md‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Model provider OAuth
2+
3+
OpenBot's Google and xAI model connections are separate from signing in to
4+
OpenBot or CopilotKit. API keys remain available for both providers.
5+
6+
## Google Gemini
7+
8+
Google OAuth uses the Gemini Developer API and the selected Google Cloud
9+
project's API quota. It does not use a personal Gemini subscription. OAuth
10+
requests use Google's native generation API; the local server translates the
11+
existing agents' Chat Completions requests, streamed replies, tool calls and
12+
screenshots. API-key connections continue to use Google's compatibility API.
13+
14+
Before distributing a configured desktop build, register a **Desktop app** OAuth
15+
client in a Google Cloud project with the Generative Language API enabled. Set
16+
these variables when building the desktop app to include its defaults. Runtime
17+
process environment variables can override those defaults:
18+
19+
- `OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID`: that desktop client's ID.
20+
- `OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET`: the client secret, when issued.
21+
- `OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT`: the project that supplies API quota.
22+
23+
For GitHub-built artifacts, configure repository Actions **variables** named
24+
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_ID` and
25+
`OPENBOT_GOOGLE_MODEL_OAUTH_QUOTA_PROJECT`, plus an Actions **secret** named
26+
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET` when the client has one. Both the
27+
Desktop artifact workflow and the Windows signing workflow pass these settings
28+
to the native build. They become defaults inside the distributed desktop app;
29+
the desktop client credential is not a user access or refresh token.
30+
31+
When calling the Desktop workflow as a reusable workflow, pass its optional
32+
`OPENBOT_GOOGLE_MODEL_OAUTH_CLIENT_SECRET` secret explicitly or use
33+
`secrets: inherit`. GitHub does not provide repository secrets to fork pull
34+
requests. Missing settings do not fail the build; API-key connections remain
35+
available, and Google sign-in requires a configured build or runtime settings.
36+
37+
Do not reuse OpenBot's web SSO credentials (`GOOGLE_OAUTH_CLIENT_ID` and
38+
`GOOGLE_OAUTH_CLIENT_SECRET`): their callback and permissions serve a different
39+
purpose. Configure the consent screen and test users, and complete Google's
40+
verification requirements before wider distribution.
41+
42+
References: [Gemini API OAuth](https://ai.google.dev/gemini-api/docs/oauth) and
43+
[Google desktop OAuth](https://developers.google.com/identity/protocols/oauth2/native-app).
44+
45+
## xAI
46+
47+
xAI sign-in uses device authorization: OpenBot opens the provider's verification
48+
page, the user approves the displayed code, and OpenBot receives refreshable
49+
credentials. API calls use `https://api.x.ai/v1`.
50+
51+
The integration follows the public OAuth flow in xAI's endorsed OpenCode
52+
integration. A distributor can set `OPENBOT_XAI_MODEL_OAUTH_CLIENT_ID` to an
53+
alternative registered client. Account entitlements and provider limits still
54+
apply.
55+
56+
References: [xAI's OpenCode integration](https://x.ai/news/grok-opencode) and
57+
[xAI OAuth discovery](https://auth.x.ai/.well-known/openid-configuration).
58+
59+
## Credential lifecycle
60+
61+
The desktop's provider credentials remain in its local deployment; React does
62+
not receive access or refresh tokens. The local server refreshes credentials
63+
before model calls and saves rotated tokens. Agent processes receive a local
64+
proxy credential instead of the provider's refresh token. Provider revocation
65+
requires signing in again.
66+
67+
Do not commit deployment credential files or include their contents in support
68+
reports. OAuth validation must include a real model call, refresh and restart,
69+
and a Bot using its browser tool and rendering a graphical component.

‎desktop/src-tauri/capabilities/default.json‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,11 @@
33
"identifier": "default",
44
"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.",
55
"windows": ["main"],
6-
"permissions": ["core:event:default"]
6+
"permissions": [
7+
"core:event:default",
8+
{
9+
"identifier": "opener:allow-open-url",
10+
"allow": [{ "url": "https://*" }, { "url": "http://*" }]
11+
}
12+
]
713
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"]}}
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",{"identifier":"opener:allow-open-url","allow":[{"url":"https://*"},{"url":"http://*"}]}]}}

0 commit comments

Comments
 (0)