Skip to content
Closed
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
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