Skip to content

fix(desktop): make proxy credential updates explicit and Host-owned - #3704

Open
Sun-GLiang wants to merge 10 commits into
apache:mainfrom
Sun-GLiang:fix/3696-proxy-password-editing
Open

fix(desktop): make proxy credential updates explicit and Host-owned#3704
Sun-GLiang wants to merge 10 commits into
apache:mainfrom
Sun-GLiang:fix/3696-proxy-password-editing

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes #3696 as a proxy-credential lifecycle migration rather than an input-mask-only patch.

  • remove the saved proxy password from persisted AppSettings and Renderer input values; reads expose only passwordConfigured, while writes use explicit keep/replace/delete semantics
  • make proxy policy and credential changes one Host-owned CAS operation with shared preflight, ordered publication, and an explicit commit_outcome_unknown result for partial durable failure
  • keep replacement text in a Renderer-local draft until blur or Enter; Escape cancels, Eye reveals only the draft, window blur does not commit, and Copy remains available on unrelated password fields
  • adapt schema-v1 import/export at the compatibility boundary so credential-only transfers remain lossless without carrying unrelated settings
  • atomically remove legacy proxy-secret fields from settings.json, keeping the credential vault as the only durable secret authority
  • make “Test current configuration” wait for a pending password commit; unsaved candidate-password testing is intentionally no longer supported

Why this lands together

The defect crosses one read/write representation boundary: a vault-owned secret was projected into ordinary Settings as an editable sentinel and then accepted back as a credential. Fixing only one layer leaves an unsafe intermediate contract.

  • Removing password from Settings requires a read-only configured-state projection and a write-only credential mutation.
  • The Renderer draft requires that explicit Host mutation; otherwise it must fall back to the sentinel or split policy and credential writes again.
  • The Host operation must validate policy revision and credential generation in one lane so an older client cannot recreate a credential after authentication is disabled.
  • Once the Settings shape changes, schema-v1 transfer needs an adapter and existing settings.json files need cleanup so compatibility paths cannot retain or reintroduce the removed secret field.
  • The Runtime Host epoch moves with this boundary because older peers can split these writes and violate the shared credential basis.

These areas remain separately testable, but under this implementation they are not separately releasable without adding a temporary dual representation or compatibility surface that preserves the defect. One revert restores the previous complete lifecycle; partial rollback of a dependent layer is not supported.

Review boundaries

The review can be evaluated as three explicit contracts inside that compatibility cut:

  1. Migration and transfer: legacy Settings cleanup plus schema-v1 credential-bearing field adaptation.
  2. Host authority: policy/vault preflight, two-client CAS, publication ordering, and partial-outcome reporting.
  3. Desktop UX: local draft, commit/cancel/reveal behavior, save failure handling, and proxy-only Copy removal.

The focused suite keeps the invariant-bearing coverage for each boundary: minimal migration and credential-only transfer cases, Host two-client and durable failure cuts, Renderer draft concurrency/failure cases, and one offline authenticated-proxy E2E.

UI behavior

The recording shows sequential draft input, working show/hide, safe reload state, Escape cancellation without closing Settings, replacement after a saved password, and the proxy-only removal of Copy.

Proxy password editing regression flow

Verification

  • review-targeted regression suites: 143/143
  • Core: 657/657
  • Storage: 940 passed, 14 platform-specific skipped
  • Desktop: 1503/1503
  • offline authenticated proxy E2E: 1/1, including replacement after reload and full Proxy-Authorization
  • npm run lint
  • npm run format:check
  • npm run build
  • npm run typecheck
  • npx knip --workspace apps/desktop
  • npx knip --workspace packages/ui

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex implemented the fix, tests, verification, review-driven corrections, and this scope clarification.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@Sun-GLiang
Sun-GLiang force-pushed the fix/3696-proxy-password-editing branch from b94e9ab to c5ec403 Compare August 25, 2026 02:09
@Sun-GLiang
Sun-GLiang marked this pull request as ready for review August 25, 2026 02:32

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed exact head c5ec40335dcaa0d19756bff067bced9cf2cf687c. I found two blocking correctness issues in the new proxy-credential write path: one cross-client race that can recreate a credential after authentication was disabled, and one partial-commit path that applies proxy policy even though the API reports failure. The hosted checks are green and the branch merges cleanly, but these state-consistency issues need to be resolved before approval.

else await setCredential(client, PROXY_CREDENTIAL, proxy.password);
}
else if (proxy.credential?.kind === "replace")
await setCredential(client, PROXY_CREDENTIAL, proxy.credential.secret);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Make proxy policy and credential replacement one Host-owned operation across clients. The new queue is local to one RuntimeHostSettingsModule, so two supported Desktop clients have independent lanes. Client A can start a replacement and pause at the credential CAS; client B can then set authEnabled=false and delete the credential; when A receives credential_stale, its retry rereads the now-empty locator and recreates the secret with expected: null. A production-adapter/CAS probe ended with authEnabled=false while the vault again contained A's replacement secret. The existing "disable wins" test uses one module and therefore cannot cover this inter-client ordering. Please move the policy decision plus keep/replace/delete into one Runtime Host atomic or recoverable operation that validates both the policy revision and credential basis, and add a two-client test asserting the vault remains empty after disable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2948461. Desktop no longer performs a client-local credential CAS/retry. It sends the observed policy revision, credential basis, complete proxy policy, and keep/replace/delete intent through the Host-owned runtime.policy.network-proxy.update operation. The Host rejects a stale client before it can recreate a deleted credential.

Added coordinator and storage tests with two clients observing the same initial basis: client B disables authentication, then client A attempts the stale replacement. The replacement is rejected, the final policy remains authEnabled: false, and the vault remains empty.

): Promise<void> {
if (patch.network?.proxy) {
const proxy = patch.network.proxy;
await client.updateRuntimePolicy((policy) => ({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Do not commit proxy policy before the credential write can still fail. A normal update containing both policy fields and credential: replace/delete first commits set_network_proxy here, then performs vault persistence below. If that second step rejects, the caller receives an error even though the Host is already using the new host/auth/username with the old credential. This is also reachable from the schema-v1 settings-plus-credentials import path. A production-adapter failure probe observed the rejected call with enabled=true, authEnabled=true, username=new-user, and the old secret still stored; the success control stored the replacement. Existing failure coverage sends a credential-only patch, so it never checks policy after the failure. Please make this a single Host-side compound operation, or define an explicit recoverable commit protocol, and test each persistence cut for committed/unchanged/known recovery semantics.

@Sun-GLiang Sun-GLiang Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 7be1502. The Desktop path now sends one Host-owned runtime.policy.network-proxy.update operation with the observed policy revision, credential basis, complete policy, and explicit keep/replace/delete intent. The Host validates both bases in its mutation lane.

Correction to my earlier reply: this implementation does not persist a transaction intent and does not perform automatic recovery. Storage orders replacement before enabling credential use, and stops policy use before deletion. A failure before the first publication leaves policy and credential unchanged; a later failure after one durable effect returns commit_outcome_unknown. Persistence-cut tests now cover both the unchanged and known-unknown cases for replacement and disable/delete.

@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

CI status note: package passed. The test job failed on the unrelated Desktop E2E case slash-command-menu.spec.ts: an open menu keeps its container and skills group across projection refreshes, where CI observed 2 DOM removals instead of 0.

This PR does not change the slash-command/menu implementation or that test relative to current upstream/main. The exact failing case passed locally once and then 10/10 repeated runs; the proxy-password E2E also passed in the CI run. This is therefore consistent with an existing full-suite timing flake rather than a proxy-password regression. Failed job: https://github.com/apache/maka/actions/runs/32820203074/job/97716509006

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

Submitted with the wrong review state — the findings here are P2 and do not block. Re-posted as a review comment below.

@Astro-Han
Astro-Han dismissed their stale review August 26, 2026 08:11

Wrong review state: no P1 findings, so this should not have requested changes.

@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 core fix is right, and it is right at the level that matters. #3696's root cause is that the masking sentinel and the real secret were one representation: password lived on the persisted NetworkProxySettings, the renderer bound it as the editable value, and applyHostPatch accepted anything !== SENSITIVE_PLACEHOLDER as a new credential. This PR deletes the field, replaces it with a per-read derived passwordConfigured, and makes the write side an explicit keep/replace/delete operation that cannot round-trip a read value. That removes the duplicate representation rather than patching the mask, and it does not introduce a new authority — the vault stays sole owner.

Two things to resolve before merge, then some smaller items.

Blocking — the outstanding review rests on a premise the branch does not implement

This is not a code defect, so I am not giving it a severity; it blocks for a different reason.

The reply to @jackwener's P2 states that proxy policy and credential changes "now use one Host-side recoverable operation backed by a persisted transaction intent", with "persistence-cut coverage for the compound operation" and an intent that "is cleared after recovery", citing 2948461d4. That commit is not in this branch's history (force-pushed; head is 6d1daf75f), and none of it is present: git diff <merge-base> HEAD adds zero occurrences of a transaction intent, and the failure path is throw commitOutcomeUnknown('Network proxy update committed only some effects') after ordering the two commits and tracking a durableChange flag. There is no persisted intent, no recovery, and no failure-injection test for the compound commit.

The implemented behavior is defensible — @jackwener asked for committed/unchanged/known recovery semantics, and commit_outcome_unknown is a known label. But the reviewer should not approve on the stated description. Please either correct the reply or add what it describes.

For the record, @jackwener's P1 is genuinely fixed: runtime.policy.network-proxy.update validates expectedPolicyRevision and expectedCredential inside one inLane on the Host, Desktop no longer does a client-local CAS/retry, and both storage-level and coordinator-level two-client tests were added.

Scope — this is two intents

prod  +1021  -160   (26 files)
test  +1507   -41   (15 files)

Cohesively, (a) the data-model fix, the new Host operation with its epoch bump, and the legacy settings.json purge are one revertable intent and are the PR. (b) The config export/import schema-v1 adaptation and the runRuntimeHostSettingsExclusive lane refactor are a second: different subsystem, independently reviewable, and the source of the first P2 below — which has nothing to do with mask corruption. Splitting also drops what a reviewer must hold at once from 41 files to roughly 30 / 11.

P2 — a credentials-only export carries and re-applies the whole settings payload

runtime-host-config-ipc-main.ts:195. The gate widened to selected.has('settings') || selected.has('credentials'), and for a credentials request the payload is restoreHostSettingsSecrets(settings, secrets). Since buildConfigBundle derives the manifest from data presence, 'settings' enters includedData; on the other side applyConfigImport applies it on presence alone with no category filter (config-transfer-service.ts:102).

So exporting credentials only to move API keys to a second machine also ships privacy, chatDefaults, workspaceInstructions, subagents, shell, projects, personalization and the full proxy host/port — and importing overwrites all of the target's settings. The manifest does list settings and the import summary does say "Settings applied", so it is disclosed and recoverable, which is why this is P2 and not higher. It is still not what the user asked for. The schema-v1 motivation is legitimate; narrow it to the credential-bearing fields, or make the export state the dependency rather than smuggle the section.

P2 — window blur commits a partially typed password

password-input.tsx:129. relatedTarget is null on window blur, so destination is falsy and onFocusExit() fires. Cmd-Tab mid-typing replaces the stored credential with a partial password. It is recoverable by retyping, but the user gets no signal — proxy auth simply starts failing later with nothing pointing back here. Worth a guard on document.hasFocus() or relatedTarget === null && document.activeElement === input.

(I checked that onBlurCapture lands on a real DOM node: InputGroup spreads ...rest onto its inner role="group" div, so currentTarget.contains(relatedTarget) works and clicking the Eye button correctly does not commit.)

P2 — a dead secret-carrying override, and an unstated behavior change

toProxyTestInput no longer sets password, and it is the only production producer of TestProxyInput.proxy (the command-palette path passes undefined). So credentialOverride(input.proxy?.password) at runtime-host-settings-ipc-main.ts:215 is unreachable in production, and password on the network-proxy.test operation is a secret-carrying IPC and protocol field with zero producers. This PR already bumps the epoch 50 to 51, which makes it the cheap moment to drop it.

Separately: "Test current configuration" now commits the typed password first (runAfterProxyPasswordCommit), so testing a candidate password without saving it is no longer possible. That is a reasonable design, but it is a behavior change and is not in the Summary.

Ready to delete

  • preserveSensitivePlaceholders (settings-ipc-helpers.ts:73) — this PR edits it without noticing it has zero production callers, here and on main; its only consumer is its own test.
  • FileSettingsStore.testNetworkProxy() (packages/storage/src/settings-store.ts:41,186) — this PR edits its validation, and the edit is only needed because it reads a password field that no longer exists. Zero callers repo-wide: the interface declaration and the implementation are the only two references.
  • The dual-shape registerRuntimeHostSettingsIpc deps union (runtime-host-settings-ipc-main.ts:164) — the legacy branch has no production caller, and the two branches produce different lanes, so a caller that takes the legacy one silently gets an unshared lane.

Security — clean

No new logging of secrets; every new error string is value-free. All fixture secrets are synthetic, and the diff removes several old proxy-secret fixtures. The settings.json purge is temp-file plus rename, and no data is lost because clientOwnedSettingsPatch never forwarded network — the vault has been the authoritative store all along. Adding runtime.policy.network-proxy.update to REMOTE_OWNER_OPERATION_GRANTS does not widen the trust boundary, since credential.vault.set is already there.

AI use: Claude Code assisted with source and issue investigation; the analysis and conclusions are my own.

@M4n5ter
M4n5ter force-pushed the fix/3696-proxy-password-editing branch from 6d1daf7 to 321b868 Compare August 26, 2026 08:36
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

@Astro-Han Addressed the review in 7be1502:

  • corrected the earlier transaction-intent/recovery claim; the implementation uses ordered publication with commit_outcome_unknown, and now has first-cut unchanged plus later-cut known-unknown failure-injection coverage
  • narrowed credentials-only schema-v1 exports to only proxy password and Tavily API key fields
  • prevented window blur from committing a partial password draft
  • removed the dead password override from network-proxy.test; the PR Summary now states that testing commits the pending password draft first
  • removed preserveSensitivePlaceholders, FileSettingsStore.testNetworkProxy(), and the legacy registration-deps branch

I kept the schema-v1 compatibility change in this PR because secret removal changes where those schema-v1 credentials come from, but reduced it to credential-bearing fields so it no longer carries or reapplies unrelated settings.

@Sun-GLiang
Sun-GLiang force-pushed the fix/3696-proxy-password-editing branch from 7be1502 to f7f4f2f Compare August 26, 2026 10:03

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at exact head 06e3dc03f45d2944e57e3b7a5ba533e344597b50 against base 6762085e2ba2372b7a8b296e08ebb67c0294dc2e. The hosted test, package, and windows_recovery checks are green.

The concrete correctness findings from the previous round are resolved here: credentials-only export is narrowed to credential-bearing schema-v1 fields, window blur no longer commits a partial draft, the dead candidate-password override and dead helpers are gone, and the failure-cut coverage now matches the implemented ordered-publication / commit_outcome_unknown contract. I found no additional P0–P3 correctness issue in this head.

Global-design blocker: this is still three independently reviewable changes presented as one small mask fix

The current review unit is 44 files with roughly +1080/-239 non-test lines. The problem is not the line count; it is that the branch carries three contracts with separate failure modes, rollback boundaries, and reviewers:

  1. schema-v1 import/export compatibility plus legacy settings-secret cleanup;
  2. one Host policy/vault CAS authority, including preflight, publication ordering, and unknown partial outcome;
  3. Renderer/Desktop removal of the sentinel plus draft, commit, cancel, reveal, and test UX.

Each is independently coherent and independently shippable. Keeping them together makes a credential representation fix also the review and rollback vehicle for a migration contract, a Host transaction contract, and a UI draft lifecycle. Please split these into three verticals.

If there is a real version-compatibility constraint that requires all three to land atomically, state that constraint explicitly and re-title/re-charter the PR as a credential-lifecycle/security migration. In that case it should be reviewed as the larger migration it is, not as a narrow mask corruption fix.

The test suite should follow the same boundary. Keep the invariant-bearing coverage: one offline authenticated-proxy E2E, Host two-client/CAS and durable failure cuts, draft save concurrency/failure cases, and minimal migration plus credential-only transfer cases. Move surface-local tests with their verticals rather than using one combined matrix to justify the combined PR; constant-only epoch assertions, repeated protocol-shape checks, and duplicate focus mechanics do not need to remain in this review unit.


Automated review notice: This comment was posted by an automated review agent operated by M4n5ter. It is not an independent human review and does not replace one.

@Sun-GLiang Sun-GLiang changed the title fix(desktop): prevent proxy password mask corruption fix(desktop): make proxy credential updates explicit and Host-owned Aug 27, 2026
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

I have updated the PR title and charter to match the change that is actually being reviewed.

The title is now:

fix(desktop): make proxy credential updates explicit and Host-owned

The PR description now presents this as a proxy-credential lifecycle migration rather than an input-mask-only fix. It separates the review into three explicit contracts:

  1. legacy Settings cleanup and schema-v1 credential transfer;
  2. Host-owned policy/vault CAS, publication ordering, and partial-outcome reporting;
  3. Renderer-local draft, commit/cancel/reveal behavior, and proxy-only Copy removal.

I have not split the branch at this point. These contracts are independently testable, but the current implementation delivers them as one compatibility boundary. Splitting them would require a temporary dual-shape compatibility layer or restoring part of the unsafe representation during the intermediate merges:

  • Removing password from ordinary Settings requires both the read-only passwordConfigured projection and a write-only credential mutation.
  • The Renderer draft depends on that explicit Host mutation; otherwise an intermediate version must retain either the sentinel or the previous split-write path.
  • The Host operation must validate the policy revision and credential generation in one lane so disabling authentication wins over an older replacement, as required by bug(desktop): proxy password editing stores the masking sentinel and corrupts credentials #3696.
  • Once the Settings shape changes, schema-v1 transfer needs an adapter and persisted legacy Settings need cleanup so compatibility paths cannot retain or reintroduce the removed secret field.
  • The Runtime Host epoch moves with this boundary because an older peer can split policy and credential writes against different bases.

The revised description does not claim an all-or-nothing storage transaction. It states the implemented contract directly: shared preflight, ordered publication, and commit_outcome_unknown when a durable failure occurs after only part of the compound operation has committed.

The verification section is now organized around the invariant-bearing coverage for each review boundary: minimal migration and credential-only transfer cases, Host two-client/CAS and durable failure cuts, Renderer draft concurrency/failure cases, and one offline authenticated-proxy E2E. I have not moved or pruned tests while the review-unit decision remains open.

Please re-evaluate the review unit under the revised title and charter. If maintainers still require a physical split after considering these compatibility dependencies, the fallback will be ordered stacked PRs rather than treating the three contracts as unrelated changes.

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

Labels

effort/XL Over 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): proxy password editing stores the masking sentinel and corrupts credentials

4 participants