Skip to content

feat(s16): guest desktop session with pinned Chromium + desktop lease beat (#410 slice 2b, closes #454) - #455

Merged
chayapats merged 3 commits into
mainfrom
jit/410-s16-desktop-session
Nov 11, 2026
Merged

chayapats merged 3 commits into
mainfrom
jit/410-s16-desktop-session

Conversation

@chayapats

@chayapats chayapats commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

DESK16-01 (#410) slice 2b, per ADR 0091 decisions 1 and 6. It also closes every REQUIRED item and NIT of #454, which Tria ruled must land before 2b merges.

Guest desktop session (ADR 0091 decisions 1, 6)

  • qemuDesktopProvisioning is a fixed, idempotent guest program that runs as root over SSH through a separate bounded provisioning lease. The lease is unfiltered, like the Docker install, and is always closed afterwards. The program does this:
    • Creates the system user and group jit-desktop with no sudo and home /var/lib/jit-desktop.
    • Downloads the pinned browser archive and refuses it unless its SHA-256 matches.
    • Unpacks it to /opt/jit-desktop/chrome-<version>.
    • Installs the archive's own deb.deps list, plus Xorg (modesetting), libinput, xinit, fluxbox and fonts from Ubuntu's archive.
    • Writes the Xorg 1280×800 screen config.
    • Writes an AppArmor profile that grants userns to that browser binary only, so the Chromium sandbox stays on. There is no --no-sandbox.
    • Enables two units:
      • jit-desktop-proxy.service: a loopback forwarder on 127.0.0.1:3129, running as jit-desktop, that adds Proxy-Authorization from /etc/jit-desktop/proxy.env. It re-reads that file on every connection, so a re-issued lease applies without a restart. This is needed because Chromium takes no credentials in a proxy URL.
      • jit-desktop.service: Xorg, fluxbox and the browser on vt7 as jit-desktop (PAM login session). The browser is restarted if it exits.
  • Pinned browser (qemuDesktopBrowser): Chrome for Testing 157.0.8103.50, Google's version-pinned Chromium build with no auto-updater. It is published for both linux64 and linux-arm64.
    • x86_64 chrome-linux64.zip: d17167bc092eb495a3b768c7467f27e35f625aa03834233a1663af67dcb668c9
    • arm64 chrome-linux-arm64.zip: 9539c1612121eb09c4b787f4f9d0c732357772eb22583620ffed64d50dd6c4cc
    • I computed both hashes myself from the published files on 2026-11-11. Google publishes no hash for these archives. I unpacked the x86_64 archive with the program's extraction logic, and chrome --version reports Google Chrome for Testing 157.0.8103.50.
  • Provisioning installs the session after cloud-init. For a VM enrolled before desktop was on, the first desktop beat of a later boot installs it through the new provisionLease host option, which startLocalVmControl composes only when the desktop lease exists. A failed install is tried once per boot and never fails the start. A headless controller installs nothing.

#454: one desktop beat per boot (every 10 min)

  • VM running with a lease held: renew it. A renewal that throws revokes the lease, and the next beat issues a new one.
  • VM running with no lease (full pool, failed delivery, guest still without the session): issue a lease, installing the session first if a provisioning source exists and this boot has not tried yet.
  • Transient inspect() throw (QMP busy with a live snapshot, management timeout): skip that beat and keep the lease and the timer.
  • VM proven not running: revoke the lease and end the beat until the next protected start.
  • vm_boundary_unproven: revoke the lease (fail closed) and keep the beat.
  • The beat is now started at every boot and on re-attach, even when the first issue or delivery failed. A stop aborts any delivery or install still in flight. Desktop work runs one item at a time.
  • jit-desktop group timing: provisioning issues the lease again after cloud-init and the desktop install, so a first boot whose group did not exist yet still ends with a lease. This uses the "re-issue after cloud-init" option that #410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453 #454 allows.
  • NIT: the s15-working-vm-v1 contract now says each renewal starts a fresh 512 MiB allowance per period.
  • NIT: the lease writer refuses $, because systemd expands it in an environment file.

Docs: updated the s15-working-vm-v1 contract (desktop lease beat and the new "Desktop session" section), s16-computer-screen-v1 (the screen now shows the desktop), ADR 0090 "Pending" and ADR 0091 status and decision 6 (which build is pinned).

Closes #454
Refs #410
Refs #392

Tests

Tested head: f0dff38. It fixes Greptile's 2 P1s and 1 P2 on fb2eb92, listed under "Greptile fixes" below.

  • bun test tests/s16-desktop-lease.test.ts: 20 pass / 0 fail. New or changed tests:
    • #410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453 #454 (a): a live snapshot holds QMP and the beat's inspect() fails as qmpExchange does after its 30s timeout. The beat is skipped, the lease is kept with no renewal and no cancel, and the next beat renews it.
    • #410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453 #454 (b): the lease pool is full at boot, so the VM runs without a lease and the beat stays armed. A beat while the pool is still full does nothing. A beat after it frees obtains and delivers a lease, and the next beat renews it.
    • Unproven boundary: the lease is revoked and the beat continues. Once the boundary is proven again, a beat issues a new lease.
    • Guest without the session and no install source: the lease is issued at boot and again after cloud-init, and both are revoked. Once the group exists, a beat gets the lease. Headless: no lease, no beat, no install.
    • Provisioning installs the pinned session (exact url, sha256 and version for the architecture), then issues the desktop lease. The Docker and desktop provisioning leases are separate and both closed.
    • A failed install is tried once per boot. After a stop and start, the beat installs through provisionLease.
    • Local control: provisionLease exists only with the desktop lease and is unfiltered.
    • The lease writer refuses $.
    • The guest program parses as Python, has two distinct per-architecture pins, has no --no-sandbox and has no sudoers entry.
    • The loopback forwarder program runs for real under python3. It replaces the client's Proxy-Authorization with exactly one header built from proxy.env, and returns 503 with nothing sent upstream when no lease file exists.
  • Greptile fixes:
    • P1 "unproven VM": the desktop lease is issued, renewed or used for an install only while inspect() shows the VM running with network proxy-only. If the network is unproven, the beat revokes the lease and keeps running, so a VM that is proven again gets a new lease. The new test adds an extra -device.
    • P1 "partial install": the installer removes the /opt/jit-desktop/ready-v1 marker first and writes it last. The lease writer requires that marker to name the pinned version and reports "absent" otherwise, so a partial or outdated install is installed again. The new test runs the real writer with only the paths patched.
    • P2 "broken desktop reports ready": the installer reports ready only after both units and the pinned browser process have stayed up for three checks in a row.
  • Mutation check for the network fix: with the network check removed, the unproven-network test fails.
  • Mutation check: with the fix reverted in the beat (any throw revokes, or no re-issue while no lease is held), tests (a) and (b) each fail.
  • bun test --timeout 30000 on s16-desktop-lease, s15-vm-qemu, s15-vm-provisioning, s16-computer-screen, s16-screen-input, s15-vm-proxy, s15-vm-local, s15-vm-vmm, s15-vm-enrollment and s15-vm-pool at f0dff38: 159 / 0.
  • tests/s15-vm.test.ts has 1 local failure on fb2eb92: descriptor-anchored search preserves real rg …. It needs the real rg binary, which this machine does not have. It is unrelated to this change, and CI installs ripgrep.
  • tsc --noEmit -p .: clean. git diff --check: clean.

Honest pending evidence

  • Not run on real QEMU. The guest program (apt satisfy of deb.deps on 26.04, rootless Xorg under the PAMName=login unit on vt7, fluxbox, the AppArmor userns profile, and the browser showing on the screen) is checked only by the deterministic tests and the local forwarder and extraction runs above. Live evidence belongs to LIVE16-01 ([Frontier] [LIVE16-01] Collect exact-build Linux, account and native-device evidence #431): the screen shows the browser, the owner signs in to a trusted fixture site, hands back, and the session survives a stop and start.
  • Residual, recorded in the contract: any guest process can use the loopback forwarder, as well as read the lease. This matches ADR 0091's accepted consequence that anything in the guest can reach non-sensitive public hosts while the VM runs.
  • The sensitive-host additions from #410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453 #454 (gmail.com, outlook.com, outlook.office365.com, idmsa.apple.com, oauth2.googleapis.com) are Champ's decision and are not changed here.
  • Download size: each browser archive is about 203 MB, under one provisioning lease's 512 MiB allowance. It needs publicWeb on the proxy (the default), because storage.googleapis.com is not a registry host.

…ase beat (#410 slice 2b, ADR 0091 decisions 1/6, closes #454)
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge.

Summary

Adds a pinned guest browser and desktop session, with a separate provisioning lease and a recurring desktop lease beat.

  • Opted-in QEMU guests now show a pinned browser desktop.
  • Desktop leases retry and renew while VMs stay eligible.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Boot or re-attach] --> B[Arm desktop beat]
  B --> C[Inspect VM]
  C -->|Stopped| D[Revoke lease and cancel beat]
  C -->|Boundary or network unproven| E[Revoke lease and keep beat]
  C -->|Temporary failure| F[Skip this beat]
  C -->|Running and proxy-only| G{Lease held?}
  G -->|Yes| H[Renew lease]
  G -->|No| I[Deliver lease or install missing desktop]
Loading

Reviews (3) · Last reviewed commit: "fix(s16): re-attach arms the desktop bea..." · Reviewed by Greptile

Comment thread src/s15/vm/qemu.ts Resolved
Comment thread src/s15/vm/qemu.ts Resolved
Comment thread src/s15/vm/qemu.ts Resolved
…l; installer verifies the running session (Greptile #455)
@chayapats

Copy link
Copy Markdown
Owner Author

@greptileai please re-review head f0dff38 (fixes for the 2 P1s and the P2).

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 11, 2026

@chayapats chayapats left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of f0dff38 (#410 slice 2b + #454)

No blocker in the diff. One SHOULD. CI on this head is still red, so this is not a merge.

SHOULD — a transient inspect during reattach never arms the beat

QemuVmManagement.reattached (src/s15/vm/qemu.ts:484-487) calls inspect() and only then armDesktop(). reattachOne (src/s15/vm/service.ts:504) swallows any throw from reattached ("The VM works without its desktop lease") and does not fence.

health() just before that also calls inspect(), then does SSH identity and egress probes while QMP is free. A snapshot that takes the QMP socket in that window holds it for up to 300s. inspect's qmp times out at 30s as vm_management_unavailable (src/s15/vm/qmp.ts), so reattached's inspect() throws before the timer exists. The VM stays ready. No lease is issued and nothing retries until the next controller restart. That misses #454: a transient inspect failure must skip a beat, not prevent the beat from existing.

Fix: arm the beat before inspect(), and handle a throw the same way desktopBeat does (boundary unproven: revoke and keep the timer; anything else: skip). A VM that is not running should still end the beat.

NIT — the ready check's pgrep -f is an unanchored regex

src/s15/vm/qemu.ts:922. pgrep -f treats the pattern as an ERE and does not anchor it. Probed locally: a process whose argv contains .../chrome_crashpad_handler matches, and so does chrome-157X0X8103X50/chrome because . is any character. The three-in-a-row check makes this narrow, but it can write ready-v1 without the pinned browser. Escape the pattern and anchor it (^.../chrome( |$)), or match /proc/<pid>/exe.

Merge gate, not a code defect

Run 43035519562 on f0dff38 is red. The test jobs die in apt-get (Temporary failure resolving security.ubuntu.com). The deterministic job is only test "failure" = success after those failures. typecheck and Greptile passed. Do not merge until a rerun is green. I could not re-run bun test here (Cannot find module 'schway/host').

Checked, not findings

  • Both pins match the published archives, hashed 2026-11-11: linux64 d17167bc092eb495a3b768c7467f27e35f625aa03834233a1663af67dcb668c9 (203,527,334 bytes), arm64 9539c1612121eb09c4b787f4f9d0c732357772eb22583620ffed64d50dd6c4cc.
  • The writer rejects $ (and newline, quote, backslash). Ran the real program.
  • desktopBeat matches the #454 ruling for the in-process timer: renew, re-issue, skip a transient throw, revoke on unproven or not proxy-only, end the beat when not running. A qmp timeout is vm_management_unavailable, not vm_boundary_unproven.
  • Real linux64 zip: no .. entries; chrome is mode 0755. The deb.deps string passed to apt-get satisfy parses. On resolute, libgtk-3-0t64 and libasound2t64 Provide the old names, so that part of deb.deps is not an obvious 26.04 miss.
  • The guest program shell-quotes and parses. The forwarder's CRLF literals are real CRLF. It connects only to 192.168.5.100:3128.
  • Sensitive-host list unchanged (Champ's question). The install lease is unfiltered, as the contract says. The test's 403 on accounts.google.com is the private DNS answer; the lookup is recorded.

Live Xorg under PAMName=login, and the AppArmor userns profile, still belong to #431. I did not find a path that issues a lease if that health check fails.

Open, not filed: the install lease is 512 MiB and the zip alone is 194 MiB, so apt on that same lease has about 318 MiB left. I did not measure the apt closure, so I am not calling it a defect.

@chayapats

Copy link
Copy Markdown
Owner Author

⚖️ คำตัดสิน (Tria): เข้าข้าง Duo ยังไม่ผ่านที่ f0dff38: ต้องแก้ SHOULD 1 ข้อใน PR นี้ และ CI ต้องเขียวก่อน merge

เหตุผล

  • SHOULD ยืนยันแล้ว ระดับคง SHOULD (ไม่ถึง BLOCKER): reattached (src/s15/vm/qemu.ts:484-487) เรียก inspect() ก่อน armDesktop() ถ้า inspect throw จะไม่มี timer ส่วน reattachOne (src/s15/vm/service.ts:504) กลืน error ไว้ ผลคือ VM อยู่ในสถานะ ready แต่ไม่มี lease และไม่มี beat ไปจนกว่า controller จะรีสตาร์ท ซึ่งขัดกับ #410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453 #454 (a) ที่ว่า inspect ล้มชั่วคราวต้องแค่ข้าม beat ไม่ใช่ทำให้ beat หายไป ที่ไม่ถึง BLOCKER เพราะเป็น fail-closed (ไม่มี lease ก็ไม่มีเว็บ) จึงไม่มีความเสี่ยงด้าน security
  • ตัวจุดชนวนที่ Duo ยกมาไม่ถูกต้องนัก แต่ข้อบกพร่องมีจริง: snapshot จาก service เข้ามาซ้อนในช่วง health() ไม่ได้ เพราะ reattach ถือ #managementBusy ไว้ (service.ts:493-494) และ system session ก็เช็ก/เพิ่มแบบ sync (service.ts:120, 137) แต่ inspect() ยัง throw ได้จากทางอื่น เช่น unitState()/systemctl, QMP timeout 30s (qemu.ts:255) หรือ qemu-img info (qemu.ts:275)
  • แก้ได้น้อยมาก: openDesktopLease จัดการกรณี inspect throw และกรณี network ไม่ใช่ proxy-only ไว้แล้ว (qemu.ts:387) ส่วน beat เจอ VM ที่ไม่ได้รันก็เรียก endDesktop() (qemu.ts:446) ดังนั้นใน reattached แค่เรียก armDesktop() ก่อน แล้วตามด้วย desktopTurn(() => openDesktopLease(signal)) โดยไม่ต้องมี inspect นำหน้า
  • NIT pgrep (qemu.ts:922): B+'/chrome' ไม่ได้ anchor และไม่ได้ escape จึงไปตรงกับ chrome_crashpad_handler ด้วย ทำให้ readiness อาจผ่านทั้งที่ chrome ไม่ได้รันอยู่ ผลกระทบต่ำแต่แก้บรรทัดเดียว
  • CI: run 43035519562 บน f0dff38 ทุก test job แดงที่ apt-get เพราะ resolve DNS ไม่ได้ (typecheck และ Greptile เขียว) เป็นปัญหา infra ไม่ใช่โค้ด แต่ยังนับเป็น merge gate

ทำต่อ

  1. Mono: แก้ reattached ตามที่ว่าไว้ และเพิ่มเทสว่า inspect ใน reattached throw แล้ว beat ยังถูก arm และ beat ถัดไปออก lease ได้ (revert fix แล้วเทสต้อง fail)
  2. Mono (แนะนำให้ทำใน commit เดียวกัน ไม่บังคับ): anchor pgrep เช่น '^'+re.escape(B+'/chrome')+'( |$)' หรือเทียบ /proc/<pid>/exe
  3. budget 512 MiB ของ install lease: ไม่บล็อก PR นี้ ให้ Mono เพิ่มเป็นรายการที่ต้องวัดใน [Frontier] [LIVE16-01] Collect exact-build Linux, account and native-device evidence #431 (ขนาด zip + apt closure จริงภายใต้ provisioning lease บน QEMU จริง ถ้าเกินค่อยเปิด follow-up ปรับ budget) เพราะวัดได้บนเครื่องจริงเท่านั้น และถ้าเกินก็เป็น fail-closed (install ล้ม 1 ครั้งต่อ boot)
  4. push แล้ว CI ทั้งชุดต้องเขียวบน head ใหม่ และ Greptile ต้องไม่มีคอมเมนต์ค้าง จากนั้น Duo re-review เฉพาะ delta แล้วส่งกลับมาที่ Tria

ความมั่นใจ: สูง

…the session's chrome pgrep (Duo/Tria #455)

QemuVmManagement.reattached no longer inspects before armDesktop(): a transient inspect failure (QMP held by a live snapshot) used to throw before the beat existed, leaving a ready desktop VM without a lease or beat until the next controller restart. openDesktopLease already swallows inspect failures and refuses unproven networks; the beat ends itself on a non-running VM. New test fails when the fix is reverted. The installer's pgrep is now anchored and escaped so chrome_crashpad_handler no longer counts as the browser.
@greptile-apps
greptile-apps Bot dismissed their stale review November 11, 2026 15:01

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@chayapats

Copy link
Copy Markdown
Owner Author

Fixes for Tria's ruling (#455 (comment)) in 60104b7:

  1. SHOULD (Duo): reattached inspected before arming the beat. QemuVmManagement.reattached now calls armDesktop() first, then desktopTurn(() => openDesktopLease(signal)), with no inspect before it. openDesktopLease already swallows inspect throws and refuses a network that is not proxy-only. The beat ends itself when it finds the VM not running. New test #455: a transient inspect failure while re-attaching still arms the beat; the next beat issues the lease: QMP throws vm_management_unavailable during reattached(), the beat is still armed with no lease, and the next beat issues one. With the old order restored, the test fails.
  2. NIT (Duo): unanchored pgrep. The installer now checks pgrep -u jit-desktop -f '^'+re.escape(B+'/chrome')+'( |$)'. Probed with real pgrep against stand-in processes: .../chrome-157.0.8103.50/chrome matches; chrome_crashpad_handler and chrome-157X0X8103X50/chrome do not.
  3. Apt closure budget. This is filed as a measurement item on [Frontier] [LIVE16-01] Collect exact-build Linux, account and native-device evidence #431 (zip plus apt closure bytes under the 512 MiB provisioning lease on real QEMU). A follow-up will be opened only if it exceeds the budget.

Local results: tests/s16-desktop-lease.test.ts 21/0. tsc is clean.

@chayapats

Copy link
Copy Markdown
Owner Author

@greptileai please re-review head 60104b7.

@chayapats chayapats left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Delta review of 60104b7 (f0dff38..60104b7)

Previous SHOULD and NIT are fixed. No new findings. CI on this head is green (run 43044244466, attempt 8). From this review the delta is shippable; Tria still rules.

SHOULD — closed

QemuVmManagement.reattached (src/s15/vm/qemu.ts:486-488) calls armDesktop() and then desktopTurn(() => openDesktopLease(signal)). The inspect() that used to run first is gone, so a throw can no longer happen before the beat exists.

openDesktopLease already swallows an inspect() throw (src/s15/vm/qemu.ts:387: try { if (!this.isolated(await this.inspect())) return; } catch { return; }). A VM that is proven not running ends the beat in desktopBeat (src/s15/vm/qemu.ts:448). repeat is setInterval at 600s, so the first beat does not race the reattach delivery.

The new test (tests/s16-desktop-lease.test.ts:255) throws vm_management_unavailable from qmp for the whole of reattached(), then expects one live beat and a lease on the next beat. Putting the old if ((await this.inspect()).state !== "running") return; back makes reattached() throw before armDesktop(), so await m.reattached() rejects and beats stays empty. CI ran that test: x64 shard 5, job 130674274974, (pass) #455: a transient inspect failure while re-attaching still arms the beat; the next beat issues the lease [311.11ms] inside 644 pass / 0 fail. arm64 shard 5 passed too. I did not re-run the revert locally (no bun on this machine); the fail-on-revert claim is from that control flow, and the pass is from CI.

NIT — closed

The ready check is now pgrep -f '^'+re.escape(B+'/chrome')+'( |$)' (src/s15/vm/qemu.ts:923). Probed with procps-ng 4.0.2: that pattern matches a process whose argv starts with /opt/jit-desktop/chrome-157.0.8103.50/chrome and does not match .../chrome_crashpad_handler or chrome-157X0X8103X50/chrome. The session script execs @B@/chrome as argv0, so the anchor matches the process the installer starts. Live Xorg remains #431.

CI — not this diff, and now green

Attempts 1–7 failed in sudo apt-get update before bun test (that step was skipped). Attempt 7, job 130661520282 (tests x64, 0): Temporary failure resolving 'azure.archive.ubuntu.com', archive.ubuntu.com, and security.ubuntu.com. Compare 51377d1...60104b7 does not touch a workflow file. The guest apt-get is a string that runs inside the VM, not on the runner, so this PR cannot cause that DNS failure.

Attempt 8 started 2026-11-11T17:01:48Z and concluded success at 17:07:39Z: typecheck, all 12 test shards, deterministic, and Greptile. https://github.com/chayapats/jit/actions/runs/43044244466

Checked, not findings

  • The only production caller of reattached is reattachOne (src/s15/vm/service.ts:504), and only after inspect plus health already showed the VM running. A stop in that window arms a beat that issues nothing and ends itself on the next tick. That is the order Tria required.
  • The 512 MiB measurement item is on #431 (comment 2026-11-11T15:02:04Z).
  • Greptile has no new comments on this commit; its check passed.

No blockers. No new SHOULD.

@chayapats

Copy link
Copy Markdown
Owner Author

⚖️ Tria ruling — PR #455 @ 60104b7: ✅ PASS (nothing to fix now)

  • Duo's delta review (f0dff38..60104b7) has no findings. The earlier SHOULD is fixed: reattached arms the beat before any inspect (src/s15/vm/qemu.ts:486-488), and the new test tests/s16-desktop-lease.test.ts:255 passed on CI. The pgrep NIT is also fixed (qemu.ts:923).
  • This delta changes only src/s15/vm/qemu.ts and tests/s16-desktop-lease.test.ts. It touches no workflow file, so the PR did not cause the red CI in attempts 1-7. Those failed on runner DNS during apt-get.
  • Verified now: run 43044244466 attempt 8 = success on head 60104b77565e. All 15 checks are green (typecheck, 12 test shards, deterministic, Greptile Review). The PR is MERGEABLE / APPROVED with 0 unresolved review threads.

Next: Mono may merge under Champ's rule: every check green on the latest head, Greptile has no open comments, and every review comment is addressed. Before merging, confirm the head is still 60104b7. If the head changes, CI must pass again first. Optional follow-up: open a separate issue/PR to add apt retry or a mirror fallback to the workflow so runner DNS stops blocking merges. That is out of scope for this PR.

Confidence: high

@chayapats
chayapats merged commit 41b4bd2 into main Nov 11, 2026
22 of 113 checks passed
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.

#410 slice 2b required residual: desktop lease beat (renew/retry/skip-transient) + NITs from #453

1 participant