Files
passepartout/harness/communication.org
Amr Gharbeia c0d3f066e8
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
Proactive doctor, setup wizard, and TUI fixes
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
2026-04-29 12:58:09 -04:00

6.0 KiB

Communication Protocol (communication.lisp)

Overview

The communication.lisp module defines the low-level transport and framing logic for OpenCortex stimuli.

Implementation

Package Context

(in-package :opencortex)

Actuator Registry

(defvar *actuator-registry* (make-hash-table :test 'equalp)
  "Global registry mapping target keywords to their physical actuator functions.")

(defun register-actuator (name fn) 
  "Registers an actuator function. Actuators receive: (ACTION CONTEXT)."
  (let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword))))
    (setf (gethash key *actuator-registry*) fn)))

Message Framing

(defun sanitize-protocol-message (msg)
  "Recursively strips non-serializable objects from a protocol plist."
  (if (and msg (listp msg))
      (let ((clean nil))
        (loop for (k v) on msg by #'cddr
              do (unless (member k '(:reply-stream :socket :stream))
                   (push k clean)
                   (push (if (listp v) (sanitize-protocol-message v) v) clean)))
        (nreverse clean))
      msg))

(defun frame-message (msg)
  "Serializes a message plist and prefixes it with a 6-character hex length."
  (let* ((sanitized (sanitize-protocol-message msg))
         (payload (let ((*print-pretty* nil) (*read-eval* nil)) (format nil "~s" sanitized)))
         (len (length payload)))
    (format nil "~6,'0x~a" len payload)))

(defun read-framed-message (stream)
  "Reads a hex-length prefixed S-expression from the stream securely."
  (let ((length-buffer (make-string 6)))
    (handler-case
        (progn
          (loop for char = (peek-char nil stream nil :eof)
                while (and (not (eq char :eof)) (member char '(#\Space #\Newline #\Tab #\Return)))
                do (read-char stream))
          (let ((count (read-sequence length-buffer stream)))
            (if (< count 6)
                :eof
                (let ((len (ignore-errors (parse-integer length-buffer :radix 16))))
                  (if (not len)
                      :error
                      (let ((msg-buffer (make-string len)))
                        (read-sequence msg-buffer stream)
                        (let ((*read-eval* nil))
                          (handler-case (read-from-string msg-buffer)
                            (error () :error)))))))))
      (error () :error))))

Server Listener (start-daemon)

(defvar *server-socket* nil)

(defun handle-client-connection (socket)
  "Handles a single TUI/CLI client connection in a dedicated thread."
  (let ((stream (usocket:socket-stream socket)))
    (handler-case
        (progn
          (format stream "~a" (frame-message (make-hello-message "0.2.0")))
          (finish-output stream)
          (loop
            (let ((msg (read-framed-message stream)))
              (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))))

(defun start-daemon (&key (port 9105))
  "Starts the network listener for TUI/CLI clients."
  (setf *server-socket* (usocket:socket-listen "127.0.0.1" port :reuse-address t))
  (harness-log "DAEMON: Listening on localhost:~a" port)
  (bt:make-thread
   (lambda ()
     (loop
       (let ((client-socket (usocket:socket-accept *server-socket*)))
         (when client-socket
           (bt:make-thread (lambda () (handle-client-connection client-socket))
                          :name "opencortex-client-handler")))))
   :name "opencortex-server-listener"))

Handshake Logic

(defun make-hello-message (version)
  "Constructs the standard HELLO handshake message."
  (list :TYPE :EVENT 
        :PAYLOAD (list :ACTION :handshake 
                       :VERSION version 
                       :CAPABILITIES '(:AUTH :ORG-AST))))

Structural Validation

(in-package :opencortex)

(defun validate-communication-protocol-schema (msg)
  "Strict structural validation for incoming protocol messages."
  (unless (listp msg) (error "Message must be a plist"))
  (let ((type (proto-get msg :type)))
    (unless (member type '(:REQUEST :EVENT :RESPONSE :LOG :STATUS))
      (error "Invalid message type '~a'" type))
    t))

Test Suite

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :fiveam :silent t))

(defpackage :opencortex-communication-tests
  (:use :cl :fiveam :opencortex)
  (:export #:communication-protocol-suite))
(in-package :opencortex-communication-tests)

(def-suite communication-protocol-suite :description "Communication Protocol Suite")
(in-suite communication-protocol-suite)

(test test-framing
  (let* ((msg '(:type :EVENT :payload (:action :handshake)))
         (framed (frame-message msg)))
    (is (string= "00002C" (string-upcase (subseq framed 0 6))))))