mirror of
https://github.com/Colton-z/AstraBox.git
synced 2026-09-28 06:02:56 +08:00
Strengthen sandbox isolation and authentication, make all five engines work through the bundled installer, and preserve conversations across sandbox and service restarts. Add team login and single-container deployment, with upgrade instructions for replacing existing 0.1.0 sandboxes. Co-Authored-By: Claude <noreply@anthropic.com>
404 lines
21 KiB
TOML
404 lines
21 KiB
TOML
[build-system]
|
|
requires = ["hatchling==1.32.0"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[project]
|
|
name = "astrabox"
|
|
version = "0.1.1"
|
|
description = "AstraBox Community — an open, self-hosted agent runtime that streams a sandboxed coding agent to your frontend over the AI-SDK Data-Stream-Protocol."
|
|
readme = "README.md"
|
|
requires-python = ">=3.12"
|
|
license = { text = "Apache-2.0" }
|
|
authors = [{ name = "Colton Qi" }]
|
|
keywords = ["agent", "runtime", "sandbox", "claude", "mcp", "docker", "ai-sdk"]
|
|
classifiers = [
|
|
"Development Status :: 4 - Beta",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: Apache Software License",
|
|
"Operating System :: POSIX :: Linux",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
]
|
|
|
|
# Core — always installed.
|
|
dependencies = [
|
|
# web + async + http
|
|
"fastapi==0.141.1",
|
|
"uvicorn[standard]==0.52.4",
|
|
"httpx==0.28.1",
|
|
"anyio==4.15.0",
|
|
"websockets>=17.1",
|
|
# config + validation
|
|
"pydantic==2.13.5",
|
|
"pydantic-settings==2.15.0",
|
|
"pyyaml==6.0.3",
|
|
# agent loop (KEYSTONE, Anthropic official MIT package)
|
|
"claude-agent-sdk==0.2.152",
|
|
# default sandbox driver -> /var/run/docker.sock
|
|
# vault secret material at rest (AES-GCM in the local secret store)
|
|
# AWS's latest encryption SDK currently requires cryptography <47 through its
|
|
# material-provider runtime. This is the newest jointly installable release.
|
|
"cryptography==46.0.7",
|
|
# OpenSandbox lifecycle CLIENT: the sandbox handle/manager the open_sandbox
|
|
# backend drives, plus the request/response models the runtime speaks
|
|
# (execd commands, filesystem writes, network policy). Stay on the compatible
|
|
# 0.1 contract. Client-side pool coordination is OpenSandbox's maintained
|
|
# Redis-backed capability; the lifecycle SERVER is the separate
|
|
# `sandbox-server` extra below.
|
|
"opensandbox[pool-redis]==0.1.17.dev0",
|
|
# MCP (the Claude Agent SDK and AstraBox share this protocol implementation)
|
|
"mcp==2.1.1",
|
|
# persistence (DEFAULT = PostgreSQL; SQLite remains for compatibility tests)
|
|
"sqlalchemy==2.0.52",
|
|
"asyncpg==0.31.0",
|
|
"aiosqlite==0.22.1",
|
|
# Embedded durable schedule/run engine. DBOS owns cron dispatch, distributed
|
|
# idempotency, recovery, retries, and the workflow ledger; AstraBox keeps the
|
|
# public Deployment/Run vocabulary and exposes none of DBOS's control plane.
|
|
"dbos==2.31.0",
|
|
# observability seam — official Prometheus client (spec-correct exposition)
|
|
"prometheus-client==0.26.0",
|
|
# resilience + token decode + misc (source-real open deps)
|
|
"tenacity==9.1.4",
|
|
"pyjwt==2.13.0",
|
|
"python-dateutil==2.9.0.post0",
|
|
"dacite==1.9.2",
|
|
"markdown-it-py==4.2.0",
|
|
# Event models and message-element parser for the private channel-gateway
|
|
# boundary. Concrete products remain AstraBox channel providers; Koishi is
|
|
# not part of the runtime.
|
|
"satori-python-core==1.3.9.post1",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
# optional MongoDB collection backend behind the get_async_collection() seam
|
|
mongo = ["pymongo==4.17.0"]
|
|
# AWS KMS envelope encryption for stateless/multi-replica Credential Vaults.
|
|
# The server image installs this extra; local library installs do not carry a
|
|
# cloud SDK unless the operator selects it.
|
|
aws-kms = ["aws-encryption-sdk[MPL]==4.0.6"]
|
|
# OTEL_EXPORTER_OTLP_ENDPOINT-driven tracing (BYO Langfuse / any OTLP backend).
|
|
# OFF unless OTEL_EXPORTER_OTLP_ENDPOINT is set (see astrabox/observability/tracing.py).
|
|
otel = [
|
|
"opentelemetry-api==1.44.0",
|
|
"opentelemetry-sdk==1.44.0",
|
|
"opentelemetry-exporter-otlp==1.44.0",
|
|
# FastAPI request-span instrumentation — the tracing foundation's span source.
|
|
"opentelemetry-instrumentation-fastapi==0.65b0",
|
|
]
|
|
# The OpenSandbox lifecycle API server, run as AstraBox's own sandbox control
|
|
# plane by `python -m astrabox.deploy.sandbox_server` (the container image
|
|
# installs this extra, and the container image is the default way to run this).
|
|
# Still OPTIONAL rather than a core dependency: it drags in docker + kubernetes
|
|
# + redis, and a deployment that points ASTRABOX_SANDBOX_OPENAPI_BASE_URL at a
|
|
# lifecycle server it already runs needs none of that. `import astrabox` must
|
|
# never depend on an extra, so astrabox/deploy/sandbox_server.py imports it
|
|
# lazily and reports a missing install as an actionable error. This is also what
|
|
# keeps the CI unit lane installable with `.[dev]` alone.
|
|
#
|
|
# Exact-pinned because this module rebinds upstream module attributes
|
|
# (docker_service.allocate_port_bindings, metadata.DEFAULT_STORE_DIR and Docker
|
|
# service methods) that have no configuration field behind them. Every rebind
|
|
# verifies itself and fails loud, but one reproducible server release must be
|
|
# what development, CI, and images run.
|
|
sandbox-server = ["opensandbox-server==0.2.3"]
|
|
dev = [
|
|
"pytest==9.1.1",
|
|
"pytest-asyncio==1.4.0",
|
|
"pytest-timeout==2.4.0",
|
|
# Unit tests execute the live-runner contract in a fake environment. The
|
|
# runner proves its worker scheduler by importing pytest-xdist, so a fresh
|
|
# [dev] install must carry the same scheduler even though live tests remain
|
|
# deselected by default.
|
|
"pytest-xdist==3.8.0",
|
|
# `make build-dist` (the self-checking release artifact path) runs
|
|
# `python -m build`; keeping it in [dev] means the release flow works from
|
|
# any dev checkout without a side install.
|
|
"build==1.6.0",
|
|
# ruff + mypy are EXACT-pinned: this dev extra is the SINGLE source of truth for
|
|
# both CI (installs `.[dev]`, runs `python -m ruff`/`python -m mypy`) and local
|
|
# `make test`. The lint/type gate is calibrated to these versions, so a float
|
|
# could silently change lint/type output between a laptop and CI. Bump both here.
|
|
"ruff==0.16.5",
|
|
"mypy==2.3.1",
|
|
]
|
|
# Live end-to-end smoke (tests/e2e/): drives a real turn against a real Docker
|
|
# daemon + the operator LLM key. httpx is already a core dep; pytest is pulled in
|
|
# so `pip install -e .[e2e]` is a self-sufficient e2e install. These tests are
|
|
# marked `e2e` and DESELECTED by default (see [tool.pytest.ini_options]).
|
|
e2e = [
|
|
"pytest==9.1.1",
|
|
"pytest-asyncio==1.4.0",
|
|
# A live turn drives a real model; a hung turn must never wedge the suite, so
|
|
# the e2e tests run under a per-test timeout (--timeout, thread/signal method).
|
|
"pytest-timeout==2.4.0",
|
|
# Most live e2e are I/O-bound on a real model and a real cluster, so they run
|
|
# across workers (`-n`). Tests that restart a shared deployment service are
|
|
# marked separately and run only after the parallel phase has exited.
|
|
"pytest-xdist==3.8.0",
|
|
"httpx==0.28.1",
|
|
]
|
|
|
|
[project.urls]
|
|
Homepage = "https://github.com/Colton-z/AstraBox"
|
|
Repository = "https://github.com/Colton-z/AstraBox"
|
|
Issues = "https://github.com/Colton-z/AstraBox/issues"
|
|
Changelog = "https://github.com/Colton-z/AstraBox/blob/main/CHANGELOG.md"
|
|
|
|
[project.scripts]
|
|
astrabox = "astrabox.cli:main"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plugin registration seams (PEP 621 entry-points).
|
|
# Third-party plugins can register their own provider names at these SAME
|
|
# entry-point groups without touching this codebase. Unknown configured names
|
|
# fail loud in the seam loaders (astrabox/providers/__init__.py::load_provider,
|
|
# astrabox/persistence/repository/backend.py::_resolve_backend,
|
|
# astrabox/providers/identity.py::load_web_identity_resolver) — no silent fallback.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Built-in sandbox provider: open_sandbox (the OpenSandbox lifecycle API, reached
|
|
# either at a URL you point it at or at the lifecycle server the image starts
|
|
# alongside AstraBox). Entry-point names stay lowercase — load_provider lowercases
|
|
# the requested name but preserves entry-point names as declared, so a cased name
|
|
# would never match.
|
|
[project.entry-points."astrabox.providers.sandbox"]
|
|
open_sandbox = "astrabox.providers.open_sandbox.sandbox:OpenSandboxSandboxProvider"
|
|
|
|
[project.entry-points."astrabox.providers.storage"]
|
|
aws_efs = "astrabox.providers.storage.aws_efs:AwsEfsStorage"
|
|
mounted_volume = "astrabox.providers.storage.mounted_volume:MountedVolumeStorage"
|
|
|
|
[project.entry-points."astrabox.providers.engine"]
|
|
claude_code = "astrabox.core.service.orchestrator.engine.claude_code:ClaudeCodeEngineAdapter"
|
|
# The assistant engine drives the open-source Hermes TUI Gateway over stdio
|
|
# JSON-RPC. One process is attached to each conversation through OpenSandbox
|
|
# execd; the per-(user, assistant) profile and workspace remain long-lived.
|
|
assistant = "astrabox.core.service.orchestrator.engine.hermes:HermesEngineAdapter"
|
|
# The deepseek_harness engine drives the DeepSeek Harness web profile through
|
|
# the same /api gateway its own browser client uses: unary calls plus one
|
|
# downlink socket carrying events, approvals and questions.
|
|
deepseek_harness = "astrabox.core.service.orchestrator.engine.deepseek_harness:DeepSeekHarnessEngineAdapter"
|
|
# The pi engine drives earendil-works/pi through its documented headless
|
|
# surface, `pi --mode rpc`: JSON lines on stdin/stdout over the same execd
|
|
# pipe. Pi ships no permission system of its own — it recommends containment
|
|
# instead, which the sandbox already provides.
|
|
pi = "astrabox.core.service.orchestrator.engine.pi:PiEngineAdapter"
|
|
|
|
# The codex engine drives OpenAI's Codex CLI through `codex app-server`, the
|
|
# bidirectional JSON-RPC interface its own VS Code extension and desktop app
|
|
# run on. The image binds the vendor-supported unix listener and a forwarder
|
|
# publishes it, so the wire is a plain websocket.
|
|
codex = "astrabox.core.service.orchestrator.engine.codex:CodexEngineAdapter"
|
|
|
|
# Model endpoint provider: the LiteLLM gateway integration (multi-provider
|
|
# model connectivity is delegated to LiteLLM, never reimplemented in-tree; a
|
|
# custom gateway registers at this same group). There is deliberately no
|
|
# bundled `direct` passthrough — see the reasoning in astrabox/providers/model.py.
|
|
[project.entry-points."astrabox.providers.model"]
|
|
litellm = "astrabox.providers.model:LiteLLMModelEndpointProvider"
|
|
|
|
[project.entry-points."astrabox.providers.extensions"]
|
|
litellm = "astrabox.providers.litellm_extensions:LiteLLMExtensionProvider"
|
|
|
|
# Credential secret material at rest. Both built-ins keep ciphertext in the
|
|
# regular metadata store: `local` uses a deployment master key and `aws_kms`
|
|
# uses the AWS Encryption SDK. Another store registers at this same group.
|
|
[project.entry-points."astrabox.providers.secrets"]
|
|
local = "astrabox.providers.secret_store:LocalEncryptedSecretStore"
|
|
aws_kms = "astrabox.providers.secret_store_aws_kms:AwsKmsSecretStore"
|
|
|
|
# Channel providers (IM/messaging integrations over the channel spine — see
|
|
# astrabox/seams/channel.py). `generic_json` is the IM-agnostic reference. The
|
|
# gateway target imports the concrete provider catalog and registers Telegram,
|
|
# Discord, Feishu, and the other bundled platforms by their own scene names.
|
|
[project.entry-points."astrabox.providers.channel"]
|
|
generic_json = "astrabox.providers.channel_generic:GenericJsonChannelProvider"
|
|
gateway = "astrabox.providers.channel_satori:GatewayChannelProvider"
|
|
|
|
# Admission controllers (deployment quota / rate / concurrency policy — see
|
|
# astrabox/seams/admission.py). No built-in ships (the base admits everything);
|
|
# a downstream registers its controller here (self-registers on import via
|
|
# register_admission_controller) so the turn/session hot path never needs a fork.
|
|
|
|
# Default persistence backend: PostgreSQL (SQLite compatibility and Mongo are
|
|
# registered at the same group). Targets are plain MODULES (not classes) — no backend takes
|
|
# a per-instance db_url; both resolve the store through settings/env at the
|
|
# single get_async_collection() seam (see astrabox/persistence/repository/backend.py).
|
|
[project.entry-points."astrabox.providers.repository"]
|
|
postgresql = "astrabox.persistence.repository.postgresql"
|
|
sqlite = "astrabox.persistence.repository.sqlite"
|
|
mongo = "astrabox.persistence.repository.mongo"
|
|
|
|
# Community default identity: no API auth. One web identity seam covers the
|
|
# browser, JSON API, and public MCP transport.
|
|
[project.entry-points."astrabox.web.identity"]
|
|
local = "astrabox.providers.identity:LocalNoAuthWebIdentityResolver"
|
|
# Config-only SSO resolvers (loader instantiates the class; VerifiedJwt fails
|
|
# loud at construction when no key material is configured -> breaks boot, not
|
|
# requests). See astrabox/providers/identity_sso.py.
|
|
trusted_header = "astrabox.providers.identity_sso:TrustedHeaderWebIdentityResolver"
|
|
jwt = "astrabox.providers.identity_sso:VerifiedJwtWebIdentityResolver"
|
|
# Built-in OIDC login: AstraBox runs the authorization-code dance itself and
|
|
# authenticates by its own session cookie; any standard OIDC IdP works (the
|
|
# compose sso profile bundles Casdoor). See astrabox/providers/identity_oidc.py.
|
|
oidc = "astrabox.providers.identity_oidc:OidcSessionWebIdentityResolver"
|
|
|
|
[project.entry-points."astrabox.api.routers"]
|
|
litellm_extensions = "astrabox.providers.litellm_extensions:register_litellm_extension_routes"
|
|
|
|
# FLAT layout: the package lives at ./astrabox (there is no src/ directory), so the
|
|
# wheel/sdist must point at the flat path or a built distribution would ship no
|
|
# package and every entry-point would dangle post-install.
|
|
#
|
|
# The built console SPA ships INSIDE the package at astrabox/_frontend_dist —
|
|
# `make build-dist` copies frontend/dist there before `hatch build`, and
|
|
# api/app.py::_mount_frontend resolves that path second (after the
|
|
# ASTRABOX_FRONTEND_DIST override, before the in-repo frontend/dist). The dir
|
|
# is gitignored build output, so it must be listed under `artifacts` (which
|
|
# overrides the VCS-ignore filter) — plain `packages`/`include` would silently
|
|
# drop it and `pip install astrabox` would serve an API with no UI.
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["astrabox"]
|
|
artifacts = ["astrabox/_frontend_dist"]
|
|
|
|
[tool.hatch.build.targets.sdist]
|
|
include = ["astrabox", "README.md", "LICENSE", "NOTICE", "pyproject.toml"]
|
|
artifacts = ["astrabox/_frontend_dist"]
|
|
|
|
[tool.ruff]
|
|
target-version = "py312"
|
|
line-length = 100
|
|
# Lint the whole tree EXCEPT the generated/vendored dirs. Anchored, because a
|
|
# bare name matches every path segment: "e2e" also excluded `tests/e2e`, the
|
|
# live Python suite, so a syntax error there was found by paying for a live
|
|
# run instead of by the gate.
|
|
extend-exclude = [".venv", "/frontend", "/e2e"]
|
|
|
|
[tool.ruff.lint]
|
|
# Pin pyflakes and the pycodestyle error subsets used by this project so a Ruff
|
|
# upgrade cannot expand the lint surface implicitly. E7xx, E9xx, and the
|
|
# non-excluded pyflakes rules apply across source, scripts, and tests.
|
|
# E402 module-level import not at top — DELIBERATE: api/app.py & friends do lazy
|
|
# imports to keep create_app() import-time clean (the clean-boot invariant).
|
|
# F401 unused import \ excluded from the repository-wide gate; avoid
|
|
# F811 redefinition > these patterns in code changed by a contributor.
|
|
# F841 unused local var /
|
|
# F821 (undefined name) remains enforced as a correctness rule.
|
|
select = ["E4", "E7", "E9", "F"]
|
|
extend-ignore = ["E402", "F401", "F811", "F841"]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mypy — two-tier strictness.
|
|
#
|
|
# Tier 1 (this section) checks the full tree with selected error categories
|
|
# disabled. `ignore_missing_imports` covers optional dependencies such as Mongo,
|
|
# OpenTelemetry, and OpenSandbox when their extras are not installed.
|
|
#
|
|
# Tier 2 ([[tool.mypy.overrides]] below) re-enables full checking for packages
|
|
# that satisfy every configured category. Those packages must remain clean.
|
|
# ---------------------------------------------------------------------------
|
|
[tool.mypy]
|
|
python_version = "3.12"
|
|
ignore_missing_imports = true
|
|
follow_imports = "silent"
|
|
namespace_packages = true
|
|
explicit_package_bases = true
|
|
exclude = '(^|/)(\.venv|frontend|e2e|tests/e2e)/'
|
|
# name-defined is deliberately NOT disabled — it is the mypy half of the
|
|
# undefined-name gate (see the ruff F821 note above); the tree is clean under it.
|
|
disable_error_code = [
|
|
"arg-type", "assignment", "union-attr", "misc", "no-redef", "attr-defined",
|
|
"dict-item", "return-value", "var-annotated", "type-var",
|
|
"index", "call-arg", "override", "method-assign", "operator", "return",
|
|
"has-type", "valid-type",
|
|
]
|
|
|
|
# Strict-mode ratchet (Tier 2, see header comment above). Each module pattern
|
|
# below is probed clean with the disable list emptied entirely — run e.g.
|
|
# python -m mypy --config-file <copy of [tool.mypy] w/o disable_error_code> <path>
|
|
# before adding to this list, and run the real gate (`python -m mypy
|
|
# astrabox/`) after, since mypy's error-code merging is additive down the
|
|
# module hierarchy: a child pattern's disable_error_code can only ADD to what
|
|
# an ancestor pattern disabled, never remove it — restoring strictness for a
|
|
# module requires enable_error_code (used here), and a wildcard here must
|
|
# never be an ANCESTOR of a still-dirty module (it would sweep that module's
|
|
# codes back off too — see the repository/sqlite carve-out below, which is
|
|
# why persistence uses explicit sibling module names instead of a single
|
|
# top-level "astrabox.persistence.*" wildcard).
|
|
# A missing AstraBox module is a typo, not an uninstalled optional dependency.
|
|
# `ignore_missing_imports` above is for optional third-party packages; left
|
|
# global it also swallows a misspelled first-party import, which then passes
|
|
# both static gates and fails at collection on the machine that runs the tests.
|
|
[[tool.mypy.overrides]]
|
|
module = ["astrabox.*"]
|
|
ignore_missing_imports = false
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = [
|
|
# seams/, testing/, config/, web/ — fully clean, no dirty descendants, so a
|
|
# wildcard is safe and auto-covers new files added under these packages.
|
|
"astrabox.seams.*",
|
|
"astrabox.testing.*",
|
|
"astrabox.config.*",
|
|
"astrabox.web.*",
|
|
# deploy/ — the container entry point and sandbox-server launcher.
|
|
"astrabox.deploy.*",
|
|
# providers/ — one wildcard also covers new provider modules.
|
|
"astrabox.providers.*",
|
|
# Persistence modules that satisfy the full configuration. The compatibility
|
|
# shim and SQLite collection use the base configuration above.
|
|
"astrabox.persistence",
|
|
"astrabox.persistence.models.*",
|
|
"astrabox.persistence.migrations.*",
|
|
"astrabox.persistence.repository.mongo.*",
|
|
# The codex engine adapter. Named rather than a wildcard because its
|
|
# siblings under engine/ are not clean yet, and a wildcard here would be an
|
|
# ancestor of theirs. Strictness matters most on this file: `manager` is
|
|
# typed `EnginePlatform`, and with `attr-defined` restored a platform method
|
|
# that is renamed on the other side becomes a typecheck failure instead of
|
|
# an AttributeError on a live turn — which is exactly how
|
|
# `resolve_model_config` becoming `resolve_model_access` was found.
|
|
"astrabox.core.service.orchestrator.engine.codex",
|
|
"astrabox.core.service.orchestrator.engine.codex_client",
|
|
"astrabox.core.service.orchestrator.engine.codex_events",
|
|
"astrabox.core.service.orchestrator.engine.codex_link",
|
|
]
|
|
enable_error_code = [
|
|
"arg-type", "assignment", "union-attr", "misc", "no-redef", "attr-defined",
|
|
"dict-item", "return-value", "var-annotated", "type-var",
|
|
"index", "call-arg", "override", "method-assign", "operator", "return",
|
|
"has-type", "valid-type",
|
|
]
|
|
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
testpaths = ["tests"]
|
|
# The live-turn e2e (tests/e2e/) needs Docker + the operator LLM key, the
|
|
# database conformance bindings need reachable PostgreSQL/MongoDB services, and
|
|
# the opensandbox live lane needs a reachable opensandbox-server, so those
|
|
# integration markers are DESELECTED by default — a
|
|
# plain `pytest` run (unit CI) skips them. Run e2e explicitly with
|
|
# `pytest -m e2e`, database bindings with their named markers, and the
|
|
# opensandbox lane with `pytest -m opensandbox` (each overrides its own
|
|
# deselect on the CLI).
|
|
# --timeout=120 (pytest-timeout): a per-test ceiling so one hung async test can
|
|
# never wedge the unit lane. Set HERE (not in the CI invocation) so `make test`
|
|
# and CI share one source of truth and stay 1:1.
|
|
# --strict-markers: a misspelled isolation marker must fail collection instead
|
|
# of silently putting a deployment restart back into the parallel phase.
|
|
# --ignore-glob='**/._*': macOS AppleDouble metadata files appear alongside
|
|
# real files when the tree lives on a non-native volume (SMB); pytest must
|
|
# never try to collect them.
|
|
addopts = "-m 'not e2e and not mongo and not postgresql and not opensandbox' --timeout=120 --strict-markers --ignore-glob='**/._*'"
|
|
markers = [
|
|
"e2e: live end-to-end turn (needs Docker + your Anthropic-compatible endpoint/model key); deselected by default in unit CI.",
|
|
"assistant_live: live contract that requires an Assistant/Hermes environment rather than the canonical Agent environment.",
|
|
"backend_restart: stops or restarts a shared backend, bundled sandbox server, or database; the live runner executes these tests in its exclusive serial phase.",
|
|
"xdist_group(name): pytest-xdist scheduling group; live e2e invocations with workers reject any scheduler other than loadgroup.",
|
|
"mongo: needs a reachable mongod + the [mongo] extra (pip install -e '.[mongo]'); deselected by default in unit CI.",
|
|
"postgresql: needs a reachable PostgreSQL service; deselected by default in unit CI.",
|
|
"opensandbox: needs a reachable opensandbox-server (ASTRABOX_SANDBOX_OPENAPI_BASE_URL); deselected by default in unit CI.",
|
|
]
|