155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import json
|
||
|
|
import urllib.request
|
||
|
|
import urllib.error
|
||
|
|
import argparse
|
||
|
|
|
||
|
|
BACKEND_URL = "http://localhost:8080"
|
||
|
|
|
||
|
|
def get_api_key():
|
||
|
|
if os.environ.get("MEATBAG_API_KEY"):
|
||
|
|
return os.environ.get("MEATBAG_API_KEY")
|
||
|
|
|
||
|
|
paths = [".meatbag_key", "backend/.meatbag_key", "../.meatbag_key"]
|
||
|
|
for path in paths:
|
||
|
|
if os.path.exists(path):
|
||
|
|
try:
|
||
|
|
with open(path, "r") as f:
|
||
|
|
return f.read().strip()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return None
|
||
|
|
|
||
|
|
def send_request(path, method="GET", payload=None, query_params=None):
|
||
|
|
url = f"{BACKEND_URL}{path}"
|
||
|
|
if query_params:
|
||
|
|
params_str = urllib.parse.urlencode(query_params)
|
||
|
|
url = f"{url}?{params_str}"
|
||
|
|
|
||
|
|
headers = {}
|
||
|
|
api_key = get_api_key()
|
||
|
|
if api_key:
|
||
|
|
headers["X-Meatbag-API-Key"] = api_key
|
||
|
|
|
||
|
|
data = None
|
||
|
|
if payload is not None:
|
||
|
|
data = json.dumps(payload).encode("utf-8")
|
||
|
|
headers["Content-Type"] = application_json = "application/json"
|
||
|
|
|
||
|
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(req) as response:
|
||
|
|
res_body = response.read().decode("utf-8")
|
||
|
|
if not res_body:
|
||
|
|
return {}
|
||
|
|
return json.loads(res_body)
|
||
|
|
except urllib.error.HTTPError as e:
|
||
|
|
err_msg = e.read().decode("utf-8")
|
||
|
|
try:
|
||
|
|
err_json = json.loads(err_msg)
|
||
|
|
print(f"Error: {err_json.get('error', err_msg)}", file=sys.stderr)
|
||
|
|
except Exception:
|
||
|
|
print(f"Error: {e.code} - {err_msg}", file=sys.stderr)
|
||
|
|
sys.exit(1)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Failed to connect to backend at {BACKEND_URL}: {str(e)}", file=sys.stderr)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
def handle_ask(args):
|
||
|
|
payload = {
|
||
|
|
"type": "ApprovalRequest",
|
||
|
|
"id": f"cli_{os.urandom(4).hex()}",
|
||
|
|
"prompt": args.prompt,
|
||
|
|
"target": args.target
|
||
|
|
}
|
||
|
|
query_params = {}
|
||
|
|
if args.urgent:
|
||
|
|
query_params["urgent"] = "true"
|
||
|
|
if args.attention:
|
||
|
|
query_params["attentionTier"] = args.attention
|
||
|
|
|
||
|
|
print(f"Submitting approval request: '{args.prompt}'...", file=sys.stderr)
|
||
|
|
result = send_request("/api/request", method="POST", payload=payload, query_params=query_params)
|
||
|
|
approved = result.get("approved", False)
|
||
|
|
|
||
|
|
print(f"Response: {'APPROVED' if approved else 'REJECTED'}", file=sys.stderr)
|
||
|
|
if approved:
|
||
|
|
sys.exit(0)
|
||
|
|
else:
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
def handle_keys(args):
|
||
|
|
if args.subcommand == "create":
|
||
|
|
res = send_request("/api/keys", method="POST", query_params={"agentName": args.name})
|
||
|
|
print(f"Created API key for {args.name}:")
|
||
|
|
print(res.get("apiKey"))
|
||
|
|
elif args.subcommand == "list":
|
||
|
|
keys = send_request("/api/keys")
|
||
|
|
print(f"{'ID':<40} | {'Agent Name':<20} | {'Status':<10}")
|
||
|
|
print("-" * 76)
|
||
|
|
for key in keys:
|
||
|
|
status = "Revoked" if key.get("revoked") else "Active"
|
||
|
|
print(f"{key.get('id'):<40} | {key.get('agentName'):<20} | {status:<10}")
|
||
|
|
elif args.subcommand == "revoke":
|
||
|
|
res = send_request(f"/api/keys/{args.id}", method="DELETE")
|
||
|
|
print(res.get("status", "Revoked successfully"))
|
||
|
|
|
||
|
|
def handle_devices(args):
|
||
|
|
if args.subcommand == "list":
|
||
|
|
devices = send_request("/api/devices")
|
||
|
|
print(f"{'Device ID':<40} | {'Type':<15} | {'Paired At':<20}")
|
||
|
|
print("-" * 81)
|
||
|
|
for dev in devices:
|
||
|
|
import datetime
|
||
|
|
paired_date = datetime.datetime.fromtimestamp(dev.get("pairedAtMs") / 1000.0).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
|
print(f"{dev.get('deviceId'):<40} | {dev.get('deviceType'):<15} | {paired_date:<20}")
|
||
|
|
elif args.subcommand == "unpair":
|
||
|
|
res = send_request(f"/api/devices/{args.id}", method="DELETE")
|
||
|
|
print(res.get("status", "Unpaired successfully"))
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description="meatbag CLI wrapper for human-in-the-loop approvals")
|
||
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||
|
|
|
||
|
|
# ask subcommand
|
||
|
|
ask_parser = subparsers.add_parser("ask", help="Ask for human approval")
|
||
|
|
ask_parser.add_argument("prompt", help="The approval prompt text")
|
||
|
|
ask_parser.add_argument("--target", default="system-agent", help="The target tier/agent identifier")
|
||
|
|
ask_parser.add_argument("--urgent", action="store_true", help="Mark the request as urgent")
|
||
|
|
ask_parser.add_argument("--attention", choices=["SILENT_HAPTIC", "BACKGROUND_NOTIFICATION", "OVERRIDE_SCREEN"], help="Specify preferred attention level")
|
||
|
|
|
||
|
|
# keys subcommand
|
||
|
|
keys_parser = subparsers.add_parser("keys", help="Manage API keys")
|
||
|
|
keys_sub = keys_parser.add_subparsers(dest="subcommand", required=True)
|
||
|
|
|
||
|
|
create_key = keys_sub.add_parser("create", help="Create a new API key")
|
||
|
|
create_key.add_argument("--name", required=True, help="Agent name for the key")
|
||
|
|
|
||
|
|
keys_sub.add_parser("list", help="List API keys")
|
||
|
|
|
||
|
|
revoke_key = keys_sub.add_parser("revoke", help="Revoke an API key")
|
||
|
|
revoke_key.add_argument("id", help="The API key ID to revoke")
|
||
|
|
|
||
|
|
# devices subcommand
|
||
|
|
devices_parser = subparsers.add_parser("devices", help="Manage paired client devices")
|
||
|
|
devices_sub = devices_parser.add_subparsers(dest="subcommand", required=True)
|
||
|
|
|
||
|
|
devices_sub.add_parser("list", help="List paired devices")
|
||
|
|
|
||
|
|
unpair_device = devices_sub.add_parser("unpair", help="Unpair a device")
|
||
|
|
unpair_device.add_argument("id", help="The device ID to unpair")
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
if args.command == "ask":
|
||
|
|
handle_ask(args)
|
||
|
|
elif args.command == "keys":
|
||
|
|
handle_keys(args)
|
||
|
|
elif args.command == "devices":
|
||
|
|
handle_devices(args)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|