Skip to content
Merged
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
4 changes: 4 additions & 0 deletions projects/start-os/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ file tracks notable changes since the move to the monorepo.

### Fixed

- **Transfers preserve the source filesystem format.** StartOS mounts source
filesystems read-only while copying persistent data, repairing ext4 only when
needed to mount it. This leaves the source drive available as a fallback.

- **An app that remembers your server's certificate sees the same certificate
across every route to that name.** Wallets and other apps that pin the first
certificate they are shown — Sparrow and the Electrum clients most visibly —
Expand Down
2 changes: 1 addition & 1 deletion projects/start-os/docs/src/initial-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ After [installing StartOS](installing-startos.md), follow these steps to initial

- **Restore from Backup**: Select this option _only_ if your existing StartOS data drive has been lost or corrupted. This is for disaster recovery only. The restored server keeps the source server's name.

- **Transfer**: Select this option if you are transferring your existing data from one drive to another. The transferred server keeps the source server's name.
- **Transfer**: Select this option if you are transferring your existing data from one drive to another. StartOS mounts the source filesystems read-only while copying. If an ext4 filesystem must be repaired before it can be mounted, StartOS repairs it and retries. The source drive remains a usable copy if the transfer fails. The transferred server keeps the source server's name. Once the new drive is running, do not boot the old one as a server again.

1. Set a strong master password. _Make it good. Write it down_. Resetting your password is non-trivial, but your data will be preserved.

Expand Down
2 changes: 1 addition & 1 deletion shared-libs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ read its own `AGENTS.md` first. `CLAUDE.md` is a one-line `@AGENTS.md` import. S
## Layout

- `crates/start-core/` — Rust backend lib (`start-core`, lib name `start_core`).
Has its own `AGENTS.md`, `ARCHITECTURE.md`, `CONTRIBUTING.md`, plus topic notes
Has its own `AGENTS.md` and `ARCHITECTURE.md`, plus topic notes
(`core-rust-patterns.md`, `i18n-patterns.md`, `patchdb.md`, `rpc-toolkit.md`,
`s9pk-structure.md`, `exver.md`, `VERSION_BUMP.md`).
- `ts-modules/` — shared TypeScript modules; the `@start9labs/shared`
Expand Down
4 changes: 2 additions & 2 deletions shared-libs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Contributing to shared-libs

`shared-libs/` groups two cross-product libraries. Contribute inside the relevant
sub-library; each has its own `CONTRIBUTING.md` with the full detail.
sub-library and follow its `AGENTS.md`.

## Documentation

Expand Down Expand Up @@ -32,7 +32,7 @@ cd shared-libs/crates/start-core && ./run-tests.sh
- Local `cargo check` is linux-only. CI builds an apple-darwin + linux-musl
matrix; consider those targets for any change touching `libc`/platform APIs or
dependencies (cfg-gate platform code rather than reimplementing it).
- See [`crates/start-core/CONTRIBUTING.md`](crates/start-core/CONTRIBUTING.md)
- See [`crates/start-core/AGENTS.md`](crates/start-core/AGENTS.md)
and the topic notes (`core-rust-patterns.md`, `patchdb.md`, `rpc-toolkit.md`,
`i18n-patterns.md`, `VERSION_BUMP.md`).

Expand Down
24 changes: 19 additions & 5 deletions shared-libs/crates/start-core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ All six product bins (`startbox`/`startd`, `start-container`, `start-cli`, `regi
`tunnelbox`, `startwrt`) link against it; all but `startwrt` are thin wrappers in the product
crates (`startwrt` is a full backend of its own that imports this crate aliased as `startos`).

`CLAUDE.md` is a one-line `@AGENTS.md` import. See [ARCHITECTURE.md](ARCHITECTURE.md) and
[CONTRIBUTING.md](CONTRIBUTING.md).
`CLAUDE.md` is a one-line `@AGENTS.md` import. See [ARCHITECTURE.md](ARCHITECTURE.md).

Topical references: [rpc-toolkit.md](rpc-toolkit.md), [patchdb.md](patchdb.md),
[i18n-patterns.md](i18n-patterns.md), [core-rust-patterns.md](core-rust-patterns.md),
Expand All @@ -24,19 +23,34 @@ Topical references: [rpc-toolkit.md](rpc-toolkit.md), [patchdb.md](patchdb.md),

## Build & test (run from the repo root)

Install stable Rust and Docker. `rust-analyzer` is recommended.

- `cargo check -p start-core` — type-check the library.
- `make start-core-test` — run the test suite (wraps `run-tests.sh`, which uses the `rust-zig-builder`
container and the `test` feature; skips `export_` tests). Or run a single test directly:
`cargo test -p start-core <name> --features=test`.
- `make start-core-format` — format this crate (`make start-core-format-check` for the read-only CI check). Nightly is required for formatting.
- `make start-core-format` — format the shared Rust crates with the pinned nightly container (`make start-core-format-check` checks formatting). Set `FMT_NATIVE=1` only when that nightly is installed locally.
- `cargo build -p start-os --bin startbox` (or the other product crate/bin) to build a binary.

Keep `README.md`, `ARCHITECTURE.md`, this file, and the topical references current when changing
project structure, conventions, build process, or product context.

## Adding an RPC endpoint

1. Define serializable request and response types.
2. Choose a handler type (`from_fn_async` for most cases).
3. Write the async handler.
4. Register it in the appropriate `ParentHandler` tree.
5. Add `#[derive(TS)]` and `#[ts(export)]` where TypeScript needs the types.

See [rpc-toolkit.md](rpc-toolkit.md) for handler patterns.

## Gotchas

- **Process invocation: use `.invoke(ErrorKind::...)`, not `.status()`.** When running CLI commands via `tokio::process::Command`, the `Invoke` trait (from `crate::util::Invoke`) captures stdout/stderr and checks exit codes. `.status()` leaks stderr directly to system logs and creates noise in production. For check-then-act patterns (e.g. `iptables -C`), use `.invoke(...).await.is_ok()` / `.is_err()` instead of `.status().await.map_or(false, |s| s.success())`.
- **File I/O: prefer `crate::util::io` over `tokio::fs`** when an equivalent helper exists. These helpers add error context and mount-aware behavior that `tokio::fs` doesn't.
- **i18n is mandatory for any user-facing string** — including CLI subcommand descriptions (`about.<name>`), CLI arg help (`help.arg.<name>`), error messages, notifications, and setup messages. All 5 locales (`en_US`, `de_DE`, `es_ES`, `fr_FR`, `pl_PL`) must be filled in `locales/i18n.yaml` (i.e. `shared-libs/crates/start-core/locales/i18n.yaml`), alphabetically ordered within their section. See `i18n-patterns.md`. Compile-time validation catches missing keys.
- **`#[ts(export)]` changes need a multi-step rebuild.** Editing a `#[ts(export)]` struct/enum in Rust does _not_ update web or container-runtime. From the repo root, run `make start-core-ts-bindings` (regenerates `shared-libs/crates/start-core/bindings/`, then rsyncs into `shared-libs/ts-modules/start-core/lib/osBindings/`) and then rebuild the affected TS lib(s): `cd shared-libs/ts-modules/start-core && make dist` (what web consumes) and/or `cd projects/start-sdk && make bundle` (the SDK bundle container-runtime consumes). See [ARCHITECTURE.md](ARCHITECTURE.md#cross-layer-verification).
- **i18n is mandatory for any user-facing string** — including CLI subcommand descriptions (`about.<name>`), CLI arg help (`help.arg.<name>`), error messages, notifications, and setup messages. All 5 locales (`en_US`, `de_DE`, `es_ES`, `fr_FR`, `pl_PL`) must be filled in `locales/i18n.yaml` (i.e. `shared-libs/crates/start-core/locales/i18n.yaml`), alphabetically ordered within their section. Use `t!()`, match the key namespace to the module path, and use kebab-case for multi-word segments. See `i18n-patterns.md`. Compile-time validation catches missing keys.
- **`#[ts(export)]` changes need a multi-step rebuild.** Use `#[serde(rename_all = "camelCase")]` for JavaScript field names. Add `#[ts(type = "string")]` for unsupported string-backed Rust types and `#[ts(type = "number")]` when a `u64` should not become `bigint`. Editing a `#[ts(export)]` struct/enum in Rust does _not_ update web or container-runtime. From the repo root, run `make start-core-ts-bindings` (regenerates `shared-libs/crates/start-core/bindings/`, then rsyncs into `shared-libs/ts-modules/start-core/lib/osBindings/`) and then rebuild the affected TS lib(s): `cd shared-libs/ts-modules/start-core && make dist` (what web consumes) and/or `cd projects/start-sdk && make bundle` (the SDK bundle container-runtime consumes). See [ARCHITECTURE.md](ARCHITECTURE.md#cross-layer-verification).
- **A `///` doc comment on a `#[ts(export)]` type lands in the binding** as a TSDoc block (see `CheckPortV6Res.ts`). Adding or editing one changes the generated `.ts`, so it is not a comment-only change — regenerate.
- **A change to the CLI surface needs `make manpages`.** Adding or renaming a subcommand, or editing the `about.*`/`help.arg.*` key behind one, changes the committed man pages under `projects/*/man/` — a new subcommand adds a page _and_ a line to its parent's. The `Generated Artifacts` CI job regenerates them and fails on any drift, in the same job as the TS bindings, so a CLI change and a `#[ts(export)]` change each need their own regeneration step. Note the gate reads `git status`, so staging the result is not enough — commit it.
- **Bindings can only come from a compile.** ts-rs emits each binding from an `export_bindings_*` `#[test]` fn the derive macro generates inside this lib, so `make start-core-ts-bindings` necessarily builds the crate; there is no lighter command, and hand-editing a generated file just fails the CI gate. If the build does not fit locally, push the branch instead: the `Generated Artifacts` job regenerates the bindings and the man pages, and a failing gate uploads them as the `generated-artifacts` artifact to download and commit. It is heavier than it needs to be because the root `Cargo.toml` sets `[profile.test] opt-level = 3` — on a constrained machine, cap the container (`--cpus`/`--memory`) and pass `CARGO_BUILD_JOBS`, which doesn't change fingerprints and so reuses a warm `target/`.
Expand Down
3 changes: 1 addition & 2 deletions shared-libs/crates/start-core/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ Until both steps run, a changed `#[ts(export)]` type is out of sync with everyth
## Further reading

- [README.md](README.md) — what this crate is and how to use it
- [CONTRIBUTING.md](CONTRIBUTING.md) — build, test, format, and contribution workflow
- [AGENTS.md](AGENTS.md) — operating rules for AI/dev work (`CLAUDE.md` is a one-line `@AGENTS.md` import)
- [AGENTS.md](AGENTS.md) — build, test, format, and operating rules (`CLAUDE.md` is a one-line `@AGENTS.md` import)
- [rpc-toolkit.md](rpc-toolkit.md) — JSON-RPC handler patterns
- [patchdb.md](patchdb.md) — Patch-DB watch patterns and TypedDbWatch
- [i18n-patterns.md](i18n-patterns.md) — Internationalization conventions
Expand Down
80 changes: 0 additions & 80 deletions shared-libs/crates/start-core/CONTRIBUTING.md

This file was deleted.

7 changes: 3 additions & 4 deletions shared-libs/crates/start-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ than enabling `MultiExecutable` entrypoints.

## Requirements

- [Rust](https://rustup.rs) (nightly toolchain for formatting)
- [Rust](https://rustup.rs) (stable; formatting uses a pinned nightly container)
- [rust-analyzer](https://rust-analyzer.github.io/) recommended
- [Docker](https://docs.docker.com/get-docker/) (for the `rust-zig-builder` cross-compile container used by the test/build scripts)

Expand All @@ -39,16 +39,15 @@ Run from the repo root (the single Cargo workspace):
```bash
cargo check -p start-core # type-check the library
make start-core-test # run the test suite (wraps run-tests.sh)
make start-core-format # nightly rustfmt on this crate
make start-core-format # pinned nightly rustfmt for shared Rust crates
```

To build a product binary, build its crate, e.g. `cargo build -p start-os --bin startbox`.

## Documentation

- [ARCHITECTURE.md](ARCHITECTURE.md) — module map, RPC pattern, patch-db data flow
- [CONTRIBUTING.md](CONTRIBUTING.md) — build, test, format, and contribution workflow
- [AGENTS.md](AGENTS.md) — operating rules for AI/dev work in this crate
- [AGENTS.md](AGENTS.md) — build, test, format, and operating rules for this crate

Topical deep-dives: [rpc-toolkit.md](rpc-toolkit.md), [patchdb.md](patchdb.md),
[i18n-patterns.md](i18n-patterns.md), [core-rust-patterns.md](core-rust-patterns.md),
Expand Down
7 changes: 7 additions & 0 deletions shared-libs/crates/start-core/locales/i18n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ setup.restoring-backup:
fr_FR: "Restauration de la sauvegarde"
pl_PL: "Przywracanie kopii zapasowej"

setup.transfer-source-is-destination:
en_US: "The transfer source and destination must be different drives"
de_DE: "Quell- und Ziellaufwerk der Übertragung müssen unterschiedlich sein"
es_ES: "Las unidades de origen y destino de la transferencia deben ser diferentes"
fr_FR: "Les disques source et destination du transfert doivent être différents"
pl_PL: "Dysk źródłowy i docelowy transferu muszą być różne"

setup.transferring-data:
en_US: "Transferring data"
de_DE: "Daten werden übertragen"
Expand Down
6 changes: 3 additions & 3 deletions shared-libs/crates/start-core/src/bins/start_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext};
use crate::disk::REPAIR_DISK_PATH;
use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::main::{DEFAULT_PASSWORD, ImportMode};
use crate::firmware::{check_for_firmware_update, update_firmware};
use crate::init::{InitPhases, STANDBY_MODE_PATH};
use crate::net::gateway::WildcardListener;
Expand Down Expand Up @@ -149,11 +149,11 @@ async fn setup_or_init(
let requires_reboot = crate::disk::main::import(
&*disk_guid,
DATA_DIR,
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
ImportMode::ReadWrite(if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive
} else {
RepairStrategy::Preen
},
}),
if disk_guid.ends_with("_UNENC") {
None
} else {
Expand Down
16 changes: 15 additions & 1 deletion shared-libs/crates/start-core/src/disk/fsck/ext4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ use crate::disk::fsck::RequiresReboot;
pub async fn e2fsck_preen(
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname).await
e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname, false).await
}

pub async fn e2fsck_preen_strict(
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname, true).await
}

fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Error>> {
Expand Down Expand Up @@ -51,13 +57,15 @@ pub async fn e2fsck_aggressive(
e2fsck_runner(
Command::new("e2fsck").arg("-y").arg("-z").arg(undo_path),
logicalname,
false,
)
.await
}

async fn e2fsck_runner(
e2fsck_cmd: &mut Command,
logicalname: impl AsRef<Path> + std::fmt::Debug,
fail_on_uncorrected: bool,
) -> Result<RequiresReboot, Error> {
let e2fsck_out = e2fsck_cmd.arg(logicalname.as_ref()).output().await?;
let e2fsck_stderr = String::from_utf8(e2fsck_out.stderr)?;
Expand Down Expand Up @@ -86,6 +94,12 @@ async fn e2fsck_runner(
),
);
}
if fail_on_uncorrected && code & 4 != 0 {
return Err(Error::new(
eyre!("{}", t!("disk.fsck.e2fsck-error", stderr = e2fsck_stderr)),
crate::ErrorKind::DiskManagement,
));
}
if code < 8 {
if code & 2 != 0 {
tracing::warn!("{}", t!("disk.fsck.reboot-required"));
Expand Down
Loading
Loading