Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8b9ba86
fix(firewall): answer :2121 on br-private, where the admin board live…
felixfelix-bot Oct 4, 2026
f217197
feat(config,network): the private SSID's credentials and the administ…
felixfelix-bot Oct 5, 2026
be601ff
fix(tls): the derived hop lands before the reload, the operator's ide…
felixfelix-bot Oct 5, 2026
1e9c4f4
fix(session): carry the meter across a MAC rotation (session tickets,…
felixfelix-bot Oct 5, 2026
cec2222
fix(setup): drop the legacy re-brand :8090 writer; uhttpd.admin stays…
felixfelix-bot Oct 6, 2026
19b5609
fix(merchant): make the test log capture concurrency-safe (intermitte…
Amperstrand Oct 6, 2026
bc0ca6e
fix(merchant): a concurrent duplicate of one note is refused before t…
Amperstrand Oct 6, 2026
ae83e28
fix(valve): a "client not found" answer is only evidence about the MA…
felixfelix-bot Oct 6, 2026
0ef5171
ci(ngit): stamp stage 1 from the commit, never the runner clock (#529)
Amperstrand Oct 6, 2026
bf31ad6
fix(packaging): refuse to build a local .ipk without staged portal bu…
Amperstrand Oct 6, 2026
5c64faa
fix(utils): hex-encode the fingerprint salt — raw edge bytes corrupte…
Amperstrand Oct 6, 2026
de5b421
fix(wallet): no same-body retry after an ambiguous mint outcome — rep…
Amperstrand Oct 6, 2026
87ba24d
fix(merchant): a paid purchase whose gate cannot open is an owed enti…
Amperstrand Oct 6, 2026
48c67cf
test(fleet): require credentials from the environment, not public def…
Amperstrand Oct 6, 2026
cd4861e
fix(merchant): bound the fee precheck against wedged mints (#525) (#533)
Amperstrand Oct 6, 2026
b873983
docs(operator-guide): cover the ssl family and upstream known; regen …
Amperstrand Oct 6, 2026
f79de9b
fix(cli): bound fw4 reload on the daemon start path — a wedged firewa…
Amperstrand Oct 6, 2026
3641706
fix(config): config.json writes are atomic and a missing config is an…
Amperstrand Oct 6, 2026
c44b710
fix(config): min_steps floors at 1 — parser accepts the legacy spelli…
Amperstrand Oct 6, 2026
ac1ab49
ci: enroll in org gitleaks scanning; watch main on push (ports fork #…
Amperstrand Oct 6, 2026
abca784
fix(config): refuse an out-of-bounds private_key at config set — a pe…
Amperstrand Oct 6, 2026
c9ebe6e
fix(cli): config get no longer returns the identities' Nostr private …
Amperstrand Oct 6, 2026
2f12b04
test(cloud-lab): conformance fast-subset lane over the PRTA fault pro…
Amperstrand Oct 6, 2026
7676365
make: one release-check gate that orchestrates the existing gates — p…
Amperstrand Oct 6, 2026
bb9c895
release: v0.6.0-rc1 — feature freeze (merge last) (#659)
Amperstrand Oct 6, 2026
41ee4d7
docs(agents): the gonuts bump touches four go.mod files, not three (#…
Amperstrand Oct 6, 2026
7072751
deps(wallet): bump gonuts-tollgate v0.13.0 — NUT-20 cdk-interop signa…
Amperstrand Oct 6, 2026
09a97b4
docs(readme): the WR3000 25.12 install paragraph gains the feed-shape…
Amperstrand Oct 6, 2026
d1612d4
test(packaging): an apk3 dependency-resolution smoke gates the releas…
Amperstrand Oct 6, 2026
6d2277e
fix(build): build-sdk-package.sh compiles package main, not main.go a…
Amperstrand Oct 6, 2026
391b82a
fix(lane): resolve the conflict-marker residue #658's rebase left in …
Amperstrand Oct 6, 2026
f91f3a1
test(merchant): the owed-grant AUTH assertion polls for its own condi…
Amperstrand Oct 6, 2026
9e87467
docs(agents): hardware and VM testing is coordinated through labgrid …
Amperstrand Oct 6, 2026
a1f75b4
docs: decision records rebased — discovery signaling (#621), ADR cita…
Amperstrand Oct 6, 2026
977fa96
feat(setup): the gateway serves NTP pre-auth — time before payment (#…
Amperstrand Oct 6, 2026
42b7666
merge main forward (35 commits, through #648): the CHANGELOG conflict…
Oct 6, 2026
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
13 changes: 13 additions & 0 deletions .github/workflows/build-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,19 @@ jobs:
python3 -m pip install --break-system-packages playwright
python3 -m playwright install --with-deps chromium

# Dependency-resolution smoke: apk3 (the only tooling that reads 25.12
# indexes) must be able to select every name the package declares from
# the standard 25.12.x feed set. This is the gate the September #552
# breakage class would have tripped: a feed rebuild changed how the
# iptables family is provided, and nothing between "artifact builds"
# and "a bench VM cannot install it" noticed. Runs before the
# happy-path suite because it is seconds, not minutes.
- name: Apk dependency-resolution smoke
run: |
set -euo pipefail
export APK=$(ls /var/tmp/hp-artifact/*.apk | head -1)
bash tests/packaging/apk-install-resolution_test.sh

# --strict is deliberate, not decoration: this artifact is built from a tip
# that carries upstream #541 (/session-state) and a portal bundle that ships
# the in-page renewal CTA (#60), so those surfaces are EXPECTED here and
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: gitleaks

on:
push:
branches: [main]
pull_request:
schedule:
- cron: "13 6 * * *"

jobs:
gitleaks:
uses: Amperstrand/.github/.github/workflows/gitleaks.yml@main
52 changes: 47 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ jobs:
# negative control fails if the comparison ever goes back to equality.
bash tests/uci-defaults-setup-marker-order_test.sh

- name: Exactly one owner of :8090, and LuCI alone on :8080
run: |
# Offline (no router, no SDK, no network): the portal-staged board
# (uhttpd.admin, written by the feed's 92-tollgate-admin-setup) is the
# ONE section that may claim :8090, and the module writes no :8090
# listener of its own. It used to: a brand-gated legacy configUI
# writer created a second section on the port, so two admin UIs bound
# the same port and one of them disappeared. The suite drives the
# shipped uci-defaults script end to end through the four install
# scenarios - fresh, upgrade, reinstall-over-marker, and a box whose
# legacy section had its listeners stripped by an older build (which
# is why the stale section must be DELETED, not port-stripped) - and
# carries detector controls so a passing assertion cannot be vacuous.
bash tests/packaging/configui-8090-single-owner_test.sh

- name: No re-brand literal anywhere in the tree
run: |
# Offline and tree-only: upstream carries no commercial re-brand's
# name - not in the setup script, not in the docs, not in the
# CHANGELOG. The gutter fails the moment a literal reappears, and
# scans itself: the pattern is a bracket expression, so this file does
# not carry the name it bans. A planted-occurrence control proves the
# scan is not inert. This is a working-tree gutter, not a history
# rewrite: the literal legitimately survives in old commits.
bash tests/packaging/rebrand-literal-gutter_test.sh

- name: Admin TLS identity (HTTPS is not inherited from the image)
run: |
# Offline (no router, no SDK, no network): the install path must
Expand Down Expand Up @@ -163,13 +189,29 @@ jobs:
# bridge's port list cleared so a port is never on two layer-2
# domains. The same-version verify/repair path re-asserts the
# placement (a factory reset is repaired), the module keep-list
# still carries /etc/config/network, and all four guard fragments
# are pinned byte-identical to main and still br-lan-scoped - that
# literal is what keeps a guest off :8090/:8443 and LuCI while a
# br-private client gets in. A neutralised-writer negative control
# must go red.
# still carries /etc/config/network, and the guard fragments are
# pinned by digest (three byte-identical to main; the :2121
# backend-firewall guard updated on purpose so the owner network can
# read the admin board's data) and still br-lan-scoped - that literal
# is what keeps a guest off :8090/:8443 and LuCI while a br-private
# client gets in. A neutralised-writer negative control must go red.
bash tests/uci-defaults-lan-private-wired_test.sh

- name: The backend API answers the owner network (admin board data path)
run: |
# Offline (no router, no daemon, no network): the :2121 API must be
# reachable from br-private as well as br-lan. The admin board is
# deliberately kept OFF the captive bridge, so br-private is the one
# network it is administered from - and the board is a thin shell
# whose every panel reads pricing/whoami/balance/ln-invoice from
# :2121. With the exemption missing, the board rendered on a freshly
# flashed GL-MT3000 (2026-10-04, alpha4-pre21) while each data call
# died with "TypeError: NetworkError when attempting to fetch
# resource" and retried forever. The suite pins the exemption set on
# both protocol families, that the rule is still a drop, that no
# foreign interface is exempted, and that the fragment compiles.
bash tests/packaging/backend-api-owner-network_test.sh

- name: One device code (hostname, captive SSID and private SSID agree)
run: |
# Offline (no router, no SDK, no network): the router's identity is
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,9 @@ scripts/token-recovery/token-recovery

# pytest bytecode from local (possibly root-owned) cloud-lab runs
tests/cloud-lab/__pycache__/
scripts/__pycache__/
tests/cloud-lab/conformance/__pycache__/

# Conformance lane runtime state (tokens stashed between phases, generated
# config, verdict parts, evidence logs) — see tests/cloud-lab/conformance/.
tests/cloud-lab/.conformance/
13 changes: 13 additions & 0 deletions .ngit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ DQ05, `embedded-act` runner, `ghcr.io/catthehacker/ubuntu:act-latest`) with
falls back to `0` when history is unavailable; tagged releases are unaffected
because their version is the tag name. `GOFLAGS=-buildvcs=false` for the same
reason.
This also removes the **commit timestamp**, which the reproducible-build pin
(#383) needs for `SOURCE_DATE_EPOCH`. `scripts/ngit-commit-epoch.sh` is the one
derivation: the commit time from local history where there is one, and
otherwise a depth-1 fetch of *exactly that commit* from the ngit mirror the
release is built from — so the value is a property of the commit, not of the
run. Stage 1 (`determine-versioning`, `build-portal`) and the shards'
`resolve-inputs` all call it. When neither source answers, the job **fails**
instead of substituting the runner clock: the earlier cascade had a
`date +%s` last resort, and because the coordinator's synthesized push payload
carries `head_commit` with no timestamp it took that branch on *every* run
(`source: job clock (NOT commit-derived …)` at `ca5d07a2`), so two builds of
one commit stamped different `BuildTime` strings and the published bytes could
never be reproduced or compared.
* **Blossom reachability from the runner.** `blossom.primal.net`,
`blossom.psbt.me`, `blossom2.orangesync.tech` and `drive.cashu.email` answer;
`blossom1.orangesync.tech` times out (25 s). The server list is left as the
Expand Down
46 changes: 27 additions & 19 deletions .ngit/act/workflows/build-package-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,31 +110,38 @@ jobs:
echo "release_channel=dev" >> "$GITHUB_OUTPUT"
fi

- name: Derive SOURCE_DATE_EPOCH from the source commit
- name: Resolve SOURCE_DATE_EPOCH from the source commit
# The reproducible-build pin from #383: every embedded timestamp —
# BuildTime in the binaries, ipk archive mtimes, portal output —
# derives from the source commit, never from the wall clock, so a
# rebuild of one commit yields the same bytes. The act workspace has
# no usable .git, so the derivation is env-first like repro-check.yml:
# triggering commit's timestamp, job-start clock as the last resort
# (which is then recorded in the stage-1 rendezvous record below, so
# stage 2 still packages with exactly the epoch the binaries used).
# rebuild of one commit yields the same bytes.
#
# scripts/ngit-commit-epoch.sh is the single derivation, shared with the
# build-portal job below and with the shards' resolve-inputs: it reads
# the commit time from the local history when there is one, and
# otherwise fetches exactly this commit (depth 1) from the ngit mirror
# the release is built from — an act job checkout has no git metadata,
# `git log` fails there, and the coordinator's synthesized push payload
# carries `head_commit` without a timestamp, so the previous cascade
# landed on `date +%s` on EVERY ngit run and stamped the binaries with
# the runner's clock (observed at ca5d07a2: "source: job clock (NOT
# commit-derived ...)"). That is exactly the failure this lane exists to
# prevent: two builds of one commit produced different BuildTime strings,
# so the published bytes could never be reproduced or compared.
#
# No wall-clock fallback on purpose. When neither source is available
# the script exits non-zero and this step fails with it: one clearly
# red run is better than a release whose artifacts silently cannot be
# rebuilt identically.
id: source-epoch
shell: bash
run: |
set -euo pipefail
: ${GITHUB_OUTPUT:=/tmp/github_output}
EPOCH="$(git -C "$GITHUB_WORKSPACE" log -1 --format=%ct HEAD 2>/dev/null || true)"
SOURCE="git"
if [ -z "$EPOCH" ] && [ -n "${{ github.event.head_commit.timestamp }}" ]; then
EPOCH="$(date -u -d "${{ github.event.head_commit.timestamp }}" +%s)"
SOURCE="head_commit.timestamp"
fi
if [ -z "$EPOCH" ]; then
EPOCH="$(date +%s)"
SOURCE="job clock (NOT commit-derived — binaries will not rebuild identically)"
fi
EPOCH=$(bash scripts/ngit-commit-epoch.sh "$GITHUB_SHA")
case "$EPOCH" in ''|*[!0-9]*) echo "ERROR: not an epoch: '$EPOCH'" >&2; exit 1 ;; esac
echo "source_date_epoch=$EPOCH" >> "$GITHUB_OUTPUT"
echo "SOURCE_DATE_EPOCH=$EPOCH (source: $SOURCE)"
echo "SOURCE_DATE_EPOCH=$EPOCH ($(date -u -d "@$EPOCH" '+%Y-%m-%d %H:%M:%S UTC'))"

- name: Report
run: |
Expand Down Expand Up @@ -303,8 +310,9 @@ jobs:
set -euo pipefail
: ${GITHUB_OUTPUT:=/tmp/github_output}
# The epoch rides the record so stage 2 packages with exactly the
# epoch these binaries were stamped with — even when the last-resort
# clock source produced it.
# epoch these binaries were stamped with. It is commit-derived (see
# the source-epoch step), so this record is stable across re-runs at
# one commit rather than reporting whenever the run happened.
RECORD_JSON=$(printf '%s' "$HASHES_JSON" | jq -c --argjson epoch "$SOURCE_DATE_EPOCH" '. + {epoch: $epoch}')
out=$(nak event --sec "$NSEC_HEX" -k 30078 \
--tag "d=tollgate-build/${BUILD_ID}/binaries" \
Expand Down
47 changes: 45 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ than inventing new harnesses.
- **gonuts-tollgate is our fork to maintain.** Upstream `elnosh/gonuts`
is dead (last release v0.4.2, 2025); we carry ~40 patches. Every
wallet-level fix lands in `OpenTollGate/gonuts-tollgate` first, is
tagged, then bumped here via the `replace` directive (three `go.mod`
files). Never fix a wallet bug by patching around the fork locally.
tagged, then bumped here via the `replace` directive — the require lands
in every nested module that carries it: `src/`, `src/cli`, `src/merchant`
and `src/tollwallet`, four `go.mod` files today. Never fix a wallet bug
by patching around the fork locally.
- **bbolt persistence.** Keyset records (which own derivation counters)
are nested under mint-URL-named buckets; the DB has no transactions
spanning "fetch keysets + swap + save proofs". This is why counter
Expand All @@ -141,6 +143,47 @@ than inventing new harnesses.
breaks that (e.g. cdk-go FFI on MIPS) belongs behind the sidecar, not
in-process.

## Hardware and VM testing (labgrid)

All router- and VM-based testing is coordinated through **labgrid**
(coordinator `ai-legion:20408`). Do not drive lab hardware ad hoc: reserve
through places (`labgrid-client -p <place> acquire` … `release`), and treat
a place held by someone else as theirs. Note some hosts carry a stale
`LG_COORDINATOR` pointing at a dead address — use the hostname form:

```bash
export LG_COORDINATOR=ai-legion:20408
labgrid-client places # inventory + comments say what each seat is
labgrid-client who # current holders
```

The lab's single source of truth is the private
**`Amperstrand/conwrt-bench`** repo (ADR-0005): `registry/` for devices,
`labgrid/` for place seeds and examples, `docs/decisions/` for the why,
sops for secrets. `conwrt-lab` is retired — do not add data there. Rules
that every hardware-touching change follows:

1. Never hardcode device IPs, MACs or serial paths — resolve from the
registry or a labgrid place.
2. Access hardware through labgrid places (`ssh|console|power`), with
acquire/release for exclusivity during a test.
3. Flashing and adoption go through conwrt tooling
(`dut_recover.py --from-lab`, `bench_net.py`), which updates the
registry.
4. A state change ends with a registry commit — flashed, moved or
adopted devices must be reflected before you walk away.
5. When surprised, reconcile first
(`lab_registry.py reconcile`) before touching anything.

VM lane: `labgrid/qemu-x86-64.yaml.example` in conwrt-bench is the
pattern — the client runs ON ai-legion (QEMUDriver executes where the
client runs), pristine per-acquire boots via `snapshot=on`. The on-target
package harness is `tests/happy-path/run.sh --artifact <extracted pkg>`;
the artifact is the published bytes from a kind-`1063` event
(hash-pinned via its `x` tag) or, for pre-tag candidates, a local
`scripts/build-sdk-package.sh` build whose sha256 is recorded in the
evidence.

## Contributing process

Follow [CONTRIBUTING.md](CONTRIBUTING.md). The parts agents most often
Expand Down
Loading