[v0.1/P1] Make dynamic session completion local-only #42
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#42
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #12 (P1). This issue retains the actionable completion finding from the SSH and CLI-argument reviews; the broader SSH spawn and timeout questions are already settled by the control-surface and carrier design records.
Why
Fish and zsh invoke the hidden
__complete-sessionshelper on<TAB>. The generated overlays currently forward--hostand--ssh-arg, so completion can start an interactive SSH connection with no felis-side deadline. An unreachable host, password prompt, or host-key prompt can therefore block the user's shell.The implementation also contradicts
docs/explanation/architecture/control-surfaces.md, which says the hidden helper stays local-only because it runs once per<TAB>.docs/reference/ipc.mdcurrently describes the implemented remote behavior and must be corrected with the code.Decision
Dynamic session completion must never start SSH. Keep local daemon completion, including an explicitly selected local
--socket, but emit no session candidates when the in-flight source or retarget target is an SSH destination. SSH destination fields continue to use the shell's own host-name completion.Do not add a general felis SSH timeout or a headless
--spawnflag here. OpenSSH owns transport timeout and multiplexing policy, and the existing session-verb versus window/spawn autostart split is already intentional and documented.Acceptance criteria
<TAB>with an in-flight--hostor SSH retarget target never executessshor another network operation.--socket.__complete-sessions.docs/reference/cli.md,docs/reference/ipc.md,docs/explanation/architecture/control-surfaces.md, generated completions, andCHANGELOG.mddescribe the same local-only contract.CLI args focused review (#55) で timeout scope に completion 経路の抜けを確認した。
fish/zsh overlay は session-id slot の
<TAB>ごとにfelis ... __complete-sessionsを起動し、in-flight--hostを転送して SSH dial する。connect_stdio_commandは SSH stderr/TTY を継承し felis-side timeout を持たないため、不達 host、password、host-key prompt で対話シェルの completion 自体が停止し得る。SSH timeout 方針を決める際は通常 verb だけでなく hidden completion dial も対象にしたい。候補は remote dynamic completion を削る/local-only に戻す、または短い hard timeout + noninteractive SSH + cache。なお
explanation/architecture/control-surfaces.mdは helper を local-only と書く一方、実装とreference/cli.mdは remote forwarding を契約化しており docs も不整合。[v0.1/SSH Review] SSH stdio のタイムアウト/ハンドシェイク/スポーン方針の未統一to [v0.1/P1] Make dynamic session completion local-onlyTriage plan (2026-09-03)
Source-grounded triage against
mainat69076d42, reviewed through seven rounds of an independent reviewer (pisol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12.Claim check
Accurate at HEAD, including the docs contradiction.
__felis_carrier_flagsechoes every in-flight--host/--socket/--ssh-argtoken (crates/felis-cli/src/cli_completions.rs:131-152), and__felis_complete_retarget_target_sessionsrebuilds--host <dest>from the retarget verb's--host, thesshalias's positional, and--ssh-arg(154-199). zsh does the same in_felis_complete_sessions(255-283) and_felis_complete_retarget_target_sessions(285-327). The module doc states it as intended (cli_completions.rs:10-13), and the unit testfish_overlay_covers_option_slots_and_forwards_the_carrier(500-516) pins the forwarding.Cmd::CompleteSessionsresolvescli.host/cli.ssh_arg(main.rs:443-454) andrun_complete_sessionscallsconn::dial(cli_completions.rs:70-79) →connect_carrier(…, RemoteSpawn::Refuse)(conn.rs:103-105) →connect_stdio_commandforCarrier::Ssh(felis-client-core/src/connector.rs:466-470, 252-266).connect_stdio_commandleavescmd.stderrat the parent default by design so password prompts reach the user (connector.rs:249-251); there is no timeout around the spawn or the handshake.ROSTER_FETCH_TIMEOUT(dial.rs:199) covers only the GUI attach roster, not this path. The overlays'2>/dev/nullhides ssh's stderr but ssh still opens/dev/ttyfor a password or host-key prompt, so the<TAB>blocks inside the interactive shell.docs/explanation/architecture/control-surfaces.md:412-416: the helper "stays local-only because it runs once per shell<TAB>".docs/reference/ipc.md:1611-1613anddocs/reference/cli.md:733-741: the overlays forward--host/--socket/--ssh-argso the roster comes from the daemon the verb will dial. The reference pages describe the code; the explanation describes the intent.--spawnquestion away. Nothing to reopen.Nothing already fixed.
Verdict
accept. Principle 1 test: the shell's own host completer already owns SSH destinations, and the daemon roster is the one candidate source only felis can provide; both stay. Removing the network hop on
<TAB>adds no capability and removes an unbounded, interactive side effect from a keystroke. Principle 4 (explicit): the contract becomes one sentence rather than "sometimes remote, sometimes not, depending on what is on the line".Approach
Code (
crates/felis-cli)main.rs:443-454Cmd::CompleteSessions: resolve the target withhost = None, ssh_args = &[]regardless of what the root flags carry, and exit 0 with no output whencli.host.is_some()(the local roster would be wrong candidates for a remote verb). Two layers on purpose: an already-installed overlay from an older build keeps forwarding--host, and the binary must still never dial ssh from it.cli_completions.rs:70-79run_complete_sessions: guardif matches!(target.carrier, Carrier::Ssh { .. }) { return 0; }beforedial, so the function itself is safe whatever the caller resolved. Update the module doc (10-16) and the fn doc (67-69).__felis_carrier_flags(131-148) forwards--socket/--socket=*only and, on seeing--host/--host=*, returns a sentinel so__felis_complete_sessions(150-152) emits nothing.__felis_complete_retarget_target_sessions(154-199): keep the--to-socket→--socketmapping; on--host/positional destination (after #23:--to-host) emit nothing; drop thessh_argsaccumulator. The-l host -a '(__fish_complete_user_at_hosts)'andsshdestination lines (402-404) stay (shell-local host completion is untouched)._felis_complete_sessions(258-274) and_felis_complete_retarget_target_sessions(290-318); the_hostssubstitutions (365-366) stay.--spawn, no felis-side ssh timeout (per the issue's decision; record the rejection in the explanation twin with the pointer to #38's OpenSSH-owns-timeouts record).Tests
cli_completions.rsunit tests: replacefish_overlay_covers_option_slots_and_forwards_the_carrier(500-516) withfish_overlay_forwards_only_the_local_socket(assert thecaselist contains--socketand not--host/--ssh-arg; assert the retarget helper contains no--hostbranch that invokesfelis); same forZSH_OVERLAY(zsh_overlay_routes_option_slots_by_value_name,521-531). Because the overlays are strings, add a stronger executable check where the interpreter exists: a#[test]that skips unlessfish/zshis onPATH, sources the generated script withfelisreplaced by a stub that records its argv, simulatesfelis --host box sessions info <TAB>, and asserts the stub was never invoked with--host(the flake's dev shell can carry both shells so CI runs it; note this intesting.md).run_complete_sessionsunit test: aReconnectorwithCarrier::Ssh { destination: "nowhere", .. }andPATH=""returns 0 immediately, prints nothing, and spawns no child (measure elapsed < 1s; a spawn attempt would fail on NotFound but the point is no spawn at all — assert via asshstub on a tempPATHthat writes a marker file, and check it is absent).tests/cli_sessions.rs):felis --socket <sock> __complete-sessionsstill lists the fixture daemon's session;felis --host nowhere __complete-sessionsexits 0 with empty stdout in well under a second.Cascade
docs/reference/cli.md:733-741completions bullet: "fish/zsh complete session ids from the default local daemon or an explicit--socket; with an SSH destination on the line no session candidates are offered; destination slots use the shell's own host completer".docs/reference/ipc.md:1611-1613: replace "even the hidden helper honors them" with the local-only sentence.docs/explanation/architecture/control-surfaces.md:412-416: keep the claim and give it its rationale inline: once per<TAB>, ssh has no felis-side deadline (owned by OpenSSH, #38 record inarchitecture/ipc.md), and an interactive prompt from a completion function blocks the shell; rejected: a short hard timeout +BatchMode=yes+ cache (felis would be choosing ssh policy the user's config already owns, and a cache is state the completion path has no place for). Revisit if OpenSSH grows a non-interactive probe felis can call without owning policy.cli_completions.rsmodule doc;CHANGELOG.md"Changed: shell completion no longer dials a remote daemon on<TAB>".skills/felis/SKILL.md: no mention of completion today (grep -n completfinds only unrelated words), so no change.__mangen: the hidden verb is excluded (cli_mangen.rs:6-7), no change.just schema: not affected.Dependencies
--host→--to-hoston the retarget verbs and deletesfelis ssh, which rewrites the same overlay functions; doing #42 first turns #23's overlay work into a token rename and deletes the alias branch it would otherwise have to port. If #23 lands first, fold this into its overlay rewrite instead of a separate pass.Risk/effort
S. Two shell-script strings, one guard, one dispatch tweak, three doc sites. Main risk: an installed older completion script keeps forwarding
--hostuntil the user regenerates it; the binary-side guard covers that. Secondary: the zsh overlay's substitution anchors (cli_completions.rs:512-535) are unchanged by this issue, so the tripwire test (536-549) still holds.Labels
Keep priority/P1, release/v0.1.0 (the #12 comment already lists it as a closure dependency). It is small enough to land ahead of the larger CLI items and unblocks a cleaner #23.