Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

187 lines
6.2 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import io
import uuid
from pathlib import Path
from typing import Literal
from agents.sandbox.manifest import Manifest
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
from agents.sandbox.session.sandbox_client import BaseSandboxClient
from agents.sandbox.session.sandbox_session import SandboxSession
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
from agents.sandbox.snapshot import NoopSnapshot, SnapshotBase, SnapshotSpec
from agents.sandbox.types import ExecResult, User
from mirage.workspace.snapshot import apply_state_dict, read_tar
from mirage.workspace.workspace import Workspace
class MirageSandboxSessionState(SandboxSessionState):
type: Literal["mirage"] = "mirage"
class MirageSandboxSession(BaseSandboxSession):
def __init__(
self,
workspace: Workspace,
state: MirageSandboxSessionState,
) -> None:
self._ws = workspace
self.state = state
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
cmd_str = " ".join(str(c) for c in command)
io_result = await self._ws.execute(cmd_str)
stdout = (io_result.stdout or b"")
stderr = (io_result.stderr or b"")
if isinstance(stdout, str):
stdout = stdout.encode("utf-8")
if isinstance(stderr, str):
stderr = stderr.encode("utf-8")
return ExecResult(
exit_code=io_result.exit_code,
stdout=stdout,
stderr=stderr,
)
async def read(
self,
path: Path,
*,
user: str | User | None = None,
) -> io.IOBase:
data = await self._ws.ops.read(str(path))
return io.BytesIO(data)
async def write(
self,
path: Path,
data: io.IOBase,
*,
user: str | User | None = None,
) -> None:
content = data.read()
if isinstance(content, str):
content = content.encode("utf-8")
parent = str(path.parent)
if parent and parent != ".":
try:
await self._ws.ops.mkdir(parent)
except (FileExistsError, ValueError):
pass
await self._ws.ops.write(str(path), content)
def _prepare_exec_command(
self,
*command: str | Path,
shell: bool | list[str],
user: str | User | None,
) -> list[str]:
return [str(c) for c in command]
async def running(self) -> bool:
return not self._ws._closed
async def persist_workspace(self) -> io.IOBase:
buf = io.BytesIO()
await self._ws.snapshot(buf)
buf.seek(0)
return buf
async def hydrate_workspace(self, data: io.IOBase) -> None:
# Restore the snapshot's non-mount state (cache, sessions,
# inodes, history, jobs) AND each resource's content (via
# load_state) into THIS workspace. The workspace must already
# have the same mount shape that was saved — Workspace.load()
# is the alternative that constructs a fresh Workspace from
# scratch.
if hasattr(data, "seek"):
data.seek(0)
state = read_tar(data)
await apply_state_dict(self._ws, state)
class MirageSandboxClient(BaseSandboxClient[None]):
# In-process integration: every sandbox session shares one Workspace
# instance owned by the agent's process. No HTTP, no daemon -- the
# agent and the workspace run on the same event loop.
#
# If you need cross-process isolation (each agent talks to a
# workspace hosted in a separate daemon process), see
# docs/plans/2026-04-17-workspace-server-cli.md -- a
# MirageRemoteSandboxClient that speaks HTTP to `mirage daemon`
# would slot into the same BaseSandboxClient interface.
backend_id: str = "mirage"
supports_default_options: bool = True
def __init__(self, workspace: Workspace) -> None:
self._ws = workspace
self._sessions: dict[uuid.UUID, MirageSandboxSession] = {}
async def create(
self,
*,
snapshot: SnapshotSpec | SnapshotBase | None = None,
manifest: Manifest | None = None,
options: None = None,
) -> SandboxSession:
session_id = uuid.uuid4()
snapshot_id = str(session_id)
snap: SnapshotBase
if isinstance(snapshot, SnapshotSpec):
snap = snapshot.build(snapshot_id)
elif isinstance(snapshot, SnapshotBase):
snap = snapshot
else:
snap = NoopSnapshot(id=snapshot_id)
state = MirageSandboxSessionState(
session_id=session_id,
snapshot=snap,
manifest=manifest or Manifest(root="/"),
)
session = MirageSandboxSession(workspace=self._ws, state=state)
self._sessions[session_id] = session
return self._wrap_session(session)
async def delete(self, session: SandboxSession) -> SandboxSession:
sid = session.state.session_id
self._sessions.pop(sid, None)
return session
async def resume(
self,
state: SandboxSessionState,
) -> SandboxSession:
sid = state.session_id
session = self._sessions.get(sid)
if session is None:
raise ValueError(f"No session to resume: {sid}")
return self._wrap_session(session)
def deserialize_session_state(
self,
payload: dict[str, object],
) -> SandboxSessionState:
return MirageSandboxSessionState.model_validate(payload)