brawny 0.1.13__py3-none-any.whl → 0.1.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brawny/__init__.py +2 -0
- brawny/_context.py +5 -5
- brawny/_rpc/__init__.py +36 -12
- brawny/_rpc/broadcast.py +14 -13
- brawny/_rpc/caller.py +243 -0
- brawny/_rpc/client.py +539 -0
- brawny/_rpc/clients.py +11 -11
- brawny/_rpc/context.py +23 -0
- brawny/_rpc/errors.py +465 -31
- brawny/_rpc/gas.py +7 -6
- brawny/_rpc/pool.py +18 -0
- brawny/_rpc/retry.py +266 -0
- brawny/_rpc/retry_policy.py +81 -0
- brawny/accounts.py +28 -9
- brawny/alerts/__init__.py +15 -18
- brawny/alerts/abi_resolver.py +212 -36
- brawny/alerts/base.py +2 -2
- brawny/alerts/contracts.py +77 -10
- brawny/alerts/errors.py +30 -3
- brawny/alerts/events.py +38 -5
- brawny/alerts/health.py +19 -13
- brawny/alerts/send.py +513 -55
- brawny/api.py +39 -11
- brawny/assets/AGENTS.md +325 -0
- brawny/async_runtime.py +48 -0
- brawny/chain.py +3 -3
- brawny/cli/commands/__init__.py +2 -0
- brawny/cli/commands/console.py +69 -19
- brawny/cli/commands/contract.py +2 -2
- brawny/cli/commands/controls.py +121 -0
- brawny/cli/commands/health.py +2 -2
- brawny/cli/commands/job_dev.py +6 -5
- brawny/cli/commands/jobs.py +99 -2
- brawny/cli/commands/maintenance.py +13 -29
- brawny/cli/commands/migrate.py +1 -0
- brawny/cli/commands/run.py +10 -3
- brawny/cli/commands/script.py +8 -3
- brawny/cli/commands/signer.py +143 -26
- brawny/cli/helpers.py +0 -3
- brawny/cli_templates.py +25 -349
- brawny/config/__init__.py +4 -1
- brawny/config/models.py +43 -57
- brawny/config/parser.py +268 -57
- brawny/config/validation.py +52 -15
- brawny/daemon/context.py +4 -2
- brawny/daemon/core.py +185 -63
- brawny/daemon/loops.py +166 -98
- brawny/daemon/supervisor.py +261 -0
- brawny/db/__init__.py +14 -26
- brawny/db/base.py +248 -151
- brawny/db/global_cache.py +11 -1
- brawny/db/migrate.py +175 -28
- brawny/db/migrations/001_init.sql +4 -3
- brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
- brawny/db/migrations/011_add_job_logs.sql +1 -2
- brawny/db/migrations/012_add_claimed_by.sql +2 -2
- brawny/db/migrations/013_attempt_unique.sql +10 -0
- brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
- brawny/db/migrations/015_add_signer_alias.sql +14 -0
- brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
- brawny/db/migrations/017_add_job_drain.sql +6 -0
- brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
- brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
- brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
- brawny/db/ops/__init__.py +3 -25
- brawny/db/ops/logs.py +1 -2
- brawny/db/queries.py +47 -91
- brawny/db/serialized.py +65 -0
- brawny/db/sqlite/__init__.py +1001 -0
- brawny/db/sqlite/connection.py +231 -0
- brawny/db/sqlite/execute.py +116 -0
- brawny/db/sqlite/mappers.py +190 -0
- brawny/db/sqlite/repos/attempts.py +372 -0
- brawny/db/sqlite/repos/block_state.py +102 -0
- brawny/db/sqlite/repos/cache.py +104 -0
- brawny/db/sqlite/repos/intents.py +1021 -0
- brawny/db/sqlite/repos/jobs.py +200 -0
- brawny/db/sqlite/repos/maintenance.py +182 -0
- brawny/db/sqlite/repos/signers_nonces.py +566 -0
- brawny/db/sqlite/tx.py +119 -0
- brawny/http.py +194 -0
- brawny/invariants.py +11 -24
- brawny/jobs/base.py +8 -0
- brawny/jobs/job_validation.py +2 -1
- brawny/keystore.py +83 -7
- brawny/lifecycle.py +64 -12
- brawny/logging.py +0 -2
- brawny/metrics.py +84 -12
- brawny/model/contexts.py +111 -9
- brawny/model/enums.py +1 -0
- brawny/model/errors.py +18 -0
- brawny/model/types.py +47 -131
- brawny/network_guard.py +133 -0
- brawny/networks/__init__.py +5 -5
- brawny/networks/config.py +1 -7
- brawny/networks/manager.py +14 -11
- brawny/runtime_controls.py +74 -0
- brawny/scheduler/poller.py +11 -7
- brawny/scheduler/reorg.py +95 -39
- brawny/scheduler/runner.py +442 -168
- brawny/scheduler/shutdown.py +3 -3
- brawny/script_tx.py +3 -3
- brawny/telegram.py +53 -7
- brawny/testing.py +1 -0
- brawny/timeout.py +38 -0
- brawny/tx/executor.py +922 -308
- brawny/tx/intent.py +54 -16
- brawny/tx/monitor.py +31 -12
- brawny/tx/nonce.py +212 -90
- brawny/tx/replacement.py +69 -18
- brawny/tx/retry_policy.py +24 -0
- brawny/tx/stages/types.py +75 -0
- brawny/types.py +18 -0
- brawny/utils.py +41 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
- brawny-0.1.22.dist-info/RECORD +163 -0
- brawny/_rpc/manager.py +0 -982
- brawny/_rpc/selector.py +0 -156
- brawny/db/base_new.py +0 -165
- brawny/db/mappers.py +0 -182
- brawny/db/migrations/008_add_transactions.sql +0 -72
- brawny/db/ops/attempts.py +0 -108
- brawny/db/ops/blocks.py +0 -83
- brawny/db/ops/cache.py +0 -93
- brawny/db/ops/intents.py +0 -296
- brawny/db/ops/jobs.py +0 -110
- brawny/db/ops/nonces.py +0 -322
- brawny/db/postgres.py +0 -2535
- brawny/db/postgres_new.py +0 -196
- brawny/db/sqlite.py +0 -2733
- brawny/db/sqlite_new.py +0 -191
- brawny-0.1.13.dist-info/RECORD +0 -141
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
brawny/config/__init__.py
CHANGED
|
@@ -8,10 +8,11 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
|
|
11
|
-
from brawny.config.models import AdvancedConfig, Config, RPCGroupConfig
|
|
11
|
+
from brawny.config.models import AdvancedConfig, Config, IntentCooldownConfig, RPCGroupConfig
|
|
12
12
|
from brawny.config.validation import (
|
|
13
13
|
InvalidEndpointError,
|
|
14
14
|
canonicalize_endpoint,
|
|
15
|
+
canonicalize_endpoints,
|
|
15
16
|
dedupe_preserve_order,
|
|
16
17
|
validate_config,
|
|
17
18
|
)
|
|
@@ -21,10 +22,12 @@ __all__ = [
|
|
|
21
22
|
# Models
|
|
22
23
|
"Config",
|
|
23
24
|
"AdvancedConfig",
|
|
25
|
+
"IntentCooldownConfig",
|
|
24
26
|
"RPCGroupConfig",
|
|
25
27
|
# Validation
|
|
26
28
|
"InvalidEndpointError",
|
|
27
29
|
"canonicalize_endpoint",
|
|
30
|
+
"canonicalize_endpoints",
|
|
28
31
|
"dedupe_preserve_order",
|
|
29
32
|
"validate_config",
|
|
30
33
|
# Errors
|
brawny/config/models.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from dataclasses import asdict, dataclass, field
|
|
9
9
|
from brawny.model.enums import KeystoreType
|
|
10
|
+
from brawny.http import HttpConfig
|
|
10
11
|
|
|
11
12
|
DEFAULT_BLOCK_HASH_HISTORY_SIZE = 256
|
|
12
13
|
DEFAULT_JOB_ERROR_BACKOFF_BLOCKS = 1
|
|
@@ -15,17 +16,15 @@ DEFAULT_NONCE_RECONCILE_INTERVAL_SECONDS = 300
|
|
|
15
16
|
DEFAULT_STUCK_TX_BLOCKS = 50
|
|
16
17
|
DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30
|
|
17
18
|
DEFAULT_RPC_RETRY_BACKOFF_BASE = 1.0
|
|
18
|
-
DEFAULT_RPC_CIRCUIT_BREAKER_SECONDS = 300
|
|
19
19
|
DEFAULT_DB_CIRCUIT_BREAKER_FAILURES = 5
|
|
20
20
|
DEFAULT_DB_CIRCUIT_BREAKER_SECONDS = 30
|
|
21
21
|
DEFAULT_GAS_REFRESH_SECONDS = 15
|
|
22
22
|
DEFAULT_FALLBACK_GAS_LIMIT = 500_000
|
|
23
|
-
DEFAULT_TELEGRAM_RATE_LIMIT_PER_MINUTE = 20
|
|
24
23
|
DEFAULT_ABI_CACHE_TTL_SECONDS = 86400 * 7
|
|
25
|
-
DEFAULT_DATABASE_POOL_TIMEOUT_SECONDS = 30.0
|
|
26
24
|
DEFAULT_NONCE_GAP_ALERT_SECONDS = 300
|
|
27
25
|
DEFAULT_MAX_EXECUTOR_RETRIES = 5
|
|
28
26
|
DEFAULT_FINALITY_CONFIRMATIONS = 12
|
|
27
|
+
DEFAULT_ALERTS_HEALTH_MAX_OLDEST_AGE_SECONDS = 120
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
@dataclass
|
|
@@ -36,7 +35,7 @@ class TelegramConfig:
|
|
|
36
35
|
bot_token: Bot token for API calls (None = disabled)
|
|
37
36
|
chats: Named chat targets (e.g., {"ops": "-100...", "dev": "-100..."})
|
|
38
37
|
default: Default targets when job.alert_to not specified
|
|
39
|
-
parse_mode: Default parse mode for telegram messages (None =
|
|
38
|
+
parse_mode: Default parse mode for telegram messages (None = disable formatting)
|
|
40
39
|
health_chat: Chat name for daemon health alerts (None = logged only)
|
|
41
40
|
health_cooldown_seconds: Deduplication window for health alerts
|
|
42
41
|
"""
|
|
@@ -49,6 +48,31 @@ class TelegramConfig:
|
|
|
49
48
|
health_cooldown_seconds: int = 1800 # 30 minutes between identical alerts
|
|
50
49
|
|
|
51
50
|
|
|
51
|
+
@dataclass
|
|
52
|
+
class GuardrailsConfig:
|
|
53
|
+
"""Guardrail configuration for linting and runtime enforcement."""
|
|
54
|
+
|
|
55
|
+
lint_paths: list[str] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class DebugConfig:
|
|
60
|
+
"""Debug-only settings (opt-in)."""
|
|
61
|
+
|
|
62
|
+
allow_console: bool = False
|
|
63
|
+
enable_null_lease_reclaim: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class IntentCooldownConfig:
|
|
68
|
+
"""Global intent cooldown configuration."""
|
|
69
|
+
|
|
70
|
+
enabled: bool = True
|
|
71
|
+
default_seconds: int = 300
|
|
72
|
+
max_seconds: int = 3600
|
|
73
|
+
prune_older_than_days: int = 30
|
|
74
|
+
|
|
75
|
+
|
|
52
76
|
@dataclass
|
|
53
77
|
class RPCGroupConfig:
|
|
54
78
|
"""A named collection of RPC endpoints."""
|
|
@@ -88,13 +112,12 @@ class AdvancedConfig:
|
|
|
88
112
|
rpc_timeout_seconds: float = 30.0
|
|
89
113
|
rpc_max_retries: int = 3
|
|
90
114
|
|
|
91
|
-
# Database pool (Postgres only)
|
|
92
|
-
database_pool_size: int = 5
|
|
93
|
-
database_pool_max_overflow: int = 10
|
|
94
|
-
|
|
95
115
|
# Job logs
|
|
96
116
|
log_retention_days: int = 7
|
|
97
117
|
|
|
118
|
+
# Alerts
|
|
119
|
+
alerts_health_max_oldest_age_seconds: int = DEFAULT_ALERTS_HEALTH_MAX_OLDEST_AGE_SECONDS
|
|
120
|
+
|
|
98
121
|
|
|
99
122
|
@dataclass
|
|
100
123
|
class Config:
|
|
@@ -126,6 +149,18 @@ class Config:
|
|
|
126
149
|
# Telegram (canonical form - parsed from telegram: or legacy fields)
|
|
127
150
|
telegram: TelegramConfig = field(default_factory=TelegramConfig)
|
|
128
151
|
|
|
152
|
+
# Guardrails (lint hints for job code)
|
|
153
|
+
guardrails: GuardrailsConfig = field(default_factory=GuardrailsConfig)
|
|
154
|
+
|
|
155
|
+
# Debug-only flags (disabled by default)
|
|
156
|
+
debug: DebugConfig = field(default_factory=DebugConfig)
|
|
157
|
+
|
|
158
|
+
# Intent cooldown (global defaults)
|
|
159
|
+
intent_cooldown: IntentCooldownConfig = field(default_factory=IntentCooldownConfig)
|
|
160
|
+
|
|
161
|
+
# HTTP (approved client for job code)
|
|
162
|
+
http: HttpConfig = field(default_factory=HttpConfig)
|
|
163
|
+
|
|
129
164
|
# Metrics
|
|
130
165
|
metrics_port: int = 9091
|
|
131
166
|
|
|
@@ -145,15 +180,6 @@ class Config:
|
|
|
145
180
|
validate_advanced_config(self._advanced_or_default())
|
|
146
181
|
|
|
147
182
|
@property
|
|
148
|
-
def is_sqlite(self) -> bool:
|
|
149
|
-
"""Check if using SQLite database."""
|
|
150
|
-
return self.database_url.startswith("sqlite:///")
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def is_postgres(self) -> bool:
|
|
154
|
-
"""Check if using PostgreSQL database."""
|
|
155
|
-
return self.database_url.startswith(("postgresql://", "postgres://"))
|
|
156
|
-
|
|
157
183
|
@classmethod
|
|
158
184
|
def from_env(cls) -> "Config":
|
|
159
185
|
"""Load configuration from environment variables."""
|
|
@@ -209,10 +235,6 @@ class Config:
|
|
|
209
235
|
}
|
|
210
236
|
redacted_groups[group_name] = group_value
|
|
211
237
|
redacted[key] = redacted_groups
|
|
212
|
-
elif key == "rpc_rate_limits" and isinstance(value, dict):
|
|
213
|
-
redacted[key] = {
|
|
214
|
-
_redact_url(str(endpoint)): limiter for endpoint, limiter in value.items()
|
|
215
|
-
}
|
|
216
238
|
elif key == "telegram" and isinstance(value, dict):
|
|
217
239
|
# Redact bot_token within telegram config
|
|
218
240
|
redacted[key] = {
|
|
@@ -280,14 +302,6 @@ class Config:
|
|
|
280
302
|
def rpc_max_retries(self) -> int:
|
|
281
303
|
return self._advanced_or_default().rpc_max_retries
|
|
282
304
|
|
|
283
|
-
@property
|
|
284
|
-
def database_pool_size(self) -> int:
|
|
285
|
-
return self._advanced_or_default().database_pool_size
|
|
286
|
-
|
|
287
|
-
@property
|
|
288
|
-
def database_pool_max_overflow(self) -> int:
|
|
289
|
-
return self._advanced_or_default().database_pool_max_overflow
|
|
290
|
-
|
|
291
305
|
@property
|
|
292
306
|
def priority_fee(self) -> int:
|
|
293
307
|
return int(self.default_priority_fee_gwei * 1_000_000_000)
|
|
@@ -343,10 +357,6 @@ class Config:
|
|
|
343
357
|
def rpc_retry_backoff_base(self) -> float:
|
|
344
358
|
return DEFAULT_RPC_RETRY_BACKOFF_BASE
|
|
345
359
|
|
|
346
|
-
@property
|
|
347
|
-
def rpc_circuit_breaker_seconds(self) -> int:
|
|
348
|
-
return DEFAULT_RPC_CIRCUIT_BREAKER_SECONDS
|
|
349
|
-
|
|
350
360
|
@property
|
|
351
361
|
def db_circuit_breaker_failures(self) -> int:
|
|
352
362
|
return DEFAULT_DB_CIRCUIT_BREAKER_FAILURES
|
|
@@ -355,18 +365,6 @@ class Config:
|
|
|
355
365
|
def db_circuit_breaker_seconds(self) -> int:
|
|
356
366
|
return DEFAULT_DB_CIRCUIT_BREAKER_SECONDS
|
|
357
367
|
|
|
358
|
-
@property
|
|
359
|
-
def rpc_rate_limit_per_second(self) -> float | None:
|
|
360
|
-
return None
|
|
361
|
-
|
|
362
|
-
@property
|
|
363
|
-
def rpc_rate_limit_burst(self) -> int | None:
|
|
364
|
-
return None
|
|
365
|
-
|
|
366
|
-
@property
|
|
367
|
-
def rpc_rate_limits(self) -> dict[str, dict[str, float | int]]:
|
|
368
|
-
return {}
|
|
369
|
-
|
|
370
368
|
@property
|
|
371
369
|
def gas_refresh_seconds(self) -> int:
|
|
372
370
|
return DEFAULT_GAS_REFRESH_SECONDS
|
|
@@ -375,18 +373,10 @@ class Config:
|
|
|
375
373
|
def fallback_gas_limit(self) -> int:
|
|
376
374
|
return DEFAULT_FALLBACK_GAS_LIMIT
|
|
377
375
|
|
|
378
|
-
@property
|
|
379
|
-
def telegram_rate_limit_per_minute(self) -> int:
|
|
380
|
-
return DEFAULT_TELEGRAM_RATE_LIMIT_PER_MINUTE
|
|
381
|
-
|
|
382
376
|
@property
|
|
383
377
|
def abi_cache_ttl_seconds(self) -> int:
|
|
384
378
|
return DEFAULT_ABI_CACHE_TTL_SECONDS
|
|
385
379
|
|
|
386
|
-
@property
|
|
387
|
-
def database_pool_timeout_seconds(self) -> float:
|
|
388
|
-
return DEFAULT_DATABASE_POOL_TIMEOUT_SECONDS
|
|
389
|
-
|
|
390
380
|
@property
|
|
391
381
|
def allow_unsafe_nonce_reset(self) -> bool:
|
|
392
382
|
return False
|
|
@@ -395,10 +385,6 @@ class Config:
|
|
|
395
385
|
def nonce_gap_alert_seconds(self) -> int:
|
|
396
386
|
return DEFAULT_NONCE_GAP_ALERT_SECONDS
|
|
397
387
|
|
|
398
|
-
@property
|
|
399
|
-
def brownie_password_fallback(self) -> bool:
|
|
400
|
-
return False
|
|
401
|
-
|
|
402
388
|
@property
|
|
403
389
|
def log_retention_days(self) -> int:
|
|
404
390
|
return self._advanced_or_default().log_retention_days
|
brawny/config/parser.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
from typing import Callable
|
|
10
11
|
from dataclasses import replace
|
|
11
12
|
|
|
12
13
|
from brawny.model.enums import KeystoreType
|
|
@@ -33,8 +34,6 @@ _ADVANCED_FIELDS = {
|
|
|
33
34
|
"fee_bump_percent",
|
|
34
35
|
"rpc_timeout_seconds",
|
|
35
36
|
"rpc_max_retries",
|
|
36
|
-
"database_pool_size",
|
|
37
|
-
"database_pool_max_overflow",
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
_REMOVED_ENV_KEYS = {
|
|
@@ -104,6 +103,7 @@ def _parse_telegram(raw: dict) -> "TelegramConfig":
|
|
|
104
103
|
default = [default]
|
|
105
104
|
default = [d.strip() for d in default if d and str(d).strip()]
|
|
106
105
|
|
|
106
|
+
parse_mode_provided = "parse_mode" in tg
|
|
107
107
|
parse_mode = tg.get("parse_mode")
|
|
108
108
|
if isinstance(parse_mode, str):
|
|
109
109
|
parse_mode = parse_mode.strip() or None
|
|
@@ -111,6 +111,8 @@ def _parse_telegram(raw: dict) -> "TelegramConfig":
|
|
|
111
111
|
raise ConfigError(
|
|
112
112
|
"telegram.parse_mode must be one of: Markdown, MarkdownV2, HTML, or null"
|
|
113
113
|
)
|
|
114
|
+
if not parse_mode_provided and parse_mode is None:
|
|
115
|
+
parse_mode = TelegramConfig().parse_mode
|
|
114
116
|
|
|
115
117
|
health_chat = tg.get("health_chat")
|
|
116
118
|
if isinstance(health_chat, str):
|
|
@@ -160,6 +162,115 @@ def _parse_telegram(raw: dict) -> "TelegramConfig":
|
|
|
160
162
|
return TelegramConfig()
|
|
161
163
|
|
|
162
164
|
|
|
165
|
+
def _parse_guardrails(raw: dict) -> "GuardrailsConfig":
|
|
166
|
+
from brawny.config.models import GuardrailsConfig
|
|
167
|
+
|
|
168
|
+
if "guardrails" in raw and isinstance(raw["guardrails"], dict):
|
|
169
|
+
guardrails = raw["guardrails"]
|
|
170
|
+
lint_paths = guardrails.get("lint_paths", [])
|
|
171
|
+
if lint_paths is None:
|
|
172
|
+
lint_paths = []
|
|
173
|
+
if isinstance(lint_paths, str):
|
|
174
|
+
lint_paths = [lint_paths]
|
|
175
|
+
lint_paths = [str(path).strip() for path in lint_paths if str(path).strip()]
|
|
176
|
+
return GuardrailsConfig(lint_paths=lint_paths)
|
|
177
|
+
return GuardrailsConfig()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_debug(raw: dict) -> "DebugConfig":
|
|
181
|
+
from brawny.config.models import DebugConfig
|
|
182
|
+
|
|
183
|
+
if "debug" in raw and isinstance(raw["debug"], dict):
|
|
184
|
+
debug = raw["debug"]
|
|
185
|
+
allow_console = debug.get("allow_console", False)
|
|
186
|
+
if not isinstance(allow_console, bool):
|
|
187
|
+
raise ConfigError("debug.allow_console must be a boolean")
|
|
188
|
+
enable_null_lease_reclaim = debug.get("enable_null_lease_reclaim", False)
|
|
189
|
+
if not isinstance(enable_null_lease_reclaim, bool):
|
|
190
|
+
raise ConfigError("debug.enable_null_lease_reclaim must be a boolean")
|
|
191
|
+
return DebugConfig(
|
|
192
|
+
allow_console=allow_console,
|
|
193
|
+
enable_null_lease_reclaim=enable_null_lease_reclaim,
|
|
194
|
+
)
|
|
195
|
+
return DebugConfig()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _parse_intent_cooldown(raw: dict) -> "IntentCooldownConfig":
|
|
199
|
+
from brawny.config.models import IntentCooldownConfig
|
|
200
|
+
|
|
201
|
+
if "intent_cooldown" in raw and isinstance(raw["intent_cooldown"], dict):
|
|
202
|
+
cfg = raw["intent_cooldown"]
|
|
203
|
+
enabled = cfg.get("enabled", True)
|
|
204
|
+
default_seconds = cfg.get("default_seconds", IntentCooldownConfig().default_seconds)
|
|
205
|
+
max_seconds = cfg.get("max_seconds", IntentCooldownConfig().max_seconds)
|
|
206
|
+
prune_older_than_days = cfg.get(
|
|
207
|
+
"prune_older_than_days", IntentCooldownConfig().prune_older_than_days
|
|
208
|
+
)
|
|
209
|
+
if not isinstance(enabled, bool):
|
|
210
|
+
raise ConfigError("intent_cooldown.enabled must be a boolean")
|
|
211
|
+
return IntentCooldownConfig(
|
|
212
|
+
enabled=enabled,
|
|
213
|
+
default_seconds=int(default_seconds),
|
|
214
|
+
max_seconds=int(max_seconds),
|
|
215
|
+
prune_older_than_days=int(prune_older_than_days),
|
|
216
|
+
)
|
|
217
|
+
return IntentCooldownConfig()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _parse_http(raw: dict) -> "HttpConfig":
|
|
221
|
+
"""Parse HTTP config, handling canonical http: block."""
|
|
222
|
+
from brawny.http import HttpConfig
|
|
223
|
+
|
|
224
|
+
if "http" in raw and isinstance(raw["http"], dict):
|
|
225
|
+
cfg = raw["http"]
|
|
226
|
+
allowed_domains = cfg.get("allowed_domains", [])
|
|
227
|
+
if isinstance(allowed_domains, str):
|
|
228
|
+
allowed_domains = [allowed_domains]
|
|
229
|
+
if not isinstance(allowed_domains, list):
|
|
230
|
+
raise ConfigError("http.allowed_domains must be a list of domains")
|
|
231
|
+
allowed_domains = [str(d).strip() for d in allowed_domains if str(d).strip()]
|
|
232
|
+
|
|
233
|
+
connect_timeout_seconds = cfg.get("connect_timeout_seconds")
|
|
234
|
+
read_timeout_seconds = cfg.get("read_timeout_seconds")
|
|
235
|
+
max_retries = cfg.get("max_retries")
|
|
236
|
+
backoff_base_seconds = cfg.get("backoff_base_seconds")
|
|
237
|
+
|
|
238
|
+
if connect_timeout_seconds is None:
|
|
239
|
+
connect_timeout_seconds = HttpConfig().connect_timeout_seconds
|
|
240
|
+
if read_timeout_seconds is None:
|
|
241
|
+
read_timeout_seconds = HttpConfig().read_timeout_seconds
|
|
242
|
+
if max_retries is None:
|
|
243
|
+
max_retries = HttpConfig().max_retries
|
|
244
|
+
if backoff_base_seconds is None:
|
|
245
|
+
backoff_base_seconds = HttpConfig().backoff_base_seconds
|
|
246
|
+
try:
|
|
247
|
+
connect_timeout_seconds = float(connect_timeout_seconds)
|
|
248
|
+
except (TypeError, ValueError) as exc:
|
|
249
|
+
raise ConfigError("http.connect_timeout_seconds must be a number") from exc
|
|
250
|
+
try:
|
|
251
|
+
read_timeout_seconds = float(read_timeout_seconds)
|
|
252
|
+
except (TypeError, ValueError) as exc:
|
|
253
|
+
raise ConfigError("http.read_timeout_seconds must be a number") from exc
|
|
254
|
+
try:
|
|
255
|
+
max_retries = int(max_retries)
|
|
256
|
+
except (TypeError, ValueError) as exc:
|
|
257
|
+
raise ConfigError("http.max_retries must be an integer") from exc
|
|
258
|
+
try:
|
|
259
|
+
backoff_base_seconds = float(backoff_base_seconds)
|
|
260
|
+
except (TypeError, ValueError) as exc:
|
|
261
|
+
raise ConfigError("http.backoff_base_seconds must be a number") from exc
|
|
262
|
+
|
|
263
|
+
return HttpConfig(
|
|
264
|
+
allowed_domains=allowed_domains,
|
|
265
|
+
connect_timeout_seconds=connect_timeout_seconds,
|
|
266
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
267
|
+
max_retries=max_retries,
|
|
268
|
+
backoff_base_seconds=backoff_base_seconds,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return HttpConfig()
|
|
272
|
+
|
|
273
|
+
|
|
163
274
|
|
|
164
275
|
|
|
165
276
|
def _interpolate_env_vars(
|
|
@@ -290,9 +401,43 @@ def _parse_env_float(key: str) -> float:
|
|
|
290
401
|
raise ConfigError(f"BRAWNY_{key} must be a number, got: {value}")
|
|
291
402
|
|
|
292
403
|
|
|
404
|
+
_ADVANCED_ENV_MAPPING: dict[str, tuple[str, Callable[[str], object]]] = {
|
|
405
|
+
"POLL_INTERVAL_SECONDS": ("poll_interval_seconds", _parse_env_float),
|
|
406
|
+
"REORG_DEPTH": ("reorg_depth", _parse_env_int),
|
|
407
|
+
"FINALITY_CONFIRMATIONS": ("finality_confirmations", _parse_env_int),
|
|
408
|
+
"DEFAULT_DEADLINE_SECONDS": ("default_deadline_seconds", _parse_env_int),
|
|
409
|
+
"STUCK_TX_SECONDS": ("stuck_tx_seconds", _parse_env_int),
|
|
410
|
+
"MAX_REPLACEMENT_ATTEMPTS": ("max_replacement_attempts", _parse_env_int),
|
|
411
|
+
"GAS_LIMIT_MULTIPLIER": ("gas_limit_multiplier", _parse_env_float),
|
|
412
|
+
"DEFAULT_PRIORITY_FEE_GWEI": ("default_priority_fee_gwei", _parse_env_float),
|
|
413
|
+
"MAX_FEE_CAP_GWEI": ("max_fee_cap_gwei", _parse_env_float),
|
|
414
|
+
"FEE_BUMP_PERCENT": ("fee_bump_percent", _parse_env_int),
|
|
415
|
+
"RPC_TIMEOUT_SECONDS": ("rpc_timeout_seconds", _parse_env_float),
|
|
416
|
+
"RPC_MAX_RETRIES": ("rpc_max_retries", _parse_env_int),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _get_advanced_env_overrides() -> dict[str, object]:
|
|
421
|
+
overrides: dict[str, object] = {}
|
|
422
|
+
for env_key, (field_name, parser) in _ADVANCED_ENV_MAPPING.items():
|
|
423
|
+
if _env_is_set(env_key):
|
|
424
|
+
overrides[field_name] = parser(env_key)
|
|
425
|
+
return overrides
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _get_advanced_env_overrides_with_keys() -> tuple[dict[str, object], list[str]]:
|
|
429
|
+
overrides: dict[str, object] = {}
|
|
430
|
+
overridden: list[str] = []
|
|
431
|
+
for env_key, (field_name, parser) in _ADVANCED_ENV_MAPPING.items():
|
|
432
|
+
if _env_is_set(env_key):
|
|
433
|
+
overrides[field_name] = parser(env_key)
|
|
434
|
+
overridden.append(f"advanced.{field_name}")
|
|
435
|
+
return overrides, overridden
|
|
436
|
+
|
|
437
|
+
|
|
293
438
|
def from_env() -> "Config":
|
|
294
439
|
"""Load configuration from environment variables."""
|
|
295
|
-
from brawny.config.models import AdvancedConfig, Config, RPCGroupConfig
|
|
440
|
+
from brawny.config.models import AdvancedConfig, Config, GuardrailsConfig, RPCGroupConfig
|
|
296
441
|
from brawny.config.validation import canonicalize_endpoint, dedupe_preserve_order, InvalidEndpointError
|
|
297
442
|
|
|
298
443
|
_fail_removed_env_vars()
|
|
@@ -328,35 +473,7 @@ def from_env() -> "Config":
|
|
|
328
473
|
f"Must be one of: {', '.join(kt.value for kt in KeystoreType)}"
|
|
329
474
|
)
|
|
330
475
|
|
|
331
|
-
advanced_kwargs
|
|
332
|
-
if _env_is_set("POLL_INTERVAL_SECONDS"):
|
|
333
|
-
advanced_kwargs["poll_interval_seconds"] = _parse_env_float("POLL_INTERVAL_SECONDS")
|
|
334
|
-
if _env_is_set("REORG_DEPTH"):
|
|
335
|
-
advanced_kwargs["reorg_depth"] = _parse_env_int("REORG_DEPTH")
|
|
336
|
-
if _env_is_set("FINALITY_CONFIRMATIONS"):
|
|
337
|
-
advanced_kwargs["finality_confirmations"] = _parse_env_int("FINALITY_CONFIRMATIONS")
|
|
338
|
-
if _env_is_set("DEFAULT_DEADLINE_SECONDS"):
|
|
339
|
-
advanced_kwargs["default_deadline_seconds"] = _parse_env_int("DEFAULT_DEADLINE_SECONDS")
|
|
340
|
-
if _env_is_set("STUCK_TX_SECONDS"):
|
|
341
|
-
advanced_kwargs["stuck_tx_seconds"] = _parse_env_int("STUCK_TX_SECONDS")
|
|
342
|
-
if _env_is_set("MAX_REPLACEMENT_ATTEMPTS"):
|
|
343
|
-
advanced_kwargs["max_replacement_attempts"] = _parse_env_int("MAX_REPLACEMENT_ATTEMPTS")
|
|
344
|
-
if _env_is_set("GAS_LIMIT_MULTIPLIER"):
|
|
345
|
-
advanced_kwargs["gas_limit_multiplier"] = _parse_env_float("GAS_LIMIT_MULTIPLIER")
|
|
346
|
-
if _env_is_set("DEFAULT_PRIORITY_FEE_GWEI"):
|
|
347
|
-
advanced_kwargs["default_priority_fee_gwei"] = _parse_env_float("DEFAULT_PRIORITY_FEE_GWEI")
|
|
348
|
-
if _env_is_set("MAX_FEE_CAP_GWEI"):
|
|
349
|
-
advanced_kwargs["max_fee_cap_gwei"] = _parse_env_float("MAX_FEE_CAP_GWEI")
|
|
350
|
-
if _env_is_set("FEE_BUMP_PERCENT"):
|
|
351
|
-
advanced_kwargs["fee_bump_percent"] = _parse_env_int("FEE_BUMP_PERCENT")
|
|
352
|
-
if _env_is_set("RPC_TIMEOUT_SECONDS"):
|
|
353
|
-
advanced_kwargs["rpc_timeout_seconds"] = _parse_env_float("RPC_TIMEOUT_SECONDS")
|
|
354
|
-
if _env_is_set("RPC_MAX_RETRIES"):
|
|
355
|
-
advanced_kwargs["rpc_max_retries"] = _parse_env_int("RPC_MAX_RETRIES")
|
|
356
|
-
if _env_is_set("DATABASE_POOL_SIZE"):
|
|
357
|
-
advanced_kwargs["database_pool_size"] = _parse_env_int("DATABASE_POOL_SIZE")
|
|
358
|
-
if _env_is_set("DATABASE_POOL_MAX_OVERFLOW"):
|
|
359
|
-
advanced_kwargs["database_pool_max_overflow"] = _parse_env_int("DATABASE_POOL_MAX_OVERFLOW")
|
|
476
|
+
advanced_kwargs = _get_advanced_env_overrides()
|
|
360
477
|
|
|
361
478
|
|
|
362
479
|
# Parse telegram config from env (legacy format)
|
|
@@ -364,6 +481,20 @@ def from_env() -> "Config":
|
|
|
364
481
|
"telegram_bot_token": _get_env("TELEGRAM_BOT_TOKEN"),
|
|
365
482
|
"telegram_chat_id": _get_env("TELEGRAM_CHAT_ID"),
|
|
366
483
|
})
|
|
484
|
+
guardrails_config = _parse_guardrails({
|
|
485
|
+
"guardrails": {
|
|
486
|
+
"lint_paths": _get_env_list("GUARDRAILS_LINT_PATHS"),
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
http_allowed = _get_env("HTTP_ALLOWED_DOMAINS")
|
|
490
|
+
http_config = _parse_http({
|
|
491
|
+
"http": {
|
|
492
|
+
"allowed_domains": [d.strip() for d in http_allowed.split(",")] if http_allowed else [],
|
|
493
|
+
"connect_timeout_seconds": _get_env("HTTP_CONNECT_TIMEOUT_SECONDS"),
|
|
494
|
+
"read_timeout_seconds": _get_env("HTTP_READ_TIMEOUT_SECONDS"),
|
|
495
|
+
"max_retries": _get_env("HTTP_MAX_RETRIES"),
|
|
496
|
+
}
|
|
497
|
+
})
|
|
367
498
|
|
|
368
499
|
config = Config(
|
|
369
500
|
database_url=database_url,
|
|
@@ -374,6 +505,9 @@ def from_env() -> "Config":
|
|
|
374
505
|
worker_count=_get_env_int("WORKER_COUNT", 1),
|
|
375
506
|
advanced=AdvancedConfig(**advanced_kwargs) if advanced_kwargs else None,
|
|
376
507
|
telegram=telegram_config,
|
|
508
|
+
guardrails=guardrails_config,
|
|
509
|
+
http=http_config,
|
|
510
|
+
intent_cooldown=IntentCooldownConfig(),
|
|
377
511
|
keystore_type=keystore_type,
|
|
378
512
|
keystore_path=_get_env("KEYSTORE_PATH", "~/.brawny/keys") or "~/.brawny/keys",
|
|
379
513
|
)
|
|
@@ -397,7 +531,7 @@ def from_yaml(path: str) -> "Config":
|
|
|
397
531
|
|
|
398
532
|
Empty/unset variables in lists are automatically filtered out.
|
|
399
533
|
"""
|
|
400
|
-
from brawny.config.models import AdvancedConfig, Config, RPCGroupConfig
|
|
534
|
+
from brawny.config.models import AdvancedConfig, Config, IntentCooldownConfig, RPCGroupConfig
|
|
401
535
|
from brawny.config.validation import (
|
|
402
536
|
canonicalize_endpoint,
|
|
403
537
|
dedupe_preserve_order,
|
|
@@ -431,6 +565,24 @@ def from_yaml(path: str) -> "Config":
|
|
|
431
565
|
|
|
432
566
|
validate_no_removed_fields(data)
|
|
433
567
|
|
|
568
|
+
guardrails_data: dict[str, object] = {}
|
|
569
|
+
if "guardrails" in data:
|
|
570
|
+
guardrails_value = data.pop("guardrails")
|
|
571
|
+
if guardrails_value is None:
|
|
572
|
+
guardrails_value = {}
|
|
573
|
+
if not isinstance(guardrails_value, dict):
|
|
574
|
+
raise ConfigError("guardrails must be a mapping")
|
|
575
|
+
guardrails_data = dict(guardrails_value)
|
|
576
|
+
|
|
577
|
+
debug_data: dict[str, object] = {}
|
|
578
|
+
if "debug" in data:
|
|
579
|
+
debug_value = data.pop("debug")
|
|
580
|
+
if debug_value is None:
|
|
581
|
+
debug_value = {}
|
|
582
|
+
if not isinstance(debug_value, dict):
|
|
583
|
+
raise ConfigError("debug must be a mapping")
|
|
584
|
+
debug_data = dict(debug_value)
|
|
585
|
+
|
|
434
586
|
advanced_data: dict[str, object] = {}
|
|
435
587
|
if "advanced" in data:
|
|
436
588
|
advanced_value = data.pop("advanced")
|
|
@@ -499,14 +651,28 @@ def from_yaml(path: str) -> "Config":
|
|
|
499
651
|
if unknown:
|
|
500
652
|
raise ConfigError(f"Unknown advanced config fields: {sorted(unknown)}")
|
|
501
653
|
data["advanced"] = AdvancedConfig(**advanced_data)
|
|
654
|
+
if guardrails_data:
|
|
655
|
+
data["guardrails"] = guardrails_data
|
|
502
656
|
|
|
503
657
|
# Parse telegram config (handles both new and legacy formats)
|
|
504
658
|
telegram_config = _parse_telegram(data)
|
|
659
|
+
http_config = _parse_http(data)
|
|
660
|
+
guardrails_config = _parse_guardrails(data)
|
|
661
|
+
debug_config = _parse_debug(data)
|
|
662
|
+
intent_cooldown_config = _parse_intent_cooldown(data)
|
|
505
663
|
# Remove raw telegram fields - they've been canonicalized
|
|
506
664
|
data.pop("telegram", None)
|
|
507
665
|
data.pop("telegram_bot_token", None)
|
|
508
666
|
data.pop("telegram_chat_id", None)
|
|
509
667
|
data["telegram"] = telegram_config
|
|
668
|
+
data.pop("http", None)
|
|
669
|
+
data["http"] = http_config
|
|
670
|
+
data.pop("guardrails", None)
|
|
671
|
+
data["guardrails"] = guardrails_config
|
|
672
|
+
data.pop("debug", None)
|
|
673
|
+
data["debug"] = debug_config
|
|
674
|
+
data.pop("intent_cooldown", None)
|
|
675
|
+
data["intent_cooldown"] = intent_cooldown_config
|
|
510
676
|
|
|
511
677
|
config = Config(**data)
|
|
512
678
|
config.validate()
|
|
@@ -515,13 +681,21 @@ def from_yaml(path: str) -> "Config":
|
|
|
515
681
|
|
|
516
682
|
def apply_env_overrides(config: "Config") -> tuple["Config", list[str]]:
|
|
517
683
|
"""Apply environment overrides to the current config."""
|
|
518
|
-
from brawny.config.models import
|
|
684
|
+
from brawny.config.models import (
|
|
685
|
+
AdvancedConfig,
|
|
686
|
+
Config,
|
|
687
|
+
GuardrailsConfig,
|
|
688
|
+
RPCGroupConfig,
|
|
689
|
+
TelegramConfig,
|
|
690
|
+
)
|
|
691
|
+
from brawny.http import HttpConfig
|
|
519
692
|
from brawny.config.validation import canonicalize_endpoint, dedupe_preserve_order, InvalidEndpointError
|
|
520
693
|
|
|
521
694
|
_fail_removed_env_vars()
|
|
522
695
|
|
|
523
696
|
overrides: dict[str, object] = {}
|
|
524
697
|
advanced_overrides: dict[str, object] = {}
|
|
698
|
+
guardrails_overrides: dict[str, object] = {}
|
|
525
699
|
overridden: list[str] = []
|
|
526
700
|
|
|
527
701
|
mapping = {
|
|
@@ -573,29 +747,63 @@ def apply_env_overrides(config: "Config") -> tuple["Config", list[str]]:
|
|
|
573
747
|
if telegram_chat_override is not None:
|
|
574
748
|
overridden.append("telegram.chat_id")
|
|
575
749
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
750
|
+
# Handle http env overrides
|
|
751
|
+
if (
|
|
752
|
+
_env_is_set("HTTP_ALLOWED_DOMAINS")
|
|
753
|
+
or _env_is_set("HTTP_CONNECT_TIMEOUT_SECONDS")
|
|
754
|
+
or _env_is_set("HTTP_READ_TIMEOUT_SECONDS")
|
|
755
|
+
or _env_is_set("HTTP_MAX_RETRIES")
|
|
756
|
+
or _env_is_set("HTTP_BACKOFF_BASE_SECONDS")
|
|
757
|
+
):
|
|
758
|
+
base_http = config.http
|
|
759
|
+
allowed_raw = _get_env("HTTP_ALLOWED_DOMAINS")
|
|
760
|
+
allowed_domains = (
|
|
761
|
+
[d.strip() for d in allowed_raw.split(",") if d.strip()] if allowed_raw is not None else base_http.allowed_domains
|
|
762
|
+
)
|
|
763
|
+
connect_timeout_seconds = (
|
|
764
|
+
_parse_env_float("HTTP_CONNECT_TIMEOUT_SECONDS")
|
|
765
|
+
if _env_is_set("HTTP_CONNECT_TIMEOUT_SECONDS")
|
|
766
|
+
else base_http.connect_timeout_seconds
|
|
767
|
+
)
|
|
768
|
+
read_timeout_seconds = (
|
|
769
|
+
_parse_env_float("HTTP_READ_TIMEOUT_SECONDS")
|
|
770
|
+
if _env_is_set("HTTP_READ_TIMEOUT_SECONDS")
|
|
771
|
+
else base_http.read_timeout_seconds
|
|
772
|
+
)
|
|
773
|
+
max_retries = (
|
|
774
|
+
_parse_env_int("HTTP_MAX_RETRIES")
|
|
775
|
+
if _env_is_set("HTTP_MAX_RETRIES")
|
|
776
|
+
else base_http.max_retries
|
|
777
|
+
)
|
|
778
|
+
backoff_base_seconds = (
|
|
779
|
+
_parse_env_float("HTTP_BACKOFF_BASE_SECONDS")
|
|
780
|
+
if _env_is_set("HTTP_BACKOFF_BASE_SECONDS")
|
|
781
|
+
else base_http.backoff_base_seconds
|
|
782
|
+
)
|
|
783
|
+
overrides["http"] = HttpConfig(
|
|
784
|
+
allowed_domains=allowed_domains,
|
|
785
|
+
connect_timeout_seconds=connect_timeout_seconds,
|
|
786
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
787
|
+
max_retries=max_retries,
|
|
788
|
+
backoff_base_seconds=backoff_base_seconds,
|
|
789
|
+
)
|
|
790
|
+
if _env_is_set("HTTP_ALLOWED_DOMAINS"):
|
|
791
|
+
overridden.append("http.allowed_domains")
|
|
792
|
+
if _env_is_set("HTTP_CONNECT_TIMEOUT_SECONDS"):
|
|
793
|
+
overridden.append("http.connect_timeout_seconds")
|
|
794
|
+
if _env_is_set("HTTP_READ_TIMEOUT_SECONDS"):
|
|
795
|
+
overridden.append("http.read_timeout_seconds")
|
|
796
|
+
if _env_is_set("HTTP_MAX_RETRIES"):
|
|
797
|
+
overridden.append("http.max_retries")
|
|
798
|
+
if _env_is_set("HTTP_BACKOFF_BASE_SECONDS"):
|
|
799
|
+
overridden.append("http.backoff_base_seconds")
|
|
800
|
+
|
|
801
|
+
if _env_is_set("GUARDRAILS_LINT_PATHS"):
|
|
802
|
+
guardrails_overrides["lint_paths"] = _get_env_list("GUARDRAILS_LINT_PATHS")
|
|
803
|
+
overridden.append("guardrails.lint_paths")
|
|
804
|
+
|
|
805
|
+
advanced_overrides, advanced_overridden = _get_advanced_env_overrides_with_keys()
|
|
806
|
+
overridden.extend(advanced_overridden)
|
|
599
807
|
|
|
600
808
|
rpc_endpoints_override: list[str] | None = None
|
|
601
809
|
if _env_is_set("RPC_ENDPOINTS"):
|
|
@@ -617,6 +825,9 @@ def apply_env_overrides(config: "Config") -> tuple["Config", list[str]]:
|
|
|
617
825
|
if advanced_overrides:
|
|
618
826
|
base_advanced = config.advanced or AdvancedConfig()
|
|
619
827
|
overrides["advanced"] = replace(base_advanced, **advanced_overrides)
|
|
828
|
+
if guardrails_overrides:
|
|
829
|
+
base_guardrails = config.guardrails or GuardrailsConfig()
|
|
830
|
+
overrides["guardrails"] = replace(base_guardrails, **guardrails_overrides)
|
|
620
831
|
|
|
621
832
|
if rpc_endpoints_override is not None:
|
|
622
833
|
default_group = overrides.get("rpc_default_group") or config.rpc_default_group or "primary"
|