Skip to content
Open
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
34 changes: 21 additions & 13 deletions docs/berdctl-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ berdctl project create --name demo
The implementation has three layers:

1. CLI: `src-tauri/crates/berdctl/`
Parses flags with clap, prints help, reads the app discovery file, and sends
JSON calls. CLI validation is convenience only.
Parses flags with clap, prints help, reads the private app discovery file,
authenticates each loopback request with its per-broker capability, and
sends JSON calls. CLI validation is convenience only.
2. Broker: `src-tauri/plugins/berdctl/`
Runs a localhost server inside the app, rejects browser-origin requests,
enforces in-flight and timeout limits, and forwards calls to the renderer
without command-specific logic.
Runs a localhost server inside the app, requires the current discovery-file
capability, rejects browser-origin requests, enforces in-flight and timeout
limits, and forwards calls to the renderer without command-specific logic.
3. Renderer registry: `src/features/berdctl/commands/`
Strict-parses args with zod, runs guards, executes through app state, and
returns JSON results. This is the trust boundary because any same-user
process can bypass the CLI and POST to the broker directly.
returns JSON results. This remains the command-policy trust boundary; the
broker capability limits access to processes that can read the owning
user's private discovery file.

## Layer rules

Expand Down Expand Up @@ -87,9 +89,13 @@ belongs in error messages, not generic help text.

## Safety model

v1 has no auth tokens and no confirmation dialogs. That remains acceptable only
while mutations are visible in the UI and either reversible or direct
user-requested product actions, such as creating a session or sending a prompt.
v1 requires a fresh 256-bit bearer capability for every broker start. The
plugin writes it beside the port and generation in the discovery file, with
owner-only directory/file permissions on Unix, and the CLI presents it on
both `/v1/ping` and `/v1/call`. Missing, malformed, wrong, stale, symlinked,
or non-private capability records fail closed. This authenticates possession
of the app-issued session endpoint; it does not replace renderer command
policy or add interactive confirmation dialogs.

Required command properties:

Expand All @@ -108,8 +114,10 @@ piecemeal auth in a command PR.

## Versioning

The broker writes a discovery file with `protocolVersion`, generation, and port.
The CLI verifies it via `/v1/ping` before calls.
The broker writes a private discovery file with `protocolVersion`, generation,
port, and a per-start capability. The CLI authenticates and verifies it via
`/v1/ping` before calls. Requiring that capability is a breaking wire reshape,
so the authenticated surface starts at protocol version 5.

Breaking wire reshapes must bump all three constants:

Expand Down Expand Up @@ -138,4 +146,4 @@ bump. Adding a command or optional field is not a wire reshape.
| safety metadata complete | berdctl command tests |

Review-only rules: single renderer dispatch point, detecting breaking wire
reshapes, and product judgment for no-auth command eligibility.
reshapes, and product judgment for command eligibility under capability authentication.
11 changes: 8 additions & 3 deletions scripts/windows/CI-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
# Runs the Rust checks that only a real Windows host can exercise: the
# `managed_node` / `managed_acp_tools` module tests (including the native gate
# that downloads and executes the real pinned Node ZIP), security-sensitive
# Windows process-launch tests, plus Windows clippy in the default and
# app-feature configurations. Invoked through `just ci-windows` for local and
# release validation.
# Windows process-launch and berdctl discovery tests, plus Windows clippy in the
# default and app-feature configurations. Invoked through `just ci-windows` for
# local and release validation.
$ErrorActionPreference = "Stop"
trap {
Write-Host $_.Exception.Message -ForegroundColor Red
Expand Down Expand Up @@ -58,6 +58,11 @@ Invoke-CargoCheck -ArgumentList @(
"test", "--lib", "commands::system::tests::windows_chrome_launch_"
) -Label "cargo test Windows Chrome launch"

# Exercise the DACL and atomic-publication paths on a native Windows filesystem.
Invoke-CargoCheck -ArgumentList @(
"test", "-p", "tauri-plugin-berdctl", "--features", "server", "discovery::tests::"
) -Label "cargo test berdctl discovery"

# Clippy compiles both configurations, so separate `cargo check` calls only
# repeat the same compile coverage.
Invoke-CargoCheck -ArgumentList @(
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/Cargo.lock

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

8 changes: 5 additions & 3 deletions src-tauri/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ The Tauri 2 shell: the app crate (`src/`), the berdctl workspace crates

The CLI embeds the contract artifacts (`crates/berdctl/api-surface.json` +
`cli-surface.json`) and builds its clap tree at startup. It locates the
broker through the `BERDCTL_LOCK` discovery file, verifies
`protocolVersion`/generation via `GET /v1/ping`, and sends
broker through the `BERDCTL_LOCK` discovery file, reads its per-start
capability, verifies `protocolVersion`/generation through an authenticated
`GET /v1/ping`, and sends authenticated
`POST /v1/call {"command", "args"}`. The broker forwards to the renderer
over Tauri IPC (`berdctl:request` event out, `submit_result` back).
Command dispatch, zod validation, guards, and execution live in the
Expand All @@ -41,7 +42,8 @@ capability grants a permission allowing that command.
window.

This ACL gates webview → Rust IPC only; the localhost HTTP side is governed
separately (discovery file, header rejection, global caps).
separately by the owner-private discovery capability, browser/DNS-rebinding
header rejection, and global caps.

Stock Tauri 2 plugin layout. Docs:
[Plugin Development](https://v2.tauri.app/develop/plugins/),
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/crates/berdctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
ureq = { version = "3", features = ["json"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_Threading",
] }

[features]
default = []
block-feedback = []
2 changes: 1 addition & 1 deletion src-tauri/crates/berdctl/api-surface-feedback.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$comment": "GENERATED FILE — do not hand-edit; run `pnpm generate:berdctl-contract`. Client-neutral wire surface of the Berd desktop app's control API: POST /v1/call {\"command\": \"<group>\", \"args\": {\"action\": \"<action>\", ...fields}} against the loopback endpoint in the berdctl discovery file. protocolVersion mirrors PROTOCOL_VERSION in both discovery.rs copies (berdctl and plugin crate tests pin them equal). Per action: description, fields (flat wire model: name, required, kind, values, description, bounds), and schema (JSON Schema 2020-12 of the args object, minus the action discriminator). Derived from the authoritative zod schemas in the colocated command modules (src/features/berdctl/commands/impl/*.ts); the renderer registry's strict zod parse — not this file — is the trust boundary. vitest asserts freshness (src/features/berdctl/__tests__/apiSurface.test.ts).",
"protocolVersion": 4,
"protocolVersion": 5,
"groups": {
"sessions": {
"description": "Manage the user's chat sessions: create (fire-and-forget, on any installed agent harness), send, open, list, get, rename, move, move to group, clear project, fork, archive.",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/berdctl/api-surface.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$comment": "GENERATED FILE — do not hand-edit; run `pnpm generate:berdctl-contract`. Client-neutral wire surface of the Berd desktop app's control API: POST /v1/call {\"command\": \"<group>\", \"args\": {\"action\": \"<action>\", ...fields}} against the loopback endpoint in the berdctl discovery file. protocolVersion mirrors PROTOCOL_VERSION in both discovery.rs copies (berdctl and plugin crate tests pin them equal). Per action: description, fields (flat wire model: name, required, kind, values, description, bounds), and schema (JSON Schema 2020-12 of the args object, minus the action discriminator). Derived from the authoritative zod schemas in the colocated command modules (src/features/berdctl/commands/impl/*.ts); the renderer registry's strict zod parse — not this file — is the trust boundary. vitest asserts freshness (src/features/berdctl/__tests__/apiSurface.test.ts).",
"protocolVersion": 4,
"protocolVersion": 5,
"groups": {
"sessions": {
"description": "Manage the user's chat sessions: create (fire-and-forget, on any installed agent harness), send, open, list, get, rename, move, move to group, clear project, fork, archive.",
Expand Down
Loading