Commit 16c0b27
feat(connect): one-click undo that restores the pre-connect client config (Spec 078 US3) (#804)
* feat(connect): one-click undo that restores the pre-connect config (Spec 078 US3)
The connect step's change is now fully reversible from the surface that made
it: the wizard row and the standalone Connect modal gain a one-click Undo next
to the backup path they surface. Undo restores the client config byte-for-byte
from the backup the connect took — the only revert that can bring back a
pre-existing same-named entry a force-connect overwrote (surgical disconnect
cannot) — or removes the file when the connect created it. Before reverting,
the change to be undone is shown again (FR-009), and the restore refuses with
a clear 409 when the file changed since the connect, never clobbering later
edits.
## Changes
- internal/connect: Service.Undo — byte-identical restore from the connect
backup; drift check replays the connect write from the backup and compares
bytes (refuses on any mismatch); no-prior-file case deletes the created
file; own safety backup before every mutation; backup paths validated to
<config>.bak.* of that client only; TCC denials map to *AccessError
- internal/connect: backup names are now collision-proof — same-second
operations get a -1/-2 numeric suffix (O_EXCL, never overwrite), fixing the
case where connect+disconnect within one second destroyed the very backup a
later undo needs; existing <config>.bak.<ts> shape kept as prefix
- httpapi: POST /api/v1/connect/{client}/undo — 200 restored|deleted,
409 conflict on drift, 404 missing backup/unknown client, 403 + remediation
on macOS App-Data denial (mirrors the other per-client connect routes)
- Web UI: wizard ClientRow Undo affordance on the session backup line +
revert-preview panel (shows the entry that will be reverted, Undo/Keep);
same session-scoped affordance on the ConnectModal result area
- Telemetry: connect_step completion is deliberately NOT retracted on undo —
the funnel event records that the step was completed; undo reverts the file
change, not the funnel transition (FR-016)
- Docs: rest-api.md (preview + undo endpoints, backup naming/retention),
setup.md wizard callout; OpenAPI regenerated
## Testing
- Go: byte-identical restore (JSON + TOML), drift refusal (with and without a
prior file), no-prior-file delete, missing/foreign backup, deterministic
same-second backup collision; httptest for the endpoint incl. 409/404/403
- vitest: wizard + modal undo flows (revert panel first, honest refusal,
session scoping) — 256 tests green
- Live smoke on :18096 (scratch HOME): force-connect over a user-owned
mcpproxy entry, undo via curl, diff byte-for-byte; drift edit → HTTP 409,
file untouched
* docs(api): drop stale 'sole endpoint' App-Data prompt claims now that preview/undo also read configs
The per-client status doc claimed GET /connect/{client} is the only Connect
endpoint that opens a client config file; the preview and undo routes
(documented in the same section) also read it on demand, so the 'sole/only
place' wording contradicted the surrounding text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(connect): reject traversal backup paths in undo (defense-in-depth)
The undo backup-path guard was prefix-only: a path like
"<config>.bak.x/../../secret.json" kept the "<config>.bak." prefix yet
escaped the config directory, letting undo read (and, in a crafted case,
restore) an arbitrary file — defeating the documented "must not become an
arbitrary-file-restore primitive" invariant. Anchor validation on the
cleaned path's parent directory and basename instead, both traversal-proof.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(connect): undo takes a validated backup basename, path resolved server-side
CodeQL flagged path injection at the connect read() seam: the undo request's
user-supplied backup_path flowed into os.ReadFile. Rather than sanitize a
client-controlled path, stop trusting one entirely.
The request now carries only backup_name — the bare filename of the backup the
connect returned. Undo resolves the full path itself by joining it with THIS
client's own config directory (derived from the client registry, never from the
request), after rejecting anything whose filepath.Base differs from the input
and requiring the strict "<config-basename>.bak." prefix. The user input can no
longer contribute a directory component, so traversal is impossible by
construction and the taint path breaks at the Base()==input guard + constant-dir
join.
- API: UndoConnectRequest.BackupPath -> BackupName (json backup_name); server
resolves + validates; unknown basename still 404, path-shaped name -> 400.
- Frontend: api.undoConnectClient sends filepath.Base (strips / and \) so both
wizard and ConnectModal call sites emit a bare name; new connect-undo-api spec
asserts the wire payload.
- Docs + OpenAPI regenerated for backup_name.
- Tests: absolute path rejected, separators-in-name rejected, unknown name 404,
path-shaped name -> 400 at the HTTP boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent ed42d46 commit 16c0b27
16 files changed
Lines changed: 1982 additions & 33 deletions
File tree
- docs
- api
- frontend
- src
- components
- services
- tests/unit
- internal
- connect
- httpapi
- oas
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
661 | 661 | | |
662 | 662 | | |
663 | 663 | | |
664 | | - | |
665 | | - | |
| 664 | + | |
| 665 | + | |
666 | 666 | | |
667 | 667 | | |
668 | 668 | | |
669 | 669 | | |
670 | 670 | | |
671 | 671 | | |
672 | | - | |
673 | | - | |
674 | | - | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
675 | 676 | | |
676 | 677 | | |
677 | 678 | | |
| |||
684 | 685 | | |
685 | 686 | | |
686 | 687 | | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
687 | 745 | | |
688 | 746 | | |
689 | 747 | | |
| |||
698 | 756 | | |
699 | 757 | | |
700 | 758 | | |
701 | | - | |
| 759 | + | |
| 760 | + | |
702 | 761 | | |
703 | 762 | | |
704 | 763 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
| 257 | + | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
243 | 306 | | |
244 | 307 | | |
245 | 308 | | |
| |||
341 | 404 | | |
342 | 405 | | |
343 | 406 | | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
344 | 420 | | |
345 | 421 | | |
346 | 422 | | |
| |||
457 | 533 | | |
458 | 534 | | |
459 | 535 | | |
460 | | - | |
| 536 | + | |
| 537 | + | |
461 | 538 | | |
462 | 539 | | |
463 | | - | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
464 | 552 | | |
465 | 553 | | |
466 | 554 | | |
467 | 555 | | |
468 | 556 | | |
469 | | - | |
| 557 | + | |
470 | 558 | | |
471 | 559 | | |
472 | 560 | | |
473 | 561 | | |
| 562 | + | |
| 563 | + | |
474 | 564 | | |
475 | 565 | | |
476 | 566 | | |
| |||
486 | 576 | | |
487 | 577 | | |
488 | 578 | | |
489 | | - | |
| 579 | + | |
490 | 580 | | |
491 | 581 | | |
492 | 582 | | |
| |||
500 | 590 | | |
501 | 591 | | |
502 | 592 | | |
503 | | - | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
504 | 630 | | |
505 | 631 | | |
506 | 632 | | |
| |||
510 | 636 | | |
511 | 637 | | |
512 | 638 | | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
513 | 642 | | |
514 | 643 | | |
515 | 644 | | |
| |||
657 | 786 | | |
658 | 787 | | |
659 | 788 | | |
| 789 | + | |
| 790 | + | |
660 | 791 | | |
661 | 792 | | |
662 | 793 | | |
| |||
674 | 805 | | |
675 | 806 | | |
676 | 807 | | |
| 808 | + | |
| 809 | + | |
677 | 810 | | |
678 | 811 | | |
679 | 812 | | |
0 commit comments