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:
@@ -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