Proactive doctor, setup wizard, and TUI fixes
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
BREAKING CHANGES / KNOWN ISSUES:
- 8 skills have syntax errors causing loader warnings:
org-skill-bouncer, org-skill-config-manager, org-skill-credentials-vault,
org-skill-engineering-standards, org-skill-gardener, org-skill-homoiconic-memory,
org-skill-peripheral-vision, org-skill-policy
- These skills fail to load but don't block system operation
- TUI works despite these errors
FEATURES ADDED:
1. Proactive Doctor System
- Doctor runs automatically on daemon startup
- Health check runs before accepting connections
- Adds /health endpoint for health status queries
- *system-health* variable tracks: :healthy, :degraded, :unhealthy, :unknown
2. Error Handling (Option B - Debugger Hook)
- TUI and CLI now run doctor diagnostics on errors
- Shows "Run opencortex doctor" message on crash
- Suggests repair commands after failures
3. Interactive Setup Wizard (org-skill-config-manager)
- Full wizard implemented in config-manager skill:
* LLM provider configuration (OpenAI, Anthropic, OpenRouter, Groq, Gemini, Ollama)
* Gateway linking (Slack, Discord)
* Memory settings (auto-save interval, history retention)
* Network settings (timeout, proxy)
- Saves to ~/.config/opencortex/.env (KEY=VALUE format)
- CLI integration: opencortex setup, setup --add-provider, setup --link
4. CLI Enhancements
- doctor --watch: Background health monitoring (60s interval)
- doctor --fix: Interactive repair (falls back to full setup if core files missing)
- setup command runs wizard or delegates to setup_system
5. TUI Fixes
- Inlined message formatting to avoid dependency issues
- Added error handling in handle-return
- Cleaner error messages
6. Thin Harness Compliance
- Removed doctor from harness (now in org-skill-diagnostics skill)
- XDG directories: only .lisp in harness, .org kept in skills for loader
This commit is contained in:
@@ -83,6 +83,17 @@ The ~communication.lisp~ module defines the low-level transport and framing logi
|
||||
(cond
|
||||
((eq msg :eof) (return))
|
||||
((eq msg :error) (return))
|
||||
((eq (getf msg :type) :health-check)
|
||||
;; Handle health check request
|
||||
(let ((health-msg (list :type :health-response
|
||||
:status (or (and (boundp 'opencortex::*system-health*)
|
||||
(symbol-value 'opencortex::*system-health*))
|
||||
:unknown)
|
||||
:checked-p (or (and (boundp 'opencortex::*health-check-ran*)
|
||||
(symbol-value 'opencortex::*health-check-ran*))
|
||||
nil))))
|
||||
(format stream "~a" (frame-message health-msg))
|
||||
(finish-output stream)))
|
||||
(t (inject-stimulus msg :stream stream))))))
|
||||
(error (c) (harness-log "CLIENT ERROR: ~a" c)))
|
||||
(ignore-errors (usocket:socket-close socket))))
|
||||
|
||||
@@ -96,6 +96,44 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce
|
||||
(defvar *shutdown-save-enabled* t)
|
||||
#+end_src
|
||||
|
||||
** Health Status
|
||||
#+begin_src lisp
|
||||
(defvar *system-health* :unknown
|
||||
"Current system health status: :healthy, :degraded, :unhealthy, or :unknown.")
|
||||
|
||||
(defvar *health-check-ran* nil
|
||||
"Flag indicating if initial health check has completed.")
|
||||
#+end_src
|
||||
|
||||
** Proactive Doctor
|
||||
#+begin_src lisp
|
||||
(defun run-startup-health-check ()
|
||||
"Runs the doctor diagnostics on startup. Returns health status."
|
||||
(format t "~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " DOCTOR: Running Startup Health Check~%")
|
||||
(format t "==================================================~%")
|
||||
(handler-case
|
||||
(progn
|
||||
(when (fboundp 'doctor-run-all)
|
||||
(let ((result (doctor-run-all :auto-install nil)))
|
||||
(setf *health-check-ran* t)
|
||||
(if result
|
||||
(progn
|
||||
(setf *system-health* :healthy)
|
||||
(format t "DAEMON: Health check passed. Starting services.~%"))
|
||||
(progn
|
||||
(setf *system-health* :degraded)
|
||||
(format t "DAEMON: Health check found issues.~%")
|
||||
(format t " Run 'opencortex doctor --fix' to repair.~%")))))
|
||||
(setf *health-check-ran* t))
|
||||
(error (c)
|
||||
(format t "DOCTOR ERROR: ~a~%" c)
|
||||
(setf *system-health* :unhealthy)
|
||||
(setf *health-check-ran* t)))
|
||||
(format t "==================================================~%~%"))
|
||||
#+end_src
|
||||
|
||||
** Main Entry Point (main)
|
||||
#+begin_src lisp
|
||||
(defun main ()
|
||||
@@ -108,16 +146,20 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce
|
||||
(load-memory-from-disk)
|
||||
(initialize-actuators)
|
||||
(initialize-all-skills)
|
||||
|
||||
;; Run proactive doctor before starting services
|
||||
(run-startup-health-check)
|
||||
|
||||
(start-heartbeat)
|
||||
(start-daemon)
|
||||
|
||||
#+sbcl
|
||||
(sb-sys:enable-interrupt sb-unix:sigint
|
||||
(lambda (sig code scp)
|
||||
(declare (ignore sig code scp))
|
||||
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
||||
(when *shutdown-save-enabled* (save-memory-to-disk))
|
||||
(uiop:quit 0)))
|
||||
(lambda (sig code scp)
|
||||
(declare (ignore sig code scp))
|
||||
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
||||
(when *shutdown-save-enabled* (save-memory-to-disk))
|
||||
(uiop:quit 0)))
|
||||
|
||||
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
|
||||
(loop
|
||||
|
||||
@@ -38,7 +38,7 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
#+begin_src lisp
|
||||
(in-package :cl-user)
|
||||
(defpackage :opencortex.tui
|
||||
(:use :cl :croatoan)
|
||||
(:use :cl :croatoan :usocket)
|
||||
(:export :main))
|
||||
(in-package :opencortex.tui)
|
||||
#+end_src
|
||||
@@ -108,13 +108,18 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(when (> (length cmd) 0)
|
||||
(enqueue-msg (format nil "⬆ ~a" cmd))
|
||||
(handler-case
|
||||
(when (and stream (open-stream-p stream))
|
||||
(format stream "~a" (opencortex:frame-message (list :TYPE :EVENT
|
||||
:META (list :SOURCE :tui)
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd))))
|
||||
(finish-output stream))
|
||||
(progn
|
||||
(when (and stream (open-stream-p stream))
|
||||
(let* ((msg (list :TYPE :EVENT
|
||||
:META (list :SOURCE :tui)
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd)))
|
||||
(payload (format nil "~s" msg))
|
||||
(len (length payload)))
|
||||
(format stream "~6,'0x~a" len payload)
|
||||
(finish-output stream)))
|
||||
(enqueue-msg "✓ Sent"))
|
||||
(error (c)
|
||||
(declare (ignore c))
|
||||
(format t "Send error: ~a~%" c)
|
||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
|
||||
248
opencortex.sh
248
opencortex.sh
@@ -32,6 +32,32 @@ if [ -f "$OC_CONFIG_DIR/.env" ]; then
|
||||
source "$OC_CONFIG_DIR/.env"
|
||||
fi
|
||||
|
||||
# --- Dependency Checker ---
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
for dep in sbcl emacs git curl socat nc; do
|
||||
if ! command_exists "$dep"; then
|
||||
missing+=("$dep")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo -e "${YELLOW}--- Missing dependencies: ${missing[*]} ---${NC}"
|
||||
if command_exists apt-get; then
|
||||
echo "Attempting to install missing dependencies..."
|
||||
if sudo apt-get update && sudo apt-get install -y sbcl emacs-nox rlwrap netcat-openbsd curl git socat libssl-dev libncurses-dev libffi-dev zlib1g-dev libsqlite3-dev 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ Dependencies installed successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Could not install dependencies. Please run with sudo or install manually:${NC}"
|
||||
echo " sudo apt-get install sbcl emacs-nox rlwrap netcat-openbsd curl git socat"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Cannot auto-install: apt-get not available${NC}"
|
||||
echo "Please install manually: sbcl emacs git curl socat netcat-openbsd"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 2. SETUP ---
|
||||
setup_system() {
|
||||
NON_INTERACTIVE=false
|
||||
@@ -60,25 +86,32 @@ setup_system() {
|
||||
cp "$SCRIPT_DIR/opencortex.asd" "$OC_DATA_DIR/"
|
||||
cp "$SCRIPT_DIR/harness"/*.org "$OC_DATA_DIR/harness/"
|
||||
cp "$SCRIPT_DIR/skills"/*.org "$OC_DATA_DIR/skills/"
|
||||
|
||||
# Create tests directory before tangling (some org files write to tests/)
|
||||
mkdir -p "$OC_DATA_DIR/tests"
|
||||
|
||||
export INSTALL_DIR="$OC_DATA_DIR"
|
||||
|
||||
# Critical: Tangle manifest first to establish system structure (into root)
|
||||
echo "Tangling harness/manifest.org..."
|
||||
(cd "$OC_DATA_DIR" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"$OC_DATA_DIR/harness/manifest.org\")" >/dev/null 2>&1) || true
|
||||
(cd "$OC_DATA_DIR" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"harness/manifest.org\")") >/dev/null 2>&1 || true
|
||||
|
||||
# Tangle harness files into harness/
|
||||
for f in harness/*.org; do
|
||||
if [ "$f" != "harness/manifest.org" ]; then
|
||||
echo "Tangling $f..."
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"$OC_DATA_DIR/$f\")" >/dev/null 2>&1) || true
|
||||
for f in "$SCRIPT_DIR/harness"/*.org; do
|
||||
fname=$(basename "$f" .org)
|
||||
if [ "$fname" != "manifest" ]; then
|
||||
echo "Tangling harness/$fname.org..."
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Tangle skill files into skills/
|
||||
for f in skills/*.org; do
|
||||
echo "Tangling $f..."
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"$OC_DATA_DIR/$f\")" >/dev/null 2>&1) || true
|
||||
for f in "$SCRIPT_DIR/skills"/*.org; do
|
||||
fname=$(basename "$f" .org)
|
||||
echo "Tangling skills/$fname.org..."
|
||||
# Copy org to XDG first (skills need to be loaded from XDG path)
|
||||
cp "$f" "$OC_DATA_DIR/skills/"
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch --eval "(require 'org)" --eval "(setq org-confirm-babel-evaluate nil)" --eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# Special handling for tests that need to go into tests/
|
||||
@@ -88,6 +121,11 @@ setup_system() {
|
||||
|
||||
# Also move run-all-tests.lisp if it landed in the wrong place
|
||||
[ -f "$OC_DATA_DIR/run-all-tests.lisp" ] && mv "$OC_DATA_DIR/run-all-tests.lisp" "$OC_DATA_DIR/harness/"
|
||||
|
||||
# Cleanup: Remove .org files from XDG harness only (skills need .org for loader)
|
||||
echo "Cleaning up .org files from XDG harness..."
|
||||
rm -f "$OC_DATA_DIR/harness"/*.org
|
||||
|
||||
cd "$SCRIPT_DIR" # Create the bin shim
|
||||
echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}"
|
||||
ln -sf "$SCRIPT_DIR/opencortex.sh" "$OC_BIN_DIR/opencortex"
|
||||
@@ -107,6 +145,66 @@ setup_system() {
|
||||
--eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))'
|
||||
}
|
||||
|
||||
# --- Doctor Repair (Lightweight Fix) ---
|
||||
doctor_repair() {
|
||||
echo -e "${BLUE}=== OpenCortex: Repair Mode ===${NC}"
|
||||
|
||||
# 1. Fix system dependencies
|
||||
echo -e "${YELLOW}--- Fixing System Dependencies ---${NC}"
|
||||
check_dependencies
|
||||
|
||||
# 2. Ensure XDG directories exist
|
||||
echo -e "${YELLOW}--- Fixing XDG Directories ---${NC}"
|
||||
mkdir -p "$OC_CONFIG_DIR" "$OC_DATA_DIR" "$OC_STATE_DIR" "$OC_BIN_DIR"
|
||||
mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills" "$OC_DATA_DIR/library"
|
||||
|
||||
# 3. Re-tangle harness files that may be broken
|
||||
echo -e "${YELLOW}--- Re-tangling Harness Files ---${NC}"
|
||||
for f in "$SCRIPT_DIR/harness"/*.org; do
|
||||
if [ -f "$f" ]; then
|
||||
fname=$(basename "$f" .org)
|
||||
echo " Checking harness/$fname..."
|
||||
# Try to load each harness file - if it fails, re-tangle
|
||||
if ! sbcl --non-interactive \
|
||||
--eval "(load \"$OC_DATA_DIR/harness/${fname}.lisp\")" \
|
||||
--eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then
|
||||
echo " Re-tangling $fname.org..."
|
||||
(cd "$OC_DATA_DIR/harness" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"$f\")" >/dev/null 2>&1) || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Re-tangle skill files that may be broken
|
||||
echo -e "${YELLOW}--- Re-tangling Skill Files ---${NC}"
|
||||
for f in "$SCRIPT_DIR/skills"/*.org; do
|
||||
if [ -f "$f" ]; then
|
||||
fname=$(basename "$f" .org)
|
||||
echo " Checking skill/$fname..."
|
||||
# Copy .org to XDG temporarily for tangle, then remove
|
||||
cp "$f" "$OC_DATA_DIR/skills/"
|
||||
if ! sbcl --non-interactive \
|
||||
--eval "(load \"$OC_DATA_DIR/skills/${fname}.lisp\")" \
|
||||
--eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then
|
||||
echo " Re-tangling $fname.org..."
|
||||
(cd "$OC_DATA_DIR/skills" && emacs -Q --batch \
|
||||
--eval "(require 'org)" \
|
||||
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||
--eval "(org-babel-tangle-file \"$OC_DATA_DIR/skills/${fname}.org\")" >/dev/null 2>&1) || true
|
||||
fi
|
||||
rm -f "$OC_DATA_DIR/skills/${fname}.org"
|
||||
fi
|
||||
done
|
||||
|
||||
# 5. Cleanup .org files
|
||||
rm -f "$OC_DATA_DIR/harness"/*.org "$OC_DATA_DIR/skills"/*.org 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}--- Repair Complete ---${NC}"
|
||||
echo "Run 'opencortex doctor' to verify the system."
|
||||
}
|
||||
|
||||
# --- 3. COMMAND ROUTER ---
|
||||
COMMAND=$1
|
||||
[ -z "$COMMAND" ] && COMMAND="cli"
|
||||
@@ -116,23 +214,82 @@ case "$COMMAND" in
|
||||
link)
|
||||
PLATFORM=$1
|
||||
TOKEN=$2
|
||||
check_dependencies
|
||||
exec sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" --eval '(ql:quickload :opencortex)' --eval '(opencortex:initialize-all-skills)' --eval "(funcall (find-symbol \"GATEWAY-MANAGER-MAIN\" :opencortex) \"$PLATFORM\" \"$TOKEN\")"
|
||||
;;
|
||||
|
||||
doctor)
|
||||
exec sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "DOCTOR-MAIN" :opencortex))'
|
||||
check_dependencies
|
||||
if [ "$1" = "--watch" ]; then
|
||||
echo "Starting background health monitor (60s interval)..."
|
||||
echo "Press Ctrl+C to stop."
|
||||
echo ""
|
||||
while true; do
|
||||
echo "--- $(date '+%Y-%m-%d %H:%M:%S') ---"
|
||||
sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "DOCTOR-RUN-ALL" :opencortex))' \
|
||||
--eval '(uiop:quit 0)' 2>&1 | grep -E "(HEALTH|OK|FAIL|WARN|SYSTEM|===)" || true
|
||||
sleep 60
|
||||
done
|
||||
elif [ "$1" = "--fix" ]; then
|
||||
# Check if major harness files exist - if not, run full setup
|
||||
if [ ! -f "$OC_DATA_DIR/harness/package.lisp" ] || [ ! -f "$OC_DATA_DIR/harness/skills.lisp" ]; then
|
||||
echo "Core files missing. Running full setup..."
|
||||
setup_system "$@"
|
||||
else
|
||||
echo "Repairing system..."
|
||||
doctor_repair
|
||||
fi
|
||||
else
|
||||
exec sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "DOCTOR-MAIN" :opencortex))'
|
||||
fi
|
||||
;;
|
||||
|
||||
setup)
|
||||
setup_system "$@"
|
||||
check_dependencies
|
||||
if [ "$1" = "--add-provider" ]; then
|
||||
echo "Adding LLM provider..."
|
||||
sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "SETUP-ADD-PROVIDER" :opencortex))'
|
||||
elif [ "$1" = "--link" ]; then
|
||||
PLATFORM=$2
|
||||
TOKEN=$3
|
||||
if [ -z "$PLATFORM" ] || [ -z "$TOKEN" ]; then
|
||||
echo "Usage: opencortex setup --link <platform> <token>"
|
||||
echo " platforms: slack, discord"
|
||||
exit 1
|
||||
fi
|
||||
echo "Linking $PLATFORM gateway..."
|
||||
$0 link "$PLATFORM" "$TOKEN"
|
||||
elif [ "$1" = "--non-interactive" ]; then
|
||||
setup_system "$@"
|
||||
else
|
||||
# Run interactive setup wizard
|
||||
echo "Starting interactive setup wizard..."
|
||||
sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex)' \
|
||||
--eval '(opencortex:initialize-all-skills)' \
|
||||
--eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))'
|
||||
fi
|
||||
;;
|
||||
|
||||
boot|--boot)
|
||||
check_dependencies
|
||||
exec sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
@@ -140,16 +297,71 @@ case "$COMMAND" in
|
||||
--eval '(opencortex:main)'
|
||||
;;
|
||||
|
||||
daemon)
|
||||
check_dependencies
|
||||
echo "Starting OpenCortex daemon in background..."
|
||||
nohup sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval "(ql:quickload '(:opencortex :croatoan))" \
|
||||
--eval '(opencortex:main)' \
|
||||
> "$OC_STATE_DIR/daemon.log" 2>&1 &
|
||||
echo "Daemon started. Waiting for port 9105..."
|
||||
for i in {1..20}; do
|
||||
if ss -tln | grep -q 9105; then
|
||||
echo "✓ Daemon ready on port 9105"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "✗ Daemon failed to start. Check $OC_STATE_DIR/daemon.log"
|
||||
exit 1
|
||||
;;
|
||||
|
||||
tui)
|
||||
exec sbcl \
|
||||
check_dependencies
|
||||
if ! ss -tln | grep -q 9105; then
|
||||
echo "Daemon not running. Starting daemon first..."
|
||||
$0 daemon
|
||||
fi
|
||||
if sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval '(ql:quickload :opencortex/tui)' \
|
||||
--eval '(opencortex.tui:main)'
|
||||
--eval '(opencortex.tui:main)'; then
|
||||
true
|
||||
else
|
||||
EXIT_CODE=$?
|
||||
echo ""
|
||||
echo "TUI exited with error. Running diagnostics..."
|
||||
$0 doctor
|
||||
echo ""
|
||||
echo "Run 'opencortex doctor --fix' to repair, or 'opencortex setup' to reconfigure."
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
;;
|
||||
|
||||
cli|boot)
|
||||
check_dependencies
|
||||
if sbcl --non-interactive \
|
||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||
--eval "(ql:quickload '(:opencortex :croatoan))" \
|
||||
--eval '(opencortex:main)'; then
|
||||
true
|
||||
else
|
||||
EXIT_CODE=$?
|
||||
echo ""
|
||||
echo "CLI exited with error. Running diagnostics..."
|
||||
$0 doctor
|
||||
echo ""
|
||||
echo "Run 'opencortex doctor --fix' to repair, or 'opencortex setup' to reconfigure."
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Available commands: setup, link, doctor, boot, tui"
|
||||
echo "Available commands: setup, link, doctor, boot, tui, cli, daemon"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -16,8 +16,8 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
** Security Configuration
|
||||
#+begin_src lisp
|
||||
(defvar *bouncer-network-whitelist*
|
||||
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com
|
||||
"Domains that the Bouncer considers safe for outbound connections.
|
||||
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com")
|
||||
"Domains that the Bouncer considers safe for outbound connections.")
|
||||
#+end_src
|
||||
|
||||
** Secret Scanning (bouncer-scan-secrets)
|
||||
@@ -56,9 +56,9 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
(let* ((target (proto-get action :target))
|
||||
(payload (proto-get action :payload))
|
||||
(text (or (proto-get payload :text) (proto-get action :text)))
|
||||
(cmd (or (proto-get payload :cmd)
|
||||
(when (and (eq target :tool) (equal (proto-get payload :tool) "shell)
|
||||
(proto-get (proto-get payload :args) :cmd))))
|
||||
(cmd (or (proto-get payload :cmd)
|
||||
(when (and (eq target :tool) (equal (proto-get payload :tool) "shell"))
|
||||
(proto-get (proto-get payload :args) :cmd)))))
|
||||
(approved (proto-get action :approved)))
|
||||
|
||||
(cond
|
||||
@@ -71,15 +71,15 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
:payload (list :level :error
|
||||
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
|
||||
|
||||
((and (or (eq target :shell)
|
||||
(and (eq target :tool) (equal (proto-get payload :tool) "shell))
|
||||
(bouncer-check-network-exfil cmd))
|
||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.
|
||||
((and (or (eq target :shell)
|
||||
(and (eq target :tool) (equal (proto-get payload :tool) "shell")))
|
||||
(bouncer-check-network-exfil cmd))
|
||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval."))
|
||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||
|
||||
((or (member target '(:shell))
|
||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file :test #'string=))
|
||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval)))
|
||||
((or (member target '(:shell))
|
||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval))))
|
||||
(harness-log "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||
|
||||
@@ -90,7 +90,7 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
#+begin_src lisp
|
||||
(defun bouncer-process-approvals ()
|
||||
"Scans for APPROVED flight plans and re-injects them."
|
||||
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED)
|
||||
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED"))
|
||||
(found-any nil))
|
||||
(dolist (node approved-nodes)
|
||||
(let* ((attrs (org-object-attributes node))
|
||||
@@ -102,7 +102,7 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
(when action
|
||||
(setf (getf action :approved) t)
|
||||
(inject-stimulus action)
|
||||
(setf (getf (org-object-attributes node) :TODO) "DONE
|
||||
(setf (getf (org-object-attributes node) :TODO) "DONE")
|
||||
(setq found-any t))))))
|
||||
found-any))
|
||||
#+end_src
|
||||
@@ -115,9 +115,9 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
||||
(harness-log "BOUNCER: Creating flight plan node '~a'..." id)
|
||||
(list :type :REQUEST :target :emacs
|
||||
:payload (list :action :insert-node :id id
|
||||
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
||||
:TODO "PLAN" :TAGS '("FLIGHT_PLAN
|
||||
:ACTION (format nil "~s" blocked-action))))))
|
||||
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
||||
:TODO "PLAN" :TAGS '("FLIGHT_PLAN")
|
||||
:ACTION (format nil "~s" blocked-action))))))
|
||||
#+end_src
|
||||
|
||||
** Gate Logic (bouncer-deterministic-gate)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-config-manager.lisp
|
||||
|
||||
* Overview
|
||||
The *Config Manager* skill provides the OpenCortex Agent with the capability to manage its own environment variables and provider configurations.
|
||||
The *Config Manager* skill provides the OpenCortex Agent with the capability to manage its own environment variables and provider configurations. It includes an interactive setup wizard for LLM providers, gateways, and system settings.
|
||||
|
||||
* Implementation
|
||||
|
||||
@@ -13,7 +13,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Configuration Logic
|
||||
** Configuration Paths
|
||||
#+begin_src lisp
|
||||
(defun get-oc-config-dir ()
|
||||
"Returns the absolute path to the opencortex config directory."
|
||||
@@ -22,19 +22,267 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(uiop:ensure-directory-pathname xdg)
|
||||
(uiop:ensure-directory-pathname (merge-pathnames ".config/opencortex/" (user-homedir-pathname))))))
|
||||
|
||||
(defun save-providers ()
|
||||
"Stubs for saving provider configuration."
|
||||
(harness-log "CONFIG: Providers saved."))
|
||||
(defun get-config-file ()
|
||||
"Returns the path to the .env config file."
|
||||
(merge-pathnames ".env" (get-oc-config-dir)))
|
||||
|
||||
(defun configure-provider (id)
|
||||
"Stubs for configuring a provider."
|
||||
(harness-log "CONFIG: Configured provider ~a" id))
|
||||
(defun ensure-config-dir ()
|
||||
"Ensures the config directory exists."
|
||||
(let ((dir (get-oc-config-dir)))
|
||||
(unless (uiop:directory-exists-p dir)
|
||||
(uiop:ensure-directory-pathname dir))
|
||||
dir))
|
||||
#+end_src
|
||||
|
||||
** Config File Operations
|
||||
#+begin_src lisp
|
||||
(defun read-config-file ()
|
||||
"Reads the .env config file and returns an alist of KEY=VALUE pairs."
|
||||
(let ((config-file (get-config-file)))
|
||||
(when (uiop:file-exists-p config-file)
|
||||
(let ((lines (uiop:read-file-lines config-file))
|
||||
(result nil))
|
||||
(dolist (line lines)
|
||||
(when (and line (> (length line) 0)
|
||||
(not (uiop:string-prefix-p "#" line)))
|
||||
(let ((eq-pos (position #\= line)))
|
||||
(when eq-pos
|
||||
(let ((key (string-trim " " (subseq line 0 eq-pos)))
|
||||
(value (string-trim " " (subseq line (1+ eq-pos)))))
|
||||
(push (cons key value) result))))))
|
||||
(nreverse result)))))
|
||||
|
||||
(defun write-config-file (config-alist)
|
||||
"Writes the config alist to the .env file."
|
||||
(ensure-config-dir)
|
||||
(let ((config-file (get-config-file)))
|
||||
(with-open-file (stream config-file :direction :output :if-exists :supersede :if-does-not-exist :create)
|
||||
(format stream "# OpenCortex Configuration~%")
|
||||
(format stream "# Generated by opencortex setup~%~%")
|
||||
(dolist (pair config-alist)
|
||||
(format stream "~a=~a~%" (car pair) (cdr pair))))))
|
||||
|
||||
(defun get-config-value (key)
|
||||
"Gets a config value by key."
|
||||
(let ((config (read-config-file)))
|
||||
(cdr (assoc key config :test #'string=))))
|
||||
|
||||
(defun set-config-value (key value)
|
||||
"Sets a config value and saves to file."
|
||||
(let ((config (read-config-file))
|
||||
(pair (cons key value)))
|
||||
(let ((existing (assoc key config :test #'string=)))
|
||||
(if existing
|
||||
(setf (cdr existing) value)
|
||||
(push pair config)))
|
||||
(write-config-file config))))
|
||||
#+end_src
|
||||
|
||||
** Input Utilities
|
||||
#+begin_src lisp
|
||||
(defun prompt (prompt-text)
|
||||
"Simple prompt that returns user input as a string."
|
||||
(format t "~a" prompt-text)
|
||||
(finish-output)
|
||||
(read-line))
|
||||
|
||||
(defun prompt-yes-no (prompt-text)
|
||||
"Prompts yes/no question. Returns T for yes, nil for no."
|
||||
(let ((response (prompt (format nil "~a [Y/n]: " prompt-text))))
|
||||
(or (string= response "")
|
||||
(string-equal response "Y")
|
||||
(string-equal response "y")
|
||||
(string-equal response "yes"))))
|
||||
|
||||
(defun prompt-choice (prompt-text options)
|
||||
"Prompts user to choose from a list of options. Returns the chosen option or nil."
|
||||
(format t "~a~%" prompt-text)
|
||||
(let ((i 1))
|
||||
(dolist (opt options)
|
||||
(format t " ~a) ~a~%" i opt)
|
||||
(incf i)))
|
||||
(let ((response (prompt "Choice")))
|
||||
(let ((num (ignore-errors (parse-integer response))))
|
||||
(when (and num (<= 1 num) (>= (length options) num))
|
||||
(nth (1- num) options)))))
|
||||
#+end_src
|
||||
|
||||
** LLM Provider Setup
|
||||
#+begin_src lisp
|
||||
(defvar *available-providers*
|
||||
'(("OpenAI" . "OPENAI_API_KEY")
|
||||
("Anthropic" . "ANTHROPIC_API_KEY")
|
||||
("OpenRouter" . "OPENROUTER_API_KEY")
|
||||
("Groq" . "GROQ_API_KEY")
|
||||
("Gemini" . "GEMINI_API_KEY")
|
||||
("Ollama (local)" . "OLLAMA_URL")))
|
||||
|
||||
(defun setup-llm-providers ()
|
||||
"Interactive wizard for configuring LLM providers."
|
||||
(format t "~%~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " LLM Provider Configuration~%")
|
||||
(format t "==================================================~%~%")
|
||||
|
||||
(let ((current-providers (loop for (name . key) in *available-providers*
|
||||
when (get-config-value key)
|
||||
collect name)))
|
||||
(when current-providers
|
||||
(format t "Current providers: ~{~a~^, ~}~%~%" current-providers))
|
||||
|
||||
(format t "Available providers:~%")
|
||||
(dolist (p *available-providers*)
|
||||
(format t " - ~a~%" (car p)))
|
||||
(format t "~%")
|
||||
|
||||
(when (prompt-yes-no "Configure a new provider?")
|
||||
(let ((chosen (prompt-choice "Select provider:" (mapcar #'car *available-providers*))))
|
||||
(when chosen
|
||||
(let ((env-key (cdr (assoc chosen *available-providers* :test #'string= :key #'car))))
|
||||
(if (string= chosen "Ollama (local)")
|
||||
(progn
|
||||
(format t "Enter Ollama URL (e.g., http://localhost:11434): ")
|
||||
(let ((url (read-line)))
|
||||
(set-config-value env-key url)
|
||||
(format t "✓ Ollama configured at ~a~%" url))))
|
||||
(progn
|
||||
(format t "Enter API key for ~a: " chosen)
|
||||
(let ((key (read-line)))
|
||||
(set-config-value env-key key)
|
||||
(format t "✓ ~a API key saved~%" chosen)))))))))))
|
||||
|
||||
(format t "~%"))
|
||||
|
||||
(defun setup-add-provider ()
|
||||
"Entry point for adding a single provider (called from CLI)."
|
||||
(setup-llm-providers))
|
||||
#+end_src
|
||||
|
||||
** Gateway Setup
|
||||
#+begin_src lisp
|
||||
(defun setup-gateways ()
|
||||
"Interactive wizard for configuring external gateways."
|
||||
(format t "~%~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " Gateway Configuration~%")
|
||||
(format t "==================================================~%~%")
|
||||
|
||||
(format t "Available gateways:~%")
|
||||
(format t " - Slack (https://api.slack.com/)~%")
|
||||
(format t " - Discord (https://discord.com/developers/)~%")
|
||||
(format t "~%")
|
||||
|
||||
(when (prompt-yes-no "Configure a gateway?")
|
||||
(let ((chosen (prompt-choice "Select platform:" '("Slack" "Discord"))))
|
||||
(when chosen
|
||||
(let ((token (prompt (format nil "Enter ~a bot token: " chosen))))
|
||||
(if (string= chosen "Slack")
|
||||
(set-config-value "SLACK_TOKEN" token)
|
||||
(set-config-value "DISCORD_TOKEN" token))
|
||||
(format t "✓ ~a gateway configured~%" chosen))))))
|
||||
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
** Skill Management
|
||||
#+begin_src lisp
|
||||
(defun setup-skills ()
|
||||
"Interactive wizard for enabling/disabling skills."
|
||||
(format t "~%~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " Skill Management~%")
|
||||
(format t "==================================================~%~%")
|
||||
|
||||
(format t "Note: Skill management is not yet implemented.~%")
|
||||
(format t "Skills are automatically loaded from ~a~%" (or (uiop:getenv "SKILLS_DIR") "default location"))
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
** Memory Settings
|
||||
#+begin_src lisp
|
||||
(defun setup-memory ()
|
||||
"Interactive wizard for memory settings."
|
||||
(format t "~%~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " Memory Settings~%")
|
||||
(format t "==================================================~%~%")
|
||||
|
||||
(let ((auto-save (prompt "Auto-save interval in seconds [300]:")))
|
||||
(when (and auto-save (> (length auto-save) 0))
|
||||
(set-config-value "MEMORY_AUTO_SAVE_INTERVAL" auto-save)))
|
||||
|
||||
(let ((history (prompt "History retention in lines [1000]:")))
|
||||
(when (and history (> (length history) 0))
|
||||
(set-config-value "MEMORY_HISTORY_RETENTION" history)))
|
||||
|
||||
(format t "✓ Memory settings saved~%")
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
** Network Settings
|
||||
#+begin_src lisp
|
||||
(defun setup-network ()
|
||||
"Interactive wizard for network settings."
|
||||
(format t "~%~%")
|
||||
(format t "==================================================~%")
|
||||
(format t " Network Settings~%")
|
||||
(format t "==================================================~%~%")
|
||||
|
||||
(let ((timeout (prompt "Request timeout in seconds [30]:")))
|
||||
(when (and timeout (> (length timeout) 0))
|
||||
(set-config-value "REQUEST_TIMEOUT" timeout)))
|
||||
|
||||
(let ((proxy (prompt "Proxy URL (leave empty for none) []:")))
|
||||
(when (and proxy (> (length proxy) 0))
|
||||
(set-config-value "HTTP_PROXY" proxy)))
|
||||
|
||||
(format t "✓ Network settings saved~%")
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
** Main Setup Wizard
|
||||
#+begin_src lisp
|
||||
(defun run-setup-wizard ()
|
||||
"Interactive setup wizard for OpenCortex."
|
||||
(format t "--- OpenCortex Setup Wizard ---~%")
|
||||
(save-providers)
|
||||
(doctor-main))
|
||||
"Main entry point for the interactive setup wizard."
|
||||
(format t "~%~%")
|
||||
(format t "╔═══════════════════════════════════════════════════╗~%")
|
||||
(format t "║ OpenCortex Setup Wizard ║~%")
|
||||
(format t "╚═══════════════════════════════════════════════════╝~%")
|
||||
(format t "~%")
|
||||
(format t "This wizard will help you configure:~%")
|
||||
(format t " 1. LLM Providers (OpenAI, Anthropic, etc.)~%")
|
||||
(format t " 2. Gateway Links (Slack, Discord)~%")
|
||||
(format t " 3. Memory Settings~%")
|
||||
(format t " 4. Network Settings~%")
|
||||
(format t "~%")
|
||||
|
||||
(ensure-config-dir)
|
||||
|
||||
;; Step 1: LLM Providers
|
||||
(when (prompt-yes-no "Configure LLM providers?")
|
||||
(setup-llm-providers))
|
||||
|
||||
;; Step 2: Gateways
|
||||
(when (prompt-yes-no "Configure gateways?")
|
||||
(setup-gateways))
|
||||
|
||||
;; Step 3: Memory
|
||||
(when (prompt-yes-no "Configure memory settings?")
|
||||
(setup-memory))
|
||||
|
||||
;; Step 4: Network
|
||||
(when (prompt-yes-no "Configure network settings?")
|
||||
(setup-network))
|
||||
|
||||
;; Summary
|
||||
(format t "==================================================~%")
|
||||
(format t " Setup Complete!~%")
|
||||
(format t "==================================================~%")
|
||||
(format t "~%")
|
||||
(format t "Configuration saved to: ~a~%" (get-config-file))
|
||||
(format t "~%")
|
||||
(format t "To verify your setup, run: opencortex doctor~%")
|
||||
(format t "~%"))
|
||||
#+end_src
|
||||
|
||||
** Skill Registration
|
||||
@@ -42,4 +290,4 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
||||
(defskill :skill-config-manager
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil))
|
||||
#+end_src
|
||||
#+end_src
|
||||
@@ -16,7 +16,7 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys
|
||||
** Vault Storage
|
||||
#+begin_src lisp
|
||||
(defvar *vault-memory* (make-hash-table :test 'equal)
|
||||
"In-memory cache of sensitive credentials.
|
||||
"In-memory cache of sensitive credentials.")
|
||||
#+end_src
|
||||
|
||||
** Secret Management
|
||||
@@ -28,11 +28,11 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys
|
||||
(if val
|
||||
val
|
||||
(let ((env-var (case provider
|
||||
(:gemini "GEMINI_API_KEY
|
||||
(:openai "OPENAI_API_KEY
|
||||
(:anthropic "ANTHROPIC_API_KEY
|
||||
(:openrouter "OPENROUTER_API_KEY
|
||||
(otherwise nil))))
|
||||
(:gemini "GEMINI_API_KEY")
|
||||
(:openai "OPENAI_API_KEY")
|
||||
(:anthropic "ANTHROPIC_API_KEY")
|
||||
(:openrouter "OPENROUTER_API_KEY")
|
||||
(otherwise nil))))
|
||||
(when env-var (uiop:getenv env-var))))))
|
||||
|
||||
(defun vault-set-secret (provider secret &key (type :api-key))
|
||||
|
||||
@@ -4,64 +4,249 @@
|
||||
#+PROPERTY: header-args:lisp :tangle org-skill-diagnostics.lisp
|
||||
|
||||
* Overview
|
||||
The *Diagnostics Skill* (Doctor) provides system-wide health checks and dependency verification.
|
||||
The *Diagnostics Skill* (Doctor) provides system-wide health checks and dependency verification. It validates external dependencies, XDG environment, and LLM provider connectivity.
|
||||
|
||||
* Implementation
|
||||
* Phase A: Demand (Thinking)
|
||||
** Why a Doctor?
|
||||
The Doctor transforms opaque startup failures into actionable engineering reports. It ensures the Brain never attempts to boot in a compromised state.
|
||||
|
||||
** Detection Invariant
|
||||
Binary detection must use shell probing (`which`) to account for varying `$PATH` inheritance between interactive and headless sessions.
|
||||
|
||||
* Phase B: Protocol (Success Criteria)
|
||||
- Dependency check passes when all required binaries are found
|
||||
- Environment check passes when XDG directories exist and are accessible
|
||||
- LLM check passes when at least one provider is configured or Ollama is running locally
|
||||
|
||||
* Phase C: Implementation (Build)
|
||||
|
||||
** Package Context
|
||||
#+begin_src lisp
|
||||
(in-package :opencortex)
|
||||
#+end_src
|
||||
|
||||
** Dependency Check (doctor-check-dependencies)
|
||||
** Global Configuration
|
||||
#+begin_src lisp
|
||||
(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc")
|
||||
"List of external binaries required for full system operation.")
|
||||
|
||||
(defvar *doctor-package-map*
|
||||
'(("sbcl" . "sbcl")
|
||||
("emacs" . "emacs")
|
||||
("git" . "git")
|
||||
("socat" . "socat")
|
||||
("nc" . "netcat-openbsd")
|
||||
("curl" . "curl")
|
||||
("rlwrap" . "rlwrap"))
|
||||
"Map binary names to apt package names.")
|
||||
|
||||
(defvar *doctor-missing-deps* nil
|
||||
"List of missing dependencies populated by doctor-check-dependencies.")
|
||||
|
||||
(defvar *doctor-auto-install* t
|
||||
"When T, doctor will attempt to install missing dependencies automatically.")
|
||||
#+end_src
|
||||
|
||||
** Dependency Verification
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-dependencies ()
|
||||
"Verifies that all required external binaries are available."
|
||||
(let ((deps '("sbcl" "emacs" "git" "curl" "nc"))
|
||||
(all-ok t))
|
||||
(format t "DOCTOR: Checking System Dependencies...~%")
|
||||
(dolist (dep deps)
|
||||
(if (uiop:run-program (list "which" dep) :ignore-error-status t)
|
||||
(format t " [OK] Found ~a~%" dep)
|
||||
(progn
|
||||
(format t " [FAIL] Missing ~a~%" dep)
|
||||
(setf all-ok nil))))
|
||||
"Verifies that required external binaries are available in the PATH via shell probe."
|
||||
(setf *doctor-missing-deps* nil)
|
||||
(let ((all-ok t))
|
||||
(format t "DOCTOR: Checking system dependencies...~%")
|
||||
(dolist (dep *doctor-required-binaries*)
|
||||
(let ((path (ignore-errors
|
||||
(uiop:run-program (list "which" dep)
|
||||
:output :string :ignore-error-status t))))
|
||||
(if (and path (> (length path) 0))
|
||||
(format t " [OK] Found ~a~%" dep)
|
||||
(progn
|
||||
(format t " [FAIL] Missing binary: ~a~%" dep)
|
||||
(push dep *doctor-missing-deps*)
|
||||
(setf all-ok nil)))))
|
||||
(when (and all-ok (null *doctor-missing-deps*))
|
||||
(format t "DOCTOR: All dependencies satisfied.~%"))
|
||||
all-ok))
|
||||
#+end_src
|
||||
|
||||
** XDG Check (doctor-check-xdg)
|
||||
** Auto-Install Dependencies
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-xdg ()
|
||||
"Verifies XDG environment variables and directory structure."
|
||||
(format t "DOCTOR: Checking XDG environment...~%")
|
||||
(let ((vars '("OC_CONFIG_DIR" "OC_DATA_DIR" "OC_STATE_DIR" "MEMEX_DIR")))
|
||||
(dolist (var vars)
|
||||
(let ((val (uiop:getenv var)))
|
||||
(if val
|
||||
(format t " [OK] ~a: ~a~%" var val)
|
||||
(format t " [WARN] ~a is not set.~%" var)))))
|
||||
t)
|
||||
(defun doctor-install-dependencies ()
|
||||
"Attempts to install missing system dependencies via apt."
|
||||
(when (null *doctor-missing-deps*)
|
||||
(format t "DOCTOR: No missing dependencies to install.~%")
|
||||
(return-from doctor-install-dependencies t))
|
||||
|
||||
(format t "DOCTOR: Attempting to install ~a missing dependencies...~%" (length *doctor-missing-deps*))
|
||||
|
||||
(let ((packages (remove-duplicates
|
||||
(mapcar (lambda (dep)
|
||||
(or (cdr (assoc dep *doctor-package-map* :test #'string=))
|
||||
dep))
|
||||
*doctor-missing-deps*)
|
||||
:test #'string=)))
|
||||
(format t "DOCTOR: Packages to install: ~a~%" packages)
|
||||
|
||||
(let ((cmd (format nil "apt-get install -y ~{~a~^ ~}" packages)))
|
||||
(format t "DOCTOR: Running: ~a~%" cmd)
|
||||
(handler-case
|
||||
(let ((output (uiop:run-program cmd
|
||||
:output :string
|
||||
:error-output :string
|
||||
:external-format :utf-8)))
|
||||
(if (zerop (uiop:run-program (format nil "which ~a" (car *doctor-missing-deps*))
|
||||
:ignore-error-status t))
|
||||
(progn
|
||||
(format t "DOCTOR: Dependencies installed successfully.~%")
|
||||
(setf *doctor-missing-deps* nil)
|
||||
t)
|
||||
(progn
|
||||
(format t "DOCTOR: Installation failed. Output: ~a~%" output)
|
||||
nil)))
|
||||
(error (c)
|
||||
(format t "DOCTOR: Installation error: ~a~%" c)
|
||||
nil)))))
|
||||
#+end_src
|
||||
|
||||
** Main Diagnostic (doctor-main)
|
||||
** XDG Environment Validation
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-env ()
|
||||
"Validates XDG directories and environment configuration."
|
||||
(format t "DOCTOR: Checking XDG environment...~%")
|
||||
(let ((all-ok t)
|
||||
(config-dir (uiop:getenv "OC_CONFIG_DIR"))
|
||||
(data-dir (uiop:getenv "OC_DATA_DIR"))
|
||||
(state-dir (uiop:getenv "OC_STATE_DIR"))
|
||||
(memex-dir (uiop:getenv "MEMEX_DIR")))
|
||||
|
||||
(flet ((check-dir (name path critical)
|
||||
(if (and path (> (length path) 0))
|
||||
(if (uiop:directory-exists-p path)
|
||||
(format t " [OK] ~a: ~a~%" name path)
|
||||
(progn
|
||||
(format t " [FAIL] ~a directory missing: ~a~%" name path)
|
||||
(when critical (setf all-ok nil))))
|
||||
(progn
|
||||
(format t " [FAIL] ~a variable not set.~%" name)
|
||||
(when critical (setf all-ok nil))))))
|
||||
|
||||
(check-dir "Config (OC_CONFIG_DIR)" config-dir t)
|
||||
(check-dir "Data (OC_DATA_DIR)" data-dir t)
|
||||
(check-dir "State (OC_STATE_DIR)" state-dir t)
|
||||
(check-dir "Memex (MEMEX_DIR)" memex-dir t))
|
||||
all-ok))
|
||||
#+end_src
|
||||
|
||||
** LLM Connectivity
|
||||
The doctor checks all supported LLM providers and detects local Ollama instances.
|
||||
|
||||
#+begin_src lisp
|
||||
(defun doctor-check-llm ()
|
||||
"Tests connectivity to LLM providers. Returns T if at least one provider is configured."
|
||||
(format t "DOCTOR: Checking LLM connectivity...~%")
|
||||
(let ((providers '((:openrouter . "OPENROUTER_API_KEY")
|
||||
(:anthropic . "ANTHROPIC_API_KEY")
|
||||
(:openai . "OPENAI_API_KEY")
|
||||
(:groq . "GROQ_API_KEY")
|
||||
(:gemini . "GEMINI_API_KEY")
|
||||
(:ollama . "OLLAMA_URL")))
|
||||
(configured nil))
|
||||
(dolist (p providers)
|
||||
(let ((env-val (uiop:getenv (cdr p))))
|
||||
(cond
|
||||
((and env-val (> (length env-val) 0))
|
||||
(format t " [OK] ~a configured~%" (car p))
|
||||
(setf configured t))
|
||||
((eq (car p) :ollama)
|
||||
(let ((ollama-check (ignore-errors
|
||||
(uiop:run-program '("curl" "-s" "http://localhost:11434/api/tags")
|
||||
:output :string :ignore-error-status t))))
|
||||
(when (and ollama-check (search "\"models\"" ollama-check))
|
||||
(format t " [OK] Ollama local model server detected~%")
|
||||
(setf configured t)))))))
|
||||
(if configured
|
||||
(progn
|
||||
(format t " [OK] LLM provider(s) available~%")
|
||||
t)
|
||||
(progn
|
||||
(format t " [WARN] No LLM provider configured.~%")
|
||||
(format t " Run 'opencortex setup' to configure a provider.~%")
|
||||
t))))
|
||||
#+end_src
|
||||
|
||||
** Orchestration
|
||||
#+begin_src lisp
|
||||
(defun doctor-run-all (&key (auto-install t))
|
||||
"Executes the full diagnostic suite and returns T if system is healthy."
|
||||
(format t "==================================================~%")
|
||||
(format t " OPENCORTEX DOCTOR: Commencing Health Check~%")
|
||||
(format t "==================================================~%")
|
||||
(let ((dep-ok (doctor-check-dependencies)))
|
||||
(when (and (not dep-ok) auto-install *doctor-auto-install*)
|
||||
(format t "DOCTOR: Attempting automatic installation...~%")
|
||||
(setf dep-ok (doctor-install-dependencies))
|
||||
(when dep-ok
|
||||
(setf dep-ok (doctor-check-dependencies))))
|
||||
(let ((env-ok (doctor-check-env))
|
||||
(llm-ok (doctor-check-llm)))
|
||||
(format t "==================================================~%")
|
||||
(if (and dep-ok env-ok)
|
||||
(progn
|
||||
(format t " ✓ SYSTEM HEALTHY: Ready for ignition.~%")
|
||||
t) ;; Explicitly return T
|
||||
(progn
|
||||
(format t "==================================================~%")
|
||||
(format t " ISSUES FOUND:~%")
|
||||
(when (not dep-ok)
|
||||
(format t " - Missing system dependencies~%"))
|
||||
(when (not llm-ok)
|
||||
(format t " - No LLM provider configured~%"))
|
||||
(format t "~%")
|
||||
(format t " RECOMMENDED ACTIONS:~%")
|
||||
(format t " 1. Run 'opencortex setup' to configure everything~%")
|
||||
(format t " 2. Or run 'opencortex doctor --fix' for auto-repair~%")
|
||||
(format t "==================================================~%")
|
||||
nil))))) ;; Return nil when issues found
|
||||
#+end_src
|
||||
|
||||
** CLI Entry Point
|
||||
#+begin_src lisp
|
||||
(defun doctor-main ()
|
||||
"Runs all diagnostic checks."
|
||||
(format t "==================================================~%")
|
||||
(format t " OpenCortex System Diagnostic~%")
|
||||
(format t "==================================================~%")
|
||||
(let ((d-ok (doctor-check-dependencies))
|
||||
(x-ok (doctor-check-xdg)))
|
||||
(format t "==================================================~%")
|
||||
(if (and d-ok x-ok)
|
||||
(format t " ✓ SYSTEM HEALTHY: Ready for ignition.~%")
|
||||
(format t " ✗ SYSTEM UNHEALTHY: Issues detected.~%"))))
|
||||
"Entry point for the 'doctor' CLI command."
|
||||
(if (doctor-run-all)
|
||||
(uiop:quit 0)
|
||||
(uiop:quit 1)))
|
||||
#+end_src
|
||||
|
||||
* Phase D: Verification (Testing)
|
||||
|
||||
** Dependency Test
|
||||
#+begin_src lisp :tangle no
|
||||
(test test-doctor-dependency-check
|
||||
"Verify that missing binaries are correctly identified as failures."
|
||||
(let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123")))
|
||||
(is (null (opencortex:doctor-check-dependencies)))))
|
||||
#+end_src
|
||||
|
||||
** Environment Test
|
||||
#+begin_src lisp :tangle no
|
||||
(test test-doctor-env-check
|
||||
"Verify that an invalid MEMEX_DIR triggers a critical failure."
|
||||
(let ((old-m (uiop:getenv "MEMEX_DIR")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setf (uiop:getenv "MEMEX_DIR") "/non/existent/path/999")
|
||||
(is (null (opencortex:doctor-check-env))))
|
||||
(setf (uiop:getenv "MEMEX_DIR") (or old-m "")))))
|
||||
#+end_src
|
||||
|
||||
* Phase E: Lifecycle
|
||||
The doctor skill should be loaded early (priority 100) to validate system health before other skills initialize.
|
||||
|
||||
** Skill Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-diagnostics
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :heartbeat))
|
||||
:deterministic (lambda (action ctx) (declare (ignore action ctx)) nil))
|
||||
#+end_src
|
||||
#+end_src
|
||||
Reference in New Issue
Block a user