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.2"
"version": "0.8.3"
}
]
}
27 changes: 25 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,22 +150,45 @@ jobs:
- name: Check macOS scripting metadata and compile external automation examples
if: runner.os == 'macOS'
run: python3 scripts/check_macos_scripting.py --app frontends/tauri/src-tauri/target/debug/bundle/macos/Conn.app
collaboration:
name: Collaboration browser flows
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- uses: ./.github/actions/setup
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
if: inputs.ref == ''
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install the lockfile-pinned test browser
run: npx playwright install chromium --with-deps
working-directory: frontends/tauri
- run: cargo build --locked -p conn-browser-harness
- run: npm run test:collaboration
working-directory: frontends/tauri
- run: npm run test:collaboration:harness
working-directory: frontends/tauri
required:
name: Required checks
if: always()
needs: [scope, quality, core, desktop]
needs: [scope, quality, core, desktop, collaboration]
runs-on: ubuntu-24.04
steps:
- name: Every job in scope passed
env:
SCOPE: ${{ needs.scope.result }}
QUALITY: ${{ needs.quality.result }}
COLLABORATION: ${{ needs.collaboration.result }}
CORE: ${{ needs.core.result }}
DESKTOP: ${{ needs.desktop.result }}
RUST_RUNNERS: ${{ needs.scope.outputs.rust }}
DESKTOP_RUNNERS: ${{ needs.scope.outputs.desktop }}
run: |
# A job may be skipped only because the scope left it nothing to do.
expected() { if [ "$1" = '[]' ]; then test "$2" = skipped; else test "$2" = success; fi; }
test "$SCOPE" = success && test "$QUALITY" = success
test "$SCOPE" = success && test "$QUALITY" = success && test "$COLLABORATION" = success
expected "$RUST_RUNNERS" "$CORE" && expected "$DESKTOP_RUNNERS" "$DESKTOP"
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.8.3 — Preview · 2026-09-21

Validation: 246 Rust tests, 46 frontend tests, 78 release tests, production-App browser fixtures, the real Rust Harness collaboration flow and a real OpenSSH authentication/sharing regression passed locally. Interactive testing of this patch on macOS was not performed because the test Mac is offline; Linux/Windows native GUI acceptance and installed-app upgrade coverage also remain open. Release CI and signing checks are reported separately.

한국어: 로컬 Rust 246개·프런트엔드 46개·릴리스 78개 테스트, 실제 App 브라우저 검증·Rust Harness 협업 흐름·실제 OpenSSH 인증 및 공유 회귀 검증을 통과했습니다. 테스트 Mac이 꺼져 있어 이번 패치의 macOS 대화형 검증은 하지 않았습니다. Linux·Windows 네이티브 GUI 및 설치된 앱의 업그레이드 검증도 남아 있습니다.

- Connection approval stays actionable in private, empty, preparing and Settings views. Failed decisions remain retryable, and late snapshots cannot resurrect resolved requests.
- Sharing validates input, live participants, history resources and the selected revision before revoking external input. Failed transitions preserve the running shell and writer; stale selections require reloading.
- Centralize connection admission, cross-session control transitions, participation cleanup and typed UI state. Owner grants and hand-backs enforce one lease per connection; explicit take/release and process exit cancel pending work. Cards, keyboard and palette decisions share their request state.
- Add required browser collaboration regressions using the production App and real Rust Harness. [Implementation and validation](docs/collaboration-refactor-results.md); native platform acceptance remains open.

한국어: 비공개 화면에서도 연결 승인을 처리하고, 공유 실패 시 기존 셸과 외부 입력 권한을 유지합니다. 연결·참여·제어권·UI 상태의 책임을 분리했으며, 오래된 공유 선택은 다시 확인하게 합니다. [구현·검증 결과](docs/collaboration-refactor-results.ko.md)에 플랫폼별 남은 검증을 구분했습니다.

- Snapshots no longer carry the constant `imageUnavailable: true` field, a remnant of the optional PNG snapshots removed in 0.8.0. Snapshots remain text only.
- Removed the native OpenAI command suggestions preview, its provider settings and the OS credential-store key storage. Themes and the extension registry stay. A key saved earlier remains in the OS credential store under `dev.eggp.conn.model-provider` until you delete it there.

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.2"
version = "0.8.3"
edition = "2021"
license = "MIT"
repository = "https://github.com/eggp-dev/conn"

[workspace.dependencies]
conn-core = { path = "crates/core", version = "0.8.2" }
conn-frontend = { path = "crates/frontend", version = "0.8.2" }
conn-core = { path = "crates/core", version = "0.8.3" }
conn-frontend = { path = "crates/frontend", version = "0.8.3" }
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.2 프리뷰 |
| 운영체제 | v0.8.3 프리뷰 |
|---|---|
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Mac용 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [설치 파일 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [.deb 다운로드](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-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.2)
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.3)

## 처음 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.2 preview |
| Platform | v0.8.3 preview |
|---|---|
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.2/conn-v0.8.2-x86_64-unknown-linux-gnu-desktop.AppImage) |
| macOS · Apple Silicon | [Download for Mac](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-aarch64-apple-darwin-desktop.dmg) |
| Windows · x64 | [Download installer](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-x86_64-pc-windows-msvc-setup.exe) |
| Ubuntu · x64 | [Download .deb](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-x86_64-unknown-linux-gnu-desktop.deb) · [AppImage](https://github.com/eggp-dev/conn/releases/download/v0.8.3/conn-v0.8.3-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.2)
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.3)

## First five minutes

Expand Down
189 changes: 189 additions & 0 deletions crates/core/src/agent_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//! Agent session operations. Owner-only operations never enter this service.
use crate::{
affordance::Actor,
authority::ConnId,
protocol::RpcError,
session::{ConnKind, Session, SessionError, SharedSession},
};
use serde_json::{json, Value};

/// Agents get one generic refusal so they cannot probe for private sessions. The
/// owner can ask why: `CONN_TRACE_REFUSALS=1` prints the cause to stderr. Reasons
/// and identifiers only, never terminal content.
pub(crate) fn trace_refusal(reason: &str, conn: ConnId, session: &str) {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *ENABLED
.get_or_init(|| std::env::var_os("CONN_TRACE_REFUSALS").is_some_and(|v| !v.is_empty()))
{
eprintln!(
"conn refusal {} conn={conn} session={} reason={reason}",
chrono::Local::now().format("%H:%M:%S%.3f"),
trace_label(session)
);
}
}

/// The named session or tab comes from the agent. Keep it to one bounded, escaped
/// token so a request cannot forge trace lines or drive the owner's terminal.
pub(crate) fn trace_label(requested: &str) -> String {
let mut label: String = requested
.chars()
.take(96)
.flat_map(char::escape_default)
.collect();
if requested.chars().nth(96).is_some() {
label.push('…');
}
label
}

pub(crate) fn session_unavailable() -> RpcError {
RpcError {
code: "not_found".into(),
message: "session unavailable".into(),
}
}

pub(crate) fn str_param(params: &Value, key: &str) -> Option<String> {
params
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}

/// Synchronous dispatch. Runs under the session lock; must not block.
pub fn dispatch(
session: &SharedSession,
conn: ConnId,
method: &str,
params: &Value,
) -> Result<Value, RpcError> {
dispatch_locked(&mut session.lock(), conn, method, params)
}

pub(crate) fn dispatch_locked(
s: &mut Session,
conn: ConnId,
method: &str,
params: &Value,
) -> Result<Value, RpcError> {
if s.is_private() {
trace_refusal("session is private", conn, "-");
return Err(session_unavailable());
}
if !s.participant_allowed(conn) {
trace_refusal("connection is not a selected participant", conn, "-");
return Err(session_unavailable());
}
if s.conn_kind(conn) != Some(ConnKind::Agent) {
return Err(RpcError {
code: "owner_required".into(),
message: "public connections must identify as agents".into(),
});
}
if !matches!(
method,
"affordances"
| "snapshot"
| "request_control"
| "release_control"
| "type"
| "send_key"
| "interrupt"
| "request_attention"
| "check_approval"
| "control_request_state"
| "proposal_state"
| "exec_state"
| "status"
) {
return Err(RpcError {
code: "owner_required".into(),
message: "this operation belongs to the local owner UI".into(),
});
}
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 }),
);
}
s.note_connection_activity(conn);
let actor = Actor::Agent { conn };
if matches!(
method,
"check_approval" | "control_request_state" | "proposal_state" | "exec_state"
) {
let key = match method {
"check_approval" => "approvalId",
"control_request_state" => "requestId",
"proposal_state" => "proposalId",
_ => "execId",
};
if !s.owns_request(conn, method, params[key].as_str().unwrap_or_default()) {
return Err(session_unavailable());
}
}
let r: Result<Value, SessionError> = match method {
"affordances" => Ok(json!(s
.affordances(actor)
.iter()
.map(|a| a.name())
.collect::<Vec<_>>())),
"snapshot" => s.snapshot(actor).map(|v| serde_json::to_value(v).unwrap()),
"request_control" => s
.agent_request_control_original(conn, params.clone())
.map(|o| serde_json::to_value(o).unwrap()),
"control_request_state" => {
let id = str_param(params, "requestId").unwrap_or_default();
match s.control_request_state(&id) {
Some(state) => Ok(json!({ "requestId": id, "state": state })),
None => Err(SessionError::NotFound(id)),
}
}
"proposal_state" => {
let id = str_param(params, "proposalId").unwrap_or_default();
match s.proposal_state(&id) {
Some(state) => Ok(json!({ "proposalId": id, "state": state })),
None => Err(SessionError::NotFound(id)),
}
}
"release_control" => s
.agent_release_control(conn)
.map(|_| json!({ "released": true })),
"type" => {
let text = str_param(params, "text").unwrap_or_default();
s.agent_type(conn, &text)
.map(|_| json!({ "typed": text.chars().count() }))
}
"send_key" => {
let key = str_param(params, "key").unwrap_or_default();
let intent = str_param(params, "intent");
s.agent_send_key_with(conn, &key, intent)
.map(|r| serde_json::to_value(r).unwrap())
}
"interrupt" => s
.agent_interrupt(conn)
.map(|_| json!({ "interrupted": true })),
"check_approval" => {
let id = str_param(params, "approvalId").unwrap_or_default();
s.check_approval(&id)
.map(|a| serde_json::to_value(a).unwrap())
}
"exec_state" => {
let id = str_param(params, "execId").unwrap_or_default();
match s.exec_state(&id) {
Some((state, reason)) => {
Ok(json!({ "execId": id, "state": state, "reason": reason }))
}
None => Err(SessionError::NotFound(id)),
}
}
"request_attention" => s
.agent_request_attention(conn, str_param(params, "reason"))
.map(|_| json!({ "requested": true })),
other => Err(SessionError::InvalidInput(format!(
"unknown method '{other}'"
))),
};
r.map_err(RpcError::from)
}
Loading