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
|
(cond
|
||||||
((eq msg :eof) (return))
|
((eq msg :eof) (return))
|
||||||
((eq msg :error) (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))))))
|
(t (inject-stimulus msg :stream stream))))))
|
||||||
(error (c) (harness-log "CLIENT ERROR: ~a" c)))
|
(error (c) (harness-log "CLIENT ERROR: ~a" c)))
|
||||||
(ignore-errors (usocket:socket-close socket))))
|
(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)
|
(defvar *shutdown-save-enabled* t)
|
||||||
#+end_src
|
#+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)
|
** Main Entry Point (main)
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun main ()
|
(defun main ()
|
||||||
@@ -108,16 +146,20 @@ The Metabolic Loop is the fundamental rhythm of OpenCortex: the continuous proce
|
|||||||
(load-memory-from-disk)
|
(load-memory-from-disk)
|
||||||
(initialize-actuators)
|
(initialize-actuators)
|
||||||
(initialize-all-skills)
|
(initialize-all-skills)
|
||||||
|
|
||||||
|
;; Run proactive doctor before starting services
|
||||||
|
(run-startup-health-check)
|
||||||
|
|
||||||
(start-heartbeat)
|
(start-heartbeat)
|
||||||
(start-daemon)
|
(start-daemon)
|
||||||
|
|
||||||
#+sbcl
|
#+sbcl
|
||||||
(sb-sys:enable-interrupt sb-unix:sigint
|
(sb-sys:enable-interrupt sb-unix:sigint
|
||||||
(lambda (sig code scp)
|
(lambda (sig code scp)
|
||||||
(declare (ignore sig code scp))
|
(declare (ignore sig code scp))
|
||||||
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
(harness-log "SHUTDOWN: SIGINT received. Saving memory...")
|
||||||
(when *shutdown-save-enabled* (save-memory-to-disk))
|
(when *shutdown-save-enabled* (save-memory-to-disk))
|
||||||
(uiop:quit 0)))
|
(uiop:quit 0)))
|
||||||
|
|
||||||
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
|
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
|
||||||
(loop
|
(loop
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
|||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(in-package :cl-user)
|
(in-package :cl-user)
|
||||||
(defpackage :opencortex.tui
|
(defpackage :opencortex.tui
|
||||||
(:use :cl :croatoan)
|
(:use :cl :croatoan :usocket)
|
||||||
(:export :main))
|
(:export :main))
|
||||||
(in-package :opencortex.tui)
|
(in-package :opencortex.tui)
|
||||||
#+end_src
|
#+end_src
|
||||||
@@ -108,13 +108,18 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
|||||||
(when (> (length cmd) 0)
|
(when (> (length cmd) 0)
|
||||||
(enqueue-msg (format nil "⬆ ~a" cmd))
|
(enqueue-msg (format nil "⬆ ~a" cmd))
|
||||||
(handler-case
|
(handler-case
|
||||||
(when (and stream (open-stream-p stream))
|
(progn
|
||||||
(format stream "~a" (opencortex:frame-message (list :TYPE :EVENT
|
(when (and stream (open-stream-p stream))
|
||||||
:META (list :SOURCE :tui)
|
(let* ((msg (list :TYPE :EVENT
|
||||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd))))
|
:META (list :SOURCE :tui)
|
||||||
(finish-output stream))
|
: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)
|
(error (c)
|
||||||
(declare (ignore c))
|
(format t "Send error: ~a~%" c)
|
||||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||||
(setf *is-running* nil))))
|
(setf *is-running* nil))))
|
||||||
(when (string= cmd "/exit") (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"
|
source "$OC_CONFIG_DIR/.env"
|
||||||
fi
|
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 ---
|
# --- 2. SETUP ---
|
||||||
setup_system() {
|
setup_system() {
|
||||||
NON_INTERACTIVE=false
|
NON_INTERACTIVE=false
|
||||||
@@ -61,24 +87,31 @@ setup_system() {
|
|||||||
cp "$SCRIPT_DIR/harness"/*.org "$OC_DATA_DIR/harness/"
|
cp "$SCRIPT_DIR/harness"/*.org "$OC_DATA_DIR/harness/"
|
||||||
cp "$SCRIPT_DIR/skills"/*.org "$OC_DATA_DIR/skills/"
|
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"
|
export INSTALL_DIR="$OC_DATA_DIR"
|
||||||
|
|
||||||
# Critical: Tangle manifest first to establish system structure (into root)
|
# Critical: Tangle manifest first to establish system structure (into root)
|
||||||
echo "Tangling harness/manifest.org..."
|
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/
|
# Tangle harness files into harness/
|
||||||
for f in harness/*.org; do
|
for f in "$SCRIPT_DIR/harness"/*.org; do
|
||||||
if [ "$f" != "harness/manifest.org" ]; then
|
fname=$(basename "$f" .org)
|
||||||
echo "Tangling $f..."
|
if [ "$fname" != "manifest" ]; then
|
||||||
(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
|
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
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Tangle skill files into skills/
|
# Tangle skill files into skills/
|
||||||
for f in skills/*.org; do
|
for f in "$SCRIPT_DIR/skills"/*.org; do
|
||||||
echo "Tangling $f..."
|
fname=$(basename "$f" .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/$f\")" >/dev/null 2>&1) || true
|
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
|
done
|
||||||
|
|
||||||
# Special handling for tests that need to go into tests/
|
# 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
|
# 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/"
|
[ -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
|
cd "$SCRIPT_DIR" # Create the bin shim
|
||||||
echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}"
|
echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}"
|
||||||
ln -sf "$SCRIPT_DIR/opencortex.sh" "$OC_BIN_DIR/opencortex"
|
ln -sf "$SCRIPT_DIR/opencortex.sh" "$OC_BIN_DIR/opencortex"
|
||||||
@@ -107,6 +145,66 @@ setup_system() {
|
|||||||
--eval '(funcall (find-symbol "RUN-SETUP-WIZARD" :opencortex))'
|
--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 ---
|
# --- 3. COMMAND ROUTER ---
|
||||||
COMMAND=$1
|
COMMAND=$1
|
||||||
[ -z "$COMMAND" ] && COMMAND="cli"
|
[ -z "$COMMAND" ] && COMMAND="cli"
|
||||||
@@ -116,23 +214,82 @@ case "$COMMAND" in
|
|||||||
link)
|
link)
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
TOKEN=$2
|
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\")"
|
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)
|
doctor)
|
||||||
exec sbcl --non-interactive \
|
check_dependencies
|
||||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
if [ "$1" = "--watch" ]; then
|
||||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
echo "Starting background health monitor (60s interval)..."
|
||||||
--eval '(ql:quickload :opencortex)' \
|
echo "Press Ctrl+C to stop."
|
||||||
--eval '(opencortex:initialize-all-skills)' \
|
echo ""
|
||||||
--eval '(funcall (find-symbol "DOCTOR-MAIN" :opencortex))'
|
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)
|
||||||
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)
|
boot|--boot)
|
||||||
|
check_dependencies
|
||||||
exec sbcl --non-interactive \
|
exec sbcl --non-interactive \
|
||||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||||
@@ -140,16 +297,71 @@ case "$COMMAND" in
|
|||||||
--eval '(opencortex:main)'
|
--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)
|
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 '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
||||||
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
--eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \
|
||||||
--eval '(ql:quickload :opencortex/tui)' \
|
--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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
|||||||
** Security Configuration
|
** Security Configuration
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defvar *bouncer-network-whitelist*
|
(defvar *bouncer-network-whitelist*
|
||||||
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com
|
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com")
|
||||||
"Domains that the Bouncer considers safe for outbound connections.
|
"Domains that the Bouncer considers safe for outbound connections.")
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Secret Scanning (bouncer-scan-secrets)
|
** 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))
|
(let* ((target (proto-get action :target))
|
||||||
(payload (proto-get action :payload))
|
(payload (proto-get action :payload))
|
||||||
(text (or (proto-get payload :text) (proto-get action :text)))
|
(text (or (proto-get payload :text) (proto-get action :text)))
|
||||||
(cmd (or (proto-get payload :cmd)
|
(cmd (or (proto-get payload :cmd)
|
||||||
(when (and (eq target :tool) (equal (proto-get payload :tool) "shell)
|
(when (and (eq target :tool) (equal (proto-get payload :tool) "shell"))
|
||||||
(proto-get (proto-get payload :args) :cmd))))
|
(proto-get (proto-get payload :args) :cmd)))))
|
||||||
(approved (proto-get action :approved)))
|
(approved (proto-get action :approved)))
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
@@ -71,15 +71,15 @@ The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces op
|
|||||||
:payload (list :level :error
|
:payload (list :level :error
|
||||||
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
|
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
|
||||||
|
|
||||||
((and (or (eq target :shell)
|
((and (or (eq target :shell)
|
||||||
(and (eq target :tool) (equal (proto-get payload :tool) "shell))
|
(and (eq target :tool) (equal (proto-get payload :tool) "shell")))
|
||||||
(bouncer-check-network-exfil cmd))
|
(bouncer-check-network-exfil cmd))
|
||||||
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.
|
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval."))
|
||||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||||
|
|
||||||
((or (member target '(:shell))
|
((or (member target '(:shell))
|
||||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file :test #'string=))
|
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
||||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval)))
|
(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))
|
(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)))
|
(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
|
#+begin_src lisp
|
||||||
(defun bouncer-process-approvals ()
|
(defun bouncer-process-approvals ()
|
||||||
"Scans for APPROVED flight plans and re-injects them."
|
"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))
|
(found-any nil))
|
||||||
(dolist (node approved-nodes)
|
(dolist (node approved-nodes)
|
||||||
(let* ((attrs (org-object-attributes node))
|
(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
|
(when action
|
||||||
(setf (getf action :approved) t)
|
(setf (getf action :approved) t)
|
||||||
(inject-stimulus action)
|
(inject-stimulus action)
|
||||||
(setf (getf (org-object-attributes node) :TODO) "DONE
|
(setf (getf (org-object-attributes node) :TODO) "DONE")
|
||||||
(setq found-any t))))))
|
(setq found-any t))))))
|
||||||
found-any))
|
found-any))
|
||||||
#+end_src
|
#+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)
|
(harness-log "BOUNCER: Creating flight plan node '~a'..." id)
|
||||||
(list :type :REQUEST :target :emacs
|
(list :type :REQUEST :target :emacs
|
||||||
:payload (list :action :insert-node :id id
|
:payload (list :action :insert-node :id id
|
||||||
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
:attributes (list :TITLE "Flight Plan: High-Risk Action"
|
||||||
:TODO "PLAN" :TAGS '("FLIGHT_PLAN
|
:TODO "PLAN" :TAGS '("FLIGHT_PLAN")
|
||||||
:ACTION (format nil "~s" blocked-action))))))
|
:ACTION (format nil "~s" blocked-action))))))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Gate Logic (bouncer-deterministic-gate)
|
** Gate Logic (bouncer-deterministic-gate)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#+PROPERTY: header-args:lisp :tangle org-skill-config-manager.lisp
|
#+PROPERTY: header-args:lisp :tangle org-skill-config-manager.lisp
|
||||||
|
|
||||||
* Overview
|
* 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
|
* Implementation
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ The *Config Manager* skill provides the OpenCortex Agent with the capability to
|
|||||||
(in-package :opencortex)
|
(in-package :opencortex)
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Configuration Logic
|
** Configuration Paths
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun get-oc-config-dir ()
|
(defun get-oc-config-dir ()
|
||||||
"Returns the absolute path to the opencortex config directory."
|
"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 xdg)
|
||||||
(uiop:ensure-directory-pathname (merge-pathnames ".config/opencortex/" (user-homedir-pathname))))))
|
(uiop:ensure-directory-pathname (merge-pathnames ".config/opencortex/" (user-homedir-pathname))))))
|
||||||
|
|
||||||
(defun save-providers ()
|
(defun get-config-file ()
|
||||||
"Stubs for saving provider configuration."
|
"Returns the path to the .env config file."
|
||||||
(harness-log "CONFIG: Providers saved."))
|
(merge-pathnames ".env" (get-oc-config-dir)))
|
||||||
|
|
||||||
(defun configure-provider (id)
|
(defun ensure-config-dir ()
|
||||||
"Stubs for configuring a provider."
|
"Ensures the config directory exists."
|
||||||
(harness-log "CONFIG: Configured provider ~a" id))
|
(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 ()
|
(defun run-setup-wizard ()
|
||||||
"Interactive setup wizard for OpenCortex."
|
"Main entry point for the interactive setup wizard."
|
||||||
(format t "--- OpenCortex Setup Wizard ---~%")
|
(format t "~%~%")
|
||||||
(save-providers)
|
(format t "╔═══════════════════════════════════════════════════╗~%")
|
||||||
(doctor-main))
|
(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
|
#+end_src
|
||||||
|
|
||||||
** Skill Registration
|
** Skill Registration
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys
|
|||||||
** Vault Storage
|
** Vault Storage
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defvar *vault-memory* (make-hash-table :test 'equal)
|
(defvar *vault-memory* (make-hash-table :test 'equal)
|
||||||
"In-memory cache of sensitive credentials.
|
"In-memory cache of sensitive credentials.")
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Secret Management
|
** Secret Management
|
||||||
@@ -28,11 +28,11 @@ The *Credentials Vault* provides secure in-memory storage for sensitive API keys
|
|||||||
(if val
|
(if val
|
||||||
val
|
val
|
||||||
(let ((env-var (case provider
|
(let ((env-var (case provider
|
||||||
(:gemini "GEMINI_API_KEY
|
(:gemini "GEMINI_API_KEY")
|
||||||
(:openai "OPENAI_API_KEY
|
(:openai "OPENAI_API_KEY")
|
||||||
(:anthropic "ANTHROPIC_API_KEY
|
(:anthropic "ANTHROPIC_API_KEY")
|
||||||
(:openrouter "OPENROUTER_API_KEY
|
(:openrouter "OPENROUTER_API_KEY")
|
||||||
(otherwise nil))))
|
(otherwise nil))))
|
||||||
(when env-var (uiop:getenv env-var))))))
|
(when env-var (uiop:getenv env-var))))))
|
||||||
|
|
||||||
(defun vault-set-secret (provider secret &key (type :api-key))
|
(defun vault-set-secret (provider secret &key (type :api-key))
|
||||||
|
|||||||
@@ -4,60 +4,245 @@
|
|||||||
#+PROPERTY: header-args:lisp :tangle org-skill-diagnostics.lisp
|
#+PROPERTY: header-args:lisp :tangle org-skill-diagnostics.lisp
|
||||||
|
|
||||||
* Overview
|
* 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
|
** Package Context
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(in-package :opencortex)
|
(in-package :opencortex)
|
||||||
#+end_src
|
#+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
|
#+begin_src lisp
|
||||||
(defun doctor-check-dependencies ()
|
(defun doctor-check-dependencies ()
|
||||||
"Verifies that all required external binaries are available."
|
"Verifies that required external binaries are available in the PATH via shell probe."
|
||||||
(let ((deps '("sbcl" "emacs" "git" "curl" "nc"))
|
(setf *doctor-missing-deps* nil)
|
||||||
(all-ok t))
|
(let ((all-ok t))
|
||||||
(format t "DOCTOR: Checking System Dependencies...~%")
|
(format t "DOCTOR: Checking system dependencies...~%")
|
||||||
(dolist (dep deps)
|
(dolist (dep *doctor-required-binaries*)
|
||||||
(if (uiop:run-program (list "which" dep) :ignore-error-status t)
|
(let ((path (ignore-errors
|
||||||
(format t " [OK] Found ~a~%" dep)
|
(uiop:run-program (list "which" dep)
|
||||||
(progn
|
:output :string :ignore-error-status t))))
|
||||||
(format t " [FAIL] Missing ~a~%" dep)
|
(if (and path (> (length path) 0))
|
||||||
(setf all-ok nil))))
|
(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))
|
all-ok))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** XDG Check (doctor-check-xdg)
|
** Auto-Install Dependencies
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun doctor-check-xdg ()
|
(defun doctor-install-dependencies ()
|
||||||
"Verifies XDG environment variables and directory structure."
|
"Attempts to install missing system dependencies via apt."
|
||||||
(format t "DOCTOR: Checking XDG environment...~%")
|
(when (null *doctor-missing-deps*)
|
||||||
(let ((vars '("OC_CONFIG_DIR" "OC_DATA_DIR" "OC_STATE_DIR" "MEMEX_DIR")))
|
(format t "DOCTOR: No missing dependencies to install.~%")
|
||||||
(dolist (var vars)
|
(return-from doctor-install-dependencies t))
|
||||||
(let ((val (uiop:getenv var)))
|
|
||||||
(if val
|
(format t "DOCTOR: Attempting to install ~a missing dependencies...~%" (length *doctor-missing-deps*))
|
||||||
(format t " [OK] ~a: ~a~%" var val)
|
|
||||||
(format t " [WARN] ~a is not set.~%" var)))))
|
(let ((packages (remove-duplicates
|
||||||
t)
|
(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
|
#+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
|
#+begin_src lisp
|
||||||
(defun doctor-main ()
|
(defun doctor-main ()
|
||||||
"Runs all diagnostic checks."
|
"Entry point for the 'doctor' CLI command."
|
||||||
(format t "==================================================~%")
|
(if (doctor-run-all)
|
||||||
(format t " OpenCortex System Diagnostic~%")
|
(uiop:quit 0)
|
||||||
(format t "==================================================~%")
|
(uiop:quit 1)))
|
||||||
(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.~%"))))
|
|
||||||
#+end_src
|
#+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
|
** Skill Registration
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defskill :skill-diagnostics
|
(defskill :skill-diagnostics
|
||||||
|
|||||||
Reference in New Issue
Block a user