File size: 4,425 Bytes
70f2179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Build a pre-baked E2B template with opencode + proxy deps already installed.

Run-time per rollout drops from ~3 min (cold install) to ~30s once the
template is built, because we skip:

  - ``curl https://opencode.ai/install | bash`` (~30-90s)
  - ``pip install fastapi uvicorn httpx`` (~30-60s)
  - directory layout setup
  - copying the proxy source

The template ships:

  - opencode CLI at ``/home/user/.opencode/bin/opencode``
  - Python deps for the in-sandbox proxy
  - The proxy source at ``/home/user/proxy/interception.py``
  - Pre-created dirs: ``~/.config/opencode``, ``~/logs/{agent,verifier}``,
    ``~/task``, ``~/workdir``, ``~/proxy``
  - Default workdir: ``/home/user/workdir``

Usage::

    .venv/bin/python envs/opencode_env/tests/build_e2b_template.py
    # → builds (or rebuilds) ``opencode-rl`` template, prints template id

Then ``test_five_sorts_e2e.py`` will use it via ``--template opencode-rl``.

Requires ``E2B_API_KEY`` in the environment. First build is ~3-8 min;
subsequent builds reuse the cache and can finish in <60s.
"""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

from e2b import Template, default_build_logger


_ENV_DIR = Path(__file__).resolve().parent
_PROXY_SOURCE = _ENV_DIR / "interception.py"


def _load_env(path: Path) -> None:
    if not path.exists():
        return
    for raw in path.read_text().splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, _, v = line.partition("=")
        k = k.strip()
        v = v.strip().strip('"').strip("'")
        if k and k not in os.environ:
            os.environ[k] = v


def build_template(name: str, *, skip_cache: bool = False) -> str:
    if not _PROXY_SOURCE.exists():
        raise RuntimeError(f"proxy source missing at {_PROXY_SOURCE}")

    # Template.copy() resolves relative paths against the caller's source
    # file directory. This script lives next to ``interception.py`` so the
    # bare filename works.

    # Stage 1 (root): system-wide pip deps for the proxy.
    # Stage 2 (user): opencode install + dir layout + proxy copy.
    template = (
        Template()
        .from_python_image("3.12")
        .pip_install(
            [
                "fastapi>=0.104",
                "uvicorn[standard]>=0.24",
                "httpx>=0.27",
            ]
        )
        .set_user("user")
        .run_cmd("curl -fsSL https://opencode.ai/install | bash")
        .run_cmd("/home/user/.opencode/bin/opencode --version")
        .make_dir("/home/user/.config/opencode")
        .make_dir("/home/user/logs/agent")
        .make_dir("/home/user/logs/verifier")
        .make_dir("/home/user/task")
        .make_dir("/home/user/workdir")
        .make_dir("/home/user/proxy")
        .copy("interception.py", "/home/user/proxy/interception.py")
        .set_workdir("/home/user/workdir")
    )
    if skip_cache:
        template = template.skip_cache()

    info = Template.build(
        template,
        name,
        cpu_count=2,
        memory_mb=2048,
        on_build_logs=default_build_logger(),
    )
    return info.template_id if hasattr(info, "template_id") else str(info)


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(prog="build_e2b_template")
    p.add_argument(
        "--name",
        default="opencode-rl",
        help="Template name (default: opencode-rl).",
    )
    p.add_argument(
        "--skip-cache",
        action="store_true",
        help="Force a clean rebuild, ignoring cache.",
    )
    args = p.parse_args(argv)

    _load_env(_ENV_DIR / ".env")
    if not os.environ.get("E2B_API_KEY"):
        print("ERROR: E2B_API_KEY required.", file=sys.stderr)
        return 2

    print(f"Building template '{args.name}' "
          f"(proxy source: {_PROXY_SOURCE})")
    print(f"Skip cache: {args.skip_cache}")
    print()

    template_id = build_template(args.name, skip_cache=args.skip_cache)
    print()
    print(f"Built. Template id/name: {template_id}")
    print(f"Use in code: Sandbox.create(template='{args.name}')")
    return 0


if __name__ == "__main__":
    sys.exit(main())