Skip to content

Commit fc81ebd

Browse files
authored
Update config.py
1 parent 64231d5 commit fc81ebd

1 file changed

Lines changed: 122 additions & 3 deletions

File tree

‎app/infra/config.py‎

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from functools import lru_cache
1111
from pathlib import Path
1212

13+
from pydantic import Field
1314
from pydantic_settings import BaseSettings, SettingsConfigDict
1415

1516

@@ -42,12 +43,93 @@ class SandboxSettings(BaseSettings):
4243
"""Idle reaper TTL: a sandbox unused this long is destroyed."""
4344

4445

46+
class StorageSettings(BaseSettings):
47+
"""Durable blob-store selection for uploads and artifacts.
48+
49+
``local`` is a directory tree (dev / single node); ``s3`` is an
50+
S3-compatible bucket (prod / multi-node). The per-conversation
51+
workspace is unaffected -- this is the durable tier behind it.
52+
"""
53+
54+
model_config = SettingsConfigDict(env_prefix="PAW_STORAGE__")
55+
56+
backend: str = "local"
57+
"""``local`` or ``s3``."""
58+
59+
local_root: str = "./storage"
60+
"""Root directory for the ``local`` backend."""
61+
62+
s3_bucket: str = ""
63+
"""Bucket name for the ``s3`` backend."""
64+
65+
s3_prefix: str = ""
66+
"""Key prefix within the bucket (namespacing, e.g. ``paw/``)."""
67+
68+
s3_region: str = ""
69+
"""AWS region; empty = SDK default resolution."""
70+
71+
s3_endpoint_url: str = ""
72+
"""Custom endpoint (e.g. MinIO / localstack); empty = real AWS."""
73+
74+
75+
class DockerSettings(BaseSettings):
76+
"""Container isolation knobs for the ``docker`` runner.
77+
78+
Defaults are the safe ones: no network, dropped capabilities,
79+
read-only rootfs, non-root user, resource ceilings. A wedged or
80+
hostile run is therefore bounded and cannot reach the host env, the
81+
database, or other tenants' workspaces.
82+
"""
83+
84+
model_config = SettingsConfigDict(env_prefix="PAW_DOCKER__")
85+
86+
image: str = "python-agent-harness:latest"
87+
"""Sandbox image; harness must be its entrypoint-able command.
88+
Pin by digest in production (``name@sha256:...``)."""
89+
90+
workdir: str = "/workspace"
91+
"""Container path the conversation workspace is mounted at (cwd)."""
92+
93+
network: str = "none"
94+
"""Container network mode; ``none`` = no egress (exfiltration/SSRF
95+
off). Override only with an explicit, filtered egress policy."""
96+
97+
mem_limit: str = "1g"
98+
"""Hard memory ceiling (docker ``mem_limit`` syntax)."""
99+
100+
nano_cpus: int = 1_000_000_000
101+
"""CPU quota in units of 1e-9 CPUs (1e9 = one core)."""
102+
103+
pids_limit: int = 256
104+
"""Max PIDs in the container (fork-bomb ceiling)."""
105+
106+
user: str = "1000:1000"
107+
"""Non-root uid:gid the harness runs as inside the container."""
108+
109+
read_only_rootfs: bool = True
110+
"""Mount the container root filesystem read-only (scratch via tmpfs)."""
111+
112+
tmpfs_size: str = "256m"
113+
"""Size of the writable ``/tmp`` tmpfs mounted into the container."""
114+
115+
stop_timeout: int = 5
116+
"""Seconds to wait after SIGTERM before the daemon kills on destroy."""
117+
118+
45119
class Settings(BaseSettings):
46120
model_config = SettingsConfigDict(
47121
env_prefix="PAW_", env_file=".env", env_file_encoding="utf-8", extra="ignore"
48122
)
49123

50124
db_url: str = "sqlite:///./paw.db"
125+
db_pool_size: int = 5
126+
"""SQLAlchemy connection pool size (server DBs only; ignored for
127+
SQLite). One pool per web replica."""
128+
db_max_overflow: int = 10
129+
"""Extra connections allowed beyond ``db_pool_size`` under load."""
130+
db_pool_recycle_s: int = 1800
131+
"""Recycle a pooled connection after this many seconds, so a
132+
connection a proxy would drop is replaced proactively."""
51133
secret_key: str = "dev-only-insecure-key-change-me-0123456789abcdef"
52134
fernet_key: str = ""
53135
"""Fernet key for the secrets store; empty = derived from
@@ -63,9 +145,12 @@ class Settings(BaseSettings):
63145
runner: str = "server"
64146
"""Sandbox runner: ``server`` (one resident ``harness serve``
65147
process per sandbox: multi-turn memory, mid-run Q&A, protocol-level
66-
cancel). Docker later."""
67-
harness: HarnessSettings = HarnessSettings()
68-
sandbox: SandboxSettings = SandboxSettings()
148+
cancel) or ``docker`` (the same, isolated in a per-sandbox
149+
container: no host env, no host filesystem, no network by default)."""
150+
harness: HarnessSettings = Field(default_factory=HarnessSettings)
151+
sandbox: SandboxSettings = Field(default_factory=SandboxSettings)
152+
docker: DockerSettings = Field(default_factory=DockerSettings)
153+
storage: StorageSettings = Field(default_factory=StorageSettings)
69154

70155
cors_origins: list[str] = []
71156
"""Extra allowed CORS origins for a split frontend."""
@@ -82,6 +167,40 @@ class Settings(BaseSettings):
82167
rate_limit_auth: int = 10
83168
"""Max login/register attempts per client IP per window."""
84169

170+
rate_limit_backend: str = "memory"
171+
"""Rate-limit store: ``memory`` (process-local, per-instance) or
172+
``redis`` (shared across instances for a global limit)."""
173+
rate_limit_redis_url: str = ""
174+
"""Redis URL for the ``redis`` backend; empty = localhost default."""
175+
176+
shutdown_drain_seconds: float = 25.0
177+
"""On shutdown, how long to wait for in-flight runs to finish before
178+
exiting. Keep under the orchestrator's SIGTERM grace period so the
179+
drain completes before a forced kill; runs still in flight at the
180+
deadline are reconciled to ``error`` on the next startup."""
181+
182+
budget_enforce: bool = False
183+
"""Enforce a per-user token budget before starting a run (the spend
184+
kill-switch). Off by default so existing/dev deployments are
185+
unaffected; turn on for a public billed platform."""
186+
budget_free_tokens: int = 1_000_000
187+
"""Token allowance for a user with no purchased points (the free
188+
tier). Total budget = this + ``tokens_per_point`` × the user's
189+
points."""
190+
budget_tokens_per_point: int = 1000
191+
"""How many tokens one account point is worth, converting the
192+
points buckets (plan_points + pack_points) into a token budget."""
193+
194+
max_upload_bytes: int = 100 * 1024 * 1024
195+
"""Per-file upload size cap."""
196+
max_user_storage_bytes: int = 1024 * 1024 * 1024
197+
"""Per-user total storage quota across all conversations (sum of
198+
uploaded file sizes). 0 disables the quota."""
199+
upload_allowed_types: list[str] = []
200+
"""Allow-list of upload content types by short name (e.g.
201+
``["xlsx", "csv", "pdf", "png"]``), validated by magic bytes. Empty
202+
= no content validation (any type accepted; dev default)."""
203+
85204

86205
@lru_cache(maxsize=1)
87206
def get_settings() -> Settings:

0 commit comments

Comments
 (0)