Skip to content

feat(bin): add duplex Signal process-event adapter - #2680

Open
FocalFactotum wants to merge 19 commits into
kunchenguid:mainfrom
FocalFactotum:fm/firstmate-signal-duplex-adapter-v4
Open

feat(bin): add duplex Signal process-event adapter#2680
FocalFactotum wants to merge 19 commits into
kunchenguid:mainfrom
FocalFactotum:fm/firstmate-signal-duplex-adapter-v4

Conversation

@FocalFactotum

Copy link
Copy Markdown

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

  • Add an optional private Signal group adapter that captures configured-group replies in a bounded envelope and sends message bytes through JSON-RPC stdin.
  • Retire the inbound source before Signal operations, restore it after success or failure, and add generation-bound readiness tracking to the generic process-event runner.
  • Document setup and architecture constraints, with fake signal-cli end-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
ok - Signal temporaries stay home-local, private, and clean across interruptions
ok - only the owning runner generation can change source readiness
ok - a foreign home cannot access Signal while another runner owns the account
ok - old account-discovery ordering stalls while receive owns the account
ok - send restores a bounded generation-owned foreground receive
ok - outbound attribution is configured, inserted once, and not duplicated
ok - failed send restores a bounded generation-owned foreground receive
ok - live route replacement revalidates private file permissions
ok - unrelated inbound messages stay quiet and structured group bytes are exact
ok - direct messages cannot spoof the configured group id via an embedded Id: line
ok - multiple selectors route through one account-scoped receive owner
ok - account discovery requires exactly one strictly formed account
ok - arm validates the complete routing table before registration
ok - Signal routing requires private file permissions
ok - send fails closed unless the restored runner reaches receive
Evidence: Unconfigured adapter CLI transcript
Public command: fm-procevent-signal.sh arm <selector>
Exit status: 1
Sanitized stderr: error: Signal is not configured
process-event registration created: no

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 before signal-cli receive starts. 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, while signal_account_accessible treats 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_file accepts any regular non-symlink file without enforcing private permissions. The documented creation path can therefore produce a mode-0644 config/signal-groups under 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." Every signal-cli invocation omits --data-dir, so the supported CLI defaults to $XDG_DATA_HOME/signal-cli or $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/signal and 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 when cmd_source starts, but each later envelope independently reopens signal-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_account discovers but discards the sole account, while this JSON-RPC request omits params.account. signal-cli 0.14.7 starts jsonRpc without -a in 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.sh
  • tests/fm-procevent.test.sh
  • bin/fm-doc-audience-check.sh
  • Isolated public CLI check: FM_HOME=&lt;unconfigured-home&gt; FM_CONFIG_OVERRIDE=&lt;empty-config&gt; 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant