crafting-table/crafting_table/config.py
Kayos 0ec3a04676 v0.1 wave 1 (steps 2+3+4): SQLite ledger + FastAPI skeleton + async job runner
- db.py: migrations + DAOs for tokens / projects / jobs / findings (SQLite WAL)
- auth.py: SHA-256 bearer hashing + LAN-CIDR allowlist + admin/app token tiers
- models.py: Pydantic shapes (Project, Subproject, Schedule, Notify, Job, CreateJobRequest)
- server.py: FastAPI on port 8810; /healthz, /admin/tokens/*, /projects/*, /jobs, /jobs/{id}, /jobs/{id}/log, /jobs/{id}/findings
- runner.py: bounded asyncio pool, per-job timeout with process-group SIGTERM→SIGKILL escalation, orphaned-job recovery on boot
- workspace.py: bare-clone + worktree materialization, gc
- config.py: env-driven
- 62 tests across db / auth / projects / jobs / runner / e2e — all green

Cross-token project access returns 404 (not 403) — existence-leak guard.
Bearer tokens hashed at rest; admin token bootstrapped on first boot.
Recipe subprocess uses start_new_session=True so killpg targets the
whole process tree on timeout — child processes can't escape SIGKILL.
Pump task guarded with wait_for(2s) + cancel fallback against any
orphan that survives the group kill.

Wave 2 (parsers + findings extraction + MCP + email digest) pending.

Spec: memory/spec-crafting-table.md
2026-04-29 08:17:41 -07:00

59 lines
1.9 KiB
Python

"""Env-driven configuration.
All settings flow through environment variables so the same image runs in
prod (compose.yml env_file) and tests (monkeypatched envs). No config files.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from pathlib import Path
# Default LAN allowlist mirrors the rules baked into the network: anything
# inside RFC1918 plus loopback. Override with CRAFTING_LAN_CIDRS if a deploy
# wants stricter scoping.
DEFAULT_LAN_CIDRS = (
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"::1/128",
)
@dataclass(frozen=True)
class Config:
db_path: Path
workspace_root: Path
log_dir: Path
admin_bearer_path: Path
max_concurrent_jobs: int
api_port: int
api_bind: str
default_job_timeout_secs: int
lan_cidrs: tuple[str, ...]
workspace_gc_interval_secs: int
workspace_gc_age_secs: int
def load() -> Config:
cidrs_raw = os.environ.get("CRAFTING_LAN_CIDRS", "").strip()
if cidrs_raw:
cidrs = tuple(c.strip() for c in cidrs_raw.split(",") if c.strip())
else:
cidrs = DEFAULT_LAN_CIDRS
return Config(
db_path=Path(os.environ.get("CRAFTING_DB", "/data/crafting.db")),
workspace_root=Path(os.environ.get("CRAFTING_WORKSPACE", "/workspace")),
log_dir=Path(os.environ.get("CRAFTING_LOG_DIR", "/data/jobs")),
admin_bearer_path=Path(os.environ.get("CRAFTING_ADMIN_BEARER", "/data/admin-bearer.txt")),
max_concurrent_jobs=int(os.environ.get("CRAFTING_MAX_CONCURRENT", "4")),
api_port=int(os.environ.get("CRAFTING_PORT", "8810")),
api_bind=os.environ.get("CRAFTING_BIND", "0.0.0.0"),
default_job_timeout_secs=int(os.environ.get("CRAFTING_DEFAULT_JOB_TIMEOUT", "1800")),
lan_cidrs=cidrs,
workspace_gc_interval_secs=int(os.environ.get("CRAFTING_GC_INTERVAL", "3600")),
workspace_gc_age_secs=int(os.environ.get("CRAFTING_GC_AGE", "86400")),
)