From 5437271510219d402d5033e7e2b7f1a7e9891f7a Mon Sep 17 00:00:00 2001 From: haras-unicorn Date: Thu, 20 Aug 2026 12:24:54 +0200 Subject: [PATCH] feat: multi agent --- src/cspell.yaml | 8 + src/deprecated/mistral-rs.nix | 67 ++- src/lib/ai/bubblewrap.nix | 58 +++ src/lib/ai/openrouter.nix | 17 + src/lib/ai/shell.nix | 42 ++ src/lib/ai/web.nix | 28 + src/lib/ai/zeroclaw.nix | 495 ++++++++++++++++++ src/modules/ai/llama-cpp.nix | 153 +++--- src/modules/ai/opencode/default.nix | 7 +- src/modules/ai/piper.nix | 33 +- src/modules/ai/stable-diffusion-cpp.nix | 99 ++-- src/modules/ai/whisper-cpp.nix | 59 ++- src/modules/ai/zeroclaw/default.nix | 619 ++++++++--------------- src/modules/desktop/xdg.nix | 11 +- src/modules/meta/ai.nix | 65 ++- src/modules/meta/search.nix | 5 + src/modules/services/searxng/searxng.nix | 5 +- 17 files changed, 1202 insertions(+), 569 deletions(-) create mode 100644 src/lib/ai/bubblewrap.nix create mode 100644 src/lib/ai/openrouter.nix create mode 100644 src/lib/ai/shell.nix create mode 100644 src/lib/ai/web.nix create mode 100644 src/lib/ai/zeroclaw.nix diff --git a/src/cspell.yaml b/src/cspell.yaml index 32bb7ff9..290b4b47 100644 --- a/src/cspell.yaml +++ b/src/cspell.yaml @@ -544,3 +544,11 @@ ignoreWords: - gitea - ahmia - hackernews + - relace + - deepinfra + - coreweave + - novita + - npmjs + - pypi + - wrapup + - multimodal diff --git a/src/deprecated/mistral-rs.nix b/src/deprecated/mistral-rs.nix index 3cbd2312..ccf0c668 100644 --- a/src/deprecated/mistral-rs.nix +++ b/src/deprecated/mistral-rs.nix @@ -1,33 +1,58 @@ +let + makeModels = pkgs: { + dia = pkgs.fetchurl { + url = "https://huggingface.co/nari-labs/Dia-1.6B/resolve/main/model.safetensors"; + hash = "sha256-yroom2D219Hlj8dE9NwlquiJlfzKRr49BeIguXFIaiY="; + }; + + dieConfig = pkgs.fetchurl { + url = "https://huggingface.co/nari-labs/Dia-1.6B/resolve/main/config.json"; + hash = "sha256-kUDoX9FbgtfyaMVoGkH7HeaFc+wga4fLOlhi/lxZjW4="; + }; + + diaDac = pkgs.fetchurl { + name = "dac.safetensors"; + url = "https://huggingface.co/EricB/dac_44khz/resolve/main/model.safetensors"; + hash = "sha256-bkAWHz1cXaXC26ZL5o/C9pgyvHq0+nNsuYdkFpBfcYA="; + }; + }; +in { - self.lib.deprecated.homeModules.mistral-rs = + self.lib.deprecated.nixosModules.mistral-rs = { pkgs, - config, lib, + config, ... }: let cuda = config.nixpkgs.config.cudaSupport; - package = pkgs.mistral-rs; - - model = pkgs.fetchurl { - url = "https://huggingface.co/nari-labs/Dia-1.6B/resolve/main/model.safetensors"; - hash = "sha256-yroom2D219Hlj8dE9NwlquiJlfzKRr49BeIguXFIaiY="; + models = makeModels pkgs; + in + lib.mkIf cuda { + dot.ai.models.dia = { + files = [ + models.dia + models.dieConfig + models.diaDac + ]; }; + }; - modelConfig = pkgs.fetchurl { - url = "https://huggingface.co/nari-labs/Dia-1.6B/resolve/main/config.json"; - hash = "sha256-kUDoX9FbgtfyaMVoGkH7HeaFc+wga4fLOlhi/lxZjW4="; - }; + self.lib.deprecated.homeModules.mistral-rs = + { + pkgs, + config, + lib, + ... + }: + let + cuda = config.nixpkgs.config.cudaSupport; - dac = pkgs.fetchurl { - name = "dac.safetensors"; - url = "https://huggingface.co/EricB/dac_44khz/resolve/main/model.safetensors"; - hash = "sha256-bkAWHz1cXaXC26ZL5o/C9pgyvHq0+nNsuYdkFpBfcYA="; - }; + package = pkgs.mistral-rs; - node-generate = pkgs.writeShellApplication { + node-describe = pkgs.writeShellApplication { name = "mistral-rs-node-describe"; runtimeInputs = [ package @@ -46,12 +71,6 @@ }; in lib.mkIf cuda { - dot.ai.models.dia.files = [ - model - modelConfig - dac - ]; - dot.processing.nodes.mistral-rs-clone-speech = { note = "Clone speech"; tags = [ @@ -63,7 +82,7 @@ ]; inputs = [ "text/plain" ]; output = "audio/wav"; - package = node-generate; + package = node-describe; }; home.packages = [ diff --git a/src/lib/ai/bubblewrap.nix b/src/lib/ai/bubblewrap.nix new file mode 100644 index 00000000..60710a6b --- /dev/null +++ b/src/lib/ai/bubblewrap.nix @@ -0,0 +1,58 @@ +{ + self.lib.ai.bubblewrap = { + flags = { + nvidia = [ + "--ro-bind-try" + "/run/opengl-driver" + "/run/opengl-driver" + "--dev-bind-try" + "/dev/dri" + "/dev/dri" + "--dev-bind-try" + "/dev/nvidia0" + "/dev/nvidia0" + "--dev-bind-try" + "/dev/nvidiactl" + "/dev/nvidiactl" + "--dev-bind-try" + "/dev/nvidia-modeset" + "/dev/nvidia-modeset" + "--dev-bind-try" + "/dev/nvidia-uvm" + "/dev/nvidia-uvm" + "--dev-bind-try" + "/dev/nvidia-uvm-tools" + "/dev/nvidia-uvm-tools" + ]; + + base = [ + "--die-with-parent" + "--unshare-all" + "--ro-bind-try" + "/nix/store" + "/nix/store" + "--ro-bind" + "/usr" + "/usr" + "--ro-bind" + "/bin" + "/bin" + "--ro-bind-try" + "/sbin" + "/sbin" + "--ro-bind-try" + "/lib" + "/lib" + "--ro-bind-try" + "/lib64" + "/lib64" + "--tmpfs" + "/tmp" + "--proc" + "/proc" + "--dev" + "/dev" + ]; + }; + }; +} diff --git a/src/lib/ai/openrouter.nix b/src/lib/ai/openrouter.nix new file mode 100644 index 00000000..b526db24 --- /dev/null +++ b/src/lib/ai/openrouter.nix @@ -0,0 +1,17 @@ +{ + self.lib.ai.openrouter = { + model = "deepseek/deepseek-v4-flash-0731"; + context = 1000 * 1000; + options = { + provider = { + only = [ + "relace/fp4" + "deepinfra/fp8" + "coreweave/fp8" + "novita/fp8" + ]; + allow_fallbacks = false; + }; + }; + }; +} diff --git a/src/lib/ai/shell.nix b/src/lib/ai/shell.nix new file mode 100644 index 00000000..b1807193 --- /dev/null +++ b/src/lib/ai/shell.nix @@ -0,0 +1,42 @@ +{ + self.lib.ai.shell = { + allowedCommands = [ + "ls" + "mkdir" + "cp" + "mv" + "touch" + "pwd" + "tree" + "stat" + "file" + "readlink" + "du" + "df" + "cat" + "head" + "tail" + "wc" + "sed" + "jq" + "grep" + "rg" + "find" + "fd" + "env" + "whoami" + "id" + "date" + "uname" + "uptime" + "ps" + "free" + "echo" + "printf" + "sha256sum" + "sleep" + "true" + "false" + ]; + }; +} diff --git a/src/lib/ai/web.nix b/src/lib/ai/web.nix new file mode 100644 index 00000000..1a199d2d --- /dev/null +++ b/src/lib/ai/web.nix @@ -0,0 +1,28 @@ +{ + self.lib.ai.web = { + allowedDomains = [ + "docs.zeroclawlabs.ai" + "opencode.ai" + + "en.wikipedia.org" + + "wiki.archlinux.org" + "developer.mozilla.org" + + "huggingface.co" + "github.com" + "githubusercontent.com" + "flakehub.com" + "codeberg.org" + # NOTE: sourcehut + "sr.ht" + + "nixos.org" + + "docs.rs" + "crates.io" + "npmjs.com" + "pypi.org" + ]; + }; +} diff --git a/src/lib/ai/zeroclaw.nix b/src/lib/ai/zeroclaw.nix new file mode 100644 index 00000000..9c58b395 --- /dev/null +++ b/src/lib/ai/zeroclaw.nix @@ -0,0 +1,495 @@ +# NOTE: shell is ok because sandboxing and zeroclaw also parses it to check for +# allowed commands and disallows certain metacharacters + +{ + self.lib.ai.zeroclaw = { + config = { + schema_version = 3; + + knowledge.enabled = true; + + verifiable_intent.enabled = true; + + link_enricher.enabled = true; + + gateway = { + require_pairing = false; + host = "127.0.0.1"; + port = 42617; + }; + + reliability = { + provider_backoff_ms = 1000; + provider_retries = 30; + }; + }; + + riskProfile = { + level = "supervised"; + workspace_only = true; + + sandbox_backend = "bubblewrap"; + sandbox_enabled = true; + + # NOTE: touch is considered medium risk, for instance, + # and we already have a sandbox + # also there is a weird bug where the agent can't even ask + # for approval because shell is in auto_approve right now + require_approval_for_medium_risk = false; + + tool_receipts = { + enabled = true; + show_in_response = true; + }; + }; + + runtimeProfile = { + max_tool_iterations = 100; + max_actions_per_hour = 1000; + max_history_messages = 64; + }; + + excludedTools = [ + "browser" + "browser_open" + "http_request" + "proxy_config" + + "schedule" + + "screenshot" + "pushover" + "canvas" + + "model_switch" + "model_routing_config" + + "memory_export" + "memory_purge" + + "git_forge" + "git_operations" + + "cron_add" + "cron_remove" + "cron_update" + + "TodoWrite" + "llm_task" + "project_intel" + "report_template" + "backup" + ]; + + chatTools = [ + "file_read" + "file_write" + "file_edit" + + "glob_search" + "content_search" + + "cron_list" + "cron_runs" + + "knowledge" + "memory_store" + "memory_recall" + "memory_forget" + + "spawn_subagent" + + "pushover" + "poll" + "reaction" + "ask_user" + "send_via" + "escalate_to_human" + "channel_room" + "vi_verify" + + "calculator" + "weather" + "image_info" + + "web_fetch" + "web_search_tool" + + "sessions_current" + "sessions_list" + "sessions_history" + "sessions_send" + + "mcp_resources" + "mcp_prompts" + ]; + + delegateTools = [ + "file_read" + "file_write" + "file_edit" + + "glob_search" + "content_search" + + "cron_list" + "cron_run" + "cron_runs" + + "knowledge" + "memory_store" + "memory_recall" + "memory_forget" + + "spawn_subagent" + "send_message_to_peer" + "delegate" + + "pushover" + "poll" + "reaction" + "ask_user" + "send_via" + "escalate_to_human" + "channel_room" + "vi_verify" + + "calculator" + "weather" + "image_info" + + "web_fetch" + "web_search_tool" + + "sessions_current" + "sessions_list" + "sessions_history" + "sessions_send" + + "mcp_resources" + "mcp_prompts" + ]; + + digestTools = [ + "file_read" + "file_write" + "file_edit" + + "glob_search" + "content_search" + + "knowledge" + "memory_store" + "memory_recall" + + "spawn_subagent" + + "calculator" + "weather" + "image_info" + + "web_fetch" + "web_search_tool" + + "mcp_resources" + "mcp_prompts" + + "rss__get_articles" + "rss__fetch_article" + ]; + + devTools = [ + "shell" + + "file_read" + "file_write" + "file_edit" + + "glob_search" + "content_search" + + "knowledge" + "memory_store" + "memory_recall" + "memory_forget" + + "spawn_subagent" + "send_message_to_peer" + + "vi_verify" + + "calculator" + "weather" + "image_info" + + "web_fetch" + "web_search_tool" + + "mcp_resources" + "mcp_prompts" + + "rss__get_articles" + "rss__fetch_article" + + "nixos__nix" + "nixos__nix_versions" + + "nix__nix_build" + "nix__nix_run" + "nix__nix_develop" + "nix__nix_check" + + "git__git_init" + "git__git_clone" + "git__git_status" + "git__git_clean" + "git__git_add" + "git__git_commit" + "git__git_diff" + "git__git_log" + "git__git_show" + "git__git_blame" + "git__git_reflog" + "git__git_changelog_analyze" + "git__git_branch" + "git__git_checkout" + "git__git_merge" + "git__git_rebase" + "git__git_cherry_pick" + "git__git_remote" + "git__git_fetch" + "git__git_pull" + "git__git_push" + "git__git_tag" + "git__git_stash" + "git__git_reset" + "git__git_worktree" + "git__git_set_working_dir" + "git__git_clear_working_dir" + "git__git_submodule" + + "github__actions_get" + "github__actions_list" + "github__get_job_logs" + "github__get_me" + "github__get_team_members" + "github__get_teams" + "github__discussion_comment_write" + "github__get_discussion" + "github__get_discussion_comments" + "github__list_discussion_categories" + "github__list_discussions" + "github__add_issue_comment" + "github__issue_read" + "github__issue_write" + "github__list_issue_fields" + "github__list_issue_types" + "github__search_issues" + "github__sub_issue_write" + "github__get_label" + "github__label_write" + "github__list_label" + "github__dismiss_notification" + "github__get_notification_details" + "github__list_notifications" + "github__manage_notification_subscription" + "github__manage_repository_notification_subscription" + "github__mark_all_notifications_read" + "github__projects_get" + "github__projects_list" + "github__projects_write" + "github__add_comment_to_pending_review" + "github__add_reply_to_pull_request_comment" + "github__create_pull_request" + "github__list_pull_requests" + "github__merge_pull_request" + "github__pull_request_read" + "github__pull_request_review_write" + "github__search_pull_requests" + "github__update_pull_request" + "github__update_pull_request_branch" + "github__create_branch" + "github__create_repository" + "github__fork_repository" + "github__get_commit" + "github__get_file_contents" + "github__get_latest_release" + "github__get_release_by_tag" + "github__get_tag" + "github__list_branches" + "github__list_commits" + "github__list_releases" + "github__list_repository_collaborators" + "github__list_tags" + "github__search_code" + "github__search_commits" + "github__search_repositories" + "github__list_starred_repositories" + "github__star_repository" + "github__unstar_repository" + "github__search_users" + ]; + + allTools = [ + "shell" + + "file_read" + "file_write" + "file_edit" + + "glob_search" + "content_search" + + "cron_list" + "cron_run" + "cron_runs" + "cron_add" + "cron_remove" + "cron_update" + + "knowledge" + "memory_store" + "memory_recall" + "memory_forget" + + "spawn_subagent" + "send_message_to_peer" + "delegate" + + "pushover" + "TodoWrite" + "report_template" + "project_intel" + "poll" + "reaction" + "ask_user" + "send_via" + "escalate_to_human" + "channel_room" + "vi_verify" + + "calculator" + "weather" + "canvas" + "image_info" + + "web_fetch" + "web_search_tool" + + "backup" + + "sessions_current" + "sessions_list" + "sessions_history" + "sessions_send" + + "git_forge" + "git_operations" + + "mcp_resources" + "mcp_prompts" + + "rss__get_articles" + "rss__fetch_article" + + "nixos__nix_versions" + "nixos__nix" + + "nix__nix_run" + "nix__nix_check" + "nix__nix_build" + "nix__nix_develop" + + "git__git_stash" + "git__git_reflog" + "git__git_pull" + "git__git_push" + "git__git_worktree" + "git__git_add" + "git__git_changelog_analyze" + "git__git_rebase" + "git__git_remote" + "git__git_clear_working_dir" + "git__git_reset" + "git__git_cherry_pick" + "git__git_init" + "git__git_branch" + "git__git_blame" + "git__git_checkout" + "git__git_merge" + "git__git_show" + "git__git_wrapup_instructions" + "git__git_tag" + "git__git_diff" + "git__git_log" + "git__git_clean" + "git__git_fetch" + "git__git_commit" + "git__git_set_working_dir" + "git__git_status" + "git__git_clone" + + "github__get_label" + "github__create_repository" + "github__discussion_comment_write" + "github__create_or_update_file" + "github__get_tag" + "github__list_branches" + "github__list_issues" + "github__get_discussion" + "github__search_code" + "github__search_commits" + "github__create_pull_request" + "github__pull_request_read" + "github__update_pull_request_branch" + "github__get_me" + "github__label_write" + "github__search_repositories" + "github__projects_write" + "github__actions_get" + "github__actions_run_trigger" + "github__delete_file" + "github__add_reply_to_pull_request_comment" + "github__list_starred_repositories" + "github__add_issue_comment" + "github__list_tags" + "github__projects_get" + "github__pull_request_review_write" + "github__get_notification_details" + "github__get_release_by_tag" + "github__dismiss_notification" + "github__actions_list" + "github__mark_all_notifications_read" + "github__push_files" + "github__update_pull_request" + "github__list_repository_collaborators" + "github__list_discussions" + "github__search_users" + "github__sub_issue_write" + "github__merge_pull_request" + "github__get_commit" + "github__get_job_logs" + "github__manage_repository_notification_subscription" + "github__unstar_repository" + "github__get_latest_release" + "github__issue_write" + "github__list_label" + "github__add_comment_to_pending_review" + "github__list_pull_requests" + "github__get_discussion_comments" + "github__create_branch" + "github__list_discussion_categories" + "github__projects_list" + "github__fork_repository" + "github__list_notifications" + "github__issue_read" + "github__star_repository" + "github__list_releases" + "github__get_file_contents" + "github__search_pull_requests" + "github__search_issues" + "github__list_commits" + "github__manage_notification_subscription" + ]; + }; +} diff --git a/src/modules/ai/llama-cpp.nix b/src/modules/ai/llama-cpp.nix index 2deecbae..59ed02fe 100644 --- a/src/modules/ai/llama-cpp.nix +++ b/src/modules/ai/llama-cpp.nix @@ -2,6 +2,7 @@ # TODO: try UD models - they might still work with FATE # TODO: try speculative decoding - this is a perfect scenario for it +# TODO: pick models based on vram somehow?? # NOTE: ubatch ~= in tps, fate cache ~= out tps, ctx size ~= smart let @@ -25,46 +26,62 @@ let }; makeModels = pkgs: { - gemma-4-e2b = pkgs.fetchurl { - name = "gemma-4-e2b.gguf"; - url = "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf"; - hash = "sha256-dAGFsh0izrg6EcOqYq1YQu8yxw9gltdWu+6FoeTsNLg="; - }; + gemma-4 = { + e2b = { + model = pkgs.fetchurl { + name = "gemma-4-e2b.gguf"; + url = "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf"; + hash = "sha256-dAGFsh0izrg6EcOqYq1YQu8yxw9gltdWu+6FoeTsNLg="; - gemma-4-e2b-mmproj = pkgs.fetchurl { - name = "gemma-4-e2b-mmproj.gguf"; - url = "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/mmproj-F16.gguf"; - hash = "sha256-FAvo14SXQfiMUHV9UpuENz7o4nBSzCI2hVtTf0qCFfo="; - }; + }; + mmproj = pkgs.fetchurl { + name = "gemma-4-e2b-mmproj.gguf"; + url = "https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/mmproj-F16.gguf"; + hash = "sha256-FAvo14SXQfiMUHV9UpuENz7o4nBSzCI2hVtTf0qCFfo="; + }; + }; - gemma-4-26b-a4b = pkgs.fetchurl { - name = "gemma-4-26b-a4b.gguf"; - url = "https://huggingface.co/bartowski/google_gemma-4-26B-A4B-it-GGUF/resolve/main/google_gemma-4-26B-A4B-it-Q4_K_M.gguf"; - hash = "sha256-oH9yIh6OP3dFWrDX92UtAan2PCYrlUqmkypTJ1oOiVo="; - }; + "26b-a4b" = { + model = pkgs.fetchurl { + name = "gemma-4-26b-a4b.gguf"; + url = "https://huggingface.co/bartowski/google_gemma-4-26B-A4B-it-GGUF/resolve/main/google_gemma-4-26B-A4B-it-Q4_K_M.gguf"; + hash = "sha256-oH9yIh6OP3dFWrDX92UtAan2PCYrlUqmkypTJ1oOiVo="; + }; + }; - gemma-4-e4b = pkgs.fetchurl { - name = "gemma-4-e4b.gguf"; - url = "https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf"; - hash = "sha256-haiWoEdVPoQvJSl+5bAx1k/zAUfZxK8XseSzlM0fq4c="; + e4b = { + model = pkgs.fetchurl { + name = "gemma-4-e4b.gguf"; + url = "https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf"; + hash = "sha256-haiWoEdVPoQvJSl+5bAx1k/zAUfZxK8XseSzlM0fq4c="; + }; + }; }; - qwen-3-6-35b-a3b = pkgs.fetchurl { - name = "qwen-3-6-35b-a3b.gguf"; - url = "https://huggingface.co/bartowski/Qwen_Qwen3.6-35B-A3B-GGUF/resolve/main/Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf"; - hash = "sha256-tG/t0z4L+wyuMIqjwVjQpLLEodIYWh7W8JPNrzkGR3I="; - }; + qwen-3 = { + "35b-a3b" = { + model = pkgs.fetchurl { + name = "qwen-3-35b-a3b.gguf"; + url = "https://huggingface.co/bartowski/Qwen_Qwen3.6-35B-A3B-GGUF/resolve/main/Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf"; + hash = "sha256-tG/t0z4L+wyuMIqjwVjQpLLEodIYWh7W8JPNrzkGR3I="; + }; + }; - qwen-3-5-4b = pkgs.fetchurl { - name = "qwen-3-5-4b.gguf"; - url = "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf"; - hash = "sha256-AP55hv9fa0Y+YkVYIRRgSdtvkxNgOTinCADR+2nvEaQ="; - }; + "4b" = { + model = pkgs.fetchurl { + name = "qwen-3-4b.gguf"; + url = "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf"; + hash = "sha256-AP55hv9fa0Y+YkVYIRRgSdtvkxNgOTinCADR+2nvEaQ="; + }; + }; - qwen-3-embedding = pkgs.fetchurl { - name = "qwen-3-embedding.gguf"; - url = "https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF/resolve/main/Qwen3-Embedding-0.6B-Q8_0.gguf"; - hash = "sha256-BlB8e0JohGnE5ymLCh4W3v8GyvKRzwpbJ4wwgknD5Dk="; + embedding = { + model = pkgs.fetchurl { + name = "qwen-3-embedding.gguf"; + url = "https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF/resolve/main/Qwen3-Embedding-0.6B-Q8_0.gguf"; + hash = "sha256-BlB8e0JohGnE5ymLCh4W3v8GyvKRzwpbJ4wwgknD5Dk="; + }; + }; }; }; in @@ -91,7 +108,7 @@ in ExecStart = builtins.concatStringsSep " " [ (lib.getExe' package "llama-server") "--model" - models.qwen-3-6-35b-a3b + models.qwen-3."35b-a3b".model "--sleep-idle-seconds" "900" "--host" @@ -149,9 +166,9 @@ in ExecStart = builtins.concatStringsSep " " [ (lib.getExe' package "llama-server") "--model" - models.gemma-4-e2b + models.gemma-4.e2b.model "--mmproj" - models.gemma-4-e2b-mmproj + models.gemma-4.e2b.mmproj "--sleep-idle-seconds" "900" "--host" @@ -202,7 +219,7 @@ in ExecStart = builtins.concatStringsSep " " [ (lib.getExe' package "llama-server") "--model" - models.qwen-3-embedding + models.qwen-3.embedding.model "--embeddings" "--pooling" "last" @@ -247,6 +264,40 @@ in }; }; + dot.ai.models = builtins.listToAttrs ( + builtins.concatMap ( + { name, value }: + let + family = name; + models = value; + in + builtins.map ({ name, value }: { + name = "${family}-${name}"; + value = { + inherit name family; + files = builtins.attrValues value; + }; + }) (lib.attrsToList models) + ) (lib.attrsToList models) + ); + + dot.ai.apis = { + gpu = { + url = "http://127.0.0.1:8080/v1"; + model = "qwen-3-35b-a3b"; + context = 196608; + }; + cpu = { + url = "http://127.0.0.1:8081/v1"; + vision = true; + model = "gemma-4-e2b"; + context = 131072; + }; + embedding = { + url = "http://127.0.0.1:8082/v1"; + model = "qwen-3-embedding"; + }; + }; }; machines.homeModules.llama-cpp = @@ -292,8 +343,8 @@ in cat > "$tmpin" export CUDA_VISIBLE_DEVICES="" llama-cli \ - --model ${models.gemma-4-e2b} \ - --mmproj ${models.gemma-4-e2b-mmproj} \ + --model ${models.gemma-4.e2b.model} \ + --mmproj ${models.gemma-4.e2b.mmproj} \ --load-mode mmap \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ @@ -322,8 +373,8 @@ in cat > "$tmpin" export CUDA_VISIBLE_DEVICES="" llama-cli \ - --model ${models.gemma-4-e2b} \ - --mmproj ${models.gemma-4-e2b-mmproj} \ + --model ${models.gemma-4.e2b.model} \ + --mmproj ${models.gemma-4.e2b.mmproj} \ --load-mode mmap \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ @@ -352,8 +403,8 @@ in cat > "$tmpin" export CUDA_VISIBLE_DEVICES="" llama-cli \ - --model ${models.gemma-4-e2b} \ - --mmproj ${models.gemma-4-e2b-mmproj} \ + --model ${models.gemma-4.e2b.model} \ + --mmproj ${models.gemma-4.e2b.mmproj} \ --load-mode mmap \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ @@ -381,8 +432,8 @@ in cat > "$tmpin" export CUDA_VISIBLE_DEVICES="" llama-cli \ - --model ${models.gemma-4-e2b} \ - --mmproj ${models.gemma-4-e2b-mmproj} \ + --model ${models.gemma-4.e2b.model} \ + --mmproj ${models.gemma-4.e2b.mmproj} \ --load-mode mmap \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ @@ -443,17 +494,5 @@ in package = node-generate-text; }; }; - - dot.ai.models = builtins.listToAttrs ( - builtins.map - (family: { - name = family; - value.files = builtins.filter (lib.hasPrefix family) (builtins.attrValues models); - }) - [ - "gemma-4" - "qwen-3" - ] - ); }; } diff --git a/src/modules/ai/opencode/default.nix b/src/modules/ai/opencode/default.nix index f770cad8..01f6314a 100644 --- a/src/modules/ai/opencode/default.nix +++ b/src/modules/ai/opencode/default.nix @@ -30,9 +30,10 @@ autoupdate = false; share = "disabled"; compaction.prune = true; - enabled_providers = [ "deepseek" ]; - model = "openrouter/deepseek/deepseek-v4-flash-0731"; - small_model = "openrouter/deepseek/deepseek-v4-flash-0731"; + provider.openrouter.options = selfLib.ai.openrouter.options; + enabled_providers = [ "openrouter" ]; + model = selfLib.ai.openrouter.model; + small_model = selfLib.ai.openrouter.model; lsp = true; permission = { bash = { diff --git a/src/modules/ai/piper.nix b/src/modules/ai/piper.nix index b342e4a9..4797d2d2 100644 --- a/src/modules/ai/piper.nix +++ b/src/modules/ai/piper.nix @@ -122,17 +122,28 @@ dot.piper.node = node; dot.piper.sampleRate = defaultVoice.sampleRate; - home-manager.users.${config.dot.user.user}.dot.ai.models.piper.files = builtins.concatMap ( - { - model, - config, - ... - }: - [ - model - config - ] - ) (builtins.attrValues voices); + dot.ai.models = builtins.listToAttrs ( + builtins.map ( + { + model, + config, + ... + }: + let + json = builtins.fromJSON (builtins.readFile config); + in + { + name = "piper-${json.dataset}"; + value = { + family = "piper"; + files = [ + model + config + ]; + }; + } + ) (builtins.attrValues voices) + ); environment.systemPackages = [ package diff --git a/src/modules/ai/stable-diffusion-cpp.nix b/src/modules/ai/stable-diffusion-cpp.nix index c4f502d4..715af9fb 100644 --- a/src/modules/ai/stable-diffusion-cpp.nix +++ b/src/modules/ai/stable-diffusion-cpp.nix @@ -1,4 +1,58 @@ +let + makeModels = pkgs: { + clip_g = pkgs.fetchurl { + url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/clip_g-Q4_0.gguf"; + hash = "sha256-wUJBEUfha3xLnMH12XfL5ZYQRDXXb95HFy09NcXli7g="; + }; + + clip_l = pkgs.fetchurl { + url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/clip_l-Q4_0.gguf"; + hash = "sha256-9a2IrirJJOtKwCmLd6+jBLXmAU/AxBKPDj30D9/MD4o="; + }; + + model = pkgs.fetchurl { + url = "https://huggingface.co/tensorart/stable-diffusion-3.5-medium-turbo/resolve/main/sd3.5m_turbo-Q4_K_M.gguf"; + hash = "sha256-PDeTgTRNKis+49ehvJf30eWPqVxrUYf7SLPORG+Z8Xs="; + }; + + t5xxl = pkgs.fetchurl { + url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/t5xxl-Q4_0.gguf"; + hash = "sha256-mHukfBWLiQwnT3j9NTJEGfUJQehGpJeJ8Jd+n+nZerc="; + }; + + vae = pkgs.fetchurl { + url = "https://huggingface.co/tensorart/stable-diffusion-3.5-medium-turbo/resolve/main/vae/diffusion_pytorch_model.safetensors"; + hash = "sha256-j1MwSnkzW1XhPsUPY+UVf+5N6y8w1frgZU4rJlPBCdw="; + }; + }; +in { + machines.nixosModules.stable-diffusion-cpp = + { + pkgs, + config, + lib, + ... + }: + let + cuda = config.nixpkgs.config.cudaSupport; + + models = makeModels pkgs; + in + lib.mkIf cuda { + dot.ai.models.sd-3-5-turbo = { + family = "sd"; + name = "3-5-turbo"; + files = [ + models.model + models.clip_g + models.clip_l + models.t5xxl + models.vae + ]; + }; + }; + machines.homeModules.stable-diffusion-cpp = { pkgs, @@ -10,6 +64,8 @@ let cuda = config.nixpkgs.config.cudaSupport; + models = makeModels pkgs; + # NOTE: like this because some libs # otherwise conflict with other packages package = pkgs.buildEnv { @@ -18,31 +74,6 @@ pathsToLink = [ "/bin" ]; }; - clip_g = pkgs.fetchurl { - url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/clip_g-Q4_0.gguf"; - hash = "sha256-wUJBEUfha3xLnMH12XfL5ZYQRDXXb95HFy09NcXli7g="; - }; - - clip_l = pkgs.fetchurl { - url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/clip_l-Q4_0.gguf"; - hash = "sha256-9a2IrirJJOtKwCmLd6+jBLXmAU/AxBKPDj30D9/MD4o="; - }; - - model = pkgs.fetchurl { - url = "https://huggingface.co/tensorart/stable-diffusion-3.5-medium-turbo/resolve/main/sd3.5m_turbo-Q4_K_M.gguf"; - hash = "sha256-PDeTgTRNKis+49ehvJf30eWPqVxrUYf7SLPORG+Z8Xs="; - }; - - t5xxl = pkgs.fetchurl { - url = "https://huggingface.co/second-state/stable-diffusion-3.5-medium-GGUF/resolve/main/t5xxl-Q4_0.gguf"; - hash = "sha256-mHukfBWLiQwnT3j9NTJEGfUJQehGpJeJ8Jd+n+nZerc="; - }; - - vae = pkgs.fetchurl { - url = "https://huggingface.co/tensorart/stable-diffusion-3.5-medium-turbo/resolve/main/vae/diffusion_pytorch_model.safetensors"; - hash = "sha256-j1MwSnkzW1XhPsUPY+UVf+5N6y8w1frgZU4rJlPBCdw="; - }; - node-generate = pkgs.writeShellApplication { name = "stable-diffusion-cpp-node-generate"; runtimeInputs = [ @@ -54,11 +85,11 @@ trap 'rm -f "$tmpin"; rm -f "$tmpout"' EXIT cat > "$tmpin" sd-cli \ - --diffusion-model "${model}" \ - --clip_g "${clip_g}" \ - --clip_l "${clip_l}" \ - --t5xxl "${t5xxl}" \ - --vae "${vae}" \ + --diffusion-model "${models.model}" \ + --clip_g "${models.clip_g}" \ + --clip_l "${models.clip_l}" \ + --t5xxl "${models.t5xxl}" \ + --vae "${models.vae}" \ --diffusion-fa \ --mmap \ --offload-to-cpu \ @@ -89,14 +120,6 @@ package = node-generate; }; - dot.ai.models.sd-3-5-turbo.files = [ - model - clip_g - clip_l - t5xxl - vae - ]; - home.packages = [ package ]; diff --git a/src/modules/ai/whisper-cpp.nix b/src/modules/ai/whisper-cpp.nix index a1a5ece4..e63011b2 100644 --- a/src/modules/ai/whisper-cpp.nix +++ b/src/modules/ai/whisper-cpp.nix @@ -1,6 +1,42 @@ { selfLib, ... }: +let + makeModels = pkgs: { + tinyVad = pkgs.fetchurl { + url = "https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v6.2.0.bin"; + hash = "sha256-KqJpt4XutTqCmDogUB3ffB2cSOM6tjpBORrGyff7aYc="; + }; + + tinyModel = pkgs.fetchurl { + url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin"; + hash = "sha256-kh5M+Ghv3Zk9zQgaXaW2w2W/3hFi5ysI11rHUomSCx8="; + }; + }; +in { + machines.nixosModules.whisper-cpp = + { + pkgs, + config, + lib, + ... + }: + let + cuda = config.nixpkgs.config.cudaSupport; + + models = makeModels pkgs; + in + lib.mkIf cuda { + dot.ai.models.whisper-tiny = { + name = "tiny"; + family = "whisper"; + files = [ + models.tinyModel + models.tinyVad + ]; + }; + }; + machines.homeModules.whisper-cpp = { pkgs, @@ -14,6 +50,8 @@ cuda = config.nixpkgs.config.cudaSupport; + models = makeModels pkgs; + # NOTE: like this because some libs # otherwise conflict with other packages package = pkgs.buildEnv { @@ -22,16 +60,6 @@ pathsToLink = [ "/bin" ]; }; - vad = pkgs.fetchurl { - url = "https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v6.2.0.bin"; - hash = "sha256-KqJpt4XutTqCmDogUB3ffB2cSOM6tjpBORrGyff7aYc="; - }; - - model = pkgs.fetchurl { - url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin"; - hash = "sha256-kh5M+Ghv3Zk9zQgaXaW2w2W/3hFi5ysI11rHUomSCx8="; - }; - transcribeNode = pkgs.writeShellApplication { name = "whisper-cpp-node-transcribe"; runtimeInputs = [ @@ -43,11 +71,11 @@ trap 'rm -f "$tmpin"; rm -f "$tmpout"' EXIT cat > "$tmpin" whisper-cli \ - --model ${model} \ + --model ${models.tinyModel} \ --flash-attn ${if cuda then "on" else "off"} \ --no-gpu ${if cuda then "off" else "on"} \ --vad \ - --vad-model "${vad}" \ + --vad-model "${models.tinyVad}" \ --no-timestamps \ --no-prints \ --output-txt \ @@ -83,7 +111,7 @@ ]; text = '' whisper-stream \ - --model ${model} \ + --model ${models.tinyModel} \ ${lib.optionalString cuda "--flash-attn"} \ ${lib.optionalString (!cuda) "--no-gpu"} \ --language en \ @@ -144,11 +172,6 @@ package = transcribeNode; }; - dot.ai.models.whisper.files = [ - model - vad - ]; - home.packages = [ package ]; diff --git a/src/modules/ai/zeroclaw/default.nix b/src/modules/ai/zeroclaw/default.nix index f4fda99f..c3600847 100644 --- a/src/modules/ai/zeroclaw/default.nix +++ b/src/modules/ai/zeroclaw/default.nix @@ -1,28 +1,25 @@ { self, + selfLib, lib, inputs, ... }: # NOTE: needed env vars: -# MATRIX_HOMESERVER -# MATRIX_USER_ID +# MATRIX_MORGAN_FETCH_HOMESERVER +# MATRIX_MORGAN_FETCH_USER_ID # MATRIX_PEER # MATRIX_DIGEST_ROOM # GIT_USER # GIT_EMAIL # GIT_SSH_KEY # GITHUB_PERSONAL_ACCESS_TOKEN -# ZEROCLAW_providers__models__deepseek__main__api_key -# ZEROCLAW_channels__matrix__main__access_token -# ZEROCLAW_channels__matrix__main__recovery_key -# ZEROCLAW_channels__matrix__main__password - -let - listenAddress = "127.0.0.1"; - listenPort = 42617; -in +# ZEROCLAW_providers__models__openrouter__main__api_key +# ZEROCLAW_channels__matrix__morgan_fetch__access_token +# ZEROCLAW_channels__matrix__morgan_fetch__recovery_key +# ZEROCLAW_channels__matrix__morgan_fetch__password + { machines.nixosModules.zeroclaw = { @@ -45,10 +42,28 @@ in envFile = "${etcDir}/.env"; cacheDir = "/var/cache/${name}"; dataDir = "/var/lib/${name}"; - agent = "main"; - agentWorkspaceDir = "${dataDir}/agents/${agent}/workspace"; + chatAgent = "main"; + delegateAgent = "delegate"; + digestAgent = "digest"; + juniorDevAgent = "junior_dev"; + seniorDevAgent = "senior_dev"; + workspaceDir = "${dataDir}/agents/${delegateAgent}/workspace"; + workspace = { + path = workspaceDir; + read_memory_from = [ + chatAgent + delegateAgent + digestAgent + juniorDevAgent + seniorDevAgent + ]; + }; sshDir = "${dataDir}/.ssh"; + gpuApi = config.dot.ai.apis.gpu; + cpuApi = config.dot.ai.apis.cpu; + embeddingApi = config.dot.ai.apis.embedding; + zeroclaw = self.packages.${system}.zeroclaw; zeroclawCli = pkgs.writeShellApplication { name = "zeroclaw"; @@ -97,294 +112,28 @@ in toml = pkgs.formats.toml { }; - generalConfig = { - agents.${agent} = { - risk_profile = "main"; - runtime_profile = "main"; - channels = [ "matrix.main" ]; - mcp_bundles = [ - "dev" - "digest" - ]; - cron_jobs = [ "digest" ]; - }; - - schema_version = 3; - + generalConfig = lib.recursiveUpdate selfLib.ai.zeroclaw.config { runtime.shell = lib.getExe pkgs.bash; - knowledge.enabled = true; - - verifiable_intent.enabled = true; - - link_enricher.enabled = true; - - project_intel.enabled = true; - - gateway = { - require_pairing = false; - host = listenAddress; - port = listenPort; - }; - web_fetch = { enabled = true; - allowed_domains = [ - "docs.zeroclawlabs.ai" - - "en.wikipedia.org" - - "wiki.archlinux.org" - "developer.mozilla.org" - - "huggingface.co" - "github.com" - "githubusercontent.com" - "flakehub.com" - "codeberg.org" - # NOTE: sourcehut - "sr.ht" - - "nixos.org" - - "docs.rs" - "crates.io" - ]; + allowed_domains = selfLib.ai.web.allowedDomains; }; - risk_profiles.main = { - level = "supervised"; - workspace_only = true; - - sandbox_backend = "bubblewrap"; - sandbox_enabled = true; - - delegation_policy.mode = "allow"; - - # NOTE: touch is considered medium risk, for instance, - # and we already have a sandbox - # also there is a weird bug where the agent can't even ask - # for approval because shell is in auto_approve right now - require_approval_for_medium_risk = false; - - tool_receipts = { - enabled = true; - show_in_response = true; - }; - - allowed_commands = [ - "ls" - "mkdir" - "cp" - "mv" - "touch" - "pwd" - "tree" - "stat" - "file" - "readlink" - "du" - "df" - "cat" - "head" - "tail" - "wc" - "sed" - "jq" - "grep" - "rg" - "find" - "fd" - "env" - "whoami" - "id" - "date" - "uname" - "uptime" - "ps" - "free" - "echo" - "printf" - "sha256sum" - "sleep" - "true" - "false" - ]; - - excluded_tools = [ - "browser" - "schedule" - "screenshot" - "browser_open" - "proxy_config" - "model_switch" - "model_routing_config" - "memory_export" - "memory_purge" - ]; - - # NOTE: shell is ok because sandboxing and zeroclaw also parses it to check for - # allowed commands and disallows certain metacharacters - auto_approve = [ - "shell" - "sessions_current" - "sessions_history" - "sessions_list" - "knowledge" - "file_read" - "file_write" - "file_edit" - "glob_search" - "content_search" - "git_operations" - "git_forge" - "memory_recall" - "memory_store" - "memory_forget" - "web_search" - "web_search_tool" - "web_fetch" - "cron_list" - "cron_run" - "cron_runs" - "spawn_subagent" - "delegate" - "escalate_to_human" - "ask_user" - "calculator" - "reaction" - "send_message_to_peer" - "sessions_send" - "project_intel" - "weather" - "TodoWrite" - "mcp_resources" - - "rss__get_articles" - "rss__fetch_article" - - "nixos__nix" - "nixos__nix_versions" - - "nix__nix_build" - "nix__nix_run" - "nix__nix_develop" - "nix__nix_check" - - "git__git_init" - "git__git_clone" - "git__git_status" - "git__git_clean" - "git__git_add" - "git__git_commit" - "git__git_diff" - "git__git_log" - "git__git_show" - "git__git_blame" - "git__git_reflog" - "git__git_changelog_analyze" - "git__git_branch" - "git__git_checkout" - "git__git_merge" - "git__git_rebase" - "git__git_cherry_pick" - "git__git_remote" - "git__git_fetch" - "git__git_pull" - "git__git_push" - "git__git_tag" - "git__git_stash" - "git__git_reset" - "git__git_worktree" - "git__git_set_working_dir" - "git__git_clear_working_dir" - "git__git_submodule" - - "github__actions_get" - "github__actions_list" - "github__get_job_logs" - "github__get_me" - "github__get_team_members" - "github__get_teams" - "github__discussion_comment_write" - "github__get_discussion" - "github__get_discussion_comments" - "github__list_discussion_categories" - "github__list_discussions" - "github__add_issue_comment" - "github__issue_read" - "github__issue_write" - "github__list_issue_fields" - "github__list_issue_types" - "github__search_issues" - "github__sub_issue_write" - "github__get_label" - "github__label_write" - "github__list_label" - "github__dismiss_notification" - "github__get_notification_details" - "github__list_notifications" - "github__manage_notification_subscription" - "github__manage_repository_notification_subscription" - "github__mark_all_notifications_read" - "github__projects_get" - "github__projects_list" - "github__projects_write" - "github__add_comment_to_pending_review" - "github__add_reply_to_pull_request_comment" - "github__create_pull_request" - "github__list_pull_requests" - "github__merge_pull_request" - "github__pull_request_read" - "github__pull_request_review_write" - "github__search_pull_requests" - "github__update_pull_request" - "github__update_pull_request_branch" - "github__create_branch" - "github__create_repository" - "github__fork_repository" - "github__get_commit" - "github__get_file_contents" - "github__get_latest_release" - "github__get_release_by_tag" - "github__get_tag" - "github__list_branches" - "github__list_commits" - "github__list_releases" - "github__list_repository_collaborators" - "github__list_tags" - "github__search_code" - "github__search_commits" - "github__search_repositories" - "github__list_starred_repositories" - "github__star_repository" - "github__unstar_repository" - "github__search_users" - ]; - }; - - runtime_profiles.main = { - max_tool_iterations = 100; - max_actions_per_hour = 1000; - max_history_messages = 64; - max_context_tokens = 196608; - }; - - channels.matrix.main = { + web_search = { enabled = true; - homeserver = "$MATRIX_HOMESERVER"; - user_id = "$MATRIX_USER_ID"; - allowed_rooms = [ ]; - reply_in_thread = false; - }; - - peer_groups.main = { - channel = "matrix.main"; - agents = [ "main" ]; - # NOTE: affects rooms as well - external_peers = [ "$MATRIX_PEER" ]; + search_provider = + if config.dot.search.type == "searxng" then + "searxng" + else + builtins.throw "Unsupported search type ${config.dot.search.type}"; + } + // lib.optionalAttrs (config.dot.search.type == "searxng") { + searxng_instance_url = config.dot.search.url; }; + }; + mcpConfig = { mcp = { enabled = true; servers = [ @@ -398,62 +147,14 @@ in transport = "stdio"; command = lib.getExe pkgs.mcp-nix; env = { - # NOTE: default + agent workspace directory MCP_NIX_SANDBOX = builtins.concatStringsSep " " ( - [ - "--die-with-parent" - "--unshare-all" - "--ro-bind-try" - "/nix/store" - "/nix/store" - "--ro-bind" - "/usr" - "/usr" - "--ro-bind" - "/bin" - "/bin" - "--ro-bind-try" - "/sbin" - "/sbin" - "--ro-bind-try" - "/lib" - "/lib" - "--ro-bind-try" - "/lib64" - "/lib64" - "--tmpfs" - "/tmp" - "--proc" - "/proc" - "--dev" - "/dev" + selfLib.ai.bubblewrap.flags.base + ++ [ "--bind" - "${agentWorkspaceDir}" - "${agentWorkspaceDir}" - ] - ++ lib.optionals hasNvidia [ - "--ro-bind-try" - "/run/opengl-driver" - "/run/opengl-driver" - "--dev-bind-try" - "/dev/dri" - "/dev/dri" - "--dev-bind-try" - "/dev/nvidia0" - "/dev/nvidia0" - "--dev-bind-try" - "/dev/nvidiactl" - "/dev/nvidiactl" - "--dev-bind-try" - "/dev/nvidia-modeset" - "/dev/nvidia-modeset" - "--dev-bind-try" - "/dev/nvidia-uvm" - "/dev/nvidia-uvm" - "--dev-bind-try" - "/dev/nvidia-uvm-tools" - "/dev/nvidia-uvm-tools" + "${workspaceDir}" + "${workspaceDir}" ] + ++ lib.optionals hasNvidia selfLib.ai.bubblewrap.flags.nvidia ); }; @@ -466,7 +167,7 @@ in MCP_TRANSPORT_TYPE = "stdio"; MCP_LOG_LEVEL = "warn"; GIT_SSH_COMMAND = lib.getExe gitSshCommand; - GIT_BASE_DIR = "${agentWorkspaceDir}/projects"; + GIT_BASE_DIR = "${workspaceDir}/projects"; }; } { @@ -513,7 +214,65 @@ in ]; }; }; + }; + + channelConfig = { + channels.matrix.morgan_fetch = { + enabled = true; + homeserver = "$MATRIX_MORGAN_FETCH_HOMESERVER"; + user_id = "$MATRIX_MORGAN_FETCH_USER_ID"; + allowed_rooms = [ ]; + reply_in_thread = false; + }; + + peer_groups.morgan_fetch = { + channel = "matrix.morgan_fetch"; + agents = [ delegateAgent ]; + # NOTE: affects rooms as well + external_peers = [ "$MATRIX_PEER" ]; + }; + }; + + providerConfig = { + memory = + if embeddingApi != null then + { + search_mode = "hybrid"; + embedding_provider = "custom:${embeddingApi.url}"; + embedding_model = embeddingApi.model; + embedding_dimensions = 1024; + } + else + { + search_mode = "bm25"; + }; + + providers.models = { + openrouter.main = { + model = selfLib.ai.openrouter.model; + provider_extra = selfLib.ai.openrouter.options; + }; + custom = + lib.optionalAttrs (gpuApi != null) { + gpu = { + uri = gpuApi.url; + timeout_secs = 300; + model = gpuApi.model; + fallback = [ "openrouter.main" ]; + }; + } + // lib.optionalAttrs (cpuApi != null) { + cpu = { + uri = cpuApi.url; + timeout_secs = 300; + model = cpuApi.model; + fallback = [ "openrouter.main" ]; + }; + }; + }; + }; + cronConfig = { cron = { digest = { job_type = "agent"; @@ -524,92 +283,132 @@ in prompt = builtins.readFile ./cron/DIGEST.md; delivery = { mode = "announce"; - channel = "matrix.main"; + channel = "matrix.morgan_fetch"; to = "$MATRIX_DIGEST_ROOM"; }; }; }; + }; - web_search = { - enabled = true; - search_provider = "searxng"; - searxng_instance_url = config.dot.search.url; + chatAgentConfig = { + agents.${chatAgent} = { + inherit workspace; + model_provider = "openrouter.main"; + risk_profile = chatAgent; + runtime_profile = chatAgent; + channels = [ "matrix.morgan_fetch" ]; + }; + + risk_profiles.${chatAgent} = selfLib.ai.zeroclaw.riskProfile // { + excluded_tools = selfLib.ai.zeroclaw.excludedTools; + allowed_tools = selfLib.ai.zeroclaw.chatTools; + auto_approve = selfLib.ai.zeroclaw.chatTools; + allowed_commands = selfLib.ai.shell.allowedCommands; }; - memory.search_mode = "bm25"; + runtime_profiles.main = selfLib.ai.zeroclaw.runtimeProfile // { + max_context_tokens = selfLib.ai.openrouter.context; + }; }; - deepseekConfig = { - providers.models.deepseek.main = { - model = "deepseek-v4-flash"; + delegateAgentConfig = { + agents.${delegateAgent} = { + inherit workspace; + model_provider = if gpuApi != null then "custom.gpu" else "openrouter.main"; + risk_profile = delegateAgent; + runtime_profile = delegateAgent; + }; + + risk_profiles.${delegateAgent} = selfLib.ai.zeroclaw.riskProfile // { + excluded_tools = selfLib.ai.zeroclaw.excludedTools; + allowed_tools = selfLib.ai.zeroclaw.delegateTools; + auto_approve = selfLib.ai.zeroclaw.delegateTools; + allowed_commands = selfLib.ai.shell.allowedCommands; + delegation_policy.mode = "allow"; }; - agents.${agent} = { - model_provider = "deepseek.main"; + runtime_profiles.main = selfLib.ai.zeroclaw.runtimeProfile // { + max_context_tokens = if gpuApi != null then gpuApi.context else selfLib.ai.openrouter.context; }; }; - llamaConfig = { - reliability = { - provider_backoff_ms = 1000; - provider_retries = 30; + juniorDevAgentConfig = { + agents.${juniorDevAgent} = { + inherit workspace; + model_provider = if cpuApi != null then "custom.gpu" else "openrouter.main"; + risk_profile = juniorDevAgent; + runtime_profile = juniorDevAgent; + mcp_bundles = [ "dev" ]; }; - providers.models.llamacpp.main = { - uri = "http://127.0.0.1:8080/v1"; - timeout_secs = 300; - model = "qwen-3-6-35b-a3b"; - vision = false; - fallback = [ "deepseek.main" ]; + risk_profiles.${juniorDevAgent} = selfLib.ai.zeroclaw.riskProfile // { + excluded_tools = selfLib.ai.zeroclaw.excludedTools; + allowed_tools = selfLib.ai.zeroclaw.devTools; + auto_approve = selfLib.ai.zeroclaw.devTools; + allowed_commands = selfLib.ai.shell.allowedCommands; }; - providers.models.llamacpp.small = { - uri = "http://127.0.0.1:8081/v1"; - timeout_secs = 300; - model = "gemma-4-e2b"; - vision = true; - fallback = [ "deepseek.main" ]; + runtime_profiles.${juniorDevAgent} = selfLib.ai.zeroclaw.runtimeProfile // { + max_context_tokens = if gpuApi != null then gpuApi.context else selfLib.ai.openrouter.context; }; + }; - agents.${agent} = { - model_provider = "llamacpp.main"; - delegates = [ - { - agent = "small"; - mode = "bounded"; - } - { - agent = "reasoner"; - mode = "bounded"; - } - ]; + seniorDevAgentConfig = { + agents.${seniorDevAgent} = { + inherit workspace; + model_provider = "openrouter.main"; + risk_profile = seniorDevAgent; + runtime_profile = seniorDevAgent; + mcp_bundles = [ "dev" ]; }; - # NOTE: delegate-only agents — no channels, reached via the delegate - # tool from agents.main. - # Workspace is space as agent that spawns them (main). - agents.small = { - model_provider = "llamacpp.small"; - risk_profile = "main"; - runtime_profile = "main"; + risk_profiles.${seniorDevAgent} = selfLib.ai.zeroclaw.riskProfile // { + excluded_tools = selfLib.ai.zeroclaw.excludedTools; + allowed_tools = selfLib.ai.zeroclaw.devTools; + auto_approve = selfLib.ai.zeroclaw.devTools; + allowed_commands = selfLib.ai.shell.allowedCommands; }; - agents.reasoner = { - model_provider = "deepseek.main"; - risk_profile = "main"; - runtime_profile = "main"; + runtime_profiles.${seniorDevAgent} = selfLib.ai.zeroclaw.runtimeProfile // { + max_context_tokens = selfLib.ai.openrouter.context; }; + }; - memory = { - search_mode = "hybrid"; - embedding_provider = "custom:http://127.0.0.1:8082/v1"; - embedding_model = "qwen-3-embedding"; - embedding_dimensions = 1024; + digestAgentConfig = { + agents.${digestAgent} = { + inherit workspace; + model_provider = if gpuApi != null then "custom.gpu" else "openrouter.main"; + risk_profile = digestAgent; + runtime_profile = digestAgent; + mcp_bundles = [ "digest" ]; + cron_jobs = [ "digest" ]; + }; + + risk_profiles.${digestAgent} = selfLib.ai.zeroclaw.riskProfile // { + excluded_tools = selfLib.ai.zeroclaw.excludedTools; + allowed_tools = selfLib.ai.zeroclaw.devTools; + auto_approve = selfLib.ai.zeroclaw.devTools; + allowed_commands = selfLib.ai.shell.allowedCommands; + }; + + runtime_profiles.${digestAgent} = selfLib.ai.zeroclaw.runtimeProfile // { + max_context_tokens = if gpuApi != null then gpuApi.context else selfLib.ai.openrouter.context; }; }; configFile = toml.generate "${name}-config.toml" ( - lib.recursiveUpdate (lib.recursiveUpdate deepseekConfig generalConfig) llamaConfig + builtins.foldl' lib.recursiveUpdate { } [ + generalConfig + mcpConfig + channelConfig + providerConfig + cronConfig + chatAgentConfig + delegateAgentConfig + juniorDevAgentConfig + seniorDevAgentConfig + digestAgentConfig + ] ); in lib.mkIf hardware.network { @@ -663,12 +462,12 @@ in chmod 0600 "${dataDir}/.config.toml.tmp" mv -f "${dataDir}/.config.toml.tmp" "${dataDir}/config.toml" - mkdir -p ${agentWorkspaceDir} - install -m 0644 ${./agent/AGENTS.md} "${agentWorkspaceDir}/AGENTS.md" - install -m 0644 ${./agent/IDENTITY.md} "${agentWorkspaceDir}/IDENTITY.md" - install -m 0644 ${./agent/SOUL.md} "${agentWorkspaceDir}/SOUL.md" - install -m 0644 ${./agent/TOOLS.md} "${agentWorkspaceDir}/TOOLS.md" - install -m 0644 ${./agent/USER.md} "${agentWorkspaceDir}/USER.md" + mkdir -p ${workspaceDir} + install -m 0644 ${./agent/AGENTS.md} "${workspaceDir}/AGENTS.md" + install -m 0644 ${./agent/IDENTITY.md} "${workspaceDir}/IDENTITY.md" + install -m 0644 ${./agent/SOUL.md} "${workspaceDir}/SOUL.md" + install -m 0644 ${./agent/TOOLS.md} "${workspaceDir}/TOOLS.md" + install -m 0644 ${./agent/USER.md} "${workspaceDir}/USER.md" ''; script = '' zeroclaw daemon @@ -745,9 +544,11 @@ in let hardware = osConfig.dot.hardware; + gateway = selfLib.ai.zeroclaw.config.gateway; + zeroclawWeb = osConfig.dot.programs.chromium.launch { name = "zeroclaw"; - address = "http://${listenAddress}:${builtins.toString listenPort}"; + address = "http://${gateway.host}:${builtins.toString gateway.port}"; }; in { diff --git a/src/modules/desktop/xdg.nix b/src/modules/desktop/xdg.nix index 427b8b53..95b9cc06 100644 --- a/src/modules/desktop/xdg.nix +++ b/src/modules/desktop/xdg.nix @@ -361,13 +361,18 @@ home.file = builtins.listToAttrs ( builtins.concatMap ( - { name, files }: + { + family, + name, + files, + ... + }: builtins.map (file: { - name = "models/${name}/${file.name}"; + name = "models/${family}/${name}/${file.name}"; value.source = file; value.force = true; }) files - ) (builtins.attrValues config.dot.ai.models) + ) (builtins.attrValues osConfig.dot.ai.models) ); }; } diff --git a/src/modules/meta/ai.nix b/src/modules/meta/ai.nix index bce3b7ff..0c68beae 100644 --- a/src/modules/meta/ai.nix +++ b/src/modules/meta/ai.nix @@ -1,23 +1,78 @@ +let + apiSubmodule = { lib, ... }: { + options = { + url = lib.mkOption { + type = lib.types.str; + description = "API base url"; + }; + + model = lib.mkOption { + type = lib.types.str; + description = "Model id and reference to dot.ai.models"; + }; + }; + }; + + multimodalApiSubmodule = { lib, ... }: { + imports = [ apiSubmodule ]; + + options = { + context = lib.mkOption { + type = lib.types.ints.unsigned; + description = "Allocated context size"; + }; + vision = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether the API supports vision"; + }; + }; + }; + + embeddingApiSubmodule = apiSubmodule; +in { - machines.homeModules.ai = { lib, ... }: { + machines.nixosModules.ai = { lib, ... }: { options.dot = { ai = { + apis = { + gpu = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule multimodalApiSubmodule); + default = null; + description = "API running inference on the GPU"; + }; + cpu = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule multimodalApiSubmodule); + default = null; + description = "API running inference on the CPU"; + }; + embedding = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule embeddingApiSubmodule); + default = null; + description = "Embedding API"; + }; + }; + models = lib.mkOption { default = { }; description = "Model definitions"; type = lib.types.attrsOf ( lib.types.submodule ( - { name, ... }: { + { name, lib, ... }: { options = { name = lib.mkOption { type = lib.types.str; default = name; - description = "Model directory name"; + description = "Model name"; + }; + family = lib.mkOption { + type = lib.types.str; + default = name; + description = "Model family"; }; files = lib.mkOption { type = lib.types.listOf lib.types.package; - default = [ ]; - description = "Model directory files"; + description = "Model files (GGUF, config, etc.)"; }; }; } diff --git a/src/modules/meta/search.nix b/src/modules/meta/search.nix index 3a6c5c8f..dcb2cc84 100644 --- a/src/modules/meta/search.nix +++ b/src/modules/meta/search.nix @@ -1,6 +1,11 @@ { machines.nixosModules.search = { lib, ... }: { options.dot.search = { + type = lib.mkOption { + type = lib.types.enum [ "searxng" ]; + description = "Search provider type"; + }; + url = lib.mkOption { type = lib.types.str; description = "URL of the active search engine instance."; diff --git a/src/modules/services/searxng/searxng.nix b/src/modules/services/searxng/searxng.nix index e4b3f158..a88e3f88 100644 --- a/src/modules/services/searxng/searxng.nix +++ b/src/modules/services/searxng/searxng.nix @@ -34,7 +34,10 @@ ); in lib.mkIf hardware.network { - dot.search.url = "http://${address}:${builtins.toString port}"; + dot.search = { + type = "searxng"; + url = "http://${address}:${builtins.toString port}"; + }; services.searx = { enable = true;