Skip to content

feat(gateway): self-service password change - #2

Closed
abeldantas wants to merge 22 commits into
mainfrom
feat/gateway-self-service-password
Closed

feat(gateway): self-service password change#2
abeldantas wants to merge 22 commits into
mainfrom
feat/gateway-self-service-password

Conversation

@abeldantas

Copy link
Copy Markdown
Owner

Summary

  • Adds PUT /auth/me/password endpoint — verifies the caller's current password and sets a new bcrypt hash. Rejects OIDC-only accounts (no local hash) with 403.
  • Adds ChangePasswordDialog in the gateway dashboard, accessible from the NavRail account menu. Follows existing dialog patterns (MUI, inline alerts, confirm-password field).
  • First slice of f17; admin-initiated password resets are not included.

Test plan

  • Log in with a password account, open the account menu, click "Change password"
  • Submit with wrong current password — expect 403 error shown in the dialog
  • Submit with correct current password and matching new passwords — expect success alert
  • Log out and log back in with the new password
  • Verify OIDC-only account (no password_hash) gets a clear rejection

amaudruz and others added 21 commits August 26, 2026 11:19
…ADME (sandbox-quantum#303)

* Show Switch working in a chat app, with room for one recording per app

* Add the Slack recording

* Replace the overview diagram

* Add the Mattermost recording

* Use the cropped overview diagram
)

* Update CLAUDE.md module layout and doc references to match the tree

The module layout listed directories that do not exist and missed some
that do, and the reference section said the synced docs/official
documentation does not exist.

* Remove tasks from the standalone feature lists

CHOO-1418 removed the task protocol from the skills, but the codex
README and the configure skills still list it as working, which can
confuse agents.

* Rename the generated docs index to TOC.md to fix a case collision

The repo tracked both INDEX.md and index.md. On macOS and Windows these
are the same path, so one of them wins, the docs/README.md link opens
the wrong file, and git status is never clean. The sync script now
writes TOC.md.

* Fix stale references in the codex docs, CONTRIBUTING and console/AGENTS.md

Small stale bits: a comparison with a Claude connector mechanism that
no longer exists, mcp_servers where the file says mcpServers, an
outdated docs row and a dead anchor in CONTRIBUTING, and connector
paths in console/AGENTS.md that resolve from the wrong directory.

* Add Mattermost to the architecture section's platform lists

The architecture section names four platforms but the README supports
five. Added Mattermost to the transport list and the image alt text.
The diagram PNG still shows four icons.

* Add release and CI badges to the readme
…4) (sandbox-quantum#304)

Dismissing the "Setting up Switch" checklist wrote the setting straight
over IPC instead of going through the settings mutation. The panel renders
from the cached `onboarding` setting and nothing broadcasts a settings
change back to the renderer, so the write persisted but the cache kept
saying `showChecklist: true` — the ✕ looked dead until the next launch.

Route the dismissal through the same mutation the Settings toggle uses, so
the cache is updated optimistically and invalidated. This also drops the
`if (!onboarding) return` guard, which silently did nothing when the
setting had not loaded; the mutation merges from the cache itself.

The existing tests mount the presentational panel with a stub `onDismiss`,
so they never touched the wiring. Add a test that mounts the hook and
asserts the cached setting flips, which fails against the old call.
…10 (sandbox-quantum#305)

switch-console 0.31.0 → 0.31.1 (patch):
- fix(console): the setup checklist's ✕ now actually dismisses it (sandbox-quantum#304)

Connector plugins patched so the sandbox-quantum#302 configure-skill correction re-downloads:
switch-connector 0.9.8 → 0.9.9, switch-connector-codex 0.3.9 → 0.3.10. The
standalone feature list no longer claims the task protocol works (removed in
CHOO-1418); codex skill also fixes an mcp_servers → mcpServers reference.
opencode untouched (0.1.5).

No switch-core release: the doc-sync PR (sandbox-quantum#295) only touched a Teams README and a
test under core/, no shipping code — bundle pin stays at 0.21.0. agent-runtime
(0.3.2) and sidecar (1.9.4) unchanged. Contracts stay 1/1.

artifacts.yaml + generated modules regenerated; artifacts-check passes.
Authored all changelog entries (every [Unreleased] was empty).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er (sandbox-quantum#307)

* fix(agent-runtime): exit when the host does, instead of running forever

The runtime intended to shut down with its host — `transport.onclose`
stopped the stream, the heartbeat and the lease renewal, then exited. It
never fired. The MCP SDK's stdio transport binds only 'data' and 'error'
on stdin, so `onclose` is reached only by an orderly `close()`, never by
a host that was killed, crashed or force-quit. That exit path was dead
code in exactly the case it existed for.

Nothing else stopped the process either: the hook listener's TCP server
held the event loop open, the 2s heartbeat and lease timers kept running
against a Switch that was gone, and `uncaughtException` swallowed the
resulting EPIPE once `serving` was true rather than letting it escalate.

Measured on one developer machine: 486 processes, 3.5 GB resident, ~198%
CPU, 242 stale loopback listeners, across five runtime versions, the
oldest 18 days. The two longest-lived had grown to 470 MB and 367 MB —
consistent with writes queueing against a stderr pipe whose reader is
gone but which was never closed.

Four independent triggers now, because each covers a case the others
miss: stdin 'end'/'close' for a dropped pipe, the termination signals
for an orderly kill, a stdout/stderr 'error' for a peer that is gone but
not closed, and a ppid watchdog as the backstop — reparenting is the one
signal that survives every way a host can vanish, and SESSION_PPID was
already recorded for the session directory. Any one of them would have
prevented all 486.

The hook listener is also unref'd, so the stdin reader is what holds the
loop open: alive now means a host is attached rather than a port is
bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(agent-runtime): clear up runtimes that already outlived their host

The shutdown fix stops new orphans. It does nothing about the ones
already running: every published version up to 0.3.2 lacks it, and those
processes sit there until the machine reboots — 486 of them, 3.5 GB, on
the machine that prompted this. A new runtime now clears them out on its
way up, which reaches a user through the connector pin without waiting
for an app release.

The signal is the process tree, not anything on disk. When the host dies
its `npm exec` wrapper is reparented to init while the runtime carries
on pointing at a wrapper that is alive but orphaned — so "is the parent
dead?" is the wrong question, and would have missed both of the worst
offenders (15765 was very much alive; only its parent was gone).
Reaching init without passing through a live host is the right one, and
it needs no port file, no session directory and no hook.

The match is anchored on argv[0] rather than a substring of the command
line. A substring test also matches any shell that merely mentions the
package — installing it, grepping for it — and an orphaned one of those
would have been killed. An early draft did exactly that; the cases are
pinned in the tests.

The reaper excludes its own chain outright rather than relying on the
walk to spare it. The walk does spare it, since a freshly spawned
runtime has a live host by construction, but a reaper that can reach
itself is one bad predicate away from killing the session it serves.

Stale session directories go too — pure litter from runtimes that were
killed outright and never ran `unpublishPort`, 8071 of them here.

Verified end to end against a real orphan built the way the bug builds
one (host killed with its stdin held open by a third party, so the
runtime never sees EOF): a fresh runtime reported `reaping 2 runtime
process(es) whose host is gone` and `removing 8016 stale session
director(ies)`, and left itself running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
agent-runtime 0.3.2 → 0.3.3 (patch):
- fix(sandbox-quantum#307): the runtime exits when its host does instead of running forever.
  Its onclose-based shutdown never fired for a killed/crashed/force-quit host,
  and the hook listener + heartbeat/lease timers kept the process alive,
  leaving stale processes and loopback listeners accumulating. Shutdown is now
  driven by four independent triggers (stdin end/close, termination signals,
  stdout/stderr write error, parent-pid watchdog).

Phase 1 of a two-phase runtime release: this publishes 0.3.3 to npm via the
switch-agent-runtime-v0.3.3 tag. The connector runtime pins (claude/codex
.mcp.json, opencode opencode.json, SWITCH_AGENT_RUNTIME_PIN), the plugin
versions, and the sidecar are DELIBERATELY left at their current versions —
a pin must never name a version npm does not have yet. Phase 2 (re-pin +
plugin/sidecar bumps) follows once 0.3.3 is live, on request.

artifacts.yaml + generated modules regenerated; artifacts-check passes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-quantum#310)

Phase 2 of the agent-runtime 0.3.3 release (0.3.3 is live on npm). Moves every
runtime pin from 0.3.2 → 0.3.3 and bumps each consumer so the host-exit fix
(sandbox-quantum#307) reaches users:

- Runtime pin → 0.3.3 in all four places: claude & codex .mcp.json, opencode
  opencode.json, and SWITCH_AGENT_RUNTIME_PIN in console distribution.ts (the
  embedded opencode config derives from that constant, so it follows).
- Plugin versions bumped so installs re-download: switch-connector 0.9.9 →
  0.9.10, switch-connector-codex 0.3.10 → 0.3.11, switch-connector-opencode
  0.1.5 → 0.1.6.
- sidecar 1.9.4 → 1.9.5 (runs the new runtime; major stays 1, wire unchanged).

claude/codex reach users on next marketplace Update; opencode + sidecar ride the
next Switch Console release. artifacts.yaml + generated modules regenerated;
artifacts-check passes; pins agree with each other (runtime-pin/connector-assets
tests green).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

* feat(console): reap orphaned agent runtimes at boot

The runtime learned to exit when its host dies, and to clear up
predecessors on startup. Both reach a machine only through the connector
pin, which moves when a user updates a marketplace plugin — so a user who
never revisits Settings → Agents keeps their backlog indefinitely. This
path reaches them when they update the app instead.

The two populations do not overlap as much as they look: someone who
updates the app but not the connector is reached only from here, and
someone running Codex standalone with no Switch Console is reached only by
the runtime. Neither alone covers the fleet.

The predicate is imported from the runtime package, not reimplemented.
Two copies of "is this runtime abandoned?" is a failure mode this repo
already knows — where they disagree, one of them kills a live session —
so `reapOrphanedRuntimes` moved to the package's public surface and both
callers share it.

It now returns what it did rather than taking a log callback. There were
two callers with two ideas of a log line: the runtime writes prose to
stderr, the app wants an enumerated event, and threading either through
the sweep was the wrong shape.

Runs unawaited after the RPC router is up and before session relaunch, so
an abandoned runtime is gone before its replacement starts, without the
window waiting on a full process scan.

Verified against real orphans rather than mocks, twice:

  reaped: 4, removedSessionDirs: 3     — four genuine orphans, all gone
  reaped: 0                            — with a healthy session running,
                                         its runtime, its wrapper and its
                                         session directory all untouched

The second matters more than the first. Killing a live session would be
far worse than leaving an orphan, and it is the case no unit test can
fully stand in for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(console): bring installed Switch connectors up to date once

A connector's pinned runtime version only moves when a user goes looking
for it in Settings → Agents. Someone who never does keeps whatever they
first installed, across every release — which is how an install ends up
several runtime versions behind without anything being broken enough to
notice.

This is a one-shot catch-up, deliberately not a standing auto-updater.
It latches a generation marker on success and does nothing on every
later launch; the Update button stays the normal path. Bumping
CATCH_UP_GENERATION — a deliberate edit, not a release — is what makes
it run again.

What it will not do:

- install a connector that is not installed. Which agents have one is
  the user's decision, and this is a version catch-up, not an installer.
- latch when the marketplace could not be refreshed. `updateAvailable`
  is false both for "no update" and "could not look", and spending the
  single shot on an answer that never arrived is the one way a one-shot
  fails silently.
- retry forever. Three attempts, then it stops, because "runs every
  launch" is precisely what this is not.

Runs unawaited before session relaunch, so a session started on the old
pin picks the new one up next launch rather than holding up this one.

Verified locally against a real gap. First launch:

  connector_catch_up_updated  claude    0.9.8 -> 0.9.10
  connector_catch_up_updated  opencode  0.1.5 -> 0.1.6
  connector_catch_up_complete updated: 2

Second launch on the same install: no catch-up activity, no marketplace
call, nothing logged — latched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
switch-console 0.31.1 → 0.31.2 (patch, per request — contains sandbox-quantum#309):
- reap orphaned agent runtimes at boot (sandbox-quantum#309)
- one-shot catch-up to bring already-installed connectors up to date once (sandbox-quantum#309)

Also ships the console-side Phase 2 rollout already on main: the bundled sidecar
(1.9.5) and OpenCode connector (0.1.6) now run agent-runtime 0.3.3. Bundle pin
stays at core 0.21.0 (no core release).

Runtime version drift from sandbox-quantum#309 (runtime source changed but package.json still
0.3.3, matching npm) is deliberately left for the next runtime release — no
user-facing runtime behaviour changed.

artifacts.yaml + generated modules regenerated; artifacts-check passes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CHOO-2455)

The New agent dialog greys out "Add agent" whenever the form is not ready but
doesn't tell people why. This fixes that.
The previous commit keyed the tooltip on !runHostReady, which is true both
while the readiness probe is still in flight and when the host is known to
be missing setup. A blocked host got "Waiting for ... to be ready", telling
the user to wait for something that never resolves. Branch on
hostReadiness.checking and hostReadiness.blocked instead, and point the
blocked case at the HostReadinessNotice that names what is missing.

Also runs the file through oxfmt, which the previous commit skipped and
which is what failed CI.
…disabled-reason

Tell the user why "Add agent" is disabled, on hover over the button (CHOO-2455)
…ion-bump-rule

Stop hand-bumping versions per commit; leave it to the release agent
A dashboard user who knows their current password can now set a new one
without involving an admin.  This is the first slice of f17 (self-service
password management); admin-initiated resets are out of scope here.

Backend: PUT /auth/me/password — verifies the current password, hashes
the replacement, and commits.  Rejects OIDC-only accounts (no hash to
verify against) with 403.

Frontend: ChangePasswordDialog accessible from the NavRail account menu,
following the existing CreateUserDialog pattern (MUI Dialog, inline
error/success alerts, confirm-password guard).
Copilot AI lite review requested due to automatic review settings August 31, 2026 13:08

Copilot AI 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.

🟡 Changes recommended

The new password-change flow has confirmed correctness/UX gaps (OIDC-only handling + dialog state) and lacks direct tests for the new auth endpoint.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a self-service password-change feature to the Gateway (API + dashboard UI), while also bundling a larger set of Switch Console/runtime maintenance and documentation/indexing updates.

Changes:

  • Adds PUT /auth/me/password plus a Gateway dashboard “Change password” dialog that calls it.
  • Improves Slack typed-command handling by translating Slack mentions in !commands.
  • Updates Switch Console + agent runtime (shutdown/reaping behavior, connector catch-up, version pins) and refreshes docs/indexing + release metadata.
File summaries
File Description
signatures/version1/cla.json Adds CLA signature entries.
scripts/sync_docs.py Writes docs index to TOC.md instead of INDEX.md.
README.md Updates marketing copy, badges, images, and embedded demo recordings.
gateway/src/layout/NavRail.tsx Adds “Change password” entry + dialog wiring in account menu.
gateway/src/layout/ChangePasswordDialog.tsx New dialog UI for changing password.
gateway/src/data/api.ts Adds changePassword() API call (PUT /auth/me/password).
docs/README.md Updates docs pointer from official/INDEX.md to official/TOC.md.
docs/official/TOC.md Adds generated docs table-of-contents file.
core/tests/switch_core/bridges/collaboration/test_slack_adapter.py Adds test ensuring typed !invite-agent <@U…> mention translation.
core/switch_core/gateway/schemas.py Adds ChangePasswordRequest schema.
core/switch_core/gateway/auth_routes.py Adds PUT /auth/me/password endpoint.
core/switch_core/bridges/collaboration/slack/adapter.py Translates mentions for typed ! commands (aligns with slash command behavior).
core/switch_core/artifacts.py Bumps artifact version constants.
CONTRIBUTING.md Updates README anchor + clarifies docs/ structure.
console/packages/switch-agent-runtime/src/reap.ts Adds orphaned-runtime and stale-session-dir reaper implementation.
console/packages/switch-agent-runtime/src/reap.test.ts Adds unit tests for reaper predicate + directory sweeping.
console/packages/switch-agent-runtime/src/index.ts Exports reaper utilities from runtime package.
console/packages/switch-agent-runtime/src/bin.ts Adds shutdown triggers + runtime self-reaping; adjusts session root constant.
console/packages/switch-agent-runtime/src/bin.shutdown.test.ts Adds tests for shutdown triggers/watchdog behavior.
console/packages/switch-agent-runtime/src/artifacts.ts Bumps artifact versions in runtime package.
console/packages/switch-agent-runtime/package.json Bumps runtime version to 0.3.3.
console/packages/shared/src/artifacts.ts Bumps shared artifact versions.
console/packages/plugins/src/distribution.ts Updates runtime pin to @sandboxaq/switch-agent-runtime@0.3.3.
console/apps/switch-console-desktop/src/renderer/tests/browser/onboarding-checklist-dismiss.test.tsx Adds renderer test ensuring checklist dismissal updates cached state + persists.
console/apps/switch-console-desktop/src/renderer/features/onboarding/use-onboarding-checklist.ts Fixes dismissal to use settings mutation (not raw IPC) so UI updates immediately.
console/apps/switch-console-desktop/src/renderer/features/locations/components/add-agent-modal/add-agent-modal.tsx Adds tooltip explaining why “Add agent” is disabled.
console/apps/switch-console-desktop/src/main/index.ts Runs orphaned-runtime reap + connector catch-up at boot (best-effort).
console/apps/switch-console-desktop/src/main/core/switch-setup/catch-up-connectors.ts Adds one-shot connector catch-up with latch + retry cap.
console/apps/switch-console-desktop/src/main/core/switch-setup/catch-up-connectors.test.ts Adds tests for catch-up behavior and latching/retry semantics.
console/apps/switch-console-desktop/src/main/core/agent-runtime/reap-orphaned-runtimes.ts Adds app-level wrapper around runtime reaper at boot.
console/apps/switch-console-desktop/src/main/core/agent-runtime/reap-orphaned-runtimes.test.ts Adds tests for app-level reaper wrapper + logging behavior.
console/apps/switch-console-desktop/package.json Bumps Switch Console desktop version to 0.31.2.
console/AGENTS.md Fixes connector-path references to be relative to console/.
connectors/opencode-plugin/package.json Bumps OpenCode connector version to 0.1.6.
connectors/opencode-plugin/opencode.json Pins runtime to @sandboxaq/switch-agent-runtime@0.3.3.
connectors/codex-plugin/skills/configure/SKILL.md Fixes MCP key name + updates feature list wording.
connectors/codex-plugin/README.md Updates wording around env forwarding + feature list.
connectors/codex-plugin/.mcp.json Pins runtime to @sandboxaq/switch-agent-runtime@0.3.3.
connectors/codex-plugin/.codex-plugin/plugin.json Bumps Codex plugin version to 0.3.11.
connectors/claude-code-plugin/skills/configure/SKILL.md Updates feature list wording.
connectors/claude-code-plugin/.mcp.json Pins runtime to @sandboxaq/switch-agent-runtime@0.3.3.
connectors/claude-code-plugin/.claude-plugin/plugin.json Bumps Claude Code plugin version to 0.9.10.
CLAUDE.md Updates repository guidance/docs section and connector notes.
CHANGELOG.md Adds release notes for console/runtime/plugin version bumps and fixes.
artifacts.yaml Bumps artifact versions for console/runtime/sidecar/connectors.
Review details

Suppressed comments (1)

core/switch_core/gateway/auth_routes.py:188

  • PUT /auth/me/password currently treats OIDC-only accounts (user.password_hash is None) as an incorrect current password and returns a misleading 403 detail. The PR description calls out a distinct 403 rejection for OIDC-only accounts, so this route should check for password_hash is None explicitly and return a clear message before calling verify_password.
  • Files reviewed: 44/47 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +180 to +185
@router.put("/auth/me/password")
async def change_password(
req: ChangePasswordRequest,
session: Annotated[AsyncSession, Depends(get_session)],
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, bool]:
Comment on lines +41 to +59
const handleSubmit = useCallback(async () => {
if (newPassword !== confirmPassword) {
setError("New passwords do not match");
return;
}
setSubmitting(true);
setError(null);
try {
await changePassword(currentPassword, newPassword);
setSuccess(true);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to change password");
} finally {
setSubmitting(false);
}
}, [currentPassword, newPassword, confirmPassword]);
Comment on lines +180 to +184
@router.put("/auth/me/password")
async def change_password(
req: ChangePasswordRequest,
session: Annotated[AsyncSession, Depends(get_session)],
user: Annotated[User, Depends(get_current_user)],
The change-password endpoint accepted any string, including empty — a
curl call could set an account's password to "" and leave it
unrecoverable from the login form.  Add min_length=8 on
ChangePasswordRequest.new_password so Pydantic rejects degenerate
values before the handler runs.

Also adds the missing test coverage: happy path, wrong current
password → 403, short password → 422, empty password → 422.
@abeldantas abeldantas closed this Aug 31, 2026
@abeldantas

Copy link
Copy Markdown
Owner Author

Superseded by sandbox-quantum#323 (relocated to the main repo).

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.

4 participants