File size: 4,715 Bytes
81663e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
144
145
"""Daytona implementation of the standalone OpenCode sandbox protocol."""

from __future__ import annotations

import math
import os
import shlex
import time
import uuid
from functools import lru_cache

from opencode_env.sandbox.base import ExecResult


@lru_cache(maxsize=1)
def client():
    from daytona import Daytona

    return Daytona()


class DaytonaBgJob:
    def __init__(self, handle, directory, pid):
        self.handle, self.directory, self._pid = handle, directory, pid

    @property
    def pid(self):
        return self._pid

    def wait(self, timeout=None):
        deadline = time.monotonic() + timeout if timeout is not None else float("inf")
        while time.monotonic() < deadline:
            if self.handle.exists(self.directory + "/exit"):
                return int(self.handle.read_text(self.directory + "/exit").strip())
            time.sleep(min(0.5, max(0, deadline - time.monotonic())))
        raise TimeoutError("Background command exceeded its deadline")

    def kill(self):
        self.handle.exec(f"kill -TERM -- -{self.pid} 2>/dev/null || true", timeout=10)


class DaytonaSandboxHandle:
    def __init__(self, sandbox):
        self._sandbox = sandbox
        self._deleted = False

    @property
    def sandbox_id(self):
        return self._sandbox.id

    def exec(self, cmd, *, envs=None, cwd=None, timeout=60):
        # The SDK's exec is not a shell. Quote the complete script once and let bash
        # interpret it; neither user text nor environment values are interpolated.
        result = self._sandbox.process.exec(
            "bash -lc " + shlex.quote(cmd),
            cwd=cwd,
            env=envs,
            timeout=max(1, math.ceil(timeout)) if timeout is not None else None,
        )
        code = result.exit_code
        return ExecResult(124 if code is None else int(code), result.result or "", "")

    def write_text(self, path, content):
        parent = path.rsplit("/", 1)[0] or "."
        result = self.exec("mkdir -p " + shlex.quote(parent))
        if result.exit_code:
            raise RuntimeError("Could not create sandbox directory")
        self._sandbox.fs.upload_file(content.encode(), path)

    def read_text(self, path):
        return self._sandbox.fs.download_file(path).decode()

    def exists(self, path):
        return self.exec("test -e " + shlex.quote(path), timeout=15).exit_code == 0

    def start_bg(self, cmd, *, envs=None, cwd=None):
        directory = "/tmp/openenv-process-" + uuid.uuid4().hex
        self.write_text(
            directory + "/run.sh",
            "#!/bin/bash\n(\n"
            + cmd
            + "\n)"
            + "\ncode=$?\nprintf '%s' \"$code\" > "
            + shlex.quote(directory + "/exit")
            + "\n",
        )
        result = self.exec(
            "setsid bash "
            + shlex.quote(directory + "/run.sh")
            + " > "
            + shlex.quote(directory + "/output")
            + " 2>&1 < /dev/null & echo $!",
            envs=envs,
            cwd=cwd,
            timeout=15,
        )
        if result.exit_code:
            raise RuntimeError("Could not launch background command")
        return DaytonaBgJob(
            self, directory, int(result.stdout.strip().splitlines()[-1])
        )

    def kill(self):
        if not self._deleted:
            client().delete(self._sandbox, timeout=60, wait=True)
            self._deleted = True


class DaytonaSandboxBackend:
    def __init__(self, *, image, snapshot=None):
        self.image, self.snapshot = (
            image,
            snapshot or os.environ.get("DATA_AGENT_DAYTONA_SNAPSHOT"),
        )

    def create(self, *, timeout_s=900, envs=None, metadata=None):
        from daytona import (
            CreateSandboxFromImageParams,
            CreateSandboxFromSnapshotParams,
            Resources,
        )

        options = {
            "language": "python",
            "os_user": "root",
            "env_vars": envs,
            "labels": {
                **(metadata or {}),
                "openenv_component": "blackbox-opencode",
                "openenv_owner": os.environ.get("RUN_OWNER", "local"),
            },
            "public": False,
            "auto_stop_interval": max(30, math.ceil(timeout_s / 60)),
            "auto_delete_interval": 0,
        }
        if self.snapshot:
            params = CreateSandboxFromSnapshotParams(snapshot=self.snapshot, **options)
        else:
            params = CreateSandboxFromImageParams(
                image=self.image,
                resources=Resources(cpu=1, memory=4, disk=5),
                **options,
            )
        return DaytonaSandboxHandle(client().create(params, timeout=300))