Briglia CLI
GitHub
Project

Security & signed releases

Every binary Briglia downloads — the installer, updates, and the phone app — is authenticated with an Ed25519 signature against a key pinned inside the client, before a single byte of it runs. Hosting is not trusted; the signature is.

How a release is authenticated

  1. Releases are published as immutable GitHub Releases of permaevidence/briglia-cli and permaevidence/briglia-ut. Once published, assets cannot be changed or deleted and the tag cannot be reused.
  2. Each release carries a signed envelope, manifest.sig.json: a manifest (version, monotonic sequence number, publication and expiry dates, and for every platform the exact asset URL, size and SHA-256) plus an Ed25519 signature over a domain-separated input — ada-release-envelope-v1 \0 channel \0 keyId \0 payload. (The format name is historical — it predates the project's rename — and is deliberately unchanged: it is part of what every envelope signs.)
  3. Clients verify before trusting. The installer script, the briglia binary (briglia upgrade) and the Ubuntu Touch app each carry the release public key and refuse the release unless: the signature verifies, the format and channel match, the manifest is within its validity window, every asset lives under the release's own GitHub location, and the downloaded bytes match the signed size and SHA-256 exactly (checked while streaming — Content-Length is not trusted, and there is a hard byte bound).
  4. Anti-rollback. The sequence must be at least the minimum baked into the client and at least the highest sequence that device has ever accepted for the same channel — so nobody can feed you an old, vulnerable release even with a valid signature.
  5. The publisher re-verifies its own work. The CI pipeline (CLI) and the local publisher (app) fetch the public release back, authenticate it with the committed public key, and require byte identity with what they signed before declaring the release done. An hourly independent watcher re-verifies both channels and alerts on any mismatch.
What this protects against

A compromised or misconfigured download host, a tampered asset, a forged “latest” manifest, and downgrade attacks. It does not protect against a compromised signing key — see the notes on key custody below — nor is it a substitute for keeping your own machine and provider accounts secure.

Release keys

ChannelKey IDEd25519 public key (raw, hex)
Briglia CLIbriglia-cli-release-v1-94d967bae0867c2e621031636aa2bb2edb64a58f2f72de7bc3559b08d717c79b4251f8b1e35b8a95
Ubuntu Touch appbriglia-ut-release-v1-7bb0163ac16c5cb3cdfa5dba857ad9276f2630c0c7028b53ea9933cc969e69f0a1cff4727ff0b7dc

The same values are pinned in the installer, in the briglia binary, in the app (py/release_verify.py) and in this website's download page. Compare them: if this page and the repositories ever disagree, trust neither and report it.

Renamed from Ada CLI. These are the same two keys that signed every Ada CLI and Ada companion release; only their IDs changed, because an ID is derived from the channel name plus the key's fingerprint (the 94d967… / 7bb016… halves are unchanged). The channels are now briglia-cli and briglia-ut, and the sequence numbers continue rather than restart — the first Briglia CLI release is sequence 60, the first Briglia app release sequence 2 — so no envelope ever exists whose sequence regresses across the rename. Clients from before the rename refuse the new channel by design (see Updates).

Key custody: the private keys never touch the build machines' disks in plaintext outside the signing step — the CLI key lives only in a protected GitHub Actions environment that can be used exclusively by version tags, the app key on the maintainer's machine with mode 0600 and encrypted offline backups. The signed manifests expire (≈180 days) so a stale channel cannot be replayed forever.

Verify a release yourself

You need OpenSSL 3 and python3. This reproduces what the clients do for the Briglia CLI channel:

# 1. Fetch the signed envelope of the latest release
curl -fsSL -o manifest.sig.json \
  https://github.com/permaevidence/briglia-cli/releases/latest/download/manifest.sig.json

# 2. Rebuild the pinned public key as DER (SPKI prefix + raw key)
python3 - <<'PY'
import base64, json, sys
env = json.load(open("manifest.sig.json"))
assert env["format"] == "ada-release-envelope-v1" and env["channel"] == "briglia-cli"
assert env["keyId"] == "briglia-cli-release-v1-94d967bae0867c2e", env["keyId"]
open("pub.der", "wb").write(bytes.fromhex("302a300506032b6570032100" + "621031636aa2bb2edb64a58f2f72de7bc3559b08d717c79b4251f8b1e35b8a95"))
payload = base64.b64decode(env["payload"], validate=True)
open("msg.bin", "wb").write(b"ada-release-envelope-v1\0briglia-cli\0" + env["keyId"].encode() + b"\0" + payload)
open("sig.bin", "wb").write(base64.b64decode(env["signature"], validate=True))
open("manifest.json", "wb").write(payload)
PY

# 3. Verify the Ed25519 signature
openssl pkeyutl -verify -rawin -pubin -keyform DER -inkey pub.der -in msg.bin -sigfile sig.bin
# → "Signature Verified Successfully"

# 4. Read the authenticated manifest, then check an asset against it
python3 -c 'import json; m=json.load(open("manifest.json")); print(m["version"], m["sequence"], m["expires"]); print(m["platforms"]["macos-arm64"])'
curl -fsSL -o briglia-macos-arm64.tar.gz "$(python3 -c 'import json; print(json.load(open("manifest.json"))["platforms"]["macos-arm64"]["url"])')"
shasum -a 256 briglia-macos-arm64.tar.gz   # must equal the sha256 in the manifest; size must match too

For the Ubuntu Touch app use channel briglia-ut, the app key above, the envelope at github.com/permaevidence/briglia-ut/releases/latest/download/manifest.sig.json, and the single click platform entry.

The installer

The one-line installer verifies the same envelope with the same pinned key using the system openssl (probed against an RFC 8032 known vector first, so an unsuitable OpenSSL is never mistaken for a bad signature) and python3, then downloads the version-pinned tarball and enforces its signed size and SHA-256. On a system with neither tool it says so plainly and falls back to HTTPS plus the published SHA-256 sidecar — first-install authenticity then rests on the TLS connection to github.com, and briglia upgrade is fully verified from then on.

Where trust state lives on your device

  • Briglia CLI records the highest accepted sequence per channel and location in ~/.local/share/briglia/release_trust.json, under a cross-process lock, monotonically; a corrupt file is reported and the floor baked into the binary still applies.
  • The Ubuntu Touch app keeps the same floor in ~/.config/briglia-ut/release_trust.json (mode 0600). Settings shows which Ed25519 backend the phone uses — the system OpenSSL, or a built-in verifier; both are proven against the RFC 8032 known-answer vectors before first use.

The agent itself

  • Briglia has a real shell and full access to your user's files, with no approval prompts by design — it must work while you are away. Run it under an account and on a machine you are comfortable entrusting; a dedicated user or a Raspberry Pi is ideal for an always-on setup.
  • Secrets live in ~/.config/briglia/secrets.json (mode 0600, multi-process safe) and are injected into commands only when needed; the Telegram bot token and any labelled service keys are redacted from command output before the model sees it, and Telegram network-error diagnostics carry only the error code and message, never the request URL (0.2.16); API keys are deliberately left visible so the agent can use them when debugging.
  • Everything under ~/.config/briglia and ~/.local/share/briglia is owner-only (directories 0700, files 0600; your projects/ folder and a userdata toolchain/ are left alone). That keeps other accounts on the same machine out, not processes running as you; every start re-tightens stragglers and briglia doctor reports anything wider.
  • The browser-automation server (Playwright MCP) is installed from a lockfile committed with the source — exact versions and integrity hashes, no install scripts — once per version into an immutable directory, verified before mcp.json is switched to it, and reused on every later start without contacting the npm registry. Playwright changes only when a release bumps the lockfile; briglia doctor shows the version in use. Before v0.2.6 every start ran npx @playwright/mcp@latest.
  • Text arriving from the outside world (web pages, emails, documents) is marked as untrusted before it reaches the model, and messages you send while Briglia is working are authenticated with per-delivery nonces so a document cannot impersonate you.
  • Backups (.mind archives) restore scheduled shell watchers in a paused, quarantined state; you approve each one explicitly with /resumewatcher.
  • /deleteuserdata quiesces all work, revokes connected email credentials and wipes local data, reporting honestly if anything could not be removed.
  • Requests to OpenCode Go carry an opaque per-conversation identifier (x-opencode-session, required by OpenCode since September 2026) and requests to OpenRouter an optional one (x-session-id). It lets the provider correlate the requests of one conversation or one subagent session under one key, which is how it keeps the prompt cache warm; it is an HMAC output — no key material, session ID or device identity can be recovered from it, and different API keys produce unrelated values. The state behind it (affinity.json) is harness-owned, never exported in a backup, wiped by /deleteuserdata, and /rotateaffinity gives every conversation a fresh identity on demand. Retention on the provider side follows OpenCode Go's per-model privacy table.

Quick setup (the local page)

briglia quicksetup serves its page from the briglia process on 127.0.0.1 (an ephemeral port) for one session. Keys go from the page to that process, which sends each to its provider for verification and stores them in secrets.json; they never reach briglia.dev. The printed link carries a single-use exchange token (five-minute lifetime) that becomes an HttpOnly, SameSite=Strict cookie; pressing Enter in the terminal revokes every earlier token and cookie. Requests are accepted only with the exact Host, a same-origin Origin, a custom header no cross-site form can send, and that cookie; the page has no inline scripts, no storage, and a strict Content-Security-Policy.

What this does not protect against: a process running as you can already read secrets.json, so the page grants it nothing new; root can capture loopback traffic and read everything anyway. Another local account can reach the port and can, for the few milliseconds the link is on the browser opener's argument list, read the exchange token — if it uses it first it obtains a cookie until you press Enter, and the terminal tells you the link was already used by something else. The system steps run as journaled child processes behind a start gate; a step whose processes cannot be confirmed gone blocks every further step until you recover explicitly.

Reporting a vulnerability

Please report privately through GitHub's advisory form — Security → Report a vulnerability on briglia-cli or briglia-ut. Don't open a public issue for anything you believe is a security problem. Include the version, platform/channel and reproduction steps; expect an acknowledgement within a few days. Only the latest release is supported: fixes ship as a new release, not as backports. The full policy is in each repository's SECURITY.md.