Commit Graph

18 Commits

Author SHA1 Message Date
e55c9d5caa fix(installer): keep user_created sticky across re-runs
Found on the first real systemd host this installer has ever run on: a
container with systemd as PID 1, installing into /usr/bin, /etc and
/var/lib for real.

`service::prepare` answers "did THIS run create the service account",
which is false on every run after the first — by then the account exists.
Recording that verbatim made the field describe the run rather than the
state, with two consequences:

- `install.json` changed on an otherwise-identical second run, breaking
  the Phase 1 promise that a re-run with nothing new to do writes
  nothing.
- `uninstall` removes only an account it created, so after any second
  `install` it silently left behind the very user this tool had added.
  Reproduced before the fix: "left the runicgateway account alone — this
  installer did not create it", on a host where it plainly had.

The record now inherits `true` from a prior record naming the same
account, and only that account: inheriting across a rename would
authorize deleting a user this installer never made.

Not visible on Windows, where the SCM's virtual account is never created
by us and goes with the service — which is why three phases of Windows
smoke runs never showed it.

Verified after the fix on the same host: fresh install records
user_created true, an identical second run leaves install.json
byte-identical, and uninstall then removes the account, the unit, the
service and the binary — leaving sidecar.toml, the database and all 24
overlay files in the ServUO tree exactly where they were.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:59:26 -05:00
80b1c0da24 feat(installer): implement Phase 4 — doctor, update and uninstall
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 59s
Completes the command surface INSTALL.md §2 published before the binary
existed. With this, `edge` cuts a binary that does everything that guide
describes.

doctor (src/doctor.rs)
  Reads only. Every row is answered by asking the thing itself — the
  installed binary (--version, --print-config), the service manager, and
  the sidecar's /health — because the record says what `install` did,
  which is a different question from what is true now. --print-config is
  run ONLY when the config already exists: that flag provisions, and a
  diagnosis must not create the state it reports on. It is also run under
  the environment the service pins (UOLINK_DB_PATH), so the config and
  database it names are the ones the service opens, not the ones the
  binary would pick on its own.

  Exit 1 when any row failed, so a monitoring script can read it; a ⚠
  never does that. A stopped shard is therefore a ⚠, not a ✗ — "you have
  not started it" and "it is running and the bridge is dead" are
  different problems and only the second is broken. Offline is a ⚠ too:
  a shard host with no route to Gitea is a supported way to run this.

  The patch row re-resolves each recorded patch against the tree from the
  cached .patch, so a core upgrade or a restored backup that silently
  removed the tier's edits is caught — nothing else here would notice.

update (src/update.rs, install.rs::Mode)
  The same pipeline as install, not a second one: PLAN.md describes it as
  "re-resolve the bundle, then move both components to it", which is what
  an install over an existing deployment already does. Writing it twice
  would give the sync rules and the protocol cross-checks two places to
  disagree. What differs is small and lives in Mode — a prior record is
  required, the tree comes from that record rather than detection, the
  patch tier's scope narrows, and the close is a diff instead of a
  handoff.

  The token is not reprinted: it has not changed and the website has it.
  A changed protocol number IS called out, because a stale value in
  Admin → Shard is answered with 409 and looks like the shard going
  offline.

  Tier scope: features an earlier run recorded are re-resolved without
  asking again (the record is the evidence of consent, including on an
  unsupported ServUO); anything new the release offers is named but not
  applied without --patches. A shard that declined stays declined.

uninstall (src/uninstall.rs, service::remove)
  Removes the binary, the service and install.json; prints the overlay
  files and the exact hunks, rendered from the cached patches with the
  rung each landed at. Files edited since deployment are flagged so
  nobody deletes their own work blind. The report is also written to a
  file in the working directory — it is the only thing still needed after
  the command exits, and it arrives at the end of the longest output this
  tool produces.

  Two deviations from PLAN.md §5, both deliberate:

  - The cached patch set and patches/originals/ SURVIVE. That table put
    them under "removed", but the report tells the operator to diff
    against those originals — advice the same command would have made
    impossible to follow. --purge removes them, with the config and the
    database.
  - --yes means yes here, not "take the default". The prompt defaults to
    no (destructive), but the operator typed the verb; reading --yes as
    "no" would leave an unattended uninstall unable to express itself,
    and a script that appears to succeed while removing nothing is the
    worse failure.

  Exit 1 if a step could not be carried out — everything else still was.

Verified on this machine against a scratch tree built from the real
ServUO 57.4 files: a healthy doctor (exit 0), one with a deleted overlay
file, an edited one and a reverted patch (all three found, exit 1), a
--verify update that wrote nothing, a real update that repaired all three
and left install.json byte-identical, uninstall with and without --purge,
a second uninstall, and doctor/update on a host with no record. Linux
fmt/clippy/tests run in Docker as well as the Windows host.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 02:57:57 -05:00
0043264482 Merge pull request 'feat(installer): implement Phase 3 — the patch tier' (#6) from feat/phase3-patch-tier into edge
Reviewed-on: #6
2026-08-05 01:08:06 +00:00
265911a58f docs(installer): drop phase references that are now this build's behaviour
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m8s
Five comments described the patch tier as work a later phase would do. It is
this phase, so they read as stale the moment the code landed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 20:02:52 -05:00
02c5ad9839 fix(installer): satisfy clippy's unnecessary_sort_by on the CI toolchain
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 58s
The two descending sorts in the applier used an explicit comparator. CI runs
clippy 1.97, where `unnecessary_sort_by` flags that and `-D warnings` turns it
into a build failure; the local toolchain here is 1.94, which does not have the
lint. `sort_by_key` with `Reverse` says the same thing.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 20:01:37 -05:00
52d330167b feat(installer): implement Phase 3 — the patch tier
Some checks failed
PR Checks / rust-gates (pull_request) Failing after 46s
Two features need edits to stock ServUO sources, because the events they
depend on do not exist. This adds the rung ladder of PLAN.md §2.2.1, the
unsupported-version path of §2.2.2, and the record and cache Phase 4 will read.

Three decisions were not settled by the plan:

* The engine is fully native, with no `git`. §2.2.1 wrote rung 1 as "apply
  verbatim with git apply", but §1 chose the release tarball specifically so
  there would be no git on the shard host, and rung 2 needs a native applier
  regardless. Rung 1 keeps its distinct, stronger verdict — the whole file
  reproduced the diff's `index` pre-image, computed as a git blob SHA1 in
  process — while the write goes through the same code path as rung 2. On the
  real trees here that is not academic: the shipped .patch files are CRLF in a
  Windows checkout and two of their three targets are LF, so `git apply`
  refuses patches this applies correctly.

* Per-patch metadata is declared by the release, with a built-in fallback.
  Which patches form one all-or-nothing unit, which companion .cs follows
  which, whether a CORE rebuild is needed and what declining costs are not
  derivable from a diff. servuo-plugins now declares them; overlay v0.1.1 is in
  the current bundle and declares nothing, so a built-in copy stands in for it.
  A checked-in fixture of the release workflow's own jq output asserts the two
  descriptions are identical, so the repos cannot drift quietly.

* Pre-images are cached in the state directory. The tier edits files the
  operator owns, and `/etc/runicgateway/patches/originals/` is what turns "here
  are the hunks we added" into a revert anyone can verify — kept out of the
  ServUO tree, which uninstall has promised never to clean up.

Everything else follows §2.2.1: exact matching with only line-ending and
trailing-whitespace normalization, exactly one occurrence or it fails,
all-or-nothing per patch file and again per feature, and a byte-preserving
splice so nothing outside a hunk can be reformatted.

Verified against the ServUO 57.4 tree on this machine across four scratch
roots: a hand-patched tree (rung 0), a reverse-applied stock one (rung 1 on the
real EventSink.cs, its blob matching the patch's declared pre-image), a
mixed-rung feature, a tree with edits inside two patched regions (rung 3 —
nothing written, nothing held back applied, no companions copied), and a
non-57.4 tree both with and without the extra consent flag. Three consecutive
runs left install.json byte-identical and the cached pre-image still pre-patch.

Three reporting defects the live runs caught are fixed with tests: a dry run
and a held-back patch both claimed to be "applied", the core-rebuild warning
fired when nothing had been written and named a Scripts file as core, and a
declined tier announced the loss of features install.json showed as applied.
Refused patches are now cached too, since the refusal message names that path.

Refs: docs/installer/PLAN.md §2.2, §5 Phase 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 19:54:36 -05:00
7122bc6911 Merge pull request 'feat(installer): implement Phase 2 — uo-link install and service' (#5) from feat/phase2-sidecar-service into edge
Reviewed-on: #5
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 21:02:00 +00:00
7bfb033957 fix(installer): make the Linux half of Phase 2 compile
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m56s
Three faults in code that only compiles under cfg(unix), none of which the
Windows build could see:

- `run(...).map(...) == Ok(true)` compared two `Result<_, anyhow::Error>`
  values, and anyhow::Error is not PartialEq. Replaced with `is_ok_and`.
- `command_line` is used only by the Windows registration path, so importing it
  unconditionally is an unused-import error under `-D warnings`. Qualified at
  its call site instead.
- A cfg(not(windows)) assertion block had ended up in the wrong test, leaving it
  referencing a binding from its original one.

Caught by running the same gates the CI runner does inside a rust:1-slim
container against this working tree — fmt, clippy --all-targets -D warnings, and
cargo test --locked all pass there now, as they do on Windows.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 15:52:02 -05:00
2228e0848b feat(installer): implement Phase 2 — uo-link install and service
Some checks failed
PR Checks / rust-gates (pull_request) Failing after 1m15s
Adds the sidecar half of a deployment to the same `install` run: download and
verify the bundle's binary, provision its config, register and start a service,
and print the token handoff PLAN.md §6 specifies. `src/sidecar.rs` owns the
binary and the config document; `src/service.rs` owns systemd and the Windows
SCM.

The order is fixed by PLAN.md §5 and matters: stop anything running the old
binary, replace it, then `--print-config` (which writes the config the service
will be pointed at), then register. Registering first points a service at a file
that does not exist yet.

Decisions worth a reviewer's attention:

- Both platforms run the sidecar as a dedicated unprivileged identity. Linux gets
  the `runicgateway` system user the plan already specified; Windows gets a
  virtual service account, `sc create ... obj= "NT SERVICE\RunicGatewayLink"`,
  which the SCM creates itself and which has no password. Plain `sc create` runs
  as LocalSystem — the most privileged local identity there is, for a process
  listening on two TCP ports while its Linux twin deliberately does not run as
  root.
- `sidecar.toml` holds the auth token and neither default location protects it:
  /etc is world-readable and %ProgramData% grants Users read by inheritance, so a
  stock install would leave the shard's token readable by any local account. The
  lockdown straddles registration because it has to — on Windows the service
  account does not exist until `sc create` creates it, so the file is first cut
  down to SYSTEM + Administrators, and the account's read grant comes after.
- Only Linux pins UOLINK_DB_PATH. On Windows config and data share a directory
  and the sidecar anchors a relative [store] path to its config's directory, so
  the pin is redundant — and `sc.exe` has no per-service environment, only a
  machine-wide one that every process inherits and that outlives an uninstall.
  The config path rides in the service's own binPath instead.
- `--verify` runs no part of the sidecar half. `--print-config` provisions: it
  writes the config and mints a token, so a dry run that called it would create
  the state it claims not to. It also carries an existing `link` section of
  install.json through untouched, so a dry run cannot make a service disappear
  from the record.
- The installed binary's protocol version is checked against the bundle before
  the service is registered. Gate 1 read that number from source at the release
  tag; this is the same check applied to the binary that will actually answer the
  website.
- RUNICGATEWAY_STATE_DIR now relocates the sidecar binary as well, and suppresses
  service registration and the file-permission hardening. There is no such thing
  as a relocated systemd unit, and hardening a scratch config against the only
  account that will ever read it just breaks the next test run.
- A host with no systemd, or where the service user cannot be created, still gets
  a working binary and config plus the exact unit and commands. There is no
  fallback to User=root or LocalSystem: a service quietly running with more
  privilege than its documentation promises is worse than one that was not
  registered.
- install.json never records the token. The `link` section carries versions, the
  binary's hash, the config and database paths, and the service's name, unit path
  and account.

Docs half: docs#91.

Tested: cargo fmt --check, clippy --all-targets -D warnings, 72 tests. End to end
on Windows against a relocated layout — bundle sidecar downloaded and verified,
config provisioned, handoff printed with URLs composed from the host rather than
the bind address, second run reporting unchanged with install.json byte-identical,
--verify over an installed host writing nothing and preserving the link section,
and a tampered binary detected by hash and replaced with no staging file left.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 15:40:56 -05:00
a4f6b7b756 Merge pull request 'feat(installer): implement Phase 1 — the installer core' (#4) from feat/phase1-installer-core into edge
Reviewed-on: #4
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 20:02:35 +00:00
dff4ad41c9 feat(installer): implement Phase 1 — the installer core
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m31s
Adds the Rust crate at the repo root and implements `install` end to end for
the overlay half of a deployment: resolve the published bundle, find and
validate the ServUO root, refuse to deploy under a running shard, sync the
plugin overlay, and record what was deployed in install.json.

`doctor`, `update` and `uninstall` parse and answer with the phase they arrive
in rather than "unrecognized command", and the run states plainly that the
uo-link sidecar (Phase 2) and the patch tier (Phase 3) were not installed —
`--patches` in particular reports REQUESTED BUT NOT APPLIED, since a quiet
completion would be read as a patched shard.

Landing on `edge` rather than `main`: release.yml publishes a binary on every
push to main, and an installer that deploys the overlay but cannot install the
sidecar is not something to hand an operator. pr-checks.yml now gates PRs into
edge on the same rules, so the branch the work happens on is not the ungated
one.

Notable decisions, all documented in docs/installer/PLAN.md §5 Phase 1:

- The code lives in a library called `rgdeploy` with a thin binary that keeps
  the published name. Windows' UAC installer detection refuses to launch an
  unsigned executable whose file name contains "install" (os error 740), and
  Cargo names test harnesses after their target — so a target under that name
  makes `cargo test` unrunnable on Windows.
- The running-shard check matches processes by path, not by process name:
  on Linux a live shard is `mono`/`dotnet` with ServUO.exe as an argument, and
  a name match would report "not running" for a shard that is running.
- install.json records a state (`deployed` / `kept-operator-modified`), not the
  run's verb, so an unchanged re-run produces an identical record and writes
  nothing.
- The Bridge.cfg keep rule compares against the hash the installer last
  deployed, not the last hash it saw — otherwise a kept file is overwritten on
  the very next run.
- Downloads are verified against the bundle's SHA256 while being written, then
  every extracted file is re-hashed against the release's own manifest.json,
  whose protocol and version are cross-checked against the bundle.

Verified against a real ServUO 57.4 tree and end to end into a scratch tree:
24 files deployed, an unchanged re-run that writes nothing, an edited
Bridge.cfg kept across repeated runs while code files are overwritten, bundle
pinning, and a refusal with a shard running out of the tree.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 14:58:17 -05:00
0e7d5f3bee Merge pull request 'ci(bundle): compose and publish the bundle manifest' (#3) from ci/bundle-compose into main
Some checks failed
Release installer / release (push) Successful in 5s
sync-project-tree / sync (push) Successful in 44s
Compose bundle / compose (push) Failing after 18s
Reviewed-on: #3
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 16:19:31 +00:00
c100d4a2d7 ci(bundle): compose and publish the bundle manifest
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
Phase 0 item 3 of docs/installer/PLAN.md (§7.1-§7.3). The installer resolves
what to install *from* the bundle, so this has to exist before Phase 1 code is
useful. Both components it composes now have releases, which is what unblocked
it.

Adds .gitea/workflows/bundle.yml — resolve both components' latest releases,
run the two compose-time gates, and publish bundles/current.json — plus the
first real bundle (2026.08.04: link v1.1.0 + overlay v0.1.1, protocol 3).

Bundles are COMMITTED under bundles/, not published as releases. This repo's own
releases are the installer binaries, and /releases/latest returns whichever
release is newest regardless of kind, so interleaving bundle releases would make
"latest" intermittently resolve to a release carrying no installer binary. The
push to main needs no new branch-protection exception: release.yml's version-bump
commit already requires it.

Gate 1 (protocol agreement) reads the sidecar's PROTOCOL_VERSION from
sidecar/src/main.rs at the release tag, not from the binary. --print-config
would answer, but only for releases from v1.1.0 on, and --bundle <tag> has to be
able to recompose an older bundle. It also avoids executing a downloaded
artifact and provisioning a throwaway config whose auth token would land in a CI
log. The overlay half comes from manifest.json inside the tarball, which is the
only statement of that version that exists.

Gate 2 (assets) downloads every asset and verifies it against the SHA256SUMS its
publishing repo shipped, then records the hash it computed itself. These
artifacts are deliberately unsigned, so a hash copied from a file nobody checked
would make the whole chain decorative. An asset with no SHA256SUMS entry is
caught separately, since `sha256sum -c` passes right over it.

Release reads are ANONYMOUS on purpose: they are exactly the requests the shipped
installer makes on a host with no Gitea credentials, so a repo flipped to private
fails here rather than on an operator's machine.

Stale components (§7.3) are dispatched, never awaited — Gitea's dispatch endpoint
returns no run handle. "Ahead of its release" counts only releasable commits and
excludes merge commits, whose subject quotes the feat/fix title and would
otherwise re-dispatch a workflow that correctly declines to run, every night.

A run that finds nothing changed writes nothing, comparing everything except
`bundle` and `generated` — that is what makes the nightly cron free rather than a
dated duplicate every morning.

Verified by running the workflow's exact compose steps in a Linux container
against the live releases: both gates pass, the composed bundle is the file
committed here, a re-run reports changed=false, and the stale-detection filter
scores 1 releasable commit for link v1.0.0..main (excluding the merge that quotes
it) and 0 for a docs-only range.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 11:10:51 -05:00
cfebbbbd4b Merge pull request 'fix(ci): preflight release credentials and recover from an orphan tag' (#2) from fix/ci-credential-preflight into main
Some checks failed
Release installer / release (push) Successful in 5s
sync-project-tree / sync (push) Failing after 5s
Reviewed-on: #2
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 15:27:28 +00:00
60ae6f1f75 fix(ci): preflight release credentials and recover from an orphan tag
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
servuo-plugins hit both of these on its first real release run; this repo
runs the same engine, so it has the same two defects latent.

REGISTRY_USER / REGISTRY_TOKEN were empty there, yet the tag push
SUCCEEDED: actions/checkout leaves an `http.<host>.extraheader` credential
in the local git config, so `git remote set-url` to a URL with empty
credentials still authenticated through that leftover header. The release
API call had no such fallback and returned 401. Net result was the worst
available outcome — the repo tagged, no release, and a failed job.

Two fixes:

  A credential preflight, before anything is built or pushed, gated on the
  run actually intending to publish so a docs:/chore:-only merge (or this
  repo's pre-crate no-op) still passes on a repo with no secrets. It names
  the missing secrets and the scope they need instead of failing wherever
  they happen to be used first.

  Orphan-tag recovery. A tag with no release behind it means an earlier run
  died after tagging, and the old code treated any existing tag as "nothing
  to release" — so that state could never clear itself: every later run
  would see the tag and stand down, forever. The plan step now asks the API
  whether a release exists for the tag, and on 404 reuses the tag and
  publishes the release it is missing. This deliberately overrides the
  RELEASE=false the bump logic just decided, which is the whole point —
  with the tag in place there are no releasable commits after it.

  Anything other than 200/404 (network failure, bad token) is refused
  rather than guessed, since assuming "no release" would republish over a
  good one.

  The tag step now reuses an existing tag instead of failing on `git tag`,
  and the changelog for a recovery run summarizes what the tag contains
  (previous-tag..this-tag) rather than the empty range after it.

sync-project-tree gets the same preflight: its first run on main failed
with an opaque `git clone` error against `https://:@host/...` that said
nothing about a missing secret.

Verified by extracting every run block and exercising the paths: empty
secrets fail the preflight with a legible message and populated ones pass;
the no-Cargo.toml guard still short-circuits to release=false; a crate with
no tag still takes the seed path; and against real repo state, a tag with a
release stands down while an orphan tag recovers.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 10:14:47 -05:00
71a9210503 Merge pull request 'ci(installer): add pr-checks, release, and project-tree sync workflows' (#1) from ci/add-workflows into main
Some checks failed
sync-project-tree / sync (push) Failing after 7s
Release installer / release (push) Has been cancelled
Reviewed-on: #1
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-04 14:16:28 +00:00
ea9aad6e9b ci(installer): add pr-checks, release, and project-tree sync workflows
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 11s
Bring this repo's CI up to parity with the other Runic Gateway repos. All
three are retargeted from RunicGateway/link, which is the closest analog
(same Rust toolchain, same release engine, same runner).

pr-checks.yml
  Gates PRs into main on cargo fmt --check, clippy -D warnings, and
  cargo test --locked, in that order, one job — mirroring release.yml's
  gates so a green PR implies a green release.

release.yml
  The conventional-commit release engine from link/, with the Rust
  adapter retargeted: crate at the repo root, binary
  runicgateway-installer, cross-compiled for x86_64 Linux and Windows.
  Artifact names follow PLAN.md §3. The generated changelog now carries
  the checksum-verification block, because releases are deliberately
  unsigned and SHA256SUMS is the trust anchor (PLAN.md §3) — that makes
  the verify instructions part of the release, not a doc someone has to
  find.

sync-project-tree.yml (+ .gitea/scripts/gen_tree.py)
  Regenerates docs/installer/PROJECT_TREE.md on every push to main and
  opens or force-updates a PR against the docs repo. Verbatim from link/
  apart from the repo/path/label env block.

Crate guard
  This repo has no Cargo project yet — Phase 1 creates it. Landing the
  workflows unguarded would red-X every governance and docs PR until
  then, and holding them back leaves the repo ungated exactly while its
  conventions are being set. So both Rust workflows check for a root
  Cargo.toml first: pr-checks skips its gates with a notice, and
  release.yml's plan step sets RELEASE=false and exits. Both arm
  themselves the moment Cargo.toml lands, with no edit here.

Verified before pushing: all three files parse as YAML, every run block
passes bash -n, and the release plan step was simulated against a throwaway
git repo both without a crate (release=false, exit 0) and with one
(first-release path -> v0.1.0 with the changelog rendered).

Not included: the bundle-manifest workflow (PLAN.md §7) and the
release-dispatch hook, which are Phase 0 item 3 and depend on
servuo-plugins having a release workflow first.

Note for setup: release.yml and sync-project-tree.yml need REGISTRY_USER
and REGISTRY_TOKEN (write:repository, plus read/write on RunicGateway/docs)
configured for this repo.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 09:14:56 -05:00
d2b311197b chore(installer): bootstrap repo with governance docs and templates
The installer repo was created empty. Seed it with the same governance set
every other Runic Gateway repo carries, so it starts on the same footing
before any Rust code lands (see docs/installer/PLAN.md for the design of
record — this repo is still in the planning phase).

Copied verbatim, byte-identical to the other repos:

  LICENSE.md (GPL-3.0-or-later), CODE_OF_CONDUCT.md, CONTRIBUTORS.md,
  .gitea/PULL_REQUEST_TEMPLATE.md, .gitea/ISSUE_TEMPLATE/{bug_report,
  feature_request}.md

Repo-specific:

  README.md          what the installer is, what it deliberately is not
                     (no curl|bash, never writes a ServUO launcher), the
                     planned commands, and the constraints a reader needs
                     up front: unsigned releases, bundle-manifest
                     composition, opt-in patch tier, and the fact that a
                     successful copy is not a working bridge.
  CONTRIBUTING.md    adapted from link/ (same Rust toolchain and checks),
                     plus a planning-status note pointing changes of scope
                     at the plan in docs/, and the two shard-testing traps.
  SECURITY.md        adds the installer to the component scope table and a
                     short subsection on its distinct trust model: unsigned
                     releases anchored on SHA256SUMS, mandatory
                     verification of downloaded artifacts, and the
                     never-contacts-the-website token handoff. This is the
                     only file that now differs from the other repos' copies.
  .gitignore         Rust build output plus local deployment state
                     (install.json, sidecar.toml, *.db) that must never be
                     committed from a test run.
  .gitea/ISSUE_TEMPLATE/config.yaml   same as elsewhere, repo-local URL.

No CI workflows yet — there is no crate for pr-checks to build, and the
release/bundle workflows are Phase 0 work that depends on servuo-plugins
gaining a release workflow first.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 09:05:56 -05:00