Watch
1
0
Fork
You've already forked RedFlag
0

raf: record the standalone pacman boundary

Pacman now has one local envelope path from Desktop intent to a joined helper receipt.

Name the same-host authority limit, missing fleet delivery, and unsupported Arch advisory mapping without painting them green.
This commit is contained in:
Fimeg 2026-09-01 14:14:00 -04:00
commit 213918f1a2
9 changed files with 543 additions and 369 deletions

View file

@ -4,7 +4,7 @@ I'm **Casey Tunturi** — Fimeg in the community. Systems architect, 25+ years o
## Why RedFlag is free
RedFlag is my resume piece. It will never be monetized no pro tier, no cloud edition, no per-agent pricing. The update manager is part of your attack surface, and everyone deserves one that treats it that way — not just organizations with an RMM budget. The code is MIT, the architecture is documented in the open, and that's the whole business model: there isn't one.
RedFlag is my resume piece. It will never be monetized: no pro tier, no cloud edition, no per-agent pricing. The update manager is part of your attack surface. Everyone deserves one that treats it that way, not just organizations with an RMM budget. The code is AGPL-3.0, the architecture is documented in the open, and that's the whole business model: there isn't one.
If you're evaluating whether I can build the thing you need built, this repository is the interview. Read the code, read the architecture docs, run it against your fleet.

View file

@ -9,20 +9,23 @@ understand the shape of the system, then follow the links into the detailed docs
## What RedFlag Is
A self-hosted update management platform for homelabs and small MSPs. Centralized
visibility and control over software updates across Linux, Windows, and Docker — with
a cryptographic supply chain gate that most commercial RMM tools don't attempt.
A machine knowledge and operations system with a self-hosted fleet surface. Agents
report operating-system, process, socket, service, container, software, update, and
security state. RedFlag connects those facts to human approval, signed authority,
privileged execution, and durable lifecycle history instead of treating inspection and
mutation as separate products.
---
## The Two Capability Tiers
## Operating Surfaces and Authority Tiers
### Tier 1: Update Management
### Tier 1: Machine Observation and Fleet Lifecycle
Agents register with a one-time token and a hardware fingerprint (TOFU). The server
issues Ed25519-signed commands; agents verify signatures, check nonces, reject replays.
Pull-based polling (5 min default, rapid mode available). Subsystem scanning across
apt, dnf, winget, WUA, and Docker.
apt, dnf, pacman, winget, WUA, and Docker sits beside on-demand process/socket
inspection and local machine telemetry.
Packages move through a server-owned state machine (`pending` through `installed` or
`failed`) with typed transitions and guarded UPDATEs — no free-form string jumps. A
@ -86,12 +89,32 @@ role-gated minting is just "anyone can get a root shell."
- [components/06-session-broker](components/06-session-broker.md) — design of record: grant format,
trust chain, audit trail, sequence diagram, scope variants
### Process Explorer
### RedFlag Desktop
The native Qt/QML Desktop is the local-machine surface. It holds no credential or
package-manager authority and speaks only to the Agent's local socket. It presents live
resources, processes, connections, storage, services, containers, installed software,
updates, security posture, and history, and may ask a standalone Agent to run the same
gate-and-helper authority chain.
Fleet-enrolled Agents refuse local minting. Standalone mode owns a stable local identity,
local scans, APT/DNF capability approval, and pacman `MutationEnvelope` approval. It does
not recreate an off-host authority boundary; its exact limits are published in
[security/06-standalone-authority](security/06-standalone-authority.md).
**Architecture docs:**
- [components/05-desktop](components/05-desktop.md) — native structure, Agent IPC, and local approval
- [security/06-standalone-authority](security/06-standalone-authority.md) — same-host trust boundary
### Process Explorer and Software Ownership
On-demand `/proc` filesystem scanning for process inventory and drill-down detail.
Triggered when a user opens the Processes tab — no background broadcasting.
25+ fields per process (osquery parity) plus 7 related data types (open files,
sockets, pipes, environment keys, memory map, namespaces, listening ports).
25+ fields per process plus open files, sockets, pipes, environment keys, memory maps,
namespaces, and listening ports. Linux drill-down also reads effective capabilities and
cgroup ownership, attributes a process to a systemd unit or container where the kernel
provides it, and joins the executable back to its installed package when a supported
package manager can prove ownership.
Data collection caps are server-controlled via `ProcessExplorerConfig` (Settings →
Process Explorer) and delivered to agents on check-in. Listening ports use socket
@ -105,28 +128,34 @@ inode correlation against `/proc/net/tcp` — not system-wide assignment.
## Architectural Boundaries
### Agent is a Stateless Executor
### Fleet Lifecycle Is Server-Owned
The agent receives commands, executes them, and reports results. It does not track
lifecycle states. The server owns every state transition. The agent's only autonomous
decisions are: verify this signature, check this nonce, reject this replay.
In fleet mode the Agent receives commands, observes the host, executes authorized work,
and reports results. It does not own update lifecycle states; the Server owns every
transition. Signature, nonce, replay, target, and helper verdicts are local enforcement
decisions. Standalone mode is an explicit exception to server dependence, not to those
checks: it keeps local observations and runs a bounded local scan/approval loop without
inventing a fleet lifecycle.
### Mutation Only Through the Helper
### Gated Mutation Only Through the Helper
On capability-gated ecosystems (dnf, apt), the agent cannot run install commands
directly. All mutation flows through `consumer.go``sudo systemd-run --wait` with
token/result files → `redflag-helper`. The agent holds zero sudo for installs.
Discovery (scan, dry-run, hash-resolve) runs unprivileged through `DiscoveryRunner`.
On capability-gated ecosystems, the Agent cannot run install commands directly. APT and
DNF flow through `consumer.go` and a capability token. Standalone pacman flows through a
signed `MutationEnvelope` whose exact archives and detached signatures are verified
before mint and again before execution. Both end at a fixed `redflag-helper` invocation;
the Agent holds no package-manager sudo. Discovery and resolution stay unprivileged.
### Two Execution Paths
### Current Execution Paths
- **Capability gate** (dnf, apt): token minted at approval → agent polls for tokens →
helper verifies + executes → agent reports receipt. No install command issued.
- **Standalone pacman envelope**: Agent resolves and hashes the official-repository
transaction → local root helper validates custody and signs → execute mode revalidates
and runs one fixed pacman plan → joined receipt returns locally.
- **Legacy command** (docker, winget, windows_update): signed command → agent executes
directly via type-asserted installer methods → reports via ReportLog.
The legacy path is a known gap — the gate design covers these ecosystems but
implementation is deferred.
Fleet pacman envelope delivery and the remaining legacy ecosystems are known gaps.
### Six Load-Bearing Constraints
@ -163,6 +192,11 @@ From `security/05-supply-chain-gate.md` — do not regress these:
- **Helper network isolation**: the current `systemd-run` unit retains host network access; complete local artifact custody and a private network boundary are not built
- **Signer in-process**: key encapsulated in SigningService, minter is the only caller — but true process isolation not built
- **Legacy ecosystems ungated**: docker, winget, windows_update still direct-mutation
- **Fleet pacman seam**: standalone pacman envelopes execute; the Server does not yet mint and deliver the same envelope to fleet Agents
- **Standalone transition**: local startup and approval work, but standalone-to-fleet join is deliberately refused until local-key retirement and trust replacement are one tested transaction
- **Local attribution**: Desktop's operator label is asserted from the session environment; socket peer identity and fresh step-up are unfinished
- **Local journal surface**: helper decisions are journaled, but Desktop cannot yet browse the root journal and no one-time fleet import exists
- **Desktop platforms**: Linux amd64 is the only native Qt release artifact; Windows transport exists in source without a proved Windows Desktop build
- **Kernel enforcement inert**: eBPF scaffold exists, not wired to the capability model
These are architectural gaps, not bugs. They define where the system's protection
@ -170,4 +204,4 @@ boundary currently ends. Task tracking for closing them lives in `docs/tasks/`.
---
*Last reviewed: 2026-08-25*
*Last reviewed: 2026-09-01*

View file

@ -12,12 +12,12 @@ It describes a system under active development. Some of it will be wrong by the
| Section | Description |
|---------|-------------|
| [OVERVIEW](OVERVIEW.md) | **START HERE**architecture overview: what RedFlag is, the two capability tiers, architectural boundaries, honest gaps |
| [OVERVIEW](OVERVIEW.md) | **START HERE**machine and fleet shape, authority tiers, architectural boundaries, honest gaps |
| [core](core/) | ETHOS principles, the foundational architectural decisions |
| [components](components/) | Server, agent, web, helper — component breakdowns |
| [components](components/) | Server, Agent, Web, native Desktop, helper — component breakdowns |
| [security](security/) | Trust boundaries, auth stack, refresh-token lifecycle, machine binding, supply chain gate, standalone authority |
| [verification](verification/) | Ed25519 signing pipeline, agent verification, key rotation, replay protection |
| [scanners](scanners/) | Every scanner (APT, DNF, Winget, WUA, Docker, process explorer) with interaction analysis |
| [scanners](scanners/) | Every scanner and resolver (APT, DNF, pacman, Winget, WUA, Docker, process explorer) with interaction analysis |
| [flows](flows/) | Data flows — registration, command execution, upgrade, heartbeat, capability advertisement, update lifecycle |
| [deployment](deployment/) | Docker stack, native agent services, CI/CD, release gate, operations runbook pointers |
| [testing](testing/) | Test pyramid, structural tests, live testing, honest gaps |
@ -56,6 +56,7 @@ architecture docs."
| Version | Date | Changes |
|---------|------|---------|
| 2.3 | 2026-09-01 | Native Desktop machine console, standalone Agent mode, and the first live pacman mutation envelope with signed artifact custody. |
| 2.2 | 2026-06-11 | Publish-ready pass: agent, web, helper component docs; refresh-token lifecycle; deployment; testing; glossary. Public framing. |
| 2.1 | 2026-06-01 | Updated for v0.2.3.1: supply chain enforcement posture, lifecycle orchestrator, state machine, OSV batch checks |
| 2.0 | 2026-05-26 | Restructured for single-source-of-truth organization |

View file

@ -1,12 +1,12 @@
# Helper Component
**A privileged, short-lived Rust executor that independently validates authority before APT/DNF mutation.**
**A privileged, short-lived Rust verifier, local minter, and executor for capability-gated mutation.**
---
## Doctrine
The helper (`helper/src/main.rs`, a single ~2,000-line binary) is the only RedFlag path allowed to mutate packages on gated ecosystems. It reads trust inputs from root-owned pinned files and performs exactly one operation per invocation — the one described by a validly signed capability token. It accepts no shell text and inherits no environment. Everything else is a typed denial.
The helper (`helper/src/main.rs`) is the only RedFlag path allowed to mutate packages on gated ecosystems. It reads trust inputs from root-owned pinned files and performs exactly one operation per invocation: the operation described by a valid capability token or `MutationEnvelope`. It accepts no shell text and inherits no environment. Everything else is a typed denial.
The current Linux transient unit is **not network-isolated**. APT and DNF can reach their configured registries while the helper runs. Networkless execution remains the design target after RedFlag can stage and re-verify every required artifact locally.
@ -16,7 +16,7 @@ Deny-by-default is the architecture, not a configuration: every failure path ret
## Invocation
On Linux the agent invokes it through `sudo systemd-run --wait --property=ProtectSystem=no`, passing root-readable token and result file paths. This avoids fd passing through dbus while escaping the agent service's own `ProtectSystem=strict` mount sandbox. No `PrivateNetwork` or equivalent property is set. The agent holds zero direct install sudo; its privileged route is this helper invocation. See [components/02-agent](02-agent.md).
On Linux the agent invokes it through `sudo systemd-run --wait --property=ProtectSystem=no`, passing fixed exchange-file paths. This avoids fd passing through dbus while escaping the agent service's own `ProtectSystem=strict` mount sandbox. No `PrivateNetwork` or equivalent property is set. The agent holds zero direct install sudo; its privileged route is an exact helper invocation. Fleet installers grant only the live capability-token execution shape. Standalone provisioning adds local mint and pacman-envelope command shapes and must not coexist with fleet credentials. See [components/02-agent](02-agent.md).
### Windows Invocation (SEC-030, decided 2026-07-01)
@ -54,23 +54,47 @@ Agent, helper, and desktop binaries update through the same gate as packages: `s
---
## Dormant Envelope Verification
## Mutation Envelopes and Pacman
`verify-envelope` is the mutation-envelope path (ARCH-002 cut 2). It runs the same
pipeline shape as execute mode — parse, host binding against the independently read
`agent_id`, SEC-021 trusted keyring, Ed25519 verification, time window, authorization
lifetime ceiling, backend payload shape — and then refuses every envelope with
`backend_not_migrated`, emitting a `MutationReceipt`.
`verify-envelope` remains inspection-only: it verifies shape, target, trust key,
signature, time, lifetime ceiling, and backend payload without consuming replay state or
executing.
Two absences are deliberate. It **records no replay state**: execute mode records a token
only after a plan is built, so an operation that cannot run never burns its slot, and
nothing here can run. And it enforces **no artifact custody**: when a backend carries a
local cached artifact as executor input the helper must rehash it, but no backend does
yet, and remote repository execution keeps the limits stated above.
`execute-envelope` is live for the pacman backend. Before the local standalone authority
signs, `mint-envelope` requires every action to carry an exact official-repository
archive and detached signature. The helper copies those files into root custody and
checks:
1. archive and signature SHA-256;
2. package name and version read from the archive with `pacman -Qp`;
3. the detached package signature with `pacman-key --verify`; and
4. forward-only movement against the installed version with `vercmp`.
The signed backend payload marks exactly one requested root. An `upgrade` must move that
root strictly forward; an `install` must introduce an absent root. Dependencies may already
be satisfied, but none may move backward.
Execute mode verifies the signed envelope, replay-claims the authorization, repeats the
custody and package checks, and invokes one fixed `pacman -U --noconfirm -- ...` plan. The
resulting `MutationReceipt` joins authorization, decision,
exit code, and the number of fully verified actions; exit zero cannot mean pacman silently
skipped an already-satisfied transaction.
Fleet pacman envelope delivery is not implemented yet. The executor exists; the Server
does not yet mint and deliver this envelope to a fleet Agent.
## Standalone Mint Mode
The helper also carries a local-authority minting path (`MintRequest` / `MintedToken` / `load_mint_key`) for deployments where the signing authority runs beside the host rather than on a central server. Design of record: [security/06-standalone-authority](../security/06-standalone-authority.md).
The helper carries two local-authority protocols: legacy `MintRequest` / `MintedToken`
for APT and DNF, and `EnvelopeMintRequest` / `MutationEnvelope` for pacman. Both load a
root-owned, owner-and-mode-validated Ed25519 key with no-follow semantics. Exchange-file
reads walk fixed directories with `openat`; writes are create-new and no-follow. Request,
response, envelope, and receipt filenames must join on the same UUID.
The helper validates evidence shape and freshness, but it does not independently re-run
OSV or attest the human operator. Root-owned key material therefore constrains the
privileged protocol without reproducing fleet authority against a compromised Agent.
Design of record: [security/06-standalone-authority](../security/06-standalone-authority.md).
---
@ -83,4 +107,4 @@ The helper also carries a local-authority minting path (`MintRequest` / `MintedT
---
*Last reviewed: 2026-08-26*
*Last reviewed: 2026-09-01*

View file

@ -1,95 +1,139 @@
# Desktop Component
# RedFlag Desktop
**A credential-less tray window onto the local agent — it can see, it can ask, it cannot mutate.**
**The native local-machine observation and operations surface. QML is the glass; the Agent and helper own the machine.**
---
## Doctrine
## Product role
The desktop app (`desktop/`, Tauri 2) holds no tokens, reads no protected config,
and never talks to the fleet server. Its entire world is the agent's local API
socket, and its authorization is its operating-system identity: the user running it
is either in the `redflag-local` group or the kernel refuses the connection. See
[security/01-trust-boundaries](../security/01-trust-boundaries.md), Local Trust
Boundary.
RedFlag Desktop is a Qt 6 / QML system monitor and local operations console for one
machine. It works in standalone mode and keeps the same domain when the machine joins a
fleet. RedFlag Web asks across machines; Desktop asks here, now, on this body.
The credential-less surface is a design decision, not an inevitability — the
reasoning lives in [security/06-standalone-authority](../security/06-standalone-authority.md):
group membership only gates the *request*; the judgment stays in the gates and the
root-owned mint key, and same-host token schemes don't reproduce an off-host
boundary. The decision is held open in one respect: the socket does not capture
peer identity (`SO_PEERCRED`), so on a multi-user host an approval journals as an
event, not as a person. Whether that needs per-user attribution (peer creds, or a
polkit prompt) is an open call, not settled doctrine.
The application currently carries eleven native surfaces:
Fleet-server context, when it comes, joins this same app rather than spawning a
second local-only application.
- Overview and Performance;
- Processes, Network, Storage, Services, and Containers;
- installed Software and available Updates;
- Security and History.
The useful unit is the join between those surfaces. A process can carry its systemd unit,
container identity, sockets, namespaces, capabilities, and owning package. Package and
update details lead toward dependency closure, advisory evidence, authorization, and
mutation history instead of remaining a separate updater product.
Souveraine Updater no longer owns a product boundary here. Useful closure and package UX
may be absorbed, but its direct `pkexec pacman` authority must not survive. The Tauri /
React Desktop runtime has also been removed; the fleet React application remains RedFlag
Web.
---
## Structure
## Authority boundary
```
desktop/
├── src/main.rs # Tauri shell: tray, window, localapi client, health reporter
├── tauri.conf.json # Window config, dev/build frontend wiring
└── icons/icon.png # 8-bit RGBA — tray icon constructor rejects 16-bit PNGs
web/
├── index.desktop.html # Desktop entry (separate from the fleet SPA's index.html)
├── src/desktop/ # LocalAgentApp — status dashboard, no auth surface
└── vite.desktop.config.ts# Builds to dist-desktop/, emits the entry as index.html
```text
RedFlag Desktop (Qt / QML)
observation + operator intent
|
v
RedFlag Agent local source of truth, resolution, gates
|
v
RedFlag Helper privileged verifier / bounded executor
|
v
platform backend pacman first; other migrated backends follow
```
One Rust binary, one React entry. The frontend is embedded at build time
(`frontendDist: ../web/dist-desktop`); release builds are self-contained — no dev
server, no localhost dependency. Dev builds (`cargo build` without `tauri build`)
compile in the Vite `devUrl` and require `npm run dev:desktop` running.
The fleet SPA and the desktop entry share the `web/` tree but are distinct
applications: the desktop entry never renders the sign-in screen because it has
nothing to sign in to.
---
## Agent IPC
The Rust shell speaks plain HTTP/1.1 over the local socket
Desktop never shells out to pacman, Docker, or systemd and never reads a signing key. It
speaks HTTP/1.1 over the Agent local socket
(`/var/lib/redflag/agent/localapi/redflag-agent.sock`; named pipe
`\\.\pipe\RedFlagAgentLocal` on Windows). The frontend reaches it only through
Tauri `invoke` commands — the webview itself has no socket access.
`\\.\pipe\RedFlagAgentLocal` on Windows). The user must be admitted to the
`redflag-local` OS group or the kernel refuses the connection.
- `local_status``GET /v1/identity` + `GET /v1/status` — the dashboard snapshot,
polled every 5s.
- Health reporter thread → `POST /v1/desktop` every 30s with version, uptime, and
window state. This is how the agent (and through it, the fleet) knows a tray is
alive on the host.
The current socket still lacks `SO_PEERCRED` attribution and fresh StepUp. Desktop sends
the session username as an assertion, not an attestation. A same-user process able to
reach the socket can express the same intent. The helper constrains what bytes and
operations can execute; it does not prove which human clicked the button.
On `EACCES` the shell diagnoses rather than reports: it distinguishes "user not in
`redflag-local`" (gives the usermod command) from "membership added but the login
session predates it" (tells the user to re-log) by comparing `/etc/group` against
the process's live groups. Group membership is stamped at login; this is the
component's most common support question, so the error message carries the answer.
Fleet enrollment removes local mint authority. Desktop may still display this machine,
but approval belongs to RedFlag Server and the Agent refuses local authorization.
---
## Lifecycle
## Native structure
Installed and provisioned by the agent install script (binary, autostart entry,
group enrollment — `linux.sh.tmpl` step 7c). Updated through the capability gate
like every other binary: `desktop-self` tokens, hash-verified and atomically
swapped by the helper ([components/04-helper](04-helper.md)). Server-side minting
and pre-tray host healing are in progress — `docs/tasks/UPDATE-002`. The component
is not yet in release version lockstep; closing that is `docs/tasks/INSTALL-001`.
```text
desktop/
├── Cargo.toml / build.rs
├── src/
│ ├── main.rs
│ └── bridge/
│ ├── local_api.rs HTTP over Unix socket / named pipe
│ └── machine.rs CXX-Qt state and invokable intent
├── qml/
│ ├── Main.qml
│ ├── Theme.qml
│ ├── MetricCard.qml
│ ├── LineGraph.qml
│ └── NavItem.qml
└── icons/icon.png
```
The Rust bridge polls bounded Agent projections. Live resource telemetry runs at one
second with a 300-point in-memory history. Larger inventory and detail reads are explicit
and asynchronous so the QML thread does not become a second scanner.
If the Agent is reachable but lacks newer routes, Desktop names the missing surface as an
Agent version gap rather than presenting an empty healthy machine. Unknown and unsupported
remain distinct from zero.
---
## Cross-References
## Local update intent
- [security/01-trust-boundaries](../security/01-trust-boundaries.md) — the Local trust boundary this client lives behind
- [components/02-agent](02-agent.md) — the localapi server side
- [components/04-helper](04-helper.md) — how the desktop binary itself updates
`POST /v1/actions/approve-update` is the only Desktop package-approval call. APT and DNF
use the closure-capability path. Pacman now uses the mutation-envelope path:
1. the Agent refreshes a private pacman database and resolves one exact dependency
transaction without touching live package state;
2. it downloads every archive and detached signature into a private cache and records
exact identity, repository, locations, and hashes in the manifest;
3. the standalone helper stages those bytes under root custody, verifies identity and
Arch signatures, enforces non-decreasing versions, checks gate evidence, and signs the
envelope with the root-owned local authority;
4. execution repeats custody, hash, identity, signature, and version checks, atomically
consumes `authorization_id`, runs one fixed pacman argv, and returns a joined receipt.
OSV has no Arch ecosystem mapping in the current RedFlag gate. Desktop displays
`unsupported` and requires a recorded override reason; it never turns missing advisory
coverage into a green check.
Override uses this same path. It is intent plus a reason inside the same authority and
journal, not a second privileged button.
---
*Last reviewed: 2026-06-14*
## Lifecycle and proof
The native Linux Desktop is built by Gitea Actions with Qt 6 and CXX-Qt. Windows still has
Agent support but no native Qt/MSVC Desktop artifact until a suitable runner and packaging
path exist. Source presence is not an installed or runtime proof.
Desktop self-update remains a capability-gated helper swap. Installation must also provide
the local socket group, autostart/background presence, and exact helper sudo protocols;
QML does not compensate for incomplete provisioning.
---
## Cross-references
- [security/01-trust-boundaries](../security/01-trust-boundaries.md)
- [security/06-standalone-authority](../security/06-standalone-authority.md)
- [components/02-agent](02-agent.md)
- [components/04-helper](04-helper.md)
- [scanners/06-pacman-scanner](../scanners/06-pacman-scanner.md)
---
*Last reviewed: 2026-09-01*

View file

@ -1,92 +1,98 @@
# Pacman Scanner
# Pacman Scanner and Resolver
**Arch Linux package manager scanning via pacman-contrib's checkupdates.**
**Arch update discovery stays unprivileged; approved mutation crosses the signed helper envelope.**
---
## Component Details
## Discovery
| Property | Value |
|----------|-------|
| Method | `checkupdates --color=never` |
| Platform | Linux (Arch, Manjaro, EndeavourOS) |
| Execution time | ~5-15 seconds per scan |
| Output format | `pkgname oldver -> newver` (one per line) |
| Failure modes | pacman-contrib not installed, network timeout, database lock |
| Root required | No (checkupdates uses a private db copy) |
| Platform | Arch Linux and pacman derivatives |
| Output | `pkgname oldver -> newver` |
| Root required | No |
| Requirements | `pacman`, `pacman-contrib`, `fakeroot` |
`agent/internal/scanner/pacman.go` runs pacman-contrib's `checkupdates` through
`DiscoveryRunner`. `checkupdates` synchronizes a private database and compares it with the
installed local database. The Agent has no `pacman -Sy` sudo grant and discovery never
refreshes live sync state.
Epoch-bearing versions remain opaque pacman versions. The parser preserves a line such as
`fakeroot 1:1.37.2-1 -> 1:1.37.2-2`; it does not split or reinterpret the epoch.
---
## Implementation
## Standalone closure resolution
**File:** `agent/internal/scanner/pacman.go`
`agent/internal/installer/pacman_resolver_linux.go` owns the approval-time resolver:
### Detection
1. create an Agent-private operation directory, sync database, and cache;
2. symlink only the installed `local` database into that private database;
3. run `fakeroot pacman -Sy` against the private database with a cleared, C locale;
4. resolve the requested exact `name=version` transaction and repository identities;
5. download the transaction with `pacman -Sw` into the private cache;
6. inspect each archive with locale-pinned `pacman -Qp`, require its adjacent detached
signature, and hash both files;
7. keep the cache alive through helper mint and execution, then remove it.
```go
func (s *PacmanScanner) IsAvailable() bool {
// Requires both pacman and checkupdates (from pacman-contrib)
if _, err := exec.LookPath("checkupdates"); err != nil {
return false
}
_, err := exec.LookPath("pacman")
return err == nil
}
```
### Scanning
Uses `DiscoveryRunner` (same as apt/dnf) for sandboxed execution:
```go
func (s *PacmanScanner) Scan() ([]client.UpdateReportItem, error) {
runner, err := installer.NewDiscoveryRunner("pacman")
// checkupdates --color=never
result, err := runner.Run(context.Background(), "--color=never")
// Parse "pkgname oldver -> newver" lines
}
```
### Output Parsing
`checkupdates` output format:
```
fakeroot 1:1.37.2-1 -> 1:1.37.2-2
linux 6.14.6.arch1-1 -> 6.15.1.arch1-1
```
The current (installed) version is embedded in the output — no secondary
query needed (unlike dnf which calls `rpm -q`).
The resolver returns an error rather than guessing when the requested root is absent, the
repository join is missing, an archive or signature is not regular, identity output is
ambiguous, or the transaction is empty.
---
## Sudoers
## Privileged path
The agent user needs `checkupdates` in sudoers. This is an unprivileged
discovery command (no mutation). Example:
The Agent encodes each package as `name@version` plus an exact JSON payload containing:
- repository;
- whether this action is the one requested root;
- archive cache path and SHA-256;
- detached-signature cache path and SHA-256.
The standalone helper will not sign a pacman envelope without every detached signature.
Before mint it copies Agent paths into a root-owned `0700` operation directory, verifies
both hashes, checks archive identity with `pacman -Qp`, verifies the detached signature
through the Arch keyring, and uses pacman's `vercmp` against installed state. Execution
repeats those checks, atomically consumes the authorization, and invokes:
```text
/usr/bin/pacman -U --noconfirm -- <staged archives...>
```
redflag-agent ALL=(ALL) NOPASSWD: /usr/bin/checkupdates
```
Source and exchange files are opened without following the final symlink. Helper exchange
directories are walked component-by-component with `openat(..., O_NOFOLLOW)` and result
files are create-new, so an Agent-controlled path cannot redirect a root write through a
swapped directory or pre-existing link.
There is no `--needed`: a successful receipt cannot conceal a package-manager skip. The
helper requires exactly one requested root. `upgrade` requires that root to be installed at
a strictly older version; `install` requires it to be absent. Equal versions remain valid
dependencies, while every older target is refused.
---
## Known Limitations
## Limits
- **Requires pacman-contrib**`pacman` alone is not sufficient. The
`checkupdates` binary is in the `pacman-contrib` package, which may not
be installed by default.
- **AUR packages not scanned**`checkupdates` only checks official repos.
AUR helpers (yay, paru) have their own update checkers but are not
integrated.
- **No dry-run for mutations** — pacman's `--print` flag exists but is not
used for hash resolution. Artifact hashing goes through the capability
gate path when it lands for pacman.
- AUR packages are not scanned or resolved.
- Requested-root identity lives in the signed pacman payload rather than the common
manifest, so other backends do not inherit pacman semantics accidentally.
- RedFlag currently has no OSV ecosystem mapping for Arch. Local approval records
`unsupported` and requires an explicit override reason.
- Package signature verification proves the artifact against the local Arch keyring. It
does not add reproducible-build or transparency-log evidence.
---
## Cross-References
## Cross-references
- [core/02-architecture-decisions](../core/02-architecture-decisions.md) — DiscoveryRunner pattern
- [security/05-supply-chain-gate](../security/05-supply-chain-gate.md) — capability token path (future for pacman)
- `agent/internal/scanner/detect.go` — scanner registration
- [security/05-supply-chain-gate](../security/05-supply-chain-gate.md)
- [security/06-standalone-authority](../security/06-standalone-authority.md)
- [components/04-helper](../components/04-helper.md)
- `protocol/README.md`
---
*Last reviewed: 2026-09-01*

View file

@ -1,6 +1,6 @@
# Supply Chain Gate
**Package-manager authorization with signed capability tokens; full closure custody, network isolation, and kernel enforcement remain design work.**
**Package-manager authorization with signed capabilities and mutation envelopes; pacman has full local artifact custody, while network isolation and kernel enforcement remain design work.**
This is the design of record for the gate — the capability model, the wire contract all
three components agree on, the load-bearing constraints, and component responsibilities.
@ -12,19 +12,28 @@ Build status and per-step implementation tracking live in `docs/tasks/GATE-000-s
RedFlag's supply chain gate inverts the traditional defense model: instead of **allow all installs and detect bad ones**, it **denies all state changes and requires explicit human authorization**.
**Current enforced scope:** APT and DNF mutation through RedFlag requires a signed
authorization token. Docker, Winget, and Windows Update still use the default-strict
signed-command path, and kernel enforcement against out-of-band root mutation is not wired.
**Current source scope:** APT and DNF mutation through RedFlag uses a signed capability
token. Standalone pacman approval uses a signed `MutationEnvelope`, exact closure archives,
detached package signatures, helper custody, and a joined receipt. Docker, Winget, and
Windows Update still use the signed-command path. Fleet pacman envelope delivery and kernel
enforcement against out-of-band root mutation are not wired.
---
## The decision: capability tokens, not a decision daemon
The gate authorizes APT/DNF installs with **signed capability tokens**, not with a runtime
decision daemon. The server is the authority: it evaluates policy and mints an
Ed25519-signed token describing exactly one operation over the artifact entries the agent
resolved and reported. The top-level hash is required. Dependency hashes are best-effort;
an unresolved dependency is logged and omitted today rather than making the report fail.
The gate authorizes APT/DNF installs with **signed capability tokens** and pacman with a
**signed mutation envelope**, not with a runtime decision daemon. In fleet mode the Server
is the authority: it evaluates policy and mints an Ed25519-signed capability over the
artifact entries the Agent resolved and reported. In standalone mode the short-lived root
helper mints locally after validating the bounded request; this preserves a privilege
boundary, not the fleet's off-host authority boundary.
APT/DNF still require the top-level hash while dependency hashes are best-effort: an
unresolved dependency is logged and omitted rather than making the report fail. Pacman is
stricter. The Agent resolves and downloads the transaction from signed repository metadata,
and the helper requires an exact archive and detached signature for every action before it
will sign or execute.
A small, privileged **executor** (`helper/`, Rust) validates token version and time, host
binding, signature, and replay state, then runs a fixed argv plan without a shell or inherited
@ -32,9 +41,9 @@ environment. It rehashes local artifact files and requires a readable matching f
mirror entry. A normal registry entry without a local path is not rehashed helper-side, and
the current transient unit retains host network access.
The intended boundary is stronger: resolve and stage the complete transitive closure,
rehash every byte at the privileged edge, then run the executor in a network-isolated unit.
That remains explicit design work, not a present guarantee.
Pacman now resolves and stages the transaction and rehashes every byte at the privileged
edge. APT/DNF full-closure custody and a network-isolated helper unit remain explicit design
work, not present guarantees.
This replaced the earlier `rs-helper` socket-decision daemon. `rs-helper`'s reusable parts
(the eBPF `InterceptEvent` struct, the package-manager allowlist, the hash cache) moved into
@ -70,7 +79,9 @@ The two tests converge on the same answer, which is the signal it's right.
| **Windows** | WDAC (Windows Defender Application Control) | winget, npm.cmd, pip.exe, choco, scoop |
| **macOS** | Endpoint Security Framework (ESF) | brew, pip, npm, bun, cargo |
**Crucial distinction:** Enforcement sits **below** userspace wrappers. A prompt-injected Claude Code session cannot bypass it with absolute paths or environment manipulation.
**Target distinction:** kernel enforcement sits below userspace wrappers. The current eBPF
scaffold is not connected to the capability model, so RedFlag does not yet claim that an
out-of-band package-manager invocation is blocked.
### Trust Chain
@ -150,19 +161,19 @@ signature = ed25519_sign(authority_priv, signed_message)
The closure is sorted before hashing so ordering can't change the digest. Tampering with any
artifact, version, or hash changes `closure_hash` and breaks verification.
### Mutation manifest contract (pinned, not wired)
### Mutation manifest contract (live for standalone pacman)
The closure token above remains the only format the runtime mints and executes. RedFlag also
pins a dormant `MutationEnvelope` contract in Go and Rust so new backends can converge without
extending the closure metaphor or preserving its unsigned `source`/`artifact_path` ambiguity.
No current operation uses this envelope yet.
The closure token remains live for APT and DNF. Pacman is the first runtime backend migrated
to `MutationEnvelope`, so new backends can converge without extending the closure metaphor
or preserving its unsigned `source`/`artifact_path` ambiguity.
Those two unsigned fields steer helper *verification* today, not helper *execution*:
Those two legacy unsigned fields steer capability-token *verification* today, not helper
execution:
`build_plan` reads only name, version, package type, and operation, and the self-update
branches take their source path from a helper constant. The server never populates
`artifact_path` and the agent reports `source=registry`, so the mirror branch is currently
unreached. The defect becomes executable the first time a backend puts a real cached path
in front of the executor — which is why the manifest lands before pacman, not after.
unreached. Pacman's envelope avoids that defect: repository, requested-root marker, cache
locations, archive hash, and signature hash all live inside each signed backend payload.
The envelope carries two objects, and the executor answers with a third:
@ -203,11 +214,13 @@ provenance, execution location, target, backend, resolved action, duplicates, or
authorization metadata, validity, decision, identifier shape, lifetime ceiling, and unknown
formats.
`redflag-helper verify-envelope` runs an envelope through the helper's real pipeline —
parse, host binding, trusted keyring, signature, time, lifetime, backend payload shape —
and refuses with `backend_not_migrated`. It records no replay state and enforces no
artifact custody: nothing executes through it yet, so it must not consume a replay slot,
and cached-byte enforcement arrives with the first backend that carries one.
`redflag-helper verify-envelope` remains an inspection-only compatibility path: it proves
the common envelope and refuses without consuming replay state. `mint-envelope` is the
standalone pacman authority. It requires exactly one requested root, detached signatures,
strict operation semantics (`upgrade` moves that root forward; `install` introduces it),
and non-decreasing dependencies. `execute-envelope` repeats identity, hash, signature, and
version checks over a fresh root stage, atomically claims the authorization, and runs one
fixed pacman plan without `--needed`.
### Reuse, don't reinvent
@ -223,19 +236,22 @@ The token extends the existing Ed25519 infrastructure rather than introducing ne
## Component responsibilities
- **Server (authority).** For dnf/apt, persist the agent-reported resolved entries, run OSV
- **Server (fleet authority).** For dnf/apt, persist the agent-reported resolved entries, run OSV
over that set, and mint a host-bound token after the approval boundary. The top-level hash
is required, but the current agent may omit a dependency whose hash did not resolve. Full
transitive resolution, the mirror tier, and signer process isolation remain target work.
- **Agent consumer (unprivileged).** Run discovery and best-effort hash resolution, receive
the token, confirm `agent_id` is this host, and hand it to the executor. It holds no signing
key and has no direct APT/DNF mutation method.
- **Agent consumer (unprivileged).** Run discovery and resolution, receive or request the
capability, confirm `agent_id` is this host, and hand it to the executor. It holds no
signing key and has no direct package-manager mutation method. For pacman it owns a private
sync database/cache and retains those files until mint and execution finish.
- **Executor (`helper/`, privileged, Rust).** Verify validity window → resolve trusted key by
`key_id` from a local pinned keyring → reconstruct `signed_message` → Ed25519 verify →
rehash each locally available artifact (and require mirror paths) → build a fixed argv plan
→ replay-guard on `token_id` → exec without a shell or inherited environment → structured
result + exit code. Registry entries without local paths are not rehashed. The current unit
is not network-isolated.
result + exit code. For pacman it also stages archives and signatures into root custody,
verifies the local Arch keyring, and refuses downgrades or operation-name lies. Legacy
registry entries without local paths are not rehashed. The current unit is not
network-isolated.
- **Kernel layer (where applicable).** Linux eBPF / Windows WDAC / macOS ESF deny
package-manager execution except via the trusted executor. This is defense-in-depth design;
the present eBPF scaffold is not wired to the capability model.
@ -419,9 +435,14 @@ override: flagged updates come back in
Auto-confirm shares the `ClosureCleared` predicate with manual approval — the two paths cannot
drift on what counts as a clean closure.
Standalone pacman does not claim that predicate. RedFlag has no Arch OSV ecosystem mapping
in this path, so the local gate records `unsupported` and requires an explicit operator
reason. Archive hashes and Arch package signatures still remain mandatory; the reason waives
only the missing advisory coverage.
---
## Build sequence (design intent)
## Build sequence (lineage)
The order the gate is built in. Per-step *status* is tracked in
`docs/tasks/GATE-000-supply-chain-gate-plan.md`; this records the intended dependency order.
@ -436,7 +457,10 @@ The order the gate is built in. Per-step *status* is tracked in
6. Mirror tier (optional): pull+hash closure at approval; verified-cache fallback.
7. Kernel adapters wire the executor as the only permitted caller.
Steps 12 are the contract. Everything else hangs off them.
Steps 15 produced the capability-token path. Pacman then exercised the envelope migration:
private resolution, local root mint, detached signatures, custody, strict forward semantics,
execution, and receipt are wired in source. Fleet envelope mint/delivery, the mirror tier,
and kernel adapters remain.
---
@ -456,12 +480,13 @@ Steps 12 are the contract. Everything else hangs off them.
are defense-in-depth on top, not a prerequisite. Userspace wrappers alone are bypassable.
**Current:** The privileged executor has a narrow argv-only API, no shell, and a stripped
environment; the agent that hands it tokens is unprivileged and holds no signing key. The
transient unit still has host network access.
environment; the Agent that hands it capabilities is unprivileged and holds no signing key.
Pacman archives and detached signatures cross root custody before a fixed offline `pacman
-U`; legacy APT/DNF registry entries may still require network. The transient unit retains
host network access.
**Target:** Stage and re-verify the full artifact closure, then enforce network isolation on
the helper unit. Neither property should be claimed until the invocation and artifact path
make it true.
**Target:** Bring APT/DNF to the same full-custody envelope and enforce network isolation on
the helper unit. Neither property applies globally until each migrated backend proves it.
**Connection:** [security/01-trust-boundaries](01-trust-boundaries.md) (kernel enforcement as trust boundary)
@ -469,4 +494,6 @@ make it true.
---
*Last reviewed: 2026-09-01*
*Last reviewed: 2026-08-26*

View file

@ -1,161 +1,188 @@
# Standalone Authority (Local Mode)
Drafted 2026-06-10. Status: APPROVED 2026-06-10 (Casey signed off on all three open
questions; resolutions inlined below).
Companion to `05-supply-chain-gate.md` (the gate contract is unchanged by this doc).
Build tracking: `docs/tasks/FEAT-003-standalone-local-authority.md`.
Status: implemented for local Agent startup, scanning, APT/DNF capability approval,
and pacman envelope approval. Fleet join remains fail-closed and unfinished.
## The problem
Companion to [05-supply-chain-gate](05-supply-chain-gate.md). Standalone changes where
authority lives; it does not create a pretend off-host boundary on one machine.
The tray app needs a local approval flow ("approve update → it installs") on a host with
no fleet server. In fleet mode the trust chain is:
---
## The Boundary That Exists
Fleet mode has a real host boundary:
```
operator → server (authority, off-host) → agent (consumer) → helper (enforcer)
operator → Server authority → Agent consumer → root helper
```
The authority lives on a different machine than the thing being updated. That is the real
boundary: a fully compromised agent host still cannot mint a capability token.
Standalone has only a privilege boundary:
In standalone mode the server does not exist. Something on the same host must mint
tokens. **No arrangement of processes on one host reproduces the off-host boundary**
local root can always reach the key. Pretending otherwise (e.g. "the agent signs its own
tokens, the helper checks them") turns the token into self-attestation theater. This doc
is honest about what survives and designs for that.
```
Desktop user → local Agent → fixed sudoers invocation → root helper and local key
```
## What the gate still buys on one host
The local Ed25519 private key is root-owned and unreadable by the Agent. That stops the
long-running Agent from copying or directly using key material, and the helper still
constrains every privileged action to a typed protocol. It does **not** make a compromised
Agent independent of the signer: the Agent is intentionally allowed to invoke the fixed
mint command and supplies the gate evidence the helper validates. The helper does not
re-run OSV or cryptographically prove that a human supplied the operator label.
The boundary that *is* preservable locally is the OS privilege boundary:
The honest threat table is therefore:
| Defended against | Fleet mode | Standalone |
| Property | Fleet | Standalone |
|---|---|---|
| Tampered local/mirror artifact (hash mismatch) | yes | **yes** — helper rehashes a supplied local path |
| Registry artifact with no local path | not helper-rehashed today | **not helper-rehashed today** |
| Known-vuln package (OSV) | yes, over reported resolved entries | **yes, over reported resolved entries** |
| Too-new package (age/soak gates) | fleet policy | **not yet applied** — local evidence records `not_applicable` |
| Unprivileged local malware minting installs | yes | **yes** — mint key is root-owned |
| Compromised agent process | yes (server refuses) | **yes** — agent user cannot read mint key |
| Local root attacker | yes (authority off-host) | **no — out of scope, say so in docs** |
| Host-bound signed authorization | yes | yes |
| Replay protection | yes | yes |
| Fixed argv, cleared environment, no shell | yes | yes |
| Pacman archive hash, identity, and detached signature verified by root helper | yes when fleet envelopes land | yes |
| APT/DNF local artifacts rehashed when a closure entry names a path | yes | yes |
| Human authority survives Agent compromise | yes, authority is off-host | **no** |
| Signing key survives managed-host compromise | yes, authority is off-host | **no** |
| Out-of-band mutation by local root prevented | **no, kernel enforcement is not wired** | **no** |
| OSV judgment independently reproduced by helper | no | no |
A compromised *agent* (unprivileged) in standalone can request a mint, exactly as a
compromised agent in fleet mode can request approval — and the same gates refuse it.
What standalone loses is only the root-compromise case, which on a single sovereign
host is game over for every other tool too.
Standalone still materially narrows mutation. It does not turn same-host signing into
proof that the host was uncompromised.
## Design
---
### Authority placement: privileged mint, root-owned key
## Provisioned State
Standalone mint runs as a **separate privileged invocation of the helper**
(`redflag-helper --mint`), the same `sudo systemd-run` pattern the executor already
uses. Rationale:
`scripts/provision-standalone-authority.sh` runs as root after the base Agent, helper,
`redflag-agent` user, and `redflag-local` group exist. It is idempotent and must never run
on a fleet-enrolled host.
- Keeps the artifact set at three (`agent`, `server`, `helper`) — no fourth binary.
- The helper is already the audited, privileged, short-lived Rust component
("auditable in one sitting"). Mint is ~the token struct it already parses, signed
instead of verified.
- Minter-equals-verifier is acceptable *here only* because both already run as root on
the same host; the boundary being enforced is root-vs-unprivileged, not
minter-vs-verifier. In fleet mode this collapse stays forbidden (constraint #2 of
`05`).
Provisioning creates or verifies:
Keys:
- a stable UUIDv4 in the Agent config via `redflag-agent --init-standalone`;
- the same UUID in root-owned `/etc/redflag/agent_id` for independent target binding;
- `/etc/redflag/authority_local.key`, root-owned `0600`;
- the public half in the helper's root-owned trusted keyring;
- Agent-owned exchange directories for mint requests, tokens, mutation requests,
envelopes, and receipts; and
- exact sudoers command shapes for legacy token mint, envelope mint, and envelope
execution.
- `authority_local.key` — Ed25519, generated at install time (same provisioning step
that creates the `redflag-local` group / helper sudoers). Root-owned, `0600`,
outside the agent's readable tree. Never leaves the host.
- The helper's pinned keyring gets the corresponding public key with its `key_id`
fingerprint, exactly like a server key. Verify path is byte-identical to fleet mode.
`Config.IsStandalone()` is true only when a stable Agent ID exists and registration,
access, and refresh tokens are all absent. Partial fleet material is neither standalone
nor registered and startup refuses it.
### Flow
The standalone Agent starts the local API and kernel monitor, scans on startup, and then
scans on its bounded local interval without contacting a Server. Pacman is included in
that local scanner set.
---
## APT and DNF Flow
```
tray (redflag-local member)
→ POST /v1/updates/:id/approve (agent local API, group ACL boundary)
→ agent: resolve closure via DiscoveryRunner (dry-run, hash-resolve — read-only)
→ agent: run the current local predicate (OSV.dev query over resolved entries)
any vuln in the checked set = full stop — no silent waiver;
override requires explicit reason, journaled locally
→ agent: write mint request file (closure + gate evidence + operator + reason)
→ sudo systemd-run redflag-helper --mint <request> (narrowed sudoers entry)
helper re-derives closure_hash, re-checks gate evidence freshness window,
signs with authority_local.key, emits token, journals the mint
→ token → normal consumer path → helper verify + execute (unchanged)
Desktop
→ POST /v1/actions/approve-update
→ Agent dry-run and closure hash resolution
→ Agent OSV query over the reported closure
non-clear verdict requires a recorded reason
→ Agent writes MintRequest
→ root helper mint
validates host, operation, closure shape, evidence freshness, and reason
signs a short-lived capability with authority_local.key
→ normal Agent consumer
→ root helper verifies, replay-claims, and executes one fixed APT/DNF plan
→ PolicyResult returns to Desktop
```
The mint step **re-validates rather than trusts** the agent's evidence where the current
contract permits: closure entry shape, evidence timestamps, host binding, operation, and
the presence of an explicit override reason when OSV was vulnerable or unreachable. It
does not re-run OSV; the unprivileged agent performs that query and the privileged mint
path journals the supplied verdict. Network reachability is not the enforcement boundary
here: the current helper unit retains host network access.
Age and soak fields are recorded as `not_applicable`; standalone has no local registry
history for those policies yet. Registry artifacts without local paths remain signed in
the closure but are not rehashed helper-side, and the transient helper unit retains host
network access.
Age and soak evidence fields exist, but the local approval handler currently records both
as `not_applicable`. Bringing the fleet age/soak policies into standalone mode remains work.
---
Doctrine carried over unchanged: signing required, forward-only, no skip-verification
path, no doctrinal knobs.
## Pacman Flow
### Audit
Pacman does not use the legacy capability payload:
Every standalone approval/mint/execute writes to a local append-only journal owned by
root (mirror of the server's security-event journal). The tray reads it through a
read-only local API endpoint. When the host later joins a fleet, the journal is
uploaded once during re-provisioning so history survives the mode switch.
```
Desktop + required reason for unavailable OSV coverage
→ Agent resolves exact official-repository transaction in a private database/cache
→ Agent hashes every archive and detached signature
→ Agent writes EnvelopeMintRequest + MutationManifest
→ root helper mint-envelope
secure openat read from the fixed exchange directory
copies artifacts into root custody
verifies exactly one requested root, hashes, package identity, repository, detached signatures, and versions
requires an upgrade root to move strictly forward or an install root to be absent
signs one host-bound MutationEnvelope
→ Agent writes the envelope into the fixed execution exchange
→ root helper execute-envelope
verifies signature, time, target, replay, custody, identity, signatures, and versions again
executes one fixed pacman -U plan without --needed
→ joined MutationReceipt returns to Desktop
```
### Fleet join ("join fleet later")
Exchange files use exact UUID filenames, no-follow directory traversal, create-new
outputs, and filename joins between request and response. Stale unprivileged outputs are
removed before the helper runs; the helper refuses symlinked or out-of-directory paths.
Registration code + one-time 2FA → agent registers against the server → helper keyring
is **replaced**: server authority key(s) in, local authority key retired and its private
half destroyed (journaled). Single authority per mode — no dual-mint window, no local
fallback authority in fleet mode. A fleet host that loses its server does what it does
today: nothing installs until the server returns (constraint #3's verified-cache
fallback applies only to already-minted operations).
OSV currently has no usable Arch package mapping. The verdict is `unsupported`, never
`clear`, and the operator reason cannot waive any cryptographic or package-identity check.
**Transition maintainability (Casey, 2026-06-12).** Fleet join is a supported
lifecycle path, not a one-off migration script — it must hold to the same standard
as install/upgrade: idempotent, re-runnable, verified by the post-join healthcheck
rather than assumed (`docs/tasks/INSTALL-001` is the enforcement pattern). Two
standing rules keep it from rotting:
---
1. **Gate logic should converge.** Fleet and standalone share capability and helper
verification primitives, while standalone currently omits the age/soak policies.
Closing that drift is required; future gate changes must extend both resolution
paths in the same change.
2. **The join flow is exercised, not trusted.** Keyring replacement, key
destruction, and journal upload need test coverage that runs both directions of
the matrix (fresh-fleet install vs standalone-then-join must converge on
identical end state). If the two end states can diverge, the transition has
already broken — it just hasn't been noticed yet.
## Audit State
## Non-goals
Mint and execution decisions write the helper's local security journal. The local API
does not yet expose that root journal as a dedicated approval-history endpoint, and no
fleet upload exists. Desktop can show the joined result returned by the current request;
durable local browsing remains unfinished.
- No local approval authority in fleet mode (server remains sole authority).
- No "lite" trust mode — gates are not weakened because the host is standalone.
- No network listener for the mint path; the local group ACL + sudoers narrowing is
the entire request surface.
- Tray never touches keys, tokens, or the mint path directly — it only calls the
agent's local API.
The Desktop-provided operator name comes from the session environment and is asserted,
not peer-attested. `SO_PEERCRED`, polkit, or another fresh step-up mechanism remains an
open authority improvement.
## Resolved questions (Casey, 2026-06-10)
---
1. **Mint placement: `redflag-helper --mint`.** Artifact set stays at three. Two keys
keep roles distinct (mint key root-owned `0600`, verify key in pinned keyring);
SEC-022 attests both invocation modes.
2. **OSV in standalone: best-effort with honest verdict.** Vuln found in a reported
resolved entry = full stop.
OSV unreachable = explicit operator acceptance of "closure unverified" with reason,
journaled — mirrors the fleet `unverified` hold from v0.2.3.1. Age/soak gates and
complete execute-time registry artifact rehashing are not current standalone guarantees.
3. **Gate-evidence freshness window: 15 minutes, hard-coded.** Not configurable
(no doctrinal knobs); expired evidence means the agent re-resolves and re-checks.
## Fleet Join Is Not Implemented
## Cross-refs
The helper has an explicit local-key retirement primitive, but there is no complete,
tested transition that retires local authority, destroys its private key, installs the
Server keyring, registers the Agent, and proves the converged end state. Registration
from a standalone config therefore refuses with an error. Operators must not add fleet
credentials beside the local authority by hand.
- `05-supply-chain-gate.md` — token contract, constraints (esp. #2, #4, #6).
- `docs/tasks/FEAT-002-local-agent-api.md` — local API surface this builds on.
- `docs/tasks/SEC-023` — package-mutation boundary (mint path must not weaken it).
- `docs/tasks/SEC-022` — binary mutual attestation (helper trust file).
- `docs/tasks/THREAT-001` — must document the standalone trust model table above.
The required future transition is one-way and idempotent:
*Last reviewed: 2026-08-25*
1. stop local mutation intake;
2. retire and destroy the local private authority;
3. replace the helper trust set with Server authority keys;
4. register and persist complete fleet credentials;
5. prove that local mint sudoers and key material are absent; and
6. resume as a fleet Agent with no local fallback.
Until that workflow and its matrix tests exist, “join this standalone machine to a
fleet” is a named gap, not a supported lifecycle.
---
## Non-Goals
- No local mint authority beside fleet credentials.
- No doctrinal switch that disables signing, replay, hashes, identity, signatures, or
forward-only enforcement.
- No package-manager command in Desktop or Agent sudoers.
- No network listener for minting.
- No claim that same-host Ed25519 recreates the fleet trust boundary.
---
## Cross-References
- [05-supply-chain-gate](05-supply-chain-gate.md) — capability and envelope contracts
- [../components/04-helper](../components/04-helper.md) — root validation and execution
- [../components/05-desktop](../components/05-desktop.md) — credential-less request surface
- [../scanners/06-pacman-scanner](../scanners/06-pacman-scanner.md) — discovery and exact transaction resolution
---
*Last reviewed: 2026-09-01*

View file

@ -1,8 +1,10 @@
# Mutation manifest wire contract
This directory owns the language-independent bytes shared by RedFlag Server, Agent, and
helper. The current closure-based capability token remains the only runtime execution path;
the mutation envelope is pinned and tested but not wired to a backend yet.
helper. APT and DNF still use the closure-based capability token. Pacman is the first live
`MutationEnvelope` backend: standalone Desktop intent now crosses Agent resolution, local
root mint, helper verification, execution, and a joined receipt without a second package
manager path in QML.
## Envelope
@ -70,10 +72,9 @@ vocabulary ahead of the thing it names would weaken a binding that already works
## Authorization discipline
`authorization_id` MUST be a canonical UUID v4 (8-4-4-4-12 lowercase hex, version nibble
`4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint already
applies to `request_id`. It is the identifier an executor will eventually record as a
replay key, and a newline-delimited ledger matched line-by-line has no defence against an
identifier that contains a newline. The shape is checked before that day arrives.
`4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint applies to
`request_id`. The executor atomically records it as a one-shot replay claim before pacman
runs. The UUID shape prevents path or record injection into that claim store.
`expires_at - not_before` MUST NOT exceed **3600 seconds**. That ceiling is derived, not
chosen: it is `DefaultTokenTTL`, the fleet minter's own window. Standalone mint is tighter
@ -220,18 +221,28 @@ signature, time, lifetime ceiling, backend payload shape — and then refuses wi
`backend_not_migrated`. This remains the inspection-only compatibility path and consumes no
replay state.
`redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` admits the
first migrated backend: pacman `install` and `upgrade` over exact cached package archives.
The helper opens every signed cache path without following a final symlink, copies it into
a root-owned `0700` operation directory, and verifies the signed SHA-256 and archive
`name@version`. It then queries installed state with a cleared environment and pacman's
`vercmp`; any resolved action older than its installed version fails closed. Absent and
equal packages remain valid closure members because format 1 does not distinguish requested
roots from resolved dependencies. Only then does it atomically consume
`authorization_id` before one fixed `pacman -U` invocation without `--needed`, so exit zero
cannot conceal an already-satisfied no-op. A receipt records the joined operation, manifest,
authorization, verified-action count, and outcome.
`redflag-helper mint-envelope --request-file <path> --envelope-out <path>` is the standalone
authority for the first migrated backend. A pacman payload marks exactly one requested root
and binds repository, cache locations, archive hash, detached-signature hash, and the exact
`name@version` identity. Before signing, the helper copies every archive and signature into
a root-owned `0700` operation directory, verifies hashes, archive identity, the Arch package
signature, and installed state through pacman's `vercmp`. An `upgrade` requires the requested
root to be strictly newer; an `install` requires it to be absent; dependencies may be absent,
equal, or newer but never older.
Every other backend still fails closed with `backend_not_migrated`. The Agent request,
resolver, authorization mint, and Desktop handoff remain separate cuts; this executor does
not infer or create any of them.
`redflag-helper execute-envelope --envelope-file <path> [--receipt-file <path>]` repeats those
checks over a fresh root-owned stage, atomically consumes `authorization_id`, and invokes one
fixed `pacman -U --noconfirm -- <archives...>` plan. There is no `--needed`, so exit zero
cannot collapse a skipped transaction and an applied one into the same receipt. The receipt
records the joined operation, manifest, authorization, fully verified-action count, and
executor outcome.
The Agent-side resolver and Desktop handoff are wired for standalone pacman in the active
source. Exchange reads walk fixed directories with `openat` and no-follow semantics; helper
writes are create-new and joined by UUID filename. RedFlag has no Arch OSV mapping yet, so
local pacman approval records `unsupported` and requires an explicit reason instead of
inventing a clear advisory verdict.
Every other backend still fails closed with `backend_not_migrated`. Server-side fleet
envelope mint and delivery remain separate work. This document describes source behavior;
CI, packaging, installation, and runtime proof remain distinct states.