FrameBridge 0.3.1 community source release

This commit is contained in:
FrameBridge Development committed 2026-10-26 22:09:18 -04:00
commit b23819fff1
1354 files changed
+343716

No files matched your search

@@ -0,0 +1,125 @@
{
"format": "FRAMEBRIDGE_ORIGINAL_SOURCE_SNAPSHOT_V1",
"source_host": "Original desktop source capture; community derivative",
"file_count": 14,
"scan_result": "HISTORICAL_CAPTURE_CHECK_ONLY; see current community privacy review",
"scope": "PUBLIC_PROGRAM_FILES_ONLY",
"runtime_state_included": false,
"configuration_files_included": false,
"embedded_lab_route_mapping_in_host_source": false,
"files": [
{
"archive_path": "tool-adapter/bin/framebridge_tool_adapter.py",
"source_path": "tool-adapter/bin/framebridge_tool_adapter.py",
"bytes": 10268,
"sha256": "dca9bf3dfbae7881aa6824ca79a2922fb4ec7ab1b61b5696b39cb64630c4d86b",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "tool-adapter/bin/framebridge_tool_client.py",
"source_path": "tool-adapter/bin/framebridge_tool_client.py",
"bytes": 1073,
"sha256": "1d1514f08653c31f6b67ebcdcf61686947321bec96752a692b7e46cee6285f6e",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "fbterm/bin/fbterm_engine.py",
"source_path": "fbterm/bin/fbterm_engine.py",
"bytes": 13763,
"sha256": "9408aa2dfba2ceecf1bcc4d9e6b1d52fec1f8971183f5baa7145f2d6357c7635",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "fbterm/bin/fbterm-run",
"source_path": "fbterm/bin/fbterm-run",
"bytes": 2628,
"sha256": "bbfc19c878746518e482adbe61cd6f8ae8560dca75a1e3db9921e1cb40b42e71",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "fbterm/bin/fbterm-shell",
"source_path": "fbterm/bin/fbterm-shell",
"bytes": 612,
"sha256": "eb6de30bfd6430165f7ebc982021f3565b2e80c4b82480b4e01e64e15e65957a",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "fbterm/bin/fbterm-prompt-ready",
"source_path": "fbterm/bin/fbterm-prompt-ready",
"bytes": 286,
"sha256": "276f657e23ff26c75f5e430731fdf5b095d777eabcbaad86e11c69b0919c691c",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "fbterm/bin/fbx",
"source_path": "fbterm/bin/fbx",
"bytes": 2517,
"sha256": "84589ad19ea0eab7cfc766ef4dba736be05c509c3918f572f4054585ee4c5efb",
"source_mode": "0o700",
"archive_mode": "0755"
},
{
"archive_path": "framebridge-native/host/framebridge_host.py",
"source_path": "framebridge-native/host/framebridge_host.py",
"bytes": 45347,
"sha256": "85d165bf8875fa8f9b202b129f985ca4cf040a1f4778ef43ed4c9f5b0164b3eb",
"source_mode": "0o755",
"archive_mode": "0755"
},
{
"archive_path": "framebridge-native/host/framebridge_cua_linux.py",
"source_path": "framebridge-native/host/framebridge_cua_linux.py",
"bytes": 45731,
"sha256": "aed73125f88b4408bad83ef4aae220fd79bd8f57e41ff15ebc9930453bdc69a1",
"source_mode": "0o600",
"archive_mode": "0644"
},
{
"archive_path": "framebridge-native/host/framebridge_voice_command.py",
"source_path": "framebridge-native/host/framebridge_voice_command.py",
"bytes": 1127,
"sha256": "ed0aff0070799d437a5570b16dbfa0a3878e51b73166c14bebaf8df45ebdc86c",
"source_mode": "0o664",
"archive_mode": "0644"
},
{
"archive_path": "framebridge-native/host/framebridge_voice_status.py",
"source_path": "framebridge-native/host/framebridge_voice_status.py",
"bytes": 1843,
"sha256": "f8520095e973a91249876d733143399fe40a0d72cd64c741c5cf85d8898a3ffb",
"source_mode": "0o664",
"archive_mode": "0644"
},
{
"archive_path": "framebridge-native/host/open-terminal.sh",
"source_path": "framebridge-native/host/open-terminal.sh",
"bytes": 690,
"sha256": "facaea7a5ca6635311a4c05e18edecdec98a137ce552fce39bdbc0ee755aa34c",
"source_mode": "0o775",
"archive_mode": "0755"
},
{
"archive_path": "framebridge-native/host/open-interactive-terminal.sh",
"source_path": "framebridge-native/host/open-interactive-terminal.sh",
"bytes": 4631,
"sha256": "e5c191c9cc354fd4d88a23c02e5eafbe454a7d3b3586265288d5f89ee9d8c602",
"source_mode": "0o775",
"archive_mode": "0755"
},
{
"archive_path": "framebridge-native/host/open-progress-console.sh",
"source_path": "framebridge-native/host/open-progress-console.sh",
"bytes": 3488,
"sha256": "cb519186ce299e21381e4698faf5490a3e947ad01187b9674f78d4e8bc21ef65",
"source_mode": "0o775",
"archive_mode": "0755"
}
],
"community_derivative": true
}
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set +e
umask 077
ROOT="$FRAMEBRIDGE_FBTERM_ROOT"
[ -n "$ROOT" ] || ROOT="$HOME/.local/share/framebridge/fbterm"
READY="$ROOT/prompt.ready"
TMP="$ROOT/.prompt.ready.$$"
mkdir -p "$ROOT" 2>/dev/null || true
printf '%s\n' "$(date +%s%N)" > "$TMP"
mv -f "$TMP" "$READY"
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
umask 077
ROOT="${FBTERM_ROOT:?FBTERM_ROOT no definido}"
# R2: mientras fbterm-run esta activo, el pane no esta disponible.
rm -f "$ROOT/prompt.ready"
JOB_ID="${1:-}"
case "$JOB_ID" in
''|*[!A-Za-z0-9._-]*)
printf 'FBTERM_ERROR=INVALID_JOB_ID\n' >&2
exit 64
;;
esac
JOB="$ROOT/jobs/$JOB_ID"
CMD="$JOB/command.bash"
TIMEOUT_FILE="$JOB/timeout.seconds"
if [ ! -f "$CMD" ]; then
printf 'FBTERM_ERROR=COMMAND_NOT_FOUND JOB_ID=%s\n' "$JOB_ID" >&2
exit 66
fi
TIMEOUT_S='120'
if [ -f "$TIMEOUT_FILE" ]; then
VALUE="$(cat "$TIMEOUT_FILE" 2>/dev/null || true)"
case "$VALUE" in
''|*[!0-9]*)
;;
*)
if [ "$VALUE" -ge 1 ] 2>/dev/null &&
[ "$VALUE" -le 3600 ] 2>/dev/null
then
TIMEOUT_S="$VALUE"
fi
;;
esac
fi
OUT_FIFO="$JOB/stdout.fifo"
ERR_FIFO="$JOB/stderr.fifo"
STDOUT="$JOB/stdout.bin"
STDERR="$JOB/stderr.bin"
rm -f "$OUT_FIFO" "$ERR_FIFO"
mkfifo "$OUT_FIFO" "$ERR_FIFO"
: > "$STDOUT"
: > "$STDERR"
date -Ins > "$JOB/started.at"
sha256sum "$CMD" |
awk '{print $1}' > "$JOB/command.sha256"
tee "$STDOUT" < "$OUT_FIFO" &
OUT_TEE_PID=$!
tee "$STDERR" < "$ERR_FIFO" >&2 &
ERR_TEE_PID=$!
/usr/bin/timeout \
--signal=TERM \
--kill-after=2s \
"${TIMEOUT_S}s" \
bash --noprofile --norc "$CMD" \
> "$OUT_FIFO" \
2> "$ERR_FIFO"
RC=$?
wait "$OUT_TEE_PID"
OUT_TEE_RC=$?
wait "$ERR_TEE_PID"
ERR_TEE_RC=$?
printf '%s\n' "$RC" > "$JOB/exit.code"
printf '%s\n' "$OUT_TEE_RC" > "$JOB/stdout-tee.exit"
printf '%s\n' "$ERR_TEE_RC" > "$JOB/stderr-tee.exit"
sha256sum "$STDOUT" |
awk '{print $1}' > "$JOB/stdout.sha256"
sha256sum "$STDERR" |
awk '{print $1}' > "$JOB/stderr.sha256"
stat -c '%s' "$STDOUT" > "$JOB/stdout.bytes"
stat -c '%s' "$STDERR" > "$JOB/stderr.bytes"
date -Ins > "$JOB/finished.at"
{
printf 'JOB_ID=%s\n' "$JOB_ID"
printf 'EXIT=%s\n' "$RC"
printf 'COMMAND_SHA256=%s\n' \
"$(cat "$JOB/command.sha256")"
printf 'STDOUT_SHA256=%s\n' \
"$(cat "$JOB/stdout.sha256")"
printf 'STDERR_SHA256=%s\n' \
"$(cat "$JOB/stderr.sha256")"
printf 'STDOUT_BYTES=%s\n' \
"$(cat "$JOB/stdout.bytes")"
printf 'STDERR_BYTES=%s\n' \
"$(cat "$JOB/stderr.bytes")"
printf 'STDOUT_TEE_EXIT=%s\n' "$OUT_TEE_RC"
printf 'STDERR_TEE_EXIT=%s\n' "$ERR_TEE_RC"
} > "$JOB/result.tmp"
mv "$JOB/result.tmp" "$JOB/result.env"
touch "$JOB/DONE"
rm -f "$OUT_FIFO" "$ERR_FIFO"
printf 'FBTERM_JOB_COMPLETE JOB_ID=%s EXIT=%s\n' \
"$JOB_ID" "$RC"
exit "$RC"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
umask 077
export HISTFILE=/dev/null
set +o history 2>/dev/null || true
printf '\n'
printf '========================================\n'
printf ' FrameBridge Terminal R1\n'
printf ' PTY persistente / comandos controlados\n'
printf '========================================\n'
printf '\n'
export FRAMEBRIDGE_FBTERM_ROOT="$HOME/.local/share/framebridge/fbterm"
if [ -n "$FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE" ]; then
export FRAMEBRIDGE_FBTERM_ROOT="$FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE"
fi
export PROMPT_COMMAND='"$FRAMEBRIDGE_FBTERM_ROOT/bin/fbterm-prompt-ready"'
exec bash --noprofile --norc
+497
View File
@@ -0,0 +1,497 @@
#!/usr/bin/env python3
import fcntl
import hashlib
import os
from pathlib import Path
import re
import subprocess
import time
class FBTermError(RuntimeError):
pass
class FBTermBusy(FBTermError):
pass
class FBTerm:
JOB_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
def __init__(self, root, session, title):
self.root = Path(root)
self.session = session
self.title = title
self.bin = self.root / "bin"
self.jobs = self.root / "jobs"
self.runner = self.bin / "fbterm-run"
self.shell = self.bin / "fbterm-shell"
self.lock_path = self.root / "controller.lock"
@staticmethod
def _run(args, **kwargs):
return subprocess.run(
args,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**kwargs,
)
def has_session(self):
p = self._run(
["tmux", "has-session", "-t", self.session]
)
return p.returncode == 0
def ensure(self, visible=True):
self.root.mkdir(mode=0o700, parents=True, exist_ok=True)
self.jobs.mkdir(mode=0o700, parents=True, exist_ok=True)
if not self.has_session():
env = os.environ.copy()
env["FBTERM_ROOT"] = str(self.root)
env["FBTERM_SESSION"] = self.session
env["PATH"] = str(self.bin) + os.pathsep + env.get("PATH", "")
p = subprocess.run(
[
"tmux",
"new-session",
"-d",
"-s",
self.session,
str(self.shell),
],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if p.returncode != 0:
raise FBTermError(
"tmux new-session fallo: " + p.stderr.strip()
)
subprocess.run(
[
"tmux",
"set-environment",
"-t",
self.session,
"FBTERM_ROOT",
str(self.root),
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[
"tmux",
"set-environment",
"-t",
self.session,
"FBTERM_SESSION",
self.session,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[
"tmux",
"set-option",
"-t",
self.session,
"allow-rename",
"off",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[
"tmux",
"set-option",
"-t",
self.session,
"set-titles",
"off",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[
"tmux",
"send-keys",
"-t",
self.session + ":0.0",
(
"export FBTERM_ROOT="
+ subprocess.list2cmdline([str(self.root)])
+ "; export FBTERM_SESSION="
+ subprocess.list2cmdline([self.session])
+ "; export PATH="
+ subprocess.list2cmdline(
[str(self.bin) + ":$PATH"]
)
),
"C-m",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(0.5)
if visible and not self.visible_window():
subprocess.Popen(
[
"gnome-terminal",
"--title=" + self.title,
"--",
"bash",
"-lc",
(
"exec tmux attach-session -r -t "
+ subprocess.list2cmdline([self.session])
),
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
for _ in range(40):
if self.visible_window():
break
time.sleep(0.1)
return self.status()
def visible_window(self):
p = self._run(["wmctrl", "-l"])
return (
p.returncode == 0
and self.title in p.stdout
)
def status(self):
if not self.has_session():
return {
"session": False,
"visible": False,
"pane_pid": None,
"pane_tty": None,
"pane_command": None,
"clients": 0,
"readonly_clients": 0,
}
fmt = (
"#{pane_pid}|#{pane_tty}|"
"#{pane_current_command}"
)
p = self._run(
[
"tmux",
"display-message",
"-p",
"-t",
self.session + ":0.0",
fmt,
]
)
parts = p.stdout.strip().split("|", 2)
pane_pid = parts[0] if len(parts) > 0 else ""
pane_tty = parts[1] if len(parts) > 1 else ""
pane_command = parts[2] if len(parts) > 2 else ""
clients = self._run(
[
"tmux",
"list-clients",
"-t",
self.session,
"-F",
"#{client_readonly}",
]
)
values = [
x.strip()
for x in clients.stdout.splitlines()
if x.strip()
]
readonly = sum(
1
for x in values
if x in ("1", "on", "true")
)
return {
"session": True,
"visible": self.visible_window(),
"pane_pid": pane_pid,
"pane_tty": pane_tty,
"pane_command": pane_command,
"clients": len(values),
"readonly_clients": readonly,
}
def execute(self, job_id, command, timeout_s=120):
if not self.JOB_RE.fullmatch(job_id):
raise FBTermError("JOB_ID invalido")
if not isinstance(command, str) or not command:
raise FBTermError("command vacio")
timeout_s = int(timeout_s)
if timeout_s < 1 or timeout_s > 3600:
raise FBTermError("timeout fuera de rango")
self.ensure(visible=True)
lock_fd = os.open(
self.lock_path,
os.O_CREAT | os.O_RDWR,
0o600,
)
try:
try:
fcntl.flock(
lock_fd,
fcntl.LOCK_EX | fcntl.LOCK_NB,
)
except BlockingIOError:
raise FBTermBusy("FBTERM ocupado")
# R2: fail-closed si existe evidencia durable incompleta.
# La reconciliacion conserva esos directorios fuera de jobs.
incomplete_jobs = []
try:
for prior in self.jobs.iterdir():
if (
prior.is_dir()
and (
not (prior / "DONE").exists()
or not (prior / "result.env").exists()
)
):
incomplete_jobs.append(prior.name)
except OSError as exc:
raise FBTermError(
"no se pudo auditar jobs previos: " + str(exc)
)
if incomplete_jobs:
sample = ", ".join(sorted(incomplete_jobs)[:3])
raise FBTermBusy(
"FBTERM tiene job incompleto: " + sample
)
job = self.jobs / job_id
if job.exists():
raise FBTermError("JOB_ID ya existe")
job.mkdir(mode=0o700)
command_file = job / "command.bash"
command_file.write_text(
command,
encoding="utf-8",
)
os.chmod(command_file, 0o600)
(job / "timeout.seconds").write_text(
str(timeout_s) + "\n",
encoding="ascii",
)
expected_command_sha = hashlib.sha256(
command.encode("utf-8")
).hexdigest()
# R2 idle gate: prompt.ready existe solo cuando el shell
# persistente esta realmente esperando el siguiente comando.
ready_path = self.jobs.parent / "prompt.ready"
ready_deadline = time.monotonic() + 3.0
while time.monotonic() < ready_deadline:
if ready_path.exists():
break
time.sleep(0.02)
else:
raise FBTermBusy(
"FBTERM pane no esta en prompt"
)
try:
ready_path.unlink()
except FileNotFoundError:
raise FBTermBusy(
"FBTERM prompt cambio antes del envio"
)
except OSError as exc:
raise FBTermError(
"no se pudo reservar prompt: " + str(exc)
)
send = self._run(
[
"tmux",
"send-keys",
"-t",
self.session + ":0.0",
"fbterm-run " + job_id,
"C-m",
]
)
if send.returncode != 0:
raise FBTermError(
"tmux send-keys fallo: "
+ send.stderr.strip()
)
deadline = time.monotonic() + timeout_s + 8
while time.monotonic() < deadline:
if (job / "DONE").exists():
break
time.sleep(0.05)
if not (job / "DONE").exists():
raise FBTermError(
"resultado durable no aparecio"
)
# R2: no liberar el lock hasta que bash haya vuelto realmente
# al prompt. fbterm-shell actualiza prompt.ready mediante
# PROMPT_COMMAND despues de que fbterm-run haya terminado.
done_path = job / "DONE"
prompt_deadline = time.monotonic() + 5.0
prompt_ready = False
while time.monotonic() < prompt_deadline:
try:
if (
ready_path.exists()
and ready_path.stat().st_mtime_ns
> done_path.stat().st_mtime_ns
):
prompt_ready = True
break
except OSError:
pass
time.sleep(0.02)
if not prompt_ready:
raise FBTermError(
"prompt durable no aparecio"
)
result = {}
for line in (job / "result.env").read_text(
encoding="utf-8"
).splitlines():
if "=" in line:
k, v = line.split("=", 1)
result[k] = v
result["EXPECTED_COMMAND_SHA256"] = (
expected_command_sha
)
result["COMMAND_HASH_GATE"] = (
"PASS"
if result.get("COMMAND_SHA256")
== expected_command_sha
else "FAIL"
)
result["STDOUT_PATH"] = str(
job / "stdout.bin"
)
result["STDERR_PATH"] = str(
job / "stderr.bin"
)
return result
finally:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
except Exception:
pass
os.close(lock_fd)
def file_text(path):
return Path(path).read_text(
encoding="utf-8",
errors="replace",
)
if __name__ == "__main__":
import json
import sys
root = os.environ["FBTERM_ROOT"]
session = os.environ["FBTERM_SESSION"]
title = os.environ["FBTERM_TITLE"]
term = FBTerm(root, session, title)
action = sys.argv[1]
if action == "ensure":
print(json.dumps(term.ensure(), sort_keys=True))
elif action == "status":
print(json.dumps(term.status(), sort_keys=True))
elif action == "exec":
job_id = sys.argv[2]
timeout_s = int(sys.argv[3])
command_path = sys.argv[4]
try:
r = term.execute(
job_id,
file_text(command_path),
timeout_s,
)
print(json.dumps(r, sort_keys=True))
except FBTermBusy as e:
print(json.dumps({
"ERROR": "BUSY",
"MESSAGE": str(e),
}))
sys.exit(75)
else:
raise SystemExit(64)
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
umask 077
ROOT="${FBTERM_ROOT:-$HOME/.local/share/framebridge/fbterm}"
LIB="$ROOT/library"
JOBS="$ROOT/jobs"
valid() {
[[ "$1" =~ ^[A-Za-z0-9._-]+$ ]]
}
secret_guard() {
grep -Eqi \
'BEGIN .*PRIVATE KEY|rclone\.conf|client_secret|access_token|refresh_token|password[[:space:]]*=|token[[:space:]]*=|cookie[[:space:]]*=' \
"$1"
}
case "${1:-}" in
list)
mkdir -p "$LIB"
find "$LIB" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' |
sed 's/\.sh$//' | sort
;;
adopt)
job="${2:-}"
name="${3:-}"
valid "$job" && valid "$name" || {
echo 'FBX_ERROR=INVALID_NAME'
exit 64
}
src="$JOBS/$job/command.bash"
dst="$LIB/$name.sh"
[ -f "$src" ] || {
echo 'FBX_ERROR=JOB_COMMAND_NOT_FOUND'
exit 66
}
[ ! -e "$dst" ] || {
echo 'FBX_ERROR=LIBRARY_NAME_EXISTS'
exit 73
}
cp -p "$src" "$dst"
chmod 600 "$dst"
sha256sum "$dst" | awk '{print $1}' > "$LIB/$name.sha256"
printf '%s\n' "$job" > "$LIB/$name.source-job"
echo "FBX_ADOPT=PASS NAME=$name JOB=$job SHA256=$(cat "$LIB/$name.sha256")"
;;
hash)
name="${2:-}"
valid "$name" || exit 64
file="$LIB/$name.sh"
[ -f "$file" ] || exit 66
sha256sum "$file"
;;
show|run)
action="$1"
name="${2:-}"
valid "$name" || {
echo 'FBX_ERROR=INVALID_NAME'
exit 64
}
file="$LIB/$name.sh"
hashfile="$LIB/$name.sha256"
[ -f "$file" ] && [ -f "$hashfile" ] || {
echo 'FBX_ERROR=LIBRARY_ITEM_NOT_FOUND'
exit 66
}
expected="$(cat "$hashfile")"
actual="$(sha256sum "$file" | awk '{print $1}')"
[ "$actual" = "$expected" ] || {
echo 'FBX_ERROR=HASH_MISMATCH'
exit 74
}
if secret_guard "$file"; then
echo 'FBX_ERROR=POSSIBLE_SECRET_REFUSE_DISPLAY'
exit 77
fi
echo
echo "========== FBX SCRIPT: $name =========="
echo "SHA256=$actual"
echo
cat "$file"
echo
echo "========== END FBX SCRIPT =========="
echo
[ "$action" = show ] && exit 0
bash --noprofile --norc "$file"
rc=$?
echo
echo "========== FBX RESULT: $name EXIT=$rc =========="
exit "$rc"
;;
status)
echo "FBX_ROOT=$ROOT"
echo "FBX_LIBRARY=$LIB"
echo "FBX_ITEMS=$(find "$LIB" -maxdepth 1 -type f -name '*.sh' | wc -l)"
;;
*)
echo 'USAGE: fbx {status|list|adopt JOB NAME|show NAME|run NAME|hash NAME}'
exit 64
;;
esac
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import json
import os
import socket
import threading
from pathlib import Path
_lock = threading.Lock()
_active = False
def _socket_path():
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"))
return runtime / "framebridge-voice-r1.sock"
def _listen_worker():
global _active
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(30)
s.connect(str(_socket_path()))
s.sendall(b'{"cmd":"listen_once"}\n')
f = s.makefile("rb")
f.readline() # respuesta local; NO se expone
s.close()
except Exception:
pass
finally:
with _lock:
_active = False
def start_listen_once():
global _active
with _lock:
if _active:
return {"ok": True, "started": False, "reason": "already_active"}
if not _socket_path().is_socket():
return {"ok": False, "error": "VOICE_DAEMON_NOT_AVAILABLE"}
_active = True
threading.Thread(target=_listen_worker, daemon=True).start()
return {"ok": True, "started": True}
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import json
import os
import stat
import time
from pathlib import Path
_ALLOWED = {"escuchando", "pensando", "trabajando", "listo", "detenido"}
_MAX_BYTES = 16384
_MAX_AGE_SECONDS = 5.0
def read_voice_status(home=None):
base = Path(home).expanduser() if home else Path.home()
path = base / ".local/share/framebridge-voice/state-r1.json"
safe = {
"state": "listo",
"available": False,
"updated_at": None,
}
try:
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
if hasattr(os, "O_NONBLOCK"):
flags |= os.O_NONBLOCK
fd = os.open(path, flags)
try:
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
return safe
if st.st_uid != os.getuid():
return safe
if st.st_size > _MAX_BYTES:
return safe
raw = os.read(fd, _MAX_BYTES + 1)
finally:
os.close(fd)
if len(raw) > _MAX_BYTES:
return safe
data = json.loads(raw.decode("utf-8"))
state = str(data.get("status", "listo")).lower()
if state not in _ALLOWED:
state = "listo"
updated = data.get("updated_at")
if not isinstance(updated, (int, float)):
return safe
age = max(0.0, time.time() - float(updated))
if age > _MAX_AGE_SECONDS:
return safe
if state == "detenido":
state = "listo"
return {
"state": state,
"available": True,
"updated_at": float(updated),
}
except (
FileNotFoundError,
PermissionError,
OSError,
ValueError,
json.JSONDecodeError,
):
return safe
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set +e
ACTION="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')"
[ -n "$ACTION" ] || ACTION="ENSURE"
[ "$ACTION" = "OPEN" ] && ACTION="ENSURE"
case "$ACTION" in
ENSURE|FOCUS|STATUS|CLOSE|TOGGLE) ;;
*)
echo "INTERACTIVE_TERMINAL_GATE=FAIL"
echo "ERROR=INVALID_ACTION"
exit 2
;;
esac
TITLE="FrameBridge - Terminal"
STATE_DIR="${FRAMEBRIDGE_UI_STATE_DIR:-$HOME/.local/share/framebridge/ui-state}"
STATE_FILE="$STATE_DIR/interactive-terminal.window"
DISPLAY_VALUE="$(printenv DISPLAY 2>/dev/null)"
[ -n "$DISPLAY_VALUE" ] || DISPLAY_VALUE=":0"
XAUTH_VALUE="$(printenv XAUTHORITY 2>/dev/null)"
[ -n "$XAUTH_VALUE" ] || XAUTH_VALUE="$HOME/.Xauthority"
TERM_FG="#a8ffb5"
TERM_BG="#010402"
TERM_CURSOR="#c8ffd1"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR" 2>/dev/null || true
window_exists()
{
W="$1"
[ -n "$W" ] || return 1
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool getwindowname "$W" >/dev/null 2>&1
}
read_state_window()
{
if [ -r "$STATE_FILE" ]; then
head -n1 "$STATE_FILE" 2>/dev/null | tr -cd '0-9'
fi
}
write_state_window()
{
W="$1"
printf '%s\n' "$W" > "$STATE_FILE"
chmod 600 "$STATE_FILE" 2>/dev/null || true
}
clear_state()
{
rm -f "$STATE_FILE"
}
W="$(read_state_window)"
if ! window_exists "$W"; then
W=""
clear_state
fi
if [ "$ACTION" = "STATUS" ]; then
echo "INTERACTIVE_TERMINAL_GATE=PASS"
if [ -n "$W" ]; then
echo "STATE=ON"
echo "WINDOW_ID=$W"
else
echo "STATE=OFF"
echo "WINDOW_ID="
fi
echo "INTERACTIVE=YES"
echo "STYLE=GREEN_MONOCHROME"
exit 0
fi
if [ "$ACTION" = "TOGGLE" ]; then
if [ -n "$W" ]; then
ACTION="CLOSE"
else
ACTION="ENSURE"
fi
fi
if [ "$ACTION" = "CLOSE" ]; then
if [ -n "$W" ]; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool windowclose "$W" >/dev/null 2>&1 || true
for _ in $(seq 1 30); do
if ! window_exists "$W"; then
break
fi
sleep 0.1
done
fi
clear_state
echo "INTERACTIVE_TERMINAL_GATE=PASS"
echo "ACTION=CLOSE"
echo "STATE=OFF"
echo "WINDOW_ID="
echo "INTERACTIVE=YES"
echo "STYLE=GREEN_MONOCHROME"
exit 0
fi
if [ -z "$W" ]; then
BEFORE="$(mktemp)"
AFTER="$(mktemp)"
trap 'rm -f "$BEFORE" "$AFTER"' EXIT
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" wmctrl -lx 2>/dev/null | awk '$3 ~ /gnome-terminal-server.Gnome-terminal/ {print $1}' | sort -u > "$BEFORE"
export FB_TERM_TITLE="$TITLE"
export FB_TERM_FG="$TERM_FG"
export FB_TERM_BG="$TERM_BG"
export FB_TERM_CURSOR="$TERM_CURSOR"
if command -v gnome-terminal >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" gnome-terminal --title="$TITLE" -- bash -lc '
printf "\033]0;%s\007" "$FB_TERM_TITLE"
printf "\033]10;%s\007" "$FB_TERM_FG"
printf "\033]11;%s\007" "$FB_TERM_BG"
printf "\033]12;%s\007" "$FB_TERM_CURSOR"
exec bash -l
' >/dev/null 2>&1 &
elif command -v xfce4-terminal >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xfce4-terminal --title="$TITLE" --command="bash -lc 'printf \"\\033]10;$TERM_FG\\007\\033]11;$TERM_BG\\007\"; exec bash -l'" >/dev/null 2>&1 &
elif command -v xterm >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xterm -T "$TITLE" -fg "$TERM_FG" -bg "$TERM_BG" -e bash -l >/dev/null 2>&1 &
else
echo "INTERACTIVE_TERMINAL_GATE=FAIL"
echo "ERROR=NO_SUPPORTED_TERMINAL"
exit 3
fi
for _ in $(seq 1 50); do
TITLE_W="$(DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool search --name "^$TITLE$" 2>/dev/null | tail -n1)"
if window_exists "$TITLE_W"; then
W="$TITLE_W"
break
fi
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" wmctrl -lx 2>/dev/null | awk '$3 ~ /gnome-terminal-server.Gnome-terminal/ {print $1}' | sort -u > "$AFTER"
NEW_HEX="$(comm -13 "$BEFORE" "$AFTER" | tail -n1)"
if [ -n "$NEW_HEX" ]; then
W="$((NEW_HEX))"
if window_exists "$W"; then
break
fi
W=""
fi
sleep 0.1
done
if [ -z "$W" ]; then
echo "INTERACTIVE_TERMINAL_GATE=FAIL"
echo "ERROR=WINDOW_NOT_FOUND"
exit 4
fi
write_state_window "$W"
fi
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool windowactivate --sync "$W" >/dev/null 2>&1 || true
echo "INTERACTIVE_TERMINAL_GATE=PASS"
echo "ACTION=$ACTION"
echo "STATE=ON"
echo "WINDOW_ID=$W"
echo "INTERACTIVE=YES"
echo "STYLE=GREEN_MONOCHROME"
echo "TERM_FG=$TERM_FG"
echo "TERM_BG=$TERM_BG"
exit 0
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set +e
ACTION="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')"
[ -n "$ACTION" ] || ACTION="ENSURE"
case "$ACTION" in
ENSURE|FOCUS|STATUS) ;;
*)
echo "PROGRESS_CONSOLE_GATE=FAIL"
echo "ERROR=INVALID_ACTION"
exit 2
;;
esac
TITLE="FrameBridge - Progreso DIG"
LOG_DIR="${FRAMEBRIDGE_PROGRESS_DIR:-$HOME/.local/state/framebridge/progress}"
LOG="${FRAMEBRIDGE_PROGRESS_LOG:-$LOG_DIR/framebridge-progress.log}"
DISPLAY_VALUE="$(printenv DISPLAY 2>/dev/null)"
[ -n "$DISPLAY_VALUE" ] || DISPLAY_VALUE=":0"
XAUTH_VALUE="$(printenv XAUTHORITY 2>/dev/null)"
[ -n "$XAUTH_VALUE" ] || XAUTH_VALUE="$HOME/.Xauthority"
mkdir -p "$LOG_DIR"
chmod 700 "$LOG_DIR" 2>/dev/null || true
touch "$LOG"
chmod 600 "$LOG" 2>/dev/null || true
if [ ! -s "$LOG" ]; then
cat > "$LOG" <<'HEADER'
============================================================
FRAMEBRIDGE ASUS // DIG PROGRESS
MODO: SOLO LECTURA
============================================================
HEADER
fi
find_window()
{
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool search --name "^$TITLE$" 2>/dev/null | tail -n1
}
W="$(find_window)"
if [ "$ACTION" = "STATUS" ]; then
echo "PROGRESS_CONSOLE_GATE=PASS"
if [ -n "$W" ]; then
echo "STATE=ON"
echo "WINDOW_ID=$W"
else
echo "STATE=OFF"
echo "WINDOW_ID="
fi
echo "LOG=$LOG"
echo "READ_ONLY=YES"
echo "STYLE=GREEN_MONOCHROME_DIG"
exit 0
fi
if [ -z "$W" ]; then
export FB_PROGRESS_LOG="$LOG"
export FB_PROGRESS_TITLE="$TITLE"
if command -v gnome-terminal >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" gnome-terminal --title="$TITLE" -- bash -lc '
printf "\033]0;%s\007" "$FB_PROGRESS_TITLE"
printf "\033]10;#a8ffb5\007"
printf "\033]11;#010402\007"
printf "\033[1;38;2;168;255;181m"
printf "============================================================\n"
printf " FRAMEBRIDGE // DIG PROGRESS // SOLO LECTURA\n"
printf "============================================================\n"
exec tail -n 200 -F "$FB_PROGRESS_LOG"
' >/dev/null 2>&1 &
elif command -v xterm >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xterm -T "$TITLE" -fg "#a8ffb5" -bg "#010402" -e bash -lc 'exec tail -n 200 -F "$FB_PROGRESS_LOG"' >/dev/null 2>&1 &
else
echo "PROGRESS_CONSOLE_GATE=FAIL"
echo "ERROR=NO_SUPPORTED_TERMINAL"
exit 3
fi
for _ in $(seq 1 50); do
W="$(find_window)"
[ -n "$W" ] && break
sleep 0.1
done
fi
if [ -z "$W" ]; then
echo "PROGRESS_CONSOLE_GATE=FAIL"
echo "ERROR=WINDOW_NOT_FOUND"
exit 4
fi
if command -v wmctrl >/dev/null 2>&1; then
GEOM="$(DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool getdisplaygeometry 2>/dev/null)"
SW="$(printf '%s' "$GEOM" | awk '{print $1}')"
SH="$(printf '%s' "$GEOM" | awk '{print $2}')"
if [ -n "$SW" ] && [ -n "$SH" ]; then
H=330
Y=$((SH - H - 28))
[ "$Y" -lt 0 ] && Y=0
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" wmctrl -ir "$W" -e "0,0,$Y,$SW,$H" >/dev/null 2>&1 || true
fi
fi
if [ "$ACTION" = "FOCUS" ]; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool windowactivate --sync "$W" >/dev/null 2>&1 || true
fi
echo "PROGRESS_CONSOLE_GATE=PASS"
echo "ACTION=$ACTION"
echo "STATE=ON"
echo "WINDOW_ID=$W"
echo "LOG=$LOG"
echo "READ_ONLY=YES"
echo "STYLE=GREEN_MONOCHROME_DIG"
exit 0
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SESSION="framebridge"
tmux has-session -t "$SESSION" 2>/dev/null || tmux new-session -d -s "$SESSION"
if command -v gnome-terminal >/dev/null 2>&1; then
exec gnome-terminal -- bash -lc "tmux attach -t '$SESSION'"
elif command -v xfce4-terminal >/dev/null 2>&1; then
exec xfce4-terminal --command="bash -lc \"tmux attach -t '$SESSION'\""
elif command -v konsole >/dev/null 2>&1; then
exec konsole -e bash -lc "tmux attach -t '$SESSION'"
elif command -v xterm >/dev/null 2>&1; then
exec xterm -e tmux attach -t "$SESSION"
else
echo "No encontré terminal gráfica compatible."
echo "Ejecuta manualmente: tmux attach -t $SESSION"
exit 1
fi
@@ -0,0 +1,425 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import re
import socket
import stat
import struct
import subprocess
import sys
from pathlib import Path
VERSION = 1
MAX_SCRIPT_BYTES = 262144
MAX_REQUEST_BYTES = 393216
JOB_RE = re.compile(r"^[A-Za-z0-9._-]{1,80}$")
HOME = Path.home()
FBROOT = HOME / ".local/share/framebridge/fbterm"
ENGINE = FBROOT / "bin/fbterm_engine.py"
ROOT = Path(
os.environ.get(
"FRAMEBRIDGE_TOOL_ROOT",
str(HOME / ".local/share/framebridge/tool-adapter")
)
)
SOCKET_PATH = ROOT / "adapter.sock"
INBOX = ROOT / "inbox"
PID_FILE = ROOT / "adapter.pid"
NATIVE_HOST = Path(os.environ.get("FRAMEBRIDGE_NATIVE_HOST", str(HOME / ".local/lib/framebridge-native/host/framebridge_host.py")))
def prepare_root():
ROOT.mkdir(parents=True, exist_ok=True)
INBOX.mkdir(parents=True, exist_ok=True)
os.chmod(ROOT, 0o700)
os.chmod(INBOX, 0o700)
def recv_request(conn):
data = bytearray()
while True:
chunk = conn.recv(65536)
if not chunk:
break
data.extend(chunk)
if len(data) > MAX_REQUEST_BYTES:
raise ValueError("REQUEST_TOO_LARGE")
if b"\n" in data:
break
raw = bytes(data).split(b"\n", 1)[0]
if not raw:
raise ValueError("EMPTY_REQUEST")
return json.loads(raw.decode("utf-8"))
def send_response(conn, obj):
raw = (
json.dumps(
obj,
ensure_ascii=True,
sort_keys=True,
separators=(",", ":"),
)
+ "\n"
).encode("utf-8")
try:
conn.sendall(raw)
except (BrokenPipeError, ConnectionResetError):
return False
return True
def peer_uid(conn):
fmt = "3i"
raw = conn.getsockopt(
socket.SOL_SOCKET,
socket.SO_PEERCRED,
struct.calcsize(fmt),
)
_pid, uid, _gid = struct.unpack(fmt, raw)
return uid
def last_result_final(stdout_path):
try:
lines = Path(stdout_path).read_text(
encoding="utf-8",
errors="replace",
).splitlines()
except OSError:
return None
for line in reversed(lines):
if line.startswith("RESULTADO_FINAL="):
return line.split("=", 1)[1].strip()[:2000]
return None
def engine_result(stdout):
for line in reversed(stdout.splitlines()):
line = line.strip()
if not line.startswith("{"):
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(obj, dict):
return obj
return None
def native_cua_roundtrip(payload, timeout_s=60):
if not NATIVE_HOST.is_file():
raise ValueError("FRAMEBRIDGE_NATIVE_HOST_NOT_FOUND")
raw = json.dumps(
payload,
ensure_ascii=True,
separators=(",", ":"),
).encode("utf-8")
frame = struct.pack("<I", len(raw)) + raw
try:
proc = subprocess.run(
[str(NATIVE_HOST)],
input=frame,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout_s,
check=False,
)
except subprocess.TimeoutExpired:
raise ValueError("CUA_NATIVE_HOST_TIMEOUT")
out = proc.stdout or b""
if len(out) < 4:
raise ValueError("CUA_NATIVE_RESPONSE_HEADER_MISSING")
size = struct.unpack("<I", out[:4])[0]
if size <= 0 or size > 2_000_000:
raise ValueError("CUA_NATIVE_RESPONSE_SIZE_INVALID")
body = out[4:4 + size]
if len(body) != size:
raise ValueError("CUA_NATIVE_RESPONSE_TRUNCATED")
try:
response = json.loads(body.decode("utf-8"))
except Exception:
raise ValueError("CUA_NATIVE_RESPONSE_INVALID_JSON")
if not isinstance(response, dict):
raise ValueError("CUA_NATIVE_RESPONSE_NOT_OBJECT")
return response
def handle_cua_request(req, job_id):
timeout_s = int(req.get("timeout_s", 60))
if not 1 <= timeout_s <= 180:
raise ValueError("INVALID_CUA_TIMEOUT")
cua = req.get("cua")
if not isinstance(cua, dict):
raise ValueError("CUA_PAYLOAD_REQUIRED")
payload = dict(cua)
payload["op"] = "cua"
payload["id"] = job_id
payload["target"] = "ASUS"
payload["authorized"] = "YES"
response = native_cua_roundtrip(
payload,
timeout_s=timeout_s,
)
return {
"version": VERSION,
"ok": bool(response.get("executed")),
"job_id": job_id,
"mode": "CUA",
"action": str(payload.get("action", ""))[:32],
"channel": str(response.get("channel", "CUA-DIRECT")),
"exit_code": response.get("exit_code"),
"frame_output": str(response.get("output", "")),
"error": str(response.get("error", "")),
}
def handle(req):
if not isinstance(req, dict):
raise ValueError("REQUEST_NOT_OBJECT")
if req.get("version") != VERSION:
raise ValueError("UNSUPPORTED_VERSION")
job_id = str(req.get("job_id", ""))
if not JOB_RE.fullmatch(job_id):
raise ValueError("INVALID_JOB_ID")
mode = str(req.get("mode", "EXEC")).upper()
if mode != "EXEC":
raise ValueError("UNSUPPORTED_MODE")
if req.get("authorized") is not True:
raise ValueError("AUTHORIZED_TRUE_REQUIRED")
request_type = str(
req.get("request_type", "exec")
).lower().strip()
if request_type == "cua":
return handle_cua_request(req, job_id)
if request_type != "exec":
raise ValueError("UNSUPPORTED_REQUEST_TYPE")
script = req.get("script")
if not isinstance(script, str) or not script:
raise ValueError("SCRIPT_REQUIRED")
script_bytes = script.encode("utf-8")
if len(script_bytes) > MAX_SCRIPT_BYTES:
raise ValueError("SCRIPT_TOO_LARGE")
timeout_s = int(req.get("timeout_s", 120))
if not 1 <= timeout_s <= 600:
raise ValueError("INVALID_TIMEOUT")
digest = hashlib.sha256(script_bytes).hexdigest()
expected = req.get("sha256")
if expected is not None:
expected = str(expected).lower()
if digest != expected:
raise ValueError("SCRIPT_SHA256_MISMATCH")
command_path = INBOX / f"{job_id}.bash"
temp_path = INBOX / f".{job_id}.tmp"
temp_path.write_bytes(script_bytes)
os.chmod(temp_path, 0o600)
temp_path.replace(command_path)
proc = subprocess.run(
[
sys.executable,
str(ENGINE),
"exec",
job_id,
str(timeout_s),
str(command_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout_s + 45,
check=False,
env={
**os.environ,
"FBTERM_ROOT": str(FBROOT),
"FBTERM_SESSION": os.environ.get(
"FBTERM_SESSION",
"framebridge-fbterm-r1",
),
"FBTERM_TITLE": os.environ.get(
"FBTERM_TITLE",
"FrameBridge Terminal R1",
),
},
)
result = engine_result(proc.stdout)
if result is None:
return {
"version": VERSION,
"ok": False,
"job_id": job_id,
"adapter_error": "ENGINE_RESULT_NOT_FOUND",
"engine_rc": proc.returncode,
"engine_stderr": proc.stderr[-2000:],
"command_sha256": digest,
}
stdout_path = result.get("STDOUT_PATH")
stderr_path = result.get("STDERR_PATH")
return {
"version": VERSION,
"ok": True,
"job_id": job_id,
"mode": mode,
"command_sha256": digest,
"engine_rc": proc.returncode,
"exit_code": result.get("EXIT"),
"result_final": (
last_result_final(stdout_path)
if stdout_path
else None
),
"stdout_path": stdout_path,
"stderr_path": stderr_path,
"stdout_sha256": result.get("STDOUT_SHA256"),
"stderr_sha256": result.get("STDERR_SHA256"),
"command_hash_gate": result.get(
"COMMAND_HASH_GATE"
),
"pty": result.get("PTY"),
"pane_pid": result.get("PANE_PID"),
"pane_tty": result.get("PANE_TTY"),
"shell_ready": result.get("SHELL_READY"),
}
def serve():
prepare_root()
if not ENGINE.is_file():
raise RuntimeError("FBTERM_ENGINE_NOT_FOUND")
if SOCKET_PATH.exists():
st = SOCKET_PATH.lstat()
if not stat.S_ISSOCK(st.st_mode):
raise RuntimeError("SOCKET_PATH_OCCUPIED")
SOCKET_PATH.unlink()
server = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
)
server.bind(str(SOCKET_PATH))
os.chmod(SOCKET_PATH, 0o600)
server.listen(4)
PID_FILE.write_text(
str(os.getpid()) + "\n",
encoding="ascii",
)
os.chmod(PID_FILE, 0o600)
try:
while True:
conn, _ = server.accept()
with conn:
try:
if peer_uid(conn) != os.getuid():
raise PermissionError(
"PEER_UID_MISMATCH"
)
req = recv_request(conn)
resp = handle(req)
except Exception as exc:
resp = {
"version": VERSION,
"ok": False,
"adapter_error": (
f"{type(exc).__name__}:"
f"{exc}"
)[:2000],
}
send_response(conn, resp)
finally:
server.close()
try:
SOCKET_PATH.unlink()
except FileNotFoundError:
pass
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else ""
if action == "serve":
serve()
else:
print(
json.dumps(
{
"ok": False,
"error": "USAGE: serve",
},
sort_keys=True,
)
)
raise SystemExit(64)
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
import json
import os
import socket
import sys
from pathlib import Path
HOME = Path.home()
ROOT = Path(
os.environ.get(
"FRAMEBRIDGE_TOOL_ROOT",
str(HOME / ".local/share/framebridge/tool-adapter")
)
)
SOCKET_PATH = ROOT / "adapter.sock"
if len(sys.argv) != 2:
print("USAGE: framebridge_tool_client.py REQUEST.json")
raise SystemExit(64)
request_path = Path(sys.argv[1])
request = json.loads(
request_path.read_text(encoding="utf-8")
)
raw = (
json.dumps(
request,
ensure_ascii=True,
separators=(",", ":"),
)
+ "\n"
).encode("utf-8")
sock = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
)
sock.connect(str(SOCKET_PATH))
sock.sendall(raw)
sock.shutdown(socket.SHUT_WR)
data = bytearray()
while True:
chunk = sock.recv(65536)
if not chunk:
break
data.extend(chunk)
sock.close()
response = json.loads(
bytes(data).decode("utf-8")
)
print(
json.dumps(
response,
ensure_ascii=True,
sort_keys=True,
)
)