pyproject.toml
Rama main · réplica de lectura del repositorio original.
[build-system]
requires = ["hatchling==1.33.0"]
build-backend = "hatchling.build"
[project]
name = "astrabox"
version = "0.1.14"
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.162.0",
# Keep the verified HTTP stack stable across image rebuilds.
"starlette==1.10.0",
"multidict==7.2.0",
"uvicorn[standard]==0.54.0",
"httpx==0.28.1",
"anyio==4.15.1",
"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.163",
# 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",
# Official A2A HTTP transport; tasks project the existing Session/turn ledger.
"a2a-sdk[http-server]==1.2.2",
# persistence: PostgreSQL through SQLAlchemy, schema migrated by Alembic
"sqlalchemy==2.1.4",
"asyncpg==0.31.0",
"alembic==1.20.0",
# 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.2.1",
"pyjwt==2.15.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.11",
"loguru==0.7.3",
]
[project.optional-dependencies]
# 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.45.1",
"opentelemetry-sdk==1.45.1",
"opentelemetry-exporter-otlp==1.45.1",
# FastAPI request-span instrumentation — the tracing foundation's span source.
"opentelemetry-instrumentation-fastapi==0.66b1",
]
# 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.2.0",
"pytest-asyncio==1.4.1",
"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.17.3",
"mypy==2.4.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.2.0",
"pytest-asyncio==1.4.1",
# 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/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.skills"]
git = "astrabox.providers.skill_git:GitSkillContentProvider"
[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"
# Which AstraBox account a messaging participant runs as when a binding runs
# as linked accounts (see astrabox/seams/channel_accounts.py). `links` reads
# the per-binding links platform administrators maintain; a directory-backed
# resolver registers here and is selected by ASTRABOX_CHANNEL_ACCOUNT_RESOLVER.
[project.entry-points."astrabox.providers.channel_accounts"]
links = "astrabox.providers.channel_account_links:LinkTableChannelAccountResolver"
# Relational engines and physical database capabilities; shared repositories
# retain the platform's atomic storage recipes.
[project.entry-points."astrabox.providers.persistence"]
postgres = "astrabox.providers.relational_postgres:PostgresPersistenceProvider"
# Scheduler providers (cron dispatch, Run ledger, retries and recovery for
# scheduled Deployments — see astrabox/seams/scheduler.py). `dbos` keeps its
# state in the repository's SQL database; ASTRABOX_SCHEDULER_PROVIDER selects one.
[project.entry-points."astrabox.providers.scheduler"]
dbos = "astrabox.providers.scheduler_dbos:DbosSchedulerProvider"
# 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.
# Environment authorization is independent of authentication and resource ACLs.
[project.entry-points."astrabox.environment.entry"]
members = "astrabox.providers.environment_entry:MembersEnvironmentEntryPolicy"
# 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.
# attr-defined and call-arg stay on: with them off, a call to a member that a
# refactor removed passes this gate and fails on first use.
disable_error_code = [
"arg-type", "assignment", "union-attr", "misc", "no-redef",
"dict-item", "return-value", "var-annotated", "type-var",
"index", "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).
# 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
# The Kubernetes client arrives only with the sandbox-server extra, which the CI
# unit lane does not install, so there mypy has always read it as untyped. The
# release `make install` resolves (37.0.1) ships type information, and following
# it makes the typecheck result depend on which extras are installed: `make
# install` adds the extra and `make typecheck` then failed where CI passed. Read
# it as untyped everywhere.
[[tool.mypy.overrides]]
module = ["kubernetes", "kubernetes.*"]
follow_imports = "skip"
[[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.*",
"astrabox.persistence",
"astrabox.persistence.models.*",
# 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 a running deployment and the operator LLM
# key, so the `e2e` marker is DESELECTED by default — a plain `pytest` run (unit
# CI) skips it. Run it explicitly with `pytest -m e2e`, which overrides the
# deselect on the CLI. Unit tests that touch the store run on PostgreSQL through
# the `database` fixture: ASTRABOX_TEST_POSTGRES_URL names a role that may
# create databases.
# --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' --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.",
"deployment_shape: live contract that needs a deployment built for it (a fixture provider or image); the live lanes exclude it and it runs against that deployment explicitly.",
"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.",
]