#+TITLE: Channel Signal (channel-signal.org) #+AUTHOR: Agent #+FILETAGS: :channel:signal: #+PROPERTY: header-args:lisp :tangle ../lisp/channel-signal.lisp * Channel Signal Extracted from gateway-messaging in v0.5.0. Isolated platform — Signal-specific poll and send logic. * Overview The Signal channel provides bidirectional communication via the ~signal-cli~ CLI tool. Messages received from Signal contacts are injected into the cognitive pipeline as ~:user-input~ signals with ~:source :signal~. Outbound messages route through the actuator registry when the pipeline targets ~:signal~. The channel uses two functions: ~signal-poll~ (inbound sensor) and ~signal-send~ (outbound actuator). Both retrieve the Signal account identifier from the credentials vault. HITL commands (~/approve~, ~/deny~) are intercepted before injection so approval flows work identically across all channels. ** Contract 1. (signal-get-account): returns the Signal phone number from the vault (via ~vault-get-secret :signal~), or nil if not configured. 2. (signal-poll): queries ~signal-cli receive --json~ for new messages, injects each non-system message as a ~:user-input~ stimulus with ~:source :signal~. Handles JSON parse failures and network errors gracefully (logs and continues). HITL commands are intercepted before injection. 3. (signal-send action context): sends a message via ~signal-cli send~. Extracts ~:chat-id~ and ~:text~ from the action plist. Logs send failures without crashing the pipeline. * Implementation #+begin_src lisp (in-package :passepartout) (defun signal-get-account () (vault-get-secret :signal)) (defun signal-poll () "Polls Signal for new messages and injects them into the harness." (let ((account (signal-get-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) (log-message "SIGNAL: Received message from ~a" source) (unless (ignore-errors (hitl-handle-message text :signal)) (stimulus-inject (list :type :EVENT :meta (list :source :signal :chat-id source) :payload (list :sensor :user-input :text text))))))))) (error (c) (log-message "SIGNAL POLL ERROR: ~a" c)))))) (defun signal-send (action context) "Sends a message via Signal." (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 (signal-get-account))) (when (and account chat-id text) (handler-case (uiop:run-program (list "signal-cli" "-u" account "send" "-m" text chat-id) :output :string :error-output :string) (error (c) (log-message "SIGNAL ERROR: ~a" c)))))) #+end_src #+end_src