Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ jobs:
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- run: cargo test --locked
- name: Independent acceptance and optional adapter tests
if: runner.os == 'Linux'
run: |
python3 -m unittest discover -s quality -p 'test_*.py'
python3 -m unittest discover -s adapters/opencode -p 'test_*.py'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
target/
.DS_Store
*.log

__pycache__/
*.pyc
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ ours.
> proof and an editable PR text. You can switch a repository to submit on its
> own.

For optional free-model authors with Codex/Claude subscription review, see
[the free-first guide](docs/free-first.md).

## What's inside

- **A fixed pipeline** of small workers: Scout, Guard, Vet, Hunter, Repro,
Expand Down
37 changes: 37 additions & 0 deletions adapters/opencode/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Free-only OpenCode process: isolated home, no project mount or inherited keys."""
import json,os,pathlib,shutil,sys
from opencode_bridge import CATALOG,STATE,allowed_models

def main():
args=sys.argv[1:]
if not args or args[0] not in ('acp','models','--version','-v'):raise SystemExit('Only ACP, model metadata and version are supported')
binary=pathlib.Path(os.environ.get('APOLLO_OPENCODE_BINARY') or shutil.which('opencode') or '').resolve()
if not binary.is_file() or not os.access(binary,os.X_OK) or binary==pathlib.Path(__file__).resolve():raise SystemExit('Set APOLLO_OPENCODE_BINARY to the installed OpenCode executable')
if args[0] in ('--version','-v'):
os.execve(str(binary),[str(binary),*args],{'PATH':'/usr/bin:/bin'})
if any(a.startswith(('--config','--dir','--attach','--agent','--port','--hostname')) or a in ('-f','--file') for a in args[1:]):raise SystemExit('Project, server and configuration overrides are not allowed')
config={'enabled_providers':['opencode'],'permission':{'*':'ask'},'share':'disabled','autoupdate':False}
if args[0]=='acp':
allowed=allowed_models(CATALOG)
if not allowed:raise SystemExit('No fresh, explicitly zero-price OpenCode models; refresh the catalog')
requested=os.environ.get('OPENCODE_MODEL','').strip()
if requested and not requested.startswith('opencode/'):requested='opencode/'+requested
for i,arg in enumerate(args):
if arg in ('-m','--model'):
if i+1>=len(args):raise SystemExit('Missing model')
requested=args[i+1]
elif arg.startswith('--model='):requested=arg.split('=',1)[1]
if requested and requested not in allowed:raise SystemExit('Unknown or non-free model refused')
default=requested or ('opencode/beam-free' if 'opencode/beam-free' in allowed else sorted(allowed)[0])
config.update(model=default,small_model=default,provider={'opencode':{'whitelist':[m.split('/',1)[1] for m in sorted(allowed)]}})
home=STATE/'home';home.mkdir(parents=True,exist_ok=True,mode=0o700)
env={'PATH':'/usr/bin:/bin','HOME':'/opencode-home','XDG_CONFIG_HOME':'/opencode-home/.config','XDG_DATA_HOME':'/opencode-home/.local/share','XDG_CACHE_HOME':'/opencode-home/.cache','OPENCODE_CONFIG_CONTENT':json.dumps(config),'OPENCODE_DISABLE_PROJECT_CONFIG':'true','OPENCODE_DISABLE_EXTERNAL_PLUGINS':'true','OPENCODE_DISABLE_CLAUDE_CODE':'true','OPENCODE_DISABLE_AUTOUPDATE':'true'}
cwd=os.getcwd();sandbox=shutil.which('bwrap')
if not sandbox:raise SystemExit('bubblewrap is required for the isolated adapter')
command=[sandbox,'--unshare-all','--share-net','--die-with-parent','--new-session','--ro-bind','/usr','/usr','--symlink','usr/lib','/lib','--symlink','usr/lib64','/lib64','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--dir','/etc']
for path in ('/etc/ssl/certs','/etc/resolv.conf','/etc/hosts','/etc/nsswitch.conf'):
if pathlib.Path(path).exists():command.extend(['--ro-bind',path,path])
command.extend(['--ro-bind',str(binary),'/opencode-bin/opencode','--bind',str(home),'/opencode-home','--dir',cwd,'--chdir',cwd,'--','/opencode-bin/opencode',*args])
os.execve(sandbox,command,env)
if __name__=='__main__':main()
615 changes: 615 additions & 0 deletions adapters/opencode/opencode_bridge.py

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions adapters/opencode/refresh_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Read OpenCode's actual verbose metadata; never infer pricing from model names."""
import json,pathlib,subprocess,time,os,tempfile
from opencode_bridge import CATALOG,allowed_models
from launch import STATE

def parse_models(text):
decoder=json.JSONDecoder();rows=[];rest=text
while rest.strip():
# `models --verbose` prints an ID line followed by its complete JSON object.
start=rest.find('{')
if start<0:break
value,used=decoder.raw_decode(rest[start:]);rest=rest[start+used:]
if not isinstance(value,dict):raise ValueError('Expected model object')
ident=value.get('id');provider=value.get('providerID')
if isinstance(ident,str) and isinstance(provider,str):
value['full_id']=provider+'/'+ident;rows.append(value)
if not rows:raise ValueError('No model metadata returned')
return rows

def main():
launcher=str(pathlib.Path(__file__).with_name('launch.py'))
output=subprocess.run([launcher,'models','opencode','--verbose','--refresh'],check=True,capture_output=True,text=True,timeout=90)
rows=parse_models(output.stdout);CATALOG.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
fd,name=tempfile.mkstemp(prefix='.catalog-',dir=CATALOG.parent)
try:
with os.fdopen(fd,'w') as f:json.dump({'updated_at':time.time(),'models':rows},f);f.flush();os.fsync(f.fileno())
free=allowed_models(pathlib.Path(name))
if not free:raise ValueError('No active zero-price models in returned metadata')
os.replace(name,CATALOG)
print(json.dumps({'free_models':sorted(free),'catalog':str(CATALOG)}))
finally:
pathlib.Path(name).unlink(missing_ok=True)
if __name__=='__main__':main()
76 changes: 76 additions & 0 deletions adapters/opencode/test_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import json
import os
from pathlib import Path
import tempfile
import time
import unittest
from unittest.mock import patch
import launch
from refresh_catalog import parse_models


class MetadataAndLauncher(unittest.TestCase):
def test_verbose_metadata_parser_handles_nested_objects(self):
row = {'id': 'new-stealth', 'providerID': 'opencode',
'cost': {'input': 0, 'output': 0, 'cache': {'read': 0}}}
parsed = parse_models('opencode/new-stealth\n' + json.dumps(row) + '\n')
self.assertEqual(parsed[0]['full_id'], 'opencode/new-stealth')
self.assertEqual(parsed[0]['cost'], row['cost'])
for malformed in ('', 'opencode/model\n{invalid'):
with self.assertRaises(ValueError):
parse_models(malformed)

def test_acp_keeps_keys_and_project_outside_sandbox(self):
with tempfile.TemporaryDirectory() as tmp:
state = Path(tmp)/'state'
binary = Path(tmp)/'opencode'
binary.write_text('#!/bin/sh\nexit 0\n')
binary.chmod(0o700)
catalog = Path(tmp)/'catalog.json'
catalog.write_text(json.dumps({'updated_at': time.time(),
'models': [{'providerID': 'opencode', 'full_id': 'opencode/test-free',
'status': 'active', 'cost': {'input': 0, 'output': 0}}]}))
captured = []
def execute(path, args, env):
captured.append((path, args, env))
with patch.object(launch, 'STATE', state), patch.object(launch, 'CATALOG', catalog), \
patch.dict(os.environ, {'APOLLO_OPENCODE_BINARY': str(binary), 'SECRET_API_KEY': 'must-not-inherit', 'OPENCODE_MODEL': 'test-free'}, clear=True), \
patch('sys.argv', ['launch.py', 'acp']), \
patch.object(launch.shutil, 'which', return_value='/usr/bin/bwrap'), \
patch.object(launch.os, 'execve', side_effect=execute):
launch.main()
_, args, env = captured[0]
self.assertNotIn('SECRET_API_KEY', env)
self.assertNotIn('--bind ' + os.getcwd(), ' '.join(args))
self.assertIn('--unshare-all', args)
self.assertEqual(json.loads(env['OPENCODE_CONFIG_CONTENT'])['permission'], {'*': 'ask'})
self.assertEqual(json.loads(env['OPENCODE_CONFIG_CONTENT'])['model'], 'opencode/test-free')

def test_unknown_model_and_configuration_override_are_refused(self):
with tempfile.TemporaryDirectory() as tmp:
binary = Path(tmp)/'opencode'
binary.write_text('#!/bin/sh\nexit 0\n');binary.chmod(0o700)
for args in (['acp', '--model', 'paid'], ['acp', '--config=/tmp/user.json']):
with patch.dict(os.environ, {'APOLLO_OPENCODE_BINARY': str(binary)}, clear=True), \
patch('sys.argv', ['launch.py', *args]), \
patch.object(launch, 'allowed_models', return_value={'opencode/free'}), \
patch.object(launch.os, 'execve') as execute:
with self.assertRaises(SystemExit):
launch.main()
execute.assert_not_called()

def test_no_bubblewrap_fails_closed(self):
with tempfile.TemporaryDirectory() as tmp:
binary = Path(tmp)/'opencode'
binary.write_text('#!/bin/sh\nexit 0\n');binary.chmod(0o700)
with patch.dict(os.environ, {'APOLLO_OPENCODE_BINARY': str(binary)}, clear=True), \
patch.object(launch, 'STATE', Path(tmp)/'state'), \
patch.object(launch, 'allowed_models', return_value={'opencode/free'}), \
patch('sys.argv', ['launch.py', 'acp']), \
patch.object(launch.shutil, 'which', return_value=None):
with self.assertRaisesRegex(SystemExit, 'bubblewrap'):
launch.main()


if __name__ == '__main__':
unittest.main()
Loading