747 lines
25 KiB
Python
747 lines
25 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Accounts, Credentials, Leases, and Platforms CRUD (no stdout).
|
|
|
|
All functions operate on a connection (passed or opened locally).
|
|
Time fields are INTEGER Unix seconds (UTC).
|
|
"""
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
from typing import Any, Optional
|
|
|
|
from db.auth_strategy import ALL_AUTH_STRATEGIES, AUTH_STRATEGY_PASSWORD_AUTO
|
|
from db.connection import get_conn, init_db
|
|
from db.platform_defaults import PLATFORM_DEFAULT_AUTH_STRATEGY
|
|
from util.logging_config import get_skill_logger
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _now_unix() -> int:
|
|
return int(time.time())
|
|
|
|
|
|
def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]:
|
|
"""Convert Unix timestamp to local ISO8601 string, or None."""
|
|
if ts is None:
|
|
return None
|
|
try:
|
|
return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds")
|
|
except (ValueError, OSError, OverflowError):
|
|
return None
|
|
|
|
|
|
_UNSET = object()
|
|
|
|
|
|
def resolve_auth_strategy_for_platform(conn, platform_key: str) -> str:
|
|
"""Pick auth_strategy from platforms.default_auth_strategy, built-in map, or password_auto."""
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT default_auth_strategy FROM platforms WHERE platform_key = ?",
|
|
(platform_key,),
|
|
)
|
|
row = cur.fetchone()
|
|
if row and row[0]:
|
|
s = str(row[0])
|
|
if s in ALL_AUTH_STRATEGIES:
|
|
return s
|
|
mapped = PLATFORM_DEFAULT_AUTH_STRATEGY.get(platform_key)
|
|
if mapped and mapped in ALL_AUTH_STRATEGIES:
|
|
return mapped
|
|
return AUTH_STRATEGY_PASSWORD_AUTO
|
|
|
|
|
|
def _acc_row_to_dict(row) -> dict[str, Any]:
|
|
"""Convert an accounts table row tuple to a dict matching the v2 schema."""
|
|
dev_fp = row[20]
|
|
return {
|
|
"id": row[0],
|
|
"platform_key": row[1],
|
|
"account_label": row[2],
|
|
"login_id": row[3] or "",
|
|
"login_id_type": row[4],
|
|
"tenant_id": row[5] or "",
|
|
"environment": row[6],
|
|
"role": row[7],
|
|
"provider_code": row[8] or "",
|
|
"profile_dir": (row[9] or "").strip(),
|
|
"url": row[10] or "",
|
|
"status": row[11],
|
|
"session_status": row[12],
|
|
"last_used_at": _unix_to_iso_local(row[13]),
|
|
"last_login_check_at": _unix_to_iso_local(row[14]),
|
|
"last_error_code": row[15] or "",
|
|
"last_error_message": row[16] or "",
|
|
"extra_json": _safe_json_loads(row[17]),
|
|
"auth_strategy": row[18],
|
|
"session_persistent": int(row[19]) if row[19] is not None else 1,
|
|
"device_fingerprint": dev_fp if dev_fp is not None else None,
|
|
"created_at": _unix_to_iso_local(row[21]),
|
|
"updated_at": _unix_to_iso_local(row[22]),
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Platforms
|
|
# ============================================================================
|
|
|
|
def platform_exists(conn, platform_key: str) -> bool:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT 1 FROM platforms WHERE platform_key = ?", (platform_key,))
|
|
return cur.fetchone() is not None
|
|
|
|
|
|
def list_platforms_from_db(conn, domain: Optional[str] = None) -> list[dict]:
|
|
cur = conn.cursor()
|
|
if domain:
|
|
cur.execute(
|
|
"SELECT platform_key, display_name, domain, provider_code, "
|
|
"default_url, aliases_json, capabilities_json, enabled, "
|
|
"default_auth_strategy, created_at, updated_at FROM platforms WHERE domain = ? ORDER BY platform_key",
|
|
(domain,),
|
|
)
|
|
else:
|
|
cur.execute(
|
|
"SELECT platform_key, display_name, domain, provider_code, "
|
|
"default_url, aliases_json, capabilities_json, enabled, "
|
|
"default_auth_strategy, created_at, updated_at FROM platforms ORDER BY platform_key"
|
|
)
|
|
rows = cur.fetchall()
|
|
result = []
|
|
for r in rows:
|
|
result.append({
|
|
"platform_key": r[0],
|
|
"display_name": r[1],
|
|
"domain": r[2],
|
|
"provider_code": r[3] or "",
|
|
"default_url": r[4] or "",
|
|
"aliases": _safe_json_loads(r[5], []),
|
|
"capabilities": _safe_json_loads(r[6], {}),
|
|
"enabled": bool(r[7]),
|
|
"default_auth_strategy": r[8],
|
|
"created_at": _unix_to_iso_local(r[9]),
|
|
"updated_at": _unix_to_iso_local(r[10]),
|
|
})
|
|
return result
|
|
|
|
|
|
def get_platform_from_db(conn, platform_key: str) -> Optional[dict]:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT platform_key, display_name, domain, provider_code, "
|
|
"default_url, aliases_json, capabilities_json, enabled, "
|
|
"default_auth_strategy, created_at, updated_at FROM platforms WHERE platform_key = ?",
|
|
(platform_key,),
|
|
)
|
|
r = cur.fetchone()
|
|
if not r:
|
|
return None
|
|
return {
|
|
"platform_key": r[0],
|
|
"display_name": r[1],
|
|
"domain": r[2],
|
|
"provider_code": r[3] or "",
|
|
"default_url": r[4] or "",
|
|
"aliases": _safe_json_loads(r[5], []),
|
|
"capabilities": _safe_json_loads(r[6], {}),
|
|
"enabled": bool(r[7]),
|
|
"default_auth_strategy": r[8],
|
|
"created_at": _unix_to_iso_local(r[9]),
|
|
"updated_at": _unix_to_iso_local(r[10]),
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Accounts — CRUD
|
|
# ============================================================================
|
|
|
|
def insert_account(
|
|
conn,
|
|
platform_key: str,
|
|
account_label: str,
|
|
login_id: Optional[str],
|
|
login_id_type: str,
|
|
tenant_id: Optional[str],
|
|
environment: str,
|
|
role: str,
|
|
provider_code: Optional[str],
|
|
profile_dir: Optional[str],
|
|
url: Optional[str],
|
|
extra_json: Optional[str],
|
|
now: int,
|
|
auth_strategy: str,
|
|
session_persistent: int = 1,
|
|
device_fingerprint: Optional[str] = None,
|
|
) -> int:
|
|
if auth_strategy not in ALL_AUTH_STRATEGIES:
|
|
raise ValueError(f"invalid auth_strategy: {auth_strategy!r}")
|
|
if session_persistent not in (0, 1):
|
|
raise ValueError("session_persistent must be 0 or 1")
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO accounts
|
|
(platform_key, account_label, login_id, login_id_type, tenant_id,
|
|
environment, role, provider_code, profile_dir, url,
|
|
status, session_status, extra_json,
|
|
auth_strategy, session_persistent, device_fingerprint,
|
|
created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'unknown', ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
platform_key, account_label, login_id, login_id_type, tenant_id,
|
|
environment, role, provider_code, profile_dir, url,
|
|
extra_json or "{}", auth_strategy, session_persistent, device_fingerprint,
|
|
now, now,
|
|
),
|
|
)
|
|
return int(cur.lastrowid)
|
|
|
|
|
|
def get_account_by_id(account_id) -> Optional[dict]:
|
|
"""Get a single account by id. Returns dict or None."""
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
SELECT id, platform_key, account_label, login_id, login_id_type,
|
|
tenant_id, environment, role, provider_code,
|
|
profile_dir, url, status, session_status,
|
|
last_used_at, last_login_check_at,
|
|
last_error_code, last_error_message, extra_json,
|
|
auth_strategy, session_persistent, device_fingerprint,
|
|
created_at, updated_at
|
|
FROM accounts WHERE id = ?
|
|
""",
|
|
(int(account_id),),
|
|
)
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
return _acc_row_to_dict(row)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def find_accounts(
|
|
platform_key: Optional[str] = None,
|
|
environment: Optional[str] = None,
|
|
tenant_id: Optional[str] = None,
|
|
role: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
provider_code: Optional[str] = None,
|
|
limit: int = 200,
|
|
) -> list[dict]:
|
|
"""Find accounts by optional filters. Returns list of dicts."""
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
conditions = []
|
|
params = []
|
|
if platform_key:
|
|
conditions.append("platform_key = ?")
|
|
params.append(platform_key)
|
|
if environment:
|
|
conditions.append("environment = ?")
|
|
params.append(environment)
|
|
if tenant_id:
|
|
conditions.append("tenant_id = ?")
|
|
params.append(tenant_id)
|
|
if role:
|
|
conditions.append("role = ?")
|
|
params.append(role)
|
|
if status:
|
|
conditions.append("status = ?")
|
|
params.append(status)
|
|
if provider_code:
|
|
conditions.append("provider_code = ?")
|
|
params.append(provider_code)
|
|
|
|
where = ""
|
|
if conditions:
|
|
where = "WHERE " + " AND ".join(conditions)
|
|
|
|
sql = (
|
|
"SELECT id, platform_key, account_label, login_id, login_id_type, "
|
|
"tenant_id, environment, role, provider_code, profile_dir, url, "
|
|
"status, session_status, last_used_at, last_login_check_at, "
|
|
"last_error_code, last_error_message, extra_json, "
|
|
"auth_strategy, session_persistent, device_fingerprint, "
|
|
"created_at, updated_at "
|
|
f"FROM accounts {where} ORDER BY updated_at DESC, id DESC LIMIT ?"
|
|
)
|
|
params.append(int(limit))
|
|
cur.execute(sql, params)
|
|
return [_acc_row_to_dict(row) for row in cur.fetchall()]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def find_account_ids(
|
|
platform_key: str,
|
|
environment: Optional[str] = None,
|
|
tenant_id: Optional[str] = None,
|
|
role: Optional[str] = None,
|
|
) -> list[int]:
|
|
"""Return account IDs matching filters, excluding those with active leases."""
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
conditions = ["a.status = 'active'", "a.platform_key = ?"]
|
|
params = [platform_key]
|
|
if environment:
|
|
conditions.append("a.environment = ?")
|
|
params.append(environment)
|
|
if tenant_id:
|
|
conditions.append("a.tenant_id = ?")
|
|
params.append(tenant_id)
|
|
if role:
|
|
conditions.append("a.role = ?")
|
|
params.append(role)
|
|
|
|
# Exclude accounts with active non-expired leases
|
|
now = _now_unix()
|
|
conditions.append(
|
|
"a.id NOT IN (SELECT account_id FROM account_leases "
|
|
"WHERE status = 'active' AND expires_at > ?)"
|
|
)
|
|
params.append(now)
|
|
|
|
where = " AND ".join(conditions)
|
|
cur.execute(
|
|
f"SELECT a.id FROM accounts a WHERE {where} "
|
|
"ORDER BY CASE WHEN a.session_status = 'expired' THEN 1 ELSE 0 END ASC, "
|
|
"a.last_used_at IS NULL DESC, a.updated_at DESC, a.id DESC",
|
|
params,
|
|
)
|
|
return [int(row[0]) for row in cur.fetchall()]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_account_last_used(account_id: int) -> None:
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"UPDATE accounts SET last_used_at = ?, updated_at = ? WHERE id = ?",
|
|
(_now_unix(), _now_unix(), account_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_account_session_status(account_id: int, session_status: str) -> None:
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"UPDATE accounts SET session_status = ?, last_login_check_at = ?, updated_at = ? WHERE id = ?",
|
|
(session_status, _now_unix(), _now_unix(), account_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_account_error(
|
|
account_id: int,
|
|
error_code: str,
|
|
error_message: str,
|
|
) -> None:
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"UPDATE accounts SET last_error_code = ?, last_error_message = ?, updated_at = ? WHERE id = ?",
|
|
(error_code, error_message, _now_unix(), account_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_account(
|
|
conn,
|
|
account_id: int,
|
|
*,
|
|
auth_strategy: Optional[str] = None,
|
|
session_persistent: Optional[int] = None,
|
|
device_fingerprint: Any = _UNSET,
|
|
) -> None:
|
|
"""Optional field updates for accounts v2 columns."""
|
|
sets: list[str] = []
|
|
params: list[Any] = []
|
|
if auth_strategy is not None:
|
|
if auth_strategy not in ALL_AUTH_STRATEGIES:
|
|
raise ValueError(f"invalid auth_strategy: {auth_strategy!r}")
|
|
sets.append("auth_strategy = ?")
|
|
params.append(auth_strategy)
|
|
if session_persistent is not None:
|
|
if session_persistent not in (0, 1):
|
|
raise ValueError("session_persistent must be 0 or 1")
|
|
sets.append("session_persistent = ?")
|
|
params.append(session_persistent)
|
|
if device_fingerprint is not _UNSET:
|
|
sets.append("device_fingerprint = ?")
|
|
params.append(device_fingerprint)
|
|
if not sets:
|
|
return
|
|
sets.append("updated_at = ?")
|
|
params.append(_now_unix())
|
|
params.append(int(account_id))
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
f"UPDATE accounts SET {', '.join(sets)} WHERE id = ?",
|
|
tuple(params),
|
|
)
|
|
|
|
|
|
def account_exists_with_login_id(conn, platform_key: str, login_id: str, environment: str, role: str) -> bool:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT id FROM accounts WHERE platform_key = ? AND login_id = ? AND environment = ? AND role = ?",
|
|
(platform_key, login_id, environment, role),
|
|
)
|
|
return cur.fetchone() is not None
|
|
|
|
|
|
# ============================================================================
|
|
# Credentials
|
|
# ============================================================================
|
|
|
|
def insert_credential(
|
|
conn,
|
|
account_id: Optional[int],
|
|
platform_key: str,
|
|
credential_label: str,
|
|
credential_type: str,
|
|
secret_storage: str,
|
|
secret_ref: Optional[str],
|
|
secret_mask: Optional[str],
|
|
expires_at: Optional[int],
|
|
extra_json: Optional[str],
|
|
now: int,
|
|
secret_ciphertext: Optional[str] = None,
|
|
) -> int:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO credentials
|
|
(account_id, platform_key, credential_label, credential_type,
|
|
secret_storage, secret_ref, secret_mask, secret_ciphertext, expires_at, status,
|
|
extra_json, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)
|
|
""",
|
|
(
|
|
account_id, platform_key, credential_label, credential_type,
|
|
secret_storage, secret_ref, secret_mask, secret_ciphertext, expires_at,
|
|
extra_json or "{}", now, now,
|
|
),
|
|
)
|
|
return int(cur.lastrowid)
|
|
|
|
|
|
def get_credential_by_id(credential_id: int) -> Optional[dict]:
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT id, account_id, platform_key, credential_label, credential_type, "
|
|
"secret_storage, secret_ref, secret_mask, expires_at, status, last_used_at, "
|
|
"extra_json, created_at, updated_at FROM credentials WHERE id = ?",
|
|
(credential_id,),
|
|
)
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
return {
|
|
"id": row[0],
|
|
"account_id": row[1],
|
|
"platform_key": row[2],
|
|
"credential_label": row[3],
|
|
"credential_type": row[4],
|
|
"secret_storage": row[5],
|
|
"secret_ref": row[6] or "",
|
|
"secret_mask": row[7] or "",
|
|
"expires_at": _unix_to_iso_local(row[8]),
|
|
"status": row[9],
|
|
"last_used_at": _unix_to_iso_local(row[10]),
|
|
"created_at": _unix_to_iso_local(row[12]),
|
|
"updated_at": _unix_to_iso_local(row[13]),
|
|
}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def find_credentials(
|
|
platform_key: str,
|
|
credential_type: Optional[str] = None,
|
|
environment: Optional[str] = None,
|
|
role: Optional[str] = None,
|
|
status: str = "active",
|
|
limit: int = 50,
|
|
) -> list[dict]:
|
|
"""Find credentials, optionally joining accounts for environment/role filter."""
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
conditions = ["c.status = ?"]
|
|
params = [status]
|
|
|
|
conditions.append("c.platform_key = ?")
|
|
params.append(platform_key)
|
|
|
|
if credential_type:
|
|
conditions.append("c.credential_type = ?")
|
|
params.append(credential_type)
|
|
|
|
# If environment/role filters provided, join accounts table
|
|
if environment or role:
|
|
conditions.append("c.account_id = a.id")
|
|
if environment:
|
|
conditions.append("a.environment = ?")
|
|
params.append(environment)
|
|
if role:
|
|
conditions.append("a.role = ?")
|
|
params.append(role)
|
|
|
|
where = " AND ".join(conditions)
|
|
cur.execute(
|
|
f"SELECT c.id, c.account_id, c.platform_key, c.credential_label, "
|
|
f"c.credential_type, c.secret_storage, c.secret_ref, c.secret_mask, "
|
|
f"c.expires_at, c.status, c.last_used_at, c.extra_json, "
|
|
f"c.created_at, c.updated_at "
|
|
f"FROM credentials c, accounts a WHERE {where} "
|
|
f"ORDER BY c.last_used_at IS NULL DESC, c.updated_at DESC, c.id DESC "
|
|
f"LIMIT ?",
|
|
params + [int(limit)],
|
|
)
|
|
else:
|
|
where = " AND ".join(conditions)
|
|
cur.execute(
|
|
f"SELECT c.id, c.account_id, c.platform_key, c.credential_label, "
|
|
f"c.credential_type, c.secret_storage, c.secret_ref, c.secret_mask, "
|
|
f"c.expires_at, c.status, c.last_used_at, c.extra_json, "
|
|
f"c.created_at, c.updated_at "
|
|
f"FROM credentials c WHERE {where} "
|
|
f"ORDER BY c.last_used_at IS NULL DESC, c.updated_at DESC, c.id DESC "
|
|
f"LIMIT ?",
|
|
params + [int(limit)],
|
|
)
|
|
|
|
rows = cur.fetchall()
|
|
result = []
|
|
for row in rows:
|
|
result.append({
|
|
"id": row[0],
|
|
"account_id": row[1],
|
|
"platform_key": row[2],
|
|
"credential_label": row[3],
|
|
"credential_type": row[4],
|
|
"secret_storage": row[5],
|
|
"secret_ref": row[6] or "",
|
|
"secret_mask": row[7] or "",
|
|
"expires_at": _unix_to_iso_local(row[8]),
|
|
"status": row[9],
|
|
"last_used_at": _unix_to_iso_local(row[10]),
|
|
"created_at": _unix_to_iso_local(row[12]),
|
|
"updated_at": _unix_to_iso_local(row[13]),
|
|
})
|
|
return result
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_credential_last_used(credential_id: int) -> None:
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"UPDATE credentials SET last_used_at = ?, updated_at = ? WHERE id = ?",
|
|
(_now_unix(), _now_unix(), credential_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ============================================================================
|
|
# Leases
|
|
# ============================================================================
|
|
|
|
def acquire_lease(
|
|
account_id: int,
|
|
lease_token: str,
|
|
holder: str,
|
|
purpose: Optional[str],
|
|
ttl_sec: int,
|
|
) -> dict:
|
|
"""
|
|
Acquire a lease for an account. Expires active leases first.
|
|
Returns the lease record dict on success.
|
|
Raises ValueError if a non-expired active lease already exists.
|
|
"""
|
|
log = get_skill_logger()
|
|
log.info("lease_acquire_attempt account_id=%s holder=%s purpose=%s ttl=%s", account_id, holder, purpose, ttl_sec)
|
|
|
|
conn = get_conn()
|
|
try:
|
|
conn.row_factory = None
|
|
cur = conn.cursor()
|
|
now = _now_unix()
|
|
|
|
# Mark any expired active leases as expired
|
|
cur.execute(
|
|
"UPDATE account_leases SET status = 'expired', updated_at = ? "
|
|
"WHERE account_id = ? AND status = 'active' AND expires_at <= ?",
|
|
(now, account_id, now),
|
|
)
|
|
if cur.rowcount > 0:
|
|
log.info("lease_expired_cleanup account_id=%s count=%s", account_id, cur.rowcount)
|
|
|
|
# Check for existing active non-expired lease
|
|
cur.execute(
|
|
"SELECT id, lease_token, holder FROM account_leases "
|
|
"WHERE account_id = ? AND status = 'active' AND expires_at > ?",
|
|
(account_id, now),
|
|
)
|
|
existing = cur.fetchone()
|
|
if existing:
|
|
log.warning(
|
|
"lease_conflict account_id=%s existing_holder=%s existing_token_prefix=%s",
|
|
account_id, existing[2], existing[1][:8],
|
|
)
|
|
raise ValueError(
|
|
f"Account {account_id} already has an active lease "
|
|
f"(holder={existing[2]}, token_prefix={existing[1][:8]}) (LEASE_CONFLICT)"
|
|
)
|
|
|
|
expires_at = now + max(1, int(ttl_sec))
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO account_leases
|
|
(account_id, lease_token, holder, purpose, expires_at, status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'active', ?, ?)
|
|
""",
|
|
(account_id, lease_token, holder, purpose, expires_at, now, now),
|
|
)
|
|
conn.commit()
|
|
lease_id = int(cur.lastrowid)
|
|
log.info("lease_acquire_success account_id=%s lease_id=%s token_prefix=%s", account_id, lease_id, lease_token[:8])
|
|
|
|
return {
|
|
"id": lease_id,
|
|
"account_id": account_id,
|
|
"lease_token": lease_token,
|
|
"holder": holder,
|
|
"purpose": purpose,
|
|
"expires_at": _unix_to_iso_local(expires_at),
|
|
"status": "active",
|
|
"created_at": _unix_to_iso_local(now),
|
|
}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def release_lease(lease_token: str) -> bool:
|
|
"""Release a lease by token. Returns True if found and released."""
|
|
log = get_skill_logger()
|
|
log.info("lease_release_attempt token_prefix=%s", lease_token[:8])
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
now = _now_unix()
|
|
cur.execute(
|
|
"UPDATE account_leases SET status = 'released', released_at = ?, updated_at = ? "
|
|
"WHERE lease_token = ? AND status = 'active'",
|
|
(now, now, lease_token),
|
|
)
|
|
conn.commit()
|
|
if cur.rowcount == 0:
|
|
log.warning("lease_release_not_found token_prefix=%s", lease_token[:8])
|
|
return False
|
|
log.info("lease_release_success token_prefix=%s", lease_token[:8])
|
|
return True
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def list_active_leases() -> list[dict]:
|
|
"""List all active (non-expired, non-released) leases."""
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
now = _now_unix()
|
|
cur.execute(
|
|
"SELECT id, account_id, lease_token, holder, purpose, expires_at, "
|
|
"heartbeat_at, released_at, status, created_at, updated_at "
|
|
"FROM account_leases WHERE status = 'active' AND expires_at > ? "
|
|
"ORDER BY created_at DESC",
|
|
(now,),
|
|
)
|
|
rows = cur.fetchall()
|
|
result = []
|
|
for r in rows:
|
|
result.append({
|
|
"id": r[0],
|
|
"account_id": r[1],
|
|
"lease_token": r[2],
|
|
"holder": r[3],
|
|
"purpose": r[4] or "",
|
|
"expires_at": _unix_to_iso_local(r[5]),
|
|
"heartbeat_at": _unix_to_iso_local(r[6]),
|
|
"released_at": _unix_to_iso_local(r[7]),
|
|
"status": r[8],
|
|
"created_at": _unix_to_iso_local(r[9]),
|
|
"updated_at": _unix_to_iso_local(r[10]),
|
|
})
|
|
return result
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def cleanup_expired_leases() -> int:
|
|
"""Mark all expired active leases as expired. Returns count cleaned."""
|
|
log = get_skill_logger()
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
now = _now_unix()
|
|
cur.execute(
|
|
"UPDATE account_leases SET status = 'expired', updated_at = ? "
|
|
"WHERE status = 'active' AND expires_at <= ?",
|
|
(now, now),
|
|
)
|
|
count = cur.rowcount
|
|
conn.commit()
|
|
if count > 0:
|
|
log.info("lease_cleanup_expired count=%s", count)
|
|
return count
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ============================================================================
|
|
# Internal helpers
|
|
# ============================================================================
|
|
|
|
def _safe_json_loads(val: Any, default: Any = None) -> Any:
|
|
if val is None:
|
|
return default
|
|
try:
|
|
return json.loads(val) if isinstance(val, str) else val
|
|
except (json.JSONDecodeError, TypeError):
|
|
return default
|