refactor!: move client adoption, MCP editing and the client scan to the desktop app - #186
Merged
Merged
Conversation
…he desktop app
Adopting a client, editing its MCP servers and scanning its configuration
change files on the machine the desktop app runs on, which need not be the
one running core. The desktop app now does all of it (it took the tw-adopt
and tw-scan crates and the file watcher), so core drops them:
- crates tw-adopt and tw-scan
- tw-control's scan.rs and the client-config watcher; clients.rs keeps only
POST /clients/{id}/key, which issues a client its own gateway key. It no
longer knows the client list, so it accepts any id shaped like one
(lowercase letters, digits, `-`) and leaves recognising clients to the app
- endpoints /scan, /clients, /clients/plan, /clients/adopt,
/clients/{id}/restore/plan, /clients/{id}/restore, /clients/{id}/why,
/mcp/targets, /mcp/plan, /mcp/apply, their types, and the ClientsChanged
and ScanAlert events
- rotating a key no longer writes the new value into an adopted client
(KeyRotated loses synced/failed), and deleting a key no longer checks
for an adopted client; the app does both on its side
- ControlState.home and tw_control::home_dir, twcore's `scan` and `clients`
subcommands, the M4 acceptance test (its checks moved with the crates)
CONTROL_API_VERSION goes to 18. msg-codes.txt loses the adopt.*/scan.*
codes plus control.key_bind_failed and control.key_used_by_client; the app
lists the ones it now emits itself. tw-guard gains a test that the two
files the client scan uses cannot write or delete anything (the scan's own
copy of that check moved with it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn
force-pushed
the
feat/drop-adopt-scan
branch
from
September 24, 2026 15:59
1b871dd to
28d1256
Compare
fylorn
added a commit
that referenced
this pull request
Sep 24, 2026
…ide follows the CLI - The manual now checks listen.control.remote against RemoteListen instead of declaring it pending: port is required (written at random, 20000-32000, by twcore init and twcore remote enable), bind defaults to all, allow_from to the private ranges, enabled to false. Regenerated both languages. - docs/server.md and its Chinese version use `twcore remote enable/disable`, show what `twcore control-key` prints on stdout and stderr, and say what a remote connection cannot do, how throttling works, and that narrowing allow_from or rotating the key closes open connections. - CONTROL_API_VERSION is 20 on top of #186's 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn
added a commit
that referenced
this pull request
Sep 24, 2026
… channel (#187) * feat(control): a remote control port, opened in addition to the local channel listen.control.remote { enabled, bind, port, allow_from } opens a TCP port for a desktop app on another machine. It is separate from the unix socket / Windows loopback channel and uses the same key and Noise handshake. - The port has no default: twcore init writes the section closed with a random port (20000-32000, never the gateway's), and `twcore remote enable|disable` opens and closes it with minimal YAML edits. Validation rejects port 0, the gateway's port, and malformed allow_from entries (new config.* codes). - The listener follows config reloads: enable, disable, bind and port changes apply live (a new address is bound before the old one is dropped; a failure keeps the old one and is reported). Closing or moving the port closes the connections made through it. It never takes down the local channel or the gateway. - Before the handshake: sources outside allow_from are closed without a byte (loopback is not waved through), a source with 5 failed handshakes in a minute is ignored for a minute, and at most 32 remote connections are open. - Remote connections speak HTTP/1.1 only and carry a task-local marker; they get 403 for POST /shutdown, the diagnostic bundle, and any config write that changes listen.control (control.remote.* codes). - Status gains remote_control { enabled, addr, error, allow_from, reachable } and gateway_reachable; CONTROL_API_VERSION is 19. - twcore control-key also prints (to stderr) where remote control listens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): revoked sources lose their remote connections, and the port moves between overlapping addresses live - Every config reload now makes each open remote connection re-check allow_from; a source the list no longer lets in is closed at once, the same way a rotated key closes connections made with the old one. - Moving the remote port between overlapping addresses on the same port (all <-> an interface address) no longer fails with EADDRINUSE: when the new bind collides on the same port, the old listener is closed first and the new one bound; if that still fails, the old address is bound again and the reason is reported. Different ports still bind the new one first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(config): check listen.control.remote against the code; server guide follows the CLI - The manual now checks listen.control.remote against RemoteListen instead of declaring it pending: port is required (written at random, 20000-32000, by twcore init and twcore remote enable), bind defaults to all, allow_from to the private ranges, enabled to false. Regenerated both languages. - docs/server.md and its Chinese version use `twcore remote enable/disable`, show what `twcore control-key` prints on stdout and stderr, and say what a remote connection cannot do, how throttling works, and that narrowing allow_from or rotating the key closes open connections. - CONTROL_API_VERSION is 20 on top of #186's 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase P3 of the remote-core plan, core side. Adopting AI clients, editing their MCP servers and scanning their configuration all change files on the machine the desktop app runs on. With remote core that may not be the machine running core, so this work moves to the app: ThinkWatch-Lite takes over the
tw-adoptandtw-scancrates and the file watcher in ThinkWatchProject/ThinkWatch-Lite#180. Merge that PR first. It stops calling these endpoints and still works against v0.46.0.Removed
tw-adoptandtw-scancratestw-control:scan.rs(scan endpoint and client-config watcher);clients.rsexceptPOST /clients/{id}/key;ControlState.homeandhome_dir()tw_api::ep:Scan,Clients,PlanAdopt,Adopt,PlanRestore,Restore,Why,McpTargets,McpPlan,McpApply, plus every type only they used (ScanRequest/Response,ScanFinding,McpView,SkillView,HookView,McpOp*,McpTargetView,ClientsResponse,DetectedClient,ManualClient,ManualSetup,AdoptRequest/Response,PlanView,FieldChange,FieldOp,FindingView,FindingLevel,TakesEffect,Verification,KeySynced,KeySyncFailed)Event::ClientsChangedandEvent::ScanAlerteventstwcore scanandtwcore clientssubcommandsKeyRotatedis just{ version, key }), and key deletion no longer checks for an adopted client. The app does both, because only it can see the client's files.tw-control/tests/m4_acceptance.rs: its checks (adopt, restore byte for byte, zero-width detection, never deleting) moved with the crates.Kept
POST /clients/{id}/key. It returns the key already bound to that client or creates one bound to it (claude-code, thenclaude-code-2if that name is taken). Core no longer holds the client list, so it accepts any id that looks like a client id (lowercase letters, digits,-, at most 64 characters). The app decides which clients exist. With no gateway key at all it still answerscontrol.no_keys.Contract
CONTROL_API_VERSION18 → 19, with a doc entry. The handshake test intests/gate.rsnow checks againstCONTROL_API_VERSIONinstead of a hardcoded 18.msg-codes.txtdropsadopt.*,scan.*,control.key_bind_failedandcontrol.key_used_by_client. Lite now keeps its ownsrc-tauri/msg-codes.txtfor the codes it emits, includingcontrol.key_used_by_client. When lite bumps to this core, it must delete thecontrol.key_bind_failedtranslation, drop the twoscan_alert/clients_changedcases insrc/types.ts, and remove the transitional sync handling inrotate_key. I checked this by building lite against this branch with a[patch]: only those spots break.Docs and tests
scripts/smoke.shchecks the client-key endpoint (issued once, then the same key again) instead of the adopt round trip.tw-guard/tests/reads_only.rs: the two files the client scan uses (hidden.rs,tools/rules.rs) cannot write or delete anything. The scan's own copy of this check moved with it.tests/adopt.rsare now intests/bundle.rs. The client-key tests are intests/client_key.rs.Local run:
cargo fmt --check,cargo clippy --workspace --all-targets -D warningsandcargo test --workspaceall pass;scripts/smoke.shpasses 56, fails 0 (after the rebase).Crate and binary versions are unchanged and will be set at merge time.
🤖 Generated with Claude Code