From b62b7f1095182ec29bc48ea93d28898a61dbd106 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Wed, 22 Apr 2026 14:47:34 -0400 Subject: [PATCH] fix: protocol validator allows REQUEST without :target if :source is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Relax validate-communication-protocol-schema to accept :REQUEST messages without :target when :source is present in :meta (reason-gate infers target from source). - This preserves 'equality of clients' — gateways don't duplicate routing logic. - Add communication-validator to ASD components. - Fixes TCP CLI gateway integration: clients can now connect and receive responses. - Verified with test client: 13/13 skills load, Perceive gate processes messages. --- harness/communication.org | 13 ++++-- library/communication-validator.lisp | 13 ++++-- library/gen/org-skill-protocol-validator.lisp | 13 ++++-- opencortex.asd | 1 + skills/org-skill-protocol-validator.org | 13 ++++-- test_cli.py | 46 +++++++++++++++++++ 6 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 test_cli.py diff --git a/harness/communication.org b/harness/communication.org index fb02139..a68ed28 100644 --- a/harness/communication.org +++ b/harness/communication.org @@ -98,10 +98,15 @@ The validator ensures that incoming messages adhere to the strict property list (case type (:REQUEST - (unless (proto-get msg :target) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :target")) - (unless (proto-get msg :payload) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))) + ;; Allow missing :target if :source is present in :meta, since reason-gate + ;; will infer :target from :source downstream. This preserves "equality of + ;; clients" — gateways need not duplicate routing logic. + (let ((target (proto-get msg :target)) + (source (proto-get (proto-get msg :meta) :source))) + (unless (or target source) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it")) + (unless (proto-get msg :payload) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload")))) (:EVENT (let ((payload (proto-get msg :payload))) diff --git a/library/communication-validator.lisp b/library/communication-validator.lisp index 329df45..5b577be 100644 --- a/library/communication-validator.lisp +++ b/library/communication-validator.lisp @@ -11,10 +11,15 @@ (case type (:REQUEST - (unless (proto-get msg :target) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :target")) - (unless (proto-get msg :payload) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))) + ;; Allow missing :target if :source is present in :meta, since reason-gate + ;; will infer :target from :source downstream. This preserves "equality of + ;; clients" — gateways need not duplicate routing logic. + (let ((target (proto-get msg :target)) + (source (proto-get (proto-get msg :meta) :source))) + (unless (or target source) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it")) + (unless (proto-get msg :payload) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload")))) (:EVENT (let ((payload (proto-get msg :payload))) diff --git a/library/gen/org-skill-protocol-validator.lisp b/library/gen/org-skill-protocol-validator.lisp index 66ff0d8..5c9411a 100644 --- a/library/gen/org-skill-protocol-validator.lisp +++ b/library/gen/org-skill-protocol-validator.lisp @@ -11,10 +11,15 @@ (case type (:REQUEST - (unless (proto-get msg :target) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :target")) - (unless (proto-get msg :payload) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))) + ;; Allow missing :target if :source is present in :meta, since reason-gate + ;; will infer :target from :source downstream. This preserves "equality of + ;; clients" — gateways need not duplicate routing logic. + (let ((target (proto-get msg :target)) + (source (proto-get (proto-get msg :meta) :source))) + (unless (or target source) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it")) + (unless (proto-get msg :payload) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload")))) (:EVENT (let ((payload (proto-get msg :payload))) diff --git a/opencortex.asd b/opencortex.asd index fb2b89e..9831c32 100644 --- a/opencortex.asd +++ b/opencortex.asd @@ -9,6 +9,7 @@ :components ((:file "library/package") (:file "library/skills") (:file "library/communication") + (:file "library/communication-validator") (:file "library/memory") (:file "library/context") (:file "library/perceive") diff --git a/skills/org-skill-protocol-validator.org b/skills/org-skill-protocol-validator.org index 8e918c0..ddfb43b 100644 --- a/skills/org-skill-protocol-validator.org +++ b/skills/org-skill-protocol-validator.org @@ -59,10 +59,15 @@ Decouple protocol parsing (framing/unframing) from semantic validation. (case type (:REQUEST - (unless (proto-get msg :target) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :target")) - (unless (proto-get msg :payload) - (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload"))) + ;; Allow missing :target if :source is present in :meta, since reason-gate + ;; will infer :target from :source downstream. This preserves "equality of + ;; clients" — gateways need not duplicate routing logic. + (let ((target (proto-get msg :target)) + (source (proto-get (proto-get msg :meta) :source))) + (unless (or target source) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :target and no :source in :meta to infer it")) + (unless (proto-get msg :payload) + (error "Communication Protocol Schema Error: REQUEST missing mandatory :payload")))) (:EVENT (let ((payload (proto-get msg :payload))) diff --git a/test_cli.py b/test_cli.py new file mode 100644 index 0000000..a740b54 --- /dev/null +++ b/test_cli.py @@ -0,0 +1,46 @@ +import socket +import struct + +def frame_message(msg_string): + payload = msg_string.encode('utf-8') + return f"{len(payload):06x}".encode('ascii') + payload + +def read_framed(sock): + header = b'' + while len(header) < 6: + chunk = sock.recv(6 - len(header)) + if not chunk: + return None + header += chunk + length = int(header, 16) + data = b'' + while len(data) < length: + chunk = sock.recv(length - len(data)) + if not chunk: + return None + data += chunk + return data.decode('utf-8') + +msg = '(:TYPE :REQUEST :PAYLOAD (:ACTION :MESSAGE :TEXT "hello") :META (:SOURCE :CLI :SESSION-ID "test1"))' + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect(('127.0.0.1', 9105)) +sock.settimeout(10.0) + +# Read handshake +handshake = read_framed(sock) +print("HANDSHAKE:", handshake) + +# Read status +status = read_framed(sock) +print("STATUS:", status) + +# Send message +sock.sendall(frame_message(msg)) +print("SENT:", msg) + +# Read response +response = read_framed(sock) +print("RESPONSE:", response) + +sock.close()