opensymphony 1.6.1

A Rust implementation of the OpenAI Symphony orchestration design
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Subscription-Based OpenAI ChatGPT/Codex Authentication with `openhands agent-server`

## Purpose

This document explains how OpenAI ChatGPT/Codex subscription-based authentication can work when using `openhands agent-server`, especially for architectures that want to use OpenHands SDK and remote execution while relying on a user's ChatGPT Plus/Pro subscription instead of an OpenAI API key.

The core idea is that OpenHands SDK subscription login does not introduce a completely separate agent interface. It authenticates with OpenAI/ChatGPT OAuth, obtains OAuth credentials, and then constructs a normal OpenHands `LLM` object configured to speak to the ChatGPT Codex backend.

That means an `Agent` and `Conversation` can use the resulting `LLM` object in the usual OpenHands SDK flow, including with a remote `agent-server` workspace. The architecture decision is where OAuth happens, where refresh credentials are stored, and how much credential material is exposed to remote execution infrastructure.

---

## Core SDK Behavior

OpenHands SDK exposes subscription authentication through:

```python
from openhands.sdk import LLM

llm = LLM.subscription_login(
    vendor="openai",
    model="gpt-5.2-codex",
)
```

At a high level, this does the following:

1. Performs OpenAI/ChatGPT OAuth for subscription-based Codex access.
2. Stores OAuth credentials locally.
3. Refreshes credentials when needed.
4. Builds a normal OpenHands `LLM` object.
5. Configures that `LLM` object for the ChatGPT Codex backend rather than the standard OpenAI API endpoint.

The underlying `OpenAISubscriptionAuth.create_llm()` path creates an `LLM` roughly equivalent to:

```python
LLM(
    model=f"openai/{model}",
    base_url="https://chatgpt.com/backend-api/codex",
    api_key=oauth_access_token,
    extra_headers={
        "originator": "codex_cli_rs",
        "OpenAI-Beta": "responses=experimental",
        "User-Agent": "...",
        "chatgpt-account-id": "...",  # when available
    },
    litellm_extra_body={"store": False},
    temperature=None,
    max_output_tokens=None,
    stream=True,
)
```

It also marks the LLM internally as subscription-backed:

```python
llm._is_subscription = True
```

This matters because subscription mode needs slightly different request handling and message transformation from normal OpenAI API usage.

---

## Credential Storage

The SDK stores OAuth credentials under the OpenHands auth directory, currently:

```text
~/.openhands/auth/
```

For OpenAI, the expected stored credential file is conceptually:

```text
~/.openhands/auth/openai_oauth.json
```

The credential payload includes:

- vendor
- access token
- refresh token
- expiration timestamp

The SDK can reuse cached credentials and refresh expired access tokens using the stored refresh token.

This credential location is fine for single-user local development. It should not be treated as an acceptable default for hosted multi-tenant systems without an additional credential isolation and encryption layer.

---

## How This Works with `openhands agent-server`

The `agent-server` model separates the SDK client and the remote execution workspace.

The SDK shape remains roughly:

```python
agent = Agent(llm=llm, tools=[...])
conversation = Conversation(agent=agent, workspace=workspace)
conversation.send_message("...")
conversation.run()
```

The key difference is that `workspace` can be backed by `DockerWorkspace` or `APIRemoteWorkspace`, causing the SDK to communicate with an `openhands agent-server` instance over HTTP/WebSocket.

So the subscription-backed `LLM` can be used with `agent-server` because, after login, it is still just an SDK `LLM` object attached to an SDK `Agent`.

---

## Mode 1: Personal or Local Agent-Server Use

For a single-user local or self-hosted setup, this is the simplest workable approach.

### Flow

1. User runs `LLM.subscription_login(...)` in the same environment that will create the `LLM`.
2. SDK opens browser OAuth or uses cached credentials.
3. Credentials are stored in `~/.openhands/auth/`.
4. SDK creates the subscription-backed `LLM`.
5. The `Agent` uses that `LLM`.
6. The `Conversation` uses a local or remote agent-server workspace.

### Example

```python
from openhands.sdk import LLM, Agent, Conversation, Tool
from openhands.tools.file_editor import FileEditorTool
from openhands.tools.terminal import TerminalTool
from openhands.sdk.workspace import DockerWorkspace

llm = LLM.subscription_login(
    vendor="openai",
    model="gpt-5.2-codex",
)

agent = Agent(
    llm=llm,
    tools=[
        Tool(name=TerminalTool.name),
        Tool(name=FileEditorTool.name),
    ],
)

with DockerWorkspace(
    server_image="ghcr.io/openhands/agent-server:latest",
) as workspace:
    conversation = Conversation(agent=agent, workspace=workspace)
    conversation.send_message("Inspect this repository and propose fixes.")
    conversation.run()
```

### Operational Requirement

Persist the user's OpenHands auth directory across runs:

```text
~/.openhands/auth/
```

If the agent-server or SDK process runs in a container, mount this directory explicitly.

Example:

```bash
-v ~/.openhands/auth:/home/openhands/.openhands/auth
```

Adjust the target path based on the container user and home directory.

---

## Mode 2: Headless or Remote Agent-Server Use

For remote or headless execution, browser OAuth can be awkward because the server may not have a GUI browser or may not be reachable through localhost callbacks.

The SDK has device-code login support for OpenAI subscription auth. This is the better path for remote/headless use.

### Example

```python
from openhands.sdk import LLM

llm = LLM.subscription_login(
    vendor="openai",
    model="gpt-5.2-codex",
    auth_method="device_code",
    open_browser=False,
)
```

The device-code flow prints a verification URL and one-time code. The user opens the URL in a browser, signs in to ChatGPT, and enters the code. The remote process polls until the authorization is complete, then exchanges the result for OAuth tokens and stores them.

### Requirements

The process performing login must have:

- Network access to OpenAI/ChatGPT auth endpoints.
- A persistent auth directory.
- A way to show the device-code URL and code to the user.
- A secure filesystem boundary for `~/.openhands/auth/`.

This is suitable for personal remote servers or self-hosted setups.

It is not sufficient as-is for a multi-tenant hosted environment unless credentials are isolated per user.

---

## Mode 3: Hosted Multi-User Architecture

For hosted multi-user use, avoid a shared `~/.openhands/auth/` inside a common agent-server container.

Recommended architecture:

```text
User browser / desktop app
  -> ChatGPT OAuth consent
  -> app backend credential broker
  -> encrypted per-user refresh token store
  -> per-session fresh access token
  -> subscription-backed LLM config
  -> OpenHands agent-server conversation
  -> ChatGPT Codex backend
```

### Recommended Responsibilities

#### Client or App Backend

- Initiates OAuth.
- Presents consent and terms information.
- Handles browser callback or device-code flow.
- Associates credentials with the authenticated app user.

#### Credential Broker

- Stores refresh tokens encrypted at rest.
- Keeps credentials namespaced per user and tenant.
- Refreshes access tokens server-side.
- Issues only short-lived credential material into execution sessions.
- Supports sign-out and token revocation workflows where possible.

#### Agent-Server Session Creator

- Receives or obtains a short-lived access token.
- Constructs an `LLM` equivalent to the SDK subscription-auth output.
- Avoids storing refresh tokens in workspace containers.
- Ensures credentials are scoped to the correct user and conversation.

#### Agent Runtime

- Uses the configured `LLM` normally.
- Does not need to know the user's refresh token.
- Should not persist OAuth material into project workspaces, logs, traces, or exported artifacts.

---

## Hosted Implementation Sketch

A hosted implementation can either call the existing SDK auth path or replicate the final `create_llm()` construction using credentials from a secure broker.

### Option A: Use SDK CredentialStore Per User

Use a per-user credential directory, mounted or configured only for that user/session.

Conceptually:

```text
/secure-auth-store/
  user_123/
    openai_oauth.json
  user_456/
    openai_oauth.json
```

Then construct a `CredentialStore` for that user's path and call the auth helper.

This is closer to SDK defaults but still requires careful isolation.

### Option B: Credential Broker Constructs the LLM

The credential broker refreshes tokens and then the server constructs the `LLM` directly:

```python
from openhands.sdk import LLM

llm = LLM(
    model="openai/gpt-5.2-codex",
    base_url="https://chatgpt.com/backend-api/codex",
    api_key=access_token,
    extra_headers={
        "originator": "codex_cli_rs",
        "OpenAI-Beta": "responses=experimental",
        "User-Agent": "your-app-name",
        "chatgpt-account-id": chatgpt_account_id,
    },
    litellm_extra_body={"store": False},
    temperature=None,
    max_output_tokens=None,
    stream=True,
)
llm._is_subscription = True
```

This is more invasive because it depends on OpenHands internal subscription behavior. It may be better to wrap or upstream a public constructor for “create subscription LLM from OAuth credentials” rather than relying on private attributes.

---

## Key Design Cautions

### 1. Access Token vs Refresh Token

An access token is short-lived and can be passed into a run with lower risk. A refresh token is long-lived and should not be copied into an agent workspace.

For hosted systems, the refresh token belongs in a credential broker, not in an agent-server workspace.

### 2. `~/.openhands/auth/` Is User-Scoped

The default SDK credential path is user-local. In a multi-user service, a shared filesystem home breaks the security model.

Each user needs separate credential storage.

### 3. Agent Workspaces Are Not Credential Vaults

Agent workspaces are for code and task execution. They should not become long-lived auth stores.

Avoid:

```text
/workspace/.openhands/auth/openai_oauth.json
```

Prefer:

```text
/secure-credential-store/{tenant}/{user}/openai_oauth.json
```

or a real encrypted secret store.

### 4. Token Refresh Needs an Owner

Long-running conversations may outlive an access token.

Decide explicitly where refresh happens:

- SDK local process
- remote agent-server process
- credential broker

For hosted systems, prefer the credential broker.

### 5. Logs and Traces Need Redaction

Subscription auth turns the OAuth access token into `api_key` on an `LLM` object. Any settings dumps, traces, debug logs, or telemetry must redact it.

---

## Recommendation by Deployment Type

| Deployment type | Recommended auth pattern |
|---|---|
| Local SDK only | `LLM.subscription_login(...)` with browser OAuth |
| Local SDK + local Docker `agent-server` | `LLM.subscription_login(...)` and persist `~/.openhands/auth/` |
| Self-hosted remote server | Device-code login with persistent per-user auth volume |
| Hosted single-tenant | Per-user auth directory or credential broker |
| Hosted multi-tenant | Credential broker with encrypted per-user refresh tokens and short-lived access-token injection |

---

## Best Initial Implementation for OpenSymphony-Style Use

For an OpenSymphony-style orchestrator using OpenHands agent-server, the pragmatic roadmap is:

### Phase 1: Personal Local Support

- Allow the orchestrator to select an OpenAI subscription-backed model.
- Call `LLM.subscription_login(vendor="openai", model="gpt-5.2-codex")`.
- Reuse SDK credential storage.
- Support local Docker or remote workspace execution.
- Document that this is single-user local/self-hosted behavior.

### Phase 2: Headless Support

- Add `auth_method="device_code"` as an option.
- Surface the verification URL and one-time code in the orchestrator UI.
- Persist credentials in a user-specific auth directory.
- Add a sign-out action that deletes stored OpenAI credentials.

### Phase 3: Hosted Credential Broker

- Move refresh-token storage out of the agent-server runtime.
- Store encrypted credentials per user and tenant.
- Refresh access tokens before session creation.
- Inject only short-lived access credentials into the `LLM` config.
- Avoid writing OAuth credentials into agent workspaces.
- Add audit logs for sign-in, token refresh, and sign-out.

### Phase 4: Upstream-Friendly SDK Extension

Consider contributing or requesting an SDK API like:

```python
LLM.from_openai_subscription_credentials(
    access_token=...,
    refresh_token=None,
    model="gpt-5.2-codex",
)
```

or:

```python
OpenAISubscriptionAuth.create_llm_from_access_token(...)
```

This would avoid relying on private fields like `_is_subscription` in hosted integrations.

---

## Minimal Agent Handoff Summary

Use `LLM.subscription_login()` when the same process that creates the OpenHands `Agent` can own OAuth credentials.

For personal or self-hosted use, persist `~/.openhands/auth/` and use browser or device-code login.

For hosted multi-user use, do not store refresh tokens inside shared agent-server containers or workspaces. Use a credential broker that stores refresh tokens per user, refreshes access tokens, and constructs a subscription-backed `LLM` for each conversation.

The reason this works with `openhands agent-server` is that subscription authentication ultimately produces a standard OpenHands SDK `LLM` object. The remote `agent-server` does not need a special OAuth UI path as long as the SDK side, server side, or credential broker can construct the correct subscription-backed `LLM` object before the conversation runs.