Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e628edc
feat(security): add permission mode selector with first-run modal and…
siracusa5 Apr 6, 2026
9455efc
fix(security): validate tool entries and fix empty-list permission fa…
siracusa5 Apr 7, 2026
26bd141
fix(ts): suppress unused id prop warning in ModeCard
siracusa5 Apr 7, 2026
3f84593
feat(security): commit missing files for auto-detection and single-in…
siracusa5 Apr 7, 2026
e424f5b
Merge branch 'feat/card-agent' into worktree-agent-a3c6a885
siracusa5 Apr 7, 2026
bef35c4
chore: scaffold agent-server package
siracusa5 Apr 7, 2026
aa5cae0
feat(agent-server): add shared types
siracusa5 Apr 7, 2026
8834e8e
feat(agent-server): add auth module with API key + OAuth fallback
siracusa5 Apr 7, 2026
0ec76cb
feat(agent-server): add SQLite checkpointer
siracusa5 Apr 7, 2026
091ad08
feat(tauri): add agent-server launchd service management
siracusa5 Apr 7, 2026
6a3cebf
feat(board-server): add phase and thread_id to task execution type
siracusa5 Apr 7, 2026
2a0dda7
feat(desktop): add agent-api client
siracusa5 Apr 7, 2026
0c31f89
feat(agent-server): add file system tool registry with bash denylist
siracusa5 Apr 7, 2026
edbdf0a
feat(desktop): add useAgentEvents hook
siracusa5 Apr 7, 2026
8e39200
feat(agent-server): add board tools via MCP adapters
siracusa5 Apr 7, 2026
acc35b1
feat(desktop): add AgentExecutionBadge component
siracusa5 Apr 7, 2026
b0dfbe5
feat(agent-server): add LangGraph state and graph skeleton
siracusa5 Apr 7, 2026
d0fb395
feat(agent-server): implement spec node
siracusa5 Apr 7, 2026
5c8b249
feat(agent-server): implement planning node with board subtask creation
siracusa5 Apr 7, 2026
c34ba19
feat(agent-server): implement coding node with tool-calling agent loop
siracusa5 Apr 7, 2026
d921e98
feat(agent-server): implement QA review and fixing nodes
siracusa5 Apr 7, 2026
be96360
feat(agent-server): add thread manager and event broadcaster
siracusa5 Apr 7, 2026
b521c48
feat(agent-server): implement agent runner wiring graph to broadcaster
siracusa5 Apr 7, 2026
d868349
feat(agent-server): add HTTP routes (start/stop/steer/status)
siracusa5 Apr 7, 2026
beca441
feat(agent-server): add WebSocket server for agent event streaming
siracusa5 Apr 7, 2026
8727f16
feat(desktop): add tab scaffold to task detail dialog
siracusa5 Apr 7, 2026
f1d28fc
feat(desktop): add LogsTab component ported from mock
siracusa5 Apr 7, 2026
2b6ec13
feat(desktop): add SubtasksTab component
siracusa5 Apr 7, 2026
ed7bd66
feat(agent-server): verify server starts and responds
siracusa5 Apr 7, 2026
25761ae
feat(desktop): add OverviewTab with phase timeline, steering, and con…
siracusa5 Apr 7, 2026
ef37790
feat(desktop): add FilesTab and DiffTab components
siracusa5 Apr 7, 2026
9ea2e47
fix(desktop): remove unused React import in AgentExecutionBadge
siracusa5 Apr 7, 2026
1d71ea1
Merge branch 'worktree-agent-a94165d6' into feat/card-agent
siracusa5 Apr 7, 2026
9f9a486
feat(desktop): wire agent execution routing and clean up dist artifacts
siracusa5 Apr 7, 2026
78864f9
fix(agent-server): address code review findings
siracusa5 Apr 7, 2026
16fd0a5
feat(agent-server): pause/resume, live card progress, steering feedback
siracusa5 Apr 7, 2026
9e53c3f
fix(security): harden agent-server auth, bash allowlist, and input va…
siracusa5 Apr 11, 2026
9285f82
fix: address all review findings from PR #86
siracusa5 Apr 11, 2026
45b23d5
fix(tauri): register agent-server commands in AppManifest
siracusa5 Apr 11, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ apps/cli/dist/
# Board server
packages/board-server/dist/

# Agent server
packages/agent-server/dist/

# Auto Claude data directory
.auto-claude/

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- **Desktop: Comparator v4** — Rebuilt from terminal multiplexer to structured evaluation workbench with 4-phase workflow (Setup → Execution → Results → Judge), session history rail, SQLite persistence, and shared Comparator types
- **Marketplace: Ratings and reviews** — install counts, trust tiers, and user ratings on plugin detail pages.
- **CLI: `detect` and `init` commands** — `harness-kit detect` shows which AI coding platforms are active in the current directory; `harness-kit init` scaffolds a new `harness.yaml` interactively.
- **Desktop: Board agent execution** — per-card autonomous coding runs via a new `packages/agent-server` LangGraph StateGraph (spec → planning → coding → qa_review → qa_fixing). Live phase/progress streaming over WebSocket, structured log view with color-coded tool blocks, phase-grouped subtask list, steering input, handoff/pause/stop controls. SQLite checkpointing at `~/.harness/board/agent-checkpoints.sqlite` — agent state survives app restarts. Board-aware context via MCP at port 4800.
- **Desktop: Agent server service** — Tauri launchd service management for `packages/agent-server` (mirrors board-server). `Install`, `Start`, and `Restart` commands registered as Tauri IPC.
- **Security: Agent server auth** — shared-secret token at `~/.harness-kit/agent-server.token` (mode 0600). All HTTP endpoints require `Authorization: Bearer <token>`; WebSocket upgrade requires `?token=<secret>`. Bash tool uses an explicit allowlist of known-safe dev commands (replaces denylist) with additional blocks for pipe-to-interpreter and absolute-path `rm` patterns.
- **Desktop: Permission mode selector** — Security → Permissions now has a Task Execution Mode section with three selectable modes: **Skip All** (`--dangerously-skip-permissions`, default), **Auto** (`--permission-mode auto`, requires Claude team/enterprise/API), and **Allowed Tools** (`--allowedTools <list>`, user-curated checklist). Mode and tool list persist in localStorage and apply globally or per-harness via an expandable overrides section. A one-time first-run modal explains the active mode before the first task runs and links to the settings page. A slide-in Tool Approval panel lets users manage the allowed tools list mid-session from within the terminals view.
- **Desktop: Security → Permissions redesign** — Permission mode cards with icons, flag badges, and clear selected state. Chip remove buttons now use SVG icons. Improved section hierarchy with a clean divider between execution mode and Claude settings.json controls.

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ harness-kit/
│ ├── shared/ ← shared TypeScript types used across apps
│ ├── ui/ ← shared React components
│ ├── board-server/ ← WebSocket + HTTP server for the Kanban board and Roadmap/Competitor Analysis features
│ ├── agent-server/ ← LangGraph execution engine for per-card agent runs (port 4801)
│ ├── chat-relay/ ← self-hosted WebSocket relay for team chat
│ └── membrain/ ← git submodule (siracusa5/membrain) — graph-based memory, excluded from pnpm workspace
├── apps/ ← end-user applications
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ A Tauri desktop companion that brings the harness concept to a native UI.
- **Observatory** — live session dashboard with stats and transcripts
- **Comparator** -- structured evaluation workbench: configure harnesses, run side-by-side comparisons, review file diffs, and judge results across a 4-phase workflow
- **Harness editor** — inline editing with custom profiles
- **Board** — kanban project board with real-time Claude-to-web sync
- **Board** — kanban project board with real-time Claude-to-web sync; per-card agent execution via LangGraph with live phase/progress streaming, subtask tracking, steering, pause/resume, and tool-level permission controls
- **Roadmap** — AI-driven product roadmap with competitor analysis, generated via Claude
- **Parity** — cross-platform feature parity tracking across AI coding tools
- **Security** — permissions editor, secrets management, and audit logging
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ fn main() {
"board_server_install",
"board_server_start",
"board_server_restart",
// Agent server
"get_agent_server_token",
"agent_server_check_installed",
"agent_server_install",
"agent_server_start",
"agent_server_restart",
// membrain
"membrain_check_installed",
"membrain_start",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@
"allow-board-server-install",
"allow-board-server-start",
"allow-board-server-restart",
"allow-get-agent-server-token",
"allow-agent-server-check-installed",
"allow-agent-server-install",
"allow-agent-server-start",
"allow-agent-server-restart",
"allow-membrain-check-installed",
"allow-membrain-start",
"allow-membrain-stop",
Expand Down
244 changes: 244 additions & 0 deletions apps/desktop/src-tauri/src/agent_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
use std::net::TcpListener;
use std::process::Command;
use std::path::PathBuf;

const PORT: u16 = 4801;
const PLIST_LABEL: &str = "com.harness-kit.agent-server";
const MAX_TRAVERSAL_DEPTH: usize = 10;

pub struct AgentServerState;

impl AgentServerState {
pub fn new() -> Self {
Self
}

/// Returns true if the agent server appears to be running.
pub fn check(&self) -> bool {
port_in_use(PORT)
}
}

fn port_in_use(port: u16) -> bool {
TcpListener::bind(("127.0.0.1", port)).is_err()
}

fn plist_path() -> PathBuf {
dirs::home_dir()
.expect("No home directory")
.join("Library/LaunchAgents/com.harness-kit.agent-server.plist")
}

fn current_uid() -> String {
let output = Command::new("id").arg("-u").output().expect("failed to get uid");
String::from_utf8_lossy(&output.stdout).trim().to_string()
}

fn find_node() -> Result<String, String> {
let output = Command::new("which")
.arg("node")
.output()
.map_err(|_| "Could not locate node — install Node.js first".to_string())?;
if !output.status.success() {
return Err("node not found — install Node.js first".to_string());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

fn find_server_dir() -> Result<PathBuf, String> {
// Traverse up from the executable to locate packages/agent-server (capped depth)
let exe = std::env::current_exe()
.map_err(|_| "Could not determine application location".to_string())?;
let mut dir = exe.parent().map(|p| p.to_path_buf());
let mut depth = 0;
while let Some(d) = dir {
if depth >= MAX_TRAVERSAL_DEPTH {
break;
}
let candidate = d.join("packages/agent-server/dist/index.js");
if candidate.exists() {
return Ok(d.join("packages/agent-server"));
}
dir = d.parent().map(|p| p.to_path_buf());
depth += 1;
}
Err("Agent server not found — run `cd packages/agent-server && pnpm build` first".to_string())
}

fn xml_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}

fn generate_plist(node_path: &str, server_dir: &str, log_dir: &str) -> String {
let node_path = xml_escape(node_path);
let server_dir = xml_escape(server_dir);
let log_dir = xml_escape(log_dir);
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{PLIST_LABEL}</string>

<key>ProgramArguments</key>
<array>
<string>{node_path}</string>
<string>{server_dir}/dist/index.js</string>
</array>

<key>WorkingDirectory</key>
<string>{server_dir}</string>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>

<key>ThrottleInterval</key>
<integer>5</integer>

<key>StandardOutPath</key>
<string>{log_dir}/agent-server.log</string>

<key>StandardErrorPath</key>
<string>{log_dir}/agent-server.log</string>

<key>EnvironmentVariables</key>
<dict>
<key>AGENT_SERVER_PORT</key>
<string>4801</string>
</dict>
</dict>
</plist>"#
)
}

const TOKEN_FILE: &str = ".harness-kit/agent-server.token";

#[tauri::command]
pub fn get_agent_server_token() -> Result<String, String> {
let path = dirs::home_dir()
.ok_or("No home directory")?
.join(TOKEN_FILE);
std::fs::read_to_string(&path)
.map(|s| s.trim().to_string())
.map_err(|_| "Agent server token not found — start the agent server first".to_string())
}

#[tauri::command]
pub fn agent_server_check_installed() -> bool {
plist_path().exists()
}

#[tauri::command]
pub fn agent_server_install() -> Result<String, String> {
let node_path = find_node()?;
let server_dir = find_server_dir()?;
let server_dir_str = server_dir.to_string_lossy().to_string();

let log_dir = dirs::home_dir()
.expect("No home directory")
.join(".harness-kit/logs");
std::fs::create_dir_all(&log_dir)
.map_err(|_| "Failed to create log directory".to_string())?;
let log_dir_str = log_dir.to_string_lossy().to_string();

let plist_content = generate_plist(&node_path, &server_dir_str, &log_dir_str);
let plist = plist_path();

std::fs::write(&plist, &plist_content)
.map_err(|_| "Failed to write service configuration".to_string())?;

// Restrict plist to owner-only access
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&plist, std::fs::Permissions::from_mode(0o600));
}

let uid = current_uid();
let domain = format!("gui/{uid}");

// Bootstrap the service (idempotent — ignores "already loaded" error)
let _ = Command::new("launchctl")
.args(["bootstrap", &domain, &plist.to_string_lossy()])
.output();

// Enable the service
let _ = Command::new("launchctl")
.args(["enable", &format!("{domain}/{PLIST_LABEL}")])
.output();

// Kickstart to run immediately
Command::new("launchctl")
.args(["kickstart", &format!("{domain}/{PLIST_LABEL}")])
.output()
.map_err(|_| "Failed to start the agent server service".to_string())?;

Ok("Agent server installed and started".to_string())
}

#[tauri::command]
pub fn agent_server_start() -> Result<String, String> {
let uid = current_uid();
let domain = format!("gui/{uid}");
let plist = plist_path();

// Bootstrap (idempotent)
let _ = Command::new("launchctl")
.args(["bootstrap", &domain, &plist.to_string_lossy()])
.output();

// Kickstart
Command::new("launchctl")
.args(["kickstart", &format!("{domain}/{PLIST_LABEL}")])
.output()
.map_err(|_| "Failed to start the agent server service".to_string())?;

Ok("Agent server started".to_string())
}

#[tauri::command]
pub fn agent_server_restart() -> Result<String, String> {
let uid = current_uid();
let domain = format!("gui/{uid}");

// Kickstart with -k flag to kill existing instance first
Command::new("launchctl")
.args(["kickstart", "-k", &format!("{domain}/{PLIST_LABEL}")])
.output()
.map_err(|_| "Failed to restart the agent server service".to_string())?;

Ok("Agent server restarted".to_string())
}

#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpListener;

#[test]
fn port_in_use_returns_false_for_free_port() {
// Bind to 0 to get an ephemeral port, then drop to free it
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
assert!(!port_in_use(port));
}

#[test]
fn port_in_use_returns_true_for_occupied_port() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
// listener still bound — port is occupied
assert!(port_in_use(port));
}
}
15 changes: 15 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod ai;
mod agent_server;
mod commands;
mod db;
mod board_server;
Expand All @@ -15,6 +16,7 @@ use tauri::{LogicalSize, Manager};
use ai::client::OllamaState;
use commands::terminal::TerminalState;
use board_server::BoardServerState;
use agent_server::AgentServerState;
use membrain_commands::MembrainServerState;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
Expand All @@ -39,6 +41,7 @@ pub fn run() {
.manage(TerminalState::default())
.manage(database)
.manage(BoardServerState::new())
.manage(AgentServerState::new())
.manage(commands::relay::LocalRelay(tokio::sync::Mutex::new(None)))
.manage(MembrainServerState::new())
.manage(OllamaState::new("http://localhost:11434"))
Expand Down Expand Up @@ -154,6 +157,12 @@ pub fn run() {
board_server::board_server_install,
board_server::board_server_start,
board_server::board_server_restart,
// Agent server
agent_server::get_agent_server_token,
agent_server::agent_server_check_installed,
agent_server::agent_server_install,
agent_server::agent_server_start,
agent_server::agent_server_restart,
// membrain
membrain_commands::membrain_check_installed,
membrain_commands::membrain_start,
Expand Down Expand Up @@ -198,6 +207,12 @@ pub fn run() {
} else {
eprintln!("[board-server] not running — install with: pnpm board:install");
}
let agent_state = app.state::<AgentServerState>();
if agent_state.check() {
eprintln!("[agent-server] running on :{}", 4801);
} else {
eprintln!("[agent-server] not running — install with: pnpm agent:install");
}
// membrain server starts on-demand when the user navigates to the
// Memory section (via useMembrainServerReady hook), not at app launch.
// This avoids opening a network listener until the Labs feature is enabled.
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,16 @@ body {
opacity: 1 !important;
}

/* ── Agent execution animations (ported from docs/plans/agent-ui-mock.html) ── */
@keyframes agent-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(.8); }
}

@keyframes agent-spin {
to { transform: rotate(360deg); }
}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
Expand Down
Loading
Loading