21 Commits
Author SHA1 Message Date
lyrathorpe 955b6640c3 Merge pull request 'fix(darwin): disable hot-corners' (#109) from fix/disable-hot-corners into main
CI / flake (push) Successful in 4m32s
Reviewed-on: #109
2026-08-28 14:51:27 +01:00
lyrathorpe ea791df4aa fix(darwin): disable hot-corners
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m29s
I hate them and I need to ensure they never come back
2026-08-28 14:46:40 +01:00
lyrathorpe a587f0ab71 Merge pull request 'fix(wsl): provide xdg-open so browser-based logins work' (#108) from fix/wsl-xdg-open into main
CI / flake (push) Successful in 4m27s
Reviewed-on: #108
2026-08-28 13:53:39 +01:00
lyrathorpe 93f1b191c8 Merge pull request 'feat(tmux): show window names in the status bar and pane titles on the border' (#107) from feat/tmux-window-and-pane-titles into main
CI / flake (push) Successful in 5m23s
Reviewed-on: #107
2026-08-28 13:49:32 +01:00
Emma Thorpe a51ed76119 fix(wsl): provide xdg-open so browser-based logins work
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m30s
WSL has no xdg-open and no Linux browser, so anything that shells out to one
fails with:

  exec: "xdg-open,x-www-browser,www-browser": executable file not found in $PATH

kubelogin's interactive login hits this, which matters because interactive is
the mode the shared cluster kubeconfig uses and the mode documented for
re-authenticating after a PIM activation.

Ships an xdg-open on PATH that hands the URL to Windows via powershell.exe,
falling back to explorer.exe, and points $BROWSER at it for tools that read the
variable instead of calling xdg-open. wslu would be the conventional answer but
has been removed from nixpkgs after upstream archived the project.
2026-08-28 13:48:52 +01:00
Emma Thorpe 5bca7e176a feat(tmux): show window names in the status bar and pane titles on the border
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m9s
Catppuccin renders #T, the pane title, in the window list. Any program in the
pane can overwrite that with an OSC escape -- the shell writes the hostname and
Claude Code writes its current task -- so a window renamed with prefix+, never
appeared anywhere.

The status bar now shows the window name, falling back to the pane title while a
window holds a single pane. Once a window is split, pane titles appear on the
pane borders instead; pane-border-status takes no format, so a
window-layout-changed hook recomputes it on both split and close. Adds a binding
for select-pane -T, which has no default.
2026-08-28 12:22:37 +01:00
lyrathorpe 684d3f5a75 Merge pull request 'docs(memory): record that Entra group member reads hide service principals' (#106) from docs/memory-entra-group-member-reads into main
CI / flake (push) Successful in 4m5s
Reviewed-on: #106
2026-08-28 11:35:21 +01:00
Emma Thorpe 99fca0f746 docs(memory): record that Entra group member reads hide service principals
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m3s
az ad group member list and GET /groups/{id}/members return an empty
collection, without error, for groups whose members are service principals.
Records the reads that do work and the rule to trust a Terraform plan over
that output.
2026-08-28 11:33:33 +01:00
lyrathorpe 9b1e2fb447 Merge pull request 'chore(nix): statix bool_comparison + stale-path comment fixes' (#58) from chore/nix-cleanup into main
CI / flake (push) Successful in 5m13s
Reviewed-on: #58
2026-08-26 20:48:37 +01:00
lyrathorpe 9d2379bb3e docs(rpi5): fix stale features.nix path in comment
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m19s
2026-08-26 20:41:05 +01:00
lyrathorpe 7a650dc7cc docs(sway): fix stale features.nix path in comment 2026-08-26 20:41:05 +01:00
lyrathorpe bb613ac803 refactor(users): drop redundant == true (statix bool_comparison) 2026-08-26 20:41:05 +01:00
lyrathorpe 47362090c3 Merge pull request 'docs(memory): record the Task Type field now required on WSP tickets' (#105) from docs/memory-jira-task-type into main
CI / flake (push) Successful in 4m8s
Reviewed-on: #105
2026-08-25 14:33:03 +01:00
Emma Thorpe 8c5773447e docs(memory): record the Task Type field now required on WSP tickets
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 57s
Creating a WSP Task now fails with "Task Type is required to create a Task
issue". The field is customfield_15622 and the create validator enforces it
even though createmeta does not list it as required - the same trap the note
already records for Bug's versions field.
2026-08-25 14:25:24 +01:00
lyrathorpe ae44982c65 Merge pull request 'docs(memory): record the WSP-32957 PIM migration project state' (#104) from docs/memory-wsp-32957-pim-migration into main
CI / flake (push) Successful in 4m44s
Reviewed-on: #104
2026-08-24 17:54:09 +01:00
Emma Thorpe c82c1bef9c docs(memory): record the WSP-32957 PIM migration project state
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m35s
Long-running epic spanning multiple sessions, with several findings that were
expensive to establish and that contradict the Jira epic text — most notably
that production runs in the subscription named "Workspace Platform Technical
Preview", not the one named "Production".

Points at ~/code/WSP-32957-CONTINUATION.md for the detail rather than carrying
it here, following the pattern used for the SIBO project.
2026-08-24 17:51:38 +01:00
lyrathorpe 39b2b1d24b Merge pull request 'fix(tmux): stop clip.exe mangling non-ASCII in the WSL clipboard' (#103) from fix/tmux-clipboard-utf8 into main
CI / flake (push) Successful in 5m47s
Reviewed-on: #103
2026-08-24 13:36:15 +01:00
lyrathorpe 0022a152e3 Merge pull request 'fix(edaas): restore passwordless wheel under sudo-rs' (#102) from fix/edaas-passwordless-sudo-rs into main
CI / flake (push) Successful in 7m44s
Reviewed-on: #102
2026-08-24 13:29:39 +01:00
Emma Thorpe d9db12c4a5 fix(tmux): stop clip.exe mangling non-ASCII in the WSL clipboard
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m8s
tmux-yank autodetects WSL and pipes the selection to clip.exe, which
decodes its stdin as the console OEM codepage rather than UTF-8. Copying
an em dash out of a pane put "ΓÇö" on the Windows clipboard; the same
applies to every non-ASCII character.

Override the copy command to route through tmux's own buffer. With
set-clipboard on, that emits OSC 52 and the terminal receives UTF-8
directly, with no Windows-side helper in the path. Windows Terminal
honours OSC 52; verified against the running client.

Guarded on /proc/version so only WSL is affected. iTerm2 does not accept
OSC 52 by default, so the Darwin hosts keep pbcopy.

Set in the plugin's extraConfig rather than the shared block because
yank.tmux bakes the copy command into its key bindings at load time, and
home-manager emits plugin extraConfig before the run-shell.
2026-08-24 13:26:21 +01:00
Emma Thorpe 4ac9d1108b docs(shell): record the EDaaS passwordless-wheel exception
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m10s
The sudo-rs section claimed the whole fleet runs the stock "wheel, with a
password" policy, which is no longer true for the WSL host. Explain why the
NixOS-WSL default does not survive the sudo-rs swap, and mark the difference in
the per-host table.
2026-08-24 11:16:15 +01:00
Emma Thorpe 0c151943de fix(edaas): restore passwordless wheel under sudo-rs
NixOS-WSL sets `security.sudo.wheelNeedsPassword = false`, but that option
belongs to the `security.sudo` module and does not carry over to the sudo-rs
swap in modules/common-nixos.nix, whose equivalent option defaults to true.
Since that swap landed, sudo on this host prompts for the account password --
which WSL set during install and nobody knows -- so escalation only worked
through `wsl -u root`.

Set `security.sudo-rs.wheelNeedsPassword = false` on the host to match the
NixOS-WSL default. Other NixOS hosts are unaffected and keep the prompt.
2026-08-24 11:16:12 +01:00
12 changed files with 173 additions and 15 deletions
+10 -2
View File
@@ -186,6 +186,14 @@ fleet's stock "wheel, with a password" policy. What it does **not** implement:
host aliases, LDAP/SSSD sudoers, `sudoreplay`, and most `Defaults` settings.
Needing any of those means reverting to `security.sudo`.
One exception to the password: the EDaaS box sets
`security.sudo-rs.wheelNeedsPassword = false`. NixOS-WSL ships that default for
`security.sudo` — WSL has no console login, so the trust boundary is the Windows
session and the Linux account password is never one the user chose — and the
option does not carry across to the `security.sudo-rs` module, which defaults to
requiring one. Without the explicit setting, `sudo` on that host prompts for a
password nobody knows.
If a host ever refuses to escalate, get a root shell that does not go through
sudo (`wsl -u root -d NixOS` on the work box; the console or a serial/HDMI login
elsewhere) and roll back with `nixos-rebuild switch --rollback`, or pick the
@@ -350,10 +358,10 @@ Claude to route new memories there.
## Per-host differences
| | Personal Linux (sway) | macOS | Work WSL (EDaaS) |
| --------------------------- | --------------------- | --------------------- | --------------------------- |
| --------------------------- | --------------------- | --------------------- | ---------------------------- |
| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) |
| `kubectl` → kubecolor | no (no kubectl) | no | yes (work module) |
| `sudo` implementation | sudo-rs | Apple sudo + Touch ID | sudo-rs |
| `sudo` implementation | sudo-rs (password) | Apple sudo + Touch ID | sudo-rs (passwordless wheel) |
| git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) |
| ssh config managed | yes | yes | no (keeps corporate config) |
| ssh-agent | yes | launchd | yes (work module) |
+2
View File
@@ -15,3 +15,5 @@
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
- [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end
- [WSP local build and test](wsp_local_build_and_test.md) — core-services-cloud on this box: dotnet via nix, artifactory creds from `~/.artifactoryenv` sourced per command, how to tell auth failure from a code failure
- [WSP-32957 PIM migration](wsp_32957_pim_migration.md) — AKS RBAC to PIM + AutoPerm decommission; prod is in the Technical Preview subscription, three tenants; resume via `~/code/WSP-32957-CONTINUATION.md`
- [Entra group member reads](entra_group_member_reads.md) — `az ad group member list` hides service principal members; use the servicePrincipal cast or transitiveMemberOf, and trust the Terraform plan over it
@@ -0,0 +1,28 @@
---
name: entra-group-member-reads
description: az ad group member list and Graph /members silently omit service principal members — use the servicePrincipal cast or transitiveMemberOf when a group is expected to hold an SPN.
metadata:
node_type: memory
type: reference
---
`az ad group member list --group <id>` and `GET /groups/{id}/members` both return an **empty collection**, with no error, for a group whose only members are service principals — at least when called with Lyra's user account. The read looks authoritative and is not.
**Reliable reads instead:**
```sh
# members, cast to the type that is being hidden
az rest --method get --url "https://graph.microsoft.com/v1.0/groups/<gid>/members/microsoft.graph.servicePrincipal?\$select=id,displayName"
# count, which does not filter
az rest --method get --url "https://graph.microsoft.com/v1.0/groups/<gid>/members/\$count" --headers ConsistencyLevel=eventual
# from the principal's side
az rest --method get --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spid>/transitiveMemberOf?\$select=id,displayName"
az rest --method post --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spid>/checkMemberGroups" \
--body '{"groupIds":["<gid>"]}' --headers "Content-Type=application/json"
```
**How to apply:** any group that deployment or automation identities belong to — `*-cluster-admins`, `*-keyvault`, anything created by `rg-prereqs` — must be checked with one of the above before concluding it is empty. Cross-check against Terraform: a plan reporting "No changes" against a `members` attribute is strong evidence the membership is present, and outranks the `az` read. On 2026-08-28 the plain read produced a Bug (WSP-33432, cancelled) claiming five `*-cluster-admins` groups across three tenants had been emptied; all five held their deployment principals the whole time.
Related: [[wsp-32957-pim-migration]].
+3 -1
View File
@@ -9,7 +9,9 @@ Field map for the **WSP (Workspace Platform)** Jira project, to create tickets w
**Issue-type IDs:** Epic `10000`, Story `10004`, Task `10008`, Bug `10123`, Sub-task `10009`.
**Fast path — use Task, not Bug.** A `Task` requires only `summary` (project/issuetype auto, reporter defaults to caller). A `Bug` requires six extra fields (below), so only pick Bug when it must be a Bug. The sibling infra/remediation tickets in WSP are Tasks.
**Fast path — use Task, not Bug.** A `Task` requires `summary` plus **Task Type** `customfield_15622` (added since this note was first written; the create validator enforces it even though `createmeta` omits it, same trap as Bug's `versions`). Options value=id: Dev Task=34065, CQE Task=34066, Investigation=34067, Security=34068, Maintenance=34069, Release=34070 — use `Maintenance` for refactors and tidy-ups, `Dev Task` for feature work. A `Bug` requires six extra fields (below), so only pick Bug when it must be a Bug. The sibling infra/remediation tickets in WSP are Tasks.
Example Task `additional_fields`: `{"customfield_15622":{"id":"34069"},"components":[{"name":"Multicluster Platform"}]}`
**Bug required fields** (enforced by the create validator; note `createmeta` omits `versions` but the API rejects without it):
@@ -0,0 +1,58 @@
---
name: wsp-32957-pim-migration
description: State of the WSP AKS-RBAC-to-PIM migration and AutoPerm decommission, and how to resume it
metadata:
node_type: memory
type: project
---
Long-running epic (August 2026) moving WSP's AKS cluster RBAC off **AutoPerm
Manager**-maintained `wsp-*` groups onto the per-subscription **`CEO-*` Azure PIM**
groups, then retiring AutoPerm. Epic **WSP-32957**; on the critical path for Zensar
L1 on-call (WSP-32193). Work happens in **`~/code/multicluster`**
(`terraform/cluster`, `terraform/cluster-k8s-resources`, `products/*/environments/*`).
**Resume from `~/code/WSP-32957-CONTINUATION.md`** — full state, branch list,
verified object IDs, findings and next steps. Jira is the durable record; that file
is the index. Keep it updated as work lands ([[docs-keep-updated]]).
**Landed:** WSP-33141 (multicluster PR #1808, merged `b047163c`) added object-ID
inputs — `admin_group_oids` on `cluster`, and `cluster_user_group_oids` /
`cluster_viewer_group_oids` / `cluster_superuser_group_oids` on
`cluster-k8s-resources`. Supplying IDs _replaces_ the display-name lookup and leaves
the `data "azuread_group"` unread, which is what will let the legacy groups be
deleted. Nothing sets them yet, so behaviour is unchanged. Repoint branches for test
(WSP-33067) and staging (WSP-33068) are pushed but have **no PR** — both gated on
decisions, not code.
**Facts that cost real effort to establish, do not re-derive:**
- **Production runs in `fc7af6ae-…` (_Workspace Platform Technical Preview_), not
`d6d75d07-…` (_Workspace Platform Production_)**, which holds no clusters. The
epic was wrong about this for its whole life and every production `CEO-*` group
name and object ID had to change. Because `CEO-*` names embed the subscription
name, **always re-verify object IDs against live Entra rather than trusting the
epic table.**
- **Three tenants**, not two: `6f4fe054` (prod, prod JP), `335836de` (staging,
staging JP, test), `3eae2746` (dev). Each `wsp-*` name is a distinct object in
each tenant.
- `wsp-staging-cluster-admins` and `wsp-test-cluster-admins` are **empty**, so
`wsp-owner` is the _only_ path to `cluster-admin` in staging. Never drop it before
`SuperAdmin-*` is proven — hence the staging branch is split into an additive
commit and a cutover commit.
- **No break-glass exists**: `disableLocalAccounts = True` on every cluster; only the
deployment SPNs authenticate non-interactively.
- Graph **PIM-for-Groups is unreadable via `az`** (the CLI's first-party client lacks
`PrivilegedAccess.Read.AzureADGroup`, on both `v1.0` and `beta`). Use the portal or
`Connect-MgGraph -Scopes PrivilegedAccess.Read.AzureADGroup`.
**Watch for:** the epic gets rewritten by James Weldrake between sessions — re-read
the description before acting, and check which child tickets are still live
(WSP-33062/33063/33064/33066 were cancelled 2026-08-24, and dev was put out of
scope). Verified findings have repeatedly contradicted the epic text
([[copilot-review-false-positives]] is the same instinct: check against reality
first).
Queued Slack messages and the leaver report live as `~/code/*.txt` alongside the
continuation file; see the table in it for what has and has not been sent
([[workflow-review-and-comments]] — show them before they go out).
+35 -1
View File
@@ -287,7 +287,22 @@ in
plugins = with pkgs.tmuxPlugins; [
sensible
vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes
yank
{
# On WSL, tmux-yank pipes the selection to clip.exe, which decodes its
# stdin as the OEM codepage instead of UTF-8 -- an em dash reaches the
# Windows clipboard as three characters. Route through tmux's own
# buffer instead: with set-clipboard on, tmux emits OSC 52 and the
# terminal takes the text as UTF-8. Windows Terminal honours OSC 52;
# iTerm2 does not by default, hence the runtime guard rather than
# overriding pbcopy/xsel on every host. yank.tmux bakes the command
# into its key bindings when it loads, so this must be set first, which
# is what plugin extraConfig gives us.
plugin = yank;
extraConfig = ''
if-shell 'grep -qi microsoft /proc/version 2>/dev/null' \
"set -g @override_copy_command 'tmux load-buffer -w -'"
'';
}
extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt
{
# Catppuccin Mocha statusline (v2 API: flavour + window options must be
@@ -298,6 +313,14 @@ in
extraConfig = ''
set -g @catppuccin_flavor 'mocha'
set -g @catppuccin_window_status_style 'rounded'
# Catppuccin's default window text is #T, the pane title, which every
# program in the pane is free to overwrite -- the shell writes the
# hostname, Claude Code writes its current task, and a hand-set window
# name never appears. Show the window name instead, falling back to the
# pane title when the window holds a single pane and the two carry the
# same information anyway.
set -g @catppuccin_window_text ' #{?#{==:#{window_panes},1},#T,#W}'
set -g @catppuccin_window_current_text ' #{?#{==:#{window_panes},1},#T,#W}'
'';
}
resurrect # save/restore sessions
@@ -342,6 +365,17 @@ in
set -g renumber-windows on
set -g set-clipboard on
# Pane titles on the border, but only once a window is split -- a single
# pane's title is already in the status bar. pane-border-status takes no
# format, so the hook recomputes it whenever the layout changes, which
# covers both splitting and closing a pane.
set -g pane-border-format " #P #{pane_title} "
set -g pane-border-status off
set-hook -g window-layout-changed 'set -Fw pane-border-status "#{?#{>:#{window_panes},1},top,off}"'
# Pane titles have no default binding.
bind T command-prompt -p "pane title:" "select-pane -T '%%'"
# Catppuccin v2 statusline. Must run after the plugin has loaded;
# home-manager appends this extraConfig after the whole plugin list.
set -g status-left-length 100
+5
View File
@@ -161,6 +161,11 @@
dock = {
show-recents = false;
mru-spaces = false; # don't reorder spaces by use
# Disable hot-corners
wvous-tr-corner = 1;
wvous-tl-corner = 1;
wvous-bl-corner = 1;
wvous-br-corner = 1;
};
finder = {
AppleShowAllExtensions = true;
+5
View File
@@ -60,6 +60,11 @@
## patch the script
systemd.services.docker-desktop-proxy.script = lib.mkForce ''${config.wsl.wslConf.automount.root}/wsl/docker-desktop/docker-desktop-user-distro proxy --docker-desktop-root ${config.wsl.wslConf.automount.root}/wsl/docker-desktop "C:\Program Files\Docker\Docker\resources"'';
# NixOS-WSL's passwordless wheel default only covers `security.sudo`; the
# sudo-rs swap in common-nixos.nix needs it set again. No console login here,
# and no account password anyone knows.
security.sudo-rs.wheelNeedsPassword = false;
features.swayDesktop.enable = false;
# NOTE: this user's systemd --user lingering -- so the home-manager renovate
+1 -1
View File
@@ -17,7 +17,7 @@
# Headless server: the Sway desktop is intentionally not set up. modules/sway.nix is
# not imported and features.swayDesktop.enable defaults to false (declared in
# system/modules/features.nix), so this host keeps plain TTY/SSH login.
# modules/features.nix), so this host keeps plain TTY/SSH login.
# Raspberry Pi boots via U-Boot + extlinux, not GRUB/systemd-boot. The
# raspberry-pi-5 nixos-hardware profile supplies the kernel, firmware and
+1 -1
View File
@@ -12,7 +12,7 @@ let
in
{
# The features.swayDesktop.enable option is declared in
# system/modules/features.nix (so headless hosts can read/set it without
# modules/features.nix (so headless hosts can read/set it without
# importing this module). This module only provides its implementation.
config = lib.mkIf cfg.enable {
programs.sway = {
+2 -2
View File
@@ -29,10 +29,10 @@
// lib.optionalAttrs (spec ? linger) { inherit (spec) linger; }
) hostUsers;
programs.firefox = lib.mkIf (config.features.swayDesktop.enable == true) {
programs.firefox = lib.mkIf config.features.swayDesktop.enable {
enable = true;
};
programs.thunderbird = lib.mkIf (config.features.swayDesktop.enable == true) {
programs.thunderbird = lib.mkIf config.features.swayDesktop.enable {
enable = true;
};
}
+16
View File
@@ -47,7 +47,23 @@
pkgs.terraform-docs # generate Terraform module docs
pkgs.yq-go # jq for YAML
pkgs.gcx # Grafana Cloud CLI (dashboards, SLOs, synthetics, alerts)
# WSL ships no xdg-open, so anything that shells out to a browser dies with
# `exec: "xdg-open,x-www-browser,www-browser": executable file not found`.
# kubelogin's interactive login is the one that bites: it is the login mode
# the shared cluster kubeconfig uses. Hand the URL to Windows instead.
# (wslu, the usual answer, is gone from nixpkgs -- upstream archived it.)
(pkgs.writeShellScriptBin "xdg-open" ''
url="$1"
if command -v powershell.exe >/dev/null 2>&1; then
exec powershell.exe -NoProfile -Command "Start-Process '$url'"
fi
exec explorer.exe "$url"
'')
];
# Honoured by tools that read $BROWSER rather than calling xdg-open.
home.sessionVariables.BROWSER = "xdg-open";
services.ssh-agent.enable = true;
# Colourised kubectl. enableAlias points `kubectl` at kubecolor, which parses