Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
# for the app and a separate druks database rebuilt by pytest.
DRUKS_DATABASE_URL=postgresql+psycopg://druks:druks@localhost:5432/druks_dev

# Encrypts stored secrets at rest. Generate one:
# python3 -c 'import base64, os; print(base64.b64encode(os.urandom(32)).decode())'
DRUKS_SECRETS_KEY=

# Short-lived coordination: webhook claims, OAuth state/token caches, and the
# sandbox provisioning gate. Workflow state lives in Postgres, not Redis.
DRUKS_REDIS_URL=redis://127.0.0.1:6379/0
Expand All @@ -15,10 +19,6 @@ DRUKS_TEST_REDIS_URL=redis://127.0.0.1:6379/15
DRUKS_DATA_DIR=~/.druks
DRUKS_LOG_LEVEL=INFO

# Optional Slack interactivity authentication. Destination webhook URLs are
# configured in the dashboard.
SLACK_SIGNING_SECRET=

DRUKS_SANDBOX_KEYS_DIR=~/.druks/sandbox-keys

# Optional harness configuration copied into sandboxes. Provider credentials
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ curl -fsSL https://druks.ai/install.sh | DRUKS_PROVIDER=exe bash
```

The installer does not ask questions. The first run writes
`~/druks/druks.toml` with generated secrets. A remote shape can require values
`~/druks/druks.toml`, and `~/druks/.env` with generated secrets. A remote shape
can require values
that only you know. These values include provider credentials and identity-edge
details.

The installer prints this list and exits. Set the values in
`druks.toml`. Then run the same command again.
`druks.toml` and `.env`. Then run the same command again.

See the [deployment runbook](https://docs.druks.ai/deployment) for
prerequisites, access control, verification, and rollback.
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/alembic_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def run_alembic_env(target_metadata=None) -> None:
if target_metadata is None:
target_metadata = config.attributes.get("target_metadata", Base.metadata)
if not config.get_main_option("sqlalchemy.url"):
config.set_main_option("sqlalchemy.url", load_settings().database_url)
config.set_main_option("sqlalchemy.url", load_settings().database_url.get_secret_value())

def include_object(obj, name, type_, reflected, compare_to):
target = compare_to if compare_to is not None else obj
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
def configure_state(app: FastAPI, settings: Settings) -> None:
ensure_data_dirs(settings)
app.state.settings = settings
app.state.engine = create_async_engine_from_url(settings.database_url)
app.state.engine = create_async_engine_from_url(settings.database_url.get_secret_value())
# Bind the ambient (``scoped_session``) factory to this engine so
# request handlers can use ``db_session()`` without per-call setup.
configure_session(app.state.engine)
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/browser/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async def open(cls, session: StoredBrowserSession) -> "LoginWindow":
browser,
session.name,
start_url=f"https://{session.site}",
login_proxy=settings.sandbox.browser_login_proxy,
login_proxy=settings.sandbox.browser_login_proxy.get_secret_value(),
login_tz=settings.sandbox.browser_login_tz,
)
except BaseException:
Expand Down
4 changes: 2 additions & 2 deletions backend/druks/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ def main() -> None:
ensure_data_dirs(settings)

if args.command == "init-db":
run_migrations(settings.database_url)
run_migrations(settings.database_url.get_secret_value())
return

if args.command == "makemigrations":
make_app_migration(args.app, args.message, settings.database_url)
make_app_migration(args.app, args.message, settings.database_url.get_secret_value())
return

raise AssertionError(f"Unhandled command: {args.command}")
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _app_migration_dirs() -> list[tuple[str, Path]]:

# Every encrypted column reads its keys from the settings at each use. The
# HKDF info predates the library and must never change: stored rows carry it.
configure(lambda: load_settings().secrets.secrets_key, info=b"druks-secrets-v1")
configure(lambda: load_settings().secrets_key.get_secret_value(), info=b"druks-secrets-v1")


def create_engine_from_url(database_url: str):
Expand Down
10 changes: 5 additions & 5 deletions backend/druks/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CheckResult:
@asynccontextmanager
async def _check_engine(settings: Settings):
# The suite patches this to hand in the fixture's connection.
engine = create_async_engine_from_url(settings.database_url)
engine = create_async_engine_from_url(settings.database_url.get_secret_value())
try:
yield engine
finally:
Expand Down Expand Up @@ -166,7 +166,7 @@ def _credentials_check(
def check_provider_credentials(settings: Settings) -> list[CheckResult]:
# One result per registered provider. Doctor is a one-off, so a plain
# session reads the rows and never binds the ambient registry.
engine = create_engine_from_url(settings.database_url)
engine = create_engine_from_url(settings.database_url.get_secret_value())
try:
with Session(engine) as session:
default_account_id = session.scalar(select(Account.id).where(Account.is_default))
Expand Down Expand Up @@ -251,7 +251,7 @@ def check_data_dir(settings: Settings) -> CheckResult:

def check_database(settings: Settings) -> CheckResult:
try:
engine = create_engine_from_url(settings.database_url)
engine = create_engine_from_url(settings.database_url.get_secret_value())
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
engine.dispose()
Expand All @@ -273,7 +273,7 @@ async def check_drukbox(settings: Settings) -> CheckResult:
)
api = SandboxAPI(
base_url=settings.sandbox.service_url,
token=settings.sandbox.service_token,
token=settings.sandbox.service_token.get_secret_value(),
timeout=settings.sandbox.timeout,
)
try:
Expand Down Expand Up @@ -400,7 +400,7 @@ async def _doctor_exec(sandbox) -> None:


def check_redis(settings: Settings) -> CheckResult:
parsed = urlparse(settings.redis_url)
parsed = urlparse(settings.redis_url.get_secret_value())
host = parsed.hostname or "127.0.0.1"
port = parsed.port or 6379
try:
Expand Down
4 changes: 2 additions & 2 deletions backend/druks/durable/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def init_dbos() -> None:
settings = load_settings()
# System URL is the app database: DBOS self-migrates its bookkeeping into
# the dbos schema there, so derived Run.state is a same-DB read.
url = _dbos_database_url(settings.database_url)
url = _dbos_database_url(settings.database_url.get_secret_value())
config: DBOSConfig = {
"name": "druks",
"system_database_url": url,
Expand Down Expand Up @@ -164,7 +164,7 @@ def configure_engine(engine) -> None:
def _step_engine():
global _engine
if not _engine:
_engine = create_async_engine_from_url(load_settings().database_url)
_engine = create_async_engine_from_url(load_settings().database_url.get_secret_value())
return _engine


Expand Down
4 changes: 3 additions & 1 deletion backend/druks/harnesses/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pathlib import Path
from typing import Literal, Self

from pydantic import SecretStr

# Execution-side types live with the executor; re-exported here
# because the harness API speaks them.
from druks.sandbox.datastructures import ( # noqa: F401
Expand Down Expand Up @@ -101,7 +103,7 @@ class ParsedUsage:
@dataclass(frozen=True)
class SandboxSettings:
service_url: str
service_token: str
service_token: SecretStr
service_timeout: float
image: str
# Each harness owns one directory under this root. Missing files are not
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
def get_client() -> aioredis.Redis:
global _client
if not _client:
_client = aioredis.from_url(load_settings().redis_url)
_client = aioredis.from_url(load_settings().redis_url.get_secret_value())
return _client


Expand Down
2 changes: 1 addition & 1 deletion backend/druks/sandbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def _api(self) -> SandboxAPI:
settings = load_settings()
return SandboxAPI(
base_url=settings.sandbox.service_url,
token=settings.sandbox.service_token,
token=settings.sandbox.service_token.get_secret_value(),
timeout=settings.sandbox.timeout,
)

Expand Down
Loading
Loading