Harden ops run identity and leases
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 33s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
This commit is contained in:
tegwick 2026-08-23 13:01:46 +02:00
parent 36161d346f
commit f0a897e088
13 changed files with 522 additions and 50 deletions

View file

@ -36,6 +36,20 @@ def default_lease_seconds() -> int:
return 900
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def _has_active_lease(row: OpsRun, *, now: datetime) -> bool:
"""Return whether ``row`` has a lease strictly after ``now``."""
lease_until = row.lease_until
if not isinstance(lease_until, datetime):
return False
if lease_until.tzinfo is None:
lease_until = lease_until.replace(tzinfo=timezone.utc)
return lease_until > now
def max_attempts() -> int:
try:
return max(1, int(os.environ.get("OPS_RUN_MAX_ATTEMPTS", "3")))
@ -163,14 +177,14 @@ async def create_ops_run_from_spec(
async def reopen_stale_claims(session: AsyncSession) -> int:
"""Return claimed rows with expired leases to open."""
now = datetime.now(timezone.utc)
now = _utcnow()
stmt = (
update(OpsRun)
.where(
and_(
OpsRun.state == "claimed",
OpsRun.lease_until.is_not(None),
OpsRun.lease_until < now,
OpsRun.lease_until <= now,
)
)
.values(
@ -252,13 +266,15 @@ async def heartbeat_ops_run(
worker_id: str,
lease_seconds: int | None = None,
) -> OpsRun | None:
row = await session.get(OpsRun, run_id)
row = await session.get(OpsRun, run_id, with_for_update=True)
if row is None:
return None
if row.state != "claimed" or row.claim_owner != worker_id:
return None
now = _utcnow()
if not _has_active_lease(row, now=now):
return None
lease_seconds = lease_seconds or default_lease_seconds()
now = datetime.now(timezone.utc)
row.lease_until = now + timedelta(seconds=lease_seconds)
row.updated_at = now
return row
@ -271,12 +287,14 @@ async def complete_ops_run(
worker_id: str,
result: dict[str, Any] | None = None,
) -> OpsRun | None:
row = await session.get(OpsRun, run_id)
row = await session.get(OpsRun, run_id, with_for_update=True)
if row is None:
return None
if row.state != "claimed" or row.claim_owner != worker_id:
return None
now = datetime.now(timezone.utc)
now = _utcnow()
if not _has_active_lease(row, now=now):
return None
row.state = "succeeded"
row.lease_until = None
row.result = normalise_ops_result(result)
@ -293,12 +311,14 @@ async def fail_ops_run(
reopen: bool = False,
result: dict[str, Any] | None = None,
) -> OpsRun | None:
row = await session.get(OpsRun, run_id)
row = await session.get(OpsRun, run_id, with_for_update=True)
if row is None:
return None
if row.state != "claimed" or row.claim_owner != worker_id:
return None
now = datetime.now(timezone.utc)
now = _utcnow()
if not _has_active_lease(row, now=now):
return None
payload = normalise_ops_result(result)
if error:
payload["error"] = error[:2000]

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import hmac
import os
import uuid
from datetime import datetime
@ -16,6 +17,7 @@ from activity_core.ops_auth import (
extract_operator_token,
extract_sso_principal,
operator_token_configured,
require_operator,
)
from activity_core.ops_run_queue import (
claim_ops_runs,
@ -34,6 +36,7 @@ router = APIRouter(prefix="/ops-runs", tags=["ops-runs"])
_get_db: Callable[[], async_sessionmaker[AsyncSession]] | None = None
WORKER_TOKEN_ENV = "ACTIVITY_CORE_WORKER_TOKEN"
WORKER_ID_ENV = "ACTIVITY_CORE_WORKER_ID"
def bind_ops_runs_deps(
@ -53,6 +56,11 @@ def _worker_token_configured() -> bool:
return bool((os.environ.get(WORKER_TOKEN_ENV) or "").strip())
def configured_worker_id() -> str | None:
value = (os.environ.get(WORKER_ID_ENV) or "").strip()
return value or None
def _extract_worker_token(
*,
x_worker_token: str | None,
@ -77,8 +85,18 @@ def require_worker_or_operator(
x_worker_token=x_worker_token, authorization=authorization
)
expected_worker = (os.environ.get(WORKER_TOKEN_ENV) or "").strip()
if expected_worker and worker_tok and worker_tok == expected_worker:
return f"worker:{worker_tok[:8]}"
if (
expected_worker
and worker_tok
and hmac.compare_digest(worker_tok, expected_worker)
):
worker_id = configured_worker_id()
if not worker_id:
raise HTTPException(
status_code=503,
detail=f"worker identity not configured ({WORKER_ID_ENV})",
)
return f"worker:{worker_id}"
sso = extract_sso_principal(request)
if sso:
@ -88,18 +106,61 @@ def require_worker_or_operator(
x_operator_token=x_operator_token, authorization=authorization
)
expected_op = (os.environ.get("ACTIVITY_CORE_OPERATOR_TOKEN") or "").strip()
if expected_op and op_tok and op_tok == expected_op:
if expected_op and op_tok and hmac.compare_digest(op_tok, expected_op):
return "operator:token"
if not _worker_token_configured() and not operator_token_configured():
if allow_unauth_mutations():
return "dev:unauth"
# Dev convenience when no tokens configured at all
return "dev:open"
raise HTTPException(status_code=503, detail="ops run auth not configured")
raise HTTPException(status_code=401, detail="worker or operator auth required")
def require_worker(
request: Request,
*,
x_worker_token: str | None = None,
authorization: str | None = None,
) -> str:
"""Return the configured worker identity for a valid worker credential."""
del request # reserved for future mTLS/proxy-bound worker principals
expected_worker = (os.environ.get(WORKER_TOKEN_ENV) or "").strip()
worker_tok = _extract_worker_token(
x_worker_token=x_worker_token, authorization=authorization
)
if expected_worker:
if not worker_tok or not hmac.compare_digest(worker_tok, expected_worker):
raise HTTPException(status_code=401, detail="worker auth required")
worker_id = configured_worker_id()
if not worker_id:
raise HTTPException(
status_code=503,
detail=f"worker identity not configured ({WORKER_ID_ENV})",
)
return worker_id
if allow_unauth_mutations() and not operator_token_configured():
return "dev:unauth"
raise HTTPException(status_code=503, detail="worker auth not configured")
def bind_worker_id(requested_worker_id: str, authenticated_worker_id: str) -> str:
"""Bind a body compatibility field to its authenticated queue principal."""
requested = requested_worker_id.strip()
if authenticated_worker_id == "dev:unauth":
return requested
if requested != authenticated_worker_id:
raise HTTPException(
status_code=403,
detail="worker_id does not match authenticated worker",
)
return authenticated_worker_id
class ClaimBody(BaseModel):
worker_id: str = Field(..., min_length=1, max_length=256)
labels: list[str] | None = None
@ -195,21 +256,20 @@ async def post_claim(
body: ClaimBody,
request: Request,
x_worker_token: str | None = Header(default=None, alias="X-Worker-Token"),
x_operator_token: str | None = Header(default=None, alias="X-Operator-Token"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
require_worker_or_operator(
authenticated_worker_id = require_worker(
request,
x_worker_token=x_worker_token,
x_operator_token=x_operator_token,
authorization=authorization,
)
worker_id = bind_worker_id(body.worker_id, authenticated_worker_id)
Session = _db()
async with Session() as session:
async with session.begin():
claimed = await claim_ops_runs(
session,
worker_id=body.worker_id,
worker_id=worker_id,
labels=body.labels,
labels_mode=body.labels_mode,
limit=body.limit,
@ -227,28 +287,27 @@ async def post_heartbeat(
body: HeartbeatBody,
request: Request,
x_worker_token: str | None = Header(default=None, alias="X-Worker-Token"),
x_operator_token: str | None = Header(default=None, alias="X-Operator-Token"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
require_worker_or_operator(
authenticated_worker_id = require_worker(
request,
x_worker_token=x_worker_token,
x_operator_token=x_operator_token,
authorization=authorization,
)
worker_id = bind_worker_id(body.worker_id, authenticated_worker_id)
Session = _db()
async with Session() as session:
async with session.begin():
row = await heartbeat_ops_run(
session,
run_id,
worker_id=body.worker_id,
worker_id=worker_id,
lease_seconds=body.lease_seconds,
)
if row is None:
raise HTTPException(
status_code=409,
detail="not claimed by this worker or not found",
detail="not actively leased by this worker or not found",
)
return ops_run_to_dict(row)
@ -259,28 +318,27 @@ async def post_complete(
body: CompleteBody,
request: Request,
x_worker_token: str | None = Header(default=None, alias="X-Worker-Token"),
x_operator_token: str | None = Header(default=None, alias="X-Operator-Token"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
require_worker_or_operator(
authenticated_worker_id = require_worker(
request,
x_worker_token=x_worker_token,
x_operator_token=x_operator_token,
authorization=authorization,
)
worker_id = bind_worker_id(body.worker_id, authenticated_worker_id)
Session = _db()
async with Session() as session:
async with session.begin():
row = await complete_ops_run(
session,
run_id,
worker_id=body.worker_id,
worker_id=worker_id,
result=body.result,
)
if row is None:
raise HTTPException(
status_code=409,
detail="not claimed by this worker or not found",
detail="not actively leased by this worker or not found",
)
return ops_run_to_dict(row)
@ -291,22 +349,21 @@ async def post_fail(
body: FailBody,
request: Request,
x_worker_token: str | None = Header(default=None, alias="X-Worker-Token"),
x_operator_token: str | None = Header(default=None, alias="X-Operator-Token"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
require_worker_or_operator(
authenticated_worker_id = require_worker(
request,
x_worker_token=x_worker_token,
x_operator_token=x_operator_token,
authorization=authorization,
)
worker_id = bind_worker_id(body.worker_id, authenticated_worker_id)
Session = _db()
async with Session() as session:
async with session.begin():
row = await fail_ops_run(
session,
run_id,
worker_id=body.worker_id,
worker_id=worker_id,
error=body.error,
reopen=body.reopen,
result=body.result,
@ -314,7 +371,7 @@ async def post_fail(
if row is None:
raise HTTPException(
status_code=409,
detail="not claimed by this worker or not found",
detail="not actively leased by this worker or not found",
)
return ops_run_to_dict(row)
@ -322,13 +379,11 @@ async def post_fail(
@router.post("/expire-leases")
async def post_expire_leases(
request: Request,
x_worker_token: str | None = Header(default=None, alias="X-Worker-Token"),
x_operator_token: str | None = Header(default=None, alias="X-Operator-Token"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
require_worker_or_operator(
await require_operator(
request,
x_worker_token=x_worker_token,
x_operator_token=x_operator_token,
authorization=authorization,
)