Commits
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}).
Also drop the explicit master ref on nixos-mailserver since the project
no longer uses that branch name as default tracking.
claude is already provided via tools.s (and the default tools include
s now), so drop the explicit claude entry and add s for direct access.
Use the new wrapper plugins option to install the GouvernAI plugin from
GitHub, providing the /guardrails command and gouvernai flow-state skill.
Allow declaratively adding extra Claude Code plugin directories alongside
the bundled one. Each entry becomes a repeatable --plugin-dir flag.
Add MegolmEvent callback that warns the user when a message can't be
decrypted and requests the missing room key. Also process invited_rooms
from the initial sync so invites that arrived while the bot was offline
are accepted instead of being silently ignored.
- Package from llm.claude-code (llm-agents.nix)
- Skills: websearch (kagi-search), nix-workflow, hedgedoc,
reverse-engineering
- Comprehensive permission allow-list: nix/clan, git, common
CLI tools, file operations, dev tools
- Extra packages on PATH: kagi-search, curl, jq, ripgrep, fd, tree
- wrapperModules/claude-code: declarative options for skills, commands,
agents, mcpServers, hooks, permissions, settings, and claudeMd
- Assembles a plugin directory (--plugin-dir) and settings.json
(--settings) injected into every claude-code invocation
- claudeMd lines are fuzzy-merged into ~/.claude/CLAUDE.md using
Levenshtein distance (only adds, never removes user content)
- 5pkgs/levenshtein: small C tool for edit distance, threshold check,
and line-level file merge
- tools/claude: rewritten to consume the new wrapper module
Use pinentry-rofi on linux and pinentry_mac on darwin directly,
removing the now-unused pinentry-auto wrapper.
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}).
- wrapperModules/claude-code: declarative options for skills, commands,
agents, mcpServers, hooks, permissions, settings, and claudeMd
- Assembles a plugin directory (--plugin-dir) and settings.json
(--settings) injected into every claude-code invocation
- claudeMd lines are fuzzy-merged into ~/.claude/CLAUDE.md using
Levenshtein distance (only adds, never removes user content)
- 5pkgs/levenshtein: small C tool for edit distance, threshold check,
and line-level file merge
- tools/claude: rewritten to consume the new wrapper module