feat: restrict backup archive download to primary admin and hide admin-hidden projects from other admins

- Add `get_admin_uids()` and `get_primary_admin_uid()` to database.py for resolving the earliest-created admin
- Modify `can_view_project()` in content.py so a project hidden by an admin is invisible to other admins (both web UI and REST API)
- Update `_download_url()` and `_backup_payload()` in admin/backups.py to accept a `can_download` flag, gating the download endpoint with `is_primary_admin()`
- Remove `role` from `_user_facts()` in docs_live.py to avoid leaking admin status in live docs
- Update doc summaries in docs_api.py to reflect the new admin-visibility and backup-download semantics
This commit is contained in:
2026-06-17 14:08:28 +00:00
parent 6b5347103b
commit 0a554ebc32
71 changed files with 1868 additions and 527 deletions
@@ -130,9 +130,13 @@ def build_analytics(
) -> dict:
if GATEWAY_LEDGER not in db.tables:
return empty_payload(hours)
hours = max(1, min(hours, MAX_WINDOW_HOURS))
now = _now()
cutoff = _iso(now - timedelta(hours=hours))
if hours <= 0:
hours = 0
cutoff = "0000-01-01T00:00:00+00:00"
else:
hours = max(1, min(hours, MAX_WINDOW_HOURS))
cutoff = _iso(now - timedelta(hours=hours))
rows = _ledger_rows(cutoff)
if not rows:
return empty_payload(hours)
@@ -27,6 +27,7 @@ PRICE_OUTPUT_PER_M_DEFAULT = 0.28
VISION_PRICE_INPUT_PER_M_DEFAULT = 0.0
VISION_PRICE_OUTPUT_PER_M_DEFAULT = 0.0
EMBED_PRICE_INPUT_PER_M_DEFAULT = 0.01
RSEARCH_COST_PER_CALL_DEFAULT = 0.0
USAGE_RETENTION_HOURS_DEFAULT = 720
@@ -267,6 +267,15 @@ class GatewayService(BaseService):
help="Fallback only; used when the embeddings upstream returns no native cost.",
group="Pricing",
),
ConfigField(
"gateway_rsearch_cost_per_call",
"rsearch cost / call ($)",
type="float",
default=config.RSEARCH_COST_PER_CALL_DEFAULT,
minimum=0,
help="Flat cost attributed to each external rsearch call (web search / AI answer / chat / image describe), recorded under backend 'rsearch' so external AI spend appears in AI usage.",
group="Pricing",
),
ConfigField(
"gateway_max_retries",
"Max retries",
+107
View File
@@ -313,6 +313,90 @@ class GatewayUsageLedger:
logger.warning("gateway usage record failed: %s", exc)
return None
def record_external(
self,
*,
owner_kind: str,
owner_id: str,
backend: str,
endpoint: str,
model: str,
cost_usd: float,
success: bool,
status_code: int,
latency_ms: float = 0.0,
) -> Optional[dict]:
try:
row = {
"created_at": _iso(_now()),
"owner_kind": owner_kind or "unknown",
"owner_id": owner_id or "unknown",
"backend": backend,
"endpoint": endpoint or "",
"requested_model": model or "",
"model": model or "",
"status_code": int(status_code or 0),
"success": 1 if success else 0,
"error_category": None,
"upstream_latency_ms": float(latency_ms or 0),
"gateway_overhead_ms": 0.0,
"queue_wait_ms": 0.0,
"connect_ms": 0.0,
"total_latency_ms": float(latency_ms or 0),
"prompt_tokens": 0,
"completion_tokens": 0,
"cache_hit_tokens": 0,
"cache_miss_tokens": 0,
"reasoning_tokens": 0,
"total_tokens": 0,
"tokens_per_second": 0.0,
"context_window": None,
"context_utilization": None,
"cost_usd": round(float(cost_usd or 0), 8),
"input_cost_usd": 0.0,
"output_cost_usd": 0.0,
"native_cost": 0,
"stream_requested": 0,
"temperature": None,
"top_p": None,
"max_tokens": None,
"has_tools": 0,
"retries_attempted": 0,
"retry_succeeded": 0,
"circuit_open": 0,
"user_agent": "",
}
get_table(GATEWAY_LEDGER).insert(row)
self._audit_external(row)
return row
except Exception as exc:
logger.warning("gateway external usage record failed: %s", exc)
return None
def _audit_external(self, row: dict) -> None:
from devplacepy.services.audit import record as audit
owner_kind = row.get("owner_kind") or "unknown"
owner_id = row.get("owner_id") or "unknown"
actor_kind, actor_uid, actor_role = audit_actor_for(owner_kind, owner_id)
audit.record_system(
"ai.gateway.call",
actor_kind=actor_kind,
actor_uid=actor_uid,
actor_role=actor_role,
origin="api",
result="success" if row.get("success") else "failure",
summary=f"external AI call by {owner_kind}/{owner_id} ({row.get('backend')})",
metadata={
"backend": row.get("backend"),
"endpoint": row.get("endpoint"),
"cost_usd": row.get("cost_usd"),
"status_code": row.get("status_code"),
"owner_kind": owner_kind,
"owner_id": owner_id,
},
)
def _audit(self, raw: dict, norm: dict, cost_usd: float) -> None:
from devplacepy.services.audit import record as audit
@@ -365,3 +449,26 @@ class GatewayUsageLedger:
get_table(GATEWAY_CONCURRENCY).delete(created_at={"<": cutoff})
)
return ledger_removed, samples_removed
def record_rsearch_call(
owner_kind: str, owner_id: str, endpoint: str, success: bool, status_code: int
) -> None:
try:
from devplacepy.services.manager import service_manager
service = service_manager.get_service("openai")
cfg = service.get_config() if service is not None else {}
cost = float(cfg.get("gateway_rsearch_cost_per_call", 0.0) or 0.0)
GatewayUsageLedger().record_external(
owner_kind=owner_kind or "system",
owner_id=owner_id or "",
backend="rsearch",
endpoint=endpoint or "/search",
model="rsearch",
cost_usd=cost,
success=success,
status_code=status_code,
)
except Exception as exc:
logger.warning("rsearch usage ledger failed: %s", exc)