Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -596,3 +596,126 @@ schema itself:
|
||||
F9` against the **real compositor** with an isolated `XDG_CONFIG_HOME`. That
|
||||
was true before this phase and is unchanged, but it is the one contract in
|
||||
this wave that writes to the running keymap, so it wants a quiet moment.
|
||||
|
||||
## Phase 9 (Network & Sharing) — append below
|
||||
|
||||
Spec: `2026-08-24-network-sharing-redesign.md`. Connections became a full
|
||||
network manager (per-connection details, forget, autoconnect, MAC
|
||||
randomization, VPN import, hotspot, enterprise Wi-Fi, airplane mode, system
|
||||
proxy), Firewall grew its add side and a zone browser, Printers gained per-
|
||||
printer defaults and job hold/release, and Sharing got copy and structure work.
|
||||
The last two GNOME punt rows in the app died with the "Owned by Fedora" card.
|
||||
|
||||
**Nothing here was run against a live harness.** Three agents edited the tree
|
||||
concurrently. What *was* verified is listed per contract below.
|
||||
|
||||
### New contracts (1)
|
||||
|
||||
`quickshell/network-tools-contract`. The README count line moves **169 → 170**;
|
||||
`setup/readme-contract` was run and passes ("170 contracts, as documented"),
|
||||
counted the same way `panama test` collects the suite.
|
||||
|
||||
### Verified
|
||||
|
||||
- **`quickshell/network-tools-contract` — RUN END TO END, PASS.** It is safe to
|
||||
run because it never reaches the machine: `env -i` with a stub directory first
|
||||
on `PATH`, stubs for nmcli/gsettings/rfkill plus every D-Bus client, a
|
||||
PyGObject stand-in on `PYTHONPATH` whose `require_version` always raises, and
|
||||
both D-Bus bus addresses pointed at sockets that do not exist. It asserts the
|
||||
first two of those before running the helper at all, and refuses to continue
|
||||
if `nmcli`, `gsettings` or `rfkill` resolves anywhere but the stub directory.
|
||||
Checked with a debug copy that it is doing real work rather than passing
|
||||
vacuously: the stub's terse listing drives the shipped parse all the way to
|
||||
`{"ip4":"192.168.7.42/24","gateway":"192.168.7.1","dns":["192.168.7.1",
|
||||
"1.1.1.1"],"mac":"AA:BB:CC:DD:EE:FF","macRandomized":true,…}` while the three
|
||||
secret properties in the same listing are absent from it.
|
||||
- The libnm/D-Bus branch of `join-enterprise` is deliberately **not**
|
||||
exercised — running it would add a real connection profile. It is covered
|
||||
statically instead, by an AST walk that fails if any command list in the
|
||||
helper carries a password-shaped identifier. The nmcli fallback is what the
|
||||
dynamic half drives, and there the password is asserted present on the
|
||||
editor's **stdin** and absent from **argv**, stdout, stderr and disk.
|
||||
- **`quickshell/connectivity-contract` — static half run, and it found a bug.**
|
||||
See "Still open" below. The new enum check reads the member list out of the
|
||||
installed `quickshell-network.qmltypes` rather than a hand-kept list, so it
|
||||
cannot drift the way the two bugs it catches did.
|
||||
- **`quickshell/firewall-contract` — static half run, PASS** (everything up to
|
||||
the first live `firewall-cmd` read).
|
||||
- **`quickshell/printers-contract` — static half run, PASS.**
|
||||
- **`quickshell/sharing-contract` — static half run, PASS.**
|
||||
- **`quickshell/gnome-handoff-contract` — RUN END TO END, PASS** (source-only):
|
||||
"ok (12 handoffs checked against 39 pages)" with `network` and `wifi` now in
|
||||
OWNED.
|
||||
- **`quickshell/health-ui-contract` — static half run, PASS.**
|
||||
- **`PANAMA_SETTINGS_STATIC_ONLY=1 settings-pages-contract` — PASS.**
|
||||
- **`setup/readme-contract` — RUN END TO END, PASS.**
|
||||
- `bash -n` on every changed contract; every embedded `python3` heredoc compiled
|
||||
separately with `py_compile`.
|
||||
|
||||
### Still open before the run
|
||||
|
||||
- **`Connectivity.qml:204` names an enum member that does not exist.**
|
||||
`ConnectionFailReason.Authentication` is not a member; the real ones are
|
||||
`NoSecrets`, `Unknown`, `WifiAuthTimeout`, `WifiClientDisconnected`,
|
||||
`WifiClientFailed`, `WifiNetworkLost`. QML resolves it to `undefined`, the
|
||||
switch arm never matches, and a wrong password is reported with the
|
||||
fallthrough text. `NoSecrets` is almost certainly the intended member.
|
||||
`connectivity-contract`'s new enum check fails on exactly this line and passes
|
||||
on a copy with it corrected, so the check is confirmed working and the fix is
|
||||
a one-word edit in a service file this phase's contract owner does not own.
|
||||
**This is the one item blocking a clean run.**
|
||||
- **The enum check is scoped to `Connectivity.qml`.** The same class of bug —
|
||||
a plausible enum member that does not exist, resolving to `undefined` in
|
||||
silence — can live anywhere that imports `Quickshell.Networking`, including
|
||||
`modules/quicksettings/WifiList.qml` and the bar's status cluster. Widening
|
||||
the sweep to every QML file that imports the module is a small change and the
|
||||
obvious next one. Owner: whoever holds `connectivity-contract` next.
|
||||
- **`network-tools-contract` cannot see the libnm branch.** That is a deliberate
|
||||
trade, but it means the branch that actually runs on this machine (NM 1.56,
|
||||
bindings present) is only ever checked by reading. A machine without PyGObject
|
||||
would exercise the fallback for real; nothing here has one.
|
||||
- **Two contracts still pin prose.** `firewall-contract` requires the add flow
|
||||
to say "permanent" and "ask for your password", and `sharing-contract`
|
||||
requires "never passes through Panama", "install it and this becomes a switch"
|
||||
and "does not install software". The spec names all five phrases, so they are
|
||||
pinned deliberately, but they are the needles a copy edit will trip.
|
||||
- **`printers-contract` pins the option vocabulary exactly** — `media` ∈
|
||||
{Letter, A4, Legal}, `sides` ∈ {one-sided, two-sided-long-edge,
|
||||
two-sided-short-edge}. Adding a third settable option is meant to be a
|
||||
deliberate act that updates this contract, but it will read as a surprise the
|
||||
first time somebody tries.
|
||||
- **`connectivity-contract`'s live half is unchanged** and still starts a
|
||||
harness beside the running session, so it wants the same quiet moment it
|
||||
always did. `PANAMA_CONNECTIVITY_STATIC_ONLY=1` runs the new source-only half
|
||||
alone.
|
||||
- Run order for this phase: the source-only contracts first
|
||||
(`gnome-handoff-contract`, `PANAMA_NETWORK_STATIC_ONLY=1
|
||||
network-tools-contract`, `PANAMA_CONNECTIVITY_STATIC_ONLY=1
|
||||
connectivity-contract`, `PANAMA_SETTINGS_STATIC_ONLY=1
|
||||
settings-pages-contract`, `setup/readme-contract`), then
|
||||
`network-tools-contract` in full — it is hermetic, so it can run at any time —
|
||||
then the read-only system contracts (`firewall-contract`,
|
||||
`printers-contract`, `sharing-contract`), then the harness contracts
|
||||
(`connectivity-contract`, `health-ui-contract`, `settings-search-contract`),
|
||||
and `settings-pages-contract` last, as before.
|
||||
|
||||
### Docs updated in the same wave
|
||||
|
||||
- `services/SettingsSearch.qml` — the three "Managed by GNOME Settings"
|
||||
connectivity entries are gone, and with them the duplicate **Printers** entry
|
||||
that routed to `connectivity` rather than to the Printers page. Ten entries
|
||||
replace them, all routing to `connectivity`: **Wi-Fi**, **Bluetooth**, **VPN**,
|
||||
**Import a VPN**, **Hotspot**, **Airplane mode**, **Network proxy**, **IP
|
||||
address**, **Forget a Wi-Fi network**, **Enterprise Wi-Fi**. Wi-Fi and
|
||||
Bluetooth were rewritten rather than deleted: the spec lists eight additions,
|
||||
none of which contains the word "Bluetooth", so deleting the lying entry
|
||||
outright would have made a switch the page has unreachable by search.
|
||||
- Checked against `settings-search-contract`'s pinned query list: none of its 21
|
||||
ranked queries, nor the nine leaf-routing queries, matches any new or changed
|
||||
entry, so no pinned top result moves. `connectivity` is a leaf in
|
||||
`SettingsRoutes`, so the routing sweep holds.
|
||||
- No settings docs or launcher commands were regenerated: this phase added no
|
||||
schema keys, so there is nothing for `panama-settings-docs` or
|
||||
`panama-settings-commands` to pick up. Verified by reading the diff — every
|
||||
new setting here is system state (NetworkManager, firewalld, CUPS, gsettings),
|
||||
not a Panama preference.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Network & Sharing redesign — the last punt dies
|
||||
|
||||
Approved mock: `home-mocks/network.html` (scratchpad, :8642). Spec wins over mock on conflict.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Connections becomes a full network manager**: per-connection details (IP/DNS/gateway/MAC),
|
||||
forget + autoconnect + MAC randomization, native VPN card with import, hotspot, airplane
|
||||
mode, enterprise Wi-Fi join, system proxy. The "Owned by Fedora" card and both GNOME punt
|
||||
rows are removed.
|
||||
2. **Firewall gains its add side**: allow service/port flow, per-connection zone dropdowns,
|
||||
default zone, zone browser — all existing helper verbs plus one new read-only one.
|
||||
3. **Printers**: per-printer defaults (paper size, two-sided) and job hold/release.
|
||||
4. **Sharing**: copy/structure polish only (error banner into a card, honest Samba row, RDP
|
||||
credential flow copy without naming the terminal in user-facing text).
|
||||
5. Search stops lying ("Managed by GNOME Settings" entries die; duplicate Printers entry
|
||||
deduped; new entries for VPN, hotspot, airplane, proxy, IP address).
|
||||
|
||||
Non-goals: Samba share management (not installed here — the row explains what installing
|
||||
unlocks), PPD/vendor drivers (contract-banned), WWAN (no hardware), per-connection static IP
|
||||
editing (view-only details this phase).
|
||||
|
||||
## New helper: `scripts/panama-network` (pinned verbs)
|
||||
|
||||
Python, the panama-vpn/panama-sharing discipline: validated inputs, JSON out, bounded
|
||||
timeouts, mutations return fresh state. Input validation: connection/SSID names against a
|
||||
conservative charset, file paths must exist and end in .conf/.ovpn for import.
|
||||
|
||||
- `details <connection>` → `{ ip4, gateway, dns: [], mac, macRandomized }` — **never secrets**.
|
||||
- `forget <connection>` — `nmcli connection delete`.
|
||||
- `set-autoconnect <connection> <bool>`.
|
||||
- `set-mac-random <connection> <bool>` — `wifi.cloned-mac-address random|permanent`; the JSON
|
||||
notes a reconnect is needed to take effect.
|
||||
- `import-vpn <file>` — `nmcli connection import type wireguard|openvpn file …` (type by
|
||||
extension); returns the imported connection's name.
|
||||
- `hotspot start <ssid>` / `hotspot stop` / `hotspot status` — `nmcli device wifi hotspot`;
|
||||
the generated password is read back via `nmcli device wifi show-password` and returned once
|
||||
for the UI to display alongside a QR (reuse the panama-wifi-qr pipeline if trivial, else
|
||||
text-only this phase).
|
||||
- `join-enterprise` — SSID + eap profile (peap-mschapv2 | ttls-pap) + identity on argv,
|
||||
**password on stdin, never argv**. Mechanism: probe for python NM gi bindings
|
||||
(`gi.require_version('NM','1.0')`) and use D-Bus AddConnection when present; else fall back
|
||||
to a scripted `nmcli connection edit` session over stdin (which keeps the secret out of ps).
|
||||
Agent A picks after probing and documents the choice in the script header.
|
||||
- `proxy get` / `proxy set <mode> [host port | pac-url]` — gsettings `org.gnome.system.proxy`
|
||||
(mode none|manual|auto; manual sets http+https+socks host/port together this phase).
|
||||
- `airplane status` / `airplane set <bool>` — rfkill, matching the existing keybind path.
|
||||
|
||||
## Services (A)
|
||||
|
||||
- **`services/NetworkTools.qml`** (new singleton) wraps panama-network: cached `detailsFor
|
||||
(connection)` (refreshed on page open + active-connection change), `forget`, `setAutoconnect`,
|
||||
`setMacRandom`, `importVpn(path)`, hotspot state/start/stop, `proxyMode`+setters,
|
||||
`airplaneOn`+toggle, `joinEnterprise(...)` (password handed through a Process stdin write),
|
||||
`busy`/`lastError`. Test seam: `PANAMA_NETWORK_HELPER` env override for the helper path.
|
||||
- **`services/Connectivity.qml`** stays pure-native (contract-pinned no-shell-out): add
|
||||
`setWifiEnabled(bool)` and `setBluetoothEnabled(bool)` wrappers (native writes) so the page
|
||||
toggles stop bypassing the service; add `objectName`-friendly derived state if needed.
|
||||
- **`services/Firewall.qml`** + `scripts/panama-firewall`: new read-only verb
|
||||
`zone-info <zone>` → services/ports/summary for the zone browser; service exposes
|
||||
`zoneInfo(zone)` with a small cache. Existing verbs untouched.
|
||||
- **`services/Printers.qml`** + `scripts/panama-printers`: new verbs
|
||||
`get-options <printer>` (lpoptions: media, sides — curated keys only), `set-option
|
||||
<printer> <key> <value>` (validated against a closed key/value vocabulary:
|
||||
media=Letter|A4|Legal, sides=one-sided|two-sided-long-edge|two-sided-short-edge),
|
||||
`hold <job>` / `release <job>` (python3-cups). Mutations return fresh snapshots.
|
||||
- `services/Vpn.qml` untouched (import lands through NetworkTools; the list/toggle stays as-is,
|
||||
now also consumed by the page).
|
||||
|
||||
## UI (B)
|
||||
|
||||
**ConnectivityPage.qml** rebuilt: Wired card (connection row expands to a details KV grid);
|
||||
Wi-Fi card (toggle via `Connectivity.setWifiEnabled`, connected network expands to details +
|
||||
autoconnect + MAC-randomize + QR share + Forget with a two-stage confirm; other networks keep
|
||||
Join/inline password; enterprise networks get an inline join form — auth dropdown, identity,
|
||||
password field, optional CA file path, Connect; hotspot row at the bottom); **VPN card**
|
||||
(list from `Vpn.qml` with per-VPN toggles, empty state + Import row → file path entry or
|
||||
zenity-free inline TextField this phase); Bluetooth card (toggle via service wrapper);
|
||||
**Radios & proxy card** (airplane toggle, proxy dropdown expanding to manual host/port or PAC
|
||||
URL fields). The "Owned by Fedora" card is deleted. Page gains `objectName: "connectivity"`.
|
||||
KV details grid is a new small component (`ConnectionDetails.qml`) with mono values.
|
||||
|
||||
**FirewallPage.qml**: existing exposure/remove flows untouched; "Allow something new" row
|
||||
expanding to a kind dropdown (Named service / Port) + validated TextField + Allow button +
|
||||
the "permanent rule, the system will ask for your password" caption; Zones card gains
|
||||
per-connection zone dropdowns (`setZone`, with a confirm that names the interface), default
|
||||
zone dropdown (`setDefaultZone`), and a zone-browser chip row driving `zoneInfo` into a detail
|
||||
line. Only render `zones[0]`'s rules card per current behavior, but the zones card now lists
|
||||
every active zone.
|
||||
|
||||
**SharingPage.qml**: error banner moves into the This-machine card; Remote-login card
|
||||
compresses port/password-mode/connect-with into row details; RDP credentials copy becomes
|
||||
"Set in a terminal so the password never passes through Panama" (kitty stays the mechanism);
|
||||
Samba row explains "install it and this becomes a switch".
|
||||
|
||||
**PrintersPage.qml**: expanded printer gains Paper size + Two-sided dropdowns (from
|
||||
`get-options`, written via `set-option`); queue rows gain Hold/Release next to Cancel; the
|
||||
two duplicate "Search the network" rows merge into the single empty-state card per mock.
|
||||
|
||||
## Search & docs (C)
|
||||
|
||||
Delete the three "Managed by GNOME Settings" connectivity entries and the duplicate Printers
|
||||
entry. Add (page connectivity): VPN, Import a VPN, Hotspot, Airplane mode, Network proxy,
|
||||
IP address, Forget a Wi-Fi network, Enterprise Wi-Fi. Keep existing printers/firewall/sharing
|
||||
entries. Docs regen: none needed (no schema keys) — verify.
|
||||
|
||||
## Contracts (C — write, never run)
|
||||
|
||||
- `connectivity-contract`: still native-only — verify the no-shell-out pin survives (all nmcli
|
||||
lives in NetworkTools/panama-network); add needles for the service-wrapper toggles.
|
||||
- New `network-tools-contract`: stubbed-nmcli fixtures (the vpn-contract pattern): details
|
||||
excludes secrets, forget/autoconnect argv shapes, import type-by-extension, enterprise
|
||||
password arrives via stdin and NEVER argv (assert the argv builder), proxy gsettings calls,
|
||||
airplane rfkill.
|
||||
- `firewall-contract`: extend for the add flow (additions are non-destructive, no confirm
|
||||
required; zone changes ARE consequence-bearing — pin that changing a connection's zone names
|
||||
the interface in its confirm), `zone-info` read-only.
|
||||
- `printers-contract`: closed option vocabulary pinned (no arbitrary lpadmin -o passthrough),
|
||||
hold/release verbs, driverless pins untouched.
|
||||
- `gnome-handoff-contract`: the `network` exclusion reason ("Panama has no VPN or
|
||||
per-connection routing") is now false — move `network` and `wifi` into OWNED so no page may
|
||||
punt to them, and verify nothing does.
|
||||
- `sharing-contract`, `settings-pages-contract`: needles reconciled.
|
||||
- Backlog: Phase 9 section.
|
||||
|
||||
## Agent ownership (parallel)
|
||||
|
||||
- **A**: `scripts/panama-network` (new), `scripts/panama-firewall`, `scripts/panama-printers`,
|
||||
`services/NetworkTools.qml` (new), `services/Connectivity.qml`, `services/Firewall.qml`,
|
||||
`services/Printers.qml`.
|
||||
- **B**: the four pages + new components (+ qmldir).
|
||||
- **C**: `services/SettingsSearch.qml`, contracts above, backlog, README count line only if
|
||||
count changes.
|
||||
|
||||
B programs against the pinned verbs/APIs; A must not change them without updating this spec.
|
||||
Live-desktop rules apply to everyone: no test runs, no harness boots, valid QML/Python at every
|
||||
save, and **no live mutations** — no nmcli writes, no firewall-cmd writes, no lpadmin, no
|
||||
rfkill, no gsettings writes; read-only probes only.
|
||||
Reference in New Issue
Block a user