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.9"
"version": "0.9.0"
}
]
}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## Unreleased

## 0.9.0 — Preview · 2026-10-26

- Add **Reduce environment checks**, off by default. The local owner can explicitly reduce environment-only review for one exact connection in the current session for 15 minutes, with a visible target-unverified countdown and Stop control. The decision stays in memory and cannot be created or extended by an agent.
- Limit reduction to documented simple query commands; other commands and unsupported syntax keep review. Preserve risk confirmation, deny rules, Privacy, participation, collaboration modes, control leases and existing input recovery. Pending approval cards are never automatically approved. Existing category allowances do not turn this option into blanket command approval.
- Recheck queued commands before PTY input. Expiry, stop, disconnect/reconnect, sharing, profile/policy or observed transport changes and human takeover revoke the decision; ordinary lease expiry preserves the decision but still blocks input without a lease. Running processes are not killed.
- Final execution targets and internal transitions remain unverified. Nested remote host changes may be undetectable; this feature does not verify target identity.

한국어: **환경 확인 줄이기**를 추가했습니다. 기본 OFF이며 로컬 소유자가 직접 동의하면 현재 세션·정확한 연결의 지원되는 단순 조회 명령에 한해 환경 확인만 15분 동안 줄입니다. 대상 미검증·남은 시간·종료 버튼을 표시하고 메모리에만 유지하며, 에이전트는 생성·연장할 수 없습니다. 위험 명령·명시 deny·해석 불가 문법·Privacy·참여 권한·기존 모드·제어 lease·입력 복구는 유지하고, 대기 승인 카드를 자동 승인하지 않습니다. 큐 명령을 실제 입력 직전에 재검증하며 만료·철회·문맥 변경·사용자 takeover 시 실행하지 않습니다. lease 자연 만료는 grant를 보존하지만 입력에는 유효 lease가 필요합니다. 중첩 원격 연결의 최종 호스트 전환을 감지하지 못할 수 있으며 대상 신원 검증을 보장하지 않습니다.

Validation and limits: automated core regressions cover fixed monotonic expiry, connection scope, nonrenewing reads, owner-only creation, command risk and syntax, pending approvals, retained input, queued-command revalidation, policy/profile/sharing changes, and lease expiry versus human takeover. Production Linux Chromium UI/PTY/MCP flows verify acknowledgement, countdown, stop, reload and reconnect in disposable sessions. Native packaging, signing and exact-release artifact checks are recorded separately. macOS/Windows interactive behavior and installed-app upgrades were not exercised locally. Local native compilation requires GTK development libraries that are unavailable in this execution environment; full-repository fmt and strict Clippy also report pre-existing formatting/lint issues.

한국어: 고정 단조시계 만료·연결 범위·조회 비갱신·소유자 전용 생성·위험/문법 확인·대기 승인·입력 잔존·큐 재검증·정책/프로필/공유 변경·lease 자연 만료와 인간 takeover 차이를 자동 검증했습니다. 격리된 Linux Chromium 실제 UI/PTY/MCP에서 동의·남은 시간·종료·재진입·재접속을 검증하며, 배포 산출물·서명 검증은 별도 기록합니다. Mac/Windows 대화형 검증·설치 앱 업그레이드는 로컬에서 수행하지 않았습니다. 실행 환경에 GTK 개발 라이브러리가 없어 로컬 네이티브 빌드는 제한되며, 전체 fmt·엄격 Clippy에는 기존 포맷/린트 문제가 남아 있습니다.

## 0.8.9 — Preview · 2026-09-30

- Keep agent connections accessible through a compact session-header summary and a keyboard-accessible list. Preserve distinct connections with repeated names, show full long names, and keep the close control visible.
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/web", "fronte
default-members = ["crates/core", "crates/cli", "crates/frontend", "crates/web"]

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

[workspace.dependencies]
conn-core = { path = "crates/core", version = "0.8.9" }
conn-frontend = { path = "crates/frontend", version = "0.8.9" }
conn-core = { path = "crates/core", version = "0.9.0" }
conn-frontend = { path = "crates/frontend", version = "0.9.0" }
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
12 changes: 7 additions & 5 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Conn은 지금 쓰는 AI 에이전트와 함께 쓰는 터미널입니다. 에

*내 서버에는 내가 로그인합니다. 에이전트가 캐시를 통째로 지우겠다고 합니다. 안 된다고 하고 내가 직접 고치면, 에이전트가 나머지를 끝냅니다.* [전체 영상 보기](https://conn.eggp.dev/ko/#film) ([파일](docs/assets/conn-remote-ko.mp4)) · [English video](https://conn.eggp.dev/#film) · [한 번에 이어서 찍었습니다: 촬영 기록](media/demo/RECORDING-REMOTE.md)

판별할 수 없는 실행 환경의 기본 매명령 확인은 유지됩니다. 로컬 소유자가 **환경 확인 줄이기**를 명시적으로 켜면 현재 세션·정확한 연결의 지원 조회 명령에만 15분 동안 적용하며, 다른 명령과 해석 불가 문법은 계속 확인합니다. [범위와 한계](docs/faq.ko.md#환경-확인-줄이기는-무엇인가요).

## 설치

```sh
Expand All @@ -29,13 +31,13 @@ curl -fsSL https://conn.eggp.dev/install.sh | sh

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

| 운영체제 | v0.8.9 프리뷰 |
| 운영체제 | v0.9.0 프리뷰 |
|---|---|
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-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.9)
Mac 배포 파일은 서명·공증되어 있습니다. Windows 프리뷰는 무서명이며, Linux는 Ubuntu 24.04에서 빌드하며, AppImage는 앱 안에서 업데이트되고 `.deb`는 새 패키지를 설치해 업데이트합니다. Intel Mac 배포는 잠시 중단했습니다. [설치·업데이트 안내](docs/getting-started.ko.md) · [전체 파일·체크섬](https://github.com/eggp-dev/conn/releases/tag/v0.9.0)

## 처음 5분

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Conn is a terminal you share with the AI agent you already use. The agent picks

*You log in to your server. Your agent asks to delete the whole cache. You say no, type the fix yourself, and it finishes the job.* [Watch the full film](https://conn.eggp.dev/#film) ([file](docs/assets/conn-remote-en.mp4)) · [한국어 영상](https://conn.eggp.dev/ko/#film) · [One continuous take: how it was recorded](media/demo/RECORDING-REMOTE.md)

The default per-command review in unverified environments is unchanged. The local owner can opt into **Reduce environment checks** for supported simple queries on one exact session/connection for 15 minutes; other commands, risks and unsupported syntax still require review. [Scope and limits](docs/policy.md#reduce-environment-checks).

## Install

```sh
Expand All @@ -29,13 +31,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.9 preview |
| Platform | v0.9.0 preview |
|---|---|
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.9/conn-v0.8.9-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.9.0/conn-v0.9.0-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.9)
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.9.0)

## First five minutes

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/agent_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub(crate) fn dispatch_locked(
}
if method == "status" {
return Ok(
json!({"controller": s.status().controller, "mode": s.mode(), "effectiveMode": s.effective_mode(), "attended": s.attended(), "shared": !s.is_private(), "surfaceAvailable": s.status().surface_available }),
json!({"environmentReview": s.environment_review_status(Some(conn), std::time::Instant::now()), "controller": s.status().controller, "mode": s.mode(), "effectiveMode": s.effective_mode(), "attended": s.attended(), "shared": !s.is_private(), "surfaceAvailable": s.status().surface_available }),
);
}
s.note_connection_activity(conn);
Expand Down
147 changes: 143 additions & 4 deletions crates/core/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ pub struct LineAnalysis {
/// Set when the line was denied for mixing a dangerous segment with others.
#[serde(rename = "isolationViolation")]
pub isolation_violation: bool,
/// Environment uncertainty alone would require confirmation for this line.
#[serde(rename = "blanketConfirm")]
pub blanket_confirm: bool,
}

fn inspect_target(path: &std::path::Path, protected: bool) -> TargetInfo {
Expand Down Expand Up @@ -483,7 +486,7 @@ impl PolicyStore {
isolation_violation = true;
worst = Decision::Deny { label: "run dangerous commands alone".into() };
}
LineAnalysis { cwd: cwd.map(|c| c.display().to_string()), segments: verdicts, decision: worst, isolation_violation }
LineAnalysis { cwd: cwd.map(|c| c.display().to_string()), segments: verdicts, decision: worst, isolation_violation, blanket_confirm: false }
}

pub fn require_intent(&self) -> bool {
Expand All @@ -494,11 +497,21 @@ impl PolicyStore {
/// Preserve deny rules, but never claim these contexts are automatically verified.
/// This review is per command and cannot be lifted by a session-wide allowance.
pub fn analyse_external(&self, line: &str, shell: crate::backend::ShellKind) -> LineAnalysis {
self.analyse_external_with_environment(line, shell, false)
}

/// Session grants may remove only the environment overlay. Risk rules and
/// unsupported grammar never use category allowances here.
pub fn analyse_external_with_environment(&self, line: &str, shell: crate::backend::ShellKind, reduce: bool) -> LineAnalysis {
use crate::backend::ShellKind;
let segments = if shell == ShellKind::Posix { crate::analysis::analyse_line(line) }
else { external_segments(line, shell) };
let mut verdicts = Vec::new();
let mut worst = Decision::Confirm { label: "Review command in this shell/remote environment".into() };
let mut worst = Decision::Allow;
let analysable = shell == ShellKind::Posix && !segments.is_empty()
&& simple_external_line(line)
&& segments.iter().all(|s| s.command.is_some() && s.opaque.is_none()
&& reducible_query(s));
for segment in &segments {
let mut decision = self.policy.evaluate_segment(segment);
// PowerShell and cmd executable names are case insensitive.
Expand All @@ -514,16 +527,142 @@ impl PolicyStore {
match &decision {
Decision::Deny { .. } => worst = decision.clone(),
Decision::Confirm { .. } if !matches!(worst, Decision::Deny { .. }) => worst = decision.clone(),
Decision::Allow => decision = Decision::Confirm { label: "Review command in this shell/remote environment".into() },
Decision::Allow => {},
_ => {}
}
verdicts.push(SegmentVerdict {text:segment.text.clone(),command:segment.command.clone(),opaque:Some("Target syntax or filesystem requires human review".into()),decision,targets:vec![]});
}
let isolation_violation = self.policy.isolate_dangerous && segments.len()>1;
if isolation_violation { worst=Decision::Deny {label:"Run commands separately in this shell/remote environment".into()}; }
LineAnalysis {cwd:None,segments:verdicts,decision:worst,isolation_violation}
let command_allowed = matches!(worst, Decision::Allow);
let blanket_confirm = command_allowed && analysable;
if command_allowed && (!reduce || !analysable) {
worst = Decision::Confirm { label: "Review command in this shell/remote environment".into() };
}
LineAnalysis {cwd:None,segments:verdicts,decision:worst,isolation_violation,blanket_confirm}
}

}

/// Fail closed on unknown executables and option grammars. Remote protected
/// targets cannot be checked against the host, so mutating/path-sensitive commands
/// and arbitrary launchers never qualify merely because a risk rule missed them.
fn reducible_query(segment: &crate::analysis::Segment) -> bool {
const QUERIES: &[&str] = &[
":", "true", "false", "pwd", "echo", "printf", "ls", "cat", "head", "tail", "wc", "grep",
"fgrep", "egrep", "cut", "tr", "cmp", "readlink", "realpath", "basename", "dirname",
"uname", "id", "whoami", "uptime", "df", "du", "free", "ps",
];
let Some(command) = segment.command.as_deref() else {
return false;
};
if !segment.wrappers.is_empty() || !QUERIES.contains(&command) {
return false;
}
// Leading assignments can replace command lookup or install shell hooks.
if segment
.argv
.first()
.and_then(|word| word.split_once('='))
.is_some_and(|(key, _)| {
!key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
})
{
return false;
}
if command != "printf" {
return true;
}
// printf can assign variables (-v/%n) and evaluate arithmetic conversions.
// Only literal text, %s and %% are supported query formats.
if segment
.args
.first()
.is_some_and(|arg| arg.starts_with("-v"))
{
return false;
}
let index = usize::from(segment.args.first().is_some_and(|arg| arg == "--"));
let Some(format) = segment.args.get(index) else {
return true;
};
let mut conversion = false;
for ch in format.chars() {
if conversion {
if !matches!(ch, 's' | '%') {
return false;
}
conversion = false;
} else if ch == '%' {
conversion = true;
}
}
!conversion
}

/// Deliberately smaller than a shell grammar: incomplete quoting, expansion,
/// compound syntax and reserved words always keep their command review.
fn simple_external_line(line: &str) -> bool {
let mut quote = None;
let mut escaped = false;
for c in line.chars() {
if escaped {
escaped = false;
continue;
}
if quote == Some('\'') {
if c == '\'' {
quote = None;
}
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if quote == Some('"') {
if c == '"' {
quote = None;
} else if matches!(c, '$' | '`' | '!' | '\n' | '\r') {
return false;
}
continue;
}
if matches!(c, '\'' | '"') {
quote = Some(c);
} else if ";|&(){}<>`$#*?[]!~^%\n\r".contains(c) {
return false;
}
}
let reserved = [
"if",
"then",
"else",
"elif",
"fi",
"for",
"while",
"until",
"do",
"done",
"case",
"esac",
"function",
"select",
"in",
"coproc",
"repeat",
"foreach",
"end",
"switch",
"endsw",
"nocorrect",
"noglob",
"!",
"[[",
"]]",
];
quote.is_none() && !escaped && !line.split_whitespace().any(|word| reserved.contains(&word))
}

/// Small dialect-aware lexer for explicit command rules. It does not certify scripts.
Expand Down
Loading
Loading