lazyBoy/tests/mcp-packages.test.py

248 lines
14 KiB
Python
Raw Normal View History

2026-09-11 02:47:43 +00:00
"""Generic archive -> immutable installation -> real stdio SDK, inside Computer."""
import hashlib
import io
import json
import os
from pathlib import Path
import subprocess
import time
import tarfile
import unittest
import uuid
SERVER = r'''
import json,os,sys
from pathlib import Path
for line in sys.stdin:
request=json.loads(line)
if 'id' not in request: continue
method=request['method']
if method=='initialize':
result={'protocolVersion':request['params']['protocolVersion'],'capabilities':{'tools':{}},'serverInfo':{'name':'generic-fixture','version':'1'}}
elif method=='tools/list':
result={'tools':[{'name':'diagnose','description':'Check binding isolation','inputSchema':{'type':'object'}}]}
elif method=='tools/call':
args=request['params'].get('arguments',{})
if args.get('delay'):
import time
Path('inflight').touch()
time.sleep(args['delay'])
count=Path('count')
value=int(count.read_text())+1 if count.exists() else 1
count.write_text(str(value))
def readable(path):
try: Path(path).read_bytes(); return True
except PermissionError: return False
if args.get('fork'):
import subprocess,time
ready=Path('child-ready')
ready.unlink(missing_ok=True)
child_code="import ctypes,time; from pathlib import Path; ctypes.CDLL(None).prctl(4,0); Path('child-ready').touch(); time.sleep(30)"
subprocess.Popen([sys.executable,'-c',child_code],start_new_session=True,stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
while not ready.exists(): time.sleep(.005)
data={'uid':os.getuid(),'groups':os.getgroups(),'home':os.environ['HOME'],'count':value,
'rootReadable':readable('/var/lib/lazyboy-runner/jobs.sqlite'),
'peerReadable':readable(args['peer']) if args.get('peer') else False,
'token':os.environ.get('LAZYBOY_CONTROL_TOKEN')}
result={'content':[{'type':'text','text':json.dumps(data)}]}
else: result={}
print(json.dumps({'jsonrpc':'2.0','id':request['id'],'result':result}),flush=True)
'''
class Packages(unittest.TestCase):
def setUp(self):
if os.environ.get('LAZYBOY_RUNNER_TEST_CONTAINER') != '1' or not Path('/.dockerenv').exists():
self.fail('requires disposable Computer')
self.identity = {'computer_id':'fixture','bot_id':'package-agent','generation':1}
self.artifacts = []
def tearDown(self):
for path in self.artifacts:
path.unlink(missing_ok=True)
def call(self, action, body, identity=None, check=True):
result = subprocess.run(['python3','-I','/var/lib/lazyboy-runner/runner_jobs.py'],
input=json.dumps({'action':action,'identity':identity or self.identity,'input':json.dumps(body)}),
text=True,capture_output=True,timeout=55)
if check: self.assertEqual(result.returncode,0,result.stdout+result.stderr)
return json.loads(result.stdout)
def archive(self, unsafe=None, server=SERVER):
data=io.BytesIO()
with tarfile.open(fileobj=data,mode='w:gz') as archive:
member=tarfile.TarInfo('server.py')
content=server.encode()
member.size=len(content)
archive.addfile(member,io.BytesIO(content))
if unsafe:
member=tarfile.TarInfo(unsafe)
member.type=tarfile.SYMTYPE if unsafe=='link' else tarfile.LNKTYPE if unsafe=='hardlink' else tarfile.REGTYPE
member.linkname='/etc/passwd'
payload=b'x'*(33*1024*1024) if unsafe=='oversize' else b''
member.size=len(payload)
archive.addfile(member,io.BytesIO(payload))
blob=data.getvalue()
path=Path('/home/lazyboy')/('package-'+uuid.uuid4().hex+'.tar.gz')
path.write_bytes(blob)
self.artifacts.append(path)
return {'artifact_relative':path.name,'binding_id':uuid.uuid4().hex,
'manifest':{'id':'fixture.'+uuid.uuid4().hex,'version':'1.0.0','sha256':hashlib.sha256(blob).hexdigest(),
'entrypoint':['python3','./server.py'],'share_immutable_package':True}}
def invoke(self, binding, args=None, identity=None):
reply=self.call('package_call',{'binding_id':binding,'method':'tools/call','name':'diagnose','arguments':args or {}},identity)
return json.loads(json.loads(reply['stdout'])['content'][0]['text'])
def test_generic_install_private_uid_state_and_exact_replay(self):
package=self.archive()
self.call('package_install',package)
self.call('package_install',package)
listed=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list'})
self.assertEqual(json.loads(listed['stdout'])[0]['name'],'diagnose')
first=self.invoke(package['binding_id'],{'fork':True})
self.assertGreaterEqual(first['uid'],20000)
self.assertEqual(first['groups'],[])
self.assertFalse(first['rootReadable'])
self.assertIsNone(first['token'])
second=self.invoke(package['binding_id'])
self.assertEqual(second['count'],2)
peer=dict(self.identity,bot_id='other-agent')
denied=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list'},peer,False)
self.assertEqual(denied['error'],'BINDING_UNAVAILABLE')
another=dict(package,binding_id=uuid.uuid4().hex)
self.call('package_install',another,peer)
other=self.invoke(another['binding_id'],{'peer':first['home']+'/count'},peer)
self.assertNotEqual(first['uid'],other['uid'])
self.assertFalse(other['peerReadable'])
self.assertEqual(other['count'],1)
for process in Path('/proc').iterdir():
if process.name.isdigit():
try:
uid_line=next(line for line in process.joinpath('status').read_text().splitlines() if line.startswith('Uid:'))
if int(uid_line.split()[2])==first['uid']:
self.assertIn(process.joinpath('stat').read_text().rsplit(')',1)[1].split()[0],('Z','X'))
except FileNotFoundError: pass
def test_digest_paths_links_and_immutable_version_are_enforced(self):
package=self.archive()
unknown=dict(package,manifest=dict(package['manifest'],needs_root=True))
self.assertEqual(self.call('package_install',unknown,check=False)['error'],'UNSUPPORTED_MANIFEST_PROFILE')
wrong=dict(package,manifest=dict(package['manifest'],sha256='0'*64))
self.assertEqual(self.call('package_install',wrong,check=False)['error'],'PACKAGE_DIGEST_MISMATCH')
for path in ('../escape','/tmp/escape','link','hardlink','oversize'):
rejected=self.call('package_install',self.archive(path),check=False)
self.assertTrue(rejected.get('error'))
self.call('package_install',package)
changed=dict(package,manifest=dict(package['manifest'],entrypoint=['python3','./server.py','extra']))
self.assertEqual(self.call('package_install',changed,check=False)['error'],'IMMUTABLE_VERSION_CONFLICT')
source=Path('/opt/lazyboy-tools')/hashlib.sha256(json.dumps(package['manifest'],sort_keys=True).encode()).hexdigest()/'server.py'
source.write_text('changed by root fault injection')
rejected=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list'},check=False)
self.assertEqual(rejected['error'],'PACKAGE_INTEGRITY_FAILURE')
def test_private_package_source_is_not_readable_by_peer_binding(self):
package=self.archive()
package['manifest']['share_immutable_package']=False
self.call('package_install',package)
self.assertEqual(self.invoke(package['binding_id'])['count'],1)
other=dict(package,binding_id=uuid.uuid4().hex)
peer=dict(self.identity,bot_id='private-peer')
self.call('package_install',other,peer)
result=self.invoke(other['binding_id'],{'peer':'/opt/lazyboy-private-tools/'+package['binding_id']+'/server.py'},peer)
self.assertFalse(result['peerReadable'])
def test_versions_pin_manifest_and_revocation_is_binding_scoped(self):
package=self.archive()
self.call('package_install',package)
another=dict(package,binding_id=uuid.uuid4().hex,manifest=dict(package['manifest'],version='2.0.0'))
self.call('package_install',another)
wrong=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list','expected_version':'2.0.0'},check=False)
self.assertEqual(wrong['error'],'STALE_PACKAGE_BINDING')
original=self.invoke(package['binding_id'])
self.assertEqual(original['count'],1)
denied=self.call('package_revoke',{'binding_id':package['binding_id']},dict(self.identity,bot_id='peer'),check=False)
self.assertEqual(denied['error'],'BINDING_UNAVAILABLE')
self.call('package_revoke',{'binding_id':package['binding_id']})
denied=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list'},check=False)
self.assertEqual(denied['error'],'BINDING_UNAVAILABLE')
self.assertEqual(self.invoke(another['binding_id'])['count'],1)
denied=self.call('package_install',package,check=False)
self.assertEqual(denied['error'],'BINDING_REVOKED')
self.call('package_revoke',{'binding_id':uuid.uuid4().hex})
fresh=dict(package,binding_id=uuid.uuid4().hex)
self.call('package_install',fresh)
renewed=self.invoke(fresh['binding_id'])
self.assertEqual(renewed['count'],1)
self.assertNotEqual(original['uid'],renewed['uid'])
denied=self.call('package_call',{'binding_id':package['binding_id'],'method':'tools/list'},check=False)
self.assertEqual(denied['error'],'BINDING_UNAVAILABLE')
def test_atomic_version_switch_pins_calls_and_replays_without_switching_again(self):
source=self.archive()
source['manifest']['share_immutable_package']=False
self.call('package_install',source)
original=self.invoke(source['binding_id'])
target=self.archive(server=SERVER.replace("'count':value,", "'count':value,'version':'v2',"))
target['manifest'].update(id=source['manifest']['id'],version='2.0.0',share_immutable_package=False)
self.call('package_install',target)
switch={'binding_id':source['binding_id'],'transition_id':uuid.uuid4().hex,
'expected_version':'1.0.0','expected_sha256':source['manifest']['sha256'],
'target_manifest':target['manifest']}
before=json.loads(self.call('package_transition_status',switch,dict(self.identity,generation=2))['stdout'])
self.assertFalse(before['recorded'])
self.assertEqual(before['version'],'1.0.0')
payload={'action':'package_call','identity':self.identity,'input':json.dumps({'binding_id':source['binding_id'],'method':'tools/call','name':'diagnose','arguments':{'delay':2}})}
worker=subprocess.Popen(['python3','-I','/var/lib/lazyboy-runner/runner_jobs.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
try:
worker.stdin.write(json.dumps(payload));worker.stdin.close();worker.stdin=None
deadline=time.monotonic()+5
while not (Path(original['home'])/'inflight').exists() and time.monotonic()<deadline:
time.sleep(.01)
self.assertTrue((Path(original['home'])/'inflight').exists())
retiring=self.call('package_retire',{'binding_id':source['binding_id']},check=False)
self.assertEqual(retiring['error'],'PACKAGE_BUSY')
busy=self.call('package_switch',switch,check=False)
self.assertEqual(busy['error'],'PACKAGE_BUSY')
output,error=worker.communicate(timeout=10)
self.assertEqual(worker.returncode,0,error+output)
old=json.loads(json.loads(json.loads(output)['stdout'])['content'][0]['text'])
self.assertNotIn('version',old)
finally:
if worker.poll() is None: worker.kill();worker.wait()
result=self.call('package_switch',switch)
after=json.loads(self.call('package_transition_status',switch,dict(self.identity,generation=2))['stdout'])
self.assertTrue(after['recorded'])
self.assertEqual(after['version'],'2.0.0')
self.assertEqual(after['sha256'],target['manifest']['sha256'])
upgraded=self.invoke(source['binding_id'])
self.assertEqual(upgraded['version'],'v2')
self.assertEqual(upgraded['uid'],original['uid'])
self.assertEqual(upgraded['count'],3)
rollback=dict(switch,transition_id=uuid.uuid4().hex,expected_version='2.0.0',expected_sha256=target['manifest']['sha256'],target_manifest=source['manifest'])
self.call('package_switch',rollback)
# A delayed duplicate of the upgrade must return its original receipt,
# not perform the upgrade again after a newer rollback.
self.assertEqual(self.call('package_switch',switch),result)
superseded=json.loads(self.call('package_transition_status',switch)['stdout'])
self.assertTrue(superseded['recorded'])
self.assertEqual(superseded['version'],'1.0.0')
restored=self.invoke(source['binding_id'])
self.assertNotIn('version',restored)
self.assertEqual(restored['count'],4)
self.assertEqual(restored['uid'],original['uid'])
altered=dict(switch,expected_version='9.0.0')
self.assertEqual(self.call('package_switch',altered,check=False)['error'],'TRANSITION_PAYLOAD_MISMATCH')
self.assertEqual(self.invoke(target['binding_id'])['version'],'v2')
self.call('package_retire',{'binding_id':target['binding_id']})
self.call('package_retire',{'binding_id':target['binding_id']})
self.assertEqual(self.call('package_call',{'binding_id':target['binding_id'],'method':'tools/list'},check=False)['error'],'BINDING_UNAVAILABLE')
self.call('package_revoke',{'binding_id':source['binding_id']})
refused=self.call('package_switch',dict(switch,transition_id=uuid.uuid4().hex),check=False)
self.assertEqual(refused['error'],'BINDING_UNAVAILABLE')
if __name__=='__main__': unittest.main()