Commits
Persistent autossh tunnel via tunnel.c-base.org exposing
ai.cbrp3.c-base.org:11434 on neoprism, reachable from retiolum at
neoprism.r:11434. ssh key generated by clan vars; pubkey must be
installed on tunnel.c-base.org under lassulus before the tunnel comes up.
PR Mic92/tincr#5 (merged 2026-05-11) removes the auto-purge in
on_del_edge that drove reachability oscillation in mixed
tincr/tinc-1.1pre18 meshes.
On top, pin Lassulus/tincr@a25eec05 which adds udp_addr clearing on
ENETUNREACH/EHOSTUNREACH/EAFNOSUPPORT/EADDRNOTAVAIL — without it,
IPv4-only hosts retry the same broken cached IPv6 udp_addr forever
when a peer's wire-advertised contact is v6 only. Submitted upstream
as Mic92/tincr#6.
Under load, the swaymsg get_tree subprocess could time out, the host
returned an empty windows list, and the extension treated that as "no
Firefox windows anywhere" — restoring a window for a workspace that
already had one. Distinguish query failure from a genuinely empty
result and skip restoration when state is unknown.
Adds `s say "..."` which speaks via flite TTS and posts a desktop
notification. The notify skill documents when to use it; the wrapper
registers the skill and allows `s` in Bash so calls don't prompt.
Upstream creates /var/lib/mautrix-discord at mode 0770 owned by
mautrix-discord:mautrix-discord, but only adds matrix-synapse to the
mautrix-discord-registration supplementary group. That left synapse
unable to traverse the directory to read discord-registration.yaml,
crashing matrix-synapse on startup.
dhtclient only announces to the DHT. lassie (and IPNI/cid.contact)
never learned about neoprism's pins, so all fetchIpfs calls fell back
to archive.org. autoclient keeps DHT client mode but also pushes
provider records to IPNI via HTTP, making content discoverable by
lassie without a fallbackUrl.
ns1.first-ns.de, robotns2.second-ns.de, robotns3.second-ns.com
tincr is wire-compatible with tinc 1.1pre18 (ExperimentalProtocol=yes,
Ed25519 only). Wrap the package with a /sbin -> bin symlink so
krebs.tinc's ${tincPackage}/sbin/tincd path resolves, and override
tincPackage. Existing peers all have Ed25519 keys; RSA keys in host
files are warned about and ignored.
The script was previously SSH_ASKPASS-only (raw PIN on stdout), which
broke rbw and gpg-agent: they speak the Assuan protocol over stdin/stdout
and could not parse the output ('failed to read password from pinentry').
Dispatch by argv:
- no args, or first arg starts with '-' (real pinentry flags like
--timeout 0 --ttyname ... --display ... --no-global-grab) -> Assuan mode
- bare prompt arg (SSH_ASKPASS convention used by ssh-tpm-agent)
-> raw PIN on stdout (existing behaviour)
Assuan mode handles SETTITLE/SETPROMPT/SETDESC/SETERROR/SETKEYINFO/OPTION/
RESET/NOP/BYE, percent-decodes incoming descriptions for the rofi -mesg,
and percent-encodes the D-line PIN.
Kernel-keyring caching is scoped to SSH_ASKPASS mode only; rbw and
gpg-agent do their own caching, and sharing one keyring slot across
unrelated secrets handed back the wrong PIN to whichever caller asked
second.
tools.kagi-search: default to 'rbw get kagi.com -f session_link'
Matches the actual vault layout (a kagi.com login with a session_link
field) instead of expecting a separate 'kagi-session-link' entry.
Bump the Workspace widget's characterCount to 20 so workspace names
fit (was capped at 2). Vertical bars are still capped to 2 chars by
noctalia regardless of this value.
When gpg-agent is reachable and the user has a secret encryption
subkey ([E] ssb), emit the age-plugin-gpg default identity
(AGE-PLUGIN-GPG-1H40XPM, the bech32 of an empty body for the gpg
plugin). gpg-agent caches the passphrase / smartcard PIN, so this is
more frictionless than the yubikey path; we therefore place it above
yubikey/solo2 but below se/tpm.
Set programs.gnupg.agent.enableSSHSupport = true so the
gpg-agent-ssh.socket unit is created and gpg-agent (in --supervised
mode) actually receives the ssh listen fd. The module also exports
SSH_AUTH_SOCK system-wide via environment.extraInit; we don't want
that, so unset it again with mkAfter. Users opt in per command via:
SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" ssh ...
nixpkgs's autotools elinks 0.19.1 fails against gettext 0.23.1
(autopoint refuses bare AM_GNU_GETTEXT, then autoheader fails on
HAVE_ALLOCA). Carry the meson-based build from upstream nixpkgs
PR #515347 locally until it lands; tools.mutt now consumes
self.packages.<system>.elinks instead of pkgs.elinks.
Inlines the libcss/libdom propagatedBuildInputs adjustments from the
same PR, scoped to this elinks build only — no global overlay.
age-plugin-gpg lets a gpg key (via gpg-agent + keygrip) act as an age
recipient/identity, so adding the corresponding age1gpg1... line to
.age-recipients makes every passage secret encryptable to gpg as well.
Caveat: gpg keygrips end up as plaintext in the encrypted file.
Newer nixpkgs renders 'settings.enable-ssh-support = true' as
'enable-ssh-support true', which gpg-agent rejects with 'argument not
expected' since it's a boolean flag. Use "" instead so the keyValue
formatter emits a bare 'enable-ssh-support ' line gpg-agent accepts.
Existing preHook only dumped matrix-synapse; the per-bridge postgres
DBs (mautrix-{whatsapp,signal,telegram,discord}) and heisenbridge's
sqlite state weren't covered. Losing them = relogin everywhere and
orphaned portal rooms.
Used via nixpkgs.overlays so per-module insecure-package permits compose
cleanly — nixpkgs.config.permittedInsecurePackages merges via
recursiveUpdate, so list values from multiple modules clobber each other.
Switch jitsi over as the first consumer.
Move realwallpaper service and lassul.us website from prism to neoprism.
DNS for lassul.us already points at neoprism.
bgt-bot is commented out on neoprism for now; can be enabled later if it
goes down on prism (which currently isn't deploying anyway).
Drop the now-redundant lassul.us -> orange.r proxy in orange-host.nix and
the lassul.us acme fallback on prism.
Wraps noctalia-shell so declarative bits are merged into the user's
~/.config/noctalia/settings.json on every start. The file stays
writable; only fields covered by 'settings' (deep-merge) and
'settingsPatches' (jq filters) are pinned, so user GUI tweaks to
other keys survive.
Also moves the sway workspace patch (named/global workspaces,
click-to-current-output) into the wrapper behind 'swayWorkspaceFix',
and ships a default config that pins the dark-mode hook and shows
workspace names in the bar.
Hetzner rotated robotns2.second-ns.de's addresses on 2023-12-04;
the acl block was updated then but the remote/NOTIFY targets were
missed, so pushes to that slave were going to dead IPs.
The built-in `auto` permission mode uses a classifier to gate per-action
approvals — covers the routine-action auto-approve role GouvernAI was
filling. Drops --dangerously-skip-permissions (no longer needed since
the mode handles approvals) and the GouvernAI plugin + fetchFromGitHub.
Sets permissions.defaultMode = "auto" in the wrapper's --settings so
every session boots into auto mode without a CLI flag.
Adds projectStateWipeKeys (default [ "mcpServers" ]). preHook rewrites
~/.claude.json via jq on every invocation, deleting the listed keys
from each projects.<dir> entry. Purges stale `claude mcp add`
registrations that pin GC-able /nix/store paths and shadow the
canonical set the wrapper writes via --settings. Empty list disables
the step.
GouvernAI's hook returns allow/deny/ask_user decisions, but Claude Code's
native permission prompts still fire on top, so safe T1 actions get a
duplicate prompt. With native prompts off, the guardrails hook becomes the
sole gate — silent on T1, notify on T2, ask on T3/T4, hard-block on
NEVER rules.
- new skills/tmux documents the claude- session prefix, run-and-wait
marker pattern, output capture, and parallel sessions
- drops the tmux-mcp MCP server and the 5pkgs/tmux-mcp wrapper package
- adds python3 + tmux to the wrapper's extraPackages so plugin hooks
resolve a real python3 inside Nix dev shells where DEVELOPER_DIR
points at a Nix Apple SDK (otherwise /usr/bin/python3 -> xcrun ->
"tool 'python3' not found")
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.
The previous translation of the dovecot 2.3 plugin/namespace blocks didn't
survive doveconf parsing or the runtime check for virtual namespaces:
- `acl = vfile` is a named-list-filter in 2.4. The driver moved to
top-level `acl_driver`, and per-identifier defaults live in
`acl <identifier> { acl_rights = ...; }` filters keyed on the ACL id
(owner/anyone/user=...). The filter name itself is parsed by
acl_identifier_parse, which is why earlier attempts with `acl vfile`
or `acl primary` failed with "Invalid identifier".
- `acl_shared_dict` was removed; the bot subscribes to shared paths
explicitly so we don't need it.
- Per-namespace `location = "driver:path:opts"` was split into separate
mail_driver / mail_path / mail_index_path settings.
- The virtual namespace also needs `mailbox_list_layout = fs` to override
the global Maildir++ - otherwise dovecot 2.4 refuses to initialise the
namespace, which broke both LMTP delivery and IMAP login.
Verified on neoprism with `doveconf -n` and a live LMTP delivery.
Dovecot 2.4 (now in nixpkgs) removed services.dovecot2.extraConfig and
mailPlugins. Translate the plugin/namespace blocks into the structured
settings tree, hoist ACL plugin keys to top-level (no more `plugin {}`
section), and update the variable-substitution syntax (%n -> %{user |
username}, %u -> %{user}).
PR Mic92/tincr#5 (merged 2026-05-11) removes the auto-purge in
on_del_edge that drove reachability oscillation in mixed
tincr/tinc-1.1pre18 meshes.
On top, pin Lassulus/tincr@a25eec05 which adds udp_addr clearing on
ENETUNREACH/EHOSTUNREACH/EAFNOSUPPORT/EADDRNOTAVAIL — without it,
IPv4-only hosts retry the same broken cached IPv6 udp_addr forever
when a peer's wire-advertised contact is v6 only. Submitted upstream
as Mic92/tincr#6.
Under load, the swaymsg get_tree subprocess could time out, the host
returned an empty windows list, and the extension treated that as "no
Firefox windows anywhere" — restoring a window for a workspace that
already had one. Distinguish query failure from a genuinely empty
result and skip restoration when state is unknown.
Upstream creates /var/lib/mautrix-discord at mode 0770 owned by
mautrix-discord:mautrix-discord, but only adds matrix-synapse to the
mautrix-discord-registration supplementary group. That left synapse
unable to traverse the directory to read discord-registration.yaml,
crashing matrix-synapse on startup.
tincr is wire-compatible with tinc 1.1pre18 (ExperimentalProtocol=yes,
Ed25519 only). Wrap the package with a /sbin -> bin symlink so
krebs.tinc's ${tincPackage}/sbin/tincd path resolves, and override
tincPackage. Existing peers all have Ed25519 keys; RSA keys in host
files are warned about and ignored.
The script was previously SSH_ASKPASS-only (raw PIN on stdout), which
broke rbw and gpg-agent: they speak the Assuan protocol over stdin/stdout
and could not parse the output ('failed to read password from pinentry').
Dispatch by argv:
- no args, or first arg starts with '-' (real pinentry flags like
--timeout 0 --ttyname ... --display ... --no-global-grab) -> Assuan mode
- bare prompt arg (SSH_ASKPASS convention used by ssh-tpm-agent)
-> raw PIN on stdout (existing behaviour)
Assuan mode handles SETTITLE/SETPROMPT/SETDESC/SETERROR/SETKEYINFO/OPTION/
RESET/NOP/BYE, percent-decodes incoming descriptions for the rofi -mesg,
and percent-encodes the D-line PIN.
Kernel-keyring caching is scoped to SSH_ASKPASS mode only; rbw and
gpg-agent do their own caching, and sharing one keyring slot across
unrelated secrets handed back the wrong PIN to whichever caller asked
second.
tools.kagi-search: default to 'rbw get kagi.com -f session_link'
Matches the actual vault layout (a kagi.com login with a session_link
field) instead of expecting a separate 'kagi-session-link' entry.
When gpg-agent is reachable and the user has a secret encryption
subkey ([E] ssb), emit the age-plugin-gpg default identity
(AGE-PLUGIN-GPG-1H40XPM, the bech32 of an empty body for the gpg
plugin). gpg-agent caches the passphrase / smartcard PIN, so this is
more frictionless than the yubikey path; we therefore place it above
yubikey/solo2 but below se/tpm.
Set programs.gnupg.agent.enableSSHSupport = true so the
gpg-agent-ssh.socket unit is created and gpg-agent (in --supervised
mode) actually receives the ssh listen fd. The module also exports
SSH_AUTH_SOCK system-wide via environment.extraInit; we don't want
that, so unset it again with mkAfter. Users opt in per command via:
SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" ssh ...
nixpkgs's autotools elinks 0.19.1 fails against gettext 0.23.1
(autopoint refuses bare AM_GNU_GETTEXT, then autoheader fails on
HAVE_ALLOCA). Carry the meson-based build from upstream nixpkgs
PR #515347 locally until it lands; tools.mutt now consumes
self.packages.<system>.elinks instead of pkgs.elinks.
Inlines the libcss/libdom propagatedBuildInputs adjustments from the
same PR, scoped to this elinks build only — no global overlay.
Move realwallpaper service and lassul.us website from prism to neoprism.
DNS for lassul.us already points at neoprism.
bgt-bot is commented out on neoprism for now; can be enabled later if it
goes down on prism (which currently isn't deploying anyway).
Drop the now-redundant lassul.us -> orange.r proxy in orange-host.nix and
the lassul.us acme fallback on prism.
Wraps noctalia-shell so declarative bits are merged into the user's
~/.config/noctalia/settings.json on every start. The file stays
writable; only fields covered by 'settings' (deep-merge) and
'settingsPatches' (jq filters) are pinned, so user GUI tweaks to
other keys survive.
Also moves the sway workspace patch (named/global workspaces,
click-to-current-output) into the wrapper behind 'swayWorkspaceFix',
and ships a default config that pins the dark-mode hook and shows
workspace names in the bar.
The built-in `auto` permission mode uses a classifier to gate per-action
approvals — covers the routine-action auto-approve role GouvernAI was
filling. Drops --dangerously-skip-permissions (no longer needed since
the mode handles approvals) and the GouvernAI plugin + fetchFromGitHub.
Sets permissions.defaultMode = "auto" in the wrapper's --settings so
every session boots into auto mode without a CLI flag.
Adds projectStateWipeKeys (default [ "mcpServers" ]). preHook rewrites
~/.claude.json via jq on every invocation, deleting the listed keys
from each projects.<dir> entry. Purges stale `claude mcp add`
registrations that pin GC-able /nix/store paths and shadow the
canonical set the wrapper writes via --settings. Empty list disables
the step.
- new skills/tmux documents the claude- session prefix, run-and-wait
marker pattern, output capture, and parallel sessions
- drops the tmux-mcp MCP server and the 5pkgs/tmux-mcp wrapper package
- adds python3 + tmux to the wrapper's extraPackages so plugin hooks
resolve a real python3 inside Nix dev shells where DEVELOPER_DIR
points at a Nix Apple SDK (otherwise /usr/bin/python3 -> xcrun ->
"tool 'python3' not found")
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.
The previous translation of the dovecot 2.3 plugin/namespace blocks didn't
survive doveconf parsing or the runtime check for virtual namespaces:
- `acl = vfile` is a named-list-filter in 2.4. The driver moved to
top-level `acl_driver`, and per-identifier defaults live in
`acl <identifier> { acl_rights = ...; }` filters keyed on the ACL id
(owner/anyone/user=...). The filter name itself is parsed by
acl_identifier_parse, which is why earlier attempts with `acl vfile`
or `acl primary` failed with "Invalid identifier".
- `acl_shared_dict` was removed; the bot subscribes to shared paths
explicitly so we don't need it.
- Per-namespace `location = "driver:path:opts"` was split into separate
mail_driver / mail_path / mail_index_path settings.
- The virtual namespace also needs `mailbox_list_layout = fs` to override
the global Maildir++ - otherwise dovecot 2.4 refuses to initialise the
namespace, which broke both LMTP delivery and IMAP login.
Verified on neoprism with `doveconf -n` and a live LMTP delivery.
Dovecot 2.4 (now in nixpkgs) removed services.dovecot2.extraConfig and
mailPlugins. Translate the plugin/namespace blocks into the structured
settings tree, hoist ACL plugin keys to top-level (no more `plugin {}`
section), and update the variable-substitution syntax (%n -> %{user |
username}, %u -> %{user}).