From 11c43f76fa8b500deb9cc266de2de0da65c8d039 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Fri, 8 May 2026 18:03:24 -0400 Subject: [PATCH] =?UTF-8?q?v0.7.2:=20Merkle=20provenance=20audit=20+=20RCE?= =?UTF-8?q?=20flake=20fix=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit audit-node exposes memory-object lineage (type, hash, scope, version). /audit TUI command. /audit verify deferred. Fixed RCE test flake: assemble-config-section used getf on non-plist cascade entries. Wrapped in handler-case. Also fixed ~/ format directive escape. Core reason: 35/35. Core: 81/81. --- docs/ROADMAP.org | 20 ++++++++++++-------- lisp/channel-tui-main.lisp | 12 ++++++++++++ lisp/core-memory.lisp | 24 ++++++++++++++++++++++++ lisp/core-reason.lisp | 6 ++++-- org/channel-tui-main.org | 12 ++++++++++++ org/core-memory.org | 27 +++++++++++++++++++++++++++ org/core-reason.org | 6 ++++-- 7 files changed, 95 insertions(+), 12 deletions(-) diff --git a/docs/ROADMAP.org b/docs/ROADMAP.org index 23dd4cc..a44e7fb 100644 --- a/docs/ROADMAP.org +++ b/docs/ROADMAP.org @@ -2049,15 +2049,19 @@ The original roadmap placed MCP at v0.9.0 and planned "10+ cognitive tools" buil - Propose installation command and retry the failed action on user approval. - Cache resolved dependency paths to avoid repeated searches. -*** v0.10.3 — TODO Voice Gateway +*** TODO Channels + providers — match OpenClaw on demand +:PROPERTIES: +:ID: id-v100-channels +:CREATED: [2026-05-08 Fri] +:END: -Rationale: OpenClaw ships voice wake words and talk mode on macOS/iOS/Android via ElevenLabs. Hermes Agent has voice memo transcription. Both treat voice as a first-class channel. Passepartout's daemon already handles text — voice is an I/O format conversion. Speech-to-text turns audio into ~:user-input~ signals. Text-to-speech turns agent responses into audio. The architecture requires no changes; the voice gateway is a skill that wraps existing REST APIs. +The daemon protocol is client-agnostic hex-framed plists over TCP. Every new channel is a new client that speaks the same protocol. OpenClaw's 23+ channels are trivially copyable — each platform needs a poll loop + send function, ~30 lines each. LLM providers are a row in ~*provider-cascade*~ — a new entry in ~neuro-provider.lisp~ with API endpoint + token pricing. Neither deserves its own release. -- Speech-to-text: POST audio to OpenAI Whisper API (~/v1/audio/transcriptions~) or local Whisper via Ollama. Receive text. Inject as a ~:user-input~ signal into the pipeline. The daemon processes it identically to a typed message. -- Text-to-speech: POST text to ElevenLabs REST API (~/v1/text-to-speech/{voice-id}~) with stream response. Also support system ~say~ (macOS) / ~espeak~ (Linux) as zero-dependency fallbacks. -- TUI voice toggle: ~/voice on~ enables voice capture, shows a ~🎤~ (listening) indicator in the status bar. ~/voice off~ returns to text-only. The microphone capture runs in a dedicated thread that feeds audio chunks to the speech-to-text backend. -- Voice mode in messaging gateways: on Telegram and Discord, the voice gateway transcribes voice messages into text and injects them as ~:user-input~ signals. Agent responses can be optionally spoken back via text-to-speech if the user's message included a voice note (reply in kind). -- The voice gateway is a skill (~defskill~~:passepartout-gateway-voice~). No core daemon changes required. The daemon receives text signals whether they originated from a keyboard, a messaging app, or a microphone. +- Channels: match OpenClaw's 23+ channels on demand. The Emacs bridge (already done, v0.4.0) proves the pattern. Each new platform (WhatsApp, iMessage, Matrix, IRC, etc.) is a skill that registers a poll-fn + send-fn. ~30 lines per channel. +- Providers: match OpenClaw/Hermes on provider count. Adding a new provider is a table entry in ~neuro-provider.lisp~: name, API endpoint, model list, pricing. ~20 lines per provider. +- Voice: STT + TTS are REST wrappers (~whisper~ / ~elevenlabs~ / ~espeak~). Already spec'd as a skill. ~50 lines. + +No separate releases. Done when needed, shipped when ready. *** TODO Web search + web fetch tools — ~search-web~, ~fetch-web~ :PROPERTIES: @@ -2157,7 +2161,7 @@ The Git policy gate (commit-before-modify) is a safety feature no competitor pro The TUI tool visualization (v0.8.1) extends seamlessly to MCP tools — the rendering layer doesn't distinguish between native tools and MCP tools. The same colored backgrounds, collapsible outputs, and gate traces apply universally. -The voice gateway (v0.10.3) adds parity with OpenClaw's voice features without architectural changes — speech-to-text and text-to-speech are thin REST wrappers that feed text signals into the existing pipeline. Combined with the Emacs bridge (v0.4.0), messaging gateways (v0.4.0), and the now-SOTA TUI (v0.7.0–v0.8.3), Passepartout supports four interaction surfaces by v0.10.3: terminal (TUI), messaging apps, Emacs, and voice. +The voice gateway and additional channels add parity with OpenClaw's multi-surface approach without architectural changes — every channel is a thin client speaking the same framed TCP protocol to the same daemon. Channels and providers are trivially copyable: each new platform is ~30 lines of poll-loop, each new provider is ~20 lines of API config. Passepartout matches OpenClaw's channel count on demand, shipping when needed rather than as a scheduled milestone. ** v0.11.0: Planning, Self-Modification & Deterministic Routing diff --git a/lisp/channel-tui-main.lisp b/lisp/channel-tui-main.lisp index 209bcd1..e43d015 100644 --- a/lisp/channel-tui-main.lisp +++ b/lisp/channel-tui-main.lisp @@ -148,6 +148,18 @@ (when (fboundp 'load-identity-file) (funcall 'load-identity-file)) (add-msg :system "Identity reloaded"))) + ;; /audit command — Merkle provenance + ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/audit ")) + (if (fboundp 'audit-node) + (let* ((node-id (string-trim '(#\Space) (subseq text 7))) + (info (funcall 'audit-node node-id))) + (if info + (add-msg :system (format nil "Node ~a: type=~a scope=~a hash=~a" + (getf info :id) (getf info :type) + (getf info :scope) + (subseq (or (getf info :hash) "(none)") 0 16))) + (add-msg :system (format nil "Node ~a not found" node-id)))) + (add-msg :system "Memory audit not available"))) ((string-equal text "/help") (add-msg :system "/focus Set project context") diff --git a/lisp/core-memory.lisp b/lisp/core-memory.lisp index 79a8b4c..2833e4e 100644 --- a/lisp/core-memory.lisp +++ b/lisp/core-memory.lisp @@ -195,6 +195,15 @@ t) (progn (log-message "REDO: No snapshots to redo") nil))) +(defun audit-node (node-id) + "Return audit info for a memory object by ID." + (let ((obj (memory-object-get node-id))) + (when obj + (list :id node-id :type (memory-object-type obj) + :version (memory-object-version obj) + :hash (or (memory-object-hash obj) "(none)") + :scope (memory-object-scope obj))))) + (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :fiveam :silent t)) @@ -302,3 +311,18 @@ (progn (setf passepartout::*undo-stack* nil) (is (null (passepartout::undo)))) (setf passepartout::*undo-stack* orig-undo)))) + +(test test-audit-node-found + "Contract v0.7.2: audit-node returns info for existing object." + (clrhash passepartout::*memory-store*) + (setf (gethash "audit-1" passepartout::*memory-store*) + (passepartout::make-memory-object :id "audit-1" :type :HEADLINE + :version 1 :hash "abc123" :scope :memex)) + (let ((info (passepartout::audit-node "audit-1"))) + (is (not (null info))) + (is (eq :HEADLINE (getf info :type))) + (is (string= "abc123" (getf info :hash))))) + +(test test-audit-node-not-found + "Contract v0.7.2: audit-node returns nil for nonexistent id." + (is (null (passepartout::audit-node "nonexistent-xxxx")))) diff --git a/lisp/core-reason.lisp b/lisp/core-reason.lisp index eec17d5..b5cc10d 100644 --- a/lisp/core-reason.lisp +++ b/lisp/core-reason.lisp @@ -84,11 +84,13 @@ (when (boundp '*provider-cascade*) (setf provider-names (format nil "~{~a~^, ~}" - (mapcar (lambda (p) (getf p :model)) + (mapcar (lambda (p) + (handler-case (or (getf p :model) (getf p :provider) "") + (error () (princ-to-string p)))) (symbol-value '*provider-cascade*))))) (when (boundp '*hitl-pending*) (setf rules-count (hash-table-count (symbol-value '*hitl-pending*)))) - (format nil "CONFIG: You are Passepartout v0.7.2. Provider: ~a. Context: ~d tokens. Security gates: ~d active. Rules learned: ~d. Documentation: ~/memex/projects/passepartout/docs/USER_MANUAL.org." + (format nil "CONFIG: You are Passepartout v0.7.2. Provider: ~a. Context: ~d tokens. Security gates: ~d active. Rules learned: ~d. Documentation: USER_MANUAL.org." (if (string= provider-names "") "default" provider-names) context-window gate-count rules-count))) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 09f0a19..38d4728 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -182,6 +182,18 @@ Event handlers + daemon I/O + main loop. (when (fboundp 'load-identity-file) (funcall 'load-identity-file)) (add-msg :system "Identity reloaded"))) + ;; /audit command — Merkle provenance + ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/audit ")) + (if (fboundp 'audit-node) + (let* ((node-id (string-trim '(#\Space) (subseq text 7))) + (info (funcall 'audit-node node-id))) + (if info + (add-msg :system (format nil "Node ~a: type=~a scope=~a hash=~a" + (getf info :id) (getf info :type) + (getf info :scope) + (subseq (or (getf info :hash) "(none)") 0 16))) + (add-msg :system (format nil "Node ~a not found" node-id)))) + (add-msg :system "Memory audit not available"))) ((string-equal text "/help") (add-msg :system "/focus Set project context") diff --git a/org/core-memory.org b/org/core-memory.org index ddd50ac..4728009 100644 --- a/org/core-memory.org +++ b/org/core-memory.org @@ -405,6 +405,18 @@ Restores memory state from a previously saved snapshot file. Called during boot (progn (log-message "REDO: No snapshots to redo") nil))) #+end_src +** Merkle Audit +#+begin_src lisp +(defun audit-node (node-id) + "Return audit info for a memory object by ID." + (let ((obj (memory-object-get node-id))) + (when obj + (list :id node-id :type (memory-object-type obj) + :version (memory-object-version obj) + :hash (or (memory-object-hash obj) "(none)") + :scope (memory-object-scope obj))))) +#+end_src + * Test Suite Verifies that the Merkle hash is deterministic and consistent across independent AST ingestions. #+begin_src lisp @@ -515,4 +527,19 @@ Verifies that the Merkle hash is deterministic and consistent across independent (progn (setf passepartout::*undo-stack* nil) (is (null (passepartout::undo)))) (setf passepartout::*undo-stack* orig-undo)))) + +(test test-audit-node-found + "Contract v0.7.2: audit-node returns info for existing object." + (clrhash passepartout::*memory-store*) + (setf (gethash "audit-1" passepartout::*memory-store*) + (passepartout::make-memory-object :id "audit-1" :type :HEADLINE + :version 1 :hash "abc123" :scope :memex)) + (let ((info (passepartout::audit-node "audit-1"))) + (is (not (null info))) + (is (eq :HEADLINE (getf info :type))) + (is (string= "abc123" (getf info :hash))))) + +(test test-audit-node-not-found + "Contract v0.7.2: audit-node returns nil for nonexistent id." + (is (null (passepartout::audit-node "nonexistent-xxxx")))) #+end_src \ No newline at end of file diff --git a/org/core-reason.org b/org/core-reason.org index 149a8d8..386cc65 100644 --- a/org/core-reason.org +++ b/org/core-reason.org @@ -239,11 +239,13 @@ each cascade call via ~cost-track-backend-call~. All four calls are (when (boundp '*provider-cascade*) (setf provider-names (format nil "~{~a~^, ~}" - (mapcar (lambda (p) (getf p :model)) + (mapcar (lambda (p) + (handler-case (or (getf p :model) (getf p :provider) "") + (error () (princ-to-string p)))) (symbol-value '*provider-cascade*))))) (when (boundp '*hitl-pending*) (setf rules-count (hash-table-count (symbol-value '*hitl-pending*)))) - (format nil "CONFIG: You are Passepartout v0.7.2. Provider: ~a. Context: ~d tokens. Security gates: ~d active. Rules learned: ~d. Documentation: ~/memex/projects/passepartout/docs/USER_MANUAL.org." + (format nil "CONFIG: You are Passepartout v0.7.2. Provider: ~a. Context: ~d tokens. Security gates: ~d active. Rules learned: ~d. Documentation: USER_MANUAL.org." (if (string= provider-names "") "default" provider-names) context-window gate-count rules-count)))