Skip to content

feat(providers): add Nous Portal (Nous Research) OAuth provider — device grant + free/paid live catalog - #1397

Draft
Cheurteenyt wants to merge 3 commits into
lidge-jun:devfrom
Cheurteenyt:codex/nous-portal-oauth
Draft

feat(providers): add Nous Portal (Nous Research) OAuth provider — device grant + free/paid live catalog#1397
Cheurteenyt wants to merge 3 commits into
lidge-jun:devfrom
Cheurteenyt:codex/nous-portal-oauth

Conversation

@Cheurteenyt

@Cheurteenyt Cheurteenyt commented Aug 10, 2026

Copy link
Copy Markdown

Closes #1148

What

Adds Nous Portal (Nous Research) as a first-class OAuth provider, matching the device-grant flow Hermes Agent uses against the same backend.

OAuth flow (RFC 8628 device authorization grant)

  • POST https://portal.nousresearch.com/api/oauth/device/code (client_id=hermes-cli, scope=inference:invoke) → user_code + verification URL surfaced via the controller (onAuth), same UX as Kimi/Kiro.
  • Poll POST .../api/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code, handling authorization_pending, slow_down (backoff), access_denied, expired_token.
  • The access token IS the per-request inference JWT (scope inference:invoke) → used directly as Authorization: Bearer against the OpenAI-compatible endpoint https://inference-api.nousresearch.com/v1 (adapter: openai-chat).

Refresh (single-use rotation)

  • Refresh posts the refresh token in the x-nous-refresh-token header (not the body) with grant_type=refresh_token + client_id.
  • Nous refresh tokens are single-use and rotated on every refresh; reuse is treated as theft and revokes the session (refresh_token_reused). The refresh path persists the rotated token immediately and stays on the default lazy-only refresh policy — no proactive background refresh for this provider.

Registry & catalog

  • nous registry entry: featured, freeTier: true, liveModels: true with discovery on /v1/models (max 512 models). Catalog is a mix of paid models and :free slugs; free-tier gating is decided live by the Portal per account, with a static fallback seed for the logged-out state (see below).
  • NousTokenError mapped to terminal refresh errors (invalid_grant, refresh_token_reused, revoked, revoked_token, expired_token).

Free model seed (verified against the live Portal list, 2026-08-10)

The registry ships a static fallback seed with the 4 :free models currently advertised by the Portal — confirmed against the public endpoint Hermes Agent uses (https://portal.nousresearch.com/api/nous/recommended-models):

  • tencent/hy3:free
  • poolside/laguna-s-2.1:free
  • stepfun/step-3.7-flash:free
  • poolside/laguna-xs-2.1:free

Note: inclusionai/ling-3.0-flash:free was removed from the Portal's free list (404 on the inference API since 2026-08-07) and is therefore not seeded.

Paid catalog value

Beyond the free tier, the Nous Portal paid catalog is significant. Nous Research's own announcements:

With liveModels: true discovery, all paid models (including the discounted DeepSeek V4 Flash 0731) show up automatically once a Portal account is connected.

Multiauth

  • Identity derived from the JWT (sub → accountId, lowercased email when present); multiple Portal accounts are stored/upserted per sub like other OAuth providers.

Tests & docs

  • tests/nous-oauth.test.ts (7 tests): JWT identity, refresh header/rotation wiring, mocked device-grant login, multiauth (append/upsert). All network calls mocked via NOUS_PORTAL_BASE_URL — no real login performed.
  • Updated golden lists in tests/provider-registry-parity.test.ts (featured set + freeTier list) and provider docs (en/ja/ko/ru/zh-cn).

Verification

  • bun run typecheck
  • bun run test tests/nous-oauth.test.ts tests/oauth-provider-reconcile.test.ts tests/catalog-oauth-observation.test.ts tests/provider-registry-parity.test.ts tests/kimi-oauth-identity.test.ts tests/kiro-oauth.test.ts tests/oauth-public-surface.test.ts tests/cli-provider.test.ts tests/oauth-login-summary.test.ts tests/repo-hygiene.test.ts ✅ (all green)

Note: no live Nous login was executed during development (credentials/OAuth state untouched); the flow is verified against Hermes Agent's hermes_cli/auth.py implementation and mocked responses.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features

    • Added Nous Portal as an OAuth provider.
    • Added device-based login with verification-code prompts, token refresh, and multi-account support.
    • Added live model discovery, including free and paid models, with a default free-tier model.
    • Updated provider documentation across supported languages.
  • Bug Fixes

    • Improved handling of authentication errors, token rotation, polling delays, cancellations, and timeouts.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/oauth/index.ts, src/oauth/nous.ts.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds Nous Portal device-grant OAuth, rotating refresh tokens, identity extraction, provider registration, live model discovery, parity tests, credential-store tests, and documentation updates.

Changes

Nous Portal integration

Layer / File(s) Summary
Nous OAuth protocol
src/oauth/nous.ts, tests/nous-oauth.test.ts
Adds device authorization, adaptive polling, cancellation, token validation, JWT identity extraction, refresh-token rotation, structured errors, and credential persistence tests.
Provider registration and parity
src/oauth/index.ts, src/providers/registry.ts, tests/provider-registry-parity.test.ts
Registers nous with OAuth, OpenAI-compatible inference, live model discovery, free-tier metadata, default-model resolution, and terminal refresh-error handling.
Provider documentation
docs-site/src/content/docs/guides/providers.md, docs-site/src/content/docs/{ja,ko,ru,zh-cn}/guides/providers.md
Documents Nous Portal and the ocx login nous command. Localized provider lists are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun, wibias

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant OAuthController
  participant NousPortal
  participant CredentialStore
  participant NousInference

  User->>OAuthController: ocx login nous
  OAuthController->>NousPortal: request device authorization
  NousPortal-->>OAuthController: return verification URL and user code
  OAuthController->>NousPortal: poll for authorization
  NousPortal-->>OAuthController: return access and rotated refresh tokens
  OAuthController->>CredentialStore: save account credentials
  User->>NousInference: use nous provider
  NousInference-->>User: return OpenAI-compatible model response
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The Russian documentation change adds GitHub Copilot instead of documenting Nous Portal, so it is unrelated to issue [#1148]. Remove or separately submit the GitHub Copilot change in docs-site/src/content/docs/ru/guides/providers.md.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the Nous Portal provider, OAuth device grant, and live free/paid catalog added by the pull request.
Linked Issues check ✅ Passed The changes implement the Nous provider, OAuth device flow, live model discovery, free-tier support, registry integration, tests, and documentation required by issue [#1148].
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions
github-actions Bot marked this pull request as draft August 10, 2026 03:58

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/ru/guides/providers.md`:
- Line 63: Update the Russian providers documentation sections around the preset
count, login commands, and provider table to match the English source: change
the OAuth total to eight, add the ocx login nous device-grant command, and add
the Nous provider row covering the openai-chat adapter, inference endpoint, live
paid/free model discovery, and rotated refresh tokens.

In `@src/oauth/nous.ts`:
- Around line 67-69: Update resolvePortalBaseUrl in src/oauth/nous.ts (lines
67-69) to parse the configured URL and reject non-HTTPS schemes, embedded
credentials, query strings, and fragments before returning the normalized base
URL. Update tests/nous-oauth.test.ts (lines 8-9) to use an HTTPS TEST_PORTAL and
add coverage proving an HTTP override fails before fetch is invoked.
- Around line 149-167: Update parseTokenPayload to remove refreshFallback and
require a non-empty refresh_token in every response; reject a returned token
equal to the refreshToken supplied to the refresh flow, and adjust that caller
to pass no fallback while preserving initial token parsing. Add a regression
test covering an omitted replacement refresh token and the consumed-token reuse
case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd2bf16e-886e-40d2-8c88-40912a1f2872

📥 Commits

Reviewing files that changed from the base of the PR and between dc4dd45 and 3c5d435.

📒 Files selected for processing (10)
  • docs-site/src/content/docs/guides/providers.md
  • docs-site/src/content/docs/ja/guides/providers.md
  • docs-site/src/content/docs/ko/guides/providers.md
  • docs-site/src/content/docs/ru/guides/providers.md
  • docs-site/src/content/docs/zh-cn/guides/providers.md
  • src/oauth/index.ts
  • src/oauth/nous.ts
  • src/providers/registry.ts
  • tests/nous-oauth.test.ts
  • tests/provider-registry-parity.test.ts

| `key` | Отправляет ваш API-ключ (`Authorization: Bearer …` либо `x-api-key` / `api-key` в зависимости от адаптера). Ключ может быть литералом или ссылкой вида `${ENV_VAR}`. | Большинство провайдеров. |
| `forward` | Передаёт провайдеру **входящие заголовки аутентификации Codex** без изменений — ключ не хранится. Это сквозной режим (passthrough) входа через ChatGPT. | OpenAI (адаптер `openai-responses`). |
| `oauth` | Берёт сохранённый OAuth-токен доступа (автоматически обновляется до истечения срока) и использует его как bearer-ключ. | xAI, Anthropic, Kimi, Kiro, Google Antigravity, Cursor, GitHub Copilot. |
| `oauth` | Берёт сохранённый OAuth-токен доступа (автоматически обновляется до истечения срока) и использует его как bearer-ключ. | xAI, Anthropic, Kimi, Kiro, Google Antigravity, Cursor, GitHub Copilot, Nous Portal. |

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Complete the Russian Nous documentation.

This line adds Nous Portal to the OAuth provider list, but docs-site/src/content/docs/ru/guides/providers.md still says “Семь пресетов” on Lines 96-99, omits ocx login nous from Lines 101-112, and omits the nous provider row from Lines 114-122.

Update those sections to match the English source: use eight OAuth presets, add the device-grant login command, and document the openai-chat adapter, inference endpoint, live paid/free model discovery, and rotated refresh tokens.

As per path instructions, translated provider documentation must not contradict the English source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/ru/guides/providers.md` at line 63, Update the
Russian providers documentation sections around the preset count, login
commands, and provider table to match the English source: change the OAuth total
to eight, add the ocx login nous device-grant command, and add the Nous provider
row covering the openai-chat adapter, inference endpoint, live paid/free model
discovery, and rotated refresh tokens.

Source: Path instructions

Comment thread src/oauth/nous.ts
Comment on lines +67 to +69
function resolvePortalBaseUrl(): string {
return (process.env.NOUS_PORTAL_BASE_URL || NOUS_PORTAL_BASE_URL).replace(/\/+$/, "");
}

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.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Require HTTPS for the Nous Portal OAuth endpoint.

Line 68 accepts http: values from NOUS_PORTAL_BASE_URL. refreshNousToken then sends x-nous-refresh-token to that endpoint, and token responses contain the bearer access token. A cleartext override exposes both credentials to a network attacker.

  • src/oauth/nous.ts#L67-L69: Parse the configured URL. Reject non-HTTPS schemes, embedded credentials, query strings, and fragments before constructing OAuth requests.
  • tests/nous-oauth.test.ts#L8-L9: Change TEST_PORTAL to an HTTPS URL. Add a test that an HTTP override fails before fetch runs.

Based on learnings: OAuth adapters that attach Bearer credentials must enforce HTTPS separately whenever cleartext transmission is unacceptable.

📍 Affects 2 files
  • src/oauth/nous.ts#L67-L69 (this comment)
  • tests/nous-oauth.test.ts#L8-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/nous.ts` around lines 67 - 69, Update resolvePortalBaseUrl in
src/oauth/nous.ts (lines 67-69) to parse the configured URL and reject non-HTTPS
schemes, embedded credentials, query strings, and fragments before returning the
normalized base URL. Update tests/nous-oauth.test.ts (lines 8-9) to use an HTTPS
TEST_PORTAL and add coverage proving an HTTP override fails before fetch is
invoked.

Source: Learnings

Comment thread src/oauth/nous.ts
Comment on lines +149 to +167
function parseTokenPayload(payload: NousTokenResponse, refreshFallback?: string): OAuthCredentials {
const access = nonEmptyString(payload.access_token);
if (!access) throw new Error("Nous Portal token response did not include an access token");
const refresh = nonEmptyString(payload.refresh_token) ?? refreshFallback;
if (!refresh) throw new Error("Nous Portal token response did not include a refresh token");

const jwtPayload = decodeJwtPayload(access);
const expMs = jwtExpiryMs(jwtPayload);
const expiresInMs = typeof payload.expires_in === "number" ? payload.expires_in * 1000 : undefined;
// Prefer the JWT `exp` claim when present (it is the authoritative inference
// JWT lifetime), else fall back to `expires_in`.
const expires = (expMs ?? (expiresInMs !== undefined ? Date.now() + expiresInMs : Date.now() + DEFAULT_DEVICE_FLOW_TTL_MS))
- OAUTH_EXPIRY_SKEW_MS;
return {
access,
refresh,
expires,
...identityFromNousTokens(access),
};

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require a replacement refresh token after refresh.

Line 152 accepts the old refresh token when the refresh response omits refresh_token. Line 278 then persists that consumed token. The next refresh reuses it and can revoke the Portal session.

Remove refreshFallback from parseTokenPayload. Reject a refresh response that has no new refresh_token. Also reject a returned token that equals refreshToken. Add a regression test for this response shape.

Proposed fix
-function parseTokenPayload(payload: NousTokenResponse, refreshFallback?: string): OAuthCredentials {
+function parseTokenPayload(payload: NousTokenResponse): OAuthCredentials {
   const access = nonEmptyString(payload.access_token);
   if (!access) throw new Error("Nous Portal token response did not include an access token");
-  const refresh = nonEmptyString(payload.refresh_token) ?? refreshFallback;
+  const refresh = nonEmptyString(payload.refresh_token);
   if (!refresh) throw new Error("Nous Portal token response did not include a refresh token");
-  return parseTokenPayload((await response.json()) as NousTokenResponse, refreshToken);
+  const credentials = parseTokenPayload((await response.json()) as NousTokenResponse);
+  if (credentials.refresh === refreshToken) {
+    throw new Error("Nous Portal refresh response did not rotate the refresh token");
+  }
+  return credentials;

Also applies to: 278-278

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/nous.ts` around lines 149 - 167, Update parseTokenPayload to remove
refreshFallback and require a non-empty refresh_token in every response; reject
a returned token equal to the refreshToken supplied to the refresh flow, and
adjust that caller to pass no fallback while preserving initial token parsing.
Add a regression test covering an omitted replacement refresh token and the
consumed-token reuse case.

@Cheurteenyt

Copy link
Copy Markdown
Author

Code review (manual pass, 2026-08-10)

Reviewed src/oauth/nous.ts, src/oauth/index.ts, src/providers/registry.ts, tests/nous-oauth.test.ts (7 tests), parity golden lists. Typecheck + 42 tests green locally. Global impression: clean, well-documented, conservative on refresh (lazy-only is the right call for single-use rotated tokens).

Non-blocking findings (no code change required before merge)

  1. client_id = "hermes-cli" is borrowed from Hermes. The device grant uses Hermes' client id against portal.nousresearch.com. It works today (same backend), but it is a coupling: if Hermes ever rotates/renames its client id, this login breaks. Worth asking Nous Research for a dedicated client id for OpenCodex (or at least documenting the dependency in providers.md). Not a blocker — this is exactly how Hermes' own docs describe the flow.

  2. Refresh fallback keeps the old token when the server omits refresh_token (parseTokenPayload(payload, refreshFallback)). Since Nous tokens are single-use and rotated, if the Portal ever rotates silently without returning the new token in the body, the next refresh would reuse a stale token and could trip refresh_token_reused (session revocation). Defensive and reasonable, but the fallback path is untested — a test asserting "no refresh_token in response → old token retained" would lock the intended behavior.

  3. terminal() for NousTokenError does not gate on HTTP status (Anthropic/Kiro require 400/401 before treating OAuth errors as terminal). Nous Portal may legitimately return other statuses, so this is a deliberate choice — just noting it differs from siblings for future maintainers.

  4. Test gaps (minor): slow_down, expired_token, access_denied, and the device-flow timeout deadline are not covered (only authorization_pending is). The logic is straightforward, but these are exactly the paths that break under real-world Portal conditions.

Windows / platform question

No platform-specific code here: the flow is a pure RFC 8628 device grant — OpenCodex displays the verification URL + code (onAuth), and the user opens any browser (Windows, macOS, Linux) and enters the code on portal.nousresearch.com/activate. There is no start/xdg-open/open shell invocation, so nothing Windows-specific needs to be stated in the PR. Manual testing on Windows works the same as anywhere else. If a future change ever auto-opens the browser, that is where platform branching would appear (and then Windows would matter).

CI note

hygiene is red on unsponsored_surface (paths src/oauth/index.ts, src/oauth/nous.ts touch the auth surface). Per MAINTAINERS.md, a maintainer must apply maintainer-sponsored after security review — expected for any auth-surface PR, not a code problem.

…lback

- access_denied / expired_token surface as terminal NousTokenError
- slow_down backs off (interval bump) then resumes polling to success
- authorization_pending until deadline raises a timed-out error
- refresh omitting a new refresh_token keeps the previous one (header sent)
@Cheurteenyt

Copy link
Copy Markdown
Author

Gaps de tests identifies en review : couverts dans le commit 6989a2e.

  • slow_down : backoff puis reprise du polling jusqu'au succes (pollCount === 2)
  • expired_token / access_denied : erreurs terminales NousTokenError avec messages clairs
  • Timeout device flow : authorization_pending jusqu'a la deadline -> "device flow timed out"
  • Fallback refresh : reponse 200 sans refresh_token -> l'ancien refresh token est conserve et le header x-nous-refresh-token est bien envoye

Verification : bun test tests/nous-oauth.test.ts -> 12 pass / 0 fail ; bun run typecheck OK.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/nous-oauth.test.ts`:
- Around line 157-171: The tests around loginNous must assert the NousTokenError
contract, not only matching messages. Update the access_denied and expired_token
cases to verify rejection with NousTokenError and confirm the error’s oauthError
value preserves the corresponding OAuth code, while retaining the existing
message assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 17ba1740-a7de-466c-81bd-baf695a97538

📥 Commits

Reviewing files that changed from the base of the PR and between 3c5d435 and 6989a2e.

📒 Files selected for processing (2)
  • src/providers/registry.ts
  • tests/nous-oauth.test.ts

Comment thread tests/nous-oauth.test.ts
Comment on lines +157 to +171
test("access_denied surfaces as a terminal NousTokenError", async () => {
globalThis.fetch = deviceFlowFetch(() =>
new Response(JSON.stringify({ error: "access_denied", error_description: "User denied the request" }), { status: 400 }),
);
const ctrl: OAuthController = { onAuth() {} };
await expect(loginNous(ctrl)).rejects.toThrow("Nous Portal device authorization denied");
});

test("expired_token surfaces as a terminal NousTokenError", async () => {
globalThis.fetch = deviceFlowFetch(() =>
new Response(JSON.stringify({ error: "expired_token", error_description: "Code expired" }), { status: 400 }),
);
const ctrl: OAuthController = { onAuth() {} };
await expect(loginNous(ctrl)).rejects.toThrow("Nous Portal device authorization expired");
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the token-error type, its terminal API, and the polling error path.
ast-grep outline src/oauth/nous.ts --items all --type class,function --match 'NousTokenError|readTokenError|pollForToken'
rg -n -C 8 '\bNousTokenError\b|\bterminal\b|\breadTokenError\b|\bpollForToken\b' \
  src/oauth/nous.ts tests/nous-oauth.test.ts

Repository: lidge-jun/opencodex

Length of output: 10441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the terminal classification contract and existing token-error assertions.
rg -n -C 4 '\bterminal\b|\bterminalError\b|\bOAuthError' src tests
rg -n -C 3 'NousTokenError|toThrowError|toThrow\(' tests src
fd -i 'oauth|nous' tests src -t f

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the codebase defines a shared terminal-error API for OAuth failures.
rg -n '\bterminal\b|\bterminalError\b|\btimedOut\b|\boauthError\b' src tests --iglob '*oauth*' --iglob '*terminal*' --iglob '*error*' | head -200

Repository: lidge-jun/opencodex

Length of output: 3788


Assert the NousTokenError contract.

tests/nous-oauth.test.ts:157-170 only asserts the error message strings, so a plain Error with the same message passes. Check the source path in src/oauth/nous.ts:238-242, then assert that access_denied and expired_token reject with NousTokenError and preserve the oauthError code so callers cannot drift from the terminal-token-error contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/nous-oauth.test.ts` around lines 157 - 171, The tests around loginNous
must assert the NousTokenError contract, not only matching messages. Update the
access_denied and expired_token cases to verify rejection with NousTokenError
and confirm the error’s oauthError value preserves the corresponding OAuth code,
while retaining the existing message assertions.

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

Labels

enhancement New feature or request intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant