Initial commit: Split Macha autonomous system into separate flake
Macha is now a standalone NixOS flake that can be imported into other systems. This provides: - Independent versioning - Easier reusability - Cleaner separation of concerns - Better development workflow Includes: - Complete autonomous system code - NixOS module with full configuration options - Queue-based architecture with priority system - Chunked map-reduce for large outputs - ChromaDB knowledge base - Tool calling system - Multi-host SSH management - Gotify notification integration All capabilities from DESIGN.md are preserved.
This commit is contained in:
847
module.nix
Normal file
847
module.nix
Normal file
@@ -0,0 +1,847 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.macha-autonomous;
|
||||
|
||||
# Python environment with all dependencies
|
||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||
requests
|
||||
psutil
|
||||
chromadb
|
||||
]);
|
||||
|
||||
# Main autonomous system package
|
||||
macha-autonomous = pkgs.writeScriptBin "macha-autonomous" ''
|
||||
#!${pythonEnv}/bin/python3
|
||||
import sys
|
||||
sys.path.insert(0, "${./.}")
|
||||
from orchestrator import main
|
||||
main()
|
||||
'';
|
||||
|
||||
# Config file
|
||||
configFile = pkgs.writeText "macha-autonomous-config.json" (builtins.toJSON {
|
||||
check_interval = cfg.checkInterval;
|
||||
autonomy_level = cfg.autonomyLevel;
|
||||
ollama_host = cfg.ollamaHost;
|
||||
model = cfg.model;
|
||||
config_repo = cfg.configRepo;
|
||||
config_branch = cfg.configBranch;
|
||||
});
|
||||
|
||||
in {
|
||||
options.services.macha-autonomous = {
|
||||
enable = mkEnableOption "Macha autonomous system maintenance";
|
||||
|
||||
autonomyLevel = mkOption {
|
||||
type = types.enum [ "observe" "suggest" "auto-safe" "auto-full" ];
|
||||
default = "suggest";
|
||||
description = ''
|
||||
Level of autonomy for the system:
|
||||
- observe: Only monitor and log, no actions
|
||||
- suggest: Propose actions, require manual approval
|
||||
- auto-safe: Auto-execute low-risk actions (restarts, cleanup)
|
||||
- auto-full: Full autonomy with safety limits (still requires approval for high-risk)
|
||||
'';
|
||||
};
|
||||
|
||||
checkInterval = mkOption {
|
||||
type = types.int;
|
||||
default = 300;
|
||||
description = "Interval in seconds between system checks";
|
||||
};
|
||||
|
||||
ollamaHost = mkOption {
|
||||
type = types.str;
|
||||
default = "http://localhost:11434";
|
||||
description = "Ollama API host";
|
||||
};
|
||||
|
||||
model = mkOption {
|
||||
type = types.str;
|
||||
default = "llama3.1:70b";
|
||||
description = "LLM model to use for reasoning";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "macha";
|
||||
description = "User to run the autonomous system as";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "macha";
|
||||
description = "Group to run the autonomous system as";
|
||||
};
|
||||
|
||||
gotifyUrl = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "http://rhiannon:8181";
|
||||
description = "Gotify server URL for notifications (empty to disable)";
|
||||
};
|
||||
|
||||
gotifyToken = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Gotify application token for notifications";
|
||||
};
|
||||
|
||||
remoteSystems = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "rhiannon" "alexander" ];
|
||||
description = "List of remote NixOS systems to monitor and maintain";
|
||||
};
|
||||
|
||||
configRepo = mkOption {
|
||||
type = types.str;
|
||||
default = if config.programs.nh.flake != null
|
||||
then config.programs.nh.flake
|
||||
else "git+https://git.coven.systems/lily/nixos-servers";
|
||||
description = "URL of the NixOS configuration repository (auto-detected from programs.nh.flake if available)";
|
||||
};
|
||||
|
||||
configBranch = mkOption {
|
||||
type = types.str;
|
||||
default = "main";
|
||||
description = "Branch of the NixOS configuration repository";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Create user and group
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
uid = 2501;
|
||||
description = "Macha autonomous system maintenance";
|
||||
home = "/var/lib/macha";
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
# Git configuration for credential storage
|
||||
programs.git = {
|
||||
enable = true;
|
||||
config = {
|
||||
credential.helper = "store";
|
||||
};
|
||||
};
|
||||
|
||||
# Ollama service for AI inference
|
||||
services.ollama = {
|
||||
enable = true;
|
||||
acceleration = "rocm";
|
||||
host = "0.0.0.0";
|
||||
port = 11434;
|
||||
environmentVariables = {
|
||||
"OLLAMA_DEBUG" = "1";
|
||||
"OLLAMA_KEEP_ALIVE" = "600";
|
||||
"OLLAMA_NEW_ENGINE" = "true";
|
||||
"OLLAMA_CONTEXT_LENGTH" = "131072";
|
||||
};
|
||||
openFirewall = false; # Keep internal only
|
||||
loadModels = [
|
||||
"qwen3"
|
||||
"gpt-oss"
|
||||
"gemma3"
|
||||
"gpt-oss:20b"
|
||||
"qwen3:4b-instruct-2507-fp16"
|
||||
"qwen3:8b-fp16"
|
||||
"mistral:7b"
|
||||
"chroma/all-minilm-l6-v2-f32:latest"
|
||||
];
|
||||
};
|
||||
|
||||
# ChromaDB service for vector storage
|
||||
services.chromadb = {
|
||||
enable = true;
|
||||
port = 8000;
|
||||
dbpath = "/var/lib/chromadb";
|
||||
};
|
||||
|
||||
# Give the user permissions it needs
|
||||
security.sudo.extraRules = [{
|
||||
users = [ cfg.user ];
|
||||
commands = [
|
||||
# Local system management
|
||||
{ command = "${pkgs.systemd}/bin/systemctl restart *"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.systemd}/bin/systemctl status *"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.systemd}/bin/journalctl *"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.nix}/bin/nix-collect-garbage *"; options = [ "NOPASSWD" ]; }
|
||||
# Remote system access (uses existing root SSH keys)
|
||||
{ command = "${pkgs.openssh}/bin/ssh *"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.openssh}/bin/scp *"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.nixos-rebuild}/bin/nixos-rebuild *"; options = [ "NOPASSWD" ]; }
|
||||
];
|
||||
}];
|
||||
|
||||
# Config file
|
||||
environment.etc."macha-autonomous/config.json".source = configFile;
|
||||
|
||||
# State directory and queue directories (world-writable queues for multi-user access)
|
||||
# Using 'z' to set permissions even if directory exists
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/macha 0755 ${cfg.user} ${cfg.group} -"
|
||||
"z /var/lib/macha 0755 ${cfg.user} ${cfg.group} -" # Ensure permissions are set
|
||||
"d /var/lib/macha/queues 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/queues/ollama 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/queues/ollama/pending 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/queues/ollama/processing 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/queues/ollama/completed 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/queues/ollama/failed 0777 ${cfg.user} ${cfg.group} -"
|
||||
"d /var/lib/macha/tool_cache 0777 ${cfg.user} ${cfg.group} -"
|
||||
];
|
||||
|
||||
# Systemd service
|
||||
systemd.services.macha-autonomous = {
|
||||
description = "Macha Autonomous System Maintenance";
|
||||
after = [ "network.target" "ollama.service" ];
|
||||
wants = [ "ollama.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = "/var/lib/macha";
|
||||
ExecStart = "${macha-autonomous}/bin/macha-autonomous --mode continuous --autonomy ${cfg.autonomyLevel} --interval ${toString cfg.checkInterval}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "30s";
|
||||
|
||||
# Security hardening
|
||||
PrivateTmp = true;
|
||||
NoNewPrivileges = false; # Need privileges for sudo
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ "/var/lib/macha" "/var/lib/macha/tool_cache" "/var/lib/macha/queues" ];
|
||||
|
||||
# Resource limits
|
||||
MemoryLimit = "1G";
|
||||
CPUQuota = "50%";
|
||||
};
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = toString ./.;
|
||||
GOTIFY_URL = cfg.gotifyUrl;
|
||||
GOTIFY_TOKEN = cfg.gotifyToken;
|
||||
CHROMA_ENV_FILE = ""; # Prevent ChromaDB from trying to read .env files
|
||||
ANONYMIZED_TELEMETRY = "False"; # Disable ChromaDB telemetry
|
||||
};
|
||||
|
||||
path = [ pkgs.git ]; # Make git available for config parsing
|
||||
};
|
||||
|
||||
# Ollama Queue Worker Service (serializes all Ollama requests)
|
||||
systemd.services.ollama-queue-worker = {
|
||||
description = "Macha Ollama Queue Worker";
|
||||
after = [ "network.target" "ollama.service" ];
|
||||
wants = [ "ollama.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = "/var/lib/macha";
|
||||
ExecStart = "${pythonEnv}/bin/python3 ${./.}/ollama_worker.py";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10s";
|
||||
|
||||
# Security hardening
|
||||
PrivateTmp = true;
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ "/var/lib/macha/queues" "/var/lib/macha/tool_cache" ];
|
||||
|
||||
# Resource limits
|
||||
MemoryLimit = "512M";
|
||||
CPUQuota = "25%";
|
||||
};
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = toString ./.;
|
||||
CHROMA_ENV_FILE = "";
|
||||
ANONYMIZED_TELEMETRY = "False";
|
||||
};
|
||||
};
|
||||
|
||||
# CLI tools for manual control and system packages
|
||||
environment.systemPackages = with pkgs; [
|
||||
macha-autonomous
|
||||
# Python packages for ChromaDB
|
||||
python313
|
||||
python313Packages.pip
|
||||
python313Packages.chromadb.pythonModule
|
||||
|
||||
# Tool to check approval queue
|
||||
(pkgs.writeScriptBin "macha-approve" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
if [ "$1" == "list" ]; then
|
||||
sudo -u ${cfg.user} ${pythonEnv}/bin/python3 ${./.}/executor.py queue
|
||||
elif [ "$1" == "discuss" ] && [ -n "$2" ]; then
|
||||
ACTION_ID="$2"
|
||||
echo "==================================================================="
|
||||
echo "Interactive Discussion with Macha about Action #$ACTION_ID"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# Initial explanation
|
||||
sudo -u ${cfg.user} ${pkgs.coreutils}/bin/env CHROMA_ENV_FILE="" ANONYMIZED_TELEMETRY="False" ${pythonEnv}/bin/python3 ${./.}/conversation.py --discuss "$ACTION_ID"
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo "You can now ask follow-up questions about this action."
|
||||
echo "Type 'approve' to approve it, 'reject' to reject it, or 'exit' to quit."
|
||||
echo "==================================================================="
|
||||
|
||||
# Interactive loop
|
||||
while true; do
|
||||
echo ""
|
||||
echo -n "You: "
|
||||
read -r USER_INPUT
|
||||
|
||||
# Check for special commands
|
||||
if [ "$USER_INPUT" = "exit" ] || [ "$USER_INPUT" = "quit" ] || [ -z "$USER_INPUT" ]; then
|
||||
echo "Exiting discussion."
|
||||
break
|
||||
elif [ "$USER_INPUT" = "approve" ]; then
|
||||
echo "Approving action #$ACTION_ID..."
|
||||
sudo -u ${cfg.user} ${pythonEnv}/bin/python3 ${./.}/executor.py approve "$ACTION_ID"
|
||||
break
|
||||
elif [ "$USER_INPUT" = "reject" ]; then
|
||||
echo "Rejecting and removing action #$ACTION_ID from queue..."
|
||||
sudo -u ${cfg.user} ${pythonEnv}/bin/python3 ${./.}/executor.py reject "$ACTION_ID"
|
||||
break
|
||||
fi
|
||||
|
||||
# Ask Macha the follow-up question in context of the action
|
||||
echo ""
|
||||
echo -n "Macha: "
|
||||
sudo -u ${cfg.user} ${pkgs.coreutils}/bin/env CHROMA_ENV_FILE="" ANONYMIZED_TELEMETRY="False" ${pythonEnv}/bin/python3 ${./.}/conversation.py --discuss "$ACTION_ID" --follow-up "$USER_INPUT"
|
||||
echo ""
|
||||
done
|
||||
elif [ "$1" == "approve" ] && [ -n "$2" ]; then
|
||||
sudo -u ${cfg.user} ${pythonEnv}/bin/python3 ${./.}/executor.py approve "$2"
|
||||
elif [ "$1" == "reject" ] && [ -n "$2" ]; then
|
||||
sudo -u ${cfg.user} ${pythonEnv}/bin/python3 ${./.}/executor.py reject "$2"
|
||||
else
|
||||
echo "Usage:"
|
||||
echo " macha-approve list - Show pending actions"
|
||||
echo " macha-approve discuss <N> - Discuss action number N with Macha (interactive)"
|
||||
echo " macha-approve approve <N> - Approve action number N"
|
||||
echo " macha-approve reject <N> - Reject and remove action number N from queue"
|
||||
fi
|
||||
'')
|
||||
|
||||
# Tool to run manual check
|
||||
(pkgs.writeScriptBin "macha-check" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
sudo -u ${cfg.user} sh -c 'cd /var/lib/macha && CHROMA_ENV_FILE="" ANONYMIZED_TELEMETRY="False" ${macha-autonomous}/bin/macha-autonomous --mode once --autonomy ${cfg.autonomyLevel}'
|
||||
'')
|
||||
|
||||
# Tool to view logs
|
||||
(pkgs.writeScriptBin "macha-logs" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
case "$1" in
|
||||
orchestrator)
|
||||
sudo tail -f /var/lib/macha/orchestrator.log
|
||||
;;
|
||||
decisions)
|
||||
sudo tail -f /var/lib/macha/decisions.jsonl
|
||||
;;
|
||||
actions)
|
||||
sudo tail -f /var/lib/macha/actions.jsonl
|
||||
;;
|
||||
service)
|
||||
journalctl -u macha-autonomous.service -f
|
||||
;;
|
||||
*)
|
||||
echo "Usage: macha-logs [orchestrator|decisions|actions|service]"
|
||||
;;
|
||||
esac
|
||||
'')
|
||||
|
||||
# Tool to send test notification
|
||||
(pkgs.writeScriptBin "macha-notify" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||
echo "Usage: macha-notify <title> <message> [priority]"
|
||||
echo "Example: macha-notify 'Test' 'This is a test' 5"
|
||||
echo "Priorities: 2 (low), 5 (medium), 8 (high)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export GOTIFY_URL="${cfg.gotifyUrl}"
|
||||
export GOTIFY_TOKEN="${cfg.gotifyToken}"
|
||||
|
||||
${pythonEnv}/bin/python3 ${./.}/notifier.py "$1" "$2" "''${3:-5}"
|
||||
'')
|
||||
|
||||
# Tool to query config files
|
||||
(pkgs.writeScriptBin "macha-configs" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
export PYTHONPATH=${toString ./.}
|
||||
export CHROMA_ENV_FILE=""
|
||||
export ANONYMIZED_TELEMETRY="False"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: macha-configs <search-query> [system-name]"
|
||||
echo "Examples:"
|
||||
echo " macha-configs gotify"
|
||||
echo " macha-configs 'journald configuration'"
|
||||
echo " macha-configs ollama macha.coven.systems"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
QUERY="$1"
|
||||
SYSTEM="''${2:-}"
|
||||
|
||||
${pythonEnv}/bin/python3 -c "
|
||||
from context_db import ContextDatabase
|
||||
import sys
|
||||
|
||||
db = ContextDatabase()
|
||||
query = sys.argv[1]
|
||||
system = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f'Searching for: {query}')
|
||||
if system:
|
||||
print(f'Filtered to system: {system}')
|
||||
print('='*60)
|
||||
|
||||
configs = db.query_config_files(query, system=system, n_results=5)
|
||||
|
||||
if not configs:
|
||||
print('No matching configuration files found.')
|
||||
else:
|
||||
for i, cfg in enumerate(configs, 1):
|
||||
print(f\"\\n{i}. {cfg['path']} (relevance: {cfg['relevance']:.1%})\")
|
||||
print(f\" Category: {cfg['metadata']['category']}\")
|
||||
print(' Preview:')
|
||||
preview = cfg['content'][:300].replace('\\n', '\\n ')
|
||||
print(f' {preview}')
|
||||
if len(cfg['content']) > 300:
|
||||
print(' ... (use macha-configs-read to see full file)')
|
||||
" "$QUERY" "$SYSTEM"
|
||||
'')
|
||||
|
||||
# Interactive chat tool (runs as invoking user, not as macha-autonomous)
|
||||
(pkgs.writeScriptBin "macha-chat" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
export PYTHONPATH=${toString ./.}
|
||||
export CHROMA_ENV_FILE=""
|
||||
export ANONYMIZED_TELEMETRY="False"
|
||||
|
||||
# Run as the current user, not as macha-autonomous
|
||||
# This allows the chat to execute privileged commands with the user's permissions
|
||||
${pythonEnv}/bin/python3 ${./.}/chat.py
|
||||
'')
|
||||
|
||||
# Tool to read full config file
|
||||
(pkgs.writeScriptBin "macha-configs-read" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
export PYTHONPATH=${toString ./.}
|
||||
export CHROMA_ENV_FILE=""
|
||||
export ANONYMIZED_TELEMETRY="False"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: macha-configs-read <file-path>"
|
||||
echo "Example: macha-configs-read apps/gotify.nix"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${pythonEnv}/bin/python3 -c "
|
||||
from context_db import ContextDatabase
|
||||
import sys
|
||||
|
||||
db = ContextDatabase()
|
||||
file_path = sys.argv[1]
|
||||
|
||||
cfg = db.get_config_file(file_path)
|
||||
|
||||
if not cfg:
|
||||
print(f'Config file not found: {file_path}')
|
||||
sys.exit(1)
|
||||
|
||||
print(f'File: {cfg[\"path\"]}')
|
||||
print(f'Category: {cfg[\"metadata\"][\"category\"]}')
|
||||
print('='*60)
|
||||
print(cfg['content'])
|
||||
" "$1"
|
||||
'')
|
||||
|
||||
# Tool to view system registry
|
||||
(pkgs.writeScriptBin "macha-systems" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
export PYTHONPATH=${toString ./.}
|
||||
export CHROMA_ENV_FILE=""
|
||||
export ANONYMIZED_TELEMETRY="False"
|
||||
${pythonEnv}/bin/python3 -c "
|
||||
from context_db import ContextDatabase
|
||||
import json
|
||||
|
||||
db = ContextDatabase()
|
||||
systems = db.get_all_systems()
|
||||
|
||||
print('Registered Systems:')
|
||||
print('='*60)
|
||||
for system in systems:
|
||||
os_type = system.get('os_type', 'unknown').upper()
|
||||
print(f\"\\n{system['hostname']} ({system['type']}) [{os_type}]\")
|
||||
print(f\" Config Repo: {system.get('config_repo') or '(not set)'}\")
|
||||
print(f\" Branch: {system.get('config_branch', 'unknown')}\")
|
||||
if system.get('services'):
|
||||
print(f\" Services: {', '.join(system['services'][:10])}\")
|
||||
if len(system['services']) > 10:
|
||||
print(f\" ... and {len(system['services']) - 10} more\")
|
||||
if system.get('capabilities'):
|
||||
print(f\" Capabilities: {', '.join(system['capabilities'])}\")
|
||||
print('='*60)
|
||||
"
|
||||
'')
|
||||
|
||||
# Tool to ask Macha questions
|
||||
(pkgs.writeScriptBin "macha-ask" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: macha-ask <your question>"
|
||||
echo "Example: macha-ask Why did you recommend restarting that service?"
|
||||
exit 1
|
||||
fi
|
||||
sudo -u ${cfg.user} ${pkgs.coreutils}/bin/env CHROMA_ENV_FILE="" ANONYMIZED_TELEMETRY="False" ${pythonEnv}/bin/python3 ${./.}/conversation.py "$@"
|
||||
'')
|
||||
|
||||
# Issue tracking CLI
|
||||
(pkgs.writeScriptBin "macha-issues" ''
|
||||
#!${pythonEnv}/bin/python3
|
||||
import sys
|
||||
import os
|
||||
os.environ["CHROMA_ENV_FILE"] = ""
|
||||
os.environ["ANONYMIZED_TELEMETRY"] = "False"
|
||||
sys.path.insert(0, "${./.}")
|
||||
|
||||
from context_db import ContextDatabase
|
||||
from issue_tracker import IssueTracker
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
db = ContextDatabase()
|
||||
tracker = IssueTracker(db)
|
||||
|
||||
def list_issues(show_all=False):
|
||||
"""List issues"""
|
||||
if show_all:
|
||||
issues = tracker.list_issues()
|
||||
else:
|
||||
issues = tracker.list_issues(status="open")
|
||||
|
||||
if not issues:
|
||||
print("No issues found")
|
||||
return
|
||||
|
||||
print("="*70)
|
||||
print(f"ISSUES: {len(issues)}")
|
||||
print("="*70)
|
||||
|
||||
for issue in issues:
|
||||
issue_id = issue['issue_id'][:8]
|
||||
age_hours = (datetime.utcnow() - datetime.fromisoformat(issue['created_at'])).total_seconds() / 3600
|
||||
inv_count = len(issue.get('investigations', []))
|
||||
action_count = len(issue.get('actions', []))
|
||||
|
||||
print(f"\n[{issue_id}] {issue['title']}")
|
||||
print(f" Host: {issue['hostname']}")
|
||||
print(f" Status: {issue['status'].upper()} | Severity: {issue['severity'].upper()}")
|
||||
print(f" Age: {age_hours:.1f}h | Activity: {inv_count} investigations, {action_count} actions")
|
||||
print(f" Source: {issue['source']}")
|
||||
if issue.get('resolution'):
|
||||
print(f" Resolution: {issue['resolution']}")
|
||||
|
||||
def show_issue(issue_id):
|
||||
"""Show detailed issue information"""
|
||||
# Find issue by partial ID
|
||||
all_issues = tracker.list_issues()
|
||||
matching = [i for i in all_issues if i['issue_id'].startswith(issue_id)]
|
||||
|
||||
if not matching:
|
||||
print(f"Issue {issue_id} not found")
|
||||
return
|
||||
|
||||
issue = matching[0]
|
||||
full_id = issue['issue_id']
|
||||
|
||||
print("="*70)
|
||||
print(f"ISSUE: {issue['title']}")
|
||||
print("="*70)
|
||||
print(f"ID: {full_id}")
|
||||
print(f"Host: {issue['hostname']}")
|
||||
print(f"Status: {issue['status'].upper()}")
|
||||
print(f"Severity: {issue['severity'].upper()}")
|
||||
print(f"Source: {issue['source']}")
|
||||
print(f"Created: {issue['created_at']}")
|
||||
print(f"Updated: {issue['updated_at']}")
|
||||
print(f"\nDescription:\n{issue['description']}")
|
||||
|
||||
investigations = issue.get('investigations', [])
|
||||
if investigations:
|
||||
print(f"\n{'─'*70}")
|
||||
print(f"INVESTIGATIONS ({len(investigations)}):")
|
||||
for i, inv in enumerate(investigations, 1):
|
||||
print(f"\n [{i}] {inv.get('timestamp', 'N/A')}")
|
||||
print(f" Diagnosis: {inv.get('diagnosis', 'N/A')}")
|
||||
print(f" Commands: {', '.join(inv.get('commands', []))}")
|
||||
print(f" Success: {inv.get('success', False)}")
|
||||
if inv.get('output'):
|
||||
print(f" Output: {inv['output'][:200]}...")
|
||||
|
||||
actions = issue.get('actions', [])
|
||||
if actions:
|
||||
print(f"\n{'─'*70}")
|
||||
print(f"ACTIONS ({len(actions)}):")
|
||||
for i, action in enumerate(actions, 1):
|
||||
print(f"\n [{i}] {action.get('timestamp', 'N/A')}")
|
||||
print(f" Action: {action.get('proposed_action', 'N/A')}")
|
||||
print(f" Risk: {action.get('risk_level', 'N/A').upper()}")
|
||||
print(f" Commands: {', '.join(action.get('commands', []))}")
|
||||
print(f" Success: {action.get('success', False)}")
|
||||
|
||||
if issue.get('resolution'):
|
||||
print(f"\n{'─'*70}")
|
||||
print(f"RESOLUTION:")
|
||||
print(f" {issue['resolution']}")
|
||||
|
||||
print("="*70)
|
||||
|
||||
def create_issue(description):
|
||||
"""Create a new issue manually"""
|
||||
import socket
|
||||
hostname = f"{socket.gethostname()}.coven.systems"
|
||||
|
||||
issue_id = tracker.create_issue(
|
||||
hostname=hostname,
|
||||
title=description[:100],
|
||||
description=description,
|
||||
severity="medium",
|
||||
source="user-reported"
|
||||
)
|
||||
|
||||
print(f"Created issue: {issue_id[:8]}")
|
||||
print(f"Title: {description[:100]}")
|
||||
|
||||
def resolve_issue(issue_id, resolution="Manually resolved"):
|
||||
"""Mark an issue as resolved"""
|
||||
# Find issue by partial ID
|
||||
all_issues = tracker.list_issues()
|
||||
matching = [i for i in all_issues if i['issue_id'].startswith(issue_id)]
|
||||
|
||||
if not matching:
|
||||
print(f"Issue {issue_id} not found")
|
||||
return
|
||||
|
||||
full_id = matching[0]['issue_id']
|
||||
success = tracker.resolve_issue(full_id, resolution)
|
||||
|
||||
if success:
|
||||
print(f"Resolved issue {issue_id[:8]}")
|
||||
else:
|
||||
print(f"Failed to resolve issue {issue_id}")
|
||||
|
||||
def close_issue(issue_id):
|
||||
"""Archive a resolved issue"""
|
||||
# Find issue by partial ID
|
||||
all_issues = tracker.list_issues()
|
||||
matching = [i for i in all_issues if i['issue_id'].startswith(issue_id)]
|
||||
|
||||
if not matching:
|
||||
print(f"Issue {issue_id} not found")
|
||||
return
|
||||
|
||||
full_id = matching[0]['issue_id']
|
||||
|
||||
if matching[0]['status'] != 'resolved':
|
||||
print(f"Issue {issue_id} must be resolved before closing")
|
||||
print(f"Use: macha-issues resolve {issue_id}")
|
||||
return
|
||||
|
||||
success = tracker.close_issue(full_id)
|
||||
|
||||
if success:
|
||||
print(f"Closed and archived issue {issue_id[:8]}")
|
||||
else:
|
||||
print(f"Failed to close issue {issue_id}")
|
||||
|
||||
# Main CLI
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: macha-issues <command> [options]")
|
||||
print("")
|
||||
print("Commands:")
|
||||
print(" list List open issues")
|
||||
print(" list --all List all issues (including resolved/closed)")
|
||||
print(" show <id> Show detailed issue information")
|
||||
print(" create <desc> Create a new issue manually")
|
||||
print(" resolve <id> Mark issue as resolved")
|
||||
print(" close <id> Archive a resolved issue")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "list":
|
||||
show_all = "--all" in sys.argv
|
||||
list_issues(show_all)
|
||||
elif command == "show" and len(sys.argv) >= 3:
|
||||
show_issue(sys.argv[2])
|
||||
elif command == "create" and len(sys.argv) >= 3:
|
||||
description = " ".join(sys.argv[2:])
|
||||
create_issue(description)
|
||||
elif command == "resolve" and len(sys.argv) >= 3:
|
||||
resolution = " ".join(sys.argv[3:]) if len(sys.argv) > 3 else "Manually resolved"
|
||||
resolve_issue(sys.argv[2], resolution)
|
||||
elif command == "close" and len(sys.argv) >= 3:
|
||||
close_issue(sys.argv[2])
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
sys.exit(1)
|
||||
'')
|
||||
|
||||
# Knowledge base CLI
|
||||
(pkgs.writeScriptBin "macha-knowledge" ''
|
||||
#!${pythonEnv}/bin/python3
|
||||
import sys
|
||||
import os
|
||||
os.environ["CHROMA_ENV_FILE"] = ""
|
||||
os.environ["ANONYMIZED_TELEMETRY"] = "False"
|
||||
sys.path.insert(0, "${./.}")
|
||||
|
||||
from context_db import ContextDatabase
|
||||
|
||||
db = ContextDatabase()
|
||||
|
||||
def list_topics(category=None):
|
||||
"""List all knowledge topics"""
|
||||
topics = db.list_knowledge_topics(category)
|
||||
if not topics:
|
||||
print("No knowledge topics found.")
|
||||
return
|
||||
|
||||
print(f"{'='*70}")
|
||||
if category:
|
||||
print(f"KNOWLEDGE TOPICS ({category.upper()}):")
|
||||
else:
|
||||
print(f"KNOWLEDGE TOPICS:")
|
||||
print(f"{'='*70}")
|
||||
|
||||
for topic in topics:
|
||||
print(f" • {topic}")
|
||||
|
||||
print(f"{'='*70}")
|
||||
|
||||
def show_topic(topic):
|
||||
"""Show all knowledge for a topic"""
|
||||
items = db.get_knowledge_by_topic(topic)
|
||||
if not items:
|
||||
print(f"No knowledge found for topic: {topic}")
|
||||
return
|
||||
|
||||
print(f"{'='*70}")
|
||||
print(f"KNOWLEDGE: {topic}")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
for item in items:
|
||||
print(f"ID: {item['id'][:8]}...")
|
||||
print(f"Category: {item['category']}")
|
||||
print(f"Source: {item['source']}")
|
||||
print(f"Confidence: {item['confidence']}")
|
||||
print(f"Created: {item['created_at']}")
|
||||
print(f"Times Referenced: {item['times_referenced']}")
|
||||
if item.get('tags'):
|
||||
print(f"Tags: {', '.join(item['tags'])}")
|
||||
print(f"\nKnowledge:")
|
||||
print(f" {item['knowledge']}\n")
|
||||
print(f"{'-'*70}\n")
|
||||
|
||||
def search_knowledge(query, category=None):
|
||||
"""Search knowledge base"""
|
||||
items = db.query_knowledge(query, category=category, limit=10)
|
||||
if not items:
|
||||
print(f"No knowledge found matching: {query}")
|
||||
return
|
||||
|
||||
print(f"{'='*70}")
|
||||
print(f"SEARCH RESULTS: {query}")
|
||||
if category:
|
||||
print(f"Category Filter: {category}")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
print(f"[{i}] {item['topic']}")
|
||||
print(f" Category: {item['category']} | Confidence: {item['confidence']}")
|
||||
print(f" {item['knowledge'][:150]}...")
|
||||
print()
|
||||
|
||||
def add_knowledge(topic, knowledge, category="general"):
|
||||
"""Add new knowledge"""
|
||||
kid = db.store_knowledge(
|
||||
topic=topic,
|
||||
knowledge=knowledge,
|
||||
category=category,
|
||||
source="user-provided",
|
||||
confidence="high"
|
||||
)
|
||||
if kid:
|
||||
print(f"✓ Added knowledge for topic: {topic}")
|
||||
print(f" ID: {kid[:8]}...")
|
||||
else:
|
||||
print(f"✗ Failed to add knowledge")
|
||||
|
||||
def seed_initial():
|
||||
"""Seed initial knowledge"""
|
||||
print("Seeding initial knowledge from seed_knowledge.py...")
|
||||
exec(open("${./.}/seed_knowledge.py").read())
|
||||
|
||||
# Main CLI
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: macha-knowledge <command> [options]")
|
||||
print("")
|
||||
print("Commands:")
|
||||
print(" list List all knowledge topics")
|
||||
print(" list <category> List topics in category")
|
||||
print(" show <topic> Show all knowledge for a topic")
|
||||
print(" search <query> Search knowledge base")
|
||||
print(" search <query> <cat> Search in specific category")
|
||||
print(" add <topic> <text> Add new knowledge")
|
||||
print(" seed Seed initial knowledge")
|
||||
print("")
|
||||
print("Categories: command, pattern, troubleshooting, performance, general")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "list":
|
||||
category = sys.argv[2] if len(sys.argv) >= 3 else None
|
||||
list_topics(category)
|
||||
elif command == "show" and len(sys.argv) >= 3:
|
||||
show_topic(sys.argv[2])
|
||||
elif command == "search" and len(sys.argv) >= 3:
|
||||
query = sys.argv[2]
|
||||
category = sys.argv[3] if len(sys.argv) >= 4 else None
|
||||
search_knowledge(query, category)
|
||||
elif command == "add" and len(sys.argv) >= 4:
|
||||
topic = sys.argv[2]
|
||||
knowledge = " ".join(sys.argv[3:])
|
||||
add_knowledge(topic, knowledge)
|
||||
elif command == "seed":
|
||||
seed_initial()
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
sys.exit(1)
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user