Skip to content

fix(core): explain a 403 "Workspace access denied" from OpenCode Console instead of showing it raw #58891

Open

Description

@GladSI

Description

Selecting any OpenCode Console (Zen) model in the TUI/CLI fails with the bare message:

Error: Workspace access denied

The message is the upstream HTTP body ({"type":"error","error":{"type":"permission_error","message":"Workspace access denied"}}).
Nothing in the client says that this is a server-side workspace/account restriction, so it reads like a local
permissions bug: users reinstall, re-login, rotate keys, and open account-support issues (see #58314, #58404, #57245,
#56703).

Every other provider keeps working from the same machine at the same time, which makes the failure look model-specific.

Environment

  • opencode v2.1.4 (Linux), TUI and opencode run
  • provider: OpenCode Console (opencode), credential stored via /connect
  • model: opencode/deepseek-v4.1-flash-free (also fails for paid Console models, e.g. opencode/deepseek-v4.1-flash)
  • control: a plain @ai-sdk/openai-compatible provider with a direct vendor key works from the same session

Evidence

Client log (~/.local/share/opencode/log/opencode.log):

level=WARN  message="failed to load OpenCode provider config"
            cause="HttpClientError: StatusCode: non 2xx status code (403 GET https://opencode.ai/console/api/v2/config)"
level=ERROR message="Failed to drain Session"
            cause="AI.Error: Workspace access denied ... [cause]: AI.Error.Authentication: Workspace access denied"

Same stored Console credential, replayed outside the client:

GET  https://opencode.ai/console/api/v2/config
  -> 403 {"_tag":"Forbidden"}

POST https://opencode.ai/inference/openai/v1/chat/completions   (model: deepseek-v4.1-flash)
  -> 403 {"type":"error","error":{"type":"permission_error","message":"Workspace access denied"}}

A second (expired) key on the same account answers 401 api_key_expired, so the two failures are distinguishable
server-side — only the workspace-denied one reaches the user unexplained.

Why it is unactionable today

In packages/core/src/plugin/provider/opencode.ts:

  • fetchConfig() handles 401 (SignedOutError) and a 403 whose body is SsoRequired. Any other 403 — including
    Console's {"_tag":"Forbidden"} — falls through to filterStatusOk, is logged as the generic
    "failed to load OpenCode provider config" warning, and produces no connection status via connectionStatus().
  • explain() only rewrites the inference response when reported is set. With reported === undefined the raw
    upstream body is left in place, so the TUI shows Workspace access denied and the CLI shows it as
    AI.Error.Authentication.

That is the missing branch: a 403 that is definitely not SSO and not an expired key currently has no message.

Proposal

  1. Parse the 403 {"_tag":"Forbidden"} from /api/v2/config and a permission_error body from inference, and give
    them a status like the existing needs_auth path — e.g. status: "restricted" with the text:
    "Your OpenCode Console workspace is restricted (HTTP 403). Check the workspace at
    https://opencode.ai/console, or contact [email protected] if it stays restricted."
  2. Keep the general fallback: for any Console 401/403 whose body is not recognised, append the Console URL and the
    support address instead of passing the raw string through, so the failure is never a dead end.
  3. Optional: treat the config-load 403 as a warning that names the workspace/connection (the x-org-id header is
    only sent when metadata.orgID exists, so an org-scoped key gives no context in the log either).

This mirrors what #51847 already did for the SSO case; the workspace-restriction case is the remaining one.

Related

Activity

  1. opencode-agent commented on Oct 25, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up, @GladSI. You're asking for a 403 from the OpenCode Console that is neither the SSO case nor an expired key to get a clear message, such as "your Console workspace is restricted, check https://opencode.ai/console or contact [email protected]". Today the raw Workspace access denied text is shown. You also suggest a general fallback for any Console 401/403 the client doesn't recognise.

    Your reading of the code is right. In v2.1.4, fetchConfig() and connectionStatus() handle only the signed-out and SsoRequired cases, and explain() rewrites the response only when one of those was reported. So this is a gap in the error messages, not a malfunction, and I've marked it as a feature request for the maintainers to consider.

    Related issues and PRs:

    The 403 itself comes from a restriction on the account, which can't be fixed from the app. If your own workspace is still restricted, please email [email protected], and keep account details out of this public issue.

  2. GladSI commented on Oct 25, 2026

    @GladSI
    Author

    Follow-up from the reporter: the 403 is credential-scoped, and the fix is an interactive re-login

    After opencode auth login opencode --method device (same account, org Default), every Console path works again — while the credential that was active before the re-login still returns the 403:

    request old credential (API key oc_sk_…, created 2026-10-18) new credential (device OAuth)
    GET /console/api/v2/config 403 {"_tag":"Forbidden"} 200, orgID=wrk_…, orgName="Default"
    POST /inference/openai/v1/chat/completions, paid deepseek-v4.1-flash 403 permission_error: Workspace access denied 200 (completion)
    opencode run -m opencode/deepseek-v4.1-flash AI.Error.Authentication: Workspace access denied OK
    opencode run -m opencode/deepseek-v4.1-flash-free same 403 OK

    So the restriction is scoped to the stored credential / the workspace it belongs to — not to the machine, the client, or the account as a whole. In my case the recovery was one command, which is exactly why the missing message costs so much: nothing in the failure suggests it, and the credential that has to be replaced is not named anywhere.

    Two details that make the message cheaper to write:

    1. An API-key credential has no metadata.orgID, so the plugin sends no x-org-id header (packages/core/src/plugin/provider/opencode.ts, headers()), while a device login carries orgID/orgName. That is precisely the credential shape that can end up workspace-restricted and cannot be described in a warning — which is why the log line is worth enriching too.
    2. The 403 from /api/v2/config is the only signal the client gets, and it is currently flattened into the generic failed to load OpenCode provider config WARN — byte-identical to a transient network failure in opencode.log, so a user pasting their log cannot tell the two apart.

    Suggested fallback text for a permission_error rejection, in addition to the restricted status proposed in the report:

    The OpenCode Console credential "…" was rejected: the workspace it belongs to is restricted (HTTP 403). Sign in again with opencode auth login opencode, or check the workspace at https://opencode.ai/console.

    The code locations in the original report are still the right place to add it (fetchConfig() / connectionStatus() / explain()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions