opensandbox-server 0.2.1__tar.gz → 0.2.3__tar.gz

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.
Files changed (124) hide show
  1. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/.gitignore +14 -0
  2. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/PKG-INFO +9 -4
  3. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/devops.py +41 -27
  4. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/lifecycle.py +32 -4
  5. opensandbox_server-0.2.3/opensandbox_server/api/metrics.py +83 -0
  6. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/proxy.py +249 -52
  7. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/schema.py +147 -5
  8. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/cli.py +2 -0
  9. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/config.py +214 -14
  10. opensandbox_server-0.2.3/opensandbox_server/constants.py +18 -0
  11. opensandbox_server-0.2.3/opensandbox_server/examples/e2e.batchsandbox-template.yaml +33 -0
  12. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/example.config.k8s.toml +4 -4
  13. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/example.config.k8s.zh.toml +4 -4
  14. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/example.config.toml +16 -4
  15. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/example.config.zh.toml +15 -4
  16. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/extensions/__init__.py +4 -2
  17. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/extensions/codec.py +23 -3
  18. opensandbox_server-0.2.3/opensandbox_server/integrations/otel/__init__.py +29 -0
  19. opensandbox_server-0.2.3/opensandbox_server/integrations/otel/metrics.py +235 -0
  20. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/consumer.py +2 -2
  21. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/main.py +115 -12
  22. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/middleware/auth.py +59 -40
  23. opensandbox_server-0.2.3/opensandbox_server/middleware/date_header.py +43 -0
  24. opensandbox_server-0.2.3/opensandbox_server/middleware/http_metrics.py +86 -0
  25. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/repositories/snapshots/sqlite.py +80 -1
  26. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/constants.py +17 -1
  27. opensandbox_server-0.2.3/opensandbox_server/services/diagnostics.py +85 -0
  28. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/__init__.py +1 -2
  29. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/container_ops.py +7 -1
  30. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/docker_diagnostics.py +103 -3
  31. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/docker_service.py +112 -180
  32. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/metadata.py +30 -8
  33. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/networking.py +76 -23
  34. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/port_allocator.py +3 -1
  35. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/runtime.py +116 -1
  36. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/snapshot_runtime.py +12 -3
  37. opensandbox_server-0.2.3/opensandbox_server/services/fleets/__init__.py +13 -0
  38. opensandbox_server-0.2.3/opensandbox_server/services/fleets/create_mapping.py +280 -0
  39. opensandbox_server-0.2.3/opensandbox_server/services/fleets/fastpath_client.py +310 -0
  40. opensandbox_server-0.2.3/opensandbox_server/services/fleets/status_mapping.py +106 -0
  41. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/helpers.py +13 -1
  42. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/agent_sandbox_provider.py +26 -3
  43. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/batchsandbox_provider.py +159 -26
  44. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/client.py +26 -6
  45. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/create_helpers.py +6 -0
  46. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/egress_helper.py +6 -0
  47. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/informer.py +60 -6
  48. opensandbox_server-0.2.3/opensandbox_server/services/k8s/k8s_diagnostics.py +455 -0
  49. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/kubernetes_service.py +265 -52
  50. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/pool_service.py +24 -22
  51. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/provider_common.py +9 -1
  52. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/snapshot_runtime.py +34 -15
  53. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/status_helpers.py +8 -1
  54. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/volume_helper.py +20 -1
  55. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/workload_mapper.py +126 -1
  56. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/workload_provider.py +9 -0
  57. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/sandbox_service.py +47 -1
  58. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_models.py +1 -0
  59. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_repository.py +2 -0
  60. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_restore.py +11 -0
  61. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_runtime.py +8 -4
  62. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_service.py +38 -4
  63. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/validators.py +39 -2
  64. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/startup_guard.py +4 -4
  65. opensandbox_server-0.2.3/opensandbox_server/tenants/__init__.py +156 -0
  66. opensandbox_server-0.2.3/opensandbox_server/tenants/context.py +30 -0
  67. opensandbox_server-0.2.3/opensandbox_server/tenants/file_provider.py +193 -0
  68. opensandbox_server-0.2.3/opensandbox_server/tenants/http_provider.py +240 -0
  69. opensandbox_server-0.2.3/opensandbox_server/tenants/models.py +25 -0
  70. opensandbox_server-0.2.3/opensandbox_server/tenants/provider.py +74 -0
  71. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/pyproject.toml +9 -2
  72. opensandbox_server-0.2.1/opensandbox_server/examples/e2e.batchsandbox-template.yaml +0 -17
  73. opensandbox_server-0.2.1/opensandbox_server/services/k8s/k8s_diagnostics.py +0 -198
  74. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/LICENSE +0 -0
  75. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/README.md +0 -0
  76. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/__init__.py +0 -0
  77. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/__init__.py +0 -0
  78. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/api/pool.py +0 -0
  79. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/.gitkeep +0 -0
  80. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/__init__.py +0 -0
  81. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/examples/example.batchsandbox-template.yaml +0 -0
  82. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/extensions/keys.py +0 -0
  83. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/extensions/validation.py +0 -0
  84. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/__init__.py +0 -0
  85. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/__init__.py +0 -0
  86. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/constants.py +0 -0
  87. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/controller.py +0 -0
  88. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/intent.py +0 -0
  89. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/logutil.py +0 -0
  90. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/proxy_renew.py +0 -0
  91. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/redis_client.py +0 -0
  92. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/integrations/renew_intent/runner.py +0 -0
  93. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/logging_config.py +0 -0
  94. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/middleware/__init__.py +0 -0
  95. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/middleware/request_id.py +0 -0
  96. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/py.typed +0 -0
  97. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/repositories/__init__.py +0 -0
  98. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/repositories/snapshots/__init__.py +0 -0
  99. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/repositories/snapshots/factory.py +0 -0
  100. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/__init__.py +0 -0
  101. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/ossfs_mixin.py +0 -0
  102. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/volumes.py +0 -0
  103. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/docker/windows_profile.py +0 -0
  104. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/endpoint_auth.py +0 -0
  105. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/extension_service.py +0 -0
  106. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/factory.py +0 -0
  107. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/__init__.py +0 -0
  108. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/agent_sandbox_template.py +0 -0
  109. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/batchsandbox_template.py +0 -0
  110. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/endpoint_resolver.py +0 -0
  111. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/error_helpers.py +0 -0
  112. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/image_pull_secret_helper.py +0 -0
  113. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/label_selector.py +0 -0
  114. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/list_helpers.py +0 -0
  115. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/provider_factory.py +0 -0
  116. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/rate_limiter.py +0 -0
  117. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/security_context.py +0 -0
  118. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/template_manager.py +0 -0
  119. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/windows_profile.py +0 -0
  120. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/k8s/workload_access.py +0 -0
  121. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/runtime_resolver.py +0 -0
  122. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/signing.py +0 -0
  123. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/snapshot_runtime_factory.py +0 -0
  124. {opensandbox_server-0.2.1 → opensandbox_server-0.2.3}/opensandbox_server/services/windows_common.py +0 -0
@@ -269,6 +269,19 @@ nbdist/
269
269
  generated/
270
270
  **/generated/**
271
271
 
272
+ # fleets FastPath gRPC stubs are committed intentionally (deterministic protoc
273
+ # output; the server package needs them at install time, so they are checked
274
+ # in rather than regenerated).
275
+ !server/opensandbox_server/services/fleets/generated/
276
+ !server/opensandbox_server/services/fleets/generated/**
277
+ server/opensandbox_server/services/fleets/generated/__pycache__/
278
+
279
+ # execd eBPF kernel BTF dumps (bpf2go build-time only, never committed —
280
+ # see components/execd/pkg/ebpf/prog/audit.bpf.c; regenerate with:
281
+ # bpftool btf dump file /sys/kernel/btf/vmlinux format c > .../vmlinux.h)
282
+ components/execd/pkg/ebpf/prog/vmlinux.h
283
+ components/execd/pkg/ebpf/prog/vmlinux_*.h
284
+
272
285
  # gVisor runtime binaries (downloaded dynamically)
273
286
  kubernetes/test/kind/gvisor/runsc
274
287
  kubernetes/test/kind/gvisor/containerd-shim-runsc-v1
@@ -276,3 +289,4 @@ bin/
276
289
  obj/
277
290
 
278
291
  .qoder/
292
+ /.vs
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: opensandbox-server
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: FastAPI control plane for OpenSandbox that manages sandbox lifecycle on Docker (ready) and Kubernetes (planned) runtimes.
5
5
  Project-URL: Homepage, https://github.com/opensandbox-group/OpenSandbox
6
6
  Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
@@ -24,11 +24,16 @@ Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.10
25
25
  Requires-Dist: docker
26
26
  Requires-Dist: fastapi>=0.137.0
27
+ Requires-Dist: grpcio>=1.83.0
27
28
  Requires-Dist: httpx[socks]
28
29
  Requires-Dist: kubernetes
30
+ Requires-Dist: opentelemetry-api
31
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http
32
+ Requires-Dist: opentelemetry-sdk
33
+ Requires-Dist: protobuf>=7.35.1
29
34
  Requires-Dist: pydantic
30
- Requires-Dist: pydantic-settings
31
- Requires-Dist: python-multipart>=0.0.30
35
+ Requires-Dist: pydantic-settings>=2.14.2
36
+ Requires-Dist: python-multipart>=0.0.31
32
37
  Requires-Dist: pyyaml
33
38
  Requires-Dist: redis>=5
34
39
  Requires-Dist: starlette>=1.3.1
@@ -15,10 +15,9 @@
15
15
  """
16
16
  API routes for OpenSandbox DevOps diagnostics.
17
17
 
18
- Requests that include `scope` target the stable Diagnostics API. The open-source
19
- server has not implemented that API yet, so these requests return a uniform
20
- not-implemented error. Requests without `scope` preserve the deprecated DevOps
21
- plain-text behavior for legacy humans, agents, and CLI clients.
18
+ Requests that include ``scope`` target the stable Diagnostics API and return an
19
+ inline JSON descriptor. Requests without ``scope`` preserve the deprecated
20
+ DevOps plain-text behavior for legacy humans, agents, and CLI clients.
22
21
  """
23
22
 
24
23
  import logging
@@ -28,19 +27,29 @@ from fastapi import APIRouter, HTTPException, Query, status
28
27
  from fastapi.responses import JSONResponse, PlainTextResponse
29
28
 
30
29
  from opensandbox_server.api.lifecycle import sandbox_service
30
+ from opensandbox_server.services.diagnostics import DiagnosticResult
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
  router = APIRouter(tags=["DevOps"])
34
34
 
35
35
 
36
- def _diagnostics_not_implemented_response() -> JSONResponse:
37
- return JSONResponse(
38
- status_code=status.HTTP_501_NOT_IMPLEMENTED,
39
- content={
40
- "code": "DIAGNOSTICS_NOT_IMPLEMENTED",
41
- "message": "The stable Diagnostics API is not implemented by this OpenSandbox server.",
42
- },
43
- )
36
+ def _diagnostic_inline_response(
37
+ result: DiagnosticResult,
38
+ ) -> JSONResponse:
39
+ """Build a Diagnostics API descriptor for inline plain-text content."""
40
+ payload: dict[str, object] = {
41
+ "sandboxId": result.sandbox_id,
42
+ "kind": result.kind,
43
+ "scope": result.scope,
44
+ "delivery": "inline",
45
+ "contentType": "text/plain; charset=utf-8",
46
+ "content": result.content,
47
+ "contentLength": len(result.content.encode("utf-8")),
48
+ "truncated": result.truncated,
49
+ }
50
+ if result.warnings:
51
+ payload["warnings"] = list(result.warnings)
52
+ return JSONResponse(status_code=status.HTTP_200_OK, content=payload)
44
53
 
45
54
 
46
55
  def _deprecated_plain_text_response(content: str) -> PlainTextResponse:
@@ -56,13 +65,10 @@ def _deprecated_plain_text_response(content: str) -> PlainTextResponse:
56
65
  status_code=status.HTTP_200_OK,
57
66
  responses={
58
67
  200: {
59
- "description": "Deprecated plain-text logs when scope is omitted",
60
- "content": {"text/plain": {}},
61
- },
62
- 501: {
63
- "description": "Stable Diagnostics API is not implemented by this server",
64
- "content": {"application/json": {}},
68
+ "description": "Stable JSON descriptor, or deprecated text when scope is omitted",
69
+ "content": {"application/json": {}, "text/plain": {}},
65
70
  },
71
+ 400: {"description": "Unsupported diagnostics scope"},
66
72
  404: {"description": "Sandbox not found"},
67
73
  },
68
74
  )
@@ -84,11 +90,21 @@ def get_sandbox_logs(
84
90
  deprecated=True,
85
91
  description="Deprecated plain-text logs only. Only return logs newer than this duration (e.g. 10m, 1h).",
86
92
  ),
93
+ container: Optional[str] = Query(
94
+ None,
95
+ deprecated=True,
96
+ description=(
97
+ "Deprecated plain-text logs only. Container name to read logs from. "
98
+ "Defaults to the canonical user container (typically 'sandbox') when "
99
+ "the runtime supports multi-container pods."
100
+ ),
101
+ ),
87
102
  ) -> JSONResponse | PlainTextResponse:
88
103
  """Retrieve diagnostic logs for a sandbox."""
89
104
  if scope is not None:
90
- return _diagnostics_not_implemented_response()
91
- text = sandbox_service.get_sandbox_logs(sandbox_id, tail=tail, since=since)
105
+ result = sandbox_service.get_sandbox_log_diagnostics(sandbox_id, scope)
106
+ return _diagnostic_inline_response(result)
107
+ text = sandbox_service.get_sandbox_logs(sandbox_id, tail=tail, since=since, container=container)
92
108
  return _deprecated_plain_text_response(text)
93
109
 
94
110
 
@@ -114,13 +130,10 @@ def get_sandbox_inspect(sandbox_id: str) -> PlainTextResponse:
114
130
  status_code=status.HTTP_200_OK,
115
131
  responses={
116
132
  200: {
117
- "description": "Deprecated plain-text events when scope is omitted",
118
- "content": {"text/plain": {}},
119
- },
120
- 501: {
121
- "description": "Stable Diagnostics API is not implemented by this server",
122
- "content": {"application/json": {}},
133
+ "description": "Stable JSON descriptor, or deprecated text when scope is omitted",
134
+ "content": {"application/json": {}, "text/plain": {}},
123
135
  },
136
+ 400: {"description": "Unsupported diagnostics scope"},
124
137
  404: {"description": "Sandbox not found"},
125
138
  },
126
139
  )
@@ -140,7 +153,8 @@ def get_sandbox_events(
140
153
  ) -> JSONResponse | PlainTextResponse:
141
154
  """Retrieve diagnostic events for a sandbox."""
142
155
  if scope is not None:
143
- return _diagnostics_not_implemented_response()
156
+ result = sandbox_service.get_sandbox_event_diagnostics(sandbox_id, scope)
157
+ return _diagnostic_inline_response(result)
144
158
  text = sandbox_service.get_sandbox_events(sandbox_id, limit=limit)
145
159
  return _deprecated_plain_text_response(text)
146
160
 
@@ -45,7 +45,10 @@ from opensandbox_server.api.schema import (
45
45
  Snapshot,
46
46
  SnapshotFilter,
47
47
  )
48
- from opensandbox_server.services.constants import SandboxErrorCodes
48
+ from opensandbox_server.services.constants import (
49
+ OPEN_SANDBOX_INGRESS_HEADER,
50
+ SandboxErrorCodes,
51
+ )
49
52
  from opensandbox_server.services.factory import create_sandbox_service
50
53
  from opensandbox_server.services.snapshot_service import create_snapshot_service
51
54
 
@@ -71,6 +74,16 @@ snapshot_service = create_snapshot_service(sandbox_service)
71
74
  400: {"model": ErrorResponse, "description": "The request was invalid or malformed"},
72
75
  401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
73
76
  409: {"model": ErrorResponse, "description": "The operation conflicts with the current state"},
77
+ 429: {
78
+ "model": ErrorResponse,
79
+ "description": "Pool capacity remained unavailable before the acquisition timeout",
80
+ "headers": {
81
+ "Retry-After": {
82
+ "description": "Suggested delay in seconds before retrying",
83
+ "schema": {"type": "integer", "minimum": 1},
84
+ }
85
+ },
86
+ },
74
87
  500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
75
88
  },
76
89
  )
@@ -431,6 +444,7 @@ def create_snapshot(
431
444
  )
432
445
  def list_snapshots(
433
446
  sandbox_id: Optional[str] = Query(None, alias="sandboxId", description="Filter snapshots by source sandbox identifier"),
447
+ name: Optional[str] = Query(None, description="Filter snapshots by exact snapshot name"),
434
448
  state: Optional[List[str]] = Query(None, description="Filter by snapshot lifecycle state. Pass multiple times for OR logic."),
435
449
  page: int = Query(1, ge=1, description="Page number for pagination"),
436
450
  page_size: int = Query(20, ge=1, le=200, alias="pageSize", description="Number of items per page"),
@@ -440,7 +454,7 @@ def list_snapshots(
440
454
  List snapshots with optional filtering and pagination.
441
455
  """
442
456
  request = ListSnapshotsRequest(
443
- filter=SnapshotFilter(sandboxId=sandbox_id, state=state),
457
+ filter=SnapshotFilter(sandboxId=sandbox_id, name=name, state=state),
444
458
  pagination=PaginationRequest(page=page, pageSize=page_size),
445
459
  )
446
460
  return snapshot_service.list_snapshots(request)
@@ -564,12 +578,26 @@ def get_sandbox_endpoint(
564
578
  endpoint = sandbox_service.get_endpoint(sandbox_id, port, expires=expires)
565
579
 
566
580
  if use_server_proxy:
567
- # Prefer configured external address when available.
568
- base_url = str(request.base_url).rstrip("/")
581
+ # Prefer configured external address when available (it is the full
582
+ # external base and may already carry a path prefix).
569
583
  eip = (get_config().server.eip or "").strip().rstrip("/")
570
584
  if eip:
571
585
  base_url = eip
586
+ else:
587
+ # Reflect how the client reached this API: same host AND same mount
588
+ # prefix (the API is served both bare and under /v1). Dropping the
589
+ # prefix pointed proxied traffic at whatever backend owns the root
590
+ # path on shared hosts (breaking HTTP with redirects and WebSocket
591
+ # handshakes outright).
592
+ mount_prefix = request.url.path.split("/sandboxes/", 1)[0]
593
+ base_url = str(request.base_url).rstrip("/") + mount_prefix
572
594
  base_url = base_url.replace("https://", "").replace("http://", "")
573
595
  endpoint.endpoint = f"{base_url}/sandboxes/{sandbox_id}/proxy/{port}"
596
+ if endpoint.headers:
597
+ endpoint.headers = {
598
+ key: value
599
+ for key, value in endpoint.headers.items()
600
+ if key.lower() != OPEN_SANDBOX_INGRESS_HEADER.lower()
601
+ } or None
574
602
 
575
603
  return endpoint
@@ -0,0 +1,83 @@
1
+ # Copyright 2026 Alibaba Group Holding Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """SDK metrics ingestion endpoints."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ from typing import Optional
22
+
23
+ from fastapi import APIRouter, Header, Response, status
24
+
25
+ from opensandbox_server.api.schema import MetricsEvent
26
+ from opensandbox_server.integrations.otel import record_sandbox_create_duration
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ router = APIRouter(tags=["Metrics"])
31
+
32
+ # OpenSandbox-Python-SDK/0.1.14, OpenSandbox-Go-SDK/0.1.0, OpenSandbox-JS-SDK/0.1.10
33
+ _SDK_USER_AGENT_RE = re.compile(
34
+ r"OpenSandbox-([A-Za-z0-9]+)-SDK/([^\s]+)",
35
+ re.IGNORECASE,
36
+ )
37
+
38
+
39
+ def parse_sdk_user_agent(user_agent: Optional[str]) -> tuple[str, str]:
40
+ """Extract (language, version) from SDK User-Agent; fallback to unknown."""
41
+ if not user_agent:
42
+ return "unknown", "unknown"
43
+ match = _SDK_USER_AGENT_RE.search(user_agent)
44
+ if not match:
45
+ return "unknown", "unknown"
46
+ return match.group(1).lower(), match.group(2)
47
+
48
+
49
+ @router.post(
50
+ "/metrics/events",
51
+ status_code=status.HTTP_204_NO_CONTENT,
52
+ responses={
53
+ 400: {"description": "Bad request"},
54
+ 401: {"description": "Unauthorized"},
55
+ 500: {"description": "Internal server error"},
56
+ },
57
+ )
58
+ def report_metrics_event(
59
+ event: MetricsEvent,
60
+ x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
61
+ user_agent: Optional[str] = Header(None, alias="User-Agent"),
62
+ ) -> Response:
63
+ """Accept best-effort SDK metrics and record to OTEL (noop when disabled)."""
64
+ _ = x_request_id
65
+ if event.event_type == "sandbox.create":
66
+ sdk_language, sdk_version = parse_sdk_user_agent(user_agent)
67
+ record_sandbox_create_duration(
68
+ create_duration_ms=event.create_duration_ms,
69
+ sdk_language=sdk_language,
70
+ sdk_version=sdk_version,
71
+ success=event.success,
72
+ )
73
+ logger.debug(
74
+ "Accepted sandbox.create metrics event sandbox_id=%s image=%s "
75
+ "duration_ms=%s sdk=%s/%s success=%s",
76
+ event.sandbox_id,
77
+ event.image,
78
+ event.create_duration_ms,
79
+ sdk_language,
80
+ sdk_version,
81
+ event.success,
82
+ )
83
+ return Response(status_code=status.HTTP_204_NO_CONTENT)