diff options
| -rw-r--r-- | Omni/Agent/Paths.hs | 39 | ||||
| -rw-r--r-- | Omni/Agent/Skills.hs | 5 | ||||
| -rw-r--r-- | Omni/Agent/Telegram.hs | 5 | ||||
| -rw-r--r-- | Omni/Agent/Tools/Outreach.hs | 16 | ||||
| -rwxr-xr-x | Omni/Dev/Beryllium.nix | 1 | ||||
| -rw-r--r-- | Omni/Dev/Beryllium/AVA.md | 111 | ||||
| -rw-r--r-- | Omni/Dev/Beryllium/Ava.nix | 48 | ||||
| -rwxr-xr-x | Omni/Dev/Beryllium/migrate-ava.sh | 102 | ||||
| -rw-r--r-- | Omni/Keys/Ava.pub | 1 | ||||
| -rw-r--r-- | Omni/Users.nix | 7 |
10 files changed, 326 insertions, 9 deletions
diff --git a/Omni/Agent/Paths.hs b/Omni/Agent/Paths.hs new file mode 100644 index 0000000..6facdc6 --- /dev/null +++ b/Omni/Agent/Paths.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Configurable paths for Ava data directories. +-- +-- In development, uses default paths under @_/var/ava/@. +-- In production, set @AVA_DATA_ROOT@ to @/home/ava@ to use the dedicated workspace. +module Omni.Agent.Paths + ( avaDataRoot, + skillsDir, + outreachDir, + userScratchRoot, + userScratchDir, + ) +where + +import Alpha +import qualified Data.Text as Text +import System.Environment (lookupEnv) +import System.FilePath ((</>)) +import System.IO.Unsafe (unsafePerformIO) + +avaDataRoot :: FilePath +avaDataRoot = unsafePerformIO <| do + m <- lookupEnv "AVA_DATA_ROOT" + pure (fromMaybe "_/var/ava" m) +{-# NOINLINE avaDataRoot #-} + +skillsDir :: FilePath +skillsDir = avaDataRoot </> "skills" + +outreachDir :: FilePath +outreachDir = avaDataRoot </> "outreach" + +userScratchRoot :: FilePath +userScratchRoot = avaDataRoot </> "users" + +userScratchDir :: Text -> FilePath +userScratchDir user = userScratchRoot </> Text.unpack user diff --git a/Omni/Agent/Skills.hs b/Omni/Agent/Skills.hs index a9953b1..1dbf23f 100644 --- a/Omni/Agent/Skills.hs +++ b/Omni/Agent/Skills.hs @@ -42,6 +42,7 @@ import qualified Data.List as List import qualified Data.Text as Text import qualified Data.Text.IO as TextIO import qualified Omni.Agent.Engine as Engine +import qualified Omni.Agent.Paths as Paths import qualified Omni.Test as Test import qualified System.Directory as Directory import qualified System.FilePath as FilePath @@ -55,7 +56,7 @@ test = "Omni.Agent.Skills" [ Test.unit "skillsDir returns correct path" <| do let dir = skillsDir - ("_/var/ava/skills" `Text.isSuffixOf` Text.pack dir) Test.@=? True, + ("skills" `Text.isSuffixOf` Text.pack dir) Test.@=? True, Test.unit "SkillMetadata parses from YAML frontmatter" <| do let yaml = "name: test-skill\ndescription: A test skill" case parseYamlFrontmatter yaml of @@ -91,7 +92,7 @@ test = -- | Base directory for all skills skillsDir :: FilePath -skillsDir = "_/var/ava/skills" +skillsDir = Paths.skillsDir -- | Skill metadata from YAML frontmatter data SkillMetadata = SkillMetadata diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index e94e73d..fd6c6b5 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -81,6 +81,7 @@ import qualified Network.HTTP.Client as HTTPClient import qualified Network.HTTP.Simple as HTTP import qualified Omni.Agent.Engine as Engine import qualified Omni.Agent.Memory as Memory +import qualified Omni.Agent.Paths as Paths import qualified Omni.Agent.Provider as Provider import qualified Omni.Agent.Skills as Skills import qualified Omni.Agent.Subagent as Subagent @@ -1281,6 +1282,10 @@ startBot maybeToken = do putText "Error: TELEGRAM_BOT_TOKEN not set and no --token provided" exitFailure + putText <| "AVA data root: " <> Text.pack Paths.avaDataRoot + putText <| "Skills dir: " <> Text.pack Paths.skillsDir + putText <| "Outreach dir: " <> Text.pack Paths.outreachDir + ensureOllama allowedIds <- loadAllowedUserIds diff --git a/Omni/Agent/Tools/Outreach.hs b/Omni/Agent/Tools/Outreach.hs index d601b36..e576cbd 100644 --- a/Omni/Agent/Tools/Outreach.hs +++ b/Omni/Agent/Tools/Outreach.hs @@ -60,8 +60,10 @@ import Data.Time (UTCTime, getCurrentTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import qualified Omni.Agent.Engine as Engine +import qualified Omni.Agent.Paths as Paths import qualified Omni.Test as Test import qualified System.Directory as Directory +import System.FilePath ((</>)) main :: IO () main = Test.run test @@ -114,19 +116,19 @@ test = ] outreachDir :: FilePath -outreachDir = "_/var/ava/outreach" +outreachDir = Paths.outreachDir pendingDir :: FilePath -pendingDir = outreachDir <> "/pending" +pendingDir = outreachDir </> "pending" approvedDir :: FilePath -approvedDir = outreachDir <> "/approved" +approvedDir = outreachDir </> "approved" rejectedDir :: FilePath -rejectedDir = outreachDir <> "/rejected" +rejectedDir = outreachDir </> "rejected" sentDir :: FilePath -sentDir = outreachDir <> "/sent" +sentDir = outreachDir </> "sent" data OutreachType = Email | Message deriving (Show, Eq, Generic) @@ -210,7 +212,7 @@ ensureDirs = do Directory.createDirectoryIfMissing True sentDir draftPath :: FilePath -> Text -> FilePath -draftPath dir draftId' = dir <> "/" <> Text.unpack draftId' <> ".json" +draftPath dir draftId' = dir </> (Text.unpack draftId' <> ".json") saveDraft :: OutreachDraft -> IO () saveDraft draft = do @@ -254,7 +256,7 @@ listDrafts status = do let jsonFiles = filter (".json" `isSuffixOf`) files drafts <- forM jsonFiles <| \f -> do - content <- TextIO.readFile (dir <> "/" <> f) + content <- TextIO.readFile (dir </> f) pure (Aeson.decode (BL.fromStrict (TE.encodeUtf8 content))) pure (catMaybes drafts) diff --git a/Omni/Dev/Beryllium.nix b/Omni/Dev/Beryllium.nix index 023523e..4d9ed09 100755 --- a/Omni/Dev/Beryllium.nix +++ b/Omni/Dev/Beryllium.nix @@ -5,6 +5,7 @@ bild.os { ../Os/Base.nix ../Packages.nix ../Users.nix + ./Beryllium/Ava.nix ./Beryllium/Configuration.nix ./Beryllium/Hardware.nix ./Beryllium/Ollama.nix diff --git a/Omni/Dev/Beryllium/AVA.md b/Omni/Dev/Beryllium/AVA.md new file mode 100644 index 0000000..620592b --- /dev/null +++ b/Omni/Dev/Beryllium/AVA.md @@ -0,0 +1,111 @@ +# Ava Deployment on Beryllium + +Ava runs as a systemd service under the `ava` user. + +## Architecture + +``` +/home/ava/ # Ava's home directory (AVA_DATA_ROOT) +├── omni/ # Clone of the omni repo +├── skills/ # Ava's skills directory +│ ├── shared/ # Skills available to all users +│ └── <username>/ # User-specific skills +├── outreach/ # Outreach approval queue +│ ├── pending/ +│ ├── approved/ +│ ├── rejected/ +│ └── sent/ +├── users/ # Per-user scratch space +│ └── <username>/ +└── .local/share/omni/ + └── memory.db # SQLite memory database +``` + +## Configuration + +The service is configured in `Ava.nix` and requires these environment variables in `/run/secrets/ava.env`: + +```bash +TELEGRAM_BOT_TOKEN=xxx +OPENROUTER_API_KEY=xxx +KAGI_API_KEY=xxx # optional +ALLOWED_TELEGRAM_USER_IDS=xxx,yyy # or * for all +``` + +## Commands + +```bash +# View logs +journalctl -u ava -f + +# Restart service +sudo systemctl restart ava + +# Check status +sudo systemctl status ava + +# Stop/Start +sudo systemctl stop ava +sudo systemctl start ava +``` + +## SSH Access + +The Ava private key is at `~/.ssh/ava_ed25519`. Use it to SSH as ava: + +```bash +ssh -i ~/.ssh/ava_ed25519 ava@beryl.bensima.com +``` + +Ben can also access ava's workspace via his own SSH key since ava is in the git group. + +## Git Setup + +Ava has its own clone of the omni repo at `/home/ava/omni`. To fetch changes from ben: + +```bash +# As ava: +cd /home/ava/omni +git fetch origin +git pull origin main +``` + +Ben can also push directly to ava's repo if needed: + +```bash +# From /home/ben/omni: +git remote add ava /home/ava/omni +git push ava main +``` + +## Redeploy + +To redeploy Ava with code changes: + +```bash +# 1. Rebuild the NixOS config +push.sh Omni/Dev/Beryllium.nix + +# 2. Or just restart the service if only env changes +sudo systemctl restart ava +``` + +## Migration from tmux + +If migrating from the old tmux-based deployment: + +1. Deploy the NixOS config with the new ava user +2. Run the migration script: `sudo ./Omni/Dev/Beryllium/migrate-ava.sh` +3. Create `/run/secrets/ava.env` with the required secrets +4. Stop the tmux ava process +5. Start the systemd service: `sudo systemctl start ava` +6. Enable on boot: `sudo systemctl enable ava` + +## Environment Variable: AVA_DATA_ROOT + +The `AVA_DATA_ROOT` environment variable controls where Ava stores its data: + +- **Development** (unset): Uses `_/var/ava/` (relative to repo) +- **Production**: Set to `/home/ava` via the systemd service + +This allows the same codebase to run in both environments without changes. diff --git a/Omni/Dev/Beryllium/Ava.nix b/Omni/Dev/Beryllium/Ava.nix new file mode 100644 index 0000000..6957352 --- /dev/null +++ b/Omni/Dev/Beryllium/Ava.nix @@ -0,0 +1,48 @@ +{...}: let + bild = import ../../Bild.nix {}; + avaPkg = bild.run ../../Ava.hs; +in { + systemd.services.ava = { + description = "Ava Telegram assistant"; + after = ["network-online.target" "ollama.service"]; + wants = ["network-online.target" "ollama.service"]; + wantedBy = ["multi-user.target"]; + + serviceConfig = { + Type = "simple"; + User = "ava"; + Group = "users"; + WorkingDirectory = "/home/ava/omni"; + + Environment = [ + "AVA_DATA_ROOT=/home/ava" + "HOME=/home/ava" + "OLLAMA_URL=http://localhost:11434" + ]; + + EnvironmentFile = "/run/secrets/ava.env"; + + ExecStart = "${avaPkg}/bin/ava"; + + Restart = "on-failure"; + RestartSec = 5; + + TimeoutStopSec = 90; + KillMode = "mixed"; + KillSignal = "SIGTERM"; + }; + }; + + systemd.tmpfiles.rules = [ + "d /home/ava 0755 ava users -" + "d /home/ava/omni 0755 ava users -" + "d /home/ava/skills 0755 ava users -" + "d /home/ava/outreach 0755 ava users -" + "d /home/ava/outreach/pending 0755 ava users -" + "d /home/ava/outreach/approved 0755 ava users -" + "d /home/ava/outreach/rejected 0755 ava users -" + "d /home/ava/outreach/sent 0755 ava users -" + "d /home/ava/users 0755 ava users -" + "d /home/ava/.local/share/omni 0755 ava users -" + ]; +} diff --git a/Omni/Dev/Beryllium/migrate-ava.sh b/Omni/Dev/Beryllium/migrate-ava.sh new file mode 100755 index 0000000..91d2740 --- /dev/null +++ b/Omni/Dev/Beryllium/migrate-ava.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Migration script: move Ava data from _/var/ava to /home/ava +# +# Run this ONCE after deploying the NixOS config with the new ava user. +# +# Usage: +# sudo ./migrate-ava.sh +# +# This script: +# 1. Copies existing data from _/var/ava to /home/ava +# 2. Copies memory.db from ben's .local to ava's .local +# 3. Clones the omni repo into /home/ava/omni +# 4. Sets proper ownership + +set -euo pipefail + +GRN='\033[0;32m' +YLW='\033[0;33m' +RED='\033[0;31m' +NC='\033[0m' + +OMNI_REPO="/home/ben/omni" +AVA_HOME="/home/ava" +OLD_DATA_ROOT="$OMNI_REPO/_/var/ava" + +echo -e "${YLW}=== Ava Migration Script ===${NC}" + +# Check we're running as root +if [[ $EUID -ne 0 ]]; then + echo -e "${RED}Error: This script must be run as root${NC}" + exit 1 +fi + +# Check ava user exists +if ! id ava &>/dev/null; then + echo -e "${RED}Error: ava user does not exist. Deploy the NixOS config first.${NC}" + exit 1 +fi + +# Create directory structure (tmpfiles should handle this, but just in case) +echo -e "${YLW}Creating directory structure...${NC}" +mkdir -p "$AVA_HOME"/{skills,outreach,users,omni,.local/share/omni} +mkdir -p "$AVA_HOME"/outreach/{pending,approved,rejected,sent} + +# Copy skills +if [[ -d "$OLD_DATA_ROOT/skills" ]]; then + echo -e "${YLW}Copying skills...${NC}" + rsync -av --progress "$OLD_DATA_ROOT/skills/" "$AVA_HOME/skills/" +else + echo -e "${YLW}No skills to migrate${NC}" +fi + +# Copy outreach +if [[ -d "$OLD_DATA_ROOT/outreach" ]]; then + echo -e "${YLW}Copying outreach data...${NC}" + rsync -av --progress "$OLD_DATA_ROOT/outreach/" "$AVA_HOME/outreach/" +else + echo -e "${YLW}No outreach data to migrate${NC}" +fi + +# Copy memory.db if it exists +BEN_MEMORY="/home/ben/.local/share/omni/memory.db" +AVA_MEMORY="$AVA_HOME/.local/share/omni/memory.db" +if [[ -f "$BEN_MEMORY" ]]; then + echo -e "${YLW}Copying memory database...${NC}" + cp -v "$BEN_MEMORY" "$AVA_MEMORY" +else + echo -e "${YLW}No memory.db found at $BEN_MEMORY${NC}" +fi + +# Clone or update the omni repo +if [[ -d "$AVA_HOME/omni/.git" ]]; then + echo -e "${YLW}Omni repo already exists, updating...${NC}" + cd "$AVA_HOME/omni" + sudo -u ava git fetch origin +else + echo -e "${YLW}Cloning omni repo...${NC}" + sudo -u ava git clone "$OMNI_REPO" "$AVA_HOME/omni" +fi + +# Set ownership +echo -e "${YLW}Setting ownership...${NC}" +chown -R ava:users "$AVA_HOME" + +# Show summary +echo "" +echo -e "${GRN}=== Migration Complete ===${NC}" +echo "" +echo "Directory structure:" +ls -la "$AVA_HOME" +echo "" +echo "Next steps:" +echo "1. Create /run/secrets/ava.env with:" +echo " TELEGRAM_BOT_TOKEN=xxx" +echo " OPENROUTER_API_KEY=xxx" +echo " KAGI_API_KEY=xxx (optional)" +echo " ALLOWED_TELEGRAM_USER_IDS=xxx (or * for all)" +echo "" +echo "2. Stop the tmux Ava process" +echo "3. Start the systemd service: sudo systemctl start ava" +echo "4. Watch logs: journalctl -u ava -f" +echo "5. Enable on boot: sudo systemctl enable ava" diff --git a/Omni/Keys/Ava.pub b/Omni/Keys/Ava.pub new file mode 100644 index 0000000..77c314c --- /dev/null +++ b/Omni/Keys/Ava.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOv/fKFUS4exJtmnWqi5Taa3W5jTxqTmAZBtvisKMKH ava@beryl.bensima.com diff --git a/Omni/Users.nix b/Omni/Users.nix index 3de5712..4ae8c17 100644 --- a/Omni/Users.nix +++ b/Omni/Users.nix @@ -30,6 +30,13 @@ in { openssh.authorizedKeys.keys = readKeys ./Keys/Deploy.pub; extraGroups = ["wheel"]; }; + ava = { + description = "Ava Telegram bot"; + isNormalUser = true; + home = "/home/ava"; + openssh.authorizedKeys.keys = readKeys ./Keys/Ava.pub; + extraGroups = ["git"]; + }; # # humans # |
