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.
848 lines
30 KiB
Nix
848 lines
30 KiB
Nix
{ 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)
|
|
'')
|
|
];
|
|
};
|
|
}
|