opensandbox-server 0.2.2__tar.gz → 1.1.0__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 (145) hide show
  1. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/.gitignore +17 -0
  2. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/PKG-INFO +5 -2
  3. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/__init__.py +1 -1
  4. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/__init__.py +1 -1
  5. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/devops.py +37 -36
  6. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/lifecycle.py +42 -17
  7. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/metrics.py +4 -9
  8. opensandbox_server-1.1.0/opensandbox_server/api/network_policy.py +60 -0
  9. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/pool.py +1 -5
  10. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/proxy.py +238 -66
  11. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/api/schema.py +274 -2
  12. opensandbox_server-1.1.0/opensandbox_server/api/templates.py +199 -0
  13. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/cli.py +50 -1
  14. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/config.py +365 -82
  15. opensandbox_server-1.1.0/opensandbox_server/constants.py +18 -0
  16. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/__init__.py +1 -1
  17. opensandbox_server-1.1.0/opensandbox_server/examples/e2e.batchsandbox-template.yaml +33 -0
  18. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/example.config.k8s.toml +31 -10
  19. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/example.config.k8s.zh.toml +29 -10
  20. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/example.config.toml +28 -3
  21. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/example.config.zh.toml +25 -3
  22. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/extensions/__init__.py +1 -1
  23. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/extensions/codec.py +1 -1
  24. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/extensions/keys.py +1 -3
  25. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/extensions/validation.py +1 -1
  26. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/__init__.py +1 -1
  27. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/otel/__init__.py +4 -2
  28. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/otel/metrics.py +78 -10
  29. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/__init__.py +1 -1
  30. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/constants.py +1 -1
  31. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/consumer.py +1 -1
  32. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/controller.py +1 -1
  33. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/intent.py +1 -1
  34. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/logutil.py +1 -1
  35. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/proxy_renew.py +1 -1
  36. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/redis_client.py +1 -1
  37. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/integrations/renew_intent/runner.py +1 -1
  38. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/logging_config.py +1 -3
  39. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/main.py +88 -14
  40. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/middleware/__init__.py +1 -1
  41. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/middleware/auth.py +3 -3
  42. opensandbox_server-1.1.0/opensandbox_server/middleware/date_header.py +43 -0
  43. opensandbox_server-1.1.0/opensandbox_server/middleware/http_metrics.py +86 -0
  44. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/middleware/request_id.py +1 -1
  45. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/repositories/__init__.py +1 -1
  46. opensandbox_server-1.1.0/opensandbox_server/repositories/snapshots/__init__.py +57 -0
  47. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/repositories/snapshots/factory.py +32 -1
  48. opensandbox_server-1.1.0/opensandbox_server/repositories/snapshots/migrate.py +255 -0
  49. opensandbox_server-1.1.0/opensandbox_server/repositories/snapshots/postgresql.py +338 -0
  50. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/repositories/snapshots/sqlite.py +13 -20
  51. {opensandbox_server-0.2.2/opensandbox_server/repositories/snapshots → opensandbox_server-1.1.0/opensandbox_server/repositories/templates}/__init__.py +12 -6
  52. opensandbox_server-1.1.0/opensandbox_server/repositories/templates/factory.py +96 -0
  53. opensandbox_server-1.1.0/opensandbox_server/repositories/templates/postgresql.py +280 -0
  54. opensandbox_server-1.1.0/opensandbox_server/repositories/templates/sqlite.py +266 -0
  55. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/__init__.py +1 -1
  56. opensandbox_server-1.1.0/opensandbox_server/services/composite_service.py +154 -0
  57. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/constants.py +37 -2
  58. opensandbox_server-1.1.0/opensandbox_server/services/diagnostics.py +85 -0
  59. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/__init__.py +1 -1
  60. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/container_ops.py +49 -33
  61. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/docker_diagnostics.py +93 -9
  62. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/docker_service.py +277 -151
  63. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/metadata.py +3 -16
  64. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/networking.py +122 -55
  65. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/ossfs_mixin.py +11 -47
  66. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/port_allocator.py +65 -11
  67. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/runtime.py +126 -12
  68. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/snapshot_runtime.py +31 -17
  69. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/volumes.py +9 -31
  70. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/docker/windows_profile.py +3 -3
  71. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/endpoint_auth.py +1 -1
  72. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/extension_service.py +1 -1
  73. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/factory.py +12 -7
  74. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/__init__.py +17 -0
  75. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/cr_mapping.py +82 -0
  76. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/cr_reader.py +100 -0
  77. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/create_mapping.py +314 -0
  78. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/endpoint.py +91 -0
  79. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/fastpath_client.py +443 -0
  80. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/network_policy.py +65 -0
  81. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/service.py +694 -0
  82. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/snapshot_runtime.py +350 -0
  83. opensandbox_server-1.1.0/opensandbox_server/services/fast_sandbox/status_mapping.py +97 -0
  84. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/helpers.py +21 -9
  85. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/__init__.py +1 -1
  86. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/agent_sandbox_provider.py +97 -42
  87. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/agent_sandbox_template.py +1 -1
  88. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/batchsandbox_provider.py +176 -82
  89. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/batchsandbox_template.py +1 -1
  90. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/client.py +115 -31
  91. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/create_helpers.py +32 -19
  92. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/egress_helper.py +54 -25
  93. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/endpoint_resolver.py +1 -1
  94. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/error_helpers.py +31 -2
  95. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/image_pull_secret_helper.py +25 -2
  96. opensandbox_server-1.1.0/opensandbox_server/services/k8s/informer.py +307 -0
  97. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/k8s_diagnostics.py +150 -15
  98. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/kubernetes_service.py +132 -141
  99. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/label_selector.py +1 -1
  100. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/list_helpers.py +1 -1
  101. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/pool_service.py +1 -2
  102. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/provider_common.py +10 -2
  103. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/provider_factory.py +1 -1
  104. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/rate_limiter.py +2 -7
  105. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/security_context.py +1 -1
  106. opensandbox_server-1.1.0/opensandbox_server/services/k8s/snapshot_runtime.py +688 -0
  107. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/status_helpers.py +28 -2
  108. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/template_manager.py +1 -1
  109. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/volume_helper.py +12 -26
  110. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/windows_profile.py +5 -5
  111. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/workload_access.py +1 -1
  112. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/workload_mapper.py +123 -2
  113. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/k8s/workload_provider.py +29 -13
  114. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/runtime_resolver.py +13 -16
  115. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/sandbox_service.py +45 -2
  116. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/signing.py +1 -1
  117. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/snapshot_models.py +16 -5
  118. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/snapshot_repository.py +6 -1
  119. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/snapshot_restore.py +23 -14
  120. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/snapshot_runtime.py +67 -5
  121. opensandbox_server-1.1.0/opensandbox_server/services/snapshot_runtime_factory.py +196 -0
  122. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/snapshot_service.py +307 -48
  123. opensandbox_server-1.1.0/opensandbox_server/services/templates/__init__.py +13 -0
  124. opensandbox_server-1.1.0/opensandbox_server/services/templates/template_models.py +84 -0
  125. opensandbox_server-1.1.0/opensandbox_server/services/templates/template_service.py +533 -0
  126. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/validators.py +6 -79
  127. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/services/windows_common.py +1 -1
  128. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/startup_guard.py +3 -4
  129. opensandbox_server-1.1.0/opensandbox_server/tenants/__init__.py +156 -0
  130. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/tenants/context.py +1 -1
  131. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/tenants/file_provider.py +11 -4
  132. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/tenants/http_provider.py +12 -6
  133. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/tenants/models.py +1 -1
  134. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/tenants/provider.py +9 -1
  135. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/pyproject.toml +19 -4
  136. opensandbox_server-0.2.2/opensandbox_server/examples/e2e.batchsandbox-template.yaml +0 -17
  137. opensandbox_server-0.2.2/opensandbox_server/services/k8s/informer.py +0 -242
  138. opensandbox_server-0.2.2/opensandbox_server/services/k8s/snapshot_runtime.py +0 -326
  139. opensandbox_server-0.2.2/opensandbox_server/services/snapshot_runtime_factory.py +0 -63
  140. opensandbox_server-0.2.2/opensandbox_server/tenants/__init__.py +0 -65
  141. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/LICENSE +0 -0
  142. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/README.md +0 -0
  143. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/.gitkeep +0 -0
  144. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/examples/example.batchsandbox-template.yaml +0 -0
  145. {opensandbox_server-0.2.2 → opensandbox_server-1.1.0}/opensandbox_server/py.typed +0 -0
@@ -269,6 +269,19 @@ nbdist/
269
269
  generated/
270
270
  **/generated/**
271
271
 
272
+ # Fast Sandbox 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/fast_sandbox/generated/
276
+ !server/opensandbox_server/services/fast_sandbox/generated/**
277
+ server/opensandbox_server/services/fast_sandbox/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
@@ -277,3 +290,7 @@ obj/
277
290
 
278
291
  .qoder/
279
292
  /.vs
293
+
294
+ # fast-sandbox pinned source checkout (materialized by
295
+ # manifests/release/build-fast-sandbox.sh from manifests/third-party/fast-sandbox.commit)
296
+ .fast-sandbox/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: opensandbox-server
3
- Version: 0.2.2
3
+ Version: 1.1.0
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,14 @@ 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
29
30
  Requires-Dist: opentelemetry-api
30
31
  Requires-Dist: opentelemetry-exporter-otlp-proto-http
31
32
  Requires-Dist: opentelemetry-sdk
33
+ Requires-Dist: protobuf>=7.35.1
34
+ Requires-Dist: psycopg[binary,pool]<4,>=3.2
32
35
  Requires-Dist: pydantic
33
36
  Requires-Dist: pydantic-settings>=2.14.2
34
37
  Requires-Dist: python-multipart>=0.0.31
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Alibaba Group Holding Ltd.
1
+ # Copyright 2025 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Alibaba Group Holding Ltd.
1
+ # Copyright 2025 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -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
  )
@@ -96,10 +102,9 @@ def get_sandbox_logs(
96
102
  ) -> JSONResponse | PlainTextResponse:
97
103
  """Retrieve diagnostic logs for a sandbox."""
98
104
  if scope is not None:
99
- return _diagnostics_not_implemented_response()
100
- text = sandbox_service.get_sandbox_logs(
101
- sandbox_id, tail=tail, since=since, container=container
102
- )
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)
103
108
  return _deprecated_plain_text_response(text)
104
109
 
105
110
 
@@ -125,13 +130,10 @@ def get_sandbox_inspect(sandbox_id: str) -> PlainTextResponse:
125
130
  status_code=status.HTTP_200_OK,
126
131
  responses={
127
132
  200: {
128
- "description": "Deprecated plain-text events when scope is omitted",
129
- "content": {"text/plain": {}},
130
- },
131
- 501: {
132
- "description": "Stable Diagnostics API is not implemented by this server",
133
- "content": {"application/json": {}},
133
+ "description": "Stable JSON descriptor, or deprecated text when scope is omitted",
134
+ "content": {"application/json": {}, "text/plain": {}},
134
135
  },
136
+ 400: {"description": "Unsupported diagnostics scope"},
135
137
  404: {"description": "Sandbox not found"},
136
138
  },
137
139
  )
@@ -151,7 +153,8 @@ def get_sandbox_events(
151
153
  ) -> JSONResponse | PlainTextResponse:
152
154
  """Retrieve diagnostic events for a sandbox."""
153
155
  if scope is not None:
154
- return _diagnostics_not_implemented_response()
156
+ result = sandbox_service.get_sandbox_event_diagnostics(sandbox_id, scope)
157
+ return _diagnostic_inline_response(result)
155
158
  text = sandbox_service.get_sandbox_events(sandbox_id, limit=limit)
156
159
  return _deprecated_plain_text_response(text)
157
160
 
@@ -189,10 +192,9 @@ def get_sandbox_diagnostics_summary(
189
192
  except HTTPException:
190
193
  raise
191
194
  except Exception:
192
- logger.exception("Failed to collect sandbox inspect diagnostics for %s", sandbox_id)
195
+ logger.exception(f"Failed to collect sandbox inspect diagnostics for {sandbox_id}")
193
196
  sections.append("[error] Failed to collect inspect diagnostics.")
194
197
 
195
- # Events
196
198
  sections.append("")
197
199
  sections.append("-" * 40)
198
200
  sections.append("EVENTS")
@@ -202,20 +204,19 @@ def get_sandbox_diagnostics_summary(
202
204
  except HTTPException:
203
205
  raise
204
206
  except Exception:
205
- logger.exception("Failed to collect sandbox event diagnostics for %s", sandbox_id)
207
+ logger.exception(f"Failed to collect sandbox event diagnostics for {sandbox_id}")
206
208
  sections.append("[error] Failed to collect event diagnostics.")
207
209
 
208
- # Logs
209
210
  sections.append("")
210
211
  sections.append("-" * 40)
211
- sections.append("LOGS (last {} lines)".format(tail))
212
+ sections.append(f"LOGS (last {tail} lines)")
212
213
  sections.append("-" * 40)
213
214
  try:
214
215
  sections.append(sandbox_service.get_sandbox_logs(sandbox_id, tail=tail))
215
216
  except HTTPException:
216
217
  raise
217
218
  except Exception:
218
- logger.exception("Failed to collect sandbox log diagnostics for %s", sandbox_id)
219
+ logger.exception(f"Failed to collect sandbox log diagnostics for {sandbox_id}")
219
220
  sections.append("[error] Failed to collect log diagnostics.")
220
221
 
221
222
  return PlainTextResponse(content="\n".join(sections) + "\n")
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Alibaba Group Holding Ltd.
1
+ # Copyright 2025 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -45,16 +45,22 @@ 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
+ OPEN_SANDBOX_ORIGIN_HEADER,
51
+ SANDBOX_ORIGIN_TEMPLATE,
52
+ SandboxErrorCodes,
53
+ )
49
54
  from opensandbox_server.services.factory import create_sandbox_service
55
+ from opensandbox_server.services.snapshot_restore import resolve_sandbox_image_from_request
50
56
  from opensandbox_server.services.snapshot_service import create_snapshot_service
51
57
 
52
- # Initialize router
53
58
  router = APIRouter(tags=["Sandboxes"])
54
59
 
55
- # Initialize service based on configuration from config.toml (defaults to docker)
56
60
  sandbox_service = create_sandbox_service()
57
61
  snapshot_service = create_snapshot_service(sandbox_service)
62
+ # React to snapshot status changes so rows converge without waiting on reads
63
+ snapshot_service.start_background_sync()
58
64
 
59
65
 
60
66
  # ============================================================================
@@ -67,10 +73,21 @@ snapshot_service = create_snapshot_service(sandbox_service)
67
73
  response_model_exclude_none=True,
68
74
  status_code=status.HTTP_202_ACCEPTED,
69
75
  responses={
70
- 202: {"description": "Sandbox creation accepted for asynchronous provisioning"},
76
+ 202: {"description": "Sandbox created and provisioned successfully"},
71
77
  400: {"model": ErrorResponse, "description": "The request was invalid or malformed"},
72
78
  401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
79
+ 403: {"model": ErrorResponse, "description": "Namespace ResourceQuota exhausted — the sandbox was not admitted (KUBERNETES::QUOTA_EXCEEDED)"},
73
80
  409: {"model": ErrorResponse, "description": "The operation conflicts with the current state"},
81
+ 429: {
82
+ "model": ErrorResponse,
83
+ "description": "Pool capacity remained unavailable before the acquisition timeout",
84
+ "headers": {
85
+ "Retry-After": {
86
+ "description": "Suggested delay in seconds before retrying",
87
+ "schema": {"type": "integer", "minimum": 1},
88
+ }
89
+ },
90
+ },
74
91
  500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
75
92
  },
76
93
  )
@@ -96,10 +113,13 @@ async def create_sandbox(
96
113
  HTTPException: If sandbox creation scheduling fails
97
114
  """
98
115
  validate_extensions(request.extensions)
116
+ # Resolve snapshotId before backend routing: the owning backend recorded
117
+ # on the snapshot row selects the fsb vs pod backend for restores.
118
+ if not (request.template_id or "").strip():
119
+ request = await resolve_sandbox_image_from_request(request)
99
120
  return await sandbox_service.create_sandbox(request)
100
121
 
101
122
 
102
- # Search endpoint
103
123
  @router.get(
104
124
  "/sandboxes",
105
125
  response_model=ListSandboxesResponse,
@@ -134,12 +154,10 @@ def list_sandboxes(
134
154
  Returns:
135
155
  ListSandboxesResponse: Paginated list of sandboxes
136
156
  """
137
- # Parse metadata query string into dictionary
138
157
  metadata_dict = {}
139
158
  if metadata:
140
159
  from urllib.parse import parse_qsl
141
160
  try:
142
- # Parse query string format: key=value&key2=value2
143
161
  # strict_parsing=True rejects malformed segments like "a=1&broken"
144
162
  parsed = parse_qsl(metadata, keep_blank_values=True, strict_parsing=True)
145
163
  metadata_dict = dict(parsed)
@@ -150,7 +168,6 @@ def list_sandboxes(
150
168
  detail={"code": "INVALID_METADATA_FORMAT", "message": f"Invalid metadata format: {str(e)}"}
151
169
  )
152
170
 
153
- # Construct request object
154
171
  request = ListSandboxesRequest(
155
172
  filter=SandboxFilter(state=state, metadata=metadata_dict if metadata_dict else None),
156
173
  pagination=PaginationRequest(page=page, pageSize=page_size)
@@ -158,9 +175,8 @@ def list_sandboxes(
158
175
 
159
176
  import logging
160
177
  logger = logging.getLogger(__name__)
161
- logger.info("ListSandboxes: %s", request.filter)
178
+ logger.info(f"ListSandboxes: {request.filter}")
162
179
 
163
- # Delegate to the service layer for filtering and pagination
164
180
  return sandbox_service.list_sandboxes(request)
165
181
 
166
182
 
@@ -196,7 +212,6 @@ def get_sandbox(
196
212
  Raises:
197
213
  HTTPException: If sandbox not found or access denied
198
214
  """
199
- # Delegate to the service layer for sandbox lookup
200
215
  return sandbox_service.get_sandbox(sandbox_id)
201
216
 
202
217
 
@@ -258,7 +273,6 @@ def delete_sandbox(
258
273
  Raises:
259
274
  HTTPException: If sandbox not found or deletion fails
260
275
  """
261
- # Delegate to the service layer for deletion
262
276
  sandbox_service.delete_sandbox(sandbox_id)
263
277
  return Response(status_code=status.HTTP_204_NO_CONTENT)
264
278
 
@@ -299,7 +313,6 @@ def pause_sandbox(
299
313
  Raises:
300
314
  HTTPException: If sandbox not found or cannot be paused
301
315
  """
302
- # Delegate to the service layer for pause orchestration
303
316
  sandbox_service.pause_sandbox(sandbox_id)
304
317
  return Response(status_code=status.HTTP_202_ACCEPTED)
305
318
 
@@ -336,7 +349,6 @@ def resume_sandbox(
336
349
  Raises:
337
350
  HTTPException: If sandbox not found or cannot be resumed
338
351
  """
339
- # Delegate to the service layer for resume orchestration
340
352
  sandbox_service.resume_sandbox(sandbox_id)
341
353
  return Response(status_code=status.HTTP_202_ACCEPTED)
342
354
 
@@ -377,7 +389,6 @@ def renew_sandbox_expiration(
377
389
  Raises:
378
390
  HTTPException: If sandbox not found or renewal fails
379
391
  """
380
- # Delegate to the service layer for expiration updates
381
392
  return sandbox_service.renew_expiration(sandbox_id, request)
382
393
 
383
394
 
@@ -520,6 +531,7 @@ def get_sandbox_endpoint(
520
531
  use_server_proxy: bool = Query(False, description="Whether to return a server-proxied URL"),
521
532
  expires: Optional[int] = Query(None, description="Request a signed route token with this Unix epoch second expiration. Requires ingress gateway with secure_access configured."),
522
533
  x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"),
534
+ response: Response = None, # type: ignore[assignment]
523
535
  ) -> Endpoint:
524
536
  """
525
537
  Get sandbox access endpoint.
@@ -561,7 +573,6 @@ def get_sandbox_endpoint(
561
573
  },
562
574
  )
563
575
 
564
- # Delegate to the service layer for endpoint resolution
565
576
  endpoint = sandbox_service.get_endpoint(sandbox_id, port, expires=expires)
566
577
 
567
578
  if use_server_proxy:
@@ -580,5 +591,19 @@ def get_sandbox_endpoint(
580
591
  base_url = str(request.base_url).rstrip("/") + mount_prefix
581
592
  base_url = base_url.replace("https://", "").replace("http://", "")
582
593
  endpoint.endpoint = f"{base_url}/sandboxes/{sandbox_id}/proxy/{port}"
594
+ if endpoint.headers:
595
+ endpoint.headers = {
596
+ key: value
597
+ for key, value in endpoint.headers.items()
598
+ if key.lower() != OPEN_SANDBOX_INGRESS_HEADER.lower()
599
+ } or None
600
+
601
+ # Tell clients the origin of this sandbox. fsb sandboxes (id prefix,
602
+ # mirroring CompositeSandboxService._backend routing) run on golden-image
603
+ # templates and have no sandbox-side egress sidecar; the value space may
604
+ # grow (image/snapshot) as server-side origin tracking matures. Clients
605
+ # treat a missing/unknown value as "not template".
606
+ if response is not None and sandbox_id.startswith("fsb-"):
607
+ response.headers[OPEN_SANDBOX_ORIGIN_HEADER] = SANDBOX_ORIGIN_TEMPLATE
583
608
 
584
609
  return endpoint
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -71,13 +71,8 @@ def report_metrics_event(
71
71
  success=event.success,
72
72
  )
73
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,
74
+ f"Accepted sandbox.create metrics event sandbox_id={event.sandbox_id} "
75
+ f"image={event.image} duration_ms={event.create_duration_ms} "
76
+ f"sdk={sdk_language}/{sdk_version} success={event.success}"
82
77
  )
83
78
  return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,60 @@
1
+ # Copyright 2026 The OpenSandbox Authors
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
5
+
6
+ """Read/replace/patch policy intent without exposing the Fastlet's loopback handler."""
7
+
8
+ import asyncio
9
+
10
+ from fastapi import APIRouter, HTTPException, Request
11
+
12
+ from opensandbox_server.api import lifecycle
13
+ from opensandbox_server.api.proxy import _proxy_http_request
14
+ from opensandbox_server.api.schema import NetworkPolicy, NetworkRule
15
+ from opensandbox_server.services.composite_service import CompositeSandboxService
16
+ from opensandbox_server.services.fast_sandbox import FastSandboxService
17
+
18
+ router = APIRouter(tags=["Sandboxes"])
19
+
20
+
21
+ def _fsb_service():
22
+ service = lifecycle.sandbox_service
23
+ if not isinstance(service, (FastSandboxService, CompositeSandboxService)):
24
+ raise HTTPException(404, detail="Fsb sandbox not found.")
25
+ return service
26
+
27
+
28
+ @router.get("/sandboxes/{sandbox_id}/networkpolicy")
29
+ async def get_network_policy(request: Request, sandbox_id: str):
30
+ if sandbox_id.startswith("fsb-"):
31
+ return await asyncio.to_thread(_fsb_service().get_network_policy, sandbox_id)
32
+ return await _proxy_http_request(request, sandbox_id, 18080, "policy", internal=True)
33
+
34
+
35
+ @router.put("/sandboxes/{sandbox_id}/networkpolicy")
36
+ async def replace_network_policy(request: Request, sandbox_id: str, policy: NetworkPolicy):
37
+ if sandbox_id.startswith("fsb-"):
38
+ return await asyncio.to_thread(_fsb_service().replace_network_policy, sandbox_id, policy)
39
+ return await _proxy_http_request(request, sandbox_id, 18080, "policy", internal=True)
40
+
41
+
42
+ @router.patch("/sandboxes/{sandbox_id}/networkpolicy")
43
+ async def patch_network_policy(request: Request, sandbox_id: str, rules: list[NetworkRule]):
44
+ """Merge rules into the persisted policy (sidecar PATCH semantics).
45
+
46
+ Incoming rules replace existing rules with the same target in place;
47
+ the first rule per target in the payload wins; the current
48
+ defaultAction is preserved.
49
+ """
50
+ if sandbox_id.startswith("fsb-"):
51
+ return await asyncio.to_thread(_fsb_service().patch_network_policy, sandbox_id, rules)
52
+ return await _proxy_http_request(request, sandbox_id, 18080, "policy", internal=True)
53
+
54
+
55
+ @router.delete("/sandboxes/{sandbox_id}/networkpolicy")
56
+ async def delete_network_policy(request: Request, sandbox_id: str, targets: list[str]):
57
+ """Remove rules by target (idempotent); the current defaultAction is preserved."""
58
+ if sandbox_id.startswith("fsb-"):
59
+ return await asyncio.to_thread(_fsb_service().delete_network_policy_rules, sandbox_id, targets)
60
+ return await _proxy_http_request(request, sandbox_id, 18080, "policy", internal=True)
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Alibaba Group Holding Ltd.
1
+ # Copyright 2025 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -70,10 +70,6 @@ def _get_pool_service():
70
70
  return PoolService(k8s_client, namespace=config.kubernetes.namespace)
71
71
 
72
72
 
73
- # ============================================================================
74
- # Pool CRUD Endpoints
75
- # ============================================================================
76
-
77
73
  @router.post(
78
74
  "/pools",
79
75
  response_model=PoolResponse,