feat(mx): add Nextcloud Talk opencode bot pointing at halo.hoyer.tail:8000
Mirrors the existing nextcloud-claude-bot setup but invokes `opencode run` against the local `halo-8000` provider/model. The bot listens on 127.0.0.1:8086, is exposed via the `/_opencode-bot/` location on nc.hoyer.xyz, and uses `@Halo` as its mention trigger in group chats. The opencode config (config/opencode/config.json) is installed into the service's $HOME/.config/opencode/ on each start, so the bot picks up the same provider definition the user uses interactively. The model map keys are renamed to `halo-8000` / `halo-8001` so the canonical `provider/model` reference works without an alias indirection.
This commit is contained in:
parent
dadfb07914
commit
d8e8293c0e
6 changed files with 576 additions and 2 deletions
178
systems/x86_64-linux/mx/nextcloud-opencode-bot/module.nix
Normal file
178
systems/x86_64-linux/mx/nextcloud-opencode-bot/module.nix
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.nextcloud-opencode-bot;
|
||||
|
||||
pythonEnv = pkgs.python3.withPackages (
|
||||
ps: with ps; [
|
||||
fastapi
|
||||
uvicorn
|
||||
httpx
|
||||
]
|
||||
);
|
||||
|
||||
botModule = pkgs.runCommand "nextcloud-opencode-bot-module" { } ''
|
||||
mkdir -p $out
|
||||
cp ${./bot.py} $out/nextcloud_opencode_bot.py
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.services.nextcloud-opencode-bot = {
|
||||
enable = mkEnableOption "Nextcloud Talk OpenCode Bot";
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8086;
|
||||
description = "Port for the webhook listener";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Host to bind to";
|
||||
};
|
||||
|
||||
nextcloudUrl = mkOption {
|
||||
type = types.str;
|
||||
example = "https://cloud.example.com";
|
||||
description = "Base URL of your Nextcloud instance";
|
||||
};
|
||||
|
||||
botSecretFile = mkOption {
|
||||
type = types.path;
|
||||
description = "Path to file containing the bot secret (shared with Nextcloud)";
|
||||
};
|
||||
|
||||
opencodePath = mkOption {
|
||||
type = types.path;
|
||||
default = "${pkgs.opencode}/bin/opencode";
|
||||
description = "Path to opencode CLI binary";
|
||||
};
|
||||
|
||||
opencodeConfig = mkOption {
|
||||
type = types.path;
|
||||
description = "Path to the opencode config.json file (placed at $HOME/.config/opencode/config.json on service start)";
|
||||
};
|
||||
|
||||
model = mkOption {
|
||||
type = types.str;
|
||||
default = "halo-8000/halo-8000";
|
||||
description = "Model identifier passed to `opencode run -m`";
|
||||
};
|
||||
|
||||
botName = mkOption {
|
||||
type = types.str;
|
||||
default = "Halo";
|
||||
description = "Bot display name (used to detect @mentions in group chats)";
|
||||
};
|
||||
|
||||
allowedUsers = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Nextcloud usernames allowed to talk to the bot (empty = all)";
|
||||
};
|
||||
|
||||
contextMessages = mkOption {
|
||||
type = types.int;
|
||||
default = 6;
|
||||
description = "Number of recent messages to keep as context";
|
||||
};
|
||||
|
||||
timeout = mkOption {
|
||||
type = types.int;
|
||||
default = 120;
|
||||
description = "Timeout in seconds for opencode CLI";
|
||||
};
|
||||
|
||||
systemPrompt = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Optional additional system prompt appended to the built-in one";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.nextcloud-opencode-bot = {
|
||||
description = "Nextcloud Talk OpenCode Bot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
path = with pkgs; [
|
||||
bash
|
||||
coreutils
|
||||
git
|
||||
curl
|
||||
jq
|
||||
];
|
||||
|
||||
environment = {
|
||||
HOME = "/var/lib/nextcloud-opencode-bot";
|
||||
BOT_HOST = cfg.host;
|
||||
BOT_PORT = toString cfg.port;
|
||||
NEXTCLOUD_URL = cfg.nextcloudUrl;
|
||||
OPENCODE_PATH = cfg.opencodePath;
|
||||
OPENCODE_MODEL = cfg.model;
|
||||
BOT_NAME = cfg.botName;
|
||||
ALLOWED_USERS = concatStringsSep "," cfg.allowedUsers;
|
||||
CONTEXT_MESSAGES = toString cfg.contextMessages;
|
||||
TIMEOUT = toString cfg.timeout;
|
||||
SYSTEM_PROMPT = cfg.systemPrompt or "";
|
||||
PYTHONPATH = botModule;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
|
||||
# Materialize the opencode config at the path opencode looks for by
|
||||
# default ($HOME/.config/opencode/config.json). We copy rather than
|
||||
# symlink so opencode's config loader sees a regular file.
|
||||
ExecStartPre = pkgs.writeShellScript "install-opencode-config" ''
|
||||
set -eu
|
||||
install -d -m 0700 "$HOME/.config/opencode"
|
||||
install -m 0600 ${cfg.opencodeConfig} "$HOME/.config/opencode/config.json"
|
||||
'';
|
||||
|
||||
ExecStart = "${pythonEnv}/bin/uvicorn nextcloud_opencode_bot:app --host ${cfg.host} --port ${toString cfg.port}";
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
|
||||
User = "opencode-bot";
|
||||
Group = "opencode-bot";
|
||||
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = "read-only";
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
MemoryDenyWriteExecute = false;
|
||||
LockPersonality = true;
|
||||
|
||||
LoadCredential = "bot-secret:${cfg.botSecretFile}";
|
||||
|
||||
StateDirectory = "nextcloud-opencode-bot";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.opencode-bot = {
|
||||
isSystemUser = true;
|
||||
group = "opencode-bot";
|
||||
home = "/var/lib/nextcloud-opencode-bot";
|
||||
};
|
||||
|
||||
users.groups.opencode-bot = { };
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue