FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,129 @@
|
||||
"""Local-only JSON-lines Unix socket prototype. No TCP listener, no shell."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import socketserver
|
||||
import stat
|
||||
import struct
|
||||
from .core import Manager, TerminalDenied
|
||||
|
||||
MAX_REQUEST = 65536
|
||||
|
||||
|
||||
class Handler(socketserver.StreamRequestHandler):
|
||||
def handle(self):
|
||||
# On Linux: SO_PEERCRED is the connecting process's kernel UID.
|
||||
try:
|
||||
_, uid, _ = struct.unpack('3i', self.request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))
|
||||
if uid != self.server.owner_uid:
|
||||
return
|
||||
raw = self.rfile.readline(MAX_REQUEST + 1)
|
||||
if len(raw) > MAX_REQUEST or not raw.endswith(b'\n'):
|
||||
self.send({'ok': False, 'error': 'REQUEST_TOO_LARGE_OR_TRUNCATED'})
|
||||
return
|
||||
req = json.loads(raw)
|
||||
if type(req) is not dict or type(req.get('op')) is not str:
|
||||
self.send({'ok': False, 'error': 'INVALID_REQUEST'})
|
||||
return
|
||||
# Only specific RPC shapes. The caller cannot supply binary argv,
|
||||
# host path, a shell, an approval bool, or an OwnerDevice verifier.
|
||||
op = req['op']
|
||||
# Enforce exact wire shape for every operation. Untrusted clients
|
||||
# may not attach arbitrary authority or additional arguments.
|
||||
permitted = {
|
||||
'status': ({'op'},),
|
||||
'open': ({'op'}, {'op','program'}),
|
||||
'observe': ({'op','session_id'},),
|
||||
'resize': ({'op','session_id','rows','cols'},),
|
||||
'abort': ({'op','session_id'},),
|
||||
'key.send': ({'op','session_id','key','owner_assertion'},),
|
||||
'text.type': ({'op','session_id','text','owner_assertion'},),
|
||||
}
|
||||
accepted = permitted.get(op)
|
||||
if accepted is None:
|
||||
raise TerminalDenied('UNKNOWN_OPERATION')
|
||||
if set(req) not in accepted:
|
||||
raise TerminalDenied('REQUEST_INVALID')
|
||||
m = self.server.manager
|
||||
if op == 'status': data = m.status()
|
||||
elif op == 'open': data = m.open(req.get('program', 'demo-readonly'))
|
||||
elif op == 'observe': data = m.observe(req['session_id'])
|
||||
elif op == 'resize': data = m.resize(req['session_id'], req['rows'], req['cols'])
|
||||
elif op == 'abort': data = m.abort(req['session_id'])
|
||||
elif op in ('key.send', 'text.type'):
|
||||
# Server default remains Manager(owner_verifier=None) -> DENY.
|
||||
# A separately deployed trusted bootstrap must bind a real
|
||||
# WebAuthn/Owner verifier. Neither AUTHORIZED nor an RPC bool
|
||||
# can alter the verifier. Never accept arbitrary shell/argv.
|
||||
if m._gate is None:
|
||||
raise TerminalDenied('OWNER_DEVICE_NOT_CONNECTED')
|
||||
if type(req.get('owner_assertion')) is not dict:
|
||||
raise TerminalDenied('OWNER_APPROVAL_REQUIRED')
|
||||
required = ({'op', 'session_id', 'key', 'owner_assertion'}
|
||||
if op == 'key.send' else
|
||||
{'op', 'session_id', 'text', 'owner_assertion'})
|
||||
if set(req) != required:
|
||||
raise TerminalDenied('REQUEST_INVALID')
|
||||
if op == 'key.send':
|
||||
data = m.key(req['session_id'], req['key'],
|
||||
approved_context=req['owner_assertion'])
|
||||
else:
|
||||
data = m.text(req['session_id'], req['text'],
|
||||
approved_context=req['owner_assertion'])
|
||||
else: raise TerminalDenied('UNKNOWN_OPERATION')
|
||||
self.send({'ok': True, 'result': data})
|
||||
except (ValueError, KeyError, TypeError, json.JSONDecodeError):
|
||||
self.send({'ok': False, 'error': 'REQUEST_INVALID'})
|
||||
except TerminalDenied as exc:
|
||||
self.send({'ok': False, 'error': str(exc)})
|
||||
except Exception:
|
||||
# Never echo arbitrary error content, terminal output or secrets.
|
||||
self.send({'ok': False, 'error': 'INTERNAL_ERROR'})
|
||||
|
||||
def send(self, data):
|
||||
self.wfile.write((json.dumps(data, ensure_ascii=True, separators=(',', ':')) + '\n').encode())
|
||||
|
||||
|
||||
class LocalServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = False
|
||||
def __init__(self, path, manager=None):
|
||||
if os.geteuid() == 0:
|
||||
raise TerminalDenied('ROOT_SERVER_FORBIDDEN')
|
||||
p = Path(path).absolute()
|
||||
p.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
# Do not operate in a shared or symlinked parent directory.
|
||||
if p.parent.is_symlink() or stat.S_IMODE(p.parent.stat().st_mode) != 0o700:
|
||||
raise TerminalDenied('SOCKET_PARENT_NOT_PRIVATE')
|
||||
if p.exists() or p.is_symlink():
|
||||
raise TerminalDenied('SOCKET_ALREADY_EXISTS')
|
||||
self.owner_uid = os.geteuid()
|
||||
self.manager = manager or Manager()
|
||||
self._socket_path = p
|
||||
old = os.umask(0o077)
|
||||
try: super().__init__(str(p), Handler)
|
||||
finally: os.umask(old)
|
||||
os.chmod(p, 0o600)
|
||||
|
||||
def server_close(self):
|
||||
self.manager.shutdown()
|
||||
super().server_close()
|
||||
try: self._socket_path.unlink(missing_ok=True)
|
||||
except OSError: pass
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description='FrameBridge CUA-TTY staging demo, local socket only')
|
||||
parser.add_argument('--socket', default=str(Path.home()/'.local/share/framebridge-cua-tty-r1/agent.sock'))
|
||||
args = parser.parse_args(argv)
|
||||
with LocalServer(args.socket) as server:
|
||||
print('CUA_TTY_R1_LOCAL_ONLY_SOCKET_READY', flush=True)
|
||||
try: server.serve_forever(poll_interval=0.2)
|
||||
except KeyboardInterrupt: pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user