Files
UmbraAudio/infra/remote_credential_broker/executor.py
B.D. Murphy 0c9bea72e0 Add enterprise/infra tooling and repoint Qwen3-TTS submodule to internal fork
Two groups of changes, committed together as the follow-up to the initial push:

1. .gitmodules — submodule Qwen3-TTS now points at the internal Gitea fork
   (ssh://git@mini-eq:2222/umbragit/Qwen3-TTS.git) instead of upstream GitHub,
   so a clone actually gets the UmbraAudio branding/integration commit
   012e452 rather than vanilla upstream.

2. New infra/ tooling for the cross-machine work in spec 21: agent_bridge,
   remote_credential_broker (Keychain-backed, secrets read at runtime and
   generated via read_secret/random_secret — no hardcoded credentials), the
   enterprise bootstrap scripts, and the oldbox SSH/auth prompts. Plus
   backup/ restore scripts, two specs (18 UmbraTrans, 19 enterprise control
   plane), umbraaudio/voices/lmstudio_gate.py, and in-flight edits to the
   discovery/attribution/profiling modules, config, chapter_builder, emotion,
   pronunciation, qwen_tts and routes.

Also adds *.db to .gitignore: SQLite project databases are runtime state and no
.db file was tracked before.

Verified before staging: .env's only change is UMBRA_TTS__QWEN_TTS_HELPER_URL and
the committed copy has zero secret-looking values; the broker ships only
config.example.json.
2026-10-06 06:57:41 -07:00

243 lines
8.4 KiB
Python

from __future__ import annotations
import os
import shlex
import shutil
import socket
import subprocess
import tempfile
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Sequence
from .config import ConfigError, RemoteProfile
from .keychain import CredentialError, CredentialVault
Runner = Callable[..., subprocess.CompletedProcess[bytes]]
class ExecutionError(RuntimeError):
"""Raised when a remote operation cannot be safely started."""
@dataclass(frozen=True)
class ExecutionResult:
exit_code: int
stdout: str
stderr: str
duration_ms: int
class SecretRedactor:
"""Redact known secrets before output leaves the broker."""
def __init__(self) -> None:
self._secrets: set[str] = set()
def add(self, secret: str | None) -> None:
if secret:
self._secrets.add(secret)
def redact(self, value: str) -> str:
result = value
for secret in sorted(self._secrets, key=len, reverse=True):
if secret:
result = result.replace(secret, "<redacted>")
return result
class _AskpassServer:
"""One-shot local channel used instead of exposing a Keychain item to ssh."""
def __init__(self, secret: str) -> None:
self._directory = Path(tempfile.mkdtemp(prefix="remote-broker-askpass-"))
self._directory.chmod(0o700)
self.path = self._directory / "secret.sock"
self._secret = secret.encode("utf-8")
self._stop = threading.Event()
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._socket.bind(str(self.path))
self._socket.listen(8)
self._socket.settimeout(0.2)
self._thread = threading.Thread(target=self._serve, name="remote-broker-askpass", daemon=True)
self._thread.start()
def _serve(self) -> None:
while not self._stop.is_set():
try:
connection, _ = self._socket.accept()
except socket.timeout:
continue
except OSError:
return
with connection:
try:
connection.sendall(self._secret)
except OSError:
pass
def close(self) -> None:
self._stop.set()
try:
self._socket.close()
except OSError:
pass
self._thread.join(timeout=1)
shutil.rmtree(self._directory, ignore_errors=True)
class RemoteExecutor:
"""Run bounded SSH operations; sudo commands are exact allowlist entries."""
def __init__(
self,
store: CredentialVault,
*,
runner: Runner = subprocess.run,
askpass_path: Path | None = None,
audit_path: Path | None = None,
) -> None:
self._store = store
self._runner = runner
self._askpass_path = askpass_path or Path(__file__).with_name("askpass_helper.py")
if self._askpass_path.exists():
self._askpass_path.chmod(0o700)
self._audit_path = audit_path
@staticmethod
def _validate_command(command: Sequence[str]) -> tuple[str, ...]:
if not command or not all(isinstance(part, str) and part for part in command):
raise ExecutionError("Remote command must be a non-empty list of strings")
if any("\x00" in part for part in command):
raise ExecutionError("Remote command contains a NUL byte")
return tuple(command)
def _audit(self, profile: RemoteProfile, command: tuple[str, ...], sudo: bool, result: int) -> None:
if self._audit_path is None:
return
self._audit_path.parent.mkdir(parents=True, exist_ok=True)
# Store command metadata only. Never store passwords or command stdin.
line = (
f"profile={profile.name} sudo={str(sudo).lower()} "
f"command={shlex.join(command)} exit={result}\n"
)
fd = os.open(self._audit_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
os.write(fd, line.encode("utf-8"))
finally:
os.close(fd)
def _base_ssh_command(self, profile: RemoteProfile, remote_command: str, use_password: bool) -> list[str]:
known_hosts = str(Path(profile.known_hosts).expanduser())
command = [
"ssh",
"-p",
str(profile.port),
"-o",
"ConnectTimeout=15",
"-o",
"ServerAliveInterval=15",
"-o",
"ServerAliveCountMax=2",
"-o",
"StrictHostKeyChecking=yes",
"-o",
f"UserKnownHostsFile={known_hosts}",
]
if use_password:
command.extend(
[
"-o",
"BatchMode=no",
"-o",
"PreferredAuthentications=publickey,password",
"-o",
"PubkeyAuthentication=yes",
]
)
command.extend([f"{profile.username}@{profile.host}", remote_command])
return command
def run(
self,
profile: RemoteProfile,
command: Sequence[str],
*,
sudo: bool = False,
timeout_seconds: int = 120,
) -> ExecutionResult:
command_tuple = self._validate_command(command)
if not 5 <= timeout_seconds <= 3600:
raise ExecutionError("timeout_seconds must be between 5 and 3600")
if sudo and not profile.sudo_allowed(command_tuple):
raise ExecutionError(
"The requested sudo command is not an exact allowlist entry for this profile"
)
ssh_secret: str | None = None
sudo_secret: str | None = None
redactor = SecretRedactor()
if profile.ssh_credential:
try:
ssh_secret = self._store.get(profile.ssh_credential)
except CredentialError as exc:
raise ExecutionError(
f"SSH credential is not configured for profile {profile.name!r}"
) from exc
redactor.add(ssh_secret)
if sudo:
if not profile.sudo_credential:
raise ExecutionError(f"No sudo credential is configured for profile {profile.name!r}")
try:
sudo_secret = self._store.get(profile.sudo_credential)
except CredentialError as exc:
raise ExecutionError(
f"Sudo credential is not configured for profile {profile.name!r}"
) from exc
redactor.add(sudo_secret)
remote_argv = list(command_tuple)
if sudo:
remote_command = "sudo -S -p '' -- " + shlex.join(remote_argv)
else:
remote_command = shlex.join(remote_argv)
ssh_command = self._base_ssh_command(profile, remote_command, use_password=ssh_secret is not None)
environment = os.environ.copy()
askpass_server: _AskpassServer | None = None
if ssh_secret is not None:
askpass_server = _AskpassServer(ssh_secret)
environment.update(
{
"SSH_ASKPASS": str(self._askpass_path),
"SSH_ASKPASS_REQUIRE": "force",
"DISPLAY": environment.get("DISPLAY", ":0"),
"UMBRA_BROKER_ASKPASS_SOCKET": str(askpass_server.path),
}
)
stdin_data = (sudo_secret + "\n").encode("utf-8") if sudo_secret is not None else None
started = time.monotonic()
try:
completed = self._runner(
ssh_command,
input=stdin_data,
capture_output=True,
timeout=timeout_seconds,
env=environment,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise ExecutionError("Remote operation timed out") from exc
except OSError as exc:
raise ExecutionError("Unable to start ssh") from exc
finally:
if askpass_server is not None:
askpass_server.close()
duration_ms = int((time.monotonic() - started) * 1000)
stdout = redactor.redact(completed.stdout.decode("utf-8", errors="replace"))
stderr = redactor.redact(completed.stderr.decode("utf-8", errors="replace"))
result = ExecutionResult(completed.returncode, stdout, stderr, duration_ms)
self._audit(profile, command_tuple, sudo, result.exit_code)
return result