Add governance and identity registry scaffolding
This commit is contained in:
parent
1da00ecdf3
commit
f6a7f0922d
13 changed files with 612 additions and 21 deletions
27
.forgejo/workflows/lint-governance.yml
Normal file
27
.forgejo/workflows/lint-governance.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
name: Lint Governance
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
governance:
|
||||||
|
name: BEP Metadata
|
||||||
|
runs-on: [self-hosted, linux, x86_64, burrow-forge]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Validate BEP metadata
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 Scripts/check-bep-metadata.py
|
||||||
23
.github/workflows/lint-governance.yml
vendored
Normal file
23
.github/workflows/lint-governance.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Governance Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
governance:
|
||||||
|
name: BEP Metadata
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Validate BEP metadata
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 Scripts/check-bep-metadata.py
|
||||||
14
AGENTS.md
Normal file
14
AGENTS.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# instructions for agents
|
||||||
|
|
||||||
|
1. Spell the project name as `Burrow` in user-facing copy and `burrow` in code, package, and protocol identifiers unless an existing integration requires a different literal.
|
||||||
|
2. Read [CONSTITUTION.md](CONSTITUTION.md) before changing Apple clients, the daemon, the control plane, forge infrastructure, identity, or security-sensitive code.
|
||||||
|
3. Anchor non-trivial changes in a Burrow Evolution Proposal (BEP) under [evolution/](evolution/README.md) so future contributors can inherit the rationale, safeguards, and rollout shape.
|
||||||
|
4. Before touching the Apple app, daemon IPC, or Tailnet flows, review:
|
||||||
|
- [evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md](evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md)
|
||||||
|
- [evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md](evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md)
|
||||||
|
- [evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md](evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md)
|
||||||
|
- [evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md](evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md)
|
||||||
|
5. Apple clients must talk only to the daemon over gRPC. Do not add direct HTTP, control-plane, or helper-process calls from Swift UI code.
|
||||||
|
6. Treat Tailnet as one protocol family. Tailscale-managed and self-hosted Headscale-style deployments differ by authority, policy, and auth details, not by a separate user-facing protocol surface.
|
||||||
|
7. Maintain canonical identity and operator metadata in [contributors.nix](contributors.nix). If Burrow forge, Authentik, Headscale, or admin/group mappings need to change, edit that registry first and derive runtime configuration from it.
|
||||||
|
8. When process or architecture is unclear, stop and draft or update a BEP instead of improvising durable behavior in code.
|
||||||
6
Makefile
6
Makefile
|
|
@ -10,6 +10,12 @@ check:
|
||||||
build:
|
build:
|
||||||
@cargo build
|
@cargo build
|
||||||
|
|
||||||
|
bep-check:
|
||||||
|
@python3 Scripts/check-bep-metadata.py
|
||||||
|
|
||||||
|
bep-list:
|
||||||
|
@Scripts/bep list
|
||||||
|
|
||||||
daemon-console:
|
daemon-console:
|
||||||
@$(sudo_cargo_console) daemon
|
@$(sudo_cargo_console) daemon
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Routine verification now runs unprivileged with `cargo test --workspace --all-fe
|
||||||
The repository now carries its own design and deployment record:
|
The repository now carries its own design and deployment record:
|
||||||
|
|
||||||
- [Constitution](./CONSTITUTION.md)
|
- [Constitution](./CONSTITUTION.md)
|
||||||
|
- [Agent Instructions](./AGENTS.md)
|
||||||
- [Burrow Evolution](./evolution/README.md)
|
- [Burrow Evolution](./evolution/README.md)
|
||||||
- [WireGuard Rust Lineage](./docs/WIREGUARD_LINEAGE.md)
|
- [WireGuard Rust Lineage](./docs/WIREGUARD_LINEAGE.md)
|
||||||
- [Protocol Roadmap](./docs/PROTOCOL_ROADMAP.md)
|
- [Protocol Roadmap](./docs/PROTOCOL_ROADMAP.md)
|
||||||
|
|
@ -19,6 +20,8 @@ The repository now carries its own design and deployment record:
|
||||||
|
|
||||||
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow. Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. Forge and deployment scaffolding live in [`flake.nix`](./flake.nix), [`nixos/`](./nixos), and [`.forgejo/workflows/`](./.forgejo/workflows/). Hosted mail backup operations live in [`docs/FORWARDEMAIL.md`](./docs/FORWARDEMAIL.md) and [`Tools/forwardemail-custom-s3.sh`](./Tools/forwardemail-custom-s3.sh).
|
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow. Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. Forge and deployment scaffolding live in [`flake.nix`](./flake.nix), [`nixos/`](./nixos), and [`.forgejo/workflows/`](./.forgejo/workflows/). Hosted mail backup operations live in [`docs/FORWARDEMAIL.md`](./docs/FORWARDEMAIL.md) and [`Tools/forwardemail-custom-s3.sh`](./Tools/forwardemail-custom-s3.sh).
|
||||||
|
|
||||||
|
Agent and governance-sensitive work should start with [AGENTS.md](./AGENTS.md), [CONSTITUTION.md](./CONSTITUTION.md), and the relevant BEPs under [`evolution/proposals/`](./evolution/proposals/). Identity and bootstrap metadata now live in [`contributors.nix`](./contributors.nix).
|
||||||
|
|
||||||
The project structure is divided in the following folders:
|
The project structure is divided in the following folders:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
133
Scripts/bep
Executable file
133
Scripts/bep
Executable file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel)
|
||||||
|
proposals_dir="$repo_root/evolution/proposals"
|
||||||
|
|
||||||
|
auto_browse() {
|
||||||
|
if command -v wisu >/dev/null 2>&1; then
|
||||||
|
exec wisu -i -g --icons "$repo_root/evolution"
|
||||||
|
fi
|
||||||
|
exec ls -la "$repo_root/evolution"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: bep [command]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list [--status <Status>] List BEPs, optionally filtered by status.
|
||||||
|
open <BEP-XXXX|XXXX|X> Open a BEP in $EDITOR.
|
||||||
|
help Show this help.
|
||||||
|
|
||||||
|
If no command is provided, bep launches a simple browser for evolution/.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_id() {
|
||||||
|
local raw="$1"
|
||||||
|
if [[ "$raw" =~ ^BEP-[0-9]+$ ]]; then
|
||||||
|
printf '%s' "$raw"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$raw" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf 'BEP-%04d' "$raw"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_status() {
|
||||||
|
local file="$1"
|
||||||
|
awk -F ': ' '/^Status:/ {print $2; exit}' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_title() {
|
||||||
|
local file="$1"
|
||||||
|
local line
|
||||||
|
line=$(head -n 1 "$file" || true)
|
||||||
|
printf '%s' "$line" | sed -E 's/^# `[^`]+`[[:space:]]+//; s/^[^A-Za-z0-9]+//'
|
||||||
|
}
|
||||||
|
|
||||||
|
list_bep() {
|
||||||
|
local filter="${1:-}"
|
||||||
|
local filter_lower=""
|
||||||
|
if [[ -n "$filter" ]]; then
|
||||||
|
filter_lower=$(printf '%s' "$filter" | tr '[:upper:]' '[:lower:]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%-10s %-18s %s\n' "BEP" "Status" "Title"
|
||||||
|
local file
|
||||||
|
local entries=()
|
||||||
|
for file in "$proposals_dir"/BEP-*.md; do
|
||||||
|
[[ -e "$file" ]] || continue
|
||||||
|
local base
|
||||||
|
base=$(basename "$file")
|
||||||
|
local id
|
||||||
|
id=$(printf '%s' "$base" | cut -d- -f1-2)
|
||||||
|
local status
|
||||||
|
status=$(read_status "$file")
|
||||||
|
local status_lower
|
||||||
|
status_lower=$(printf '%s' "$status" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if [[ -n "$filter_lower" && "$status_lower" != "$filter_lower" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local title
|
||||||
|
title=$(read_title "$file")
|
||||||
|
entries+=("$(printf '%-10s %-18s %s' "$id" "$status" "$title")")
|
||||||
|
done
|
||||||
|
if [[ ${#entries[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${entries[@]}" | sort
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
open_bep() {
|
||||||
|
local raw="$1"
|
||||||
|
local id
|
||||||
|
if ! id=$(normalize_id "$raw"); then
|
||||||
|
echo "Unknown BEP id: $raw" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local matches
|
||||||
|
matches=("$proposals_dir"/"$id"-*.md)
|
||||||
|
if [[ ${#matches[@]} -eq 0 || ! -e "${matches[0]}" ]]; then
|
||||||
|
echo "No proposal found for $id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ${#matches[@]} -gt 1 ]]; then
|
||||||
|
echo "Multiple proposals match $id:" >&2
|
||||||
|
printf ' %s\n' "${matches[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local editor="${EDITOR:-vi}"
|
||||||
|
exec "$editor" "${matches[0]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
command=${1:-}
|
||||||
|
case "$command" in
|
||||||
|
"")
|
||||||
|
auto_browse
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
if [[ ${2:-} == "--status" && -n ${3:-} ]]; then
|
||||||
|
list_bep "$3"
|
||||||
|
else
|
||||||
|
list_bep
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
open)
|
||||||
|
if [[ -z ${2:-} ]]; then
|
||||||
|
echo "bep open requires an id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
open_bep "$2"
|
||||||
|
;;
|
||||||
|
help|-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $command" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
94
Scripts/check-bep-metadata.py
Executable file
94
Scripts/check-bep-metadata.py
Executable file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
PROPOSALS_DIR = REPO_ROOT / "evolution" / "proposals"
|
||||||
|
ALLOWED_STATUSES = {
|
||||||
|
"Pitch",
|
||||||
|
"Draft",
|
||||||
|
"In Review",
|
||||||
|
"Accepted",
|
||||||
|
"Implemented",
|
||||||
|
"Rejected",
|
||||||
|
"Returned for Revision",
|
||||||
|
"Superseded",
|
||||||
|
"Archived",
|
||||||
|
}
|
||||||
|
REQUIRED_FIELDS = [
|
||||||
|
"Status",
|
||||||
|
"Proposal",
|
||||||
|
"Authors",
|
||||||
|
"Coordinator",
|
||||||
|
"Reviewers",
|
||||||
|
"Constitution Sections",
|
||||||
|
"Implementation PRs",
|
||||||
|
"Decision Date",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def text_block_lines(path: pathlib.Path) -> list[str]:
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"```text\n(.*?)\n```", content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("missing leading ```text metadata block")
|
||||||
|
return [line.rstrip() for line in match.group(1).splitlines() if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def validate(path: pathlib.Path) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
proposal_id = path.name.split("-", 2)[:2]
|
||||||
|
expected_id = "-".join(proposal_id).removesuffix(".md")
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines = text_block_lines(path)
|
||||||
|
except ValueError as exc:
|
||||||
|
return [f"{path}: {exc}"]
|
||||||
|
|
||||||
|
field_names = [line.split(":", 1)[0] for line in lines]
|
||||||
|
if field_names != REQUIRED_FIELDS:
|
||||||
|
errors.append(
|
||||||
|
f"{path}: metadata fields must appear in order {', '.join(REQUIRED_FIELDS)}"
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
|
fields = dict(line.split(":", 1) for line in lines)
|
||||||
|
fields = {key.strip(): value.strip() for key, value in fields.items()}
|
||||||
|
|
||||||
|
if fields["Status"] not in ALLOWED_STATUSES:
|
||||||
|
errors.append(f"{path}: invalid Status {fields['Status']!r}")
|
||||||
|
|
||||||
|
if fields["Proposal"] != expected_id:
|
||||||
|
errors.append(
|
||||||
|
f"{path}: Proposal field {fields['Proposal']!r} does not match filename id {expected_id!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if fields["Status"] in {"Accepted", "Implemented", "Superseded", "Rejected", "Archived"} and fields["Decision Date"] == "Pending":
|
||||||
|
errors.append(
|
||||||
|
f"{path}: Decision Date must not be Pending once status is {fields['Status']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
errors: list[str] = []
|
||||||
|
for path in sorted(PROPOSALS_DIR.glob("BEP-*.md")):
|
||||||
|
errors.extend(validate(path))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"checked {len(list(PROPOSALS_DIR.glob('BEP-*.md')))} BEPs")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
47
contributors.nix
Normal file
47
contributors.nix
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
groups = {
|
||||||
|
users = "burrow-users";
|
||||||
|
admins = "burrow-admins";
|
||||||
|
};
|
||||||
|
|
||||||
|
identities = {
|
||||||
|
contact = {
|
||||||
|
displayName = "Burrow";
|
||||||
|
canonicalEmail = "contact@burrow.net";
|
||||||
|
sourceEmail = "net.burrow@gmail.com";
|
||||||
|
isAdmin = true;
|
||||||
|
forgeAuthorized = true;
|
||||||
|
bootstrapAuthentik = true;
|
||||||
|
sshPublicKeyPath = ./nixos/keys/contact_at_burrow_net.pub;
|
||||||
|
roles = [
|
||||||
|
"operator"
|
||||||
|
"forge-admin"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
conrad = {
|
||||||
|
displayName = "Conrad Kramer";
|
||||||
|
canonicalEmail = "conrad@burrow.net";
|
||||||
|
sourceEmail = "ckrames1234@gmail.com";
|
||||||
|
isAdmin = true;
|
||||||
|
forgeAuthorized = false;
|
||||||
|
bootstrapAuthentik = true;
|
||||||
|
roles = [
|
||||||
|
"operator"
|
||||||
|
"founder"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
agent = {
|
||||||
|
displayName = "Burrow Agent";
|
||||||
|
canonicalEmail = "agent@burrow.net";
|
||||||
|
isAdmin = false;
|
||||||
|
forgeAuthorized = true;
|
||||||
|
bootstrapAuthentik = false;
|
||||||
|
sshPublicKeyPath = ./nixos/keys/agent_at_burrow_net.pub;
|
||||||
|
roles = [
|
||||||
|
"automation"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -58,3 +58,17 @@ evolution/
|
||||||
```
|
```
|
||||||
|
|
||||||
Use ASCII Markdown. Keep metadata at the top of each proposal so tooling and future agents can parse it quickly.
|
Use ASCII Markdown. Keep metadata at the top of each proposal so tooling and future agents can parse it quickly.
|
||||||
|
|
||||||
|
## BEP Helper
|
||||||
|
|
||||||
|
Use the `bep` helper under `Scripts/` to browse or list proposals:
|
||||||
|
|
||||||
|
- `Scripts/bep` opens a quick browser for `evolution/`.
|
||||||
|
- `Scripts/bep list --status Draft` lists proposals by status.
|
||||||
|
- `Scripts/bep open BEP-0005` opens a proposal in `$EDITOR`.
|
||||||
|
|
||||||
|
Validate proposal metadata with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 Scripts/check-bep-metadata.py
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
# `BEP-0005` - Daemon IPC and Apple Boundary
|
||||||
|
|
||||||
|
```text
|
||||||
|
Status: Draft
|
||||||
|
Proposal: BEP-0005
|
||||||
|
Authors: gpt-5.4
|
||||||
|
Coordinator: gpt-5.4
|
||||||
|
Reviewers: Pending
|
||||||
|
Constitution Sections: II, III, IV, V
|
||||||
|
Implementation PRs: Pending
|
||||||
|
Decision Date: Pending
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Burrow should formalize one Apple/runtime boundary: Apple clients speak only to the daemon over gRPC on the app-group Unix socket, and the daemon owns all external control-plane, helper-process, and runtime coordination work. This prevents UI code from accreting side HTTP paths or ad hoc control-plane integrations that bypass the system Burrow is supposed to own.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- The current Tailnet work already showed the failure mode: Swift UI code started reaching around the daemon boundary to talk to helper HTTP endpoints directly.
|
||||||
|
- Apple-specific process ownership is easy to blur between the app, the network extension, and helper daemons unless the contract is explicit.
|
||||||
|
- If Burrow wants a durable multi-runtime architecture, the daemon must remain the only orchestration boundary between clients and control/data-plane behavior.
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
|
||||||
|
- Apple UI and Apple support libraries may call only daemon gRPC methods over the declared Burrow Unix socket.
|
||||||
|
- Direct Swift calls to external control-plane HTTP APIs, localhost helper HTTP servers, or runtime-specific subprocesses are forbidden.
|
||||||
|
- The daemon is responsible for:
|
||||||
|
- discovery of Tailnet authorities and related metadata
|
||||||
|
- control-plane session setup and tracking
|
||||||
|
- login/session lifecycle brokering
|
||||||
|
- runtime start/stop/reconcile
|
||||||
|
- translating helper or bridge processes into stable daemon RPCs
|
||||||
|
- `burrow/src/control/` owns transport-neutral control-plane semantics such as discovery, authority normalization, and request/response shaping.
|
||||||
|
- Apple UI owns presentation only:
|
||||||
|
- forms
|
||||||
|
- local state
|
||||||
|
- presenting returned auth URLs or statuses
|
||||||
|
- surfacing daemon availability and errors
|
||||||
|
- Any new Apple-facing runtime capability requires a daemon RPC first.
|
||||||
|
|
||||||
|
## Security and Operational Considerations
|
||||||
|
|
||||||
|
- Keeping control-plane I/O out of Swift UI reduces accidental secret, token, and callback sprawl across app code.
|
||||||
|
- The daemon boundary makes testing and kill-switch behavior tractable because runtime integration is localized.
|
||||||
|
- Apple daemon lifecycle ownership must be explicit: either the app ensures the daemon is running before RPC or the extension owns it and the UI surfaces daemon-unavailable state clearly.
|
||||||
|
|
||||||
|
## Contributor Playbook
|
||||||
|
|
||||||
|
- Before adding a new Apple-side workflow, identify the daemon RPC that should own it.
|
||||||
|
- If the RPC does not exist, add the protocol shape in `proto/burrow.proto`, implement it in the daemon, and only then wire Swift UI.
|
||||||
|
- Verify that no Swift UI or support code calls external control-plane HTTP endpoints directly.
|
||||||
|
- For Tailnet and similar flows, test:
|
||||||
|
- daemon unavailable behavior
|
||||||
|
- successful RPC path
|
||||||
|
- error propagation through the UI
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
- Let Apple UI call control-plane endpoints directly for convenience. Rejected because it creates parallel orchestration paths and breaks the daemon contract.
|
||||||
|
- Allow one-off exceptions for login helpers. Rejected because those exceptions become the architecture.
|
||||||
|
|
||||||
|
## Impact on Other Work
|
||||||
|
|
||||||
|
- Governs the Tailnet refactor and future Apple runtime work.
|
||||||
|
- Interacts with BEP-0002 control-plane bootstrap and BEP-0003 transport refactoring.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Pending.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `Apple/UI/`
|
||||||
|
- `Apple/Core/`
|
||||||
|
- `Apple/NetworkExtension/`
|
||||||
|
- `burrow/src/daemon/`
|
||||||
|
- `burrow/src/control/`
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# `BEP-0006` - Tailnet Authority-First Control Plane
|
||||||
|
|
||||||
|
```text
|
||||||
|
Status: Draft
|
||||||
|
Proposal: BEP-0006
|
||||||
|
Authors: gpt-5.4
|
||||||
|
Coordinator: gpt-5.4
|
||||||
|
Reviewers: Pending
|
||||||
|
Constitution Sections: I, II, IV, V
|
||||||
|
Implementation PRs: Pending
|
||||||
|
Decision Date: Pending
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Burrow should treat Tailnet as one protocol family. Tailscale-managed and self-hosted Headscale-style deployments differ by authority, policy, and auth details, not by a distinct user-facing protocol. Burrow’s config and UI should therefore be authority-first rather than provider-first.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- Splitting Tailscale and Headscale into separate user-facing providers causes fake architectural divergence.
|
||||||
|
- Discovery already naturally returns an authority and optional issuer; that is the stable contract users actually need.
|
||||||
|
- Future managed or enterprise deployments should fit the same model without requiring another protocol picker.
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
|
||||||
|
- Tailnet configuration is centered on:
|
||||||
|
- account
|
||||||
|
- identity
|
||||||
|
- authority/login server URL
|
||||||
|
- optional tailnet name
|
||||||
|
- optional hostname
|
||||||
|
- auth method/material
|
||||||
|
- User-facing surfaces should not force a protocol choice between Tailscale and Headscale.
|
||||||
|
- Provider inference may remain internal metadata for compatibility and diagnostics:
|
||||||
|
- default managed Tailscale authority
|
||||||
|
- custom self-hosted authority
|
||||||
|
- Burrow-owned authority when explicitly applicable
|
||||||
|
- Discovery returns authority and related metadata; editing the authority is the mechanism that moves a configuration from managed default to custom control server.
|
||||||
|
- The daemon and control layer own provider inference; the UI should primarily present “Tailnet” plus the selected authority.
|
||||||
|
|
||||||
|
## Security and Operational Considerations
|
||||||
|
|
||||||
|
- Authority-first config reduces UI complexity and makes misconfiguration easier to reason about.
|
||||||
|
- Provider-specific assumptions must not leak into packet or control-plane semantics unless the authority actually requires them.
|
||||||
|
- Auth material must remain authority-scoped and identity-scoped in daemon storage.
|
||||||
|
|
||||||
|
## Contributor Playbook
|
||||||
|
|
||||||
|
- Remove provider pickers from Tailnet UI unless a concrete protocol difference requires one.
|
||||||
|
- Store the authority explicitly in payloads and infer provider internally only when needed.
|
||||||
|
- Prefer tests that validate authority normalization and discovery behavior over UI-provider branching.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
- Keep separate user-facing providers for Tailscale and Headscale. Rejected because it models deployment shape as protocol shape.
|
||||||
|
- Collapse all control planes into one opaque Burrow provider. Rejected because the authority still matters operationally and diagnostically.
|
||||||
|
|
||||||
|
## Impact on Other Work
|
||||||
|
|
||||||
|
- Refines BEP-0002’s Tailscale-shaped control-plane work.
|
||||||
|
- Constrains the Tailnet Apple refactor and future daemon control-plane storage.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Pending.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `burrow/src/control/`
|
||||||
|
- `Apple/UI/Networks/`
|
||||||
|
- `proto/burrow.proto`
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# `BEP-0007` - Identity Registry and Operator Bootstrap
|
||||||
|
|
||||||
|
```text
|
||||||
|
Status: Draft
|
||||||
|
Proposal: BEP-0007
|
||||||
|
Authors: gpt-5.4
|
||||||
|
Coordinator: gpt-5.4
|
||||||
|
Reviewers: Pending
|
||||||
|
Constitution Sections: II, III, IV, V
|
||||||
|
Implementation PRs: Pending
|
||||||
|
Decision Date: Pending
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Burrow should maintain one canonical registry for project identities, aliases, bootstrap users, SSH keys, and admin-group mappings. Forgejo, Authentik, and related bootstrap configuration should derive from that registry instead of hardcoding overlapping identity facts in multiple modules.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- Burrow currently hardcodes operator and admin/bootstrap user facts directly in host configuration.
|
||||||
|
- Multi-account and self-hosted identity are becoming core architecture, not incidental infra details.
|
||||||
|
- A single registry reduces drift across Forgejo, Authentik, Headscale, SSH authorization, and future control-plane bootstrap.
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
|
||||||
|
- Add a root-level identity registry (`contributors.nix`) as the canonical source of truth for:
|
||||||
|
- usernames
|
||||||
|
- display names
|
||||||
|
- canonical emails
|
||||||
|
- external source emails or aliases
|
||||||
|
- admin scope
|
||||||
|
- bootstrap eligibility
|
||||||
|
- forge authorized SSH keys
|
||||||
|
- named roles
|
||||||
|
- Consume that registry from host configuration for:
|
||||||
|
- Forgejo authorized keys
|
||||||
|
- Forgejo bootstrap admin defaults
|
||||||
|
- Authentik bootstrap users
|
||||||
|
- Burrow user/admin group names
|
||||||
|
- Future work may derive contributor docs, OIDC bootstrap, and additional runtime configuration from the same registry.
|
||||||
|
|
||||||
|
## Security and Operational Considerations
|
||||||
|
|
||||||
|
- Identity drift is a security bug when it affects admin groups, bootstrap accounts, or SSH authorization.
|
||||||
|
- The registry stores metadata only; secrets remain in agenix or other declared secret paths.
|
||||||
|
- Changes to the registry should receive explicit review because they affect access and governance.
|
||||||
|
|
||||||
|
## Contributor Playbook
|
||||||
|
|
||||||
|
- Edit `contributors.nix` first when changing operator, admin, alias, or bootstrap identity state.
|
||||||
|
- Derive runtime configuration from the registry instead of duplicating the same facts elsewhere.
|
||||||
|
- Keep secret references separate from identity metadata.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
- Continue hardcoding users in module options. Rejected because drift is inevitable once Forgejo, Authentik, and Headscale all depend on the same identities.
|
||||||
|
- Create separate per-service user lists. Rejected because it duplicates governance facts and weakens review.
|
||||||
|
|
||||||
|
## Impact on Other Work
|
||||||
|
|
||||||
|
- Supports forge auth, Authentik group sync, and future multi-account Burrow control-plane work.
|
||||||
|
- Creates the basis for stronger contributor and operator provenance later.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Pending.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `contributors.nix`
|
||||||
|
- `nixos/hosts/burrow-forge/default.nix`
|
||||||
|
- `nixos/modules/burrow-authentik.nix`
|
||||||
|
- `nixos/modules/burrow-forge.nix`
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
{ config, self, ... }:
|
{ config, lib, self, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
contributors = import ../../../contributors.nix;
|
||||||
|
identities = contributors.identities;
|
||||||
|
bootstrapUsers = lib.mapAttrsToList
|
||||||
|
(
|
||||||
|
username: identity: {
|
||||||
|
inherit username;
|
||||||
|
name = identity.displayName;
|
||||||
|
email = identity.canonicalEmail;
|
||||||
|
sourceEmail = identity.sourceEmail or null;
|
||||||
|
isAdmin = identity.isAdmin or false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(lib.filterAttrs (_: identity: identity.bootstrapAuthentik or false) identities);
|
||||||
|
forgeAuthorizedKeys = map
|
||||||
|
(username: builtins.readFile identities.${username}.sshPublicKeyPath)
|
||||||
|
(builtins.attrNames (lib.filterAttrs (_: identity: identity.forgeAuthorized or false) identities));
|
||||||
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -59,12 +78,14 @@
|
||||||
|
|
||||||
services.burrow.forge = {
|
services.burrow.forge = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
contactEmail = identities.contact.canonicalEmail;
|
||||||
|
adminUsername = "contact";
|
||||||
|
adminEmail = identities.contact.canonicalEmail;
|
||||||
adminPasswordFile = "/var/lib/burrow/intake/forgejo_pass_contact_at_burrow_net.txt";
|
adminPasswordFile = "/var/lib/burrow/intake/forgejo_pass_contact_at_burrow_net.txt";
|
||||||
|
oidcAdminGroup = contributors.groups.admins;
|
||||||
|
oidcRestrictedGroup = contributors.groups.users;
|
||||||
oidcClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path;
|
oidcClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path;
|
||||||
authorizedKeys = [
|
authorizedKeys = forgeAuthorizedKeys;
|
||||||
(builtins.readFile ../../keys/contact_at_burrow_net.pub)
|
|
||||||
(builtins.readFile ../../keys/agent_at_burrow_net.pub)
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.burrow.forgeRunner = {
|
services.burrow.forgeRunner = {
|
||||||
|
|
@ -92,22 +113,9 @@
|
||||||
googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path;
|
googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path;
|
||||||
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
|
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
|
||||||
googleLoginMode = "redirect";
|
googleLoginMode = "redirect";
|
||||||
bootstrapUsers = [
|
userGroupName = contributors.groups.users;
|
||||||
{
|
adminGroupName = contributors.groups.admins;
|
||||||
username = "contact";
|
bootstrapUsers = bootstrapUsers;
|
||||||
name = "Burrow";
|
|
||||||
email = "contact@burrow.net";
|
|
||||||
sourceEmail = "net.burrow@gmail.com";
|
|
||||||
isAdmin = true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
username = "conrad";
|
|
||||||
name = "Conrad Kramer";
|
|
||||||
email = "conrad@burrow.net";
|
|
||||||
sourceEmail = "ckrames1234@gmail.com";
|
|
||||||
isAdmin = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.burrow.headscale = {
|
services.burrow.headscale = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue