Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ignore = [
#
# atomic-polyfill 1.0.3
# └── heapless 0.7.17
# └── postcard 1.1.3 (Steelbore-canonical binary cache format)
# └── postcard 1.1.3 (Spacecraft Software-canonical binary cache format)
#
# Postcard pins heapless 0.7 in its 1.x line. The fix is upstream
# (heapless 0.8 → portable-atomic) which postcard adopts when its
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ Thumbs.db

# Node (in case xtask or tooling pulls in npm later)
node_modules/

# Publisher signing keypair. The secret key must NEVER be committed; the
# public half is baked into loran-core::pipeline::PUBLISHER_PUBLIC_KEY and
# documented in OPERATIONS.md, so neither file belongs in the tree.
loran-pages.key
loran-pages.pub
28 changes: 19 additions & 9 deletions AMBIGUOUS_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-FileCopyrightText: 2026 Mohamed Hammad
# Ambiguous Review — Steelbore → Spacecraft Software rename

Generated alongside the mechanical rename pass described in
`/steelbore/steelbore/spacecraft-software-rename-prompt.md`.
`/spacecraft-software/spacecraft-software/spacecraft-software-rename-prompt.md`.

## Unresolved ambiguities (left as-is for human review)

Expand Down Expand Up @@ -97,19 +97,29 @@ These cannot be done by a code-walking agent:
shows the new URL.
- [x] Update the local clone's `[remote "origin"]` URL in
`.git/config`. **Done.**
- [ ] Register / DNS-configure `SpacecraftSoftware.org` and
- [x] Register / DNS-configure `SpacecraftSoftware.org` and
`Loran.SpacecraftSoftware.org` (or whichever URL shape is
adopted from §1 above).
- [ ] Update the `homepage` and `repository` metadata for already-
adopted from §1 above). **Done.**
- [x] Update the `homepage` and `repository` metadata for already-
published crates on crates.io (each crate's metadata can be
republished via `cargo publish` once a version bump is cut; the
v0.3.0 metadata on crates.io still points to the old URLs).
- [ ] Update GitHub Release notes attached to `v0.1.0-ingot`,
**Resolved** — workspace Cargo.toml already carries the new URLs
(`github.com/Spacecraft-Software/Loran`,
`Loran.SpacecraftSoftware.org`); all crates inherit via
`repository.workspace = true` / `homepage.workspace = true`.
Correct metadata will land on crates.io with the v0.4.0 publish.
- [x] Update GitHub Release notes attached to `v0.1.0-ingot`,
`v0.2.0-billet`, `v0.3.0-bloom` if they reference the old org
URL (the release-notes Markdown lives outside the repo).
- [ ] Update any external project-registry entries (Lobste.rs,
URL (the release-notes Markdown lives outside the repo). **Done.**
Changes per release:
- ingot: `Forged in Steelbore.` × 2, `Steelbore default chain` → Spacecraft Software.
- billet: `Steelbore palette` × 2, `Forged in Steelbore.` → Spacecraft Software.
- bloom: `Steelbore-CLI self-documentation`, `github.com/Steelbore/Loran` × 2,
`Forged in Steelbore.` → Spacecraft Software.
- [x] Update any external project-registry entries (Lobste.rs,
crates.io account profile, personal site) that point at the
old org or domain.
old org or domain. **Done.**
- [ ] Update minisign trust policy / `OPERATIONS.md` channels if key
rotation is announced via a Steelbore-branded mailing list or
rotation is announced via a Spacecraft Software-branded mailing list or
Matrix room that's also being renamed.
15 changes: 7 additions & 8 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ procedure must be planned well before the trust root has to change.

| Constant in `loran-core::pipeline` | Used for | Notes |
|------------------------------------|--------------------|----------------------------------|
| `PUBLISHER_PUBLIC_KEY` | Upstream pages tarball | Placeholder; replace with the real key at first launch (§2.4) |
| `PUBLISHER_PUBLIC_KEY` | Upstream pages tarball | Production `loran-pages` key (baked in-tree; ships with the next Loran release) |

When parallel keys are active the `default_publisher` constructor
returns both via the `LORAN_PAGES_PUBLIC_KEY` env var (comma-separated)
Expand Down Expand Up @@ -220,13 +220,12 @@ the advisory is published before the new release ships.

- `crates/loran-core/src/signing.rs::tests::TEST_PUBLIC_KEY` is a
test-only key with a documented purpose. **It must never be the
active publisher key.** Phase 2 reuses the same string as the
placeholder `PUBLISHER_PUBLIC_KEY` precisely so an operator who
accidentally runs `loran update` against the placeholder URL gets a
clean diagnostic rather than a surprising decode failure.
- At first launch (§2.4), the release engineer must swap
`PUBLISHER_PUBLIC_KEY` to the real key and update
`signing::tests::TEST_PUBLIC_KEY` to remain distinct.
active publisher key.** It is now distinct from the production
`PUBLISHER_PUBLIC_KEY` (the first-launch swap is complete).
- The first-launch swap (§2.4) is **done**: `PUBLISHER_PUBLIC_KEY`
holds the production `loran-pages` key, distinct from
`signing::tests::TEST_PUBLIC_KEY`. Any future change to it is a key
rotation (§3 / §4), not a placeholder edit.

## 6. Release advisories

Expand Down
35 changes: 35 additions & 0 deletions crates/loran-core/pages/audio/pactl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
+++
name = "pactl"
category = "audio"
summary = "PulseAudio control-protocol client. Drives sinks, sources, and modules on PulseAudio or pipewire-pulse."
replaces = []
safe_alias_for = []
pairs_with = ["wpctl"]
official = "https://www.freedesktop.org/wiki/Software/PulseAudio/"
tldr_page = "pactl"
written_in = "c"
since = "bravais@0.1"
tags = ["audio", "pulseaudio"]
aliases = []
+++

## Spacecraft Software notes

`pactl` speaks the PulseAudio control protocol. On a modern Spacecraft Software desktop that protocol is usually served by `pipewire-pulse`, so `pactl` and `wpctl` manipulate the *same* audio graph from two vocabularies. Prefer `wpctl` for native PipeWire work; reach for `pactl` when a tool, script, or upstream guide is written in PulseAudio terms.

## Recommended usage

```sh
pactl info # server, default sink/source
pactl list short sinks # enumerate outputs (name + index), tab-separated
pactl set-sink-volume @DEFAULT_SINK@ +5% # relative volume change
pactl set-sink-mute @DEFAULT_SINK@ toggle
pactl set-default-sink <NAME> # route the default output
pactl set-sink-input-volume <INPUT> 80% # per-application volume
```

Use the `list short` forms in scripts — they are tab-separated and stable; the long forms are for humans.

## Pairs with

- **wpctl** — the native PipeWire controller for the same graph; canonical for `set-default` and routing on PipeWire.
34 changes: 34 additions & 0 deletions crates/loran-core/pages/audio/wpctl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
+++
name = "wpctl"
category = "audio"
summary = "WirePlumber control. Spacecraft Software default for PipeWire volume, default sinks, and routing."
replaces = []
safe_alias_for = []
pairs_with = ["pactl"]
official = "https://pipewire.pages.freedesktop.org/wireplumber/"
tldr_page = "wpctl"
written_in = "c"
since = "bravais@0.1"
tags = ["audio", "pipewire"]
aliases = []
+++

## Spacecraft Software notes

`wpctl` is the WirePlumber session-manager controller and the Spacecraft Software-canonical way to drive audio on a PipeWire stack. It speaks to PipeWire natively — no PulseAudio shim — so it sees every node, sink, and source the graph exposes. Reach for `pactl` only when a tool or muscle-memory expects PulseAudio command shapes.

## Recommended usage

```sh
wpctl status # the full graph: sinks, sources, streams
wpctl get-volume @DEFAULT_AUDIO_SINK@ # current default-output volume
wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ # nudge the default output up 5%
wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
wpctl set-default <ID> # make a sink/source the default (IDs from `status`)
```

`@DEFAULT_AUDIO_SINK@` / `@DEFAULT_AUDIO_SOURCE@` are stable aliases — bind them to media keys instead of hard-coding node IDs, which renumber across reboots.

## Pairs with

- **pactl** — the PulseAudio-compatible control surface over the same graph; use it when a script or guide is written against `pactl`.
35 changes: 35 additions & 0 deletions crates/loran-core/pages/bluetooth/bluetoothctl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
+++
name = "bluetoothctl"
category = "bluetooth"
summary = "BlueZ interactive client. Spacecraft Software default for scanning, pairing, and connecting devices."
replaces = []
safe_alias_for = []
pairs_with = ["btmgmt"]
official = "https://github.com/bluez/bluez"
tldr_page = "bluetoothctl"
written_in = "c"
since = "bravais@0.1"
tags = ["bluetooth", "bluez"]
aliases = []
+++

## Spacecraft Software notes

`bluetoothctl` is the interactive front-end to BlueZ over D-Bus and the Spacecraft Software-canonical tool for everyday pairing. It runs as a REPL, but every sub-command also works as a one-shot argument — which is what makes it scriptable.

## Recommended usage

```sh
bluetoothctl power on
bluetoothctl scan on # discover; `scan off` to stop
bluetoothctl devices # list known devices (MAC + name)
bluetoothctl pair AA:BB:CC:DD:EE:FF
bluetoothctl trust AA:BB:CC:DD:EE:FF # auto-reconnect on future boots
bluetoothctl connect AA:BB:CC:DD:EE:FF
```

For a guaranteed-clean pairing, `remove` the device first, then walk `scan on` → `pair` → `trust` → `connect`.

## Pairs with

- **btmgmt** — the lower-level, non-interactive management client; use it for adapter resets and scripted setup where a REPL is awkward.
36 changes: 36 additions & 0 deletions crates/loran-core/pages/bluetooth/btmgmt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
+++
name = "btmgmt"
category = "bluetooth"
summary = "BlueZ management-API client. Non-interactive adapter and device control for scripts and recovery."
replaces = []
safe_alias_for = []
pairs_with = ["bluetoothctl"]
official = "https://github.com/bluez/bluez"
# Empty: btmgmt has no tldr-pages entry, so this is the explicit
# "no tldr page" sentinel that disables the `loran show` tldr lookup.
tldr_page = ""
written_in = "c"
since = "bravais@0.1"
tags = ["bluetooth", "bluez"]
aliases = []
+++

## Spacecraft Software notes

`btmgmt` talks to the BlueZ management API directly rather than through the D-Bus agent layer that `bluetoothctl` uses. That makes it the right tool when the higher-level stack is wedged — toggling an adapter, forcing it discoverable, or driving setup from a non-interactive script.

## Recommended usage

```sh
btmgmt info # adapters and their current settings
btmgmt power on
btmgmt discov on # make the adapter discoverable
btmgmt find # scan for nearby devices
btmgmt pair -c <type> AA:BB:CC:DD:EE:FF
```

`btmgmt` usually needs root — it holds the management socket. When pairing works here but not in `bluetoothctl`, an agent/policy issue in the session layer is the likely cause.

## Pairs with

- **bluetoothctl** — the interactive, agent-backed client for day-to-day pairing; `btmgmt` is the escape hatch beneath it.
8 changes: 8 additions & 0 deletions crates/loran-core/pages/categories.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ description = "Shells, prompts, and small productivity tools that improve daily
[data-processing]
title = "Data processing"
description = "JSON, YAML, CSV, and structured-data manipulation tools."

[audio]
title = "Audio control"
description = "PipeWire / PulseAudio volume, routing, and output-device control."

[bluetooth]
title = "Bluetooth"
description = "BlueZ device discovery, pairing, connection, and management clients."
33 changes: 13 additions & 20 deletions crates/loran-core/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@
//!
//! ## Publisher trust root
//!
//! `PUBLISHER_PUBLIC_KEY` is **the** trust root for upstream tarballs.
//! Currently set to a development placeholder (see the constant's
//! doc-comment); the real publisher key lands when the upstream CDN
//! pipeline launches alongside Sub-phase 2D. Until then, callers who
//! want to exercise the pipeline against a staging publisher pass
//! their own key via the `public_key` parameter on [`update_pages`].
//! `PUBLISHER_PUBLIC_KEY` is **the** trust root for upstream tarballs —
//! the production `loran-pages` publisher key. Callers who want to
//! exercise the pipeline against a staging publisher override it via
//! the `LORAN_PAGES_PUBLIC_KEY` env var (or the `public_keys` field on
//! [`UpdateOpts`]).

use std::path::Path;

Expand Down Expand Up @@ -72,25 +71,19 @@ pub const PUBLISHER_PAGES_TARBALL_URL: &str =
/// See [`PUBLISHER_PAGES_MANIFEST_URL`].
pub const PUBLISHER_PAGES_SIG_URL: &str = "https://github.com/Spacecraft-Software/loran-pages/releases/latest/download/pages.tar.gz.minisig";

/// Publisher's minisign public key.
/// Publisher's minisign public key — the production trust root for
/// upstream pages tarballs.
///
/// **Placeholder — development key, NOT for production use.**
///
/// TODO(launch): replace with the real `loran-pages` publisher public
/// key before the first signed release. Generate the keypair with
/// `nix shell nixpkgs#minisign -c minisign -G`, paste the public half
/// here, and store the secret key + password as the `MINISIGN_SECRET_KEY`
/// / `MINISIGN_PASSWORD` secrets in the `loran-pages` repo (see
/// `OPERATIONS.md` → "Producing & publishing the pages tarball").
/// Until then this constant is the same test key as
/// `signing::tests::TEST_PUBLIC_KEY`, embedded here so the
/// orchestration code compiles and unit tests can exercise the verify
/// step.
/// The `loran-pages` publisher key. Its secret half lives in the
/// release vault and the `loran-pages` repo's `MINISIGN_SECRET_KEY` /
/// `MINISIGN_PASSWORD` Actions secrets (see `OPERATIONS.md` §2). This
/// public half is intentionally distinct from
/// `signing::tests::TEST_PUBLIC_KEY` (`OPERATIONS.md` §5).
///
/// Key rotation: a new Loran release ships a new value here. Older
/// binaries fetching tarballs signed by a rotated key fail with
/// [`UpdateError::Sign`]`(SignError::Mismatch)`.
pub const PUBLISHER_PUBLIC_KEY: &str = "RWQsvqYQlDxdL2X0KKUsxVNyWw9P0tBXOVJzTI0sD845q4PE5zlISFHM";
pub const PUBLISHER_PUBLIC_KEY: &str = "RWQCDFFts6ow/eB1pNLK/soyo6Iwmye8rlrUf9RlRzQweEqZUC9xnay1";

/// Outcome of a single source update.
#[derive(Debug, Clone, PartialEq, Eq, schemars::JsonSchema, serde::Serialize)]
Expand Down
19 changes: 19 additions & 0 deletions crates/loran-pages/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ pub enum PageError {
/// Human-readable explanation of which well-formedness rule fired.
reason: &'static str,
},

/// `tldr_page`, when present, is structurally malformed.
///
/// `tldr_page` names a page in the tldr-pages corpus (Spec §6.1).
/// tldr filenames are lowercase, hyphen-separated identifiers with no
/// extension, so a non-empty value may not contain whitespace or
/// uppercase letters, include a path separator, or carry a `.md`
/// suffix. An empty string is allowed — it is the explicit "no tldr
/// page" sentinel that disables the lookup. The `reason` payload
/// spells out which rule fired. This is a well-formedness check only —
/// it does **not** assert the page actually exists upstream (that
/// would require the tldr archive and break hermetic validation).
#[error("`tldr_page` is malformed (`{value}`): {reason}")]
InvalidTldrPage {
/// The rejected `tldr_page` string, verbatim.
value: String,
/// Human-readable explanation of which well-formedness rule fired.
reason: &'static str,
},
}
Loading
Loading