Add reusable multi-session skill and five-page project workbench
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Initialize one private control runtime and optionally its own user service."""
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo', required=True)
|
||||
parser.add_argument('--runtime', required=True)
|
||||
parser.add_argument('--bind', required=True)
|
||||
parser.add_argument('--port', type=int, default=8796)
|
||||
parser.add_argument('--hostname', required=True)
|
||||
parser.add_argument('--install-user-service', action='store_true')
|
||||
parser.add_argument('--start', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if os.name != 'posix':
|
||||
raise ValueError('Service setup requires the authorized Linux service account')
|
||||
address = ipaddress.IPv4Address(args.bind)
|
||||
if not any(address in ipaddress.ip_network(net) for net in ['10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','127.0.0.0/8']):
|
||||
raise ValueError('Explicit LAN or loopback IPv4 required')
|
||||
if not 1024 <= args.port <= 65535:
|
||||
raise ValueError('Unprivileged port required')
|
||||
if any(c.isspace() for c in args.hostname) or any(c in args.hostname for c in '/:@'):
|
||||
raise ValueError('Invalid hostname')
|
||||
repo, runtime = Path(args.repo).resolve(), Path(args.runtime).expanduser().resolve()
|
||||
if not (repo/'agent_control/server.py').is_file():
|
||||
raise ValueError('Control service source is missing from this repository')
|
||||
if not runtime.is_relative_to(Path.home()):
|
||||
raise ValueError('Runtime must be within the service account home')
|
||||
runtime.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
info = runtime.stat()
|
||||
if info.st_uid != os.getuid() or info.st_mode & 0o077:
|
||||
raise ValueError('Runtime must be owned by current account with mode 700')
|
||||
token = runtime/'access-token'
|
||||
if token.is_symlink():
|
||||
raise ValueError('Token cannot be a symlink')
|
||||
if not token.exists():
|
||||
fd = os.open(str(token), os.O_CREAT|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
with os.fdopen(fd,'w') as stream:
|
||||
stream.write(secrets.token_urlsafe(48))
|
||||
config_path = runtime/'config.json'
|
||||
config = {'bind':args.bind,'port':args.port,'runtime_root':str(runtime),'token_file':str(token),
|
||||
'allowed_hosts':[args.hostname+':'+str(args.port)],'metrics':{},
|
||||
'title':'Agent Workbench','primary_session_id':'human-progress'}
|
||||
if config_path.is_symlink():
|
||||
raise ValueError('Config cannot be a symlink')
|
||||
if config_path.exists():
|
||||
old = json.loads(config_path.read_text())
|
||||
if any(old.get(k) != config[k] for k in ['bind','port','runtime_root','token_file']):
|
||||
raise ValueError('Existing runtime configuration differs; inspect it before explicit changes')
|
||||
else:
|
||||
fd = os.open(str(config_path), os.O_CREAT|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
with os.fdopen(fd,'w') as stream:
|
||||
json.dump(config,stream,ensure_ascii=False,indent=2)
|
||||
if args.start and not args.install_user_service:
|
||||
raise ValueError('--start requires --install-user-service')
|
||||
if args.install_user_service:
|
||||
# This is a user-owned unit, never a system service and never sudo.
|
||||
units = Path.home()/'.config/systemd/user'
|
||||
units.mkdir(parents=True,exist_ok=True)
|
||||
unit = units/'agent-workbench.service'
|
||||
# systemd accepts double-quoted paths, independently of shell quoting.
|
||||
quote = lambda value: '"'+str(value).replace('\\','\\\\').replace('"','\\"').replace('%','%%')+'"'
|
||||
if '\n' in str(repo) or '\r' in str(repo):
|
||||
raise ValueError('Invalid repository path')
|
||||
content = ('[Unit]\nDescription=Agent Workbench\nAfter=network.target\n\n'
|
||||
'[Service]\nType=simple\nWorkingDirectory='+str(repo).replace('%','%%')+'\nExecStart='+quote(sys.executable)+
|
||||
' -m agent_control.server --config '+quote(config_path)+
|
||||
'\nRestart=on-failure\nRestartSec=5\nUMask=0077\nNoNewPrivileges=true\n\n'
|
||||
'[Install]\nWantedBy=default.target\n')
|
||||
previous = content.replace('WorkingDirectory='+str(repo).replace('%','%%'), 'WorkingDirectory='+quote(repo))
|
||||
if unit.exists() and unit.read_text() not in (content,previous):
|
||||
raise ValueError('Existing user unit differs; inspect before replacement')
|
||||
unit.write_text(content)
|
||||
subprocess.run(['systemctl','--user','daemon-reload'],check=True)
|
||||
if args.start:
|
||||
subprocess.run(['systemctl','--user','enable','--now',unit.name],check=True)
|
||||
print(json.dumps({'config':str(config_path),'token_file':str(token),'url':'http://'+args.hostname+':'+str(args.port),
|
||||
'user_service_started':bool(args.start)},ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user