Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "conn",
"source": "./plugin",
"description": "Conn: MCP server + working-etiquette skill.",
"version": "0.8.4"
"version": "0.8.5"
}
]
}
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## Unreleased

## 0.8.5 — Preview · 2026-09-22

- Restore policy-based Autopilot in POSIX SSH: ordinary commands run, risky commands request approval, and deny rules remain blocked. Remote paths are never inspected as host files.
- Make **Allow this session** work for SSH confirm rules and publish the allowance immediately. Programs requiring individual review explain that requirement instead of showing an inert button. The control indicator explains the selected collaboration mode.
- Stop resizing the underlying terminal when opening the modal settings sheet. The obsolete side-panel width animation repeatedly resized SSH line editors and corrupted long input/cursor placement.
- Keep approval actions visible while long command details scroll inside the common decision card.

한국어: POSIX SSH의 Autopilot에서 일반 명령은 자동 실행하고 위험 명령은 승인을 받도록 수정했습니다. 세션 허용은 해당 확인 규칙에 적용하고 즉시 표시하며, 명령별 검토가 필요한 경우에는 이유를 안내합니다. 모달 설정 뒤에서 남아 있던 터미널 너비 변경을 제거해 재현된 긴 입력·커서 깨짐을 수정했습니다. 긴 명령의 승인 내용은 카드 안에서 스크롤하고 승인 버튼은 계속 표시합니다.

Validation: 258 Rust tests, 48 frontend tests, 85 core/xterm comparisons, real SSH browser regressions and the 12-stage collaboration harness passed. On Apple Silicon macOS 27.0, the same product code was also exercised in an ad-hoc signed native debug app with real WebKit, Tauri IPC and loopback SSH: long input, cursor editing, approval/session allowances, takeover and original-shell return passed. Release artifact signing/notarization and installation checks are separate.

Remaining coverage: physical-keyboard Korean IME composition and the user's exact SSH setup remain unverified. WebKit emitted ResizeObserver warnings without a persistent mismatch in the checked steps. Other macOS versions, Windows interactive GUI and full installed-app upgrades remain open. [Validation report](docs/ssh-input-review-results.md).

한국어: Rust 258개·프런트엔드 48개·화면 비교 85회·실제 SSH 브라우저 회귀·협업 하네스 12단계를 통과했습니다. Apple Silicon macOS 27.0의 실제 WebKit·Tauri 디버그 앱과 SSH에서 긴 입력·커서 편집·승인·세션 허용·제어권 회수·원래 셸 복귀도 확인했습니다. 배포 파일 서명·공증과 설치 검사는 별도입니다. 실제 키보드 한글 IME 조합과 사용자의 정확한 SSH 환경은 미검증이며, ResizeObserver 경고가 관찰됐습니다. 다른 macOS 버전, Windows 대화형 UI와 설치된 앱의 전체 업그레이드 검증은 남아 있습니다. [검증 보고서](docs/ssh-input-review-results.ko.md).

## 0.8.4 — Preview · 2026-09-21

- Keep pending requests, active control and recent agent activity visible across tabs. Clicking a notice opens the exact terminal and request; opening or switching an agent's tab preserves the person's current selection.
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ members = ["crates/core", "crates/cli", "crates/frontend", "crates/browser-harne
default-members = ["crates/core", "crates/cli", "crates/frontend", "crates/browser-harness"]

[workspace.package]
version = "0.8.4"
version = "0.8.5"
edition = "2021"
license = "MIT"
repository = "https://github.com/eggp-dev/conn"

[workspace.dependencies]
conn-core = { path = "crates/core", version = "0.8.4" }
conn-frontend = { path = "crates/frontend", version = "0.8.4" }
conn-core = { path = "crates/core", version = "0.8.5" }
conn-frontend = { path = "crates/frontend", version = "0.8.5" }
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "sync", "time", "macros", "signal"] }
portable-pty = "0.8"
vt100 = { path = "vendor/vt100", version = "0.15.2" }
Expand Down
10 changes: 5 additions & 5 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ curl -fsSL https://conn.eggp.dev/install.sh | sh

Rust나 Node.js는 필요 없습니다. 에이전트 연결 도구가 함께 들어 있습니다. 두 방법 모두 아래 다운로드와 같은 서명된 릴리스 파일을 설치하며, 이후에는 앱 안에서 업데이트됩니다. Windows는 설치 파일을 사용하세요.

| 운영체제 | v0.8.4 프리뷰 |
| 운영체제 | v0.8.5 프리뷰 |
|---|---|
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-unknown-linux-gnu-desktop.AppImage) |

Mac 배포 파일은 서명·공증되어 있습니다. Windows 프리뷰는 무서명이며, Linux는 Ubuntu 24.04에서 빌드하며, AppImage는 앱 안에서 업데이트되고 `.deb`는 새 패키지를 설치해 업데이트합니다. Intel Mac 배포는 잠시 중단했습니다. [설치·업데이트 안내](docs/getting-started.ko.md) · [전체 파일·체크섬](https://github.com/eggp-dev/conn/releases/tag/v0.8.4)
Mac 배포 파일은 서명·공증되어 있습니다. Windows 프리뷰는 무서명이며, Linux는 Ubuntu 24.04에서 빌드하며, AppImage는 앱 안에서 업데이트되고 `.deb`는 새 패키지를 설치해 업데이트합니다. Intel Mac 배포는 잠시 중단했습니다. [설치·업데이트 안내](docs/getting-started.ko.md) · [전체 파일·체크섬](https://github.com/eggp-dev/conn/releases/tag/v0.8.5)

## 처음 5분

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ curl -fsSL https://conn.eggp.dev/install.sh | sh

No Rust or Node.js required; the agent connector is included. Both install the same signed release files as the downloads below, and the app updates itself afterwards. Windows: use the installer.

| Platform | v0.8.4 preview |
| Platform | v0.8.5 preview |
|---|---|
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.4/conn-v0.8.4-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.5/conn-v0.8.5-x86_64-unknown-linux-gnu-desktop.AppImage) |

Mac downloads are signed and notarized. Windows previews are unsigned. Linux builds use Ubuntu 24.04, and the AppImage updates in-app while the `.deb` updates by installing a new package; Intel Mac distribution is paused. [Installation and updates](docs/getting-started.md) · [All assets and checksums](https://github.com/eggp-dev/conn/releases/tag/v0.8.4)
Mac downloads are signed and notarized. Windows previews are unsigned. Linux builds use Ubuntu 24.04, and the AppImage updates in-app while the `.deb` updates by installing a new package; Intel Mac distribution is paused. [Installation and updates](docs/getting-started.md) · [All assets and checksums](https://github.com/eggp-dev/conn/releases/tag/v0.8.5)

## First five minutes

Expand Down
1 change: 1 addition & 0 deletions crates/core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ impl Engine {

session.lock().set_output_frame_sink(cfg.output_frame);
session.lock().set_execution_profile(profile);
session.lock().set_ssh_transport(std::path::Path::new(&plan.program).file_stem().is_some_and(|name| name == "ssh"));
#[cfg(unix)]
let integration = Arc::new(parking_lot::Mutex::new(integration));
#[cfg(unix)]
Expand Down
99 changes: 97 additions & 2 deletions crates/core/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ pub struct Session {
surface_id: String,
external_writer_active: bool,
execution_profile: Option<crate::backend::Profile>,
ssh_transport: bool,
ssh_foreground_sequence: Option<u64>,
authority: Authority,
policy: PolicyStore,
approvals: ApprovalQueue,
Expand Down Expand Up @@ -518,6 +520,8 @@ impl Session {
fn with_origin(cfg: SessionConfig, external_private: bool) -> Self {
Self {
external_origin: external_private,
ssh_transport: false,
ssh_foreground_sequence: None,
participation: Participation::new(external_private),
participation_listener: None,
surface_generation: 1,
Expand Down Expand Up @@ -586,10 +590,24 @@ impl Session {
}

pub fn review_required(&self) -> bool {
if self.remote_policy() { return false; }
self.external_origin || self.execution_profile.as_ref().is_some_and(|profile| profile.needs_review())
|| self.ssh_foreground_sequence.is_some()
|| (self.shell_integration.state == "active" && (!self.shell_prompt_confirmed || self.shell_submission_inflight))
}

/// SSH changes where commands and paths are interpreted, not the selected
/// collaboration mode. Use POSIX policy without inspecting the host filesystem.
fn remote_policy(&self) -> bool {
self.execution_profile.as_ref().is_some_and(|profile| {
profile.shell == crate::backend::ShellKind::Posix
&& (profile.backend == crate::backend::BackendKind::Ssh || self.ssh_transport
|| (self.shell_integration.state == "active" && self.ssh_foreground_sequence.is_some()))
})
}

pub(crate) fn set_ssh_transport(&mut self, ssh: bool) { self.ssh_transport = ssh; }

pub fn completion_prompt_ready(&self) -> bool {
self.participation.shared && self.process_alive && self.shell_integration.state == "active" && self.shell_command.is_none()
&& self.shell_prompt_confirmed && !self.shell_submission_inflight
Expand Down Expand Up @@ -641,8 +659,9 @@ impl Session {
if !self.process_alive { return; }
// Lifecycle sequence alone is retained privately. It identifies the outer
// shell's foreground program without retaining its command or arguments.
let matching_end = kind == "end" && self.shell_foreground_sequence.is_some()
&& self.shell_foreground_sequence == text.parse::<u64>().ok();
let ended_sequence = text.parse::<u64>().ok();
let matching_end = kind == "end" && ended_sequence.is_some()
&& (self.shell_foreground_sequence == ended_sequence || self.ssh_foreground_sequence == ended_sequence);
let record_start = self.participation.shared && self.shell_recording_ready && self.shell_recording_armed;
match kind {
"ready" => {
Expand All @@ -654,18 +673,24 @@ impl Session {
}
"start" => {
self.shell_foreground_sequence = Some(sequence);
// Retain only transport identity, including during a private login.
// A word containing "ssh" (or a compound script) is not an SSH launch.
let segments = crate::analysis::analyse_line(text);
self.ssh_foreground_sequence = (segments.len() == 1 && segments[0].command.as_deref() == Some("ssh")).then_some(sequence);
self.shell_prompt_confirmed = false;
self.shell_recording_ready = false;
self.shell_recording_armed = false;
}
"end" if matching_end => {
self.shell_foreground_sequence = None;
self.ssh_foreground_sequence = None;
self.shell_submission_inflight = false;
self.shell_prompt_confirmed = true;
self.shell_recording_ready = self.participation.shared && self.shell_integration.state == "active";
self.shell_recording_armed = false;
}
"gap" | "unavailable" => {
self.shell_integration.state = "unavailable".into();
self.shell_prompt_confirmed = false;
self.shell_recording_ready = false;
self.shell_recording_armed = false;
Expand Down Expand Up @@ -794,6 +819,7 @@ impl Session {
}

fn analyse(&self, cmd: &str) -> crate::policy::LineAnalysis {
if self.remote_policy() { return self.policy.analyse(cmd, None, None); }
if self.review_required() {
let shell = self.execution_profile.as_ref().filter(|p| p.needs_review()).map_or(crate::backend::ShellKind::Custom, |p| p.shell);
return self.policy.analyse_external(cmd, shell);
Expand Down Expand Up @@ -1853,6 +1879,7 @@ impl Session {
if decision == ApprovalDecision::AllowSession {
self.policy.allow_for_session(&req.label);
self.audit.record("human", "session_allow", json!({ "label": req.label }));
self.broadcast(ServerEvent::SessionAllowsChanged { allows: self.policy.session_allows() });
}
Ok(ApprovalInfo { approval_id: req.id, state: req.state, cmd: req.cmd, label: req.label })
}
Expand Down Expand Up @@ -2023,3 +2050,71 @@ impl Session {
&self.screen
}
}


#[cfg(test)]
mod ssh_policy_tests {
use super::*;
use crate::backend::{Profile, ShellKind};

fn session() -> Session {
let mut s = Session::new(SessionConfig {
rows: 24, cols: 80, audit: Audit::null(),
policy: PolicyStore::from_policy(crate::policy::Policy::parse(crate::policy::EXAMPLE_POLICY).unwrap()),
pty_writer: Box::new(Vec::<u8>::new()), master: None,
pacing: Pacing::default(), shell_pid: None,
});
s.set_execution_profile(Profile::local("local".into(), "/bin/bash".into()));
s.set_cwd_override(Some(std::env::temp_dir()));
s.shell_event(1, "ready", "bash", "", "");
s
}

#[test]
fn nested_ssh_uses_remote_rules_until_its_matching_exit() {
let mut s = session();
s.set_shared(false).unwrap();
s.shell_event(2, "start", "/usr/bin/ssh example-host", "/private", "");
s.set_shared(true).unwrap();
assert!(!s.review_required());
let analysis = s.analyse_line("rm file");
assert!(analysis.cwd.is_none());
assert!(matches!(analysis.decision, PolicyDecision::Confirm { .. }));
s.shell_event(3, "end", "99", "", "0");
assert!(s.remote_policy(), "an unrelated end cannot change the target");
s.shell_event(4, "end", "2", "", "0");
assert!(!s.remote_policy());
assert!(s.analyse_line("printf local").cwd.is_some());
}

#[test]
fn lost_ssh_lifecycle_requires_review_without_using_local_paths() {
let mut s = session();
s.shell_event(2, "start", "ssh example-host", "", "");
s.shell_event(4, "gap", "", "", "");
assert!(s.review_required());
let analysis = s.analyse_line("printf remote");
assert!(analysis.cwd.is_none());
assert!(matches!(analysis.decision, PolicyDecision::Confirm { .. }));
s.shell_event(5, "end", "2", "", "0");
assert!(!s.remote_policy());
assert!(!s.review_required());
}

#[test]
fn other_foreground_programs_and_non_posix_ssh_still_require_review() {
for command in ["printf ssh", "ssh host; printf local", "sh", "vim file"] {
let mut s = session();
s.shell_event(2, "start", command, "", "");
assert!(!s.remote_policy(), "{command}");
assert!(s.review_required(), "{command}");
}
let mut s = session();
let mut profile = Profile::local("remote".into(), "pwsh".into());
profile.backend = crate::backend::BackendKind::Ssh;
profile.shell = ShellKind::PowerShell;
s.set_execution_profile(profile);
s.set_ssh_transport(true);
assert!(s.review_required());
}
}
Loading