implement generic x/elevate with socket broker pattern
x/elevate is now a generic client: reads OLLIE_ELEVATE_SOCKET, sends
{cmd,cwd}, reads back {stdout,stderr,exit_code}. All backend-specific
logic moves to adapters. Add elevate-adapter-superpowerd as the
reference adapter: owns the socket lifecycle, translates the ollie
protocol to superpowerd JSON-RPC ExecuteCommand. Add OLLIE_ELEVATE_SOCKET
to env docs. Update scripts/README.md with protocol spec, adapter
setup, and guide for writing custom adapters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1de43c1105
commit
7a88fb3437
|
|
@ -55,6 +55,7 @@ OLLIE_KIRO_TOKEN=... # bearer token or sqlite:// path (auto-detected f
|
|||
OLLIE_MODEL=qwen/qwen3-235b-a22b
|
||||
OLLIE_TOOLS_PATH=~/.config/ollie/tools # directory for tool scripts ({tool} steps)
|
||||
OLLIE_MEMORY_PATH=~/.config/ollie/memory # directory for memory files (ollie/m)
|
||||
OLLIE_ELEVATE_SOCKET=~/.run/ollie-elevate.sock # socket path for x/elevate adapter
|
||||
```
|
||||
|
||||
Shell environment variables take precedence over the env file.
|
||||
|
|
|
|||
|
|
@ -209,77 +209,52 @@ Outputs to stdout, making it composable with other pipeline stages.
|
|||
## x/elevate
|
||||
|
||||
Sandbox escape hatch invoked by `execute_code` when a step is marked `elevated: true`.
|
||||
The Landlock sandbox is enforced at the kernel level — certain operations (filesystem
|
||||
paths outside the sandbox config, specific syscalls) are simply blocked regardless of
|
||||
what the code does. `x/elevate` provides a controlled way to run those steps outside
|
||||
the sandbox entirely, without changing user privileges.
|
||||
The Landlock sandbox is enforced at the kernel level — restrictions apply regardless of
|
||||
user privileges within the sandboxed process. `x/elevate` is a generic client that
|
||||
forwards the command to an adapter running outside the sandbox, which executes it and
|
||||
returns the result.
|
||||
|
||||
### Interface
|
||||
`x/elevate` knows nothing about the backend. It connects to the socket at
|
||||
`OLLIE_ELEVATE_SOCKET`, sends `{cmd, cwd}`, and reads back `{stdout, stderr,
|
||||
exit_code}`. The adapter on the other end is backend-specific. Multiple adapters can
|
||||
coexist at different socket paths; switching backends is a single env var change.
|
||||
|
||||
`execute_code` resolves the plugin path and calls:
|
||||
Set `OLLIE_ELEVATE_SOCKET` in `~/.config/ollie/env`:
|
||||
|
||||
```sh
|
||||
x/elevate -- <cmd>
|
||||
```
|
||||
OLLIE_ELEVATE_SOCKET=~/.run/ollie-elevate.sock
|
||||
```
|
||||
|
||||
The working directory is already set to the session's `cwd` before the call. The
|
||||
script must:
|
||||
### Protocol
|
||||
|
||||
- Write combined stdout/stderr to its own stdout
|
||||
- Exit with the command's exit code (non-zero is treated as failure)
|
||||
`x/elevate` sends a newline-terminated JSON object and reads a JSON response:
|
||||
|
||||
That's the entire contract. Any script that satisfies it is a valid backend.
|
||||
|
||||
The plugin is looked up in `OLLIE_PLUGINS_PATH` (first colon-separated entry) if set,
|
||||
otherwise in `~/.config/ollie/scripts/x/`. Override via env to use a different location
|
||||
without modifying the installed scripts.
|
||||
|
||||
### The constraint
|
||||
|
||||
Because Landlock is kernel-enforced, the backend cannot simply exec the command inside
|
||||
the sandboxed process — the restrictions apply regardless. The command must reach a
|
||||
process that was spawned outside of landrun entirely. The only viable pattern is to
|
||||
forward the command to an external daemon over a socket or pipe, where that daemon runs
|
||||
in an unsandboxed context and sends the result back.
|
||||
|
||||
### Minimal example
|
||||
|
||||
The simplest possible backend — direct exec, no sandbox. Suitable for development or
|
||||
environments where the sandbox itself is not in use:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# x/elevate: run command outside the sandbox (no landrun in this environment).
|
||||
shift # drop the '--'
|
||||
exec sh -c "$1"
|
||||
```
|
||||
→ {"cmd": "<shell command>", "cwd": "<working directory>"}\n
|
||||
← {"stdout": "...", "stderr": "...", "exit_code": 0}
|
||||
```
|
||||
|
||||
### superpowerd example
|
||||
Any process that listens on a Unix stream socket and speaks this protocol is a valid
|
||||
adapter.
|
||||
|
||||
The default `x/elevate` uses superpowerd as its backend. superpowerd is a hypothetical
|
||||
external command broker: a daemon running outside any sandbox that receives
|
||||
`ExecuteCommand` requests over a Unix socket, runs them, and returns the result. This
|
||||
satisfies the constraint above — the command executes in the daemon's process, which
|
||||
was never wrapped by landrun.
|
||||
### x/elevate-adapter-superpowerd
|
||||
|
||||
**How it works, step by step:**
|
||||
The included adapter bridges the ollie elevate protocol to superpowerd, a hypothetical
|
||||
external command broker. superpowerd is a daemon running outside any sandbox that
|
||||
accepts `ExecuteCommand` requests over its own Unix socket and runs them in an
|
||||
unsandboxed context — satisfying the kernel-level constraint.
|
||||
|
||||
1. If `SUPERPOWERD_SESSION_TOKEN` is not in the environment, the script re-execs itself
|
||||
under `superpowers run-session`, which registers a session with the daemon and
|
||||
re-runs the script with the token set:
|
||||
**Start the adapter** (outside the sandbox, once per session):
|
||||
|
||||
```python
|
||||
if "SUPERPOWERD_SESSION_TOKEN" not in os.environ:
|
||||
os.execvp("superpowers", ["superpowers", "run-session", "--",
|
||||
sys.executable, __file__] + sys.argv[1:])
|
||||
```
|
||||
```sh
|
||||
superpowers run-session -- python3 $OLLIE/x/elevate-adapter-superpowerd
|
||||
```
|
||||
|
||||
2. With the token present, the script connects to the superpowerd Unix socket. The
|
||||
socket path is resolved in order from `SUPERPOWERD_SOCKET`,
|
||||
`SUPERPOWERD_SOCKET_DIR/superpowerd.sock`, `$XDG_RUNTIME_DIR/superpowerd/superpowerd.sock`,
|
||||
or the default `/run/user/<uid>/superpowerd/superpowerd.sock`.
|
||||
`superpowers run-session` acquires a session token and re-runs the adapter with
|
||||
`SUPERPOWERD_SESSION_TOKEN` set. The adapter then:
|
||||
|
||||
3. A JSON-RPC `ExecuteCommand` request is sent with the token, command string, and cwd:
|
||||
1. Creates the socket at `OLLIE_ELEVATE_SOCKET` and listens for connections
|
||||
2. For each request, connects to superpowerd and sends a JSON-RPC `ExecuteCommand`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -295,16 +270,23 @@ was never wrapped by landrun.
|
|||
}
|
||||
```
|
||||
|
||||
4. The response's `stdout`, `stderr`, and `exit_code` are forwarded back to
|
||||
`execute_code`.
|
||||
3. Translates the response to the ollie protocol and returns it to `x/elevate`
|
||||
4. Cleans up the socket on exit (`SIGTERM`/`SIGINT`)
|
||||
|
||||
### Custom backends
|
||||
The superpowerd socket is resolved from `SUPERPOWERD_SOCKET`,
|
||||
`SUPERPOWERD_SOCKET_DIR/superpowerd.sock`, `$XDG_RUNTIME_DIR/superpowerd/superpowerd.sock`,
|
||||
or `/run/user/<uid>/superpowerd/superpowerd.sock`.
|
||||
|
||||
Any daemon-and-socket design works. The daemon must be running outside landrun; the
|
||||
`x/elevate` script is the bridge between the sandboxed agent and that daemon.
|
||||
### Writing your own adapter
|
||||
|
||||
**Disabling elevation:** replace `x/elevate` with a script that exits non-zero.
|
||||
Steps marked `elevated: true` will fail rather than escape the sandbox:
|
||||
Any daemon that owns `OLLIE_ELEVATE_SOCKET` and speaks the two-message JSON protocol
|
||||
is a valid adapter. The daemon must be started outside landrun; that's the only
|
||||
constraint. Use `elevate-adapter-superpowerd` as a template.
|
||||
|
||||
### Disabling elevation
|
||||
|
||||
Replace `x/elevate` with a script that exits non-zero. Steps marked `elevated: true`
|
||||
will fail rather than escape the sandbox:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Elevation plugin: run a command outside the sandbox via superpowers.
|
||||
"""Sandbox escape hatch for execute_code elevated steps.
|
||||
|
||||
Connects to the socket at OLLIE_ELEVATE_SOCKET, sends the command and cwd,
|
||||
and returns the result. The adapter listening on that socket is responsible
|
||||
for running the command outside the Landlock sandbox.
|
||||
|
||||
Usage: elevate -- <cmd>
|
||||
|
||||
Replace this script to use a different elevation mechanism.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
|
@ -11,69 +13,38 @@ import socket
|
|||
import sys
|
||||
|
||||
|
||||
def _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 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")
|
||||
if not sock_path:
|
||||
print("elevate: OLLIE_ELEVATE_SOCKET not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# sys.argv: ['elevate', '--', '<cmd>']
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: elevate -- <cmd>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[2]
|
||||
cwd = os.getcwd()
|
||||
token = os.environ["SUPERPOWERD_SESSION_TOKEN"]
|
||||
req = json.dumps({"cmd": sys.argv[2], "cwd": os.getcwd()}).encode() + b"\n"
|
||||
|
||||
sock_path = _socket_path()
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(sock_path)
|
||||
except OSError as e:
|
||||
print(f"elevate: connect {sock_path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
req = json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "ExecuteCommand",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"auth": {"session_token": token},
|
||||
"command": cmd,
|
||||
"working_dir": cwd,
|
||||
"approval_timeout": 7200,
|
||||
},
|
||||
}).encode()
|
||||
|
||||
with sock:
|
||||
sock.sendall(req)
|
||||
data = sock.recv(1024 * 1024)
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
data = b""
|
||||
while chunk := sock.recv(65536):
|
||||
data += chunk
|
||||
|
||||
resp = json.loads(data)
|
||||
rpc_err = resp.get("error")
|
||||
if rpc_err:
|
||||
msg = rpc_err.get("message", str(rpc_err)) if isinstance(rpc_err, dict) else str(rpc_err)
|
||||
print(f"elevate: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
result = resp.get("result", {})
|
||||
out = result.get("stdout", "")
|
||||
err = result.get("stderr", "")
|
||||
if out:
|
||||
sys.stdout.write(out)
|
||||
if err:
|
||||
sys.stderr.write(err)
|
||||
sys.exit(result.get("exit_code", 0))
|
||||
if resp.get("stdout"):
|
||||
sys.stdout.write(resp["stdout"])
|
||||
if resp.get("stderr"):
|
||||
sys.stderr.write(resp["stderr"])
|
||||
sys.exit(resp.get("exit_code", 0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Elevation adapter: bridges the ollie elevate protocol to superpowerd.
|
||||
|
||||
Creates a Unix socket at OLLIE_ELEVATE_SOCKET, accepts {cmd, cwd} requests
|
||||
from x/elevate, and forwards them to superpowerd via its ExecuteCommand
|
||||
JSON-RPC interface. Run this outside the Landlock sandbox.
|
||||
|
||||
Usage:
|
||||
superpowers run-session -- python3 elevate-adapter-superpowerd
|
||||
|
||||
Environment:
|
||||
OLLIE_ELEVATE_SOCKET path for the listening socket (required)
|
||||
SUPERPOWERD_SESSION_TOKEN set automatically by superpowers run-session
|
||||
SUPERPOWERD_SOCKET override superpowerd socket path
|
||||
SUPERPOWERD_SOCKET_DIR directory containing superpowerd.sock
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
|
||||
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 handle(conn):
|
||||
with 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:
|
||||
resp = {"stdout": "", "stderr": f"elevate-adapter: bad request: {e}", "exit_code": 1}
|
||||
conn.sendall(json.dumps(resp).encode())
|
||||
return
|
||||
|
||||
token = os.environ.get("SUPERPOWERD_SESSION_TOKEN", "")
|
||||
sp_path = sp_socket_path()
|
||||
|
||||
sp = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
|
||||
try:
|
||||
sp.connect(sp_path)
|
||||
except OSError as e:
|
||||
resp = {"stdout": "", "stderr": f"elevate-adapter: connect {sp_path}: {e}", "exit_code": 1}
|
||||
conn.sendall(json.dumps(resp).encode())
|
||||
return
|
||||
|
||||
sp_req = json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "ExecuteCommand",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"auth": {"session_token": token},
|
||||
"command": req["cmd"],
|
||||
"working_dir": req["cwd"],
|
||||
"approval_timeout": 7200,
|
||||
},
|
||||
}).encode()
|
||||
|
||||
with sp:
|
||||
sp.sendall(sp_req)
|
||||
sp_data = sp.recv(1024 * 1024)
|
||||
|
||||
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)
|
||||
resp = {"stdout": "", "stderr": f"elevate-adapter: {msg}", "exit_code": 1}
|
||||
else:
|
||||
result = sp_resp.get("result", {})
|
||||
resp = {
|
||||
"stdout": result.get("stdout", ""),
|
||||
"stderr": result.get("stderr", ""),
|
||||
"exit_code": result.get("exit_code", 0),
|
||||
}
|
||||
|
||||
conn.sendall(json.dumps(resp).encode())
|
||||
|
||||
|
||||
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")
|
||||
if not sock_path:
|
||||
print("elevate-adapter-superpowerd: OLLIE_ELEVATE_SOCKET not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
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()
|
||||
Loading…
Reference in New Issue