genesis-sandbox-client-python 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.
@@ -0,0 +1,119 @@
1
+ """只读控制前提查询;不把Docker配置事实当成工作负载隔离验收。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .errors import ProtocolError
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class RuntimeControlCapabilities:
14
+ available: bool
15
+ runtime_kind: str
16
+ supports_resource_limits: bool
17
+
18
+ def __post_init__(self) -> None:
19
+ if (
20
+ type(self.available) is not bool
21
+ or type(self.supports_resource_limits) is not bool
22
+ or not isinstance(self.runtime_kind, str)
23
+ or self.runtime_kind not in {"docker", "runsc"}
24
+ ):
25
+ raise ProtocolError("invalid runtime control facts")
26
+
27
+
28
+ def parse_control_capabilities(value: Any) -> RuntimeControlCapabilities:
29
+ allowed = {
30
+ "available",
31
+ "runtime_kind",
32
+ "default_runtime",
33
+ "available_runtimes",
34
+ "runtime_profiles",
35
+ "supported_task_types",
36
+ "supported_languages",
37
+ "supported_state_policies",
38
+ "queue",
39
+ "queue_snapshots",
40
+ "execd",
41
+ "supports_runsc",
42
+ "supports_network_none",
43
+ "supports_resource_limits",
44
+ "message",
45
+ }
46
+ required = {
47
+ "available",
48
+ "runtime_kind",
49
+ "default_runtime",
50
+ "available_runtimes",
51
+ "queue",
52
+ "execd",
53
+ "supports_runsc",
54
+ "supports_network_none",
55
+ "supports_resource_limits",
56
+ }
57
+ if not isinstance(value, dict) or not required.issubset(value) or set(value) - allowed:
58
+ raise ProtocolError("invalid runtime control capability fields")
59
+ for field in ("available", "supports_runsc", "supports_network_none", "supports_resource_limits"):
60
+ if type(value[field]) is not bool:
61
+ raise ProtocolError("invalid runtime control boolean")
62
+ if (
63
+ not isinstance(value["runtime_kind"], str)
64
+ or value["runtime_kind"] not in {"docker", "runsc"}
65
+ or not isinstance(value["default_runtime"], str)
66
+ ):
67
+ raise ProtocolError("invalid runtime control kind")
68
+ for field in ("available_runtimes", "supported_task_types", "supported_languages", "supported_state_policies"):
69
+ if field not in value:
70
+ continue
71
+ items = value[field]
72
+ if items is None and not value["available"] and field == "available_runtimes":
73
+ continue
74
+ if (
75
+ not isinstance(items, list)
76
+ or len(items) > 256
77
+ or any(not isinstance(item, str) or len(item) > 256 for item in items)
78
+ ):
79
+ raise ProtocolError("invalid runtime control list")
80
+ if not isinstance(value["queue"], dict) or not isinstance(value["execd"], dict):
81
+ raise ProtocolError("invalid runtime control components")
82
+ if set(value["execd"]) != {"command", "session", "files", "metrics"} or any(
83
+ type(item) is not bool for item in value["execd"].values()
84
+ ):
85
+ raise ProtocolError("invalid runtime control execd declaration")
86
+ # 明确只消费driver.Probe与store.Ping的前提事实;注册runtime与execd声明不是隔离证明。
87
+ return RuntimeControlCapabilities(value["available"], value["runtime_kind"], value["supports_resource_limits"])
88
+
89
+
90
+ class RuntimeControlClientMixin:
91
+ def _request(
92
+ self,
93
+ method: str,
94
+ path: str,
95
+ body: dict[str, Any] | None = None,
96
+ timeout: float | None = None,
97
+ max_retries: int | None = None,
98
+ deadline_seconds: float | None = None,
99
+ ) -> Any:
100
+ raise NotImplementedError
101
+
102
+ def get_runtime_control_capabilities(self, *, timeout_seconds: float = 10) -> RuntimeControlCapabilities:
103
+ if (
104
+ isinstance(timeout_seconds, bool)
105
+ or not isinstance(timeout_seconds, (int, float))
106
+ or not math.isfinite(timeout_seconds)
107
+ or not 0 < timeout_seconds <= 30
108
+ ):
109
+ raise ValueError("control probe timeout must be finite and at most 30 seconds")
110
+ # 即便是GET也只发送一次;整批readiness的截止时间不得被客户端重试累加。
111
+ return parse_control_capabilities(
112
+ self._request(
113
+ "GET",
114
+ "/v1/sandbox/capabilities",
115
+ timeout=timeout_seconds,
116
+ max_retries=1,
117
+ deadline_seconds=timeout_seconds,
118
+ )
119
+ )
@@ -0,0 +1,96 @@
1
+ """控制探测专用绝对读取截止;不改变其他业务调用的传输语义。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import http.client
6
+ import io
7
+ import socket
8
+ import time
9
+ import urllib.request
10
+ from typing import Any
11
+
12
+
13
+ def _remaining(deadline: float) -> float:
14
+ remaining = deadline - time.monotonic()
15
+ if remaining <= 0:
16
+ raise TimeoutError("control probe deadline exceeded")
17
+ return remaining
18
+
19
+
20
+ class _DeadlineReader(io.RawIOBase):
21
+ def __init__(self, raw: Any, connection: socket.socket, deadline: float) -> None:
22
+ super().__init__()
23
+ self.raw, self.connection, self.deadline = raw, connection, deadline
24
+
25
+ def readable(self) -> bool:
26
+ return True
27
+
28
+ def readinto(self, buffer: Any) -> int:
29
+ self.connection.settimeout(_remaining(self.deadline))
30
+ return self.raw.readinto(buffer)
31
+
32
+ def close(self) -> None:
33
+ try:
34
+ self.raw.close()
35
+ finally:
36
+ super().close()
37
+
38
+
39
+ class _DeadlineSocket:
40
+ def __init__(self, connection: socket.socket, deadline: float) -> None:
41
+ self.connection, self.deadline = connection, deadline
42
+
43
+ def __getattr__(self, name: str) -> Any:
44
+ return getattr(self.connection, name)
45
+
46
+ def makefile(self, mode: str, buffering: int | None = None) -> io.BufferedReader:
47
+ if mode != "rb":
48
+ raise ValueError("control probe only supports binary response reads")
49
+ # 使用原socket的file引用:HTTPConnection关闭时响应体仍由原file持有。
50
+ raw = self.connection.makefile("rb", buffering=0)
51
+ return io.BufferedReader(_DeadlineReader(raw, self.connection, self.deadline))
52
+
53
+
54
+ class _DeadlineHTTPConnection(http.client.HTTPConnection):
55
+ def __init__(self, *args: Any, deadline: float, **kwargs: Any) -> None:
56
+ self.deadline = deadline
57
+ super().__init__(*args, **kwargs)
58
+
59
+ def connect(self) -> None:
60
+ self.timeout = _remaining(self.deadline)
61
+ super().connect()
62
+ assert self.sock is not None
63
+ self.sock.settimeout(_remaining(self.deadline))
64
+ self.sock = _DeadlineSocket(self.sock, self.deadline) # type: ignore[assignment]
65
+
66
+
67
+ class _DeadlineHTTPSConnection(http.client.HTTPSConnection):
68
+ def __init__(self, *args: Any, deadline: float, **kwargs: Any) -> None:
69
+ self.deadline = deadline
70
+ super().__init__(*args, **kwargs)
71
+
72
+ def connect(self) -> None:
73
+ self.timeout = _remaining(self.deadline)
74
+ super().connect()
75
+ assert self.sock is not None
76
+ self.sock.settimeout(_remaining(self.deadline))
77
+ self.sock = _DeadlineSocket(self.sock, self.deadline) # type: ignore[assignment]
78
+
79
+
80
+ def deadline_urlopen(request: urllib.request.Request, timeout_seconds: float) -> Any:
81
+ from .client import _NoRedirect
82
+
83
+ deadline = time.monotonic() + timeout_seconds
84
+
85
+ class HTTP(urllib.request.HTTPHandler):
86
+ def http_open(self, req: Any) -> Any:
87
+ return self.do_open(lambda *args, **kwargs: _DeadlineHTTPConnection(
88
+ *args, deadline=deadline, **kwargs), req)
89
+
90
+ class HTTPS(urllib.request.HTTPSHandler):
91
+ def https_open(self, req: Any) -> Any:
92
+ return self.do_open(lambda *args, **kwargs: _DeadlineHTTPSConnection(
93
+ *args, deadline=deadline, **kwargs), req)
94
+
95
+ return urllib.request.build_opener(_NoRedirect(), HTTP(), HTTPS()).open(
96
+ request, timeout=_remaining(deadline))
@@ -0,0 +1,133 @@
1
+ """Typed exceptions exposed by the Genesis Sandbox Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ class SandboxError(Exception):
10
+ """Base class for all SDK-originated errors."""
11
+
12
+
13
+ class APIError(SandboxError):
14
+ """Structured error returned by the Sandbox Service."""
15
+
16
+ def __init__(
17
+ self,
18
+ status_code: int,
19
+ error_code: str,
20
+ message: str,
21
+ *,
22
+ request_id: str = "",
23
+ details: dict[str, Any] | None = None,
24
+ retry_after: float | None = None,
25
+ ) -> None:
26
+ self.status_code = status_code
27
+ self.error_code = error_code
28
+ self.message = message
29
+ self.request_id = request_id
30
+ self.details = details or {}
31
+ self.retry_after = retry_after
32
+ super().__init__(
33
+ f"sandbox API {error_code or status_code} "
34
+ f"(status={status_code}, request_id={request_id or 'unknown'}): {message}"
35
+ )
36
+
37
+ @property
38
+ def retryable(self) -> bool:
39
+ value = self.details.get("retryable")
40
+ if isinstance(value, bool):
41
+ return value
42
+ return self.status_code == 429 or self.status_code >= 500
43
+
44
+
45
+ class TransportError(SandboxError):
46
+ """Network or protocol failure before a valid API response was received."""
47
+
48
+
49
+ class ProtocolError(TransportError):
50
+ """服务端返回了无法按契约解析的响应(如非法 JSON)。
51
+
52
+ 继承 TransportError 以保持既有 except TransportError 调用方兼容;
53
+ classify_error 将其归类为 "protocol" 而不是 "transport"。
54
+ """
55
+
56
+
57
+ class ExecRecoveryError(SandboxError):
58
+ """An exec may still be running; use its operation identity to resume observation.
59
+
60
+ ``phase`` 语义(Adapter 依赖稳定取值):
61
+ - ``submit``:提交请求结果未知,远端执行可能已启动,须按 operation_id 查回执。
62
+ - ``observe``:观察回执失败,执行状态未确认,保持原 operation_id 继续观察。
63
+ - ``lookup``:按 operation_id 查回执失败,结果未知。
64
+ - ``cancel``:取消后停止未确认(stop unknown)。终态与停止确认是两件事;
65
+ 未确认停止前不得回收资源或再次提交替代执行,须先按 operation_id 核对。
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ session_id: str,
71
+ operation_id: str,
72
+ *,
73
+ exec_id: str | None = None,
74
+ phase: str,
75
+ cause: BaseException | None = None,
76
+ ) -> None:
77
+ self.session_id = session_id
78
+ self.operation_id = operation_id
79
+ self.exec_id = exec_id
80
+ self.phase = phase
81
+ self.cause = cause
82
+ identity = f"session_id={session_id}, operation_id={operation_id}"
83
+ if exec_id:
84
+ identity += f", exec_id={exec_id}"
85
+ super().__init__(f"exec {phase} requires recovery ({identity})")
86
+
87
+
88
+ def classify_error(exc: BaseException) -> str:
89
+ """把 SDK 异常映射为稳定分类字符串,供上层 Adapter 转换为通用契约错误码。
90
+
91
+ 返回值(枚举式字符串,取值长期稳定):
92
+
93
+ - ``"invalid_request"``:请求本身不合法,不可重试。
94
+ APIError 4xx(401/403/404/409/412/429 除外);本地参数校验 ValueError。
95
+ - ``"unauthorized"``:APIError 401/403。
96
+ - ``"not_found"``:APIError 404。
97
+ - ``"conflict"``:APIError 409/412(含 operation/版本冲突)。
98
+ - ``"rate_limited"``:APIError 429。
99
+ - ``"transient"``:APIError 5xx;可按 retry_advice 有限重试观察类请求。
100
+ - ``"transport"``:TransportError(网络/连接层失败,未获得有效 API 响应)。
101
+ - ``"protocol"``:ProtocolError、JSON 解析失败(json.JSONDecodeError /
102
+ UnicodeDecodeError),以及 APIError 3xx(重定向被拒绝,属意外响应)。
103
+ - ``"unknown"``:其余未归类异常(含无 cause 的 ExecRecoveryError)。
104
+
105
+ ExecRecoveryError 代表“结果未知、需按 operation_id 核对”,不是可直接映射的
106
+ 终态错误;此处按其 cause 递归分类,无 cause 时返回 "unknown"。
107
+ """
108
+ if isinstance(exc, APIError):
109
+ status = exc.status_code
110
+ if status in (401, 403):
111
+ return "unauthorized"
112
+ if status == 404:
113
+ return "not_found"
114
+ if status in (409, 412):
115
+ return "conflict"
116
+ if status == 429:
117
+ return "rate_limited"
118
+ if 400 <= status < 500:
119
+ return "invalid_request"
120
+ if 500 <= status < 600:
121
+ return "transient"
122
+ return "protocol"
123
+ if isinstance(exc, ExecRecoveryError):
124
+ return classify_error(exc.cause) if exc.cause is not None else "unknown"
125
+ if isinstance(exc, ProtocolError):
126
+ return "protocol"
127
+ if isinstance(exc, TransportError):
128
+ return "transport"
129
+ if isinstance(exc, (json.JSONDecodeError, UnicodeDecodeError)):
130
+ return "protocol"
131
+ if isinstance(exc, ValueError):
132
+ return "invalid_request"
133
+ return "unknown"
@@ -0,0 +1,270 @@
1
+ """原调用治理身份与维护只读证明;不从文件名或 UID 区间推导授权。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ import json
8
+ import re
9
+ from datetime import UTC, datetime
10
+ from typing import Any
11
+
12
+ from .errors import ProtocolError
13
+
14
+ _DIGEST = re.compile(r"[0-9a-f]{64}\Z")
15
+ _CONTEXT = {
16
+ "tenant_id",
17
+ "user_id",
18
+ "run_id",
19
+ "trace_id",
20
+ "authorization_scope",
21
+ "decision_reference",
22
+ "subject_kind",
23
+ "invocation_id",
24
+ "execution_id",
25
+ "attempt_id",
26
+ }
27
+
28
+
29
+ def _object(value: Any, fields: set[str], name: str) -> dict[str, Any]:
30
+ if not isinstance(value, dict) or set(value) != fields:
31
+ raise ValueError(f"{name} requires exact original fields")
32
+ return value
33
+
34
+
35
+ def _opaque(value: Any, name: str) -> str:
36
+ if (
37
+ not isinstance(value, str)
38
+ or not value
39
+ or len(value.encode("utf-8")) > 256
40
+ or any(ord(char) < 32 or ord(char) == 127 or char in "\u2028\u2029" for char in value)
41
+ ):
42
+ raise ValueError(f"{name} requires a bounded opaque identity")
43
+ return value
44
+
45
+
46
+ def call_context(value: Any) -> dict[str, Any]:
47
+ context = _object(value, _CONTEXT, "context")
48
+ for key in ("tenant_id", "user_id", "run_id", "trace_id", "authorization_scope"):
49
+ _opaque(context[key], key)
50
+ if context["subject_kind"] != "enterprise":
51
+ raise ValueError("enterprise call context required")
52
+ if context["decision_reference"] is not None:
53
+ _opaque(context["decision_reference"], "decision_reference")
54
+ if any(context[key] is not None for key in ("invocation_id", "execution_id", "attempt_id")):
55
+ for key in ("invocation_id", "execution_id", "attempt_id"):
56
+ _opaque(context[key], key)
57
+ return copy.deepcopy(context)
58
+
59
+
60
+ def lease_ref(value: Any) -> dict[str, Any]:
61
+ lease = _object(value, {"provider_id", "lease_id", "workspace", "generation", "expires_at"}, "lease_ref")
62
+ workspace = _object(
63
+ lease["workspace"], {"workspace_key", "provider_id", "resource_id", "scope", "generation"}, "workspace_ref"
64
+ )
65
+ for obj, keys in (
66
+ (lease, ("provider_id", "lease_id")),
67
+ (workspace, ("workspace_key", "provider_id", "resource_id", "scope")),
68
+ ):
69
+ for key in keys:
70
+ _opaque(obj[key], key)
71
+ if type(obj["generation"]) is not int or not 0 < obj["generation"] <= 2**63 - 1:
72
+ raise ValueError("original reference generation must be a positive int64")
73
+ if lease["provider_id"] != workspace["provider_id"]:
74
+ raise ValueError("original lease/workspace provider mismatch")
75
+ if not isinstance(lease["expires_at"], str):
76
+ raise ValueError("original lease timestamp required")
77
+ timestamp = datetime.fromisoformat(lease["expires_at"].replace("Z", "+00:00"))
78
+ if timestamp.tzinfo is None or timestamp.utcoffset() != UTC.utcoffset(timestamp):
79
+ raise ValueError("original lease timestamp must be UTC")
80
+ return copy.deepcopy(lease)
81
+
82
+
83
+ def trusted_governance(value: Any) -> dict[str, Any]:
84
+ record = _object(
85
+ value,
86
+ {
87
+ "context",
88
+ "lease_ref",
89
+ "platform_operation_id",
90
+ "request_digest",
91
+ "source_identity",
92
+ "workspace_id",
93
+ "authorized_work_write",
94
+ },
95
+ "trusted_governance",
96
+ )
97
+ call_context(record["context"])
98
+ if any(record["context"][key] is None for key in ("invocation_id", "execution_id", "attempt_id")):
99
+ raise ValueError("governed execution requires complete original invocation identity")
100
+ lease_ref(record["lease_ref"])
101
+ if record["lease_ref"]["workspace"]["scope"] != record["context"]["authorization_scope"]:
102
+ raise ValueError("original lease scope differs from trusted execution context")
103
+ for key in ("platform_operation_id", "workspace_id"):
104
+ _opaque(record[key], key)
105
+ for key in ("request_digest", "source_identity"):
106
+ if not isinstance(record[key], str) or not _DIGEST.fullmatch(record[key]):
107
+ raise ValueError(f"{key} must be a lowercase SHA256")
108
+ if type(record["authorized_work_write"]) is not bool:
109
+ raise ValueError("work-write authorization must be a trusted boolean")
110
+ if not record["authorized_work_write"]:
111
+ raise ValueError("per-invocation read-only work enforcement is unavailable")
112
+ return copy.deepcopy(record)
113
+
114
+
115
+ def maintenance_query(
116
+ workspace_id: str, context: dict[str, Any], operation_id: str, source_identity: str, claim_digest: str
117
+ ) -> dict[str, Any]:
118
+ payload = {
119
+ "workspace_id": _opaque(workspace_id, "workspace_id"),
120
+ "context": call_context(context),
121
+ "operation_id": _opaque(operation_id, "operation_id"),
122
+ "source_identity": source_identity,
123
+ }
124
+ if not isinstance(source_identity, str) or not _DIGEST.fullmatch(source_identity):
125
+ raise ValueError("source_identity must be a lowercase SHA256")
126
+ raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
127
+ if claim_digest != hashlib.sha256(raw).hexdigest():
128
+ raise ValueError("original maintenance query digest mismatch")
129
+ return {**payload, "claim_digest": claim_digest}
130
+
131
+
132
+ def maintenance_proof(value: Any, query: dict[str, Any]) -> dict[str, Any]:
133
+ try:
134
+ proof = _object(
135
+ value,
136
+ {
137
+ "context",
138
+ "operation_id",
139
+ "claim_digest",
140
+ "source_identity",
141
+ "root_device",
142
+ "root_inode",
143
+ "calls",
144
+ "leaves",
145
+ },
146
+ "maintenance_proof",
147
+ )
148
+ for key in ("context", "operation_id", "claim_digest", "source_identity"):
149
+ if proof[key] != query[key]:
150
+ raise ValueError("original maintenance query scope changed")
151
+ for key in ("root_device", "root_inode"):
152
+ if type(proof[key]) is not int or not 0 <= proof[key] <= 2**63 - 1:
153
+ raise ValueError("source root tuple exceeds signed int64")
154
+ if proof["root_inode"] == 0 or not isinstance(proof["calls"], list) or len(proof["calls"]) > 10000:
155
+ raise ValueError("original same-source call list exceeds bound")
156
+ calls: dict[str, dict[str, Any]] = {}
157
+ seen_uids: set[int] = set()
158
+ for call in proof["calls"]:
159
+ _object(
160
+ call,
161
+ {
162
+ "call_id",
163
+ "context",
164
+ "lease_ref",
165
+ "operation_id",
166
+ "request_digest",
167
+ "execution_id",
168
+ "output_directory",
169
+ "uid",
170
+ "gid",
171
+ "root_device",
172
+ "root_inode",
173
+ "authorized_work_write",
174
+ "stop_confirmed",
175
+ },
176
+ "maintenance_call",
177
+ )
178
+ call_context(call["context"])
179
+ if (
180
+ any(call["context"][key] is None for key in ("invocation_id", "execution_id", "attempt_id"))
181
+ or call["execution_id"] != call["context"]["execution_id"]
182
+ ):
183
+ raise ValueError("original call execution identity mismatch")
184
+ lease_ref(call["lease_ref"])
185
+ if call["lease_ref"]["workspace"]["scope"] != call["context"]["authorization_scope"]:
186
+ raise ValueError("original call lease scope differs from context")
187
+ for key in ("call_id", "operation_id", "execution_id"):
188
+ _opaque(call[key], key)
189
+ if (
190
+ call["context"]["tenant_id"] != query["context"]["tenant_id"]
191
+ or call["context"]["user_id"] != query["context"]["user_id"]
192
+ ):
193
+ raise ValueError("foreign same-source call owner")
194
+ if call["stop_confirmed"] is not True or type(call["authorized_work_write"]) is not bool:
195
+ raise ValueError("original stop/work-write facts missing")
196
+ if not isinstance(call["request_digest"], str) or not _DIGEST.fullmatch(call["request_digest"]):
197
+ raise ValueError("original request digest missing")
198
+ for key in ("uid", "gid", "root_inode", "root_device"):
199
+ if type(call[key]) is not int or not 0 <= call[key] <= 2**63 - 1:
200
+ raise ValueError("original allocation tuple exceeds signed int64")
201
+ if min(call["uid"], call["gid"], call["root_inode"]) < 1 or call["root_device"] != proof["root_device"]:
202
+ raise ValueError("original same-source allocation mismatch")
203
+ if call["call_id"] in calls or call["uid"] in seen_uids:
204
+ raise ValueError("ambiguous original allocation identity")
205
+ if not isinstance(call["output_directory"], str) or not re.fullmatch(
206
+ r"output/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", call["output_directory"]
207
+ ):
208
+ raise ValueError("original output root is not a single managed segment")
209
+ calls[call["call_id"]] = call
210
+ seen_uids.add(call["uid"])
211
+ if not isinstance(proof["leaves"], list) or len(proof["leaves"]) > 100000:
212
+ raise ValueError("maintenance leaf budget exceeded")
213
+ seen_paths: set[str] = set()
214
+ for leaf in proof["leaves"]:
215
+ _object(leaf, {"path", "device", "inode", "uid", "gid", "mode", "directory", "call_id"}, "maintenance_leaf")
216
+ path = leaf["path"]
217
+ if (
218
+ not isinstance(path, str)
219
+ or path.split("/")[0] not in {"work", "output"}
220
+ or any(part in {"", ".", ".."} for part in path.split("/"))
221
+ or path in seen_paths
222
+ ):
223
+ raise ValueError("maintenance leaf path invalid or duplicate")
224
+ seen_paths.add(path)
225
+ for key in ("device", "inode", "uid", "gid", "mode"):
226
+ if type(leaf[key]) is not int or not 0 <= leaf[key] <= 2**63 - 1:
227
+ raise ValueError("maintenance leaf tuple invalid")
228
+ if (
229
+ leaf["inode"] < 1
230
+ or leaf["device"] != proof["root_device"]
231
+ or leaf["mode"] > 0o7777
232
+ or type(leaf["directory"]) is not bool
233
+ ):
234
+ raise ValueError("maintenance leaf source/type/mode invalid")
235
+ if leaf["call_id"] is not None and leaf["call_id"] not in calls:
236
+ raise ValueError("maintenance leaf references an unknown original call")
237
+ if leaf["call_id"] is not None:
238
+ owner = calls[leaf["call_id"]]
239
+ if path == owner["output_directory"]:
240
+ if (
241
+ not leaf["directory"]
242
+ or leaf["uid"] != 0
243
+ or leaf["gid"] != owner["gid"]
244
+ or leaf["device"] != owner["root_device"]
245
+ or leaf["inode"] != owner["root_inode"]
246
+ or leaf["mode"] != 0o770
247
+ ):
248
+ raise ValueError("original managed output root changed")
249
+ elif (
250
+ leaf["uid"] != owner["uid"]
251
+ or leaf["gid"] not in {owner["gid"], 65532}
252
+ or (path.startswith("work/") and not owner["authorized_work_write"])
253
+ or (path.startswith("output/") and not path.startswith(owner["output_directory"] + "/"))
254
+ ):
255
+ raise ValueError("maintenance leaf original call ownership mismatch")
256
+ elif leaf["uid"] not in {0, 65532} or (
257
+ path.startswith("output/") and leaf["uid"] == 0 and leaf["gid"] >= 100001
258
+ ):
259
+ raise ValueError("foreign leaf original call identity absent")
260
+ if not {"work", "output"} <= seen_paths:
261
+ raise ValueError("maintenance public root tuples absent")
262
+ for call in calls.values():
263
+ if not any(
264
+ leaf["path"] == call["output_directory"] and leaf["call_id"] == call["call_id"]
265
+ for leaf in proof["leaves"]
266
+ ):
267
+ raise ValueError("original call output root proof absent")
268
+ return copy.deepcopy(proof)
269
+ except (ValueError, TypeError, KeyError) as exc:
270
+ raise ProtocolError(str(exc)) from exc
@@ -0,0 +1,11 @@
1
+ """调用输出协议只携带逻辑单段目录,不接受宿主路径授权。"""
2
+
3
+ import re
4
+
5
+
6
+ def output_request(value: str | None) -> dict[str, str]:
7
+ if value is None:
8
+ return {}
9
+ if not isinstance(value, str) or not re.fullmatch(r"output/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", value):
10
+ raise ValueError("invalid invocation output directory")
11
+ return {"output_directory": value}
@@ -0,0 +1,31 @@
1
+ """响应JSON拒绝重复字段和非有限数值,避免原调用身份被最后字段覆盖。"""
2
+
3
+ import json
4
+ import math
5
+ from typing import Any
6
+
7
+ from .errors import ProtocolError
8
+
9
+
10
+ def _object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
11
+ result: dict[str, Any] = {}
12
+ for key, value in pairs:
13
+ if key in result:
14
+ raise ProtocolError("sandbox response contains duplicate JSON fields")
15
+ result[key] = value
16
+ return result
17
+
18
+
19
+ def _nonfinite(value: str) -> None:
20
+ raise ProtocolError("sandbox response contains non-finite JSON number")
21
+
22
+
23
+ def _float(value: str) -> float:
24
+ number = float(value)
25
+ if not math.isfinite(number):
26
+ raise ProtocolError("sandbox response contains overflowing JSON number")
27
+ return number
28
+
29
+
30
+ def loads(value: str | bytes) -> Any:
31
+ return json.loads(value, object_pairs_hook=_object, parse_constant=_nonfinite, parse_float=_float)
File without changes