/** Fixed remote program. Inputs travel as JSON, never interpolated into Python or shell. */ export const remoteProgram = String.raw` import os,sys,json,hashlib,base64,tempfile,subprocess,fcntl,stat,shutil p=json.load(sys.stdin) def fail(code): print(json.dumps({'error':code}));sys.exit(0) def digest(data):return hashlib.sha256(data).hexdigest() def run(args,cwd=None,env=None): r=subprocess.run(args,cwd=cwd,env=env,capture_output=True,text=True) if r.returncode:fail('command_failed') return r.stdout.strip() root=p['root'] if not root.startswith('/home/user/paperclip/') or '..' in root.split('/'):fail('invalid') # Reject symlinks even in existing ancestors; one Unix user is not an OS boundary. def safe(path): if not isinstance(path,str) or path.startswith('/') or '\x00' in path or '..' in path.split('/'):fail('invalid') current=root for part in path.split('/'): if part in ('','.'):continue current=os.path.join(current,part) if os.path.islink(current):fail('invalid') if os.path.commonpath([root,os.path.realpath(current)])!=root:fail('invalid') return current current='/' for part in root.split('/'): if not part:continue current=os.path.join(current,part) if os.path.islink(current):fail('invalid') act=p['action'] if act=='owned-port': port=p['port'];owner=p['ownerId'];inodes=set() for table in ['/proc/net/tcp','/proc/net/tcp6']: for line in open(table).readlines()[1:]: cols=line.split() if int(cols[1].split(':')[1],16)==port and cols[3]=='0A':inodes.add(cols[9]) for pid in os.listdir('/proc'): if not pid.isdigit():continue try: if 'paperclip-'+owner+'.slice' not in open('/proc/'+pid+'/cgroup').read():continue for fd in os.listdir('/proc/'+pid+'/fd'): target=os.readlink('/proc/'+pid+'/fd/'+fd) if target.startswith('socket:[') and target[8:-1] in inodes:print('{}');sys.exit(0) except (FileNotFoundError,PermissionError,ProcessLookupError):pass fail('conflict') if act=='seed': if os.path.exists(root):print(json.dumps({'seeded':False}));sys.exit(0) os.makedirs(os.path.dirname(root),exist_ok=True) temp=tempfile.mkdtemp(prefix='.seed-',dir=os.path.dirname(root)) try: for name,value in p['files'].items(): dest=safe(name);relative=os.path.relpath(dest,root) if relative=='.':fail('invalid') target=os.path.join(temp,relative);os.makedirs(os.path.dirname(target),exist_ok=True) with open(target,'wb') as f:f.write(base64.b64decode(value,validate=True)) try:os.rename(temp,root);result=True except FileExistsError:result=False print(json.dumps({'seeded':result})) finally: if os.path.exists(temp):shutil.rmtree(temp) sys.exit(0) if act=='workspace': os.makedirs(root,exist_ok=True) repo=p.get('repositoryUrl');branch=p.get('branch');task=p.get('taskId');base_ref=p.get('baseRef') if base_ref and (base_ref.startswith('-') or '\n' in base_ref):fail('invalid') checkout=os.path.join(root,'checkout') if repo: if not (repo.startswith('https://') or repo.startswith('ssh://') or repo.startswith('git@')) or '\n' in repo:fail('invalid') if not os.path.exists(checkout): # Resolve credentials on the controller for this operation. Only the clone # child receives the helper environment; no credential file or Git config persists. auth=p.get('gitAuth') or {};git_env=dict(os.environ) git_env.update({'GIT_TERMINAL_PROMPT':'0','GIT_CONFIG_GLOBAL':'/dev/null','GIT_CONFIG_NOSYSTEM':'1'}) git_env.update(auth.get('env',{})) run(['git']+auth.get('configArgs',[])+['clone','--',repo,checkout],env=git_env) elif run(['git','remote','get-url','origin'],checkout)!=repo:fail('conflict') else: os.makedirs(checkout,exist_ok=True) if not os.path.isdir(os.path.join(checkout,'.git')):run(['git','init',checkout]) if branch: if branch.startswith('-') or '\n' in branch:fail('invalid') run(['git','check-ref-format','--branch',branch]) if p.get('mode')=='worktree': if not task:fail('invalid') target=os.path.join(root,'tasks',task);os.makedirs(os.path.dirname(target),exist_ok=True) task_branch=branch or 'paperclip/task-'+task if not os.path.exists(target):run(['git','worktree','add','-b',task_branch,target,base_ref or 'HEAD'],checkout) run(['git','rev-parse','--show-toplevel'],target) if run(['git','symbolic-ref','--quiet','--short','HEAD'],target)!=task_branch:fail('conflict') else: target=checkout if branch:run(['git','checkout',branch],checkout) print(json.dumps({'remoteCwd':target}));sys.exit(0) # Hold directory descriptors throughout each operation. Ancestor symlink swaps cannot # redirect a checked pathname outside the selected placement. def directory(parts,create=False,start=None): fd=os.dup(start) if start is not None else os.open('/',os.O_RDONLY|os.O_DIRECTORY) try: for part in parts: if not part or part=='.':continue if part=='..':fail('invalid') try:nxt=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd) except FileNotFoundError: if not create:raise try:os.mkdir(part,0o700,dir_fd=fd) except FileExistsError:pass nxt=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd) os.close(fd);fd=nxt return fd except BaseException:os.close(fd);raise # Initial uploads stay in a private sibling until a no-replace rename publishes them. # Each request carries at most one bounded chunk, never the whole home. if act in ('seed-begin','seed-chunk','seed-commit','seed-abort'): import uuid,ctypes,errno token=p.get('seedId','') try: if str(uuid.UUID(token))!=token:fail('invalid') except (ValueError,TypeError,AttributeError):fail('invalid') parentfd=directory(os.path.dirname(root).split('/'),create=True) name=os.path.basename(root);staging='.paperclip-seed-'+name+'-'+token stagefd=None;lockfd=None try: if act=='seed-abort': try:shutil.rmtree(staging,dir_fd=parentfd) except FileNotFoundError:pass print('{}');sys.exit(0) if act=='seed-begin': try: existing=os.stat(name,dir_fd=parentfd,follow_symlinks=False) if not stat.S_ISDIR(existing.st_mode):fail('invalid') print(json.dumps({'started':False}));sys.exit(0) except FileNotFoundError:pass os.mkdir(staging,0o700,dir_fd=parentfd) stagefd=os.open(staging,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parentfd) lockfd=os.open('lock',os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW,0o600,dir_fd=stagefd) fcntl.flock(lockfd,fcntl.LOCK_EX) if act=='seed-begin': os.mkdir('tree',0o700,dir_fd=stagefd) print(json.dumps({'started':True})) elif act=='seed-chunk': relative=p.get('path','');safe(relative) parts=[part for part in relative.split('/') if part not in ('','.')] if not parts:fail('invalid') data=base64.b64decode(p['base64'],validate=True);offset=p.get('offset') if len(data)>1024*1024 or type(offset)!=int or offset<0 or offset+len(data)>256*1024*1024:fail('invalid') destfd=directory(['tree']+parts[:-1],create=True,start=stagefd) try:fd=os.open(parts[-1],os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600,dir_fd=destfd) finally:os.close(destfd) with os.fdopen(fd,'r+b') as f: info=os.fstat(f.fileno()) if not stat.S_ISREG(info.st_mode) or info.st_nlink!=1:fail('invalid') if info.st_size!=offset:fail('conflict') f.seek(offset);f.write(data) print('{}') else: libc=ctypes.CDLL(None,use_errno=True) if hasattr(libc,'renameat2'):status=libc.renameat2(stagefd,b'tree',parentfd,os.fsencode(name),1) elif hasattr(libc,'renameatx_np'):status=libc.renameatx_np(stagefd,b'tree',parentfd,os.fsencode(name),4) else:fail('invalid') if status!=0: code=ctypes.get_errno() if code not in (errno.EEXIST,errno.ENOTEMPTY):raise OSError(code,os.strerror(code)) if not stat.S_ISDIR(os.stat(name,dir_fd=parentfd,follow_symlinks=False).st_mode):fail('invalid') print(json.dumps({'seeded':status==0})) except FileNotFoundError:fail('not_found') except (OSError,ValueError):fail('invalid') finally: if lockfd is not None:os.close(lockfd) if stagefd is not None:os.close(stagefd) if act=='seed-commit': try:shutil.rmtree(staging,dir_fd=parentfd) except FileNotFoundError:pass os.close(parentfd) sys.exit(0) try:rootfd=directory(root.split('/')) except FileNotFoundError:fail('not_found') except OSError:fail('invalid') def parent(relative,create=False): safe(relative) parts=[part for part in relative.split('/') if part not in ('','.')] if not parts:fail('invalid') return directory(parts[:-1],create,rootfd),parts[-1] path=p.get('path','');safe(path) lock=os.open('.paperclip-editor.lock',os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW,0o600,dir_fd=rootfd) fcntl.flock(lock,fcntl.LOCK_EX) try: if act=='list': fd=directory(path.split('/'),start=rootfd) try: limit=p.get('limit',1000) if type(limit)!=int or limit<1 or limit>1000:fail('invalid') out=[];truncated=False;scanned=0 with os.scandir(fd) as entries: for entry in entries: if entry.name=='.paperclip-editor.lock':continue if scanned>=limit:truncated=True;break scanned+=1 if entry.is_symlink():continue try:info=entry.stat(follow_symlinks=False) except FileNotFoundError:continue if not stat.S_ISDIR(info.st_mode) and not stat.S_ISREG(info.st_mode):continue out.append({'name':entry.name,'kind':'directory' if stat.S_ISDIR(info.st_mode) else 'file','size':info.st_size,'mtimeMs':info.st_mtime*1000}) print(json.dumps({'entries':out,'truncated':truncated})) finally:os.close(fd) elif act=='stat': parentfd,name=parent(path) try:info=os.stat(name,dir_fd=parentfd,follow_symlinks=False) finally:os.close(parentfd) if not stat.S_ISDIR(info.st_mode) and not stat.S_ISREG(info.st_mode):fail('invalid') print(json.dumps({'name':name,'kind':'directory' if stat.S_ISDIR(info.st_mode) else 'file','size':info.st_size,'mtimeMs':info.st_mtime*1000})) elif act=='hash': parentfd,name=parent(path) try:fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parentfd) finally:os.close(parentfd) with os.fdopen(fd,'rb') as f: if not stat.S_ISREG(os.fstat(f.fileno()).st_mode):fail('invalid') h=hashlib.sha256();size=0 while True: block=f.read(65536) if not block:break h.update(block);size+=len(block) print(json.dumps({'sha256':h.hexdigest(),'size':size})) elif act=='read': parentfd,name=parent(path) try:fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parentfd) finally:os.close(parentfd) with os.fdopen(fd,'rb') as f: st=os.fstat(f.fileno()) if not stat.S_ISREG(st.st_mode) or st.st_size>p.get('maxBytes',16*1024*1024):fail('invalid') data=f.read(p.get('maxBytes',16*1024*1024)+1) if len(data)>p.get('maxBytes',16*1024*1024):fail('invalid') print(json.dumps({'base64':base64.b64encode(data).decode(),'sha256':digest(data)})) elif act in ('write','remove','move'): parentfd,name=parent(path,create=act=='write') try: old=None try:fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parentfd) except FileNotFoundError:fd=None if fd is not None: with os.fdopen(fd,'rb') as f: if not stat.S_ISREG(os.fstat(f.fileno()).st_mode):fail('invalid') h=hashlib.sha256() while True: block=f.read(65536) if not block:break h.update(block) old=h.hexdigest() if old!=p.get('expectedSha256'):fail('conflict') if act=='write': data=base64.b64decode(p['base64'],validate=True) if len(data)>16*1024*1024:fail('invalid') import secrets temp='.paperclip-write-'+secrets.token_hex(16) fd=os.open(temp,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600,dir_fd=parentfd) try: with os.fdopen(fd,'wb') as f:f.write(data);f.flush();os.fsync(f.fileno()) os.replace(temp,name,src_dir_fd=parentfd,dst_dir_fd=parentfd) finally: try:os.unlink(temp,dir_fd=parentfd) except FileNotFoundError:pass print(json.dumps({'sha256':digest(data)})) elif act=='remove': if old is None:fail('not_found') os.unlink(name,dir_fd=parentfd);print('{}') else: if old is None:fail('not_found') targetfd,target=parent(p['to'],create=True) try: try:os.link(name,target,src_dir_fd=parentfd,dst_dir_fd=targetfd,follow_symlinks=False) except FileExistsError:fail('conflict') os.unlink(name,dir_fd=parentfd) finally:os.close(targetfd) print(json.dumps({'sha256':old})) finally:os.close(parentfd) else:fail('invalid') except FileNotFoundError:fail('not_found') except OSError:fail('invalid') finally:os.close(lock);os.close(rootfd) `;