Skip to content

fix(lore-0123): apply the fixes from the review of #137 - #178

Merged
karczuRF merged 6 commits into
developfrom
lore-0123-pr137-review-fixes
Nov 12, 2026
Merged

karczuRF merged 6 commits into
developfrom
lore-0123-pr137-review-fixes

Conversation

@karczuRF

@karczuRF karczuRF commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Task 0123: the fixes from the code review of the third promotion (#137), which covered the 55 commits its description had not checked. OpenBao's snapshot job now stops retrying after three tries, its runbook steps match what the job does, and three tests the review found missing are in.

  • Snapshot job (deploy/systemd/x402-openbao-snapshot.service, deploy/libexec/x402-openbao-snapshot): the unit stops after the run and two retries and stays failed until the next night's timer (D2); the retry comment is true (P1); cleanup removes a half-decrypted credential (P5), and removes the decrypted files before its calls to OpenBao and Docker, which can hang until a kill; the header names the retention as it is (P12).
  • Its tests (deploy/test/): a units test pins 3h and 3 and that the window is longer than three whole tries and shorter than a day (D2); the lock test waits for the holder instead of sleeping 200 ms, and has a case for a lock that is freed, which frees it only once the script is seen waiting (P3, P4).
  • Runbook (docs/runbook.md): the passphrase change stops the service as well as the timer (P2); setup step 3 tells an SSH or sudo failure from a stored passphrase (P9); step 5 says what a failed first run needs; "What is where" and Credentials name the backup paths (P10).
  • Conformance gate (conformance/src/verdict.ts, gate.ts): the "no facilitator commit" and "uncommitted gate changes" rules move from the gate script into judge(), as a required argument, and the local or hosted folder into resultsFolder(), so all three are tested (P7). From Stan's review, hosted is now named: only our deployed facilitator (HOSTED_FACILITATORS), so a dry run or a wrong URL can't replace the run of record. The proxy is tested against a facilitator that answers in gzip (P8).
  • Signer pool (packages/signer-pool/test/settlement-guards.test.ts): a second call on a channel after a rewrap must land on its first envelope (P6). Test only.
  • 0118 (lore/1-tasks/backlog/): gets the facts and the three decisions on how the weekly check shares the lock with the nightly run (P11).

Reviewers, please confirm:

  • @karolko9 (0029): judge() now takes the run's record as a required third argument and holds the two rules gate.ts used to add after it; the list of problems and its order are the same for every run. One behaviour change in your code, from Stan's review: resultsFolder() files a run as hosted only for a host in HOSTED_FACILITATORS (today x402-facilitator.sorobanscan.com), not for any host but three local ones; the README's x402.org dry run now goes to testnet-local/. The proxy has one new test and no code change. One older rule moved as it was, for you to judge: gate.ts trims the status column off the first line of git status --porcelain.

Please confirm

  1. The retry window rests on three other settings (platform): the limit works only while StartLimitIntervalSec is longer than three whole tries, 3 x (TimeoutStartSec + TimeoutStopSec + RestartSec) = 154.5 minutes today (Stan: the stop time counts). The units test fails when a change to any of them breaks that. The window counts from a burst's first start, so a by-hand start that fails again in the 3 hours before 03:15 refuses that night's run; the comment and the runbook say so.
  2. A start by hand after a bad night needs reset-failed (platform): with the limit, the unit stays failed after three tries, and systemctl start is refused inside the window. The runbook says so in OpenBao snapshots, and the passphrase change does it before its last start. This is beyond the review's findings; it follows from D2.
  3. P11's three values stay 0118's to decide (platform): the review asked 0118 to name the check's time, its wait for the lock and the alarm's age. This PR gives 0118 the facts and the three decisions, with a criterion, and names no value, so P11 stays unticked in 0123 as moved to 0118. Two facts for it: a bad night can still be retrying at about 05:40 UTC, and a run missed while the server was down starts at boot at any hour, so "away from 03:15-03:30" is not enough.
  4. The cleanup of a half-decrypted credential is pinned by source text only (platform): the script's test runs on plain files and never reaches that line. A test that runs it needs new overrides in the production script; it stays the first item of the task's "Not in this task". Next to it there, from Stan's review: a run killed during cleanup leaves the files until the next run, and RuntimeDirectory= is the fix at the source.

Testing

  • pnpm test → 20 of 20 tasks pass: deploy 113, conformance 44, signer-pool 189, facilitator 1012 (3 skipped), infra 355, mcp 115. That run was before the last two commits; after them, deploy (113) and conformance (50) ran again and pass.
  • pnpm lint, pnpm typecheck, pnpm build → pass. pnpm format:check → every tracked file passes.
  • Each of these tests was run once without the line it guards, and failed: the two StartLimit lines (D2), channel.sequence = txSequence in the rewrap branch (P6: two envelopes sent, not one), the two record rules in judge() (P7), content-encoding in the proxy's notForwarded (P8).
  • Testnet: N/A — no settlement code changes: the signer-pool change is a test, and the gate's rules moved without changing its verdict. The unit and the script reach the server with the next playbook run; the runbook's changed shell lines were not run on the server.
  • A code review of the branch ran before this PR; its fixes are b6309854. Stan's review is answered in f9a0d418.

A failure that lasts (a wrong passphrase or host key, a full box) retried
every 20 minutes all day, each try a login to OpenBao and an SSH session
to the shared Storage Box. The unit now stops after the run and two
retries and stays failed until the next night's timer (D2).

With it, from the review of #137: the retry comment says what a retry
that meets the lock does (P1), cleanup removes a half-decrypted
credential (P5), the header names the retention as it is (P12), and the
lock test waits for the holder instead of sleeping, with a case for a
lock that is freed (P3, P4).
From the review of #137. The passphrase change stopped only the timer,
so a retry waiting to restart could still run in the middle of it; it
now stops the service too (P2). Setup step 3 took an SSH or sudo failure
for a stored passphrase and said the step was done (P9). "What is where"
and Credentials name the backup paths and say the snapshot script
decrypts its own credentials on the host (P10).

The retry limit of this task leaves the unit failed after three tries,
so a start by hand needs `reset-failed` first: said in OpenBao snapshots
and done before the passphrase change's last start.
Three tests the review of #137 found missing.

The gate's "no facilitator commit" and "uncommitted gate changes" rules
sat in gate.ts, which runs the gate when loaded, so nothing tested them.
They move into judge() as a required argument, and the local or hosted
results folder into resultsFolder(); the list of problems and its order
stay the same for every run (P7).

The proxy is tested against a facilitator that answers in gzip (P8), and
a second call on a channel after a rewrap must land on its first
envelope, which pins the sequence the rewrap leaves (P6).
The review of #137 asked 0118 to say how the weekly check and the
nightly snapshot share the lock (P11). The time of the check, its wait
for the lock and the alarm's age stay 0118's to decide: it gets the
facts from the units as this task left them, the three decisions and a
criterion for them. 0123's P11 says so.
From the code review of this branch.

The test for a freed lock released it on a 300 ms timer, so a slow
script could find the lock free and pass without waiting, the kind of
timing P3 removed. It now frees the lock only once the script is seen
waiting in its own flock. The lock probe takes only flock's exit 1 as
"held", and the gzip proxy test sends the compressed length too.

The runbook says that stopping the service ends a running snapshot and
that a failed first run needs `reset-failed`; 0118 gets the runs that
start off the clock and the two texts a second job on the lock changes.

@stkrolikiewicz stkrolikiewicz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three comments: the gate's results folder, the snapshot job's start limit (my answer to Please confirm 1), and the runbook's line on the decrypted credentials.

Comment thread conformance/src/verdict.ts Outdated
* of git. `URL` keeps an IPv6 host's brackets.
*/
export function resultsFolder(url: string): "testnet-local" | "testnet-hosted" {
const local = ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resultsFolder() calls a run local only for three exact hostnames, so any other local
address is filed as hosted: 0.0.0.0, 127.0.0.2, localhost., or a LAN IP (the new test
pins 192.168.1.20 as hosted). The cost is more than a wrong folder name. gate.ts then
rmSyncs results/testnet-hosted/ and writes the local run there, failed or not, so the
committed run of record behind the M1 evidence is replaced in the working tree, one git add
away from a commit.

I'd turn it around: name the hosted facilitator positively (its known host, or an explicit
setting) and file everything else as local. A mistyped URL then costs a gitignored folder, not
the evidence. Fine as a follow-up if you'd rather keep this PR to moves.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed in this PR rather than as a follow-up (f9a0d41). Hosted is now named: HOSTED_FACILITATORS in verdict.ts, today only x402-facilitator.sorobanscan.com, and every other host goes to testnet-local/. That includes the README's own x402.org dry run, which used to land in testnet-hosted/ too. The tests pin our host (also with a slash and in upper case) as hosted, and x402.org, 0.0.0.0, 127.0.0.2, localhost., 192.168.1.20 and a lookalike …sorobanscan.com.example.org as local. A new deployment joins the list in its own PR before its first run of record.

# session to the shared box every 20 minutes all day. The window is longer than three whole tries,
# 3 x (TimeoutStartSec + RestartSec), so the count can't run out between them, and shorter than a
# day, so it never refuses the timer's next run.
StartLimitIntervalSec=3h

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming 1, with two corrections.

  • The window counts from the first start of a burst, not from the night's run. Three
    failed starts in the 3 hours before 03:15 use up the burst, and the timer's own start is
    refused with start-limit-hit. Example: an admin fixes the cause at 01:30 and starts the
    job by hand, as the runbook now says to. It fails again at 01:30, 01:50 and 02:10, so the
    burst is spent until 04:30. The 03:15 run is refused, that night has no snapshot, and the
    alarm fires. The same happens when the server comes back shortly before 03:15 after missing
    the previous night's run. So "never refuses the timer's next run" isn't quite true. I'd
    reword it to say it can't refuse the next night's run unless the burst started in the
    3 hours before it.
  • A whole try rests on three settings, not two. A try that hits TimeoutStartSec is
    stopped, and the stop can take up to TimeoutStopSec (90 s by default) before RestartSec
    starts. wholeTry in the units test leaves that out. With TimeoutStartSec=39min the test
    passes (3 × 59 = 177 < 180), while the real span is 181.5 min: the fourth start lands
    outside the window and the retries never stop. Adding the stop time (90 s when the unit
    doesn't set it) to wholeTry makes the test guard what the comment says.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both corrections hold, and both are in (f9a0d41).

  • The comment now says the window counts from a burst's first start, so a by-hand start that fails again in the 3 hours before the night's run refuses that run. The runbook says the same next to the reset-failed steps: clear the limit again after the last failed try.
  • wholeTry adds TimeoutStopSec, 90 s when the unit sets none. Today that's 3 × 51.5 = 154.5 < 180; your TimeoutStartSec=39min example now fails the test (181.5).

Comment thread docs/runbook.md Outdated
| `/etc/x402/` | the CloudWatch agent's config, the rotation's, the helper's ARNs, the Storage Box's pinned host key |
| `/srv/x402/backup/creds/*.cred` | the backup jobs' credentials (OpenBao snapshots) |
| `/srv/x402/backup/creds/*.cred` | the backup jobs' credentials, decrypted by the snapshot script on the host, not for a container |
| `/run/x402/backup/` | the snapshot job's decrypted credentials and the snapshot, during a run only (tmpfs) |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trap covers every way out but one: a stop that ends in SIGKILL. If a run hangs on a
wedged OpenBao or Docker, TimeoutStartSec stops it, and the trap's bao token revoke and
docker compose exec … rm hang on the same daemon. After TimeoutStopSec, systemd kills bash
partway through the cleanup, and the passphrase, the Storage Box key and the secret-id stay in
/run/x402/backup/ until the next run removes them. That isn't a leak: the files are
root-only, and root can decrypt the .cred files anyway. It does make "during a run only"
here not quite true.

Two small changes: say "removed when the run ends, or by the next run after a kill" here, and
add RuntimeDirectory= to the task's "Not in this task" as the fix at the source. systemd
removes that directory however the process died. The admin borg path runs outside the unit,
so it would need a directory of its own.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; both your changes are in (f9a0d41): the row now says the files are removed when the run ends, or by the next run after a kill, and RuntimeDirectory= is in 0123's "Not in this task", with your note on the admin borg path.

One more change on top: cleanup now removes the snapshot file and the credentials first, before bao token revoke and the docker compose exec … rm. A wedged OpenBao or Docker then hangs after the files are gone, so your scenario no longer leaves them; only a kill before the trap runs at all still does.

From Stan's review of #178.

The gate filed every host it didn't know as hosted, so a dry run
against x402.org, 0.0.0.0 or a LAN machine replaced the committed run
of record. Hosted is now named: only the hosts in HOSTED_FACILITATORS,
and everything else goes to the git-ignored local folder.

The snapshot unit's start limit counts a try's stop time too, and its
comment and the runbook say a failed burst in the 3 hours before the
night's run refuses that run. Cleanup removes the decrypted files
before it calls OpenBao and Docker, which can hang until a kill;
RuntimeDirectory= is listed as the fix at the source.
@karczuRF
karczuRF merged commit 7966672 into develop Nov 12, 2026
4 checks passed
karczuRF added a commit that referenced this pull request Nov 12, 2026
#178 merged: D2, P1-P10 and P12 are done, P11 moved to 0118. Spawn its
two follow-ups: 0142 (the snapshot job's decrypted files in a directory
systemd removes, and a test of their cleanup) and 0143 (the snapshot
AppRole's secret-id expiry, binding and rotation).
@karczuRF
karczuRF deleted the lore-0123-pr137-review-fixes branch November 12, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants