303 lines
9.2 KiB
Python
Executable File
303 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Elevation adapter: runs commands outside the Landlock sandbox with streaming I/O.
|
|
|
|
Creates a Unix socket at OLLIE_ELEVATE_SOCKET, accepts {cmd, cwd} requests
|
|
from x/elevate, sends them to superpowerd via ExecuteCommandStreaming with
|
|
fd-passing, and streams the output back to the client in real-time.
|
|
|
|
Protocol (client-facing, streaming):
|
|
Client sends: {"cmd": ..., "cwd": ...}\n then shutdown(WR)
|
|
Server sends frames: <type:1><length:4 big-endian><payload>
|
|
type 'd' = output data (combined stdout+stderr)
|
|
type 'x' = exit code (payload is ASCII integer)
|
|
|
|
Usage:
|
|
superpowers run-session -- python3 elevate-adapter-superpowerd
|
|
|
|
Environment:
|
|
OLLIE_ELEVATE_SOCKET path for the listening socket
|
|
(default: $XDG_RUNTIME_DIR/ollie/elevate.sock)
|
|
SUPERPOWERD_SESSION_TOKEN set automatically by superpowers run-session
|
|
SUPERPOWERD_SOCKET override superpowerd socket path
|
|
SUPERPOWERD_SOCKET_DIR directory containing superpowerd.sock
|
|
"""
|
|
import sys
|
|
sys.path = sys.path[1:] # strip script dir — may be a non-navigable fs (e.g. 9P)
|
|
import array
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import struct
|
|
import subprocess
|
|
import threading
|
|
|
|
_token_lock = threading.Lock()
|
|
|
|
|
|
def refresh_token():
|
|
"""Obtain a fresh session token from superpowerd via run-session.
|
|
|
|
Returns the new token string, or None on failure.
|
|
Updates os.environ so subsequent calls use the fresh token.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["superpowers", "run-session", "--child-session", "--",
|
|
"sh", "-c", "echo $SUPERPOWERD_SESSION_TOKEN"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
token = result.stdout.strip()
|
|
if token and result.returncode == 0:
|
|
os.environ["SUPERPOWERD_SESSION_TOKEN"] = token
|
|
print(f"elevate-adapter-superpowerd: refreshed session token", file=sys.stderr)
|
|
return token
|
|
except (subprocess.TimeoutExpired, OSError) as e:
|
|
print(f"elevate-adapter-superpowerd: token refresh failed: {e}", file=sys.stderr)
|
|
return None
|
|
|
|
|
|
def default_socket_path():
|
|
xdg = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
|
|
return os.path.join(xdg, "ollie", "elevate.sock")
|
|
|
|
|
|
def sp_socket_path():
|
|
if p := os.environ.get("SUPERPOWERD_SOCKET"):
|
|
return p
|
|
if d := os.environ.get("SUPERPOWERD_SOCKET_DIR"):
|
|
return d + "/superpowerd.sock"
|
|
if x := os.environ.get("XDG_RUNTIME_DIR"):
|
|
return x + "/superpowerd/superpowerd.sock"
|
|
return f"/run/user/{os.getuid()}/superpowerd/superpowerd.sock"
|
|
|
|
|
|
def send_frame(conn, frame_type, payload):
|
|
"""Send a framed message: <type:1><length:4 big-endian><payload>"""
|
|
data = payload if isinstance(payload, bytes) else payload.encode()
|
|
header = frame_type.encode() + struct.pack(">I", len(data))
|
|
conn.sendall(header + data)
|
|
|
|
|
|
def send_error(conn, msg, exit_code=1):
|
|
"""Send an error message and exit frame."""
|
|
send_frame(conn, "d", msg + "\n")
|
|
send_frame(conn, "x", str(exit_code))
|
|
|
|
|
|
def _is_auth_error(sp_data):
|
|
"""Check if superpowerd response is an authentication/session error."""
|
|
if not sp_data:
|
|
return False
|
|
try:
|
|
resp = json.loads(sp_data)
|
|
err = resp.get("error")
|
|
if isinstance(err, dict):
|
|
msg = err.get("message", "").lower()
|
|
return "invalid session token" in msg or "session not found" in msg
|
|
except (json.JSONDecodeError, ValueError):
|
|
pass
|
|
return False
|
|
|
|
|
|
def _execute_on_superpowerd(conn, cmd, cwd, token):
|
|
"""Execute a command via superpowerd. Returns (sp_data, True) on success/normal
|
|
error, or (sp_data, False) if the caller should retry (auth error, no output streamed)."""
|
|
sp_path = sp_socket_path()
|
|
|
|
# Connect to superpowerd via SEQPACKET
|
|
sp = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
|
|
try:
|
|
sp.connect(sp_path)
|
|
except OSError as e:
|
|
send_error(conn, f"elevate-adapter: connect {sp_path}: {e}")
|
|
return None, True
|
|
|
|
# Create pipes for stdout+stderr (merged) and stdin (/dev/null)
|
|
stdin_r = os.open("/dev/null", os.O_RDONLY)
|
|
stdout_r, stdout_w = os.pipe()
|
|
|
|
# Build JSON-RPC request
|
|
sp_req = json.dumps({
|
|
"jsonrpc": "2.0",
|
|
"method": "ExecuteCommandStreaming",
|
|
"id": 1,
|
|
"params": {
|
|
"auth": {"session_token": token},
|
|
"command": cmd,
|
|
"working_dir": cwd,
|
|
"approval_timeout": 7200,
|
|
},
|
|
}).encode()
|
|
|
|
# Send request + 3 fds (stdin, stdout, stderr) via SCM_RIGHTS
|
|
fds = array.array("i", [stdin_r, stdout_w, stdout_w])
|
|
ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
|
|
try:
|
|
sp.sendmsg([sp_req], ancdata)
|
|
except OSError as e:
|
|
os.close(stdin_r)
|
|
os.close(stdout_r)
|
|
os.close(stdout_w)
|
|
sp.close()
|
|
send_error(conn, f"elevate-adapter: sendmsg: {e}")
|
|
return None, True
|
|
|
|
# Close write-ends in this process (superpowerd has them now)
|
|
os.close(stdin_r)
|
|
os.close(stdout_w)
|
|
|
|
# Stream output from the pipe to the elevate client
|
|
stream_done = threading.Event()
|
|
client_broken = threading.Event()
|
|
output_received = threading.Event()
|
|
|
|
def stream_output():
|
|
try:
|
|
while True:
|
|
chunk = os.read(stdout_r, 4096)
|
|
if not chunk:
|
|
break
|
|
output_received.set()
|
|
send_frame(conn, "d", chunk)
|
|
except (BrokenPipeError, OSError):
|
|
client_broken.set()
|
|
finally:
|
|
os.close(stdout_r)
|
|
stream_done.set()
|
|
|
|
t = threading.Thread(target=stream_output, daemon=True)
|
|
t.start()
|
|
|
|
# Wait for superpowerd's response (comes after command exits)
|
|
try:
|
|
sp_data = sp.recv(1024 * 1024)
|
|
except OSError:
|
|
sp_data = b""
|
|
sp.close()
|
|
|
|
# Wait for streaming thread to finish
|
|
stream_done.wait()
|
|
|
|
if client_broken.is_set():
|
|
return None, True
|
|
|
|
# If auth error and no output was streamed, signal retry
|
|
if _is_auth_error(sp_data) and not output_received.is_set():
|
|
return sp_data, False
|
|
|
|
return sp_data, True
|
|
|
|
|
|
def handle(conn):
|
|
with conn:
|
|
try:
|
|
_handle_inner(conn)
|
|
except Exception as e:
|
|
try:
|
|
send_error(conn, f"elevate-adapter: internal error: {e}")
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _handle_inner(conn):
|
|
data = b""
|
|
while chunk := conn.recv(65536):
|
|
data += chunk
|
|
if b"\n" in data:
|
|
break
|
|
|
|
try:
|
|
req = json.loads(data)
|
|
except json.JSONDecodeError as e:
|
|
send_error(conn, f"elevate-adapter: bad request: {e}")
|
|
return
|
|
|
|
cmd = req.get("cmd", "")
|
|
cwd = req.get("cwd", os.getcwd())
|
|
|
|
token = os.environ.get("SUPERPOWERD_SESSION_TOKEN", "")
|
|
|
|
# First attempt
|
|
sp_data, done = _execute_on_superpowerd(conn, cmd, cwd, token)
|
|
if done:
|
|
_finish_response(conn, sp_data)
|
|
return
|
|
|
|
# Auth error — refresh token and retry once
|
|
with _token_lock:
|
|
# Re-read in case another thread already refreshed
|
|
current = os.environ.get("SUPERPOWERD_SESSION_TOKEN", "")
|
|
if current == token:
|
|
new_token = refresh_token()
|
|
else:
|
|
new_token = current
|
|
|
|
if not new_token:
|
|
# Refresh failed — report the original auth error
|
|
_finish_response(conn, sp_data)
|
|
return
|
|
|
|
sp_data, _ = _execute_on_superpowerd(conn, cmd, cwd, new_token)
|
|
_finish_response(conn, sp_data)
|
|
|
|
|
|
def _finish_response(conn, sp_data):
|
|
"""Parse superpowerd response and send exit code frame to client."""
|
|
if sp_data is None:
|
|
return
|
|
|
|
exit_code = 1
|
|
try:
|
|
sp_resp = json.loads(sp_data)
|
|
rpc_err = sp_resp.get("error")
|
|
if rpc_err:
|
|
msg = rpc_err.get("message", str(rpc_err)) if isinstance(rpc_err, dict) else str(rpc_err)
|
|
send_frame(conn, "d", f"elevate-adapter: {msg}\n".encode())
|
|
else:
|
|
result = sp_resp.get("result", {})
|
|
exit_code = result.get("exit_code", 0)
|
|
except (json.JSONDecodeError, ValueError):
|
|
pass
|
|
|
|
send_frame(conn, "x", str(exit_code))
|
|
|
|
|
|
def main():
|
|
if "SUPERPOWERD_SESSION_TOKEN" not in os.environ:
|
|
os.execvp("superpowers", ["superpowers", "run-session", "--",
|
|
sys.executable, __file__] + sys.argv[1:])
|
|
|
|
sock_path = os.environ.get("OLLIE_ELEVATE_SOCKET") or default_socket_path()
|
|
sock_path = os.path.expanduser(sock_path)
|
|
os.makedirs(os.path.dirname(sock_path), exist_ok=True)
|
|
|
|
if os.path.exists(sock_path):
|
|
os.unlink(sock_path)
|
|
|
|
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
srv.bind(sock_path)
|
|
srv.listen()
|
|
|
|
def shutdown(sig, frame):
|
|
srv.close()
|
|
if os.path.exists(sock_path):
|
|
os.unlink(sock_path)
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGTERM, shutdown)
|
|
signal.signal(signal.SIGINT, shutdown)
|
|
|
|
print(f"elevate-adapter-superpowerd: listening on {sock_path}", file=sys.stderr)
|
|
|
|
while True:
|
|
try:
|
|
conn, _ = srv.accept()
|
|
except OSError:
|
|
break
|
|
threading.Thread(target=handle, args=(conn,), daemon=True).start()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|