Skip to content

feat(cli): add the OrcaRouter Cyber Inference model provider - #3739

Open
beilsteinernest215-cmd wants to merge 1 commit into
chainloop-dev:mainfrom
beilsteinernest215-cmd:orcarouter/task-10736
Open

beilsteinernest215-cmd wants to merge 1 commit into
chainloop-dev:mainfrom
beilsteinernest215-cmd:orcarouter/task-10736

Conversation

@beilsteinernest215-cmd

@beilsteinernest215-cmd beilsteinernest215-cmd commented Oct 28, 2026 •

Copy link
Copy Markdown

What this adds

Chainloop Trace records AI coding sessions, and its Codex harness is the one place in the tree that names a model provider (app/cli/internal/trace/e2e/codex.go builds model_providers.openrouter today). This adds OrcaRouter Cyber Inference as a named, first-class provider next to it, with both credential paths in one place. OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint. OrcaRouter's cyber tier is aimed at security teams doing authorized, scoped work: https://www.orcarouter.ai/cyber and https://www.orcarouter.ai/models?tier=cyber.

Provider and credentials

  • internal/orcarouter holds the provider definition (BaseURL = "https://api.orcarouter.ai/v1", EnvKey = "ORCAROUTER_API_KEY"), the credential seam, its lifetime rules, the model catalog with capability filters, and the chat client.
  • Two adapters end at the same durable OrcaRouter API key, and nothing downstream knows which one produced it:
    • API key — a pasted sk-orca-… key, stored next to auth.token in the CLI's existing devel config profile and cleared by logout.
    • Connect with OrcaRouter (OAuth 2.0 + PKCE) — Flow A loopback redirect, chosen because the CLI can bind 127.0.0.1 and receives the code without copy/paste. S256 challenge from a per-attempt crypto/rand verifier, fresh state per attempt, and a constant-time state comparison before the code is used. The minted key is durable, not a refresh token, so a 401 marks exactly the account/generation that made the request as needing reauthentication; there is no fake refresh grant.
  • The two origins stay apart: consent and exchange on https://www.orcarouter.ai (/auth, /api/v1/auth/keys), inference and model discovery on https://api.orcarouter.ai/v1.
  • chainloop orcarouter login|connect|status|models|chat|logout exposes both paths; connect --no-browser prints the authorization URL for headless machines.

Models

  • The model control is a selector built from live GET /v1/models, never free text. chat filters to chat-capable models and --image-input narrows to models whose architecture.input_modalities explicitly declare image; undeclared models fail closed.
  • Recommended order: orcarouter/auto is the default; OrcaCyber Ultra (orcacyber-ultra-1.0, security code scanning and multi-step reasoning over large repositories) and OrcaCyber Zero (orcacyber-zero-1.5, OrcaCyber-harness driven) are the dedicated cyber models for teams with business verification; orcarouter/fusion is the general fallback. The small cold-start seed exists only for outage; a successful live fetch is authoritative.

Trace harness

The Codex harness accepts orcarouter:<model id> and passes a named model_providers.orcarouter={name=…,base_url=…,env_key=…}, so a Trace session can run on orcarouter/auto or a cyber model. No other provider's behavior changes.

Verification

All checks run through the provider code in this patch in a scrubbed environment:

  • go test ./internal/orcarouter/ — provider, both credential adapters, PKCE (verifier/challenge/state, authorize URL, exchange path and body, rejection, state mismatch, code reuse, scope read back from the response, 401 generation handling, 429, network failure), catalog and capability filters, error classification. Tests assert the key and verifier never appear in errors or logs.
  • go test ./app/cli/cmd/ -run 'TestOrcaRouter|TestDescribeConnectError' — command wiring for both paths.
  • go test ./app/cli/internal/trace/e2e/ -run 'TestCodex…' — the harness still emits the OpenRouter definition unchanged and now emits the OrcaRouter one.
  • Live: through the implemented client, GET /v1/models returned 21 models, an --image-input run narrowed to 3, and a chat completion returned a non-empty answer. The verification key is not entitled to orcarouter/auto or orcarouter/fusion yet (HTTP 403 model_access_denied), so the live chat used deepseek/deepseek-v4.1-flash — a connectivity probe only, not a user-facing recommendation. That 403 is handled explicitly as a model-permission error rather than a rejected credential, since a new login would not fix it; TestChatModelNotAllowed covers it.

Limits

  • Only the Codex harness and the new chat command are model entry points in this tree, and both are text-only, so no multimodal selector is wired to a UI.
  • Flow C device grant is not implemented; Flow A covers headless use via the printed URL.

I'm an engineer on the OrcaRouter team.

It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes.

View guided diff

Signed-off-by: beilsteinernest215-cmd <beilsteinernest215-cmd@users.noreply.github.com>
@chainloop-platform

chainloop-platform Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

PR validation — ⚠️ 1 failing

Status Policy Material Messages
⚠️ Failed pr-min-approvals pr-info PR/MR #3739 has 0 approving reviews, 1 required.
✅ Passed pr-description-required pr-info -
✅ Passed pr-user-story-linked pr-info -

View attestation ↗

AI Session Checks — ⚠️ no AI session found

Missing AI Coding Sessions

This organization requires every PR to be backed by a Chainloop Trace AI coding session, and none was found for this one.

Please make sure the AI coding session evidence has been sent by the Chainloop CLI, or add the skip-ai-session label to this PR to bypass this check.

Learn more about Chainloop Trace.

Security Checks — ✅ 5 passing

✅ secret-scan

Status Policy Messages
✅ Passed secrets-detection -

✅ sast-scan

Status Policy Messages
✅ Passed owasp-top10-2025 -
✅ Passed sast -
✅ Passed cwe-top25 -
✅ Passed cwe-top26-40-cusp -
Scans not applied (3)
Scan Reason
vulnerability-scan no manifest/lockfile changed
github-actions-scan no workflow files changed
iac-scan no IaC files changed

View attestation ↗

Security context

✅ Nothing this change touches has a recorded security-fix history.

View in Chainloop ↗ · How this works ↗


Powered by Chainloop and Chainloop Trace

@cubic-dev-ai cubic-dev-ai 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.

12 issues found across 20 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/cli/README.md">

<violation number="1" location="app/cli/README.md:20">
P2: This setup implies that `login` or `connect` configures `trace run --codex`, but Trace never reads the stored OrcaRouter credential; Codex still requires `ORCAROUTER_API_KEY` in its environment and a provider/model selection. Clarify the required environment/configuration here, or inject the stored credential into the wrapped Codex process.</violation>
</file>

<file name="internal/orcarouter/pkce.go">

<violation number="1" location="internal/orcarouter/pkce.go:243">
P2: The printed `--no-browser` URL cannot complete when opened on another machine because the redirect targets that machine's `127.0.0.1`, not the CLI host. Keep this mode explicitly local-only or provide a reachable/manual callback flow instead of advertising that it works elsewhere.</violation>

<violation number="2" location="internal/orcarouter/pkce.go:401">
P2: `Exchange` treats every scope string that differs byte-for-byte from the request as a downgrade. OAuth scopes are order-insensitive token sets, so multi-scope grants such as `"profile api"` are rejected when `"api profile"` was requested; compare scope tokens and reject only when a requested token is absent.</violation>
</file>

<file name="internal/orcarouter/inference.go">

<violation number="1" location="internal/orcarouter/inference.go:87">
P2: Trim the model before marshaling it. Currently surrounding whitespace passes validation but is sent to OrcaRouter, so a quoted `--model` value fails even when it names a valid catalog model.</violation>
</file>

<file name="app/cli/cmd/orcarouter.go">

<violation number="1" location="app/cli/cmd/orcarouter.go:292">
P2: `models` treats a 401 as degraded discovery and never marks the requesting credential generation for reauthentication, then lists the seed models as usable. Mark the generation and return an actionable reauthentication error for `ErrNeedsReauth` instead of falling back.</violation>

<violation number="2" location="app/cli/cmd/orcarouter.go:355">
P2: The chat command sends `orcarouter/auto` even when the current credential's live catalog does not offer that model, and it accepts arbitrary IDs despite saying models come from the catalog. Select or revalidate the default against live discovery, or require an explicitly listed `--model` instead of defaulting to an inaccessible recommendation.</violation>

<violation number="3" location="app/cli/cmd/orcarouter.go:367">
P2: The 401 reauthentication marker is lost when the `chat` process exits, so a subsequent `orcarouter status` cannot tell the user that the stored key was rejected. Persist the rejected generation (or another reauthentication marker) alongside the credential and reload it when constructing the manager.</violation>
</file>

<file name="internal/orcarouter/provider.go">

<violation number="1" location="internal/orcarouter/provider.go:57">
P2: `RecommendedModels` claims these IDs are included in cold-start seeding, but the degraded catalog omits both cyber models. Add verified seed entries for them, or update this comment and API so the recommendation list is explicitly documentation-only.</violation>
</file>

<file name="internal/orcarouter/catalog.go">

<violation number="1" location="internal/orcarouter/catalog.go:166">
P1: `fetchCatalog` sends the credential before validating the configured origin. A remote `http://` `ORCA_API_BASE_URL` can therefore receive the API key in cleartext; call `ValidateOrigin` before creating the request, as `Client.Chat` does.</violation>

<violation number="2" location="internal/orcarouter/catalog.go:230">
P2: `rawModel.model` drops the catalog’s declared output modalities, so the output-modality guard in `IsTextChat` never runs. Carry that metadata into `Model` and exclude records whose declared outputs omit text; otherwise non-chat models can appear in the text selector and fail when used.</violation>
</file>

<file name="app/cli/internal/trace/README.md">

<violation number="1" location="app/cli/internal/trace/README.md:220">
P2: The checked-in E2E suite does not run any OrcaRouter model: `config.yaml` lists only the OpenRouter Codex model, and the runner filters configured models rather than adding new ones. Add an OrcaRouter model to the suite configuration or describe this as harness support only.</violation>
</file>

<file name="app/cli/documentation/cli-reference.md">

<violation number="1" location="app/cli/documentation/cli-reference.md:2068">
P2: The documented browser-failure fallback is not implemented: a failed default browser opener returns an error without printing the URL. Say that users must pass `--no-browser` to print the authorization URL.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

View guided diff | Re-trigger cubic

if cred.Key == "" {
return nil, errors.New("no OrcaRouter credential")
}
baseURL = NormalizeBase(baseURL)

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.

P1: fetchCatalog sends the credential before validating the configured origin. A remote http:// ORCA_API_BASE_URL can therefore receive the API key in cleartext; call ValidateOrigin before creating the request, as Client.Chat does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/orcarouter/catalog.go, line 166:

<comment>`fetchCatalog` sends the credential before validating the configured origin. A remote `http://` `ORCA_API_BASE_URL` can therefore receive the API key in cleartext; call `ValidateOrigin` before creating the request, as `Client.Chat` does.</comment>

<file context>
@@ -0,0 +1,365 @@
+	if cred.Key == "" {
+		return nil, errors.New("no OrcaRouter credential")
+	}
+	baseURL = NormalizeBase(baseURL)
+	if baseURL == "" {
+		baseURL = BaseURL
</file context>

Comment thread app/cli/README.md

## OrcaRouter Cyber Inference

`chainloop orcarouter` configures [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible inference gateway for security teams, so `chainloop trace run --codex` and the Trace end-to-end suite can route a Codex session through `https://api.orcarouter.ai/v1` with `model_providers.orcarouter`. The provider is `OrcaRouter Cyber Inference`: cyber models for an authorized red team, gateway controls for a blue team.

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.

P2: This setup implies that login or connect configures trace run --codex, but Trace never reads the stored OrcaRouter credential; Codex still requires ORCAROUTER_API_KEY in its environment and a provider/model selection. Clarify the required environment/configuration here, or inject the stored credential into the wrapped Codex process.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/cli/README.md, line 20:

<comment>This setup implies that `login` or `connect` configures `trace run --codex`, but Trace never reads the stored OrcaRouter credential; Codex still requires `ORCAROUTER_API_KEY` in its environment and a provider/model selection. Clarify the required environment/configuration here, or inject the stored credential into the wrapped Codex process.</comment>

<file context>
@@ -15,6 +15,22 @@ b) [Attestation Crafting Process](https://docs.chainloop.dev/getting-started/att
 
+## OrcaRouter Cyber Inference
+
+`chainloop orcarouter` configures [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible inference gateway for security teams, so `chainloop trace run --codex` and the Trace end-to-end suite can route a Codex session through `https://api.orcarouter.ai/v1` with `model_providers.orcarouter`. The provider is `OrcaRouter Cyber Inference`: cyber models for an authorized red team, gateway controls for a blue team.
+
+Two credential paths end at the same durable `sk-orca-…` API key, and either works alone:
</file context>
Suggested change
`chainloop orcarouter` configures [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible inference gateway for security teams, so `chainloop trace run --codex` and the Trace end-to-end suite can route a Codex session through `https://api.orcarouter.ai/v1` with `model_providers.orcarouter`. The provider is `OrcaRouter Cyber Inference`: cyber models for an authorized red team, gateway controls for a blue team.
`chainloop orcarouter` manages [OrcaRouter](https://www.orcarouter.ai) credentials, model discovery, and direct chat. To route Codex through `https://api.orcarouter.ai/v1`, set `ORCAROUTER_API_KEY` and select a model as `orcarouter:<model id>`; `chainloop trace run --codex` wraps the supplied Codex command and does not read the stored key. The provider is `OrcaRouter Cyber Inference`: cyber models for an authorized red team, gateway controls for a blue team.


// Listen first, so the callback URL is known before the browser opens and
// no approval can race the listener.
listener, err := net.Listen("tcp", "127.0.0.1:0")

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.

P2: The printed --no-browser URL cannot complete when opened on another machine because the redirect targets that machine's 127.0.0.1, not the CLI host. Keep this mode explicitly local-only or provide a reachable/manual callback flow instead of advertising that it works elsewhere.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/orcarouter/pkce.go, line 243:

<comment>The printed `--no-browser` URL cannot complete when opened on another machine because the redirect targets that machine's `127.0.0.1`, not the CLI host. Keep this mode explicitly local-only or provide a reachable/manual callback flow instead of advertising that it works elsewhere.</comment>

<file context>
@@ -0,0 +1,422 @@
+
+	// Listen first, so the callback URL is known before the browser opens and
+	// no approval can race the listener.
+	listener, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return Credential{}, fmt.Errorf("listen on loopback: %w", err)
</file context>

if cred.Key == "" {
return "", ErrNoCredential
}
if strings.TrimSpace(req.Model) == "" {

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.

P2: Trim the model before marshaling it. Currently surrounding whitespace passes validation but is sent to OrcaRouter, so a quoted --model value fails even when it names a valid catalog model.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/orcarouter/inference.go, line 87:

<comment>Trim the model before marshaling it. Currently surrounding whitespace passes validation but is sent to OrcaRouter, so a quoted `--model` value fails even when it names a valid catalog model.</comment>

<file context>
@@ -0,0 +1,158 @@
+	if cred.Key == "" {
+		return "", ErrNoCredential
+	}
+	if strings.TrimSpace(req.Model) == "" {
+		return "", errors.New("no model selected")
+	}
</file context>

Comment thread app/cli/cmd/orcarouter.go
return fmt.Errorf("%w; run `chainloop orcarouter login` or `chainloop orcarouter connect`", orcarouter.ErrNoCredential)
}
if model == "" {
model = orcarouter.DefaultModel

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.

P2: The chat command sends orcarouter/auto even when the current credential's live catalog does not offer that model, and it accepts arbitrary IDs despite saying models come from the catalog. Select or revalidate the default against live discovery, or require an explicitly listed --model instead of defaulting to an inaccessible recommendation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/cli/cmd/orcarouter.go, line 355:

<comment>The chat command sends `orcarouter/auto` even when the current credential's live catalog does not offer that model, and it accepts arbitrary IDs despite saying models come from the catalog. Select or revalidate the default against live discovery, or require an explicitly listed `--model` instead of defaulting to an inaccessible recommendation.</comment>

<file context>
@@ -0,0 +1,374 @@
+		return fmt.Errorf("%w; run `chainloop orcarouter login` or `chainloop orcarouter connect`", orcarouter.ErrNoCredential)
+	}
+	if model == "" {
+		model = orcarouter.DefaultModel
+	}
+
</file context>

if id == "" {
return Model{}, false
}
out := Model{

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.

P2: rawModel.model drops the catalog’s declared output modalities, so the output-modality guard in IsTextChat never runs. Carry that metadata into Model and exclude records whose declared outputs omit text; otherwise non-chat models can appear in the text selector and fail when used.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/orcarouter/catalog.go, line 230:

<comment>`rawModel.model` drops the catalog’s declared output modalities, so the output-modality guard in `IsTextChat` never runs. Carry that metadata into `Model` and exclude records whose declared outputs omit text; otherwise non-chat models can appear in the text selector and fail when used.</comment>

<file context>
@@ -0,0 +1,365 @@
+	if id == "" {
+		return Model{}, false
+	}
+	out := Model{
+		ID:                   id,
+		Name:                 m.Name,
</file context>


The configured OpenCode 2, Pi, and Codex models use OpenRouter and need `OPENROUTER_API_KEY`. A Codex model without the `openrouter/` prefix uses the default Codex provider and needs `CODEX_API_KEY`. The pinned Codex model is `openrouter/openai/gpt-6-luna`. GPT-5 Mini frequently ended the scripted task before committing and pushing, so it is not the default test model.

A Codex model can also run on [OrcaRouter](https://www.orcarouter.ai), the OpenAI-compatible gateway for security teams: write it as `orcarouter:<model id>`, for example `orcarouter:orcarouter/auto`, and set `ORCAROUTER_API_KEY`. The suite passes `model_providers.orcarouter` to Codex with `base_url = https://api.orcarouter.ai/v1`, the same definition `chainloop orcarouter` uses. `chainloop orcarouter login` stores a pasted key and `chainloop orcarouter connect` obtains one with OAuth 2.0 + PKCE; either credential works, and `chainloop orcarouter models` lists the live catalog. See the [Cyber Inference models](https://www.orcarouter.ai/models?tier=cyber).

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.

P2: The checked-in E2E suite does not run any OrcaRouter model: config.yaml lists only the OpenRouter Codex model, and the runner filters configured models rather than adding new ones. Add an OrcaRouter model to the suite configuration or describe this as harness support only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/cli/internal/trace/README.md, line 220:

<comment>The checked-in E2E suite does not run any OrcaRouter model: `config.yaml` lists only the OpenRouter Codex model, and the runner filters configured models rather than adding new ones. Add an OrcaRouter model to the suite configuration or describe this as harness support only.</comment>

<file context>
@@ -217,6 +217,8 @@ The harnesses run as follows:
 
 The configured OpenCode 2, Pi, and Codex models use OpenRouter and need `OPENROUTER_API_KEY`. A Codex model without the `openrouter/` prefix uses the default Codex provider and needs `CODEX_API_KEY`. The pinned Codex model is `openrouter/openai/gpt-6-luna`. GPT-5 Mini frequently ended the scripted task before committing and pushing, so it is not the default test model.
 
+A Codex model can also run on [OrcaRouter](https://www.orcarouter.ai), the OpenAI-compatible gateway for security teams: write it as `orcarouter:<model id>`, for example `orcarouter:orcarouter/auto`, and set `ORCAROUTER_API_KEY`. The suite passes `model_providers.orcarouter` to Codex with `base_url = https://api.orcarouter.ai/v1`, the same definition `chainloop orcarouter` uses. `chainloop orcarouter login` stores a pasted key and `chainloop orcarouter connect` obtains one with OAuth 2.0 + PKCE; either credential works, and `chainloop orcarouter models` lists the live catalog. See the [Cyber Inference models](https://www.orcarouter.ai/models?tier=cyber).
+
 Some scenarios add the git commands of a human after the session, for example a commit that replaces an AI line, an amend, or a rename, and then push again. They work on a feature branch, because the push reads only the commits that are not on the default branch of the remote.
</file context>

Comment thread app/cli/cmd/orcarouter.go
return fmt.Errorf("%w; run `chainloop orcarouter login` or `chainloop orcarouter connect`", orcarouter.ErrNoCredential)
}

cat := orcarouter.DiscoverCatalog(cmd.Context(), nil, orcarouter.APIOrigin(), cred)

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.

P2: models treats a 401 as degraded discovery and never marks the requesting credential generation for reauthentication, then lists the seed models as usable. Mark the generation and return an actionable reauthentication error for ErrNeedsReauth instead of falling back.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/cli/cmd/orcarouter.go, line 292:

<comment>`models` treats a 401 as degraded discovery and never marks the requesting credential generation for reauthentication, then lists the seed models as usable. Mark the generation and return an actionable reauthentication error for `ErrNeedsReauth` instead of falling back.</comment>

<file context>
@@ -0,0 +1,374 @@
+				return fmt.Errorf("%w; run `chainloop orcarouter login` or `chainloop orcarouter connect`", orcarouter.ErrNoCredential)
+			}
+
+			cat := orcarouter.DiscoverCatalog(cmd.Context(), nil, orcarouter.APIOrigin(), cred)
+			if cat.Degraded {
+				fmt.Fprintf(cmd.OutOrStderr(), "Live discovery failed (%v); showing the verified fallback catalog.\n", cat.Err)
</file context>

Comment on lines +2068 to +2069
When no browser can be opened, the authorization URL is printed so it can be
visited elsewhere; the loopback callback still receives the result.

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.

P2: The documented browser-failure fallback is not implemented: a failed default browser opener returns an error without printing the URL. Say that users must pass --no-browser to print the authorization URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/cli/documentation/cli-reference.md, line 2068:

<comment>The documented browser-failure fallback is not implemented: a failed default browser opener returns an error without printing the URL. Say that users must pass `--no-browser` to print the authorization URL.</comment>

<file context>
@@ -1964,6 +1964,318 @@ Options inherited from parent commands
+The key is durable and is reused until it is revoked. Re-authorizing on every
+launch would exhaust the per-user issuance limit.
+
+When no browser can be opened, the authorization URL is printed so it can be
+visited elsewhere; the loopback callback still receives the result.
+
</file context>
Suggested change
When no browser can be opened, the authorization URL is printed so it can be
visited elsewhere; the loopback callback still receives the result.
Use `--no-browser` to print the authorization URL when a browser is unavailable; the loopback callback still receives the result.

}

cred := Credential{Key: decoded.Key, Source: SourcePKCE, UserID: decoded.UserID, Scope: decoded.Scope}
if requestedScope != "" && decoded.Scope != "" && decoded.Scope != requestedScope {

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.

P2: Exchange treats every scope string that differs byte-for-byte from the request as a downgrade. OAuth scopes are order-insensitive token sets, so multi-scope grants such as "profile api" are rejected when "api profile" was requested; compare scope tokens and reject only when a requested token is absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/orcarouter/pkce.go, line 401:

<comment>`Exchange` treats every scope string that differs byte-for-byte from the request as a downgrade. OAuth scopes are order-insensitive token sets, so multi-scope grants such as `"profile api"` are rejected when `"api profile"` was requested; compare scope tokens and reject only when a requested token is absent.</comment>

<file context>
@@ -0,0 +1,422 @@
+	}
+
+	cred := Credential{Key: decoded.Key, Source: SourcePKCE, UserID: decoded.UserID, Scope: decoded.Scope}
+	if requestedScope != "" && decoded.Scope != "" && decoded.Scope != requestedScope {
+		return Credential{}, fmt.Errorf("%w: granted %q, requested %q", ErrScopeDowngrade, decoded.Scope, requestedScope)
+	}
</file context>

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