feat(bin): add duplex Signal process-event adapter - #2680
Open
FocalFactotum wants to merge 19 commits into
Open
Conversation
…d temp-file leaks
…m error, timeout, and mktemp dup
…eign Signal owners
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix the reproducible Signal bridge defect exposed by the approved hybrid reply policy: after an automatically re-armed blocking signal-cli receive source owns the linked account, a Signal-originated substantive reply must still be delivered. Implement the smallest upstream-appropriate optional Signal adapter over the existing generic process-event runner. The supported outbound path must retire the live inbound source before every signal-cli operation, including account discovery, take message bytes only from stdin or a file and never argv, send to a configured private group selector, and restore inbound registration and listening after both success and ordinary failure. All external commands must be bounded so account-lock recurrence cannot wedge the caller. Retirement and re-arm must be idempotent, preserve runner ownership, avoid duplicate owners, and never shell-background a source. Inbound bytes remain untrusted and must be durably captured by the generic runner, never executed, shell-interpolated, or appended raw to task status. Ignore unrelated chats without emitting empty pseudo-messages while continuing to wait. Emit configured-group content in a bounded parseable envelope that preserves bytes without treating them as instructions. Keep group ids, account identifiers, phone numbers, message bodies, Signal state, and credentials private and home-local; normal output, status, tests, docs, commits, and PR prose must not expose them. Keep the adapter optional and inert when unconfigured. Preserve the reviewed security corrections for adapter inertness and source backoff, temp-file cleanup, strict account-number validation, body-boundary parsing, direct-message group-id spoof resistance, bounded usage and command timeouts, re-arm failure reporting, and shellcheck-safe return handling. Public executable-interface regression tests must use a fake signal-cli and real process behavior to prove the old ordering stalls while receive owns the account, the supported ordering succeeds, success and failure restore receive, unrelated messages do not wake, no message body reaches argv, repeated arm-retire-send does not duplicate owners, and tests cannot leak real local Signal data. Update only the correct runner or adapter help, operator setup owner, architecture and verification owner, and audience inventory required by existing documentation classification, with no Signal procedure detail in AGENTS.md unless universally necessary. Keep one authoritative owner per contract, one sentence per tracked Markdown line, plain dashes, shellcheck-clean bin scripts, and colocated tests. Do not build a daemon, scheduler, parallel control plane, broad messaging framework, or wrapper machinery without an executable blocker. Do not modify or copy from the untracked operational prototype or private helpers, run a real Signal account, read group-id files, or include any personal identifier, message, path, or credential. Validate focused tests, repository task tests, lint, and documentation audience checks. Push only to the configured FocalFactotum fork and create a green PR for review; do not merge.
What Changed
signal-cliend-to-end coverage for routing, privacy, timeouts, lifecycle restoration, and single-owner behavior.Risk Assessment
✅ Low: The optional adapter is well-bounded, the approved privacy fixes and JSON-RPC account propagation are correct, and the remaining readiness and cross-home races are explicitly accepted and documented limitations.
Testing
Inspected the target diff and intent, ran the focused Signal adapter and generic process-event executable regressions, verified documentation audience ownership, and manually exercised an unconfigured isolated home. All checks passed; evidence captures the account-lock counterfactual and inert public CLI behavior. This is a CLI-only change, so visual UI evidence was not applicable.
Evidence: Signal adapter end-to-end transcript
Evidence: Unconfigured adapter CLI transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
docs/verification/process-event-sources.md- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
🔧 **Review** - 2 issues found → auto-fixed (4) ✅
bin/fm-procevent-signal.sh:275- Required criterion: "restore inbound registration and listening after both success and ordinary failure." The adapter marks the generation ready beforesignal-cli receivestarts. With a receive startup delay longer than wait-ready's two 100 ms observations, send returns successfully while no receiver owns the account. The changed test demonstrates this with a 0.5-second delay and only checks that listening returns eventually. Publish readiness only after the receive operation has actually acquired the account, at the earliest shared boundary that can prove this without adapter-owned backgrounding.bin/fm-procevent-signal.sh:182- Required criterion: "Keep group ids, account identifiers, phone numbers, message bodies, Signal state, and credentials private and home-local." Account discovery output and all message staging files are created under${TMPDIR:-/tmp}, outside the effective Firstmate home. Although mode 0600 and ordinary cleanup protect them, interruption by SIGKILL can strand sensitive bytes outside home-owned lifecycle cleanup. Stage these files in a private directory beneath$STATE/signal, or obtain explicit authorization to relax the home-local requirement.🔧 Fix: Captain, keep Signal temporaries private and home-local
2 errors still open:
bin/fm-procevent-signal.sh:132- Required criterion: "Retirement and re-arm must be idempotent, preserve runner ownership, avoid duplicate owners." The lifecycle lock is under each home's$STATE, whilesignal_account_accessibletreats an absent global claim as permission. After home A retires its receive claim, home B can enter its separate lock and run account discovery/send concurrently before A restores ownership. Serialize the entire retire-operation-rearm sequence at a machine-wide Signal-account boundary.bin/fm-procevent-signal.sh:99- Required criterion: "Keep group ids ... private and home-local."ensure_groups_fileaccepts any regular non-symlink file without enforcing private permissions. The documented creation path can therefore produce a mode-0644config/signal-groupsunder a normal umask, exposing every private group id to other local users. Require a private file mode at this configuration boundary and document secure creation.🔧 Fix: Captain, enforce private Signal routing configuration
3 errors still open:
bin/fm-procevent-signal.sh:198- Required criterion: "Keep ... Signal state, and credentials private and home-local." Everysignal-cliinvocation omits--data-dir, so the supported CLI defaults to$XDG_DATA_HOME/signal-clior$HOME/.local/share/signal-cli, outside the effective Firstmate home. Consequently account discovery, receive, and send use global Signal credentials and state despite the documentation claiming home-local state. Establish a private directory beneath$STATE/signaland pass it at a shared invocation boundary for every Signal operation.bin/fm-procevent-signal.sh:217- Required criterion: "Keep group ids ... private and home-local." The mode and symlink check runs only whencmd_sourcestarts, but each later envelope independently reopenssignal-groups. If an editor atomically replaces the live file with a mode-0644 file or symlink, the long-running receiver continues consuming it without refusal. Validate the opened routing file itself at the shared parser boundary, including regular-file, no-symlink, and mode-0600 properties.bin/fm-procevent-signal.sh:291- Required criterion: "Keep group ids, account identifiers, phone numbers ... private." The indefinitely blocking receive places the discovered phone number in process argv, and send additionally places the private group id there. These values are observable through process inspection even though adapter output is sanitized. With exactly one account in a home-local data directory, avoid passing the account identifier explicitly; route outbound group and message data through a signal-cli stdin-capable interface so neither identifier enters argv.🔧 Fix: Captain, harden Signal state, routes, and identifiers
1 error still open:
bin/fm-procevent-signal.sh:283- Required criterion: "The supported outbound path must ... send to a configured private group selector."validate_single_accountdiscovers but discards the sole account, while this JSON-RPC request omitsparams.account. signal-cli 0.14.7 startsjsonRpcwithout-ain multi-account mode and requires an account parameter for account-specific requests, so a real send returns an error even with exactly one configured account. Preserve the discovered account and include it in the private stdin JSON request, and make the fake CLI reject a missing or mismatched account so the regression proves the real protocol.🔧 Fix: Captain, include validated account in Signal JSON-RPC
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
tests/fm-procevent-signal.test.shtests/fm-procevent.test.shbin/fm-doc-audience-check.shIsolated public CLI check:FM_HOME=<unconfigured-home> FM_CONFIG_OVERRIDE=<empty-config> PATH=/usr/bin:/bin bin/fm-procevent-signal.sh arm team✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.