Skip to content

feat(desktop): keep MCP config secrets on the main-process side of IPC - #2919

Merged
Astro-Han merged 1 commit into
apache:mainfrom
GabrielDrapor:feat/mcp-secret-boundary
Aug 20, 2026
Merged

feat(desktop): keep MCP config secrets on the main-process side of IPC#2919
Astro-Han merged 1 commit into
apache:mainfrom
GabrielDrapor:feat/mcp-secret-boundary

Conversation

@GabrielDrapor

@GabrielDrapor GabrielDrapor commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Extracted from #2653 per the review recommendation: the secret-safe configuration boundary lands as the small preceding PR, and the narrowed OAuth-engine PR retargets onto it.

This PR also implements the review's first architectural correction: one shared secret-location/value plan owning both halves of the boundary.

What

@maka/core/mcp-secrets — the single secret-location authority. listMcpSecretLocations(config) enumerates every credential-bearing position in an MCP server config (remote header values, the static OAuth clientSecret, stdio env values, sensitive-flag command-line arguments, sensitive URL query parameters). Two consumers derive from this one plan so they cannot drift:

  • the desktop IPC guard (mcp-secret-guard.ts, this PR) masks exactly these locations before a config crosses toward the semi-trusted renderer (SECURITY.md §3);
  • the @maka/mcp runtime scrubber (feat(mcp): OAuth for remote MCP servers #2653) builds its outbound scrub inventory from the same plan.

The plan also fixes the scrubbing semantics both consumers share: values long enough to be unambiguous are substituted in place; a credential too short to splice out of prose forces the whole containing message to be withheld — the boundary allows no third option.

Sentinel masking with basis-bound restores. Configs leave main with secrets replaced by sentinels; configs coming back have sentinels restored from disk, and each restore is bound to the full basis the value was configured for (URL for remote credentials — plus clientId for a clientSecret; command/args/cwd/env-set for stdio, since any of those redirect where the secret flows). A renderer cannot keep a sentinel, repoint it, and have main forward a secret it cannot read. All env values mask at the boundary — key-name heuristics miss PGPASSWORD and DATABASE_URL.

Config schema: the remote oauth block (clientId, clientSecret, scopes, callbackPort) with storage normalization/bounds, plus one invariant the review asked for enforced at the store: a config may declare a static Authorization header or oauth, never both — one authority per header, rejected at write time. The engine PR enforces the same exclusivity on the wire.

Tests

Guard tests cover masking per location kind and restore-basis binding (repointed sentinels refuse to restore); IPC tests prove a clientSecret round-trips renderer-side as a sentinel while the store and manager see the real value; storage tests cover oauth-block normalization bounds and the Authorization/oauth conflict.

Stacked on #2918 (linear branch chain from the fork): the diff shows its small commit too until it merges — review the feat(desktop): keep MCP config secrets… commit.

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

https://claude.ai/code/session_01TMwYxgNEbz2RFmuK6AXGcj

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex automated review

The shared secret-location plan and basis-bound sentinel restore are the right ownership direction, and the focused checks for this head are green. I found two concrete boundary issues that should be addressed before this slice is treated as complete; details are inline.

This PR is already presented as the secret-boundary slice stacked after #2918, which is a useful separation. Please keep the merge order explicit: the command-line editor can land first, this config boundary next, and the runtime scrubber/OAuth engine after it. The status/error channels still depend on that later runtime scrubber, so this PR should not by itself be described as closing every main-to-renderer credential path.

Disclosure: This is an automated review performed by Codex using delegated adversarial review passes and a final evidence check. It has not been independently verified by Astro-Han or another human reviewer, does not constitute human approval, and does not represent the final judgment of a human reviewer.

Comment thread apps/desktop/src/main/mcp-ipc-main.ts
Comment thread apps/desktop/src/renderer/mcp-page.tsx Outdated
@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from 3dae326 to ee51986 Compare August 13, 2026 07:23
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

Both findings fixed in the updated head:

P1 — remove/cancel paths now redact. mcp:remove and mcp:cancelInstall return through redactMcpConfigSecrets like every other full-config crossing. The redaction IPC test now covers both: removing/cancelling an unrelated server returns the survivors with their clientSecret still masked.

P2 — the OAuth block survives an edit. The draft round-trip moved out of the page component into a pure module, mcp-editor-draft.ts: draftFromConfig carries oauth as opaque draft state (the editor has no OAuth fields to show) and configFromDraft writes it back, so Edit → Save of an OAuth-configured server preserves clientId/scopes/callbackPort and the masked clientSecret sentinel — which main then restores from disk. New mcp-editor-draft.test.ts covers the Edit → Save regression (OAuth preserved through an untouched edit, no invented block for servers without one, stdio round-trip intact).

Desktop suite is green at the new head (786 tests). The stacked PRs are rebased on top.

@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch 2 times, most recently from a2e66a9 to 7596046 Compare August 14, 2026 01:35

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The ownership direction is good: mcp-secrets.ts is the shared location authority, main remains the only persistence owner, basis-bound restoration prevents masked drafts from becoming a confused-deputy channel, and the previously reported remove/cancel and OAuth round-trip gaps are fixed.

One free-form argument shape still crosses IPC in cleartext. From first principles, pattern recognition must apply to the value regardless of whether it happens to follow an arbitrary --flag= prefix; flag sensitivity is an additional signal, not an exclusion. The smallest fix is to check flag.value with isPatternSecret for every parsed flag and emit the same arg-flag-value location. The branch must also be rebased to drop the already-merged #2918 parent commit; the security-only commit applies cleanly to current main, while the stacked form conflicts in mcp-page.tsx.

Reviewed with Codex using two independent reviewer agents; I reproduced the arbitrary-flag leak and verified the latest head, shared secret plan, prior fixes, stacked-commit conflict, and CI state.

中文

职责方向正确:mcp-secrets.ts 是共享 location authority,main 仍是唯一持久化 owner,basis-bound restoration 防止 masked draft 变成 confused-deputy channel;此前的 remove/cancel 和 OAuth round-trip 问题也已修复。

但仍有一种 free-form argument 会明文穿过 IPC。按第一性原理,无论 token 前面是否恰好有任意 --flag= 前缀,都应对 value 做模式识别;flag 是否敏感只能作为额外信号,不能成为排除条件。最小修复是对所有 parsed flag 的 flag.value 调用 isPatternSecret,命中后生成相同的 arg-flag-value location。分支还必须 rebase,去掉已合并的 #2918 父提交;security-only commit 可干净应用到当前 main,而当前 stacked 形式会在 mcp-page.tsx 冲突。

本次由 Codex 配合两个独立 reviewer agent 审查;我复现了 arbitrary-flag 泄漏,并核验了最新 head、共享 secret plan、此前修复、stacked commit 冲突和 CI 状态。

Comment thread packages/core/src/mcp-secrets.ts
@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from 7596046 to 18a86db Compare August 18, 2026 19:36
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GabrielDrapor, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Limit details: You’ve used all 3 included reviews currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b04bec02-0695-4595-b269-93f31fd21ec2

📥 Commits

Reviewing files that changed from the base of the PR and between 88be145 and ed960ad.

⛔ Files ignored due to path filters (2)
  • .maka-shots/after-dialog.png is excluded by !**/*.png
  • .maka-shots/before-dialog.png is excluded by !**/*.png
📒 Files selected for processing (14)
  • apps/desktop/src/main/__tests__/mcp-editor-draft.test.ts
  • apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts
  • apps/desktop/src/main/mcp-ipc-main.ts
  • apps/desktop/src/main/mcp-secret-guard.ts
  • apps/desktop/src/renderer/mcp-editor-draft.ts
  • apps/desktop/src/renderer/mcp-page.tsx
  • packages/core/package.json
  • packages/core/src/__tests__/mcp-secrets.test.ts
  • packages/core/src/mcp-secrets.ts
  • packages/core/src/mcp.ts
  • packages/core/src/redaction.ts
  • packages/storage/src/__tests__/mcp-config-store.test.ts
  • packages/storage/src/mcp-config-store.ts
📝 Walkthrough

Problem solved

The PR prevents MCP secrets from crossing desktop IPC to the renderer in cleartext.

It masks remote headers, OAuth client secrets, stdio environment values, sensitive command arguments, and sensitive URL query parameters. The main process restores masked values only when the incoming configuration matches the stored configuration basis. Restore failures now raise McpSecretRestoreError.

The PR also adds remote OAuth configuration with validation. It rejects configurations that contain both OAuth credentials and a static Authorization header.

Source of truth

The PR extends the existing MCP configuration and storage paths.

packages/core/src/mcp-secrets.ts provides one shared authority for secret locations and scrubbing. The desktop main process applies that authority at the renderer boundary. Storage remains responsible for serialized transformations and OAuth normalization. The editor draft module preserves OAuth data during edits without creating a second persistence path.

Scope and complexity

The design is the smallest coherent solution shown by the diff:

  • Core centralizes secret discovery and scrubbing.
  • The desktop main process owns masking and restoration.
  • Storage owns OAuth validation and normalization.
  • The draft module isolates editor conversion logic.
  • McpConfigStore.transform serializes read-transform-write operations.

The restoration basis checks are necessary to prevent secrets from being restored into changed endpoints, commands, arguments, environment keys, or OAuth clients. The editor extraction removes duplicated parsing and conversion logic from mcp-page.tsx.

No further deletion is apparent without weakening behavior or regression coverage.

Validation

Tests cover:

  • Secret masking and restoration across IPC operations.
  • Restoration binding to the original configuration basis.
  • Rejection of stale, altered, displaced, or unavailable sentinels.
  • Remote headers, stdio environment values, command arguments, URL query parameters, and OAuth client secrets.
  • Redaction from removal and install-cancellation responses.
  • OAuth preservation during edits and saves.
  • Stdio and remote draft round trips.
  • OAuth normalization, callback-port bounds, and client-ID validation.
  • Rejection of conflicting Authorization and OAuth configuration.
  • Serialized store transformations during concurrent updates, cancellation, and publication failure.
  • Secret scrubbing order and handling of short credentials.

The author reports that the desktop suite is green and that other jobs pass. The reported packages/eval timing-sensitive failure also occurs on main. Required-check status is unverified from direct evidence.

Complexity delta

The PR adds:

  • One shared MCP secret-location authority.
  • Secret inventories and recursive scrubbing.
  • Main-process masking and restoration branches.
  • Optional OAuth configuration fields.
  • Serialized store transformation state.
  • Editor draft state that carries OAuth data.
  • Public core exports.
  • Focused regression tests.

The PR removes duplicated draft conversion and parsing logic from mcp-page.tsx.

Total maintenance complexity increases in the short term. The increase is justified by the security boundary, OAuth validation requirements, and centralized secret discovery. The diff provides no evidence that the added complexity exceeds those requirements.

Review-relevant risks

  • The PR changes security behavior at the desktop IPC boundary. Secret exposure and restoration require independent human review under repository policy.
  • The PR changes the public MCP configuration contract through McpOAuthConfig, McpConfigStore.transform, and the @maka/core/mcp-secrets export. Public contract changes require independent human review under repository policy.
  • The PR changes user-visible MCP editing and persistence behavior by preserving OAuth data and rejecting conflicting configurations. These behavior changes require independent human review under repository policy.
  • The PR adds validation rules that can reject previously accepted configurations. Compatibility impact requires independent human review under repository policy.
  • The PR requires UI review of sanitized before-and-after dialog screenshots, as requested by the reviewer.

Required-check status remains unverified from direct evidence. The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The change adds MCP OAuth configuration support, shared secret discovery and scrubbing, identity-bound secret restoration in desktop IPC, and centralized renderer draft conversion. Tests cover normalization, redaction, restoration, and IPC response handling.

Changes

MCP secret protection

Layer / File(s) Summary
Core secret and OAuth contracts
packages/core/src/mcp.ts, packages/core/src/mcp-secrets.ts, packages/core/src/redaction.ts, packages/core/package.json, packages/core/src/__tests__/mcp-secrets.test.ts
The core package adds OAuth fields, secret inventory and scrubbing helpers, recursive payload scrubbing, tests, and the ./mcp-secrets export.
Storage OAuth normalization
packages/storage/src/mcp-config-store.ts, packages/storage/src/__tests__/mcp-config-store.test.ts
The store adds serialized transform operations. Remote configuration normalization validates OAuth fields and rejects invalid callback ports, missing client IDs, and conflicts with Authorization headers.
Renderer editor draft conversion
apps/desktop/src/renderer/mcp-editor-draft.ts, apps/desktop/src/renderer/mcp-page.tsx
MCP draft creation, conversion, command parsing, and map parsing move into a shared React-independent module. OAuth data remains in remote drafts.
Desktop secret redaction and restoration
apps/desktop/src/main/mcp-secret-guard.ts, apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts
The secret guard masks transport-specific secrets and restores them only when the launch configuration, endpoint, and OAuth client identity match.
Desktop IPC secret flow
apps/desktop/src/main/mcp-ipc-main.ts, apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
IPC handlers restore matching stored secrets within serialized store transformations and redact returned configurations across set, upsert, install, cancellation, and removal paths.

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

Merge Risk: 🟡 Moderate · up to 6d0c7

The PR adds a secret-masking and restoration boundary, but valid configurations can currently fail to restore when URL query encoding differs, and an argument edge case can alter how a command is launched. These bounded correctness issues make the change not merge-ready until fixed; the missing restore guidance is a lower-impact follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer
  participant MCP IPC
  participant Config Store
  participant MCP Secret Guard
  participant MCP Manager
  Renderer->>MCP IPC: Submit configuration with secret markers
  MCP IPC->>Config Store: Run serialized transform
  Config Store->>MCP Secret Guard: Restore matching stored secrets
  MCP IPC->>MCP Manager: Synchronize restored configuration
  MCP IPC-->>Renderer: Return redacted configuration
Loading

Possibly related PRs

Suggested reviewers: jackwener

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the architecture and tests, but it omits the required Verification, AI use selection, and Checklist sections. Add the required template sections, report verification results, select one AI use option with tool scope, and complete the test and quality checkboxes.
Ai Use Disclosure ⚠️ Warning The PR description selects neither required declaration; its introduced commit names Claude but has no valid Generated-by: trailer. Update the PR description and add Generated-by: Claude if applicable; ensure it survives squash or amend. See CONTRIBUTING.md, “Human ownership and AI attribution”.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: keeping MCP configuration secrets in the desktop main process and out of renderer IPC.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

Both asks are in the updated head:

P1 — pattern recognition applies to every flag value. listMcpSecretLocations now emits an arg-flag-value location when flag.value is pattern-recognized, regardless of the flag's name — --custom=sk-ant-… masks exactly like --api-key=…; the sensitive flag NAME remains an additional signal, never an exclusion. Covered at both layers: the guard test's arg matrix gained the arbitrary-named flag (mask, restore, and basis-binding: a repointed command drops it), and the IPC round-trip test proves mcp:getConfig returns it masked while an edit round-trips the real value back to the store from disk.

Rebase. The stack is rebased onto current main; the merged #2918 parent is gone, so this branch is now the security-only commit applying directly to main (the mcp-page.tsx conflict is resolved — the draft round-trip rides the merged command-line field). Upstream's mcp:reconnect removal is absorbed.

Desktop suite green at the head (952 incl. the stacked slices; this slice's own jobs pass standalone).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b37e028-1b0d-44c8-a9a9-34b2da47bb52

📥 Commits

Reviewing files that changed from the base of the PR and between 781fa8d and 18a86db.

⛔ Files ignored due to path filters (2)
  • .maka-shots/after-dialog.png is excluded by !**/*.png
  • .maka-shots/before-dialog.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • apps/desktop/src/main/__tests__/mcp-editor-draft.test.ts
  • apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts
  • apps/desktop/src/main/mcp-ipc-main.ts
  • apps/desktop/src/main/mcp-secret-guard.ts
  • apps/desktop/src/renderer/mcp-editor-draft.ts
  • apps/desktop/src/renderer/mcp-page.tsx
  • packages/core/package.json
  • packages/core/src/mcp-secrets.ts
  • packages/core/src/mcp.ts
  • packages/core/src/redaction.ts
  • packages/storage/src/__tests__/mcp-config-store.test.ts
  • packages/storage/src/mcp-config-store.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 1 remains after this review.

Comment thread apps/desktop/src/main/mcp-secret-guard.ts
Comment thread apps/desktop/src/main/mcp-secret-guard.ts
Comment thread apps/desktop/src/main/mcp-secret-guard.ts Outdated
Comment thread packages/storage/src/mcp-config-store.ts
@Astro-Han

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale snapshot restores old secrets 🐞 Bug ≡ Correctness
Description
The IPC handlers read previous separately from the queued store mutation, so a concurrent update
can commit a new secret between those operations and then have an older secret restored over it. For
example, two same-server upserts can both read secret S1, one write S2, and the later
sentinel-bearing upsert restore and persist S1.
Code

apps/desktop/src/main/mcp-ipc-main.ts[R43-44]

+    const previous = await deps.store.get();
+    const next = await deps.store.upsert(serverId, restoreMcpServerSecret(serverId, config, previous));
Relevance

●●● Strong

Recent history accepts concurrency and stale-state correctness fixes; this is a concrete
serialized-mutation race in the new secret boundary.

PR-#2174

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each handler completes get() before starting set() or upsert(). The store serializes each
method call independently, so another mutation can run between them, while restoration derives the
persisted credential from the stale previous argument.

apps/desktop/src/main/mcp-ipc-main.ts[35-44]
apps/desktop/src/main/mcp-ipc-main.ts[59-63]
packages/storage/src/mcp-config-store.ts[47-75]
packages/storage/src/mcp-config-store.ts[121-133]
apps/desktop/src/main/mcp-secret-guard.ts[196-225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Secret restoration reads a snapshot outside the serialized store mutation, allowing concurrent writes to restore stale credentials.

## Issue Context
Use the closest existing serialization seam so reading the current config, restoring sentinels, and writing the result execute as one operation. A local IPC mutation queue is the smallest correction if the store cannot expose a compound update; adding a store transaction API introduces a new public surface and callback contract, so do that only if local serialization cannot cover every writer.

## Fix Focus Areas
- apps/desktop/src/main/mcp-ipc-main.ts[35-64]
- packages/storage/src/mcp-config-store.ts[47-75]
- packages/storage/src/mcp-config-store.ts[121-133]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Sentinel collides with valid values 🐞 Bug ≡ Correctness
Description
The fixed sentinel is a valid config string but is unconditionally interpreted as a restoration
marker, so entering that literal value can restore unrelated prior argument text or be silently
dropped when no prior value exists. This makes valid configs impossible to represent reliably and
lets sentinel placement act as a wildcard in the stdio basis comparison.
Code

apps/desktop/src/main/mcp-secret-guard.ts[R158-162]

+function argMatchesPrior(arg: string, priorArg: string): boolean {
+  if (arg === MCP_SECRET_SENTINEL) return true;
+  const flag = parseCommandFlag(arg);
+  if (flag?.value === MCP_SECRET_SENTINEL) {
+    return priorArg.startsWith(`${flag.raw}=`);
Relevance

●●● Strong

The sentinel makes valid user data unrepresentable and breaks restoration semantics; recent
boundary-contract correctness fixes are accepted.

PR-#3028

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Every argument equal to the constant sentinel matches any prior argument and is replaced by that
prior value. Environment variables and headers use the same unconditional marker semantics, while
storage permits this non-empty, NUL-free string in string arrays and maps.

apps/desktop/src/main/mcp-secret-guard.ts[41-41]
apps/desktop/src/main/mcp-secret-guard.ts[137-152]
apps/desktop/src/main/mcp-secret-guard.ts[158-173]
apps/desktop/src/main/mcp-secret-guard.ts[204-225]
packages/storage/src/mcp-config-store.ts[230-250]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A public fixed string serves as both valid user data and the secret placeholder, causing silent restoration or deletion.

## Issue Context
First restrict restoration to locations the prior redaction plan actually marked; this consolidates authority and removes the argument wildcard for ordinary positions. That alone cannot represent a genuinely new credential equal to the sentinel, so either reject the reserved value explicitly before restoration or use an authenticated/opaque placeholder; the latter introduces per-process state or signing-key lifecycle and should be chosen only if reserving one documented value is unacceptable.

## Fix Focus Areas
- apps/desktop/src/main/mcp-secret-guard.ts[41-41]
- apps/desktop/src/main/mcp-secret-guard.ts[124-153]
- apps/desktop/src/main/mcp-secret-guard.ts[158-173]
- apps/desktop/src/main/mcp-secret-guard.ts[204-225]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Repeated query values are lost 🐞 Bug ≡ Correctness
Description
Redacting a URL with repeated credential query keys reduces locations to unique keys and calls
URLSearchParams.set, collapsing all duplicate values into one sentinel. After a normal renderer
round-trip or unrelated edit is saved, restoration can recover only the first prior value, silently
removing valid values and changing the endpoint configuration.
Code

apps/desktop/src/main/mcp-secret-guard.ts[R238-241]

+  for (const key of queryKeys) {
+    if (key !== undefined && parsed.searchParams.has(key)) {
+      parsed.searchParams.set(key, MCP_SECRET_SENTINEL);
+    }
Relevance

●●● Strong

Duplicate query values are valid persisted configuration, and collapsing them is a deterministic
data-loss bug in this secret-boundary feature.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The shared planner emits one location for every searchParams.entries() occurrence, including
duplicate names, and storage accepts and preserves HTTP(S) URLs through URL.toString, including
duplicate query entries. However, the guard converts planned names to a key set and masks them with
set, destroying occurrence identity; restoration then observes and writes only the single
surviving sentinel/value per key.

packages/core/src/mcp-secrets.ts[150-157]
apps/desktop/src/main/mcp-secret-guard.ts[231-269]
packages/core/src/mcp-secrets.ts[150-155]
packages/storage/src/mcp-config-store.ts[149-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Masking a sensitive URL query parameter by key with `URLSearchParams.set()` collapses repeated occurrences. A configuration such as `?api_key=one&api_key=two` is returned with only one sentinel, so a subsequent renderer save cannot restore the second value and silently changes the endpoint configuration.

## Issue Context
The shared secret-location plan enumerates query entries individually, but the IPC guard reduces them to a key set and mutates the URL through `URLSearchParams.set()`. Preserve every query occurrence and its ordering while replacing and restoring only the corresponding value; because the current plan identifies query locations only by key/value, extend the shared authority with an occurrence index if occurrence identity cannot be derived locally, accepting the associated planner and guard test maintenance rather than introducing a second independent sensitivity rule.

## Fix Focus Areas
- apps/desktop/src/main/mcp-secret-guard.ts[231-269]
- packages/core/src/mcp-secrets.ts[150-157]
- apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts[159-189]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (2)
4. Flags become secret values 🐞 Bug ≡ Correctness
Description
Disposition: fix-now. A sensitive flag without an argument followed by another flag (for example,
--token --verbose) classifies --verbose as credential material; it is hidden from the editor and
is dropped when any launch-basis change prevents restoration, altering the command that is launched.
Code

packages/core/src/mcp-secrets.ts[R107-112]

+        previousFlag &&
+        previousFlag.value === undefined &&
+        isSensitiveFlagName(previousFlag.name)
+      ) {
+        locations.push({ kind: 'arg', index, value: arg, credential: true });
+        continue;
Relevance

●●● Strong

This deterministic parser misclassifies a following flag as secret material, causing command
alteration; comparable correctness fixes are accepted.

PR-#2061

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The planner does not inspect the current token before adding it as the prior sensitive flag's value.
The guard replaces every planned argument location with the sentinel, and its restore path filters a
sentinel whose basis no longer matches.

packages/core/src/mcp-secrets.ts[101-112]
apps/desktop/src/main/mcp-secret-guard.ts[99-105]
apps/desktop/src/main/mcp-secret-guard.ts[149-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The argument scanner treats the token immediately following a sensitive flag as its secret value even when that token is itself another command-line flag. The IPC guard then masks it as a sentinel and removes it if restore-basis validation fails.

## Issue Context
A valid argument list can contain a flag with no value followed by another flag. Only non-flag arguments should be assigned to a preceding sensitive flag absent command-specific syntax proving otherwise.

## Fix Focus Areas
- packages/core/src/mcp-secrets.ts[101-135]
- apps/desktop/src/main/mcp-secret-guard.ts[149-173]
- apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts[112-157]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Short secrets bypass withholding 🐞 Bug ⛨ Security
Description
Disposition: fix-now. scrubKnownMcpSecrets substitutes long values before checking short
credential fragments, so a credential such as Bearer ab is returned as an in-place redaction
rather than the required withheld-message result because replacement removes the ab substring
before the withholding check.
Code

packages/core/src/mcp-secrets.ts[R182-187]

+export function scrubKnownMcpSecrets(message: string, secrets: McpSecretInventory): string {
+  let result = message;
+  for (const secret of secrets.substitute) result = result.split(secret).join('[redacted]');
+  if (secrets.withhold.some((secret) => result.includes(secret))) {
+    return MCP_SECRET_WITHHELD_MESSAGE;
+  }
Relevance

●●● Strong

The scrubber violates the explicitly documented withholding invariant for short credential
fragments; this is a direct security correctness fix.

PR-#1755

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Inventory construction includes both a full credential value and its whitespace parts, classifying
short credential parts for withholding. The scrubber removes full values first and only then
searches the modified message for short parts, contradicting the declared ordering-independent
withholding rule.

packages/core/src/mcp-secrets.ts[43-53]
packages/core/src/mcp-secrets.ts[165-177]
packages/core/src/mcp-secrets.ts[182-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The scrubber's documented invariant says any message containing an un-spliceable short credential must be withheld. Checking `withhold` only after long-value replacements lets a long credential containing that short value avoid withholding.

## Issue Context
`mcpSecretInventory` intentionally adds the complete value and whitespace-separated parts. For a credential-position header value `Bearer ab`, the complete string is substitutable while `ab` is in `withhold`.

## Fix Focus Areas
- packages/core/src/mcp-secrets.ts[161-188]
- packages/core/src/mcp-secrets.ts[43-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a security-sensitive IPC boundary with substantial new shared logic across core, storage, main-process, and renderer paths, creating multiple independent, easy-to-miss correctness and secret-leak risks beyond a single-pass review.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/desktop/src/main/mcp-ipc-main.ts Outdated
Comment thread apps/desktop/src/main/mcp-secret-guard.ts Outdated
Comment thread apps/desktop/src/main/mcp-secret-guard.ts Outdated
Comment thread packages/core/src/mcp-secrets.ts
Comment thread packages/core/src/mcp-secrets.ts
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

CI triage: the only red here is test_workspaces failing on packages/evala request arriving while the proxy drains is refused, not counted (the test job is just its aggregation gate). The identical test fails on main's own CI (run 32146332714, the 0.1.11 release-prep commit) — a timing-sensitive flake unrelated to this slice, which touches no eval code. A rerun should clear it; I don't have rerun permission here. Every other job including e2e, windows lanes and the new astryx/CodeRabbit gates is green.

@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from 18a86db to d53f29a Compare August 19, 2026 00:53
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

CodeRabbit's four findings are addressed in the updated head:

  • Silent env destruction on a basis change (Major) — restore semantics changed from drop to REJECT: any sentinel that cannot be restored (changed basis, or nothing on disk behind it) now throws a typed McpSecretRestoreError naming the position (never the value), so the renderer reports the condition instead of mcp.json losing PGPASSWORD unnoticed. The file-header invariant now states exactly that.
  • Orphaned flag after a dropped sentinel arg (Minor) — subsumed by the same change: there is no drop path left; --token <sentinel> either restores whole or rejects whole, so --verbose can never become the token value.
  • sameEndpoint comparing normalized vs raw URL (Major) — both sides now compare through WHATWG normalization (normalizeUrl), and a regression covers the reported case: a stored un-normalized https://api.example.com?api_key=… with a header and clientSecret round-trips intact.
  • clientSecret without clientId (Major) — the store now rejects it (clientId is required when clientSecret is configured) with a regression test.

Also landed in this slice's shared plan (flagged on the sibling PRs): substitution now replaces longer secrets before their prefixes (dedup + length-desc sort — abcdEFGH can no longer surface as [redacted]EFGH), and short whitespace PARTS of a longer credential no longer withhold ("Bearer x" no longer collapses every message containing "x"; a whole short credential still does). Both have regressions in the new mcp-secrets.test.ts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts (1)

223-232: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the rejection message excludes the secret value.

mcp-secret-guard.ts lines 28-29 state the invariant: "The rejection message names the position, never the value." No test protects it. A future edit to McpSecretRestoreError could interpolate the stored value into the message, and every current test would still pass. assert.throws with only the error class does not inspect the message.

Add one assertion to an existing rejection test. This reuses existing coverage instead of adding a new test case.

Disposition: optional.

Proposed assertion
     notionIn.url = 'https://attacker.example/mcp';
-    assert.throws(() => restoreMcpConfigSecrets(incoming, previous), McpSecretRestoreError);
+    assert.throws(() => restoreMcpConfigSecrets(incoming, previous), (error: unknown) => {
+      assert.ok(error instanceof McpSecretRestoreError);
+      // The message names the position, never the value.
+      assert.doesNotMatch(error.message, /real-secret/u);
+      return true;
+    });

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ea1a2d4-2206-4dd0-95dc-7f8a1c41a41a

📥 Commits

Reviewing files that changed from the base of the PR and between 18a86db and d53f29a.

📒 Files selected for processing (6)
  • apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts
  • apps/desktop/src/main/mcp-secret-guard.ts
  • packages/core/src/__tests__/mcp-secrets.test.ts
  • packages/core/src/mcp-secrets.ts
  • packages/storage/src/__tests__/mcp-config-store.test.ts
  • packages/storage/src/mcp-config-store.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 1 remains after this review.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moving secret redaction/restoration to the main-process IPC boundary is the right authority decision, and the current head covers the normal get/set/upsert/install/remove/cancel round trips without exposing raw values.

This is not approval-ready because the existing Qodo threads are reproducible and remain open: stale get → restore → write snapshots can roll a rotated secret back; the fixed sentinel collides with valid config values; repeated sensitive query keys cannot round-trip; and sensitive-flag parsing can consume the next flag as a value. I am not duplicating those inline findings. From first principles, the smallest coherent correction is one store-level atomic transform for restore+mutation, plus an occurrence-bound, non-confusable redaction representation. That removes the race and the marker ambiguity without adding a second secret authority.

All live checks are green, but merge state remains blocked by unresolved review findings.

Reviewed with Codex using three independent reviewer agents and OpenCode Go DeepSeek V4 Flash (high); I verified the exact head, current discussion, IPC/store serialization, redaction locations, round-trip failures, and live CI.

中文

把 secret redaction/restore 放到 main-process IPC 边界是正确的权威设计,当前 head 也覆盖了正常的 get/set/upsert/install/remove/cancel round-trip,不会把原始值返回 renderer。

但现在还不能批准,因为已有 Qodo 线程都能复现且仍未解决:过期的 get → restore → write snapshot 会把已轮换 secret 回滚;固定 sentinel 会与合法配置值冲突;重复敏感 query key 无法 round-trip;敏感 flag 解析还会把下一个 flag 误当作 value。我不重复发布这些行内问题。按第一性原理,最小且一致的修复是提供一个 store-level atomic transform,把 restore+mutation 放在同一串行边界内,并使用 occurrence-bound、不可混淆的 redaction representation。这样能同时消除竞态和 marker 歧义,也不新增第二套 secret 权威。

实时检查全绿,但 unresolved findings 仍使 merge state blocked。

本次由 Codex 配合三个独立 reviewer agent,以及 OpenCode Go DeepSeek V4 Flash(high)审查;我核验了精确 head、现有讨论、IPC/store serialization、redaction locations、round-trip failures 和实时 CI。

@Astro-Han

Copy link
Copy Markdown
Contributor

This PR includes before and after dialog image files in the branch, but they are not currently displayed or linked in the PR description. Since the change affects how MCP configuration values are presented and round-tripped in the editor, could you please embed those screenshots in the PR description or add them in a comment so the UI evidence is directly reviewable? Please ensure all secret values are sanitized. Thanks!

Posted by Codex on behalf of Astro-Han.

@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from d53f29a to 6d0c7af Compare August 19, 2026 10:19
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

All four Qodo threads plus the synthesis are addressed in the updated head, along the prescribed shape — one store-level atomic transform, plus an occurrence-bound, non-confusable marker:

Stale snapshot restores (High) — the store gained transform(apply): one serialized read-transform-write inside its queue. mcp:setConfig/mcp:upsert/mcp:install (and mcp:add in the stacked PR) now run restore INSIDE that transform, so the restore basis IS the config the write commits over — the two-upserts-roll-back-a-rotated-secret interleaving is structurally gone. Regression: transform sees the latest committed config, not a caller snapshot.

Sentinel collides with valid values (High) — the fixed public sentinel is replaced by a marker carrying a per-process random nonce (a legitimate value cannot collide with it, stale markers from a previous run are rejected) AND a position tag: env.<key>, header.<key>, arg.<index>, query.<key>.<occurrence>, oauth. Restore compares each position against the exact marker redaction would have stamped there; any marker-shaped value anywhere else rejects the write — a marker can no longer act as a wildcard. Regression: rejects a marker moved to a different position.

Repeated query values lost (High) — query masking/restoring is per occurrence, in order, preserving duplicates; the URL identity check is positional. Regression: ?token=a&region=eu&token=b round-trips both values.

Flags become secret values (High) — the scanner no longer classifies a flag following a sensitive flag as its value: --token --verbose keeps --verbose visible and launchable.

Short secrets bypass withholding (High, filed on the shared plan)scrubKnownMcpSecrets checks withholding on the RAW message before any substitution, so a short credential embedded in a longer secret can no longer be consumed past the check. Regression added.

The reject-not-drop restore semantics from the previous round carry over unchanged (a moved/stale/unrestorable marker throws McpSecretRestoreError naming the position, never the value).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts (1)

263-280: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the restored env value, and add the rejection path.

Two gaps in an otherwise strong round trip.

First, seenScratch carries a masked API_TOKEN, but no assertion checks that it returns as scratch-token. Env values use the absolute masking rule, so that position deserves the same assertion the arg gets at line 270.

Second, no IPC test covers a rejected restore. The guard tests prove McpSecretRestoreError is thrown; this file should prove the handler rejects and the store stays unwritten.

🧪 Proposed additions
   assert.deepEqual(storedScratch.args, ['server', '--custom=sk-ant-api03-abcdef123456']);
+  assert.deepEqual(storedScratch.env, { API_TOKEN: 'scratch-token' });
+
+  // A changed launch basis with a kept sentinel must reject, and must not
+  // commit a partially-restored config.
+  await assert.rejects(
+    upsertScratch({}, 'scratch', { ...seenScratch, command: 'curl' }),
+    /cannot be restored/,
+  );
+  const unchanged = config.mcpServers.scratch;
+  assert.ok(unchanged && 'command' in unchanged);
+  assert.equal(unchanged.command, 'npx');

The rejection case needs seenScratch re-fetched through mcp:getConfig if the earlier upsert already replaced the stored values; adjust ordering as needed.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1de3e0e2-1a6c-4419-ab89-943e328a83e3

📥 Commits

Reviewing files that changed from the base of the PR and between d53f29a and 6d0c7af.

📒 Files selected for processing (7)
  • apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/mcp-secret-guard.test.ts
  • apps/desktop/src/main/mcp-ipc-main.ts
  • apps/desktop/src/main/mcp-secret-guard.ts
  • packages/core/src/mcp-secrets.ts
  • packages/storage/src/__tests__/mcp-config-store.test.ts
  • packages/storage/src/mcp-config-store.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.

Comment thread apps/desktop/src/main/mcp-ipc-main.ts
Comment thread apps/desktop/src/main/mcp-secret-guard.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the extensive secret-boundary hardening. On the current head, the nonce markers, occurrence binding, fail-closed restore behavior, and most existing review items are addressed. Two nonduplicate P2 boundary gaps remain inline: inconsistent URL canonicalization can reject an unchanged endpoint and lose restoration, and a short credential fragment can still escape when reflected without its longer header value.

Please fix these owner-level issues here before the dependent OAuth stack is restacked. Several old unresolved threads appear already fixed; those should be resolved rather than patched again. The existing AI disclosure is sufficient: the commits identify Claude co-authorship and link the contributing session. No new visual design is introduced by this slice, so I am not adding a screenshot gate.

Reviewed with Codex as an AI-assisted code review. I verified the exact-head diff, secret inventory/restore paths, existing threads, CI, and provenance; no external model output was used.

中文说明

当前 head 已修好大部分 secret boundary 问题,但仍有两个新的 P2:URL restore 两侧 canonical form 不一致,会把未改变的 endpoint 判成改变;短 credential fragment 单独被上游回显时仍可能泄漏。请先在 #2919 的 owner 层修复,再让后续 OAuth PR restack。已有 AI 说明完整,本层没有新增可见 UI,不额外要求截图。

Comment thread apps/desktop/src/main/mcp-secret-guard.ts
Comment thread packages/core/src/mcp-secrets.ts Outdated
@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from 6d0c7af to ed960ad Compare August 19, 2026 13:09
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

Both P2s are fixed in the updated head (rebased onto current main):

Inconsistent URL canonicalizationnormalizeUrl now re-serializes the query through the SAME serializer restore uses (URLSearchParams append), so a stored URL whose raw encoding differs only cosmetically (%20 vs +) is no longer misclassified as a changed endpoint. Regression: an un-normalized stored URL with %20, a masked header AND a clientSecret round-trips intact.

Short credential fragments reflected alone — the shared inventory gained a third class, substituteBounded: short fragments of a longer credential-position value ("Bearer x" → "x") are substituted at token boundaries (lookaround on word characters), so an upstream reflecting the bare fragment loses it while "example" stays untouched; whole short credentials still withhold. Regressions cover the standalone-fragment reflection and the non-shredding of ordinary words.

Housekeeping from this round's comments: the stray .maka-shots images that had leaked into the branch are evicted from the commit — this slice is runtime/IPC-only per your review (the dialog evidence lives on #2918, where it is already embedded). The old resolved threads are ready to be marked resolved on your side; nothing was re-patched.

The renderer is semi-trusted (SECURITY.md §3), so credential material
in mcp.json never crosses the bridge in cleartext, wherever it sits:
the static OAuth clientSecret, every remote header value, every stdio
env value (key-name heuristics miss real secrets — PGPASSWORD,
DATABASE_URL — and the boundary is absolute), URL query parameters
under a sensitive name or with a pattern-recognized value, and
command-line arguments a sensitive flag introduces.

WHERE those secrets live is enumerated once, in @maka/core/mcp-secrets:
a shared location plan that this IPC guard masks by and that the MCP
runtime's outbound scrubber will consume for error/status/payload
scrubbing — one authority, so the two halves of the boundary cannot
drift. The plan also fixes the scrubbing semantics: a value long
enough to be unambiguous is substituted in place; a credential too
short to splice out of prose forces the whole containing message to be
withheld, because the boundary allows no third option.

Every config leaves main with secrets replaced by sentinels; configs
coming back have sentinels restored from what is on disk, and every
restore is bound to the full basis the value was configured for — the
URL for remote credentials (plus the clientId for a clientSecret);
command, arguments, cwd and the rest of the env for stdio, since any
of those can redirect which executable runs or where it connects. A
renderer cannot keep a sentinel, repoint where it flows, and have main
forward a secret it cannot read.

The config schema gains the remote `oauth` block (clientId,
clientSecret, scopes, callbackPort — consumed by the upcoming OAuth
engine) with storage normalization and bounds, and one invariant
enforced at the store: a config may declare a static Authorization
header or oauth, never both — one authority per header, rejected at
write time rather than arbitrated at request time.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMwYxgNEbz2RFmuK6AXGcj
@GabrielDrapor
GabrielDrapor force-pushed the feat/mcp-secret-boundary branch from ed960ad to d46c4e4 Compare August 19, 2026 14:18
@GabrielDrapor

Copy link
Copy Markdown
Contributor Author

Restacked onto latest main (074f719, config version 2 / dual-era negotiation). Notable adaptation: this PR's mcp-editor-draft.ts module is gone — upstream's mcp-page-model.ts now owns the draft round-trip, so the opaque oauth pass-through field and its regression tests moved into that model (an edit → save of an OAuth-configured server still cannot drop the block). Version literals swept to MCP_CONFIG_VERSION. All suites green (storage 829, desktop 989 at this tier).

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved exact head d46c4e4c2da223864fcde21ab427bfdc0b0d7b87.

I verified that the prior URL-canonicalization and short-fragment redaction P2s are fixed, the focused Desktop/Core/Storage tests pass, the Claude disclosure/provenance is complete, and this slice introduces no new UI/UX behavior. I resolved 11 superseded review threads.

One existing non-blocking P3 remains: the renderer replaces the actionable “re-enter the secret” restore error with generic copy. That should be improved, but it does not block this approval.

I approved the exact-head fork workflows; this is not merge-ready until they finish green.

AI-assisted review disclosure: OpenAI Codex performed the exact-head secret-handling, thread, provenance, and CI analysis; I verified the fixes, local focused results, severity, and live GitHub state before approving.

中文说明

旧的 URL 规范化与短 secret 片段泄漏两个 P2 已修复,11 个过时线程已关闭,AI provenance 合规,也没有新增 UI 变化。仍保留一个非阻塞 P3:恢复 secret 失败时可操作提示被通用文案替换。当前 workflows 已授权,全部绿色后才算 merge-ready。

@Astro-Han
Astro-Han merged commit 5eb590b into apache:main Aug 20, 2026
19 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants