:PROPERTIES: :ID: gateway-signal-skill :CREATED: [2026-04-11 Sat 16:30] :END: #+TITLE: SKILL: Signal Gateway (Universal Literate Note) #+STARTUP: content #+FILETAGS: :gateway:signal:io:autonomy: #+DEPENDS_ON: id:credentials-vault-skill * Overview The *Signal Gateway* provides bi-directional communication between the Autonomous and the OpenCortex via the `signal-cli` tool. It features a non-blocking polling sensor and a high-integrity actuator for outbound messaging. * Phase A: Demand (PRD) :PROPERTIES: :STATUS: SIGNED :END: ** 1. Purpose Enable secure Signal communication for the OpenCortex. ** 2. Success Criteria - [ ] *Inbound:* Messages received via `signal-cli receive` are injected into the harness Bus. - [ ] *Outbound:* The `:signal` target correctly routes messages via `signal-cli send`. - [ ] *Robustness:* Handles JSON output from `signal-cli` and filters system messages. * Phase B: Blueprint (PROTOCOL) :PROPERTIES: :STATUS: SIGNED :END: ** 1. Architectural Intent Wraps the `signal-cli` binary. Polling is done in a background thread to prevent blocking the harness. ** 2. Semantic Interfaces - `(:type :EVENT :meta (:source :signal :chat-id "+1...") :payload (:sensor :user-input :text "..."))` - `(:type :REQUEST :target :signal :payload (:action :message :text "..."))` * Phase D: Build (Implementation) ** Package Context #+begin_src lisp #+end_src ** State: Signal Identity Retrieves the Signal account number from the secure vault. #+begin_src lisp (defun get-signal-account () (vault-get-secret :signal)) #+end_src ** State: Polling Thread Reference to the background thread responsible for message reception. #+begin_src lisp (defvar *signal-polling-thread* nil) #+end_src ** Actuator: sendMessage Executes the `signal-cli send` command. #+begin_src lisp (defun execute-signal-action (action context) "Sends a message via signal-cli." (declare (ignore context)) (let* ((payload (getf action :payload)) (meta (getf action :meta)) (chat-id (or (getf meta :chat-id) (getf payload :chat-id) (getf action :chat-id))) (text (or (getf payload :text) (getf action :text))) (account (get-signal-account))) (when (and account chat-id text) (harness-log "SIGNAL: Sending message to ~a..." chat-id) (handler-case (uiop:run-program (list "signal-cli" "-u" account "send" "-m" text chat-id) :output :string :error-output :string) (error (c) (harness-log "SIGNAL ERROR: ~a" c)))))) #+end_src ** Sensor: receive & Injection Polls for new messages and injects them into the harness. #+begin_src lisp (defun signal-process-updates () "Polls for new messages via signal-cli and injects them into the harness." (let ((account (get-signal-account))) (when account (handler-case (let* ((output (uiop:run-program (list "signal-cli" "-u" account "receive" "--json") :output :string :error-output :string :ignore-error-status t)) (lines (cl-ppcre:split "\\n" output))) (dolist (line lines) (when (and line (> (length line) 0)) (let* ((json (ignore-errors (cl-json:decode-json-from-string line))) (envelope (cdr (assoc :envelope json))) (source (cdr (assoc :source envelope))) (data-message (cdr (assoc :data-message envelope))) (text (cdr (assoc :message data-message)))) (when (and source text) (harness-log "SIGNAL: Received message from ~a" source) (inject-stimulus (list :type :EVENT :meta (list :source :signal :chat-id source) :payload (list :sensor :user-input :text text)))))))) (error (c) (harness-log "SIGNAL POLL ERROR: ~a" c)))))) #+end_src ** Start Polling Initializes the Signal background thread. #+begin_src lisp (defun start-signal-gateway () "Initializes the Signal background thread." (unless (and *signal-polling-thread* (bt:thread-alive-p *signal-polling-thread*)) (setf *signal-polling-thread* (bt:make-thread (lambda () (loop (signal-process-updates) (sleep 5))) :name "opencortex-signal-gateway")) (harness-log "SIGNAL: Gateway polling active."))) #+end_src ** Stop Polling Gracefully terminates the background thread. #+begin_src lisp (defun stop-signal-gateway () (when (and *signal-polling-thread* (bt:thread-alive-p *signal-polling-thread*)) (bt:destroy-thread *signal-polling-thread*) (setf *signal-polling-thread* nil))) #+end_src ** Registration: Actuator Register the Signal channel as a physical actuator. #+begin_src lisp (register-actuator :signal #'execute-signal-action) #+end_src ** Registration: Skill Define the passive skill entry for the gateway. #+begin_src lisp (defskill :skill-gateway-signal :priority 150 :trigger (lambda (ctx) (declare (ignore ctx)) nil) ;; Passive :probabilistic nil :deterministic (lambda (action ctx) (declare (ignore ctx)) action)) #+end_src ** Initialization Trigger the polling loop upon loading. #+begin_src lisp (start-signal-gateway) #+end_src