Skip to content

fix(provider-auth): send the installation device ID on ChatGPT subscription login - #462

Merged
bubbleptr merged 2 commits into
mainfrom
fix/openai-subscription-device-id
Oct 13, 2026
Merged

bubbleptr merged 2 commits into
mainfrom
fix/openai-subscription-device-id

Conversation

@bubbleptr

@bubbleptr bubbleptr commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

OpenAI rejects a ChatGPT subscription login that carries no agent host ID, so the openai card's Login with subscription button failed before it could open a browser. Pace never passed LoginOptions.getDeviceId:

   await runtime.login(providerId, "oauth", {
     prompt: …,
     notify: …,
-  });
+  }, { getDeviceId });

The ID comes from Pi's settings manager — the same store the Pi TUI uses — so a machine registers as one installation rather than one per client:

createProviderAuthService (packages/backend/src/workspace/provider-auth.ts)
  getDeviceId = createDeviceIdReader(agentDir)
    SettingsManager.create(agentDir, agentDir).getOrCreateDeviceId()   # ~/.pi/agent/settings.json
  loginOAuth → runtime.login(providerId, "oauth", interaction, { getDeviceId })

Only the OAuth login path changes; the API-key path never reads a device ID, and the legacy openai-codex flow (which does not require one) is untouched.

Evidence

  • Before: clicking the button rejected with Sign in with ChatGPT requires a device ID (UUID) for this installation (pi-ai/dist/auth/oauth/openai-chatgpt.js:186), thrown before the authorize URL was built.
  • After: the real flow now builds the authorize URL with the ID that lands in settings:
authorize host: urn:uuid:259d3d49-1b19-4436-9e19-60ae48528a7d
stored deviceId: 259d3d49-1b19-4436-9e19-60ae48528a7d
  • Two tests, both red before the fix (getDeviceId was undefined):
    • keeps one ChatGPT device ID for the installation — creates a UUID and persists it to settings.json before a later login reads it back.
    • reuses the device ID the Pi TUI stored for this installation — a pre-written deviceId is what the login sends.
  • bun run typecheck, bun run lint, bun run test (2122 passed) and bun run build are green.

Merge Danger

Door: two-way. Reverting restores the previous behavior (login rejected for the OpenAI subscription card); no data migration, and the deviceId field Pi already writes is unaffected.

Blast Radius: one provider card. A deviceId is now written to ~/.pi/agent/settings.json on the first OpenAI subscription login — the same field the Pi TUI writes, shared with it. Users signed in through the legacy openai-codex card keep working exactly as before.


Devin Review

…iption login

OpenAI registers a ChatGPT subscription login against an agent host ID and
rejects it with "Sign in with ChatGPT requires a device ID (UUID) for this
installation" when the app supplies none. Pace never passed
`LoginOptions.getDeviceId`, so the OpenAI card's login failed before it could
open a browser; the legacy `openai-codex` flow, which does not read a device ID,
is why this went unnoticed.

Read and create the ID through Pi's settings manager, where the Pi TUI keeps it,
so both clients register as one installation instead of two.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +262 to +265
deviceId ??= SettingsManager.create(agentDir, agentDir, {
projectTrusted: false,
}).getOrCreateDeviceId();
return deviceId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Installation ID can change between logins

On the first login, getDeviceId returns before getOrCreateDeviceId persists the ID. Another process can read the old settings and register a second agent host.

Learn more

Pi's settings manager queues writes rather than making them durable in its setters; withPackages waits for flush() and checks drainErrors() before reporting success. Here, the first login gives the new ID to the OAuth flow before the queued settings write finishes. A separate backend or Pi process can create another ID during that gap; a failed write can leave every future process without the original ID. The new test explicitly waits for persistence before it starts the second login, so it does not exercise this gap.

Example: Pace creates ID A for its first ChatGPT login. Before its settings write completes, Pi starts a login, sees no device ID on disk, and creates ID B. The two clients register different agent hosts for one installation.

Recommended fix: Persist the newly generated ID and check SettingsManager.drainErrors() before handing it to the OAuth callback, or initialize it asynchronously before calling runtime.login. Serialize concurrent initializations and test immediate cross-instance logins and write failures.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both halves check out, and the gap was real: at the moment the flow asked for the ID, <agentDir>/settings.json had no deviceId yet (expected undefined to be '33117e80-…'), and with a corrupt settings file the login resolved instead of rejecting. save() returns early when the file fails to load, and flush() only awaits the queue — errors land in drainErrors().

Fixed in 104555b: the ID is resolved, flushed and checked before runtime.login, matching the convention resource-management.ts already uses for Pi's settings writes.

       if (!provider.auth.oauth) { … }
+      const deviceId = await resolveDeviceId(options.agentDir);
       await runtime.login(providerId, "oauth", interaction, { getDeviceId: () => deviceId });
async function resolveDeviceId(agentDir: string): Promise<string> {
  const settings = SettingsManager.create(agentDir, agentDir, { projectTrusted: false });
  const deviceId = settings.getOrCreateDeviceId();
  await settings.flush();
  const [failure] = settings.drainErrors();
  if (failure) throw new Error(`Could not save this installation's ID to ${failure.path ?? failure.scope}: …`);
  return deviceId;
}

The test no longer waits for persistence — the stub reads the settings file synchronously inside login, exactly as another process would, and asserts it already holds the ID handed to the flow. A second test covers the write failure (corrupt settings file → login rejected before the flow runs, login never called).

One part I did not take: serializing concurrent first-time creations. getOrCreateDeviceId() decides from the globalSettings snapshot loaded at construction, outside the storage lock — persistScopedSettings only merges modified fields into the freshly-read file after the decision. So a client that already loaded settings (a running Pi TUI) generates its own ID no matter how early we flush; two clients that both start before either writes will still disagree. That is Pi's settings-manager design and the TUI has the same property, so it needs an upstream fix (a lock-protected read-or-create) rather than one here. Eager + flushed persistence is what Pace can do: it guarantees the ID exists for every client that starts later, which is the case that matters in practice.

Pi's settings setters only queue their write, so the first ChatGPT login handed
the OAuth flow an ID that was not on disk yet, and a failed save went unnoticed:
`save()` returns early when the settings file cannot be loaded, and `flush()`
folds write errors into `drainErrors()`.

Resolve the ID before `runtime.login` and flush it, so the Pi TUI — which reads
the same file from another process — sees the ID OpenAI was told. Surface a save
that fails instead of churning the agent host on every later login.
@bubbleptr
bubbleptr merged commit 48e342b into main Oct 13, 2026
1 check passed
@bubbleptr
bubbleptr deleted the fix/openai-subscription-device-id branch October 13, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant