For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a secure, lazy, per-data-source connection pool framework for PostgreSQL and MySQL while keeping the platform PostgreSQL connection path separate.
Architecture: Flask-SQLAlchemy continues to own only the platform control database. External DataSource definitions are resolved through a DataSourceConnectionManager, which loads encrypted credentials from PostgreSQL, selects an allowlisted adapter, and acquires a connection from a worker-local lazy pool registry. Credential/configuration version changes invalidate old pools; per-source circuit breakers and diagnostics isolate failures.
Tech Stack: Flask 2.3, Flask-SQLAlchemy 3.1, SQLAlchemy 2.0 QueuePool/NullPool, PostgreSQL, MySQL 8, Neo4j, AES-256-GCM from cryptography, psycopg2, PyMySQL, Alembic, pytest, Docker Compose.
| File | Responsibility |
|---|---|
app/core/data_source/errors.py |
Stable data-source error classes and safe API codes |
app/core/data_source/redaction.py |
Recursive request and exception redaction |
app/core/data_source/models.py |
Immutable definitions, credentials and pool-status value objects |
app/core/data_source/credentials.py |
AES-GCM codec and PostgreSQL credential repository |
app/core/data_source/definitions.py |
Neo4j DataSource definition repository without secrets |
app/core/data_source/adapters/base.py |
Adapter protocol and allowlisted common options |
app/core/data_source/adapters/postgresql.py |
PostgreSQL Engine and connection probe |
app/core/data_source/adapters/mysql.py |
MySQL Engine and connection probe |
app/core/data_source/circuit_breaker.py |
Per-source closed/open/half-open state machine |
app/core/data_source/pool_registry.py |
Worker-local Engine registry, leases, draining and idle eviction |
app/core/data_source/manager.py |
Public connect(uid, purpose) context manager |
app/core/data_source/runtime.py |
One manager instance per Gunicorn worker |
app/core/data_source/service.py |
Save/update/delete/test orchestration across PostgreSQL and Neo4j |
app/api/data_source/routes.py |
HTTP boundary; no direct Engine or credential access |
app/commands/report_datasource_credentials.py |
Read-only legacy credential audit |
app/commands/migrate_datasource_credentials.py |
Explicit encryption/backfill and plaintext removal |
app/commands/reconcile_datasource_credentials.py |
Cross-store orphan/stale reference report and repair |
migrations/versions/20260718_50_datasource_credentials.py |
Encrypted credential and audit schema |
tests/core/data_source/ |
Unit and contract tests |
tests/integration/test_datasource_pools.py |
PostgreSQL/MySQL pool integration |
tests/integration/test_datasource_pool_failures.py |
Outage, recovery and invalidation acceptance |
frontend/src/views/dataGovernance/dataSource/ |
Secret-free PostgreSQL/MySQL management UI |
Files:
requirements.txtapp/config/config.pydeploy/docker/.env.exampledeployment/dataops.envTest: tests/core/data_source/test_config.py
[ ] Step 1: Write the failing configuration test
def test_datasource_pool_defaults_are_bounded(monkeypatch):
monkeypatch.delenv("DATASOURCE_POOL_SIZE", raising=False)
monkeypatch.delenv("DATASOURCE_MAX_OVERFLOW", raising=False)
from app.config.config import BaseConfig
assert BaseConfig.DATASOURCE_POOL_SIZE == 2
assert BaseConfig.DATASOURCE_MAX_OVERFLOW == 3
assert BaseConfig.DATASOURCE_POOL_TIMEOUT == 10
assert BaseConfig.DATASOURCE_POOL_RECYCLE == 1800
assert BaseConfig.DATASOURCE_POOL_IDLE_TTL == 900
assert BaseConfig.DATASOURCE_MAX_IDLE_POOLS == 20
assert BaseConfig.DATASOURCE_QUERY_TIMEOUT == 30
def test_pool_limits_reject_unsafe_values(monkeypatch):
monkeypatch.setenv("DATASOURCE_POOL_SIZE", "100")
from app.config.config import datasource_pool_settings
with pytest.raises(ValueError, match="DATASOURCE_POOL_SIZE"):
datasource_pool_settings()
def test_datasource_override_is_clamped_to_server_limits():
settings = datasource_pool_settings(
{"pool_size": 4, "max_overflow": 5}
)
assert settings["pool_size"] == 4
assert settings["max_overflow"] == 5
with pytest.raises(ValueError, match="pool_size"):
datasource_pool_settings({"pool_size": 11})
Run:
.venv/bin/python -m pytest tests/core/data_source/test_config.py -q
Expected: FAIL because the pool settings do not exist.
Add:
cryptography==43.0.3
PyMySQL==1.1.1
Add to app/config/config.py:
def _bounded_int(name: str, default: int, minimum: int, maximum: int) -> int:
value = int(os.environ.get(name, str(default)))
if value < minimum or value > maximum:
raise ValueError(f"{name} must be between {minimum} and {maximum}")
return value
def datasource_pool_settings(overrides: Mapping[str, int] | None = None) -> dict:
settings = {
"pool_size": _bounded_int("DATASOURCE_POOL_SIZE", 2, 1, 10),
"max_overflow": _bounded_int("DATASOURCE_MAX_OVERFLOW", 3, 0, 10),
"pool_timeout": _bounded_int("DATASOURCE_POOL_TIMEOUT", 10, 1, 60),
"pool_recycle": _bounded_int("DATASOURCE_POOL_RECYCLE", 1800, 60, 86400),
"idle_ttl": _bounded_int("DATASOURCE_POOL_IDLE_TTL", 900, 60, 86400),
"max_idle_pools": _bounded_int("DATASOURCE_MAX_IDLE_POOLS", 20, 1, 100),
"query_timeout": _bounded_int("DATASOURCE_QUERY_TIMEOUT", 30, 1, 300),
}
return apply_allowlisted_pool_overrides(settings, overrides or {})
Expose the same values on BaseConfig, plus:
DATASOURCE_CREDENTIAL_MASTER_KEY = os.environ.get(
"DATASOURCE_CREDENTIAL_MASTER_KEY", ""
)
DATASOURCE_CREDENTIAL_KEY_VERSION = os.environ.get(
"DATASOURCE_CREDENTIAL_KEY_VERSION", "v1"
)
DATASOURCE_CERT_DIR = os.environ.get(
"DATASOURCE_CERT_DIR", "/etc/dataops-platform/datasource-certs"
)
Only pool_size and max_overflow may be overridden per data source in phase
one. Reject unknown keys and values outside the same server-side bounds; never
accept pool_timeout, driver names or raw connect_args from an API request.
Add template values:
DATASOURCE_CREDENTIAL_MASTER_KEY=replace-with-base64-32-byte-key
DATASOURCE_CREDENTIAL_KEY_VERSION=v1
DATASOURCE_POOL_SIZE=2
DATASOURCE_MAX_OVERFLOW=3
DATASOURCE_POOL_TIMEOUT=10
DATASOURCE_POOL_RECYCLE=1800
DATASOURCE_POOL_IDLE_TTL=900
DATASOURCE_MAX_IDLE_POOLS=20
DATASOURCE_QUERY_TIMEOUT=30
DATASOURCE_CERT_DIR=/etc/dataops-platform/datasource-certs
Run:
.venv/bin/python -m pytest tests/core/data_source/test_config.py -q
Expected: PASS.
git add requirements.txt app/config/config.py deploy/docker/.env.example \
deployment/dataops.env tests/core/data_source/test_config.py
git commit -m "feat: define external datasource pool settings"
Files:
migrations/versions/20260718_50_datasource_credentials.pytests/core/data_source/test_credential_schema.pyModify: tests/test_database_migrations.py
[ ] Step 1: Write the failing migration contract test
def test_datasource_credential_migration_is_encrypted_and_versioned():
source = Path(
"migrations/versions/20260718_50_datasource_credentials.py"
).read_text(encoding="utf-8")
assert "datasource_credentials" in source
assert "encrypted_payload" in source
assert "nonce" in source
assert "key_version" in source
assert "credential_version" in source
assert "UNIQUE (data_source_uid, credential_version)" in source
assert "password VARCHAR" not in source
.venv/bin/python -m pytest \
tests/core/data_source/test_credential_schema.py -q
Expected: FAIL because the migration does not exist.
Use down_revision = "20260716_40" and create:
CREATE TABLE IF NOT EXISTS public.datasource_credentials (
id UUID PRIMARY KEY,
data_source_uid UUID NOT NULL,
credential_version INTEGER NOT NULL CHECK (credential_version > 0),
encrypted_payload BYTEA NOT NULL,
nonce BYTEA NOT NULL CHECK (octet_length(nonce) = 12),
key_version VARCHAR(40) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'retired', 'revoked')),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
retired_at TIMESTAMPTZ,
UNIQUE (data_source_uid, credential_version)
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_datasource_credential_active
ON public.datasource_credentials(data_source_uid)
WHERE status = 'active';
Also create datasource_credential_audit_events with event type, UID, version,
actor UID, success flag, safe detail and timestamp. Do not create password,
connection URL or raw request columns.
Keep downgrade() data-preserving, matching the current migration policy.
Add datasource_credentials and datasource_credential_audit_events to the
post-upgrade expected table set, not to the original baseline migration set.
TEST_POSTGRES_ADMIN_URL=postgresql://dataops:dataops-test-password@localhost:15432/postgres \
.venv/bin/python -m pytest \
tests/core/data_source/test_credential_schema.py \
tests/test_database_migrations.py -q
Expected: all tests PASS; temporary database is dropped.
git add migrations/versions/20260718_50_datasource_credentials.py \
tests/core/data_source/test_credential_schema.py \
tests/test_database_migrations.py
git commit -m "feat: add encrypted datasource credentials schema"
Files:
app/core/data_source/__init__.pyapp/core/data_source/errors.pyapp/core/data_source/models.pyapp/core/data_source/credentials.pyTest: tests/core/data_source/test_credentials.py
[ ] Step 1: Write failing codec tests
def test_credential_round_trip_and_tamper_detection():
codec = CredentialCodec(
master_key=b"k" * 32,
key_version="v1",
)
sealed = codec.encrypt(
data_source_uid="01900000-0000-7000-8000-000000000010",
credential_version=1,
credential=DataSourceCredential("reader", "secret"),
)
assert b"secret" not in sealed.encrypted_payload
assert codec.decrypt(sealed) == DataSourceCredential("reader", "secret")
tampered = dataclasses.replace(
sealed,
encrypted_payload=sealed.encrypted_payload[:-1] + b"x",
)
with pytest.raises(DataSourceCredentialUnavailable):
codec.decrypt(tampered)
def test_credential_cannot_be_replayed_for_another_datasource():
sealed = codec.encrypt(DATA_SOURCE_A, 1, credential)
replayed = dataclasses.replace(sealed, data_source_uid=DATA_SOURCE_B)
with pytest.raises(DataSourceCredentialUnavailable):
codec.decrypt(replayed)
.venv/bin/python -m pytest \
tests/core/data_source/test_credentials.py -q
Expected: FAIL with missing modules.
@dataclass(frozen=True)
class DataSourceCredential:
username: str
password: str
options: Mapping[str, str] = field(default_factory=dict)
@dataclass(frozen=True)
class SealedCredential:
id: str
data_source_uid: str
credential_version: int
encrypted_payload: bytes
nonce: bytes
key_version: str
status: str = "active"
Define DataSourceError subclasses for every stable phase-one code:
class DataSourceCredentialUnavailable(DataSourceError):
code = "DATASOURCE_CREDENTIAL_UNAVAILABLE"
http_status = 503
The complete set is DATASOURCE_NOT_FOUND,
DATASOURCE_TYPE_UNSUPPORTED, DATASOURCE_CONFIGURATION_INVALID,
DATASOURCE_CREDENTIAL_UNAVAILABLE, DATASOURCE_CONNECTION_FAILED,
DATASOURCE_POOL_TIMEOUT, DATASOURCE_CIRCUIT_OPEN,
DATASOURCE_QUERY_TIMEOUT, and DATASOURCE_READ_ONLY_VIOLATION. Safe public
messages must not include DBAPI exception text.
Use:
aad = f"{data_source_uid}:{credential_version}".encode("utf-8")
nonce = os.urandom(12)
payload = json.dumps(
{
"username": credential.username,
"password": credential.password,
"options": dict(credential.options),
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
ciphertext = AESGCM(master_key).encrypt(nonce, payload, aad)
Decode the environment key with base64.urlsafe_b64decode; require exactly
32 bytes. Convert InvalidTag, malformed JSON and key mismatch to
DataSourceCredentialUnavailable("credential cannot be decrypted").
Repository methods accept a SQLAlchemy Session and use parameterized text().
create_version() takes an advisory transaction lock on data_source_uid,
increments the version and retires the prior active row in the same transaction.
.venv/bin/python -m pytest \
tests/core/data_source/test_credentials.py -q
Expected: PASS.
git add app/core/data_source tests/core/data_source/test_credentials.py
git commit -m "feat: encrypt external datasource credentials"
Files:
app/core/data_source/redaction.pyapp/api/data_source/routes.pytests/core/data_source/test_redaction.pyTest: tests/test_datasource_api_security.py
[ ] Step 1: Write failing redaction tests
def test_recursive_redaction_covers_connection_secrets():
value = {
"name": "warehouse",
"password": "plain",
"options": {
"api_key": "key",
"url": "postgresql://user:secret@db:5432/app",
},
}
assert redact_mapping(value) == {
"name": "warehouse",
"password": "[redacted]",
"options": {
"api_key": "[redacted]",
"url": "postgresql://[redacted]@db:5432/app",
},
}
def test_datasource_list_never_serializes_credentials(client, viewer_token):
response = client.post(
"/api/datasource/list",
json={},
headers=bearer(viewer_token),
)
text = response.get_data(as_text=True).lower()
assert "password" not in text
assert "encrypted_payload" not in text
assert "credential_configured" in text
.venv/bin/python -m pytest \
tests/core/data_source/test_redaction.py \
tests/test_datasource_api_security.py -q
Expected: FAIL because request logging and serialization expose sensitive keys.
Redact case-insensitive keys:
SENSITIVE_KEYS = {
"password",
"passwd",
"credential",
"credentials",
"api_key",
"token",
"authorization",
"encrypted_payload",
"nonce",
"conn_str",
"connection_string",
}
Redact URL userinfo using urllib.parse.urlsplit() and return a URL containing
[redacted]@host. Truncate sanitized exception messages to 1000 characters.
Replace every pattern like:
logger.debug("保存数据源请求数据: %s", json.dumps(data))
with:
logger.debug("保存数据源请求: %s", redact_mapping(data or {}))
The list response must strip username, password, credential_ref,
encrypted_payload and nonce; expose only:
item["credential_configured"] = bool(item.pop("credential_version", None))
.venv/bin/python -m pytest \
tests/core/data_source/test_redaction.py \
tests/test_datasource_api_security.py -q
rg -n "json\\.dumps\\(data|log_data.*password|RETURN n.*password" \
app/api/data_source app/core/data_source
Expected: tests PASS and rg returns no unsafe logging pattern.
git add app/core/data_source/redaction.py app/api/data_source/routes.py \
tests/core/data_source/test_redaction.py \
tests/test_datasource_api_security.py
git commit -m "fix: prevent datasource credential disclosure"
Files:
app/core/data_source/definitions.pyTest: tests/core/data_source/test_definitions.py
[ ] Step 1: Write failing repository tests
def test_definition_repository_assigns_uid_and_excludes_secrets(fake_graph):
repository = DataSourceDefinitionRepository(fake_graph)
saved = repository.save(
DataSourceDefinition(
uid=None,
name_en="warehouse",
database_type="postgresql",
host="source-postgres",
port=5432,
database="analytics",
schema="public",
credential_ref="01900000-0000-7000-8000-000000000011",
credential_version=1,
)
)
assert uuid.UUID(saved.uid).version == 7
assert "password" not in fake_graph.last_parameters["properties"]
assert "username" not in fake_graph.last_parameters["properties"]
.venv/bin/python -m pytest \
tests/core/data_source/test_definitions.py -q
Expected: FAIL because the repository does not exist.
Support:
get(uid: str) -> DataSourceDefinition | None
list(filters: DataSourceFilters) -> list[DataSourceDefinition]
save(definition: DataSourceDefinition) -> DataSourceDefinition
delete(uid: str) -> bool
credential_references() -> dict[str, tuple[str, int]]
remove_legacy_secret_fields(uid: str) -> None
Use uid, not Neo4j internal numeric IDs, for new writes and updates. Keep a
temporary numeric-ID lookup only inside the migration command.
Extend app/commands/reconcile_governance_uids.py with:
CREATE CONSTRAINT datasource_uid_unique IF NOT EXISTS
FOR (node:DataSource) REQUIRE node.uid IS UNIQUE
.venv/bin/python -m pytest \
tests/core/data_source/test_definitions.py \
tests/test_governance_identifiers.py -q
Expected: PASS.
git add app/core/data_source/definitions.py \
app/commands/reconcile_governance_uids.py \
tests/core/data_source/test_definitions.py \
tests/test_governance_identifiers.py
git commit -m "feat: add secret-free datasource definitions"
Files:
app/core/data_source/adapters/__init__.pyapp/core/data_source/adapters/base.pyapp/core/data_source/adapters/postgresql.pyapp/core/data_source/adapters/mysql.pyTest: tests/core/data_source/test_adapters.py
[ ] Step 1: Write failing adapter tests
@pytest.mark.parametrize(
("database_type", "drivername"),
[
("postgresql", "postgresql+psycopg2"),
("mysql", "mysql+pymysql"),
],
)
def test_adapter_uses_allowlisted_driver(database_type, drivername):
adapter = adapter_for(database_type)
url = adapter.build_url(definition, credential)
assert url.drivername == drivername
assert url.password == "secret"
def test_unknown_driver_is_rejected():
with pytest.raises(DataSourceTypeUnsupported):
adapter_for("sqlite")
def test_adapter_rejects_arbitrary_connection_options():
with pytest.raises(DataSourceConfigurationInvalid):
adapter.validate_options({"driver": "custom", "connect_args": {}})
def test_read_purpose_configures_database_read_only_transaction():
adapter.configure_transaction(connection, purpose="metadata_preview")
assert connection.executed == ["SET TRANSACTION READ ONLY"]
def test_connection_test_uses_null_pool_and_disposes(monkeypatch):
engine = FakeEngine()
monkeypatch.setattr(base, "create_engine", lambda *a, **kw: engine)
adapter.test_connection(definition, credential)
assert engine.pool_class is NullPool
assert engine.disposed is True
.venv/bin/python -m pytest \
tests/core/data_source/test_adapters.py -q
Expected: FAIL with missing adapter modules.
class DataSourceAdapter(Protocol):
database_type: str
def build_url(
self,
definition: DataSourceDefinition,
credential: DataSourceCredential,
) -> URL: ...
def connect_args(
self,
definition: DataSourceDefinition,
credential: DataSourceCredential,
) -> dict[str, object]: ...
def validate_options(self, options: Mapping[str, object]) -> None: ...
def configure_transaction(
self,
connection: Connection,
purpose: str,
) -> None: ...
def probe(self, connection: Connection) -> None: ...
PostgreSQL returns:
{
"connect_timeout": 5,
"keepalives": 1,
"keepalives_idle": 30,
"keepalives_interval": 10,
"keepalives_count": 3,
"application_name": "dataops-platform-datasource",
"options": "-c statement_timeout=30000",
}
MySQL returns:
{
"connect_timeout": 5,
"read_timeout": 30,
"write_timeout": 30,
"charset": "utf8mb4",
}
Both execute text("SELECT 1") in probe().
Build the PostgreSQL statement_timeout value from the validated
query_timeout; keep it platform-controlled. MySQL uses the same bounded value
for read_timeout and write_timeout.
Allow only documented TLS settings: PostgreSQL sslmode, sslrootcert,
sslcert, and sslkey; MySQL ssl_ca, ssl_cert, and ssl_key. File paths
must resolve under the configured certificate directory. Reject unknown option
keys, URL fragments, frontend driver names, and raw connect_args.
For every purpose except dataflow_write, configure the database transaction
as read-only before yielding the connection. PostgreSQL executes
SET TRANSACTION READ ONLY; MySQL executes the equivalent supported by MySQL
DATASOURCE_READ_ONLY_VIOLATION. Never retry or replay write transactions.engine = create_engine(
adapter.build_url(definition, credential),
poolclass=NullPool,
connect_args=adapter.connect_args(definition, credential),
)
try:
with engine.connect() as connection:
adapter.probe(connection)
finally:
engine.dispose()
.venv/bin/python -m pytest \
tests/core/data_source/test_adapters.py -q
Expected: PASS.
git add app/core/data_source/adapters \
tests/core/data_source/test_adapters.py requirements.txt
git commit -m "feat: add PostgreSQL and MySQL datasource adapters"
Files:
app/core/data_source/circuit_breaker.pyTest: tests/core/data_source/test_circuit_breaker.py
[ ] Step 1: Write failing state-machine tests
def test_breaker_opens_after_three_failures_and_recovers():
clock = FakeClock()
breaker = CircuitBreaker(
failure_threshold=3,
recovery_seconds=30,
clock=clock,
)
breaker.record_failure()
breaker.record_failure()
breaker.record_failure()
assert breaker.state == "open"
with pytest.raises(DataSourceCircuitOpen):
with breaker.probe_permission():
pass
clock.advance(30)
with breaker.probe_permission():
breaker.record_success()
assert breaker.state == "closed"
.venv/bin/python -m pytest \
tests/core/data_source/test_circuit_breaker.py -q
Expected: FAIL because CircuitBreaker is undefined.
Use a threading.Lock. Only one caller may own the half-open probe. A normal
query failure does not open the breaker; only connection creation, checkout
disconnect and probe failures count.
.venv/bin/python -m pytest \
tests/core/data_source/test_circuit_breaker.py -q
Expected: PASS, including concurrent half-open callers.
git add app/core/data_source/circuit_breaker.py \
tests/core/data_source/test_circuit_breaker.py
git commit -m "feat: isolate datasource failures with circuit breakers"
Files:
app/core/data_source/pool_registry.pyTest: tests/core/data_source/test_pool_registry.py
[ ] Step 1: Write failing reuse, drain and eviction tests
def test_registry_reuses_same_engine_and_invalidates_new_version():
registry = PoolRegistry(settings)
first = PoolKey(DATA_SOURCE_UID, 1, "hash-a")
second = PoolKey(DATA_SOURCE_UID, 2, "hash-b")
with registry.lease(first, lambda: engine_factory(first)) as engine_a:
with registry.lease(first, lambda: engine_factory(first)) as engine_b:
assert engine_a is engine_b
with registry.lease(second, lambda: engine_factory(second)) as engine_c:
assert engine_c is not engine_a
assert engine_a.disposed is True
def test_idle_pool_is_evicted_only_after_last_lease_returns():
with registry.lease(key, lambda: engine):
clock.advance(901)
registry.evict_idle()
assert engine.disposed is False
registry.evict_idle()
assert engine.disposed is True
.venv/bin/python -m pytest \
tests/core/data_source/test_pool_registry.py -q
Expected: FAIL because the registry does not exist.
@dataclass
class PoolEntry:
key: PoolKey
engine: Engine
breaker: CircuitBreaker
created_at: float
last_used_at: float
leases: int = 0
draining: bool = False
checkout_wait_ms: float = 0.0
Protect the entry map with threading.RLock. Do not hold the registry lock
while executing SQL. Create one Engine per key using:
create_engine(
url,
pool_size=settings.pool_size,
max_overflow=settings.max_overflow,
pool_timeout=settings.pool_timeout,
pool_recycle=settings.pool_recycle,
pool_pre_ping=True,
pool_use_lifo=True,
pool_reset_on_return="rollback",
connect_args=connect_args,
)
Resolve settings from the server defaults plus the validated per-source
pool_size and max_overflow override. A lease for a new key must mark all
older keys for the same UID as draining.
Implement:
invalidate(data_source_uid: str, reason: str) -> None
expire_draining(now: float | None = None) -> int
evict_idle(now: float | None = None) -> int
close_all() -> None
snapshot() -> list[PoolStatus]
When the idle-entry count exceeds max_idle_pools, dispose least-recently-used
idle entries first. invalidate() immediately blocks new leases for the old
key, lets existing leases drain for a bounded grace period, then
expire_draining() force-disposes the Engine. Returning a lease after forced
disposal must not re-register the old Engine.
Track connect, checkout, checkin and invalidate. Store counts only;
never inspect or log DBAPI connection arguments.
.venv/bin/python -m pytest \
tests/core/data_source/test_pool_registry.py -q
Expected: PASS, including a 50-thread lease/release test with no duplicate Engine for the same key and no leaked leases.
git add app/core/data_source/pool_registry.py \
tests/core/data_source/test_pool_registry.py
git commit -m "feat: add lazy worker-local datasource pool registry"
Files:
app/core/data_source/manager.pyapp/core/data_source/runtime.pytests/core/data_source/test_manager.pyModify: gunicorn_config.py
[ ] Step 1: Write failing manager tests
def test_manager_resolves_definition_credential_and_adapter():
with manager.connect(DATA_SOURCE_UID, purpose="metadata_preview") as connection:
connection.execute(text("SELECT 1"))
assert definitions.requested_uid == DATA_SOURCE_UID
assert credentials.requested_version == 4
assert adapters.requested_type == "postgresql"
assert registry.last_key.credential_version == 4
def test_write_requires_explicit_write_purpose(manager, fake_engine):
with manager.connect(DATA_SOURCE_UID, purpose="metadata_preview"):
pass
read_transaction = fake_engine.transactions[-1]
assert read_transaction.rolled_back is True
assert read_transaction.committed is False
with manager.connect(DATA_SOURCE_UID, purpose="dataflow_write"):
pass
write_transaction = fake_engine.transactions[-1]
assert write_transaction.committed is True
.venv/bin/python -m pytest \
tests/core/data_source/test_manager.py -q
Expected: FAIL because DataSourceConnectionManager does not exist.
@contextmanager
def connect(self, data_source_uid: str, purpose: str):
definition = self.definitions.get(data_source_uid)
if definition is None:
raise DataSourceNotFound(data_source_uid)
credential = self.credentials.get_active(
data_source_uid,
definition.credential_version,
)
adapter = adapter_for(definition.database_type)
key = PoolKey(
data_source_uid=data_source_uid,
credential_version=definition.credential_version,
config_fingerprint=definition.connection_fingerprint(),
)
with self.registry.lease(
key,
lambda: adapter.create_pooled_engine(definition, credential, self.settings),
) as engine:
with engine.connect() as connection:
transaction = connection.begin()
try:
adapter.configure_transaction(connection, purpose)
yield ManagedConnection(connection, purpose)
if purpose == "dataflow_write":
transaction.commit()
else:
transaction.rollback()
except Exception:
transaction.rollback()
raise
ManagedConnection.execute() records query duration and translates known
driver timeout and read-only exceptions to the stable safe errors. Measure
checkout wait around engine.connect(). It must not retry statements or
transactions.
runtime.py lazily constructs one manager in a process-local variable guarded
by a lock. Add:
def close_data_source_runtime() -> None:
manager = _manager
if manager is not None:
manager.close()
Call it from Gunicorn worker_exit, not from every Flask request.
.venv/bin/python -m pytest \
tests/core/data_source/test_manager.py -q
Expected: PASS.
git add app/core/data_source/manager.py app/core/data_source/runtime.py \
tests/core/data_source/test_manager.py gunicorn_config.py
git commit -m "feat: expose managed datasource connections"
Files:
app/core/data_source/service.pyapp/api/data_source/routes.pyapp/core/system/permissions.pyTest: tests/test_datasource_lifecycle_api.py
[ ] Step 1: Write failing API lifecycle tests
def test_create_stores_secret_only_in_credential_store(
client, editor_token, fake_graph, credential_repository
):
response = client.post(
"/api/datasource/save",
json=POSTGRES_SOURCE_PAYLOAD,
headers=bearer(editor_token),
)
assert response.status_code == 201
graph_properties = fake_graph.saved_properties
assert "password" not in graph_properties
assert "username" not in graph_properties
assert graph_properties["credential_version"] == 1
assert credential_repository.decrypt_latest().password == "source-pass"
def test_update_invalidates_old_pool(client, editor_token, pool_registry):
response = client.post(
"/api/datasource/save",
json={**UPDATED_PAYLOAD, "uid": DATA_SOURCE_UID},
headers=bearer(editor_token),
)
assert response.status_code == 200
assert pool_registry.invalidations == [
(DATA_SOURCE_UID, "configuration_changed")
]
.venv/bin/python -m pytest \
tests/test_datasource_lifecycle_api.py -q
Expected: FAIL because routes still write request properties directly to Neo4j.
DataSourceService.save() must:
postgresql and mysql.NullPool.datasource.credential_version_created with safe definition fields.On Neo4j failure, retain the prior definition and record the newly created credential as an orphan candidate for reconciliation.
For an existing source, omitted username/password means “reuse the active credential internally”: decrypt it only inside the service, combine any explicit replacement fields, and write the next immutable credential version. Never send the old credential back to the frontend. Per-source pool overrides and TLS fields pass through the server allowlists before connection testing.
Order:
registry.invalidate(uid, reason="datasource_deleted")
definitions.delete(uid)
credentials.revoke_all(uid, actor_id)
enqueue_outbox(..., event_type="datasource.deleted", payload={"uid": uid})
If PostgreSQL finalization fails, the reconciliation command must detect a deleted graph definition with active credentials.
Routes call DataSourceService only. Remove imports of create_engine and
URL from app/api/data_source/routes.py. Use real HTTP status codes.
Keep the existing governance permissions for normal data-source operations:
READ_GOVERNANCE for list, detail and approved read use;EDIT_GOVERNANCE for save, update, delete,
connection test and dataflow use;DATASOURCE_POOL_MANAGE = "datasources:pools:manage" for pool diagnostics
and forced invalidation.Do not grant DATASOURCE_POOL_MANAGE to editor or viewer.
.venv/bin/python -m pytest \
tests/test_datasource_lifecycle_api.py \
tests/test_permission_matrix.py \
tests/test_datasource_api_security.py -q
Expected: PASS.
.venv/bin/python scripts/generate_openapi.py
.venv/bin/python -m pytest tests/test_architecture_artifacts.py -q
Expected: generated contract matches route source.
git add app/core/data_source/service.py app/api/data_source/routes.py \
app/core/system/permissions.py tests/test_datasource_lifecycle_api.py \
tests/test_permission_matrix.py tests/test_datasource_api_security.py \
docs/architecture/OPENAPI.yaml
git commit -m "feat: secure datasource lifecycle APIs"
Files:
frontend/src/views/dataGovernance/dataSource/index.vuefrontend/src/views/dataGovernance/dataSource/components/edit.vuefrontend/src/api/dataOrigin.jsCreate: tests/test_datasource_frontend_contract.py
[ ] Step 1: Write the failing frontend contract test
def test_datasource_ui_does_not_render_or_replay_stored_credentials():
listing = Path(
"frontend/src/views/dataGovernance/dataSource/index.vue"
).read_text(encoding="utf-8")
editor = Path(
"frontend/src/views/dataGovernance/dataSource/components/edit.vue"
).read_text(encoding="utf-8")
assert "value: 'username'" not in listing
assert "value: 'param'" not in listing
assert "credential_configured" in listing
assert "Object.assign(this.formValues, this.itemData)" not in editor
assert "datasourceParse" not in editor
assert "key: 'param'" not in editor
assert "postgresql" in editor
assert "mysql" in editor
.venv/bin/python -m pytest \
tests/test_datasource_frontend_contract.py -q
Expected: FAIL because the existing UI lists username and arbitrary connection parameters and copies the full list item into the credential form.
Use uid, never the Neo4j numeric id, for edit and delete. Remove username
and free-form connection-parameter columns. Show a
credential_configured status chip instead. Keep host, port, database and
schema visible because they are non-secret definition fields.
Use a fixed database-type selector containing only PostgreSQL and MySQL.
Remove quick connection-string parsing and the free-form param field. On
edit, copy only safe definition fields into formValues; initialize username
and password as empty and show “凭据已配置,留空则沿用当前凭据”. Omit blank
credential fields from the request.
For a new source, require username and password. For an existing source, send its UID so the backend may combine safe field edits with the current credential internally and create the next immutable credential version. Connection testing follows the same rule: new source uses entered credentials; existing source may use UID plus any explicitly entered replacement credentials.
Remove datasourceParse() when no other import remains. Keep the backend
compatibility endpoint out of navigation until its later removal is separately
verified.
.venv/bin/python -m pytest \
tests/test_datasource_frontend_contract.py \
tests/test_datasource_api_security.py -q
cd frontend
npm run build
Expected: tests PASS and the production frontend build exits 0.
git add frontend/src/views/dataGovernance/dataSource/index.vue \
frontend/src/views/dataGovernance/dataSource/components/edit.vue \
frontend/src/api/dataOrigin.js \
tests/test_datasource_frontend_contract.py
git commit -m "fix: make datasource management UI secret-free"
Files:
app/api/data_source/routes.pyapp/core/system/health.pyTest: tests/test_datasource_pool_diagnostics.py
[ ] Step 1: Write failing diagnostics tests
def test_admin_pool_status_is_safe(client, admin_token, seeded_pool):
response = client.get(
f"/api/datasource/{DATA_SOURCE_UID}/pool",
headers=bearer(admin_token),
)
body = response.get_json()["data"]
assert body["data_source_uid"] == DATA_SOURCE_UID
assert body["pool_state"] == "healthy"
assert "host" not in body
assert "username" not in body
assert "password" not in response.get_data(as_text=True).lower()
def test_platform_health_contains_only_pool_aggregates(client):
body = client.get("/api/system/health").get_json()["data"]["components"]
assert body["datasource_pools"] == {
"active_pool_count": 1,
"degraded_pool_count": 0,
"open_circuit_count": 0,
"checked_out_total": 0,
"pool_timeout_total": 0,
}
.venv/bin/python -m pytest \
tests/test_datasource_pool_diagnostics.py -q
Expected: FAIL because diagnostics are not registered.
GET /api/datasource/pools
GET /api/datasource/{data_source_uid}/pool
POST /api/datasource/{data_source_uid}/pool/invalidate
All require datasources:pools:manage; invalidation accepts only a fixed
reason enum and records an audit event.
If the runtime has not created a registry, report zeros without creating any data-source connections. Do not perform a network probe from platform health.
.venv/bin/python -m pytest \
tests/test_datasource_pool_diagnostics.py \
tests/test_outbox.py -q
Expected: PASS.
git add app/api/data_source/routes.py app/core/system/health.py \
tests/test_datasource_pool_diagnostics.py
git commit -m "feat: expose safe datasource pool diagnostics"
Files:
app/commands/report_datasource_credentials.pyapp/commands/migrate_datasource_credentials.pyapp/commands/reconcile_datasource_credentials.pyTest: tests/core/data_source/test_credential_migration.py
[ ] Step 1: Write failing migration tests
def test_report_never_returns_plaintext(fake_graph):
report = inspect_legacy_datasources(fake_graph)
assert report == [
{
"data_source_uid": DATA_SOURCE_UID,
"issues": ["plaintext_credentials"],
}
]
assert "legacy-password" not in json.dumps(report)
def test_migration_removes_plaintext_only_after_round_trip_verification(
fake_graph, credential_repository
):
result = migrate_one(DATA_SOURCE_UID, fake_graph, credential_repository)
assert result.status == "migrated"
assert credential_repository.decrypt_latest().password == "legacy-password"
assert "password" not in fake_graph.node
assert "username" not in fake_graph.node
.venv/bin/python -m pytest \
tests/core/data_source/test_credential_migration.py -q
Expected: FAIL because commands do not exist.
Report issue categories only:
missing_uid
plaintext_credentials
missing_credential_reference
unsupported_database_type
incomplete_connection_definition
Default output is JSON Lines and excludes host, username and password.
CLI contract:
python -m app.commands.migrate_datasource_credentials \
--uid <uuid> \
--confirm-encrypt-and-remove-plaintext
Without the confirmation flag, exit non-zero without mutation. For each source:
Default is report-only. Compare Neo4j references to PostgreSQL credentials and report:
orphaned_credential
missing_credential
stale_credential_version
revoked_credential_referenced
plaintext_remaining
Repairs require --repair --uid <uuid>. Never delete source data or credentials
in a broad repair run.
.venv/bin/python -m pytest \
tests/core/data_source/test_credential_migration.py -q
Expected: PASS.
git add app/commands/report_datasource_credentials.py \
app/commands/migrate_datasource_credentials.py \
app/commands/reconcile_datasource_credentials.py \
tests/core/data_source/test_credential_migration.py
git commit -m "feat: migrate legacy datasource credentials safely"
Files:
deploy/docker/docker-compose.ymldeploy/docker/datasources/postgres/init.sqldeploy/docker/datasources/mysql/init.sqldeploy/docker/.env.exampletests/test_local_docker_contract.pyCreate: tests/integration/test_datasource_pools.py
[ ] Step 1: Write failing Compose contract tests
def test_compose_has_isolated_external_datasources():
source = COMPOSE.read_text(encoding="utf-8")
assert "source-postgres:" in source
assert "source-mysql:" in source
assert "dataops-test-source-postgres:" in source
assert "dataops-test-source-mysql:" in source
assert "DATASOURCE_CREDENTIAL_MASTER_KEY:" in source
.venv/bin/python -m pytest \
tests/test_local_docker_contract.py -q
Expected: FAIL because the services are absent.
Use pinned major/minor image tags:
source-postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: source_db
POSTGRES_USER: source_reader
POSTGRES_PASSWORD: source-postgres-test-password
ports: ["25432:5432"]
volumes:
- dataops-test-source-postgres:/var/lib/postgresql/data
- ./datasources/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
source-mysql:
image: mysql:8.4
environment:
MYSQL_DATABASE: source_db
MYSQL_USER: source_reader
MYSQL_PASSWORD: source-mysql-test-password
MYSQL_ROOT_PASSWORD: source-mysql-root-test-password
ports: ["23306:3306"]
volumes:
- dataops-test-source-mysql:/var/lib/mysql
- ./datasources/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
Both join dataops-test-net and define real health checks. Add test-only
32-byte base64 master key to the backend environment.
Both databases create:
CREATE TABLE acceptance_customers (
customer_id INTEGER PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL
);
INSERT INTO acceptance_customers VALUES
(1, 'Alpha'),
(2, 'Beta');
Test:
SELECT COUNT(*) through DataSourceConnectionManager;2;pool_size + max_overflow;delete a source and prove no new connection can be acquired.
[ ] Step 6: Start services and run integration
docker compose -f deploy/docker/docker-compose.yml up -d \
source-postgres source-mysql backend
TEST_DATABASE_URL=postgresql://dataops:dataops-test-password@localhost:15432/dataops \
TEST_SOURCE_POSTGRES_URL=postgresql://source_reader:source-postgres-test-password@localhost:25432/source_db \
TEST_SOURCE_MYSQL_URL=mysql+pymysql://source_reader:source-mysql-test-password@localhost:23306/source_db \
.venv/bin/python -m pytest \
tests/integration/test_datasource_pools.py -q
Expected: PASS; Compose reports both sources healthy.
git add deploy/docker/docker-compose.yml deploy/docker/datasources \
deploy/docker/.env.example tests/test_local_docker_contract.py \
tests/integration/test_datasource_pools.py
git commit -m "test: add isolated datasource pool stack"
Files:
tests/integration/test_datasource_pool_failures.pyModify: deploy/docker/README.md
[ ] Step 1: Add outage acceptance test
The test helper must execute these externally controlled phases:
assert query(POSTGRES_UID).scalar_one() == 2
assert query(MYSQL_UID).scalar_one() == 2
compose_stop("source-mysql")
wait_for_error(MYSQL_UID, "DATASOURCE_CONNECTION_FAILED", attempts=3)
assert query_error(MYSQL_UID).code == "DATASOURCE_CIRCUIT_OPEN"
assert query(POSTGRES_UID).scalar_one() == 2
assert platform_health()["database"] is True
compose_start_and_wait_healthy("source-mysql")
clock.advance(30)
assert query(MYSQL_UID).scalar_one() == 2
assert pool_status(MYSQL_UID)["pool_state"] == "healthy"
The test may run Docker Compose commands only when
RUN_DATASOURCE_OUTAGE_TEST=1. Inject a fake monotonic clock into the breaker
so recovery does not require a 30-second sleep. Wrap the outage in
try/finally; the finally block always starts source-mysql and waits for
its Compose health condition. A failed assertion must never leave the service
stopped.
.venv/bin/python -m pytest \
tests/integration/test_datasource_pool_failures.py -q
Expected: SKIP unless RUN_DATASOURCE_OUTAGE_TEST=1.
RUN_DATASOURCE_OUTAGE_TEST=1 \
.venv/bin/python -m pytest \
tests/integration/test_datasource_pool_failures.py -q
Within the same test process: establish both pools, stop MySQL, open only the MySQL circuit after three failures, prove PostgreSQL and the platform control database remain healthy, restart MySQL, advance the injected clock, and prove the half-open probe closes the circuit. Then update a test credential version, assert the old Engine is disposed, and prove the next query uses the new version. Poll the Compose health condition rather than sleeping a fixed duration.
Add:
explicit warning that platform PostgreSQL is not managed by this registry.
[ ] Step 6: Commit
git add tests/integration/test_datasource_pool_failures.py \
deploy/docker/README.md
git commit -m "test: verify datasource outage and recovery"
Files:
docs/architecture/ARCHITECTURE_OVERVIEW.mddocs/architecture/DATA_MODEL.mddocs/architecture/OPENAPI.yamldeployment/sync_release.sh only if new source directories are not already copiedTest: existing full test suites
[ ] Step 1: Update architecture artifacts
Document:
failure and security boundaries.
[ ] Step 2: Regenerate OpenAPI and sync release source
.venv/bin/python scripts/generate_openapi.py
bash deployment/sync_release.sh
diff -qr app deployment/app
diff -qr migrations deployment/migrations
cmp requirements.txt deployment/requirements.txt
Expected: all diff/cmp commands exit 0.
TEST_POSTGRES_ADMIN_URL=postgresql://dataops:dataops-test-password@localhost:15432/postgres \
TEST_DATABASE_URL=postgresql://dataops:dataops-test-password@localhost:15432/dataops \
TEST_ADMIN_PASSWORD=AdminPass123 \
.venv/bin/python -m pytest -q
Expected: zero failures and zero errors.
cd frontend
npm run build
Expected: build exits 0; pre-existing Vue 2 dependency warnings may remain, but no new ESLint errors are allowed.
docker compose -f deploy/docker/docker-compose.yml up -d --build
docker compose -f deploy/docker/docker-compose.yml ps
curl -fsS http://localhost:18183/ >/dev/null
curl -fsS http://localhost:15500/api/system/health
Expected: platform services, source-postgres and source-mysql are healthy.
rg -n \
"password.*logger|logger.*password|postgresql://[^[:space:]]+:[^[:space:]@]+@" \
app deployment/app docs/architecture
Expected: no data-source credential disclosure. Test-only Compose credentials
must remain limited to deploy/docker.
git add -- \
docs/architecture/ARCHITECTURE_OVERVIEW.md \
docs/architecture/DATA_MODEL.md \
docs/architecture/OPENAPI.yaml \
deployment/app/api/data_source/routes.py \
deployment/app/commands/migrate_datasource_credentials.py \
deployment/app/commands/reconcile_datasource_credentials.py \
deployment/app/commands/reconcile_governance_uids.py \
deployment/app/commands/report_datasource_credentials.py \
deployment/app/config/config.py \
deployment/app/core/data_source \
deployment/app/core/system/health.py \
deployment/app/core/system/permissions.py \
deployment/gunicorn_config.py \
deployment/migrations/versions/20260718_50_datasource_credentials.py \
deployment/requirements.txt
git commit -m "docs: finalize datasource pool operations"
Before staging, inspect git diff --name-only -- deployment/ and confirm every
changed release-copy file is the generated counterpart of a source file from
Tasks 1–15. Do not stage unrelated pre-existing workspace changes.
Use the requesting-code-review skill. Review specifically for: