mitos-run 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mitos/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ from mitos import guest
2
+ from mitos.aio import AsyncAgentRun, AsyncSandbox
3
+ from mitos.client import AgentRun
4
+ from mitos.direct import DirectSandbox, SandboxServer, create
5
+ from mitos.errors import (
6
+ AgentRunError,
7
+ ExecutionDeadlineError,
8
+ IdleTimeoutError,
9
+ NotFoundError,
10
+ RateLimitedError,
11
+ RequestCanceledError,
12
+ TimeoutTooLargeError,
13
+ UnauthorizedError,
14
+ )
15
+ from mitos.sandbox import Sandbox
16
+ from mitos.template import Template
17
+ from mitos.types import (
18
+ Execution,
19
+ ExecResult,
20
+ ExecutionError,
21
+ FileInfo,
22
+ ForkPolicy,
23
+ Network,
24
+ Result,
25
+ )
26
+
27
+ __all__ = [
28
+ "create",
29
+ "DirectSandbox",
30
+ "SandboxServer",
31
+ "AgentRun",
32
+ "AgentRunError",
33
+ "ExecutionDeadlineError",
34
+ "IdleTimeoutError",
35
+ "NotFoundError",
36
+ "RateLimitedError",
37
+ "RequestCanceledError",
38
+ "TimeoutTooLargeError",
39
+ "UnauthorizedError",
40
+ "AsyncAgentRun",
41
+ "AsyncSandbox",
42
+ "Sandbox",
43
+ "Template",
44
+ "ExecResult",
45
+ "Execution",
46
+ "ExecutionError",
47
+ "Result",
48
+ "FileInfo",
49
+ "ForkPolicy",
50
+ "Network",
51
+ "guest",
52
+ ]
mitos/_envelope.py ADDED
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+
7
+ from mitos.errors import AgentRunError, error_for_code
8
+
9
+
10
+ def _redact(text: str, token: Optional[str]) -> str:
11
+ """Replaces every occurrence of a non-empty token with [REDACTED]. Mirrors
12
+ the TypeScript redact helper and internal/mcp redaction."""
13
+ if not token:
14
+ return text
15
+ return text.replace(token, "[REDACTED]")
16
+
17
+
18
+ # Default code and remediation per HTTP status, used when the body is not the
19
+ # structured server envelope (an older server, a proxy 502, a transport layer).
20
+ _STATUS_CODE = {
21
+ 400: "bad_request",
22
+ 401: "unauthorized",
23
+ 403: "forbidden",
24
+ 404: "not_found",
25
+ 409: "conflict",
26
+ 413: "request_too_large",
27
+ 429: "rate_limited",
28
+ 500: "internal_error",
29
+ 503: "unavailable",
30
+ }
31
+
32
+ _STATUS_REMEDIATION = {
33
+ 401: "Check the sandbox bearer token is set and authorizes this sandbox.",
34
+ 403: "Check the sandbox bearer token is set and authorizes this sandbox.",
35
+ 404: "Confirm the sandbox id exists and is Ready before calling.",
36
+ 413: "Reduce the request payload size (file content is hex-encoded and bounded by the server).",
37
+ 429: "Back off and retry the request after a short delay.",
38
+ }
39
+
40
+
41
+ def _status_code(status: int) -> str:
42
+ if status in _STATUS_CODE:
43
+ return _STATUS_CODE[status]
44
+ return "server_error" if status >= 500 else "request_failed"
45
+
46
+
47
+ def _status_remediation(status: int) -> str:
48
+ if status in _STATUS_REMEDIATION:
49
+ return _STATUS_REMEDIATION[status]
50
+ if status >= 500:
51
+ return "Retry the request; if it persists, inspect the forkd or sandbox-server logs."
52
+ return "Inspect the request fields against the sandbox API contract."
53
+
54
+
55
+ def error_from_response(resp: httpx.Response, token: Optional[str] = None) -> AgentRunError:
56
+ """Builds an AgentRunError from a non-2xx response. Prefers the structured
57
+ server envelope {error:{code,message,cause,remediation}}; falls back to
58
+ status-derived defaults for an older or non-mitos server. Any bearer token
59
+ echoed in the body is redacted before it becomes the cause."""
60
+ status = resp.status_code
61
+ body_text = _redact(resp.text, token)
62
+
63
+ code = _status_code(status)
64
+ message = f"sandbox API request failed: HTTP {status} ({code})"
65
+ cause = body_text.strip() or f"HTTP {status}"
66
+ remediation = _status_remediation(status)
67
+ context: dict = {}
68
+
69
+ try:
70
+ parsed = resp.json()
71
+ except Exception: # noqa: BLE001 not JSON; keep the text fallback
72
+ parsed = None
73
+
74
+ if isinstance(parsed, dict):
75
+ err = parsed.get("error")
76
+ if isinstance(err, dict):
77
+ # New structured envelope.
78
+ code = err.get("code") or code
79
+ message = err.get("message") or message
80
+ cause = _redact(err.get("cause", ""), token) or cause
81
+ remediation = err.get("remediation") or remediation
82
+ ctx = err.get("context")
83
+ if isinstance(ctx, dict):
84
+ context = ctx
85
+ elif isinstance(err, str):
86
+ # Legacy bare {"error": "msg"} shape.
87
+ cause = _redact(err, token) or cause
88
+
89
+ # Build the TYPED subclass for the code (issue #216): a caller branches on
90
+ # the exception type, never on the message. An unknown code falls back to the
91
+ # base AgentRunError inside error_for_code.
92
+ return error_for_code(
93
+ code,
94
+ message,
95
+ cause=cause,
96
+ remediation=remediation,
97
+ status=status,
98
+ context=context,
99
+ )
100
+
101
+
102
+ def raise_for_status(resp: httpx.Response, token: Optional[str] = None) -> None:
103
+ """Raises AgentRunError on a non-2xx response, leaving 2xx untouched. Drop-in
104
+ for httpx Response.raise_for_status() but yields the structured type."""
105
+ if resp.is_success:
106
+ return
107
+ raise error_from_response(resp, token=token)
108
+
109
+
110
+ def raise_for_status_stream(resp: httpx.Response, token: Optional[str] = None) -> None:
111
+ """Like raise_for_status, for a streaming Response whose body has not been
112
+ read. On a non-2xx status it reads the (small) error body so the structured
113
+ envelope can be parsed; on success it leaves the stream unread so the caller
114
+ iterates it normally."""
115
+ if resp.is_success:
116
+ return
117
+ resp.read()
118
+ raise error_from_response(resp, token=token)
mitos/_k8s.py ADDED
@@ -0,0 +1,69 @@
1
+ """Lazy accessor for the optional ``kubernetes`` client (issue #22).
2
+
3
+ The cluster-mode modules (``client``, ``aio``, ``sandbox``, ``workspace``) need
4
+ the official Kubernetes client, but direct mode (``mitos.direct`` over the
5
+ sandbox-server REST API) and the in-guest SDK (``mitos.guest``) speak only
6
+ httpx and must import with no Kubernetes installed. To keep ``import mitos`` and
7
+ ``from mitos.direct import SandboxServer`` light, those modules import the
8
+ ``kubernetes`` symbols through :func:`k8s` at the moment a cluster code path
9
+ runs, never at module import time. This mirrors the TypeScript SDK, whose
10
+ ``@kubernetes/client-node`` is lazy-loaded so direct mode never pulls it in.
11
+
12
+ A missing ``kubernetes`` raises a clear, actionable AgentRunError naming the
13
+ install command, rather than a bare ModuleNotFoundError from deep in an import.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ from mitos.errors import AgentRunError
21
+
22
+ if TYPE_CHECKING:
23
+ # Annotation-only imports: TYPE_CHECKING is False at runtime, so these never
24
+ # force the kubernetes import for callers that only use direct mode.
25
+ from kubernetes import client as _client_mod
26
+ from kubernetes import config as _config_mod
27
+ from kubernetes.client.rest import ApiException as _ApiException
28
+
29
+
30
+ class _K8s:
31
+ """The three kubernetes symbols the cluster modules use, bound lazily."""
32
+
33
+ __slots__ = ("client", "config", "ApiException")
34
+
35
+ def __init__(self, client, config, ApiException):
36
+ self.client = client
37
+ self.config = config
38
+ self.ApiException = ApiException
39
+
40
+
41
+ _cached: "_K8s | None" = None
42
+
43
+
44
+ def k8s() -> "_K8s":
45
+ """Import and return the kubernetes client/config/ApiException, cached.
46
+
47
+ Called only from cluster code paths. If the optional ``kubernetes`` package
48
+ is not installed, raises an AgentRunError that names the fix instead of
49
+ letting a raw ModuleNotFoundError surface from an unexpected place."""
50
+ global _cached
51
+ if _cached is not None:
52
+ return _cached
53
+ try:
54
+ from kubernetes import client as k8s_client
55
+ from kubernetes import config as k8s_config
56
+ from kubernetes.client.rest import ApiException
57
+ except ModuleNotFoundError as exc:
58
+ raise AgentRunError(
59
+ "cluster mode requires the kubernetes client, which is not installed",
60
+ code="kubernetes_not_installed",
61
+ cause=f"importing the kubernetes package failed: {exc}",
62
+ remediation=(
63
+ "Install it with 'pip install kubernetes' or 'pip install mitos-run[k8s]'. "
64
+ "Direct mode (mitos.create / mitos.direct) and the in-guest SDK "
65
+ "(mitos.guest) do not need it."
66
+ ),
67
+ ) from exc
68
+ _cached = _K8s(k8s_client, k8s_config, ApiException)
69
+ return _cached