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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[workspace]
members = [".", "predicate-authority-desktop"]
resolver = "2"

[package]
name = "predicate-authorityd"
version = "0.7.2"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ cargo build --release
./target/release/predicate-authorityd version
```

### Desktop companion

Optional launcher and policy UI (egui):

```bash
cargo build -p predicate-authority-desktop --release
./target/release/predicate-authority-desktop
```

See [`predicate-authority-desktop/README.md`](predicate-authority-desktop/README.md).

### Install via pip (Python SDK)

```bash
Expand Down
67 changes: 62 additions & 5 deletions docs/sidecar-user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ A comprehensive guide to installing, configuring, and operating the Predicate Au
7. [Policy Files](#policy-files)
8. [API Reference](#api-reference)
9. [Terminal Dashboard](#terminal-dashboard)
10. [Delegation Chains](#delegation-chains)
11. [Security Features (Phase 5)](#security-features-phase-5)
12. [Secret Injection](#secret-injection)
13. [Troubleshooting](#troubleshooting)
14. [Demos](#demos)
10. [Desktop companion (GUI)](#desktop-companion-gui)
11. [Delegation Chains](#delegation-chains)
12. [Security Features (Phase 5)](#security-features-phase-5)
13. [Secret Injection](#secret-injection)
14. [Troubleshooting](#troubleshooting)
15. [Demos](#demos)

---

Expand Down Expand Up @@ -859,6 +860,62 @@ When you quit the dashboard, a session summary is printed:

---

## Desktop companion (GUI)

The repository includes an optional **desktop application** (`predicate-authority-desktop`) built with [egui](https://github.com/emilk/egui). It is a **local companion**: it can start and stop the sidecar process, tail stdout/stderr, poll `/health` and `/status`, edit and validate policy (same `policy_loader` as the daemon), reload policy over HTTP, and manage paths and launch flags. It does **not** replace the embedded **Web UI** for the per-request ALLOW/DENY event stream; use the browser dashboard when Web UI is enabled for that view.

For a full feature list, see [`predicate-authority-desktop/README.md`](../predicate-authority-desktop/README.md).

### Prerequisites

- [Rust](https://rustup.rs/) (stable) and Cargo
- Same clone as the sidecar: the desktop crate lives in the **workspace root** next to `predicate-authorityd` (see root `Cargo.toml` `members`).

### Build

From the `rust-predicate-authorityd` directory (workspace root):

```bash
cargo build -p predicate-authority-desktop --release
```

The binary is written to:

- **Linux / macOS:** `target/release/predicate-authority-desktop`
- **Windows:** `target\release\predicate-authority-desktop.exe`

Debug build (faster compile):

```bash
cargo build -p predicate-authority-desktop
```

### Run

From the same workspace root:

```bash
cargo run -p predicate-authority-desktop
```

Or run the release binary directly:

```bash
./target/release/predicate-authority-desktop
```

### First-time setup in the app

1. Open the **Config** tab.
2. Set **Sidecar binary** to your `predicate-authorityd` executable (e.g. `target/release/predicate-authorityd` after `cargo build --release` on the main package).
3. Set **Policy file** to your JSON or YAML policy path.
4. Optionally set **Config TOML**, **Host** / **Port**, **Web UI**, **Audit mode**, and **Policy reload secret** (must match `--policy-reload-secret` on the daemon if you use reload).
5. Use **Home** → **Start sidecar**. When Web UI is enabled, use **Open dashboard in browser** after the log line shows the URL.

If `predicate-authorityd` is in the **same directory** as the desktop binary, the app can offer **Use as binary** for quick setup.

---

## Delegation Chains

Delegation allows agents to pass limited permissions to sub-agents.
Expand Down
28 changes: 28 additions & 0 deletions predicate-authority-desktop/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "predicate-authority-desktop"
version = "0.1.0"
edition = "2021"
description = "Desktop companion for predicate-authorityd (launcher, policy editor, reload, diagnostics)"
license = "MIT"
publish = false

[[bin]]
name = "predicate-authority-desktop"
path = "src/main.rs"

[dependencies]
predicate-authorityd = { path = ".." }

eframe = { version = "0.29", default-features = true }
egui = "0.29"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
regex = "1"
rfd = "0.15"
open = "5"
serde_yaml = "0.9"
keyring = "3"
similar = "2"
zip = { version = "2", default-features = false, features = ["deflate"] }
dirs = "5"
24 changes: 24 additions & 0 deletions predicate-authority-desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Predicate Authority Desktop

GUI companion for [`predicate-authorityd`](../): start/stop/**restart** the sidecar, tail logs, poll `/health` and `/status`, open the Web UI from the printed URL, and edit policies via **templates**, a **simple rule builder** (duplicate / reorder), or **raw JSON/YAML** with validation via the same `policy_loader` as the daemon. **Import/export** policies, **diff vs last successful reload**, optional **keychain** for the reload secret, **`check-config`** against your TOML, and **diagnostics ZIP** export from the Logs tab. **Startup presets** (saved under the OS config directory) can restore paths/host/flags on launch. If `predicate-authorityd` sits **next to** the desktop binary, the app offers **Use as binary** and can run **`--version`**. **Reload** uses `POST /policy/reload` with optional `Authorization: Bearer` when you configure a reload secret.

## Build

From the workspace root (`rust-predicate-authorityd/`):

```bash
cargo build -p predicate-authority-desktop --release
```

Binary: `target/release/predicate-authority-desktop`

## Run

1. Set **Sidecar binary** to your `predicate-authorityd` executable (e.g. `target/release/predicate-authorityd`).
2. Set **Policy file** to a path (e.g. `./policy.json`).
3. Optionally set **Config TOML**, **reload secret** (must match `--policy-reload-secret` on the daemon), host/port, **Web UI**, **Audit mode**.
4. **Start sidecar**, then **Open dashboard** once the log line `Web UI enabled: http://...` appears (if Web UI is on).

## Workspace layout

This crate lives next to the daemon in a Cargo workspace; the root `Cargo.toml` lists `members = [".", "predicate-authority-desktop"]`.
90 changes: 90 additions & 0 deletions predicate-authority-desktop/src/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Blocking HTTP calls to the local sidecar.

use predicate_authorityd::models::PolicyRule;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub mode: String,
pub uptime_s: u64,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StatusResponse {
pub status: String,
pub mode: String,
pub identity_mode: String,
pub uptime_s: u64,
pub rule_count: usize,
pub total_allowed: u64,
pub total_denied: u64,
pub event_count: usize,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct PolicyReloadResponse {
pub success: bool,
pub rule_count: usize,
pub message: String,
}

fn client() -> Result<reqwest::blocking::Client, String> {
reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| e.to_string())
}

pub fn base_url(host: &str, port: &str) -> String {
format!("http://{}:{}", host.trim(), port.trim())
}

pub fn fetch_health(host: &str, port: &str) -> Result<HealthResponse, String> {
let url = format!("{}/health", base_url(host, port));
let r = client()?.get(url).send().map_err(|e| e.to_string())?;
if !r.status().is_success() {
return Err(format!("GET /health -> {}", r.status()));
}
r.json().map_err(|e| e.to_string())
}

pub fn fetch_status(host: &str, port: &str) -> Result<StatusResponse, String> {
let url = format!("{}/status", base_url(host, port));
let r = client()?.get(url).send().map_err(|e| e.to_string())?;
if !r.status().is_success() {
return Err(format!("GET /status -> {}", r.status()));
}
r.json().map_err(|e| e.to_string())
}

pub fn policy_reload(
host: &str,
port: &str,
rules: &[PolicyRule],
bearer_secret: Option<&str>,
) -> Result<PolicyReloadResponse, String> {
let url = format!("{}/policy/reload", base_url(host, port));
let body = serde_json::json!({ "rules": rules });
let client = client()?;
let mut req = client.post(url).json(&body);
if let Some(s) = bearer_secret {
if !s.trim().is_empty() {
req = req.header("Authorization", format!("Bearer {}", s.trim()));
}
}
let r = req.send().map_err(|e| e.to_string())?;
let status = r.status();
let text = r.text().unwrap_or_default();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err(format!(
"401 Unauthorized: policy reload requires a matching bearer secret ({text})"
));
}
if !status.is_success() {
return Err(format!("POST /policy/reload -> {status}: {text}"));
}
serde_json::from_str(&text).map_err(|e| format!("invalid reload JSON: {e}: {text}"))
}
Loading
Loading