Skip to content
Draft
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
37 changes: 26 additions & 11 deletions docs/berdctl-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ 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
Parses flags with clap, prints help, reads non-secret endpoint discovery,
obtains the broker-generation capability over authenticated local IPC, 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.
Admits only kernel-identified descendants of this Berd instance's owned
`goosed` tree, then requires the issued capability on the loopback server,
rejects browser-origin requests, enforces in-flight and timeout limits, and
forwards calls 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 admission boundary prevents unrelated same-user processes from
directly reaching it.

## Layer rules

Expand Down Expand Up @@ -87,9 +90,18 @@ 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 publishes no bearer in the discovery file. Discovery contains only the
loopback port, generation, protocol version, and local bootstrap address. The
CLI connects to that local IPC endpoint; the broker obtains the peer PID from
the kernel and admits it only when it is a descendant of the exact app-owned
`goosed` process on Unix or a member of the exact retained no-breakaway Job
Object on Windows. Only then does it return the per-broker 256-bit capability,
which the CLI presents on `/v1/ping` and `/v1/call`.

This blocks direct broker use by unrelated same-user processes. It deliberately
does not claim protection from same-user malware that can inspect or inject
into an admitted descendant. Closing that stronger boundary requires OS
isolation or interactive user authorization, not another ambient bearer.

Required command properties:

Expand All @@ -108,8 +120,11 @@ 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 non-secret local bootstrap address. The CLI obtains the bearer only
after peer-process admission, then authenticates and verifies `/v1/ping` before
calls. This authenticated bootstrap is a breaking wire reshape, so the surface
starts at protocol version 5.

Breaking wire reshapes must bump all three constants:

Expand Down
15 changes: 10 additions & 5 deletions scripts/windows/CI-Windows.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Native x64 MSVC CI gate for the managed Node runtime + npm ACP bridge.
#
# 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), plus Windows clippy in
# the default and app-feature configurations. Invoked through `just ci-windows`
# for local and release validation.
# Runs the Rust checks that only a real Windows host can exercise: berdctl's
# Job Object / named-pipe authorization tests, the `managed_node` /
# `managed_acp_tools` module tests (including the native gate that downloads and
# executes the real pinned Node ZIP), 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 @@ -45,6 +46,10 @@ Invoke-CargoCheck -ArgumentList @("fmt", "--check") -Label "cargo fmt --check"
# Both managed-Node modules share this test-name prefix. Run them in one process
# so the Windows test binary is linked once. The live ACP bridge install has no
# equivalent macOS/Linux CI coverage, so leave it for targeted manual runs.
Invoke-CargoCheck -ArgumentList @(
"test", "-p", "tauri-plugin-berdctl", "--features", "server"
) -Label "cargo test berdctl plugin"

Invoke-CargoCheck -ArgumentList @(
"test", "--lib", "services::managed_", "--", "--skip",
"native_gate_installs_and_launches_a_bridge_by_bare_name"
Expand Down
41 changes: 41 additions & 0 deletions src-tauri/Cargo.lock

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

11 changes: 7 additions & 4 deletions src-tauri/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ 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
`POST /v1/call {"command", "args"}`. The broker forwards to the renderer
broker through the `BERDCTL_LOCK` discovery file, connects to its non-secret
local bootstrap address, and receives a capability only after the broker admits
the kernel-reported process as a descendant of this app's owned `goosed` tree.
It then verifies `protocolVersion`/generation through 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
renderer registry (`src/features/berdctl/`). The two crates share no code;
Expand All @@ -41,7 +43,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 process-authenticated capability bootstrap, browser/DNS-rebinding
header rejection, and global caps.

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

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

[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