11 Commits
Author SHA1 Message Date
c461a0363d feat: merge knowledge graph + conversation arc into memdir (#1811)
* feat: merge knowledge graph + conversation arc into memdir

Replace the standalone KG/ARC system (SQLite + JSON + Orama storage)
with direct integration into the existing auto-memory directory.

What changed:
- New memdir/vectorIndex.ts — Orama full-text index over all memory/ .md
  files, replacing the separate knowledge.orama binary
- New memdir/autoExtractFacts.ts — auto-detects env vars, paths,
  versions, URLs, IPs, backtick concepts from conversation and writes
  them as structured .md files into memory/.facts/ with frontmatter
- conversationArc.ts now persists arc state (goals, decisions,
  milestones, phase) to memory/.arc.json sidecar instead of the KG
- knowledgeGraph.ts gutted from 728→165 lines — now a thin
  compatibility layer that reads .facts/ files from memdir and
  delegates vector search to vectorIndex.ts
- build.ts: enabled CONVERSATION_ARC and MULTI_TURN_CONTEXT feature
  flags (previously undefined → dead-code eliminated in production)

Removed:
- src/utils/storage/ (SQLiteProvider, JSONProvider, 3 test files) —
  unused after KG migration
- src/utils/knowledgeGraph.test.ts, .stress.test.ts
- src/utils/conversationArc.test.ts, .perf.test.ts
- ~1700 lines of redundant storage code

Benefits:
- Single memory system (memdir) instead of two parallel systems
- Auto-extracted facts are plain .md files — visible to the model,
  discoverable by the existing Sonnet prefetch
- Vector search indexes real memory content, not a separate DB
- Arc state survives across sessions via .arc.json
- ~700 lines removed from the production bundle

* fix: type errors, add test suites, fix resetArc() disk-write bug

- Fix vectorIndex.ts: parseFrontmatter returns nested {frontmatter, content},
  Orama DB typed as 'any' matching existing code pattern
- Fix resetArc(): was overwriting .arc.json on disk — now clears only in-memory state
- Add vectorIndex.test.ts (6 tests): build/search/persist/rebuild
- Add autoExtractFacts.test.ts (10 tests): env vars, paths, versions,
  URLs, backtick concepts, PascalCase, React/Redux, file signatures, frontmatter
- Add conversationArc.test.ts (14 tests): arc init, persistence, goals,
  decisions, milestones, phase detection, arc summary, finalize, stats
- Update verify-kg-merge.sh: add test suite check (33 tests)

* fix: remove generic type param from restore() call

* fix: address CodeRabbit findings — YAML injection, secrets leak, cache invalidation, frontmatter parsing, test weakness

* fix: redact URL credentials/query/hash in endpoint extraction; add persistence + reindex regression test

* test: add regression for URL credential/query/hash redaction in fact extraction

* feat: wire getOrchestratedMemory into query.ts prompt; remove dead promises array in autoExtractFacts

* feat: enhance memory management by adding clearArcArtifacts function and integrating it into the clear command; implement file count tracking in vector index

* feat: enhance fact extraction by adding tests for absolute paths, backtick concepts, technical terms, project file signatures, and IP addresses; implement clearArcArtifacts function in tests

* Review-fix: scoped IP tagging, scrubbedContent, arcMemoryDir null, vector-index cleanup

* Review-fix: isAutoMemoryEnabled gate, scrubbed paths, clearIndex in cleanup

* Review-fix: URL-stripped path scan, digit-key/quoted-value env redaction, rm isAutoMemory gate

* Review-fix: quoted multi-token env values fully redacted, regression test

* Review-fix: freshness check on each search, integration test, typecheck in verifier

* Review-fix: missing-index-file reinit, full-pipeline integration test

* Review-fix: gate arc/RAG on isAutoMemoryEnabled, add integration+stale-index tests

Addresses three P1/P2 findings from code review:

1. P1: Honor auto-memory opt-out before writing arc facts
   - Add isAutoMemoryEnabled() checks in query.ts before calling
     updateArcPhase() and getOrchestratedMemory()
   - Add same check inside conversationArc.ts extractFactsAutomatically()
   - Prevents .facts file writes when auto-memory disabled via --bare,
     CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, or memory.autoWrite: false

2. P2: Add query-level integration test coverage
   - New test in conversationArc.test.ts verifies query.ts path
   - Confirms arc functions called behind feature gates and results
     appended to system prompt (lines 555-575)

3. P2: Add stale-index regression tests
   - 6 new tests in vectorIndex.test.ts cover:
     * Searching after adding files
     * Searching after editing files
     * Searching after removing files
     * Searching when .vector-index missing
     * Searching when .vector-index-meta.json missing
     * Mixed stale conditions

All tests pass (31 total, 99 assertions), typecheck clean.

* Review-fix: use >= for mtime staleness check to catch same-ms edits

The verification agent discovered a timing-dependent bug in the stale
index detection. When a file edit and index save occur within the same
millisecond, latestMtime equals indexMtime, causing the check
`latestMtime > indexMtime` to return false. The stale index is not
refreshed and searches miss the updated content.

Changed line 220 from `>` to `>=` in the mtime comparison. The file
count check catches add/remove operations, but edits that don't change
the file count rely on mtime comparison.

All 12 vector index tests now pass consistently, including the
"searching after editing a file picks up changes" test that previously
failed intermittently.

* Review-fix: move auto-memory gate to persistence layer

Addresses inline review comment: query.ts was incorrectly skipping
updateArcPhase() entirely when isAutoMemoryEnabled() returned false,
leaving the in-memory arc state stale.

Fixed by:
- Removed isAutoMemoryEnabled() gate from query.ts line 449
- Added gate inside conversationArc.ts updateArcPhase() at persistence
  layer (line 223), so phase advances but only persistence is disabled
- Arc state tracking now works regardless of auto-memory setting
- Only disk writes (.arc.json, .facts files, index rebuilds) are gated

The 2ms delay in vectorIndex.test.ts is kept as a pragmatic fix for the
timing race. Content-hash detection would be ideal but adds complexity;
the mtime check works reliably in production.

All 31 tests pass, typecheck clean.

* Refactor memory handling: consolidate metadata retrieval and improve auto-memory checks

* Fix: update expectation to use toEqual for prompt comparison in conversationArc tests

* fix: detect same-size content changes via content hash; handle text-block user messages in arc query

* fix: skip symlinked dirs in vector index walk; enable arc/multi-turn flags; add production-path tests

* fix: follow symlinked dirs in vector index walk per review

* fix: skip symlinked dirs in vector index walk; add symlink-boundary regression tests

* fix: skip indexing symlinked directories and ensure only files are processed

* fix: show cmd output on failure in verifier; add multi-turn coverage; clear mempath cache; cover arc reset in knowledge clear test

* fix: clear memoized auto-mem path after teardown in knowledge + conversationArc tests

* fix: isolate vector index per memdir, filter secrets from backtick facts, clean trailing ws

* fix: extend credential filter for AWS/GitLab tokens; reject all symlinks in vector index

* fix: use repo's redactSecretSubstringsForDisplay; add npm/glpat/AKIA/ASIA/xox to shared patterns; cover NPM+JWT in tests

* fix: P1 backtick credential safety + untrusted-data boundary; P2 legacy migration + non-fatal writes; P3 type safety + build regression

* fix: B1-B5, M6, M8 — no message mutation, migration data loss, empty-file, non-fatal writes, probe, rebuildIndex resilience, dead import

* fix: address 8 reviewer findings (R1-R8)

P1:
- Keep retrieved facts in DATA ONLY block with strict system instruction
- Catch lowercase config secrets (api_key=...) in env scrubber
- Migrate SQLite working store (knowledge.db) before deleting provider
- /knowledge clear atomically archives legacy sources

P2:
- Run legacy migration on getOrchestratedMemory retrieval path
- Preserve entity attributes in migration frontmatter
- Only rebuild vector index when facts actually changed
- Fix feature-flag verifier --define syntax (declare const)

* fix: close 9 memory findings — approval gate, non-fatal writes, secret scrub, per-project migration, attribute/relation preservation, WAL cleanup, cheaper index

- Gate auto fact extraction on isMemoryWriteApprovalRequired() + isAutoMemoryEnabled() so default projects cannot silently persist conversation content
- Make ensureFactsDir/writeFactMemory degrade non-fatally (no turn-breaking throw on read-only dirs)
- Scrub token-like URL/path/hyphenated segments from durable facts via looksLikeSecret (reuses providerSecrets.looksLikeSecretValue)
- Restore passive project-rule extraction as rule facts
- Honor isAutoMemoryEnabled() before legacy migration; scope the migration guard per project (Set) instead of a single global
- Preserve legacy entity attributes and relations through migration (indented attributes + relation fact file, reconstructed in getGlobalGraph)
- Clear SQLite WAL/SHM sidecars on /knowledge clear
- Replace per-turn content hashing in vectorIndex getMdStats with size+mtime metadata; drop redundant initMemdirIndex call in getOrchestratedMemory
- Add knowledgeGraph tests covering P1#2/P1#4/P2#5/P2#8 and extend autoExtractFacts tests

* test: avoid global cwd pollution in knowledgeGraph tests

Replace process.chdir with a per-test setFsImplementation mock cwd that is
reset via setOriginalFsImplementation in afterEach, so the test no longer
leaks a changed process.cwd() into other test files. Assert the auto-memory
gate via the project-specific legacy file rather than the shared resolved
memdir dir (which bun runs concurrently across it blocks).

* fix: address 15 reviewer findings (R1-R15)

P1:
- Gate saveArcToDisk/finalizeArcTurn/saveIndex/migration on memory-write approval
- Retire legacy sources after successful migration (rmSync, backup preserved)
- Slugify entity.type and summary.id in migration filenames (path traversal)
- Fall back to JSON when SQLite has zero entities
- Scrub rule-fact extraction on scrubbedContent + looksLikeSecret/redact check

P2:
- Track skipped vs completed migration; re-enable clears skip marker
- Walk back to latest human text for tool-round vector queries
- Auto-extract goals/decisions from user messages in updateArcPhase
- /knowledge clear message says durable wipe, not session-only
- Bind arc state to projectKey (re-resolve on cwd change)
- clearIndex(memoryDir?) scopes to one memdir
- yamlQuote all migration frontmatter fields
- Real SHA-256 contentHash alongside fileFingerprint for same-size edits
- Stop claiming production-pipeline coverage in tests/verifier

* test: isolate governance mock by removing afterEach clear

Remove setGovernancePolicySettingsForSourceForTesting(null) from afterEach
in autoExtractFacts, conversationArc, and knowledgeGraph test files. The
module-level mock is set in each file's beforeEach and since there is no
afterEach cleardown, parallel test execution can no longer corrupt the
mock state across files.

This fixes 26 CI test failures caused by one file's afterEach clearing
the mock that another concurrently-running file had set in its beforeEach.

* fix: isolate governance mock per async context via executionAsyncId tracking

Replace the module-level variable in governancePolicy.ts with an
executionAsyncId-keyed Map and an async_hooks.createHook that propagates
the override from parent to child async resources. This ensures each
concurrent test's beforeEach/afterEach cannot corrupt the override set
by another test file, even when test bodies directly mutate the mock.

Also restore setGovernancePolicySettingsForSourceForTesting(null) calls
in afterEach hooks (removed in 39c68376), which are now safe because
each afterEach only clears its own async context.

Fixes 26 CI test failures across knowledgeGraph (2), conversationArc (7),
and autoExtractFacts (17/22, governance-gate tests).

* Refactor knowledge graph legacy migration, secure secrets and IP octets, optimize build-time feature flags, cap conversation arc collections, and resolve all verification check gaps

* Fix governancePolicy enablement in full test runs by checking for test runner globals

* fix: ensure auto memory is enabled in conversationArc, knowledgeGraph, autoExtractFacts tests

Delete CLAUDE_CODE_DISABLE_AUTO_MEMORY and CLAUDE_CODE_SIMPLE env vars
in beforeEach hooks so tests are not blocked when CI sets these vars.
Also revert governancePolicy.ts to the simple module-level variable
(removing the async ID tracking approach that broke with Bun v1.3.13).

Fixes 33 CI test failures where isAutoMemoryEnabled() returned false
causing all disk-persistence guards to fire.

* fix: address P1/P2 review findings — redaction, bounds, legacy backup

[P1] Redact goal/decision descriptions with redactLikelySecrets before
persisting to .arc.json, session summaries, and prompt summaries so
credentials captured by auto-extraction regexes are not durably stored.

[P1] Bound multi-turn tool input serialization to 2000 chars and apply
redactLikelySecrets, preventing oversized/credential-bearing tool inputs
from overflowing the next provider request or exposing secrets.

[P2] Archive the non-selected legacy store (JSON or SQLite) and its WAL
sidecars before retiring both sources, ensuring a recoverable snapshot
exists if generated fact files are incomplete or a migration bug surfaces.

* fix: address P1 findings — safe legacy retirement, multi-turn aggregate budget

[P1] Do not retire a legacy store unless every existing source was
successfully archived. Track archived sources in a Set and skip deletion
of any source whose backup failed (knowledgeGraph.ts).

[P1] Archive the selected SQLite WAL/SHM sidecars alongside its migration
backup, since committed state may reside only in the WAL file and the
advertised recovery backup would otherwise be incomplete (knowledgeGraph.ts).

[P1] Bound the aggregate multi-turn tool replay to 10KB total and stop
appending further turns once the budget is exceeded, preventing many
Agent/MCP calls per turn from adding unbounded text to system prompts
(conversationArc.ts).

* fix: address P1/P2 review findings — rule extraction gate, SQLite read status, atomic WAL/SHAM, KG status gate, byte budgets

* fix: tighten looksLikeOpaqueToken to avoid flagging compound model names

* fix: address P1/P2 review findings — rebase, test isolation, SQLite retry, attribute redaction, entity aliases, body content, project-scoped multiturn, skip unchanged writes, knowledge list gate

* fix: address P1/P2/P3 review findings — secret scrub on migrate, lean decision gate, git-root legacy lookup, backup retention, index rebuild chaining, single vector search, drop unreferenced fixture

* fix: scrub secret entity names on migrate, exclude summary facts from entities, reset multi-turn on /knowledge clear

* fix: address P1 review findings for legacy graph redaction, recovery-safe clear, stable change guards

- Redact embedded secrets in migrated legacy knowledge-graph entities,
  summaries, and rules via shared sanitizeLegacyText() policy
- Preserve legacy artifacts (json/db/wal/shm) as migration-backup before
  /knowledge clear; resetGlobalGraph returns { archived, failures }
- Skip rewrite + index rebuild on unchanged turns by stripping the volatile
  detectedAt timestamp (facts and arc session summaries)
- Always recompute the authoritative content hash in getMdStats so edits of
  equal size with preserved mtime are detected and served correctly

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: redact protocol-relative URL userinfo in legacy migration

new URL() throws for scheme-less URLs, so the catch branch now redacts
obvious //user:pass@host userinfo instead of persisting credentials.

* fix(memory): harden memdir migration and retrieval

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kevin Codex <kevin@gitlawb.com>
2026-08-19 19:43:17 +08:00
GravireiGitHubGravireiClaude Opus 4.6coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>openhands
aa936cda11 Centralize credential redaction in src/utils/redaction.ts + channel gate tests (#1711)
* feat(utils): add centralized redaction utility

Single source of truth for stripping API keys, tokens, and other
secrets from strings and JSON. Provider env-var coverage is generated
from getKnownProviderSecretEnvKeys() so adding a new provider cannot
silently create an unredacted path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(Feedback): import redactSensitiveInfo from utils

Remove the inline 40-line regex implementation in favor of the
centralized redaction utility, eliminating drift between Feedback
and the transcript share path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(submitTranscriptShare): import redactSensitiveInfo from utils

Update import path to point at the centralized utility instead of the
Feedback component. Removes the implicit re-export contract that
required Feedback.tsx to keep redactSensitiveInfo exported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(log,debug): redact secrets in default error and debug output

Wire the centralized redaction utility into logError and logForDebugging
so secrets cannot leak into in-memory error logs or the debug file even
if a caller forgets to pass through redactSensitiveInfo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(api/logging): redact error message in logAPIError

Apply the centralized redaction utility to the error string passed to
logEvent so analytics events cannot capture unredacted credentials from
upstream API failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve merge conflict from upstream sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(channelNotification): allow null in getEffectiveChannelAllowlist signature

ChannelsNotice.tsx passes getSubscriptionType() which returns
SubscriptionType | null, but the signature only accepted string |
undefined. Widen to string | null so the call site typechecks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(redaction): exclude specific token fields from redaction process

* fix(redaction): lower AIza minimum length to {10,}

Real GCP/Gemini keys are 39 chars total (4 prefix + 35 suffix), but
the {35} suffix bound missed short tokens like 'AIzaSyDUMMY-secret-token'
(21 chars after AIza). Lower to {10,} to match the diagnostics module
and catch any AIza-shaped value. Same precision trade-off the
diagnostics redaction makes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction,log): address review feedback

- Drop quotes from ANTHROPIC/OPENAI key negative lookarounds so
  JSON-shaped values like "sk-ant-..." redact.
- Add private_key pattern to GENERIC_HEADER_FIELD_PATTERN and
  privatekey to SENSITIVE_FIELD_SUBSTRINGS.
- logError now builds a sanitized Error (redacted message + stack)
  before passing to the sink and queue, not just the in-memory log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(redaction): consolidate into single module + add channel gate tests

Address the three P2 review findings on the central-redaction PR:

[1] Consolidate four redaction modules into src/utils/redaction.ts.
    Previously lived in:
      - src/utils/redaction.ts            (logs/bug reports/transcript shares)
      - src/utils/urlRedaction.ts         (URL display)
      - src/utils/statusRedaction.ts      (/status output)
      - src/utils/diagnostics/redaction.ts (doctor reports)
    The four surfaces share the same regex set / credential lists
    but had drifted into separate per-domain files. Merged into
    one module; deleted the three shim files. Updated six direct
    consumers (openaiShim.ts, ProviderManager.tsx, status.tsx,
    requestSizeBreakdown.ts, diagnostics/issueReport.ts,
    scripts/system-check.ts) and three test files to import from
    redaction.js.

[2] Add gateChannelServer() test coverage.
    src/services/mcp/channelNotification.test.ts: 13 cases for the
    six gate paths (capability, runtime, session, marketplace,
    plugin allowlist, server-entry dev) plus end-to-end register.
    Mocks channelAllowlist.js (GrowthBook-backed) so tests stay
    independent of feature-flag state.

[3] Apply jsonRedactor in transcript share.
    src/components/FeedbackSurvey/submitTranscriptShare.ts now does
    redactSensitiveInfo(jsonStringify(data, jsonRedactor)) — the
    key-aware redaction applies during serialization, and the text
    pass stays as defense in depth for free-form fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(channelNotification): cover findChannelEntry multi-candidate branch

Regression test for the disambiguation path in `findChannelEntry`
(channelNotification.ts:201-230): when two same-name plugin entries
exist in the allowed-channels list with different marketplaces,
`pluginSource` must select the matching entry before the marketplace
and allowlist gates evaluate.

Without this branch being exercised, the gate could lock onto
whichever entry sorts first and either skip the user's real
installation or wrongly authorize a typo-squatted one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction): align URL fallback regex + add path-prefix boundary check

Two related redaction correctness fixes:

[1] URL fallback regex covers the same parameter set as the primary
    path. The malformed-URL branch in `redactUrlForDisplay` previously
    had a hand-rolled alternation of credential parameter names that
    could drift behind `SENSITIVE_URL_QUERY_PARAM_TOKENS`. New
    `MALFORMED_URL_PARAM_PATTERN` derives from that same list, so
    the two paths can never diverge. Tests cover the full credential
    set (`api_key`, `access_token`, `refresh_token`, `signature`,
    `sig`, `secret`, `password`, `apikey`) plus a non-sensitive
    `model` that must survive.

[2] `redactPathForStatus` now requires a path-separator boundary
    after the home prefix. The previous `startsWith` check matched
    `/home/alice2/project` against `/home/alice` and emitted
    `~2/project`. The fix requires the character at
    `normalizedCandidate.length` to be `/` or `\` so `alice` no longer
    matches `alice2` or `alice.bak`. Test pins the false-positive
    paths and the true-positive (`/home/alice/project` → `~/project`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(channel,redaction): restore dev-channel warning + align URL fallback

Two related security fixes:

[1] Restore DevChannelsDialog when --dangerously-load-development-channels
    is passed and the channels feature is enabled. The previous logic
    skipped the dialog when OAuth was absent, which was safe only while
    gateChannelServer() blocked no-OAuth sessions. With the OAuth/org-
    policy gates removed in this PR, an API-key session could pass the
    flag, skip the warning, and still register the dev channel. The
    only remaining skip is the genuinely-disabled feature case
    (`!isChannelsEnabled()`), where the dialog is moot.

[2] Malformed-URL fallback now uses the same substring predicate as
    the primary `URL` parser path. The previous regex matched only
    exact parameter names (`api_key=`, `access_token=`, …), so
    `my_api_key=SECRET` and `x_access_token=TOKEN` slipped through
    unchanged even though `shouldRedactUrlQueryParam` flags them as
    sensitive. New `redactMalformedQuery` walks the query pairs and
    runs the predicate on each key. Three new tests cover prefixed
    keys, non-sensitive keys, and fragment preservation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction): widen key boundary class + tighten dev-channel comment

Two small follow-ups from the latest CodeRabbit review:

[1] Boundary class on key-prefix patterns widened from `[A-Za-z0-9]`
    to `[A-Za-z0-9_-]` so a raw key embedded in a JSON string value
    (`"sk-ant-..."`, `"AIza..."`, `"ghp_..."`, etc.) is still caught.
    Quotes act as delimiters, not blockers — the previous boundary
    class was correct for unquoted text but let quoted keys slip
    through.

[2] Tighten the dev-channel dialog comment in interactiveHelpers.tsx
    so future readers don't misread the security boundary. Skip
    condition is `isChannelsEnabled()` (the channels feature flag
    gate), not KAIROS / KAIROS_CHANNELS as the previous wording
    implied. Comment now matches the code.

Skipped with reason:
- getEffectiveChannelAllowlist divergence from gateChannelServer
  allowlist — by design; the effective-list override is a UI hint
  consumed only by ChannelsNotice for the org-override indicator.
  Trust boundary is enforced by gateChannelServer() reading the
  hardcoded ledger.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction,channel): address P1/P2 review findings

P1 - malformed URL fallback secrets:
- Decode percent-encoded query param keys via decodeURIComponent() before
  applying shouldRedactUrlQueryParam (e.g. %74oken -> token)
- Stop userinfo regex at ? and # delimiters to avoid consuming query params
  when matching @ signs in email addresses or fragment delimiters

P2 - channel notice/gate allowlist sync:
- Remove org override path from getEffectiveChannelAllowlist() so
  ChannelsNotice startup guidance uses the same ledger source as
  gateChannelServer's runtime enforcement
- Simplify ChannelsNotice to drop unused sub/policy params and the
  source === 'org' conditional

* fix(channel): apply marketplace matching to permission relays, remove stale OAuth/org-policy blockers, add dev-channel dialog coverage

P1: Thread runtime pluginSource through filterPermissionRelayClients
so findChannelEntry disambiguates same-name plugin entries from
different marketplaces before sending permission request previews.

P2: Remove stale noAuth and policyBlocked branches from ChannelsNotice
that would render '--channels ignored' before reaching the listening
message, confusing non-OAuth users.

P2: Add test coverage that mocks isChannelsEnabled() both true and
false, verifies DevChannelsDialog appears with onAccept marking entries
dev:true in the enabled case, and verifies the disabled branch registers
entries directly without dialog.

* test(dev-channel): clarify count assertion comment + add afterEach with mock.restore()

* fix(channel): mirror marketplace gate in permission relay + restore mock

Two follow-ups from the latest review:

[1] Permission relay predicate no longer relies on findChannelEntry
    alone. After resolving the entry, the predicate now requires a
    runtime pluginSource whose marketplace matches the session
    entry's marketplace for plugin-kind entries — mirroring the
    gateChannelServer check at channelNotification.ts:303-312. A
    `plugin:slack@evilcorp` client whose session allows
    `plugin:slack@anthropic` is now rejected instead of piggy-backing
    on the approved entry to receive permission-request previews.
    Server-kind entries still match on bare name.

[2] bugfixes.test.ts now re-registers the real channelAllowlist
    module in afterEach via a cache-busted reference, so the
    neighbor channelNotification.test.ts continues to import
    getChannelAllowlist after this suite runs. mock.restore() does
    not clear module-level mock.module() overrides in bun (the
    registry is process-global). Pattern matches compact.test.ts:27-36.

Also expanded the dev-map count comment in bugfixes.test.ts to
document the security invariant (a dev entry must never be confused
with a production entry in the allowlist check) per CodeRabbit's
request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(redaction): consolidate into single module + add channel gate tests

Address the three P2 review findings on the central-redaction PR:

[1] Consolidate four redaction modules into src/utils/redaction.ts.
    Previously lived in:
      - src/utils/redaction.ts            (logs/bug reports/transcript shares)
      - src/utils/urlRedaction.ts         (URL display)
      - src/utils/statusRedaction.ts      (/status output)
      - src/utils/diagnostics/redaction.ts (doctor reports)
    The four surfaces share the same regex set / credential lists
    but had drifted into separate per-domain files. Merged into
    one module; deleted the three shim files. Updated six direct
    consumers (openaiShim.ts, ProviderManager.tsx, status.tsx,
    requestSizeBreakdown.ts, diagnostics/issueReport.ts,
    scripts/system-check.ts) and three test files to import from
    redaction.js.

[2] Add gateChannelServer() test coverage.
    src/services/mcp/channelNotification.test.ts: 13 cases for the
    six gate paths (capability, runtime, session, marketplace,
    plugin allowlist, server-entry dev) plus end-to-end register.
    Mocks channelAllowlist.js (GrowthBook-backed) so tests stay
    independent of feature-flag state.

[3] Apply jsonRedactor in transcript share.
    src/components/FeedbackSurvey/submitTranscriptShare.ts now does
    redactSensitiveInfo(jsonStringify(data, jsonRedactor)) — the
    key-aware redaction applies during serialization, and the text
    pass stays as defense in depth for free-form fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): align malformed URL fragment expectation with preservation behavior

* fix: address review findings P1 and P2

[P1] Enforce dev flag for server-kind entries in permission relay
predicate, matching gateChannelServer() behavior. Add coverage for
both dev and non-dev server relay paths.

[P2] Drop fragments in malformed URL fallback (redactMalformedQuery)
to match the valid-URL path, preventing credential leaks via
fragment-carried tokens. Update existing tests and add regression
for fragment-only malformed URLs.

* test(relay): add plugin-kind marketplace regression tests

* fix: address review findings P1 and P2

[P1] Add PEM private key redaction pattern to redactSensitiveInfo
so multi-line PEM values are fully consumed instead of leaking
after the first whitespace. Add [ to generic header pattern's
value exclusion set to prevent re-consuming [REDACTED] tokens.

[P2] Use truthy check (Boolean()) for claude/channel capability in
filterPermissionRelayClients to match gateChannelServer's behavior,
rejecting explicit false capabilities.

* fix(debug): redact before JSON-stringify multiline messages

Reorder logForDebugging so redactSensitiveInfo runs before jsonStringify,
ensuring PEM/private-key patterns match the raw (unescaped) message text
rather than the JSON-encoded form where colons and quotes are escaped.

* test(debug): add end-to-end regression for multiline PEM redaction in logForDebugging

Uses mock.module on process.js to capture stderr output and exercises the
full logForDebugging path with multiline PEM private_key input, verifying
the redact-before-JSON-stringify ordering produces redacted output.

* fix(test): preserve original process.env.DEBUG and process.argv in logForDebugging test hooks

* fix: address PR review findings P1-P3/P5-P7

- P1: clear isDebugMode/isDebugToStdErr memoize caches in test beforeEach
      + cache-busting query param for fresh debug.ts imports
- P2: restore mock.module afterAll instead of leaking mock
      + mutate err in-place in logError to preserve name/cause
- P3: post-processing regex absorbs trailing bracket content after [REDACTED]
- P5: (was P3) expand jsonRedactor EXCLUDED_KEYS for maxTokens etc.
- P7: capture HOME/USERPROFILE per-test instead of at module scope

* fix: address CodeRabbit review findings

- interactiveHelpers.tsx: update dev-channel comment — OAuth/org-policy
  gates removed from gateChannelServer(), org policy is not enforced
- channelNotification.test.ts: add afterAll mock.restore() to clean up
  process-global channelAllowlist.js mock
- channelNotification.ts: fix comments — isChannelsEnabled() still reads
  tengu_harbor, not always true
- log.ts: sanitize err.message and err.stack separately so message
  doesn't get replaced with full stack trace
- redaction.ts: add 'i' flag to redactHomePath regex for Windows
  case-insensitive path matching

* fix: address second review round

- interactiveHandler.ts: [P2] redact input_preview via redactSensitiveInfo
  before sending to channel servers
- log.ts: [P3] copy error via Object.assign(Object.create(err), err)
  before sanitizing instead of mutating in-place

* fix: address CodeRabbit second round

- channelPermissions.ts: redact before truncate in truncateForPreview
  so partial credentials don't leak at the 200-char boundary
- interactiveHandler.ts: remove outer redactSensitiveInfo — now
  handled inside truncateForPreview
- log.ts: derive errorInfo.error from already-sanitized sanitizedErr;
  fix Object.assign comment to accurately describe what is copies

* fix: improve permission relay client filtering and enhance redaction functions

* fix: address third review round (P1, P2, P3)

- P1: update test expectations for [REDACTED_*] output format
- P2: add total_tokens, prompt_tokens, completion_tokens to jsonRedactor EXCLUDED_KEYS
- P3: remove ) and } from GENERIC_HEADER_FIELD_PATTERN value capture to prevent content leak after embedded parens
- Fix buildKnownEnvVarPattern capture group to preserve env-var separator ([REDACTED])
- Add & to GENERIC_CREDENTIAL_ENV_PATTERN value exclusion to prevent URL query over-consumption

* fix: address latest reviewer P2/P3 findings (errorLogSink redaction, X_API_KEY/AUTHORIZATION patterns, regression tests)

* fix: address reviewer P1/P2 — bracketed values and multi-word header values

- P1: Remove  and  from value captures in X_API_KEY_PATTERN,
  AUTHORIZATION_PATTERN, GENERIC_HEADER_FIELD_PATTERN,
  GENERIC_CREDENTIAL_ENV_PATTERN so bracketed secrets like
  are fully redacted instead of passing through unchanged.

- P2: Widen header-style value captures to include spaces by removing
   from exclusions, using  as delimiter (stops at newlines
  and URL query separators). Fixes multi-word leaks:
  , ,
  , .

- GENERIC_CREDENTIAL_ENV_PATTERN: add  to negative lookbehind
   to prevent matching  inside
  when the latter is already redacted.

- GENERIC_HEADER_FIELD_PATTERN replacer: skip values starting with
   to preserve specific labels from earlier passes.

- Add 7 regression tests covering both finding categories.

* fix: address reviewer findings P1-P4

P1: Custom enumerable error properties now redacted in log.ts
  logError iterates all own enumerable properties on the original error
  and applies redactSensitiveInfo to string values and jsonRedactor to
  object values, preventing credential-bearing custom fields from leaking
  through the sanitized error. Regression tests added in log.test.ts.

P2: Soften single-source-of-truth claim; migrate easy call sites
  Header comment in redaction.ts updated to acknowledge that specialized
  scanners (secretScanner.ts, xaa.ts) are intentional exceptions.
  src/services/mcp/client.ts and src/services/mcp/auth.ts now use
  jsonRedactor for header redaction instead of ad-hoc key checks.

P3: Fix mock.restore cleanup in channelNotification.test.ts
  Cache-bust the real channelAllowlist module at describe-entry and
  re-register it in afterAll, following the pattern from bugfixes.test.ts.
  mock.restore alone does not clear mock.module overrides in Bun.

P4: Remove unused ChannelGateResult kinds
  Removed 'auth' and 'policy' from the skip kind union and removed
  corresponding dead branches in useManageMCPConnections.ts.

* fix: extract sanitizeError() to fix CI test fragility

The logError tests were failing in CI due to parallel test execution
racing on the module-level errorLogSink singleton. Extract the inline
sanitization logic into an exported sanitizeError() helper and test
that directly — it's pure, has no env-var or sink dependencies, and
doesn't interact with shared mutable state.

* fix: use Object.getPrototypeOf(err) instead of err as prototype in sanitizeError

Object.create(err) sets the original error instance as the prototype of the
sanitized copy, leaking non-enumerable own properties through the prototype
chain. Use Object.getPrototypeOf(err) instead so the prototype is the error
constructor's prototype (e.g. TypeError.prototype), preserving instanceof
checks without exposing the original error's non-enumerable fields.

Add a regression test verifying non-enumerable properties do not leak and
update the prototype-chain test to assert Object.getPrototypeOf result.

* fix: apply key-aware redaction and fail closed on non-serializable error props

- String properties: use jsonRedactor(key, value) instead of
  redactSensitiveInfo(value) so keys like apiKey with innocuous values
  (e.g. 'my-key') are still caught via SENSITIVE_FIELD_SUBSTRINGS.
- Object path: catch now replaces non-serializable/circular references
  with '[REDACTED]' instead of leaving the original object reference.
- Add 2 regression tests for key-aware redaction and fail-closed behavior.

* Update src/utils/log.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: redact bare auth header keys in JSON/header objects

- Add 'auth' to SENSITIVE_FIELD_SUBSTRINGS in src/utils/redaction.ts:109 to match URL/diagnostic redactors treatment of auth
- Add regression test for bare auth header keys in src/utils/diagnostics/redaction.test.ts:88

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: narrow auth matching, redact nested transcript JSONL, fix channel skip message

* fix: address CodeRabbit nits — comment, hint, JSONL fallback redaction

* fix: key-aware malformed JSONL fallback and auth/x-auth in free-form text

* fix: strengthen redactJsonLines trailing rest redaction and auth test assertions

* fix: preserve non-JSON prefix in redactJsonLines fallback and redact it

* fix: tighten redactJsonLines prefix test to exact output assertion

* fix: redact MCP log sink payloads and errorStr before writing to disk

* fix: address P1 findings — URL #-in-password, ;-delimited query params, split channel trust-boundary

- Allow  in URL userinfo password on malformed-URL fallback path
  (new URL() fails when password contains fragment delimiter).
- Redact -delimited sensitive query params by splitting on both & and ;
  in redactMalformedQuery, plus redactSemicolonQueryParams post-processor
  for valid-URL output.
- Restore channelNotification.ts to upstream/main to fully split
  OAuth/org-policy trust-boundary changes from credential redaction PR.

* fix: update callers to match upstream/main function signatures

channelNotification.ts was restored to upstream/main to split
trust-boundary changes from the redaction PR. This commit updates
the three caller sites that previously passed extra arguments:

- ChannelsNotice.tsx: pass getSubscriptionType() + undefined to
  getEffectiveChannelAllowlist (needs 2 args upstream)
- interactiveHandler.ts, channelNotification.test.ts: drop 3rd
  pluginSource arg from findChannelEntry (takes 2 args upstream)

* fix: address reviewer findings — OAuth mock, notice states, marketplace disambiguation

P1: Mock getClaudeAIOAuthTokens and getSubscriptionType in channel
notification tests so they pass on CI where no real OAuth exists.

P2: Restore blocked-auth/org-policy notice states in ChannelsNotice.tsx
so the UI shows the correct blocker when gateChannelServer rejects
unauthenticated users or orgs without channelsEnabled.

P2: Add pluginSource disambiguation to findChannelEntry so same-name
plugin entries from different marketplaces are matched by runtime
source rather than first-match order. Add regression test with
non-matching marketplace first to cover the bug.

* fix: address reviewer findings — relay gate parity and allowlist regression test

- Replace filterPermissionRelayClients in interactiveHandler with inline
  gateChannelServer call so the relay predicate checks ALL gates including
  disabled-channel, auth, org policy, and approved-plugin allowlist, not
  just session entry + marketplace.
- Clean up unused imports (getAllowedChannels, parsePluginIdentifier,
  findChannelEntry, filterPermissionRelayClients).
- Add regression test: gateChannelServer rejects marketplace-matched
  plugin not on approved allowlist (full-gate path).

* fix: redact mixed semicolon secrets in valid-URL path and route OpenAI shim through centralized redactor

P1: Pre-redact semicolon-delimited sensitive query params from the raw
query string in redactUrlForDisplay BEFORE URLSearchParams encodes
; as %3B. Previously model=ok;token=SECRET leaked because
parsed.toString() reserialized to model=ok%3Btoken%3DSECRET, making
it invisible to the post-process pass.

P1: Route openaiShim's redactUrlForDiagnostics through the centralized
redactUrlForDisplay so the semicolon fix, malformed-URL fallback, and
all future redaction improvements apply to OpenAI-compatible
diagnostic logs too. Keep redactSecretValueForDisplay as an additional
safety net after the centralized pass.

Add 3 regression tests for mixed-separator queries.

* Update src/utils/redaction.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: add fragment-query credential regression test and correct dev-channel gate comments

P2: Add regression test for redactUrlForDisplay with query-like credential
in fragment (e.g. #debug?token=SECRET). Fix raw-query pre-processing to
only extract query before the first #, preventing fragment content from
being treated as query parameters.

P3: Update comments in interactiveHelpers.tsx to match the actual gate
order — OAuth and org-policy gates still exist in gateChannelServer()
after restoring to upstream/main; the --dangerously-load-development-
channels flag only bypasses the allowlist gate.

* fix: add port+fragment+@ fallback test and restructure dev-channels dialog tests

* fix: registerDevChannels seam, bare-host #-in-password heuristic, and coverage restructure

* fix: add OAuth and org-policy gate test coverage

- Refactor auth module mock to use mutable variables per test
- Auth gate test: empty OAuth tokens -> kind:auth
- Policy gate test: team subscription without channelsEnabled -> kind:policy

* fix: prefer exact server channel entries before plugin disambiguation

- Return exact server-kind candidate first when candidates include both server and plugin entries with same name
- Added regression test covering mixed server/plugin --channels entries to ensure exact server opt-in is not overridden by plugin candidate
- This prevents a plugin marketplace mismatch from incorrectly rejecting a server the user explicitly selected via server:plugin:slack

* fix: only trust exact [REDACTED] placeholder in generic header field pattern

- Changed GENERIC_HEADER_FIELD_PATTERN to only bypass exact '[REDACTED]' canonical placeholder
- Prevents non-canonical placeholders like '[REDACTED_API_KEY]' or '[REDACTED_actual_secret]' from leaking through
- Updated tests to expect canonical '[REDACTED]' output for generic pattern

* fix: handle bare hosts in malformed URL userinfo fallback

- Added regex to recognize bare hostnames (with optional port) in the fragment heuristic
- Added tests for //alice:sec#ret@host and //alice:sec#ret@host:443

* fix: add relay dispatch path test for non-allowlisted plugin

- Added test using full gateChannelServer predicate in filterPermissionRelayClients
- Mirrors the exact relay dispatch path used in interactiveHandler
- Ensures marketplace-matched plugin not on allowlist is excluded from permission preview

* fix: enhance URL redaction logic to handle valid hosts before fragment

* fix: refine URL redaction logic to ensure valid host checks before fragment

* fix: enhance redaction logic to handle embedded URLs in free-form text

* fix: update redaction logic to remove user info from OpenAI base URL in diagnostic report

* fix: ensure findChannelEntry returns undefined when no exact matches are found

* fix: improve URL redaction logic to remove user info and ensure proper formatting

* fix: enhance redactDiagnosticUrl to preserve query-param values and trailing slashes

* fix: refine redaction logic to preserve meaningful path segments and handle trailing slashes correctly

* fix: enhance redactDiagnosticUrl to preserve literal path segments and handle trailing slashes correctly

* fix: preserve semicolon-delimited query params during redaction

* fix: update redaction logic to support semicolon-delimited query parameters

* fix: enhance redactUrlForDisplay to handle bare hosts and improve fragment redaction

* fix: enhance redactUrlForDisplay to correctly handle username-only userinfo with fragments

* fix: address privacy findings — URL redaction in jsonRedactor, base URL redaction, diagnostic object collapsing, structural channel previews, pluginSource telemetry

* fix: preserve falsey env-presence values in diagnostic redaction

- false, "", and 0 under isEnvPresenceKey keys are now preserved as-is
  instead of misrepresented as "[set]"
- Added regression test for absent/falsey env-presence inputs

* fix: address CodeRabbit findings — sync describe, heartbeat emitter, responsesBody filtering, dev entry precedence

* chore: remove stray Windows path artifact

* fix: update redaction import path in taskReport module

* fix: address CodeRabbit P1-P3 findings and rebase regressions

- F1: rebase onto upstream/main, fix taskReport.ts import path
- F2: Ollama native chat code recovered via rebase (6 functions)
- F3: &-truncation in credential regexes fixed via post-processing pass
- F4: 'tokens' added to jsonRedactor EXCLUDED_KEYS
- F5: redactHomePath case-sensitivity aligned with redactPathForStatus
- F6: credential metadata object preserved in issue report (sensitive-key check
  moved inside type branches)
- F7: heartbeat tests updated for pre-drain write behavior
- F8: reportTask test expects [REDACTED] (matches centralized output)
- rm: stray C:\repo\ Windows path artifact

* fix: address reviewer findings — generic regex &-handling and diagnostic secret-key masking

- Remove & from excluded char classes in 4 generic patterns so they consume
  full secret values (URL-query &-splitting belongs in redactUrlForDisplay).
- Remove now-obsolete &-tail post-processor pass.
- Remove credential from DIAGNOSTIC_SECRET_KEY_PATTERN so issue report
  credential metadata objects are traversed, not collapsed.
- Restore broad isDiagnosticSecretKey check before type dispatch in
  redactDiagnosticObjectInternal so objects/arrays under secret-marked keys
  (auth, password, token, etc.) are masked.
- Update issue report test baseUrl expectation (no trailing &mode=test after
  generic redactor consumes past &).

* fix: address reviewer findings — URL delimiter safety, jsonRedactor #-drop, embedded URL query redaction

- Restore &#; delimiters in generic pattern value classes (F1) so safe
  query tails (&mode=test) survive. Re-add &-tail post-processor for
  non-URL abc&def case.
- Gate redactUrlForDisplay in jsonRedactor to https?:// strings only (F2)
  to prevent #-drop on ordinary text like 'fails after #setup'.
- Add URL query redaction step to redactSensitiveInfo (F3) that extracts
  https?:// URLs from free-form text and routes them through
  redactUrlForDisplay, catching signature/sig params that generic patterns
  miss. Skip already-redacted URLs to avoid double-redaction.

* fix: add Cookie/Set-Cookie semicolon-safe redaction pass, tighten &-tail regex

* fix: COOKIE_PATTERN consume comma-joined multi-cookie values

* fix: address P2 findings — URL redact skip, pre-drain write promise, permission truthy check

* fix: update log.test.ts expectation, add protocol-relative URL support

* fix: enhance redaction for provider env-vars in URLs, preserve safe query params

* fix: enhance redaction for uppercase provider keys and cookie query params

* fix: enhance redaction for bare Bearer and JWT tokens in sensitive info

* fix: update report task test expectations for new redaction format

* fix: limit token exemption to numeric values, protect semicolon cookie query tails

* test: add tests for truncateForPreview to ensure sensitive data redaction

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-07 22:01:40 +08:00
22fa5b4227 fix(copilot): auto-refresh Copilot token on 401 instead of only showing re-auth hint (#1766)
* fix(copilot): auto-refresh Copilot token on 401 instead of only showing re-auth hint

When GitHub Copilot returns 401 'token expired', the existing flow only
showed a hint to run /onboard-github (the #1042 fix) but never actually
refreshed the token mid-session.

Now _doRequest detects the 401 + 'token expired' body in GitHub mode,
calls refreshCopilotTokenOn401() to exchange the stored OAuth token for
a fresh Copilot token, updates process.env, and retries the request.

Fixes #1746

* fix: promote refreshedCopilotToken in auth precedence and add 401 retry integration test

- Fix auth value precedence in buildHeadersForAttempt so refreshedCopilotToken
  takes effect even when a CredentialPool is active (single key still creates
  a pool, shadowing the refreshed token on retry)
- Add integration test verifying GitHub Copilot 401 'token expired' triggers
  refreshCopilotTokenOn401() and retries chat_completions with the new token
  (P2 for PR #1766 review)

* test: add codex_responses 401 retry integration test (CodeRabbit P2)

Adds a new test 'GitHub Copilot 401 codex_responses retries with refreshed
token' that verifies the 401 retry path for the codex_responses transport
(lines 2349-2390). Mocks performCodexRequest to throw APIError(401, 'token
expired') on first call and return a valid SSE response on second, verifies
refreshCopilotTokenOn401 is invoked and the retry uses the refreshed token.

* review: shared token-expired helper, credential-pool test, edge case tests

Finding 1: Promote token-expired detection into isCopilotTokenExpiredError()
helper used by both chat_completions and codex_responses paths.

Finding 2: Add credential-pool integration test verifying that
refreshedCopilotToken takes precedence over credentialPool.next() when
OPENAI_API_KEYS is active.

Finding 3: Add edge case tests - 'token has expired' variant, 401 without
expired substring (no refresh), and same-token refresh (retries with original
token, exhausted, then fails).

* review: credential source check in codex retry path, fix mock leak

- Add credential source gate (apiKey === process.env.OPENAI_API_KEY) to
  codex_responses 401 retry path, matching the chat_completions path guard
- Add same-token guard (newApiKey !== apiKey) after refresh in codex path
- Remove leaking mock.module('./codexShim.js') from providerOverride test,
  use fetch mock instead so performCodexRequest converts 401 to APIError
- Remove duplicate old test that was left behind by previous edit
- Add providerOverride test verifying the credential source gate blocks refresh

* review: P2 refreshedCopilotCodexToken, P3 guard continue on last attempt

P2: Add refreshedCopilotCodexToken variable that takes precedence over
providerOverride.apiKey on retry, mirroring the chat path's refreshedCopilotToken
in buildHeadersForAttempt. Prevents stale override key from being reused after
a successful refresh.

P3: Guard the chat path's continue with attempt < maxAttempts - 1 so the
original 401 is surfaced when the 401 lands on the final retry slot, instead
of falling through to the generic 500 exit error.

* review: P1 chat path credential gate, P3 log refresh failures

P1: Add oldToken === (process.env.OPENAI_API_KEY ?? '') check in chat
path before calling refreshCopilotTokenOn401(), matching the codex path
guard. Prevents credential substitution when providerOverride.apiKey,
route credential, or custom auth header was the failing credential.
Add regression test for chat path with mismatched providerOverride.

P3: Log caught error with logForDebugging before returning false in
refreshCopilotTokenOn401() catch block, so secure storage read failures,
token exchange rejections, and persistence failures are visible in debug
logs instead of silently swallowed.

* fix: restrict codex refresh path to Copilot/GHE endpoints only

isGithubWithCodexTransport was using isGithubMode which is true for all
GitHub modes including GitHub Models API and custom routes. Changed to
isGithubCopilotEndpoint that checks githubEndpointType === 'copilot' ||
githubEndpointType === 'ghe', matching the chat path's isGithubCopilot
guard. Prevents COPILOT_HEADERS and refreshCopilotTokenOn401 from being
triggered for non-Copilot endpoints.

* fix: move didRefreshCopilotToken assignment after credential source check

The refresh flag was set before confirming oldToken matches
process.env.OPENAI_API_KEY, burning the single refresh chance when the
credential pool rotates to a non-matching key first. Moved inside the
credential source check so it's only consumed for eligible credentials.

* fix: add oldToken guard to chat-path credential source check

Prevents '' === '' match when both Authorization and OPENAI_API_KEY are
empty from burning the refresh flag on an unauthenticated request.

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-26 06:53:19 +08:00
9c0d5c61e2 fix(deps): remove deprecated uuid install path by replacing vertex-sdk with local client (#1771)
* fix(deps): remove deprecated uuid install path

* fix(api): address PR #1232 review — type the local Vertex client surface

Resolves the blocker raised by @Vasanthdev2004, @gnanam1990, and @jatmn: the
in-repo AnthropicVertex replacement compiled under bun (no type-check) but added
4 `tsc --noEmit` errors that the upstream typed SDK did not.

- Declare `messages`/`beta` as typed class fields (BaseAnthropic doesn't, but
  the upstream @anthropic-ai/vertex-sdk client did), so typed consumers —
  client.ts `new AnthropicVertex(...)` and the SDK calling `.messages` — keep
  the resource surface. (vertexClient.ts:145/146, test:53)
- Widen the header-merge helpers to accept the base client's request header type
  (HeadersLike), and handle the NullableHeaders shape it actually passes so the
  merge stays correct, not just type-clean. (vertexClient.ts:182)

Also drops the now-stale `@anthropic-ai/vertex-sdk` entries left behind by the
dependency removal:
- scripts/externals.ts INTENTIONALLY_BUNDLED (P3)
- knip.json ignoreDependencies

Testing: `tsc --noEmit` clean; vertex/client/gemini tests 51 pass; smoke green
(INTENTIONALLY_BUNDLED back in sync, 57 entries); knip clean.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address CodeRabbit review on PR #1232 — auth precedence + coverage

- Security (vertexClient.ts): merge resolved Google auth headers LAST so a
  caller-supplied Authorization / x-goog-user-project can't override the Vertex
  credential and send the wrong token upstream. Other request headers still
  pass through unchanged.
- Tests (vertexClient.test.ts): add focused regression coverage for the
  previously-unguarded routing/auth branches —
    * streaming → :streamRawPredict path (+ model stripped, stream preserved)
    * count_tokens → count-tokens:rawPredict path rewrite
    * auth-header precedence: caller Authorization does NOT override the Vertex
      token (guards the fix above + exercises the NullableHeaders merge branch).

Testing: tsc clean; vertexClient tests 5 pass; full src/services/api 840 pass;
smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): validate and encode Vertex model before building the URL

CodeRabbit follow-up on PR #1232: `model` was interpolated straight into the
Vertex endpoint path, so a missing/non-string model would silently route to
`.../models/undefined:rawPredict` instead of failing fast. Now throw a clear
error on a missing/empty model and encodeURIComponent the value before building
the path. Adds a focused test for the missing-model case.

Testing: tsc clean; vertexClient tests 6 pass; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address remaining review items from PR #1232

- [P2] Fix count_tokens method guard: apply method==='post' to both paths
- [P3] Remove unused accessToken option from AnthropicVertex
- [P3] Narrow batches type on messages/beta resources with Omit

* test: add count_tokens?beta=true routing regression test

---------

Co-authored-by: Kevin Codex <kevin@gitlawb.com>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-24 13:03:29 +08:00
3eb57c6d13 fix: upgrade shell-quote 1.8.3 -> 1.8.4 (CVE-2026-9277) (#1764)
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-24 09:44:01 +08:00
1aabe261db feat(bughunter): make /bughunter public + add /bughunter-security & /bughunter-perf with robust fallback prompts (#1621)
* feat(bughunter): split into /bughunter, /bughunter-security, /bughunter-perf

Replace the single /bughunter command with three siblings that share a
common prefix:

  /bughunter          — general bug hunt (existing prompt, untouched)
  /bughunter-security — OWASP-aligned, exploit-driven, confidence ≥ 8
  /bughunter-perf     — hot-path complexity, sync I/O, leaks, N+1

Both new subcommands are prompt commands built with
createMovedToPluginCommand so they migrate to the bughunter marketplace
plugin unchanged once it ships. While the marketplace is private they
inline the full audit prompt (frontmatter + !`git ...` blocks) just like
the existing /bughunter.

All three stay in the public COMMANDS list (not INTERNAL_ONLY_COMMANDS)
so non-ant users can invoke them. clearCommandMemoizationCaches() now
also flushes the zero-arg COMMANDS() and builtInCommandNames() memos so
tests can switch USER_TYPE mid-run without poisoning the cache.

Adds regression tests in src/commands.test.ts covering:
  - bughunter stays public for non-ant users
  - bughunter-security and bughunter-perf are in the public list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(bughunter): remove orphan index.js after .js → .ts rename

The bughunter command directory was renamed from a single .js file to
index.ts in the previous commit, but git tracked them as separate paths
so the old .js was left in the tree. Drop it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(bughunter): enhance fallback prompts for robustness in non-git environments

- Add graceful error handling to all git commands in fallback prompts (|| echo fallbacks)
- Add explicit non-git fallback guidance in Phase 1 for all three commands
- /bughunter: search for entry points, core business logic, recently modified files
- /bughunter-security: search for auth/middleware, validation, DB, config, upload code
- /bughunter-perf: search for handlers, loops, data access, serialization, build configs
- Improve context labels to clarify git context may be empty

* fix(bughunter): address CodeRabbit feedback

- Fix test isolation: restore USER_TYPE/IS_DEMO env vars in finally blocks
- Add non-git fallback test cases for all three bughunter commands
- Fix bash pipeline issue: replace if/then/else subshells with simple git commands + static fallback text in template
- Fix output format contradiction: remove LOW confidence from scoring (Phase 3 drops LOW, so scoring only includes Critical/Medium)

* fix(test): correct case and prefix in git fallback assertions for bughunter-security and bughunter-perf tests

* fix(test): add missing opening parenthesis in bughunter test assertions

* fix(bughunter): complete non-git fallback and propagate allowedTools

- Fix git commands in all three prompts to always succeed with fallback text (using || echo)
- Modify createMovedToPluginCommand to accept allowedTools parameter
- Add allowedTools to all three bughunter commands so slash-command turn grants declared tools
- Parse allowed-tools from frontmatter at command creation time

* fix(bughunter): complete non-git fallback and allowedTools propagation

- Fix git commands in prompts to always succeed with fallback text (using || echo)
- Modify createMovedToPluginCommand to accept allowedTools parameter
- Add allowedTools to all three bughunter commands so slash-command turn grants declared tools
- Fix RECENTLY COMMITTED FILES command to avoid command substitution (permission check rejects )
- Update tests to accept shell tool's '(Bash completed with no output)' for empty results
- Use runWithCwdOverride and additionalWorkingDirectories for proper test isolation

* fix(bughunter): prevent shell injection via user-provided args

The user-provided scope was interpolated into the prompt template BEFORE
executeShellCommandsInPrompt() ran, so any !command or ```! block
syntax in the args would be interpreted and executed as shell commands.

Fix: parse frontmatter from the raw template and run shell execution first
(with {{ARGS}} still in place — inert to shell patterns), then replace
{{ARGS}} with the user scope on the processed output. This ensures args
are never fed through the shell command parser.

* refactor(bughunter): use createGetAppStateWithAllowedTools helper

Replaces duplicate inline getAppState overrides across all three bughunter
commands (bughunter, bughunter-security, bughunter-perf) with the shared
helper from src/utils/forkedAgent.ts. This:
- Eliminates ~30 lines of duplicated permission context modification
- Merges allowedTools with existing alwaysAllowRules.command (vs overwrite)

* fix(bughunter): address jatmn review - String.replace special patterns + test isolation

- Replace '{{ARGS}}' with a replacer function () => scope instead of
  the plain string 'scope'. JavaScript's String.replace treats $&, $',
  $', , 32855 specially even in string replacements, so a scope like
  'src/auth $&' would render as 'src/auth {{ARGS}}' instead of literal
  text. The replacer function bypasses all special patterns.

- Restore USER_TYPE and IS_DEMO env vars in the injection regression
  test's finally block, matching the isolation pattern used by all other
  bughunter tests.

* fix(bughunter): make fallback prompt generation work on Windows

Wrap executeShellCommandsInPrompt() in a try/catch in all three bughunter
commands. On platforms where bash is unavailable (e.g. Windows without Git
Bash), the bash-specific shell syntax (2>/dev/null, | head -N) would cause
executeShellCommandsInPrompt to throw MalformedCommandError, preventing the
prompt from being generated at all.

The catch handler replaces the !`command` inline patterns with a static
placeholder, allowing the LLM to still receive the full audit instructions
and non-git search strategies in Phase 1.

* fix(bughunter-perf): remove Low severity contradiction

The summary line included Low: L but Phase 3 drops non-measurable findings
and exclusions remove micro-optimizations. Low findings (measurable but
not user-visible) would never survive the filter, so remove Low from the
severity categories and summary line.

fix(bughunter-security): align log-forging exclusion with A9 criteria

Exclusion #11 blocked all log spoofing/forging, but A9 says to flag
log injection when it enables audit-trail forgery. Narrowed the exclusion
to allow concrete audit-trail attacks through while still excluding
generic non-exploitable logging suggestions.

* fix(bughunter-security): tighten log-forging exclusion threshold

Reword exclusion #11 to require concrete evidence of a log-entry or
structured-field forgery path, not merely unsanitized user input.

* fix(bughunter): preserve fallback text on Windows/no-bash path

Replace generic '(Shell execution unavailable)' placeholder with a regex
that extracts the || echo "..." fallback text from each shell command.
This ensures the prompt shows meaningful messages like
'(If empty: not a git repository or git unavailable)' even when bash is
unavailable (e.g. Windows without Git Bash), matching what Linux users see
from working shell execution.

Also make injection test assertion platform-agnostic — accept either bash
output or the static echo fallback text.

* refactor(test): extract duplicate mockContext into createMockToolContext helper

The three non-git fallback tests each had an identical ~42-line mockContext
object. Moved it to a shared createMockToolContext(cwd, commands) helper
and a FULL_GIT_COMMANDS constant. Also updated the injection test to use
the same helper. Net -89 lines.

* fix(createMovedToPluginCommand): only grant allowedTools when fallback prompt runs

The ant (USER_TYPE === 'ant') branch returns a plugin-install notice that
doesn't need Read/Glob/Grep/Bash tools, but allowedTools was statically
attached to the command object. This caused processSlashCommand to grant
turn-scoped permissions for tools that were never used.

Changed to a getter that returns undefined in the ant branch, so the
plugin-install notice runs without unnecessary tool permissions.

* fix(bughunter): simplify shell commands to single git commands, narrow catch to surface interruptions

* fix(bughunter): surface permission-denied/aborted shell preprocessing, fix Windows cleanup

* fix(dragDropPaths.test): resolve package.json relative to test file, not process.cwd()

* fix(commands.test): restore original cwd in rmRetry, guarantee env/cache cleanup on rm failure

* fix(bughunter): bound diff to 400 lines, swap HEAD~10 for git log -10

Address both P2 reviewer findings on feat/bughunter-command-v3-new.

(1) Fresh-repo HEAD~10 lookup stripped every snippet. In a one-commit
    repo, `git diff --name-only HEAD~10..HEAD --diff-filter=AM` exits
    128 (HEAD~10 doesn't resolve). The shell-execution catch then ran
    the outer "strip all snippets" fallback, leaving git status /
    diff --cached / diff HEAD empty even though those commands would
    have produced useful context. Switched to `git log -10 --name-only
    --diff-filter=AM`, which works at any history depth and yields the
    same file list. Applied to bughunter, bughunter-security, and
    bughunter-perf.

(2) Diff cap removed in 73d0bcb. The prompt label still advertised
    "first 400 lines" but the snippet was just `git diff HEAD -- .`,
    and a 900-line diff was injected verbatim. Added a new `lineLimits`
    option to `executeShellCommandsInPrompt` that bounds output by
    command prefix. The cap is applied to stdout *before*
    processToolResultBlock, so the persistence + empty-content guard
    flows run once on the bounded payload, and large diffs no longer
    hit the 30k Bash result cap and spill into the prompt. Each
    bughunter command passes
    `{ lineLimits: { 'git diff HEAD -- .': 400 } }`. Allowed-tools
    frontmatter is unchanged — no compound `| head -400` that the
    permission parser might reject.

Tests:
- `executeShellCommandsInPrompt applies per-prefix line limits` +
  `does not truncate below the cap` (new unit tests in
  promptShellExecution.test.ts).
- `bughunter keeps git context populated in a fresh single-commit
  repo` (regression for finding 1, uses real one-commit git repo).
- `bughunter diff block is bounded to 400 lines` (regression for
  finding 2, builds 1000-line diff and asserts ≤400 lines).
- `FULL_GIT_COMMANDS` and the injection test now include
  `git log -10 --name-only --diff-filter=AM` in place of the
  removed HEAD~10 form.

* fix(bughunter): keep recent-files path-only, cover all three siblings

Two review follow-ups on the previous P2 commit.

(P3) `git log -10 --name-only` defaulted to --pretty=fuller, so the
"RECENTLY COMMITTED FILES" block injected commit hash, author, date,
and message lines into the prompt under a files-only heading — that
extra metadata crowded out the scoped file list the command was
trying to provide. Added `--pretty=format:` to suppress the commit
header on all three commands (bughunter, bughunter-security,
bughunter-perf). Verified locally: the previous form emitted ~7
header lines per commit; the new form emits just the file paths.

(P2) The fresh-repo and 400-line cap regression tests only exercised
/bughunter, so a sibling could regress back to the old shallow-history
failure or lose the diff cap without this suite failing. Parameterized
both tests over {bughunter, bughunter-security, bughunter-perf} via a
BUGHUNTER_SIBLINGS const; each command now runs both regressions in
its own tmp dir (six new test cases total). Typecheck clean, 27
tests pass.

* fix(promptShellExecution): granular snippet fallback, restore rich error for other callers

- Add granularFallback option to executeShellCommandsInPrompt. When
  enabled, a failing shell snippet is blanked in place and the rest of
  the snippets keep their output. Permission denials and interrupted
  ShellError still rethrow as MalformedCommandError, never swallowed.
- Restore the formatted MalformedCommandError wrapping in the default
  path. Previously a no-op that rethrew the raw ShellError, which made
  processSlashCommand render only 'ShellError: Shell command failed'
  for /commit, /security-review, /commit-push-pr, loaded skills, and
  plugin commands. Now includes the failing pattern and formatted
  stdout/stderr.
- /bughunter, /bughunter-security, /bughunter-perf opt into
  granularFallback and drop the catch-and-strip-all pattern. A failing
  'git log -10' on a zero-commit repo no longer discards git status
  output.
- Tests cover per-snippet blanking, default-path rich error wrapping,
  and that permission denials still surface under granularFallback.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(promptShellExecution): preserve trailing newline in applyLineLimit truncation

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-17 11:04:01 +08:00
c2cf603344 feat(ctx): add /ctx context window visualization and token bars to /cost (#1610)
* feat(ctx): add /ctx context window visualization and token bars to /cost

Adds a new /ctx slash command that surfaces exactly what the model sees
on the next API call, with per-category token bars, last-response
breakdown, session token usage, per-model totals, and a session summary.

The command reuses the same pipeline as /context (compact boundary,
optional context-collapse transform, microcompact, analyzeContextUsage)
so the totals match what is actually sent, not a rough estimate.

A single 'local' command with supportsNonInteractive: true is registered
in the public COMMANDS array (replacing the disabled /ctx_viz stub) and
added to REMOTE_SAFE_COMMANDS and BRIDGE_SAFE_COMMANDS, so /ctx works
identically in interactive REPL, headless -p, remote, and bridge modes.

/cost gains a Token usage section with colored bars for input, output,
cache read, and cache write tokens, appended after the existing
cost/duration/code-changes block without changing the per-model line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ctx-viz: fix bar scale to use contextWindow denominator

- Current Context block: use contextWindow as barMax so bars visually match percentage column
- Session Token Usage: use sessionTotalTokens as sessionMax for same reason
- Update test assertions for new tokens/contextWindow scale

* cost-tracker: add format test for token bar display

* test: add missing mocks for ctx_viz rendering test

* fix(ctx_viz): restore mocks after test; filter capacity rows from model-seen breakdown

* test: verify capacity rows are filtered from ctx viz output

- Add 'Free space' capacity row to mocked analyzeContextUsage categories
- Add assertion that rendered output does not contain 'Free space'
- Verifies CAPACITY_ROWS filtering logic in ctx-noninteractive.ts

* fix: isolate ctx_viz mocks and filter deferred categories

- Restore real modules in afterEach to prevent mock.module() leakage
  into downstream tests (autoCompact, compression)
- Filter deferred categories (isDeferred: true) from model-visible rows
  to avoid overstating context usage

* fix: eliminate mock.module() for leaky modules in ctx_viz test

Only analyzeContext.js is mocked (single data fixture). All other
modules (autoCompact, microCompact, context, model, state) use
their real implementations to avoid process-global mock.module()
leakage into downstream autoCompact and compression tests.

* fix: eliminate mock.module() entirely via renderCtxReport extraction

Extract renderCtxReport() from call() so the rendering test can
construct a hand-crafted RenderInput directly, requiring zero
mock.module() calls. This avoids process-global mock leakage into
downstream autoCompact/compression tests AND makes the test immune
to mock pollution from any preceding test file.

* chore: derive RenderInput from collectCtxData return type, use it in test

- RenderInput is now Awaited<ReturnType<typeof collectCtxData>> — single
  source of truth, no manual property duplication.
- Test imports RenderInput type and uses it in the renderCtxReport assertion
  instead of 'unknown', enabling compile-time fixture validation.

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-16 06:50:45 +08:00
0b24b60ce9 feat(provider): add Fireworks AI as official OpenAI-compatible provider (#1590)
* feat(provider): add Fireworks AI as official OpenAI-compatible provider

Includes vendor descriptor, brand descriptor (276 models), model
descriptors (full + merged), routing metadata, env auto-detection,
profile support, client defaults, and docs.

* test: add focused regression tests for Fireworks AI auth and routing

- Add 7 env-only routing tests in client.test.ts (shim routing,
  stale model replacement, base URL override, shim option cleanup,
  non-Fireworks override ignored, priority with MiniMax, Bedrock yield)
- Add FIREWORKS_API_KEY auto-detection test in providerAutoDetect.test.ts
- Add profile apply/persistence/env-drift tests in providerProfiles.test.ts
- Fix FIREWORKS_API_KEY propagation in strictEnv early return path

* fix: address reviewer comments on Fireworks integration

- Remove OPENAI_API_KEY exclusion so Fireworks cred wins over stale OpenAI key
- Fix TS type error in test by using String() wrapper
- Add Fireworks to detection priority comment in providerAutoDetect.ts
- Add useFireworksEnvOnlyProvider to shim condition for pattern consistency
- Replace loose .includes('fireworks.ai') with isFireworksBaseUrl() exact hostname check

* fix: add explicit case 'fireworks' in applyProviderFlag for credential precedence

- Add 'fireworks' to PREFERRED_PROVIDER_ORDER
- Add case 'fireworks' with dedicated key winning pattern (mirrors atlas-cloud)
- Add FIREWORKS_API_KEY to copiedOpenAIKeyProvider detection so stale
  keys are cleaned up when switching away from Fireworks

* fix: guard fireworks defaultModel assignment against 'undefined' string coercion

* fix: remove leftover conflict marker in providerProfiles.ts

* docs(fireworks): add JSDoc to Fireworks functions for coderabbit docstring coverage

Adds JSDoc annotations to isFireworksBaseUrl, getFireworksBaseUrlOverride,
hasFireworksEnvOnlyProviderIntent, isFireworksModelName, and
applyFireworksEnvOnlyDefaults.

* fix(fireworks): cross-check NEARAI_API_KEY in env-only intent functions

hasNearaiEnvOnlyProviderIntent and hasFireworksEnvOnlyProviderIntent were
missing mutual cross-checks. When both NEARAI_API_KEY and FIREWORKS_API_KEY
are set, neither excludes the other, and nearai silently wins by ordering.
Adding !hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) to the nearai intent
and !hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) to the fireworks intent
ensures both return false, forcing explicit provider selection.

* fix(fireworks): fix typo in JSDoc — OPENAI_API_API_BASE -> OPENAI_API_BASE

* fix(fireworks): remove merge artifact and preserve no-key auth headers

- src/utils/providerAutoDetect.ts: remove leftover ======= conflict
  marker and stale duplicate priority lines
- src/utils/providerProfiles.ts: preserve apiFormat, authHeader,
  authScheme, authHeaderValue in the no-key OpenAI-compatible
  fallback path so saved Responses mode / custom auth config
  survives restart

* fix: Fireworks env-only startup preservation and MIMO priority comment

- Add FIREWORKS_API_KEY check to hasConcreteProviderSelection() so env-only
  Fireworks setup is not overwritten by Gitlawb Opengateway default
- Add regression test verifying FIREWORKS_API_KEY survives no-profile startup
- Fix providerAutoDetect.ts priority comment to include MIMO_API_KEY (position 8)
  and renumber subsequent entries to match actual detection order

* fix: also preserve env-only NEAR AI startup in hasConcreteProviderSelection()

* fix: remove duplicate Fireworks model descriptor, add FIREWORKS_API_KEY to test env cleanup

* fix: move duplicate model check to generation-time, add OPENAI_AUTH_* env cleanup to test harness

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-13 01:10:19 +08:00
eacc7d8fac feat: add NEAR AI provider integration (#1594)
* feat: add NEAR AI provider integration

- Create vendor, brand, and model descriptors for NEAR AI (22 models)
- Add NEAR AI to route metadata, client, provider auto-detect, and profiles
- Update compatibility tests and ProviderManager test PRESET_ORDER
- Add README and docs entries for NEAR AI provider
- Update .env.example with NEAR AI configuration

* fix: address CodeRabbit review comments

- Fix .env.example: change 'Option N' to 'Option 11' in quick reference
- Narrow isNearaiModelName to use explicit NearAI model prefixes instead of broad includes('/')
- Add NEARAI_API_KEY propagation in strictEnv startup path

* fix: align NEAR AI validation host matching with wildcard subdomain routing

- Add *.completions.near.ai to matchBaseUrlHosts in vendor descriptor
- Add matchHostnameAgainstRouteHosts helper with wildcard (*.) prefix support
- Use helper in both resolveRouteIdFromBaseUrl and getRuntimeValidationTarget
- Add regression test for qwen35-122b.completions.near.ai TEE endpoint
- Add NEARAI_API_KEY to test env cleanup list

* fix: align Near AI integration with env-only provider best practices

- Replace loose .includes('near.ai') with isNearaiBaseUrl() in providerProfiles.ts
  for exact hostname validation (all 4 instances)
- Add NEARAI_API_KEY to copiedOpenAIKeyProvider detection in providerFlag.ts
- Add case 'nearai' to applyProviderFlag switch with dedicated key precedence
- Add 'nearai' to PREFERRED_PROVIDER_ORDER
- Add useNearaiEnvOnlyProvider to OpenAI shim condition in client.ts
- Remove OPENAI_API_KEY exclusion from hasNearaiEnvOnlyProviderIntent (dedicated
  key wins over stale generic key, consistent with xAI pattern)
- Update detection priority comment in providerAutoDetect.ts to include
  MIMO_API_KEY, XAI_API_KEY, and NEARAI_API_KEY

* fix: add exact completions.near.ai host to isNearaiBaseUrl

* fix: add higher-precedence provider key exclusions to hasNearaiEnvOnlyProviderIntent

* fix: add OPENAI_API_KEY and MINIMAX_API_KEY exclusions to hasNearaiEnvOnlyProviderIntent

* fix(near-ai): don't let stale OPENAI_API_KEY suppress Near AI routing

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-11 12:09:25 +08:00
286d403093 Update(zen-go): add claude-opus-4-8, minimax-m3, mimo-v2.5-free models and proper effort level integration for Zen/Go models (#1505)
* feat(provider): add OpenCode Zen/Go subscription support

Add OpenCode as a first-class provider, enabling users to connect their
Zen (pay-as-you-go) and Go ($10/mo) subscriptions via the /provider command.

New integration descriptors:
- vendors/opencode.ts — OpenCode Zen vendor (41 models)
- gateways/opencode-go.ts — OpenCode Go gateway (12 models)
- brands/opencode.ts — brand descriptor
- models/opencode.ts — full model catalog (GPT, Claude, Gemini, Qwen,
  GLM, Kimi, MiniMax, Grok, DeepSeek, MiMo, Nemotron)

Modified files:
- integrationArtifacts.generated.ts — register descriptors and presets
- providerProfile.ts — add OPENCODE_API_KEY env/secret key, 'opencode'
  profile type, and buildLaunchEnv handler
- providerConfig.ts — add DEFAULT_OPENCODE_BASE_URL constants

Auth: OPENCODE_API_KEY env var or interactive key entry in /provider
Transport: openai-compatible (chat_completions)
Base URLs: https://opencode.ai/zen/v1 (Zen), /zen/go/v1 (Go)

* feat(provider): add [Zen]/[Go] tags to OpenCode preset labels

Add visual tags in the /provider preset selection to distinguish
OpenCode Zen (pay-as-you-go) from OpenCode Go (subscription).

* feat(provider): enable dynamic model discovery for OpenCode

Switch OpenCode vendor and Go gateway from static to hybrid model
catalog with openai-compatible discovery. Models are fetched from
/v1/models on startup and cached for 1 hour. Manual refresh is
supported via the /provider UI.

Static model list is preserved as fallback when discovery fails.

* test(provider): add comprehensive OpenCode Zen/Go test suite

97 tests across 2 files covering:

Integration tests (72 tests):
- Vendor descriptor: id, label, classification, base URL, model, auth,
  transport, preset, validation, catalog, discovery, usage metadata
- Gateway descriptor: id, label, vendorId, category, base URL, model,
  auth, transport, preset, catalog, discovery
- Brand descriptor: id, label, canonicalVendorId, capabilities, modelIds
- Model catalog: registration, vendor/gateway associations, required
  fields, valid classifications, reasoning/coding tags, no duplicates,
  model counts (41 Zen, 12 Go), modelDescriptorId consistency
- Cross-reference: brand↔model, vendor↔model, gateway↔model,
  shared OPENCODE_API_KEY
- Registry validation: no errors, no preset conflicts
- Edge cases: unique ids, unique apiNames, non-empty labels, valid
  contextWindow/maxOutputTokens, valid defaultModel format, validation
  message content, discovery config

Profile tests (25 tests):
- Type guard: isProviderProfile('opencode'), rejects invalid values
- buildLaunchEnv: persisted env, defaults, process env precedence,
  OPENCODE_API_KEY mapping, whitespace/null/undefined/empty handling,
  very long keys, special characters, concurrent access, boundary
  values, no credential leakage

* fix(provider): add per-model endpoint routing (P1)

Add endpointPath field to OpenAIShimTransportConfig so catalog entries
can specify which API path to use per model. This addresses the
maintainer's [P1] finding that all models were routed to
/chat/completions regardless of their upstream endpoint.

Changes:
- descriptors.ts: add endpointPath?: string to OpenAIShimTransportConfig
- openaiShim.ts: buildRequestUrl checks shimConfig.endpointPath first
- vendors/opencode.ts: add transportOverrides to 31 catalog entries
  (GPT→/responses, Claude/Qwen→/messages, Gemini→/models/<id>)
  + switch to source: 'static' to prevent free models from live API
- gateways/opencode-go.ts: add transportOverrides to 4 entries
  (MiniMax/Qwen→/messages) + switch to source: 'static'
- opencode.test.ts: update tests for static source, remove discovery tests

* refactor(opencode): model OpenCode Zen/Go as gateways (P2)

* docs(provider): document OpenCode setup and move badge metadata to descriptors

- Add OpenCode Zen/Go rows to README supported providers table
- Add OpenCode Zen/Go examples and OPENCODE_API_KEY to advanced-setup.md
- Add PresetBadge type to descriptor/manifest with badge propagation in
  artifact generator
- Move 4 hard-coded preset badges ([FREE], [Sponsor], [Zen], [Go]) from
  ProviderManager.tsx into descriptor preset metadata
- Add badge field to providerUiMetadata so UI components read from manifest
- Update integration overview docs to recommend preset.badge for future
  gateways

* fix(provider): match request body to endpoint format for OpenCode /messages and /responses (P1)

Extend the openaiShim transport so that endpointPath overrides select
both the URL and the correct body/response format:

- /responses → OpenAI Responses API body (input, max_output_tokens)
- /messages  → Anthropic Messages API body (content blocks, system, max_tokens)

Also fixes: abort listener leak in SSE passthrough, system prompt
content-block flattening, and removes [Zen]/[Go] badge entries (P3).

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(provider): add Google AI SDK body/response format for OpenCode Zen Gemini models (P1)

The three Gemini models in the OpenCode Zen catalog (gemini-3.5-flash,
gemini-3.1-pro, gemini-3-flash) were sending chat-completions body to
the /models/gemini-* endpoint, which expects Google AI SDK format.

- effectiveTransport now detects /models/gemini- endpointPath → 'gemini'
- buildGeminiBody() converts Anthropic messages → Google contents[]
  with role mapping, systemInstruction, generationConfig, functionDeclarations
- geminiSseToAnthropic() parses Google SSE frames → Anthropic stream events
  with text deltas, functionCall tool_use, finishReason mapping
- _convertGeminiToAnthropicResponse() for non-streaming responses
- Streaming/non-streaming routing via URL detection (/models/gemini-)
- serializeBody(), hasToolsPayload, omitGeminiTools all updated

* fix: prevent OpenCode model descriptors from shadowing canonical limits

P1: Prefix all defaultModel values in opencode.ts with 'opencode-'
so the fallback findModelDescriptorForApiName() doesn't match
canonical model names. The OpenCode descriptors are still found
via catalog entry lookup when the OpenCode route is active.

P2: Add 'OpenCode Go' and 'OpenCode Zen' to PRESET_ORDER in
ProviderManager.test.tsx between 'OpenAI' and 'OpenRouter'
so navigateToPreset() sends the correct number of j keypresses.

* fix: align OpenCode Go descriptor metadata with Zen

- category: 'hosted' → 'aggregating' (both are aggregating gateways)
- add validation block with OPENCODE_API_KEY guidance
- update test assertion from 'hosted' to 'aggregating'

* fix: accept OPENAI_API_KEY as fallback in OpenCode validation

When users set up OpenCode Zen/Go via /provider, the key is saved as
OPENAI_API_KEY (via buildCompatibilityProcessEnv). The validation block
only checked OPENCODE_API_KEY, causing a startup warning even though
the runtime auth header had the key it needed.

Add OPENAI_API_KEY to validation.credentialEnvVars for both gateways,
matching the pattern used by Hicap and Gitlawb Opengateway.

* chore: trigger mergeability recheck

* feat(shim): forward effort/thinking to OpenCode Zen/Go endpoints

- buildResponsesBody: add reasoning_effort + reasoning_summary + include
- buildAnthropicMessagesBody: add thinking config (adaptive/enabled/budget)
- buildGeminiBody: add thinkingConfig with thinkingLevel mapping
- modelSupportsEffort: allow OpenCode Claude and Gemini models
- modelSupportsMaxEffort: add opus-4-7
- getAvailableEffortLevels: show standard levels for OpenCode native models
- opencode-go: add missing validation block

* feat: update OpenCode Zen and Go model counts, add new models, and enhance effort level handling

* feat: implement xhigh effort support for specific models and adjust effort level handling

* fix(effort): address reviewer feedback on xhigh + new models

- docs/advanced-setup.md: bump OpenCode Go count 12 → 13
- openaiShim.ts: include opus-4-8 / opus-4.8 in the adaptive thinking
  detection so the new model uses the adaptive + effort path instead
  of falling back to budgetTokens
- effort.ts: modelUsesOpenAIEffort now also rejects models that include
  'claude-' or 'gemini-' — without this, OpenCode Claude/Gemini
  routes (provider=openai) were misclassified as OpenAI-style and
  could leak xhigh past the new gate
- effort.codex.test.ts: lock in the new exclusion with a regression
  test against the openai provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): address reviewer feedback on xhigh effort + new models

Closes the three P2 findings from PR #1505 review:

1. Settings schema now accepts 'xhigh' so a persisted xhigh survives
   restart instead of being silently dropped by .catch(undefined).
2. ModelPicker /effort cycle is driven by getAvailableEffortLevels(model)
   instead of a boolean includeMax, so models supporting xhigh
   (opus-4-7/4-8, OpenAI/Codex) can actually select it from the picker.
   displayEffort clamp now uses the available levels list, so stale
   xhigh also clamps to high when the focused model doesn't support it.
3. SDK/control metadata uses getAvailableEffortLevels(model) instead of
   the EFFORT_LEVELS fallback that advertised xhigh to every max-capable
   model. SDK schema + generated types extended to include 'xhigh'.

Also fixes a latent generator bug: the array case in generate-sdk-types
now parenthesizes union/intersection elements so the trailing [] binds
the whole type, e.g. ("a"|"b")[] rather than "a"|"b[]. Without this,
the regenerated xhigh levels ended up typed as the single-literal
"xhigh"[] and broke the modelInfo assignability check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(effort): order xhigh before max in EFFORT_LEVELS

EFFORT_LEVELS now matches getAvailableEffortLevels() output order
(['low', 'medium', 'high', 'xhigh', 'max']), and the order asserted by
the existing effort.codex.test.ts tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(effort): order xhigh before max in settings + SDK schemas

Matches the EFFORT_LEVELS / getAvailableEffortLevels order from the
previous commit. The Zod enum order doesn't affect runtime validation,
but keeps the source consistent and avoids confusion if anyone reads
the enum literal to infer display order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): clamp ModelPicker selection and mark xhigh as current

- ModelPicker.handleSelect: clamp the emitted/persisted effort to the
  focused model's available levels so a toggled-but-unsupported level
  (e.g. 'xhigh' on a model that doesn't support it) is never written
  to settings.json or handed to the consumer. Add focusedAvailableLevels
  + focusedDefaultEffort to the memo guard so the function regenerates
  when the focused model changes.
- EffortPicker: compare the xhigh option against the persisted 'xhigh'
  level directly. The 'max' alias path is kept only for legacy
  settings.json values that still hold 'max' from before xhigh was
  introduced.

* docs(effort): fix stale EffortPicker comment about xhigh normalization

openAIEffortToStandard is a type cast that passes 'xhigh' through as a
first-class EffortLevel — the shim only converts to 'max' at the
Anthropic request boundary, not here. Update the comment to match.

* docs(effort): update /effort help to match xhigh support matrix

The /effort --help output still described max as "Opus 4.6 only" and
xhigh as an "alias for max", but this PR promotes xhigh to a first-class
EffortLevel and allows it for OpenCode Claude Opus 4.7/4.8 (with max
also allowed for those Opus variants). Update the help so it matches
the picker/runtime behavior:
- max: "(Opus 4.6+)"
- xhigh: "(OpenAI/Codex and Opus 4.7+)"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sdk): address reviewer P2 — sync xhigh across override union, schemas, CLI

- Add 'xhigh_effort' to ModelCapabilityOverride union so the new
  call at effort.ts:93 typechecks (P2 finding 1).
- Add 'xhigh' to AgentDefinition.effort enum (coreSchemas.ts) and
  control.applied.effort enum (controlSchemas.ts), then regenerate
  coreTypes.generated.ts so the SDK public contract matches the
  first-class effort level (P2 finding 2).
- Add 'xhigh' to the --effort CLI flag allowed list and help text
  (main.tsx:945-951) so users can actually pass --effort xhigh
  instead of hitting "It must be one of: low, medium, high, max".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): narrow allowlist to shim-serialized models; sync max description

Address reviewer findings on PR #1505:

P2: The broad `m.includes('opus-4') || m.includes('sonnet-4')` branch
made older variants (claude-opus-4-1, claude-sonnet-4-5) advertise
effort support, but the Anthropic /messages shim only serializes
low/medium as anthropicBody.effort for the isAdaptive || isOpus45
set (opus-4-5/4-6/4-7/4-8, sonnet-4-6). For other models the shim
only emits thinking for high/max, so low/medium on those models
was silently dropped on the wire. Collapse the two 4-model branches
into one that matches the shim's serialization set; the substring
match still covers prefix variations (claude-, opencode-claude-).

P3: getEffortLevelDescription('max') said "Opus 4.6 only" but
modelSupportsMaxEffort now allows opus-4-6, opus-4-7, opus-4-8.
Update the shared description to "Opus 4.6+" so the picker and
/effort confirmation agree with the new support matrix (matching
the /effort --help text from 3cf5de2).

Add effort.codex.test.ts coverage: assert that opus-4-5/4-6/4-7/4-8
and sonnet-4-6 support effort, while opus-4-1, opus-4-2, and
sonnet-4-5 do not (the latter three were previously true via the
broad substring match and are now correctly excluded).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger CodeRabbit re-review

* fix(effort): gate modelSupportsXHighEffort on modelSupportsEffort

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 08:43:20 +08:00
5a22d604f8 feat(provider): add OpenCode Zen/Go subscription support (#1350)
* feat(provider): add OpenCode Zen/Go subscription support

Add OpenCode as a first-class provider, enabling users to connect their
Zen (pay-as-you-go) and Go ($10/mo) subscriptions via the /provider command.

New integration descriptors:
- vendors/opencode.ts — OpenCode Zen vendor (41 models)
- gateways/opencode-go.ts — OpenCode Go gateway (12 models)
- brands/opencode.ts — brand descriptor
- models/opencode.ts — full model catalog (GPT, Claude, Gemini, Qwen,
  GLM, Kimi, MiniMax, Grok, DeepSeek, MiMo, Nemotron)

Modified files:
- integrationArtifacts.generated.ts — register descriptors and presets
- providerProfile.ts — add OPENCODE_API_KEY env/secret key, 'opencode'
  profile type, and buildLaunchEnv handler
- providerConfig.ts — add DEFAULT_OPENCODE_BASE_URL constants

Auth: OPENCODE_API_KEY env var or interactive key entry in /provider
Transport: openai-compatible (chat_completions)
Base URLs: https://opencode.ai/zen/v1 (Zen), /zen/go/v1 (Go)

* feat(provider): add [Zen]/[Go] tags to OpenCode preset labels

Add visual tags in the /provider preset selection to distinguish
OpenCode Zen (pay-as-you-go) from OpenCode Go (subscription).

* feat(provider): enable dynamic model discovery for OpenCode

Switch OpenCode vendor and Go gateway from static to hybrid model
catalog with openai-compatible discovery. Models are fetched from
/v1/models on startup and cached for 1 hour. Manual refresh is
supported via the /provider UI.

Static model list is preserved as fallback when discovery fails.

* test(provider): add comprehensive OpenCode Zen/Go test suite

97 tests across 2 files covering:

Integration tests (72 tests):
- Vendor descriptor: id, label, classification, base URL, model, auth,
  transport, preset, validation, catalog, discovery, usage metadata
- Gateway descriptor: id, label, vendorId, category, base URL, model,
  auth, transport, preset, catalog, discovery
- Brand descriptor: id, label, canonicalVendorId, capabilities, modelIds
- Model catalog: registration, vendor/gateway associations, required
  fields, valid classifications, reasoning/coding tags, no duplicates,
  model counts (41 Zen, 12 Go), modelDescriptorId consistency
- Cross-reference: brand↔model, vendor↔model, gateway↔model,
  shared OPENCODE_API_KEY
- Registry validation: no errors, no preset conflicts
- Edge cases: unique ids, unique apiNames, non-empty labels, valid
  contextWindow/maxOutputTokens, valid defaultModel format, validation
  message content, discovery config

Profile tests (25 tests):
- Type guard: isProviderProfile('opencode'), rejects invalid values
- buildLaunchEnv: persisted env, defaults, process env precedence,
  OPENCODE_API_KEY mapping, whitespace/null/undefined/empty handling,
  very long keys, special characters, concurrent access, boundary
  values, no credential leakage

* fix(provider): add per-model endpoint routing (P1)

Add endpointPath field to OpenAIShimTransportConfig so catalog entries
can specify which API path to use per model. This addresses the
maintainer's [P1] finding that all models were routed to
/chat/completions regardless of their upstream endpoint.

Changes:
- descriptors.ts: add endpointPath?: string to OpenAIShimTransportConfig
- openaiShim.ts: buildRequestUrl checks shimConfig.endpointPath first
- vendors/opencode.ts: add transportOverrides to 31 catalog entries
  (GPT→/responses, Claude/Qwen→/messages, Gemini→/models/<id>)
  + switch to source: 'static' to prevent free models from live API
- gateways/opencode-go.ts: add transportOverrides to 4 entries
  (MiniMax/Qwen→/messages) + switch to source: 'static'
- opencode.test.ts: update tests for static source, remove discovery tests

* refactor(opencode): model OpenCode Zen/Go as gateways (P2)

* docs(provider): document OpenCode setup and move badge metadata to descriptors

- Add OpenCode Zen/Go rows to README supported providers table
- Add OpenCode Zen/Go examples and OPENCODE_API_KEY to advanced-setup.md
- Add PresetBadge type to descriptor/manifest with badge propagation in
  artifact generator
- Move 4 hard-coded preset badges ([FREE], [Sponsor], [Zen], [Go]) from
  ProviderManager.tsx into descriptor preset metadata
- Add badge field to providerUiMetadata so UI components read from manifest
- Update integration overview docs to recommend preset.badge for future
  gateways

* fix(provider): match request body to endpoint format for OpenCode /messages and /responses (P1)

Extend the openaiShim transport so that endpointPath overrides select
both the URL and the correct body/response format:

- /responses → OpenAI Responses API body (input, max_output_tokens)
- /messages  → Anthropic Messages API body (content blocks, system, max_tokens)

Also fixes: abort listener leak in SSE passthrough, system prompt
content-block flattening, and removes [Zen]/[Go] badge entries (P3).

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(provider): add Google AI SDK body/response format for OpenCode Zen Gemini models (P1)

The three Gemini models in the OpenCode Zen catalog (gemini-3.5-flash,
gemini-3.1-pro, gemini-3-flash) were sending chat-completions body to
the /models/gemini-* endpoint, which expects Google AI SDK format.

- effectiveTransport now detects /models/gemini- endpointPath → 'gemini'
- buildGeminiBody() converts Anthropic messages → Google contents[]
  with role mapping, systemInstruction, generationConfig, functionDeclarations
- geminiSseToAnthropic() parses Google SSE frames → Anthropic stream events
  with text deltas, functionCall tool_use, finishReason mapping
- _convertGeminiToAnthropicResponse() for non-streaming responses
- Streaming/non-streaming routing via URL detection (/models/gemini-)
- serializeBody(), hasToolsPayload, omitGeminiTools all updated

* fix: prevent OpenCode model descriptors from shadowing canonical limits

P1: Prefix all defaultModel values in opencode.ts with 'opencode-'
so the fallback findModelDescriptorForApiName() doesn't match
canonical model names. The OpenCode descriptors are still found
via catalog entry lookup when the OpenCode route is active.

P2: Add 'OpenCode Go' and 'OpenCode Zen' to PRESET_ORDER in
ProviderManager.test.tsx between 'OpenAI' and 'OpenRouter'
so navigateToPreset() sends the correct number of j keypresses.

* fix: align OpenCode Go descriptor metadata with Zen

- category: 'hosted' → 'aggregating' (both are aggregating gateways)
- add validation block with OPENCODE_API_KEY guidance
- update test assertion from 'hosted' to 'aggregating'

* fix: accept OPENAI_API_KEY as fallback in OpenCode validation

When users set up OpenCode Zen/Go via /provider, the key is saved as
OPENAI_API_KEY (via buildCompatibilityProcessEnv). The validation block
only checked OPENCODE_API_KEY, causing a startup warning even though
the runtime auth header had the key it needed.

Add OPENAI_API_KEY to validation.credentialEnvVars for both gateways,
matching the pattern used by Hicap and Gitlawb Opengateway.

* chore: trigger mergeability recheck

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
2026-05-30 14:45:08 +08:00