ctfy.sdk.admin_resources.authoring_review
One competition's uploads, for the people allowed to rule on them.
docs/community-authoring-plan.md P2.3, decision 16. Two resources:
admin.authoring_reviewers— who may rule on one event's uploads (super-admin only), the competition-admin grant's shape.admin.competition_authored_challenges— the review itself, scoped to one event by the path: list, read, audit and rule. The platform queue (admin.authored_challenges) keeps everything else — publish, curate, push, smoke — because what a tree runs stays with a platform admin.
1"""One competition's uploads, for the people allowed to rule on them. 2 3``docs/community-authoring-plan.md`` P2.3, decision 16. Two resources: 4 5* ``admin.authoring_reviewers`` — who may rule on one event's uploads 6 (super-admin only), the competition-admin grant's shape. 7* ``admin.competition_authored_challenges`` — the review itself, scoped 8 to one event by the path: list, read, audit and rule. The platform 9 queue (``admin.authored_challenges``) keeps everything else — publish, 10 curate, push, smoke — because what a tree *runs* stays with a platform 11 admin. 12""" 13 14from __future__ import annotations 15 16import builtins 17from typing import Literal 18 19from ctfy.core.enums import ( 20 AuthoredDestination, 21 AuthoredFlowAction, 22 AuthoredQueueOrder, 23 AuthoredScoresChoice, 24 AuthoredSmokeFilter, 25) 26from ctfy.core.state.models import ReviewStatus 27from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 28from ctfy.sdk.base import BaseHttpClient 29from ctfy.server.models import ( 30 AdminAuthoredChallengeDetail, 31 AdminAuthoredChallengeRow, 32 AdminAuthoredRevisionRow, 33 AdminAuthoredVersionRow, 34 AdminMaliceInfo, 35 AdminMaliceStanding, 36 AdminModelReview, 37 AdminModelReviewInfo, 38 AdminProbeInfo, 39 AdminTaskInfo, 40 AuthoredAuditReport, 41 AuthoredChallengeFileBody, 42 AuthoredFlow, 43 AuthoredHistory, 44 AuthoredPointsAtStake, 45 AuthoredQueueCounts, 46 AuthoredRevisionDiff, 47 AuthoredSmokeLive, 48 AuthoredSmokeLog, 49 AuthoredSmokeLogSummary, 50 AuthoredSmokeStepLines, 51 AuthoredVersionDiff, 52 AuthoredVersionMessage, 53 AuthoringReviewerInfo, 54 PrecheckRunsInfo, 55) 56 57 58class AdminAuthoringReviewersResource: 59 """Grant / revoke the right to rule on one competition's uploads.""" 60 61 def __init__(self, http: BaseHttpClient) -> None: 62 self._http = http 63 64 def list( 65 self, competition_id: str, offset: int = 0, limit: int = 50 66 ) -> PagedList[AuthoringReviewerInfo]: 67 """Who may rule on this competition's uploads.""" 68 resp = self._http.request( 69 "GET", 70 f"/admin/competitions/{competition_id}/authoring-reviewers", 71 params={"offset": offset, "limit": limit}, 72 ) 73 _raise_for_status(resp) 74 return _extract_items(resp.json(), AuthoringReviewerInfo) 75 76 def grant( 77 self, competition_id: str, *, user_id: str = "", email: str = "" 78 ) -> AuthoringReviewerInfo: 79 """Grant it, to a user named by ``user_id`` or ``email``. The 80 holder reaches this event's uploads and nothing else of it — not 81 the organiser's console, not the entrants' registration details.""" 82 resp = self._http.request( 83 "POST", 84 f"/admin/competitions/{competition_id}/authoring-reviewers", 85 json={"user_id": user_id, "email": email}, 86 ) 87 _raise_for_status(resp) 88 return AuthoringReviewerInfo.model_validate(resp.json()) 89 90 def revoke(self, competition_id: str, user_id: str) -> None: 91 """Revoke it.""" 92 resp = self._http.request( 93 "DELETE", f"/admin/competitions/{competition_id}/authoring-reviewers/{user_id}" 94 ) 95 _raise_for_status(resp) 96 97 98class AdminCompetitionAuthoredChallengesResource: 99 """Read and rule on one competition's uploads. 100 101 ⚠️ The competition is the **grant**, not a filter: another event's 102 upload answers 404 here, and there is no parameter that widens the 103 listing. 104 """ 105 106 def __init__(self, http: BaseHttpClient) -> None: 107 self._http = http 108 109 def list( 110 self, 111 competition_id: str, 112 *, 113 review_status: ReviewStatus | None = None, 114 smoke: AuthoredSmokeFilter | None = None, 115 revision: ReviewStatus | None = None, 116 order: AuthoredQueueOrder | None = None, 117 destination: AuthoredDestination | None = None, 118 offset: int = 0, 119 limit: int = 50, 120 ) -> PagedList[AdminAuthoredChallengeRow]: 121 """This event's uploads, oldest first. ``smoke``, ``revision``, 122 ``destination`` and ``order`` work as on the platform queue — except that each 123 uploader's ``author_reputation``, and so the ``reputation`` order, 124 counts only the uploads this reviewer may attribute to them.""" 125 params: dict[str, object] = {"offset": offset, "limit": limit} 126 if review_status is not None: 127 params["review_status"] = review_status 128 if smoke is not None: 129 params["smoke"] = smoke 130 if revision is not None: 131 params["revision"] = revision 132 if order is not None: 133 params["order"] = order 134 if destination is not None: 135 params["destination"] = destination 136 resp = self._http.request( 137 "GET", f"/admin/competitions/{competition_id}/authored-challenges", params=params 138 ) 139 _raise_for_status(resp) 140 return _extract_items(resp.json(), AdminAuthoredChallengeRow) 141 142 def queue_counts(self, competition_id: str) -> AuthoredQueueCounts: 143 """This event's per-tab counts and longest wait (#2037 B1).""" 144 resp = self._http.request( 145 "GET", f"/admin/competitions/{competition_id}/authored-challenges/queue-counts" 146 ) 147 _raise_for_status(resp) 148 return AuthoredQueueCounts.model_validate(resp.json()) 149 150 def get(self, competition_id: str, challenge_id: str) -> AdminAuthoredChallengeDetail: 151 """One upload, with the paths, sizes and execution surface of 152 what is in it.""" 153 resp = self._http.request( 154 "GET", f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 155 ) 156 _raise_for_status(resp) 157 return AdminAuthoredChallengeDetail.model_validate(resp.json()) 158 159 def audit(self, competition_id: str, challenge_id: str) -> AuthoredAuditReport: 160 """Run the challenge audit over this upload. Read ``outcome`` 161 before the counts — ``timed_out`` and ``failed`` are the audit 162 declining to answer, never a clean bill.""" 163 resp = self._http.request( 164 "GET", 165 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/audit", 166 ) 167 _raise_for_status(resp) 168 return AuthoredAuditReport.model_validate(resp.json()) 169 170 def flow(self, competition_id: str, challenge_id: str) -> AuthoredFlow: 171 """Where one of this event's uploads is in its lifecycle, and what the 172 caller can do next as the event's reviewer or organiser (#2147).""" 173 resp = self._http.request( 174 "GET", 175 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/flow", 176 ) 177 _raise_for_status(resp) 178 return AuthoredFlow.model_validate(resp.json()) 179 180 def precheck_runs(self, competition_id: str, challenge_id: str) -> PrecheckRunsInfo: 181 """The AI pre-check's managed-eval runs of one of this event's uploads' 182 pending versions, for its reviewers and organisers (#3189).""" 183 resp = self._http.request( 184 "GET", 185 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 186 "/precheck-runs", 187 ) 188 _raise_for_status(resp) 189 return PrecheckRunsInfo.model_validate(resp.json()) 190 191 def precheck_run_artifact(self, competition_id: str, challenge_id: str, run_id: str) -> bytes: 192 """What one pre-check run's agent left behind, as ``.tar.gz`` bytes, 193 for this event's reviewers and organisers — never its author (#3260).""" 194 resp = self._http.request( 195 "GET", 196 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 197 f"/precheck-runs/{run_id}/artifact", 198 ) 199 _raise_for_status(resp) 200 return bytes(resp.content) 201 202 def history(self, competition_id: str, challenge_id: str) -> AuthoredHistory: 203 """One of this event's uploads' review history, oldest first (#3093).""" 204 resp = self._http.request( 205 "GET", 206 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/history", 207 ) 208 _raise_for_status(resp) 209 return AuthoredHistory.model_validate(resp.json()) 210 211 def smoke_logs( 212 self, competition_id: str, challenge_id: str 213 ) -> builtins.list[AuthoredSmokeLogSummary]: 214 """This event's upload's smoke runs, newest first (#2024).""" 215 resp = self._http.request( 216 "GET", 217 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-logs", 218 ) 219 _raise_for_status(resp) 220 return [AuthoredSmokeLogSummary.model_validate(row) for row in resp.json()] 221 222 def versions( 223 self, competition_id: str, challenge_id: str 224 ) -> builtins.list[AdminAuthoredVersionRow]: 225 """Every stored version of one of this event's uploads (#2037).""" 226 resp = self._http.request( 227 "GET", 228 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/versions", 229 ) 230 _raise_for_status(resp) 231 return [AdminAuthoredVersionRow.model_validate(row) for row in resp.json()] 232 233 def version_diff( 234 self, competition_id: str, challenge_id: str, *, from_version: str, to_version: str 235 ) -> AuthoredVersionDiff: 236 """Two versions of one of this event's uploads compared (#2037).""" 237 resp = self._http.request( 238 "GET", 239 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 240 "/versions/diff", 241 params={"from_version": from_version, "to_version": to_version}, 242 ) 243 _raise_for_status(resp) 244 return AuthoredVersionDiff.model_validate(resp.json()) 245 246 def version_messages( 247 self, competition_id: str, challenge_id: str, version_id: str 248 ) -> builtins.list[AuthoredVersionMessage]: 249 """One version's thread, for this event's reviewers (#2037).""" 250 resp = self._http.request( 251 "GET", 252 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 253 f"/versions/{version_id}/messages", 254 ) 255 _raise_for_status(resp) 256 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()] 257 258 def post_version_message( 259 self, 260 competition_id: str, 261 challenge_id: str, 262 version_id: str, 263 body: str, 264 *, 265 as_review_group: bool = True, 266 ) -> builtins.list[AuthoredVersionMessage]: 267 """Write to an upload's authors as this event's reviewer (#2037).""" 268 resp = self._http.request( 269 "POST", 270 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 271 f"/versions/{version_id}/messages", 272 json={"body": body, "as_review_group": as_review_group}, 273 ) 274 _raise_for_status(resp) 275 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()] 276 277 def version_archive(self, competition_id: str, challenge_id: str, version_id: str) -> bytes: 278 """One version of one of this event's uploads, as ``.tar.gz`` bytes.""" 279 resp = self._http.request( 280 "GET", 281 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 282 f"/versions/{version_id}/archive", 283 ) 284 _raise_for_status(resp) 285 return bytes(resp.content) 286 287 def smoke_log(self, competition_id: str, challenge_id: str, log_id: str) -> AuthoredSmokeLog: 288 """One smoke run of one of this event's uploads (#2024).""" 289 resp = self._http.request( 290 "GET", 291 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 292 f"/smoke-logs/{log_id}", 293 ) 294 _raise_for_status(resp) 295 return AuthoredSmokeLog.model_validate(resp.json()) 296 297 def smoke_live(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 298 """The smoke run of one of this event's uploads that is in flight (#2145).""" 299 resp = self._http.request( 300 "GET", 301 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-live", 302 ) 303 _raise_for_status(resp) 304 return AuthoredSmokeLive.model_validate(resp.json()) 305 306 def cancel_smoke(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 307 """Take back a waiting smoke run of one of this event's uploads (#2145).""" 308 resp = self._http.request( 309 "POST", 310 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 311 "/smoke-live/cancel", 312 ) 313 _raise_for_status(resp) 314 return AuthoredSmokeLive.model_validate(resp.json()) 315 316 def smoke_run_lines( 317 self, 318 competition_id: str, 319 challenge_id: str, 320 run_id: str, 321 key: str, 322 *, 323 before: int = 0, 324 after: int = 0, 325 limit: int = 200, 326 ) -> AuthoredSmokeStepLines: 327 """A window of one step's full output of one of this event's uploads (#2145).""" 328 resp = self._http.request( 329 "GET", 330 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 331 f"/smoke-runs/{run_id}/lines", 332 params={"step": key, "before": before, "after": after, "limit": limit}, 333 ) 334 _raise_for_status(resp) 335 return AuthoredSmokeStepLines.model_validate(resp.json()) 336 337 def model_review(self, competition_id: str, challenge_id: str) -> AdminModelReviewInfo: 338 """The model's reading of one of this event's uploads (#1965 D) — 339 `admin.authored_challenges.model_review`'s answer, for its reviewers.""" 340 resp = self._http.request( 341 "GET", 342 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 343 ) 344 _raise_for_status(resp) 345 return AdminModelReviewInfo.model_validate(resp.json()) 346 347 def assistant_review(self, competition_id: str, challenge_id: str) -> AdminProbeInfo: 348 """The AI pre-check of one of this event's uploads (#2830, #3053) — 349 every model's run, the numbers and the white-box findings, for its 350 reviewers and organisers.""" 351 resp = self._http.request( 352 "GET", 353 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 354 "/assistant-review", 355 ) 356 _raise_for_status(resp) 357 return AdminProbeInfo.model_validate(resp.json()) 358 359 def rerun_assistant_review(self, competition_id: str, challenge_id: str) -> AdminTaskInfo: 360 """Pre-check the version under review again (#3053): the event's 361 organisers only, on an isolated node, counted against the version's 362 cap (``409 authoring_probe_budget_spent`` once it is spent).""" 363 resp = self._http.request( 364 "POST", 365 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 366 "/assistant-review", 367 ) 368 _raise_for_status(resp) 369 return AdminTaskInfo.model_validate(resp.json()) 370 371 def run_model_review( 372 self, competition_id: str, challenge_id: str, *, revision_id: str = "" 373 ) -> AdminTaskInfo: 374 """Ask the model to read one of this event's uploads — advice.""" 375 params = {"revision_id": revision_id} if revision_id else None 376 resp = self._http.request( 377 "POST", 378 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 379 params=params, 380 ) 381 _raise_for_status(resp) 382 return AdminTaskInfo.model_validate(resp.json()) 383 384 def override_model_review( 385 self, competition_id: str, challenge_id: str, review_id: str, *, note: str 386 ) -> AdminModelReview: 387 """Say the text a review flagged is not an injection (``note`` is 388 the audit's *why*); a version it rejected is decided again.""" 389 resp = self._http.request( 390 "POST", 391 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 392 f"/model-review/{review_id}/override", 393 json={"note": note}, 394 ) 395 _raise_for_status(resp) 396 return AdminModelReview.model_validate(resp.json()) 397 398 def malice_review(self, competition_id: str, challenge_id: str) -> AdminMaliceInfo: 399 """The malice check of one of this event's uploads (#3211), for its 400 reviewers. Never calls a model.""" 401 resp = self._http.request( 402 "GET", 403 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 404 "/malice-review", 405 ) 406 _raise_for_status(resp) 407 return AdminMaliceInfo.model_validate(resp.json()) 408 409 def override_malice_review( 410 self, 411 competition_id: str, 412 challenge_id: str, 413 review_id: str, 414 *, 415 decision: Literal["clear", "confirm"], 416 note: str, 417 ) -> AdminMaliceStanding: 418 """Rule on a malice hold of one of this event's uploads: ``clear`` 419 or ``confirm``, with the audit's *why*.""" 420 resp = self._http.request( 421 "POST", 422 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 423 f"/malice-review/{review_id}/override", 424 json={"decision": decision, "note": note}, 425 ) 426 _raise_for_status(resp) 427 return AdminMaliceStanding.model_validate(resp.json()) 428 429 def read_file( 430 self, competition_id: str, challenge_id: str, path: str 431 ) -> AuthoredChallengeFileBody: 432 """One file's contents — attacker-authored text written to be 433 run. ``binary`` and ``truncated`` say what you are not seeing.""" 434 resp = self._http.request( 435 "POST", 436 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/file", 437 json={"path": path}, 438 ) 439 _raise_for_status(resp) 440 return AuthoredChallengeFileBody.model_validate(resp.json()) 441 442 def review( 443 self, 444 competition_id: str, 445 challenge_id: str, 446 *, 447 status: ReviewStatus, 448 note: str = "", 449 ) -> AdminAuthoredChallengeRow: 450 """Record a content verdict. ``rejected`` requires a note, and 451 ``pending`` withdraws a verdict. Publishing stays with a 452 platform admin, who reviews what the tree runs.""" 453 resp = self._http.request( 454 "POST", 455 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/review", 456 json={"status": status, "note": note}, 457 ) 458 _raise_for_status(resp) 459 return AdminAuthoredChallengeRow.model_validate(resp.json()) 460 461 def release_to_catalog( 462 self, competition_id: str, challenge_id: str 463 ) -> AdminAuthoredChallengeRow: 464 """Put an upload this competition curates in the public catalog 465 (#1989), from that competition — **platform admins only** (owner 466 decision on #2756): an organiser is refused 403. Refused (409, with reasons) for an 467 upload the competition does not curate, and for a tree an automatic 468 review released that no person has reviewed; confirming what such a 469 tree runs stays with a platform admin 470 (``admin.authored_challenges.release_to_catalog``).""" 471 resp = self._http.request( 472 "POST", 473 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 474 "/catalog-release", 475 ) 476 _raise_for_status(resp) 477 return AdminAuthoredChallengeRow.model_validate(resp.json()) 478 479 def points_at_stake( 480 self, competition_id: str, challenge_id: str, action: AuthoredFlowAction 481 ) -> AuthoredPointsAtStake: 482 """The organiser's read of what a removal would decide about the 483 points already scored on an upload this competition holds (#2991).""" 484 resp = self._http.request( 485 "GET", 486 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/points", 487 params={"action": action}, 488 ) 489 _raise_for_status(resp) 490 return AuthoredPointsAtStake.model_validate(resp.json()) 491 492 def unpublish( 493 self, 494 competition_id: str, 495 challenge_id: str, 496 *, 497 reason: str, 498 scores: AuthoredScoresChoice | None = None, 499 ) -> AdminAuthoredChallengeRow: 500 """Take down an upload this competition holds, as one of its full 501 organisers (#1965 H) — with a reason, recorded on the upload's 502 publication history. There is no automatic take-down: a failing 503 re-check or players' reports alert the organisers, and this is what 504 they press. It leaves **every** competition that uses it, not only 505 this one (#2740 D7). Its solves keep their points unless ``scores`` says 506 otherwise; its authoring credit is void while it stays down. Refused (409, with reasons) for an upload 507 the competition does not hold; ``422`` for a blank reason. 508 ``scores`` (#2991): ``removed`` takes the points already scored on it 509 off the board; ``kept`` and ``None`` keep them.""" 510 body: dict[str, str] = {"reason": reason} 511 if scores: 512 body["scores"] = scores 513 resp = self._http.request( 514 "POST", 515 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/unpublish", 516 json=body, 517 ) 518 _raise_for_status(resp) 519 return AdminAuthoredChallengeRow.model_validate(resp.json()) 520 521 def republish( 522 self, competition_id: str, challenge_id: str, *, reason: str = "" 523 ) -> AdminAuthoredChallengeRow: 524 """Restore an upload that was taken down (#1965 H) — the take-down 525 overruled, recorded beside it. Only a take-down is reversible here: 526 publishing a tree the first time stays a platform admin's act. 527 Refused (409, with reasons) when the upload's latest record is not a 528 take-down, or by any publish rule.""" 529 resp = self._http.request( 530 "POST", 531 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/republish", 532 json={"reason": reason}, 533 ) 534 _raise_for_status(resp) 535 return AdminAuthoredChallengeRow.model_validate(resp.json()) 536 537 def revisions( 538 self, competition_id: str, challenge_id: str 539 ) -> builtins.list[AdminAuthoredRevisionRow]: 540 """Every revision of one of this event's uploads, newest first.""" 541 resp = self._http.request( 542 "GET", 543 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/revisions", 544 ) 545 _raise_for_status(resp) 546 return [AdminAuthoredRevisionRow.model_validate(row) for row in resp.json()] 547 548 def revision_diff( 549 self, competition_id: str, challenge_id: str, revision_id: str 550 ) -> AuthoredRevisionDiff: 551 """A proposed revision against the live tree (plan P3.1).""" 552 resp = self._http.request( 553 "GET", 554 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 555 f"/revisions/{revision_id}/diff", 556 ) 557 _raise_for_status(resp) 558 return AuthoredRevisionDiff.model_validate(resp.json()) 559 560 def read_revision_file( 561 self, competition_id: str, challenge_id: str, revision_id: str, path: str 562 ) -> AuthoredChallengeFileBody: 563 """One file of a revision, whole.""" 564 resp = self._http.request( 565 "POST", 566 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 567 f"/revisions/{revision_id}/file", 568 json={"path": path}, 569 ) 570 _raise_for_status(resp) 571 return AuthoredChallengeFileBody.model_validate(resp.json()) 572 573 def review_revision( 574 self, 575 competition_id: str, 576 challenge_id: str, 577 revision_id: str, 578 *, 579 status: ReviewStatus, 580 note: str = "", 581 ) -> AdminAuthoredRevisionRow: 582 """A content verdict on a revision of one of this event's uploads. 583 Applying it stays with a platform admin.""" 584 resp = self._http.request( 585 "POST", 586 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 587 f"/revisions/{revision_id}/review", 588 json={"status": status, "note": note}, 589 ) 590 _raise_for_status(resp) 591 return AdminAuthoredRevisionRow.model_validate(resp.json())
59class AdminAuthoringReviewersResource: 60 """Grant / revoke the right to rule on one competition's uploads.""" 61 62 def __init__(self, http: BaseHttpClient) -> None: 63 self._http = http 64 65 def list( 66 self, competition_id: str, offset: int = 0, limit: int = 50 67 ) -> PagedList[AuthoringReviewerInfo]: 68 """Who may rule on this competition's uploads.""" 69 resp = self._http.request( 70 "GET", 71 f"/admin/competitions/{competition_id}/authoring-reviewers", 72 params={"offset": offset, "limit": limit}, 73 ) 74 _raise_for_status(resp) 75 return _extract_items(resp.json(), AuthoringReviewerInfo) 76 77 def grant( 78 self, competition_id: str, *, user_id: str = "", email: str = "" 79 ) -> AuthoringReviewerInfo: 80 """Grant it, to a user named by ``user_id`` or ``email``. The 81 holder reaches this event's uploads and nothing else of it — not 82 the organiser's console, not the entrants' registration details.""" 83 resp = self._http.request( 84 "POST", 85 f"/admin/competitions/{competition_id}/authoring-reviewers", 86 json={"user_id": user_id, "email": email}, 87 ) 88 _raise_for_status(resp) 89 return AuthoringReviewerInfo.model_validate(resp.json()) 90 91 def revoke(self, competition_id: str, user_id: str) -> None: 92 """Revoke it.""" 93 resp = self._http.request( 94 "DELETE", f"/admin/competitions/{competition_id}/authoring-reviewers/{user_id}" 95 ) 96 _raise_for_status(resp)
Grant / revoke the right to rule on one competition's uploads.
65 def list( 66 self, competition_id: str, offset: int = 0, limit: int = 50 67 ) -> PagedList[AuthoringReviewerInfo]: 68 """Who may rule on this competition's uploads.""" 69 resp = self._http.request( 70 "GET", 71 f"/admin/competitions/{competition_id}/authoring-reviewers", 72 params={"offset": offset, "limit": limit}, 73 ) 74 _raise_for_status(resp) 75 return _extract_items(resp.json(), AuthoringReviewerInfo)
Who may rule on this competition's uploads.
77 def grant( 78 self, competition_id: str, *, user_id: str = "", email: str = "" 79 ) -> AuthoringReviewerInfo: 80 """Grant it, to a user named by ``user_id`` or ``email``. The 81 holder reaches this event's uploads and nothing else of it — not 82 the organiser's console, not the entrants' registration details.""" 83 resp = self._http.request( 84 "POST", 85 f"/admin/competitions/{competition_id}/authoring-reviewers", 86 json={"user_id": user_id, "email": email}, 87 ) 88 _raise_for_status(resp) 89 return AuthoringReviewerInfo.model_validate(resp.json())
Grant it, to a user named by user_id or email. The
holder reaches this event's uploads and nothing else of it — not
the organiser's console, not the entrants' registration details.
91 def revoke(self, competition_id: str, user_id: str) -> None: 92 """Revoke it.""" 93 resp = self._http.request( 94 "DELETE", f"/admin/competitions/{competition_id}/authoring-reviewers/{user_id}" 95 ) 96 _raise_for_status(resp)
Revoke it.
99class AdminCompetitionAuthoredChallengesResource: 100 """Read and rule on one competition's uploads. 101 102 ⚠️ The competition is the **grant**, not a filter: another event's 103 upload answers 404 here, and there is no parameter that widens the 104 listing. 105 """ 106 107 def __init__(self, http: BaseHttpClient) -> None: 108 self._http = http 109 110 def list( 111 self, 112 competition_id: str, 113 *, 114 review_status: ReviewStatus | None = None, 115 smoke: AuthoredSmokeFilter | None = None, 116 revision: ReviewStatus | None = None, 117 order: AuthoredQueueOrder | None = None, 118 destination: AuthoredDestination | None = None, 119 offset: int = 0, 120 limit: int = 50, 121 ) -> PagedList[AdminAuthoredChallengeRow]: 122 """This event's uploads, oldest first. ``smoke``, ``revision``, 123 ``destination`` and ``order`` work as on the platform queue — except that each 124 uploader's ``author_reputation``, and so the ``reputation`` order, 125 counts only the uploads this reviewer may attribute to them.""" 126 params: dict[str, object] = {"offset": offset, "limit": limit} 127 if review_status is not None: 128 params["review_status"] = review_status 129 if smoke is not None: 130 params["smoke"] = smoke 131 if revision is not None: 132 params["revision"] = revision 133 if order is not None: 134 params["order"] = order 135 if destination is not None: 136 params["destination"] = destination 137 resp = self._http.request( 138 "GET", f"/admin/competitions/{competition_id}/authored-challenges", params=params 139 ) 140 _raise_for_status(resp) 141 return _extract_items(resp.json(), AdminAuthoredChallengeRow) 142 143 def queue_counts(self, competition_id: str) -> AuthoredQueueCounts: 144 """This event's per-tab counts and longest wait (#2037 B1).""" 145 resp = self._http.request( 146 "GET", f"/admin/competitions/{competition_id}/authored-challenges/queue-counts" 147 ) 148 _raise_for_status(resp) 149 return AuthoredQueueCounts.model_validate(resp.json()) 150 151 def get(self, competition_id: str, challenge_id: str) -> AdminAuthoredChallengeDetail: 152 """One upload, with the paths, sizes and execution surface of 153 what is in it.""" 154 resp = self._http.request( 155 "GET", f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 156 ) 157 _raise_for_status(resp) 158 return AdminAuthoredChallengeDetail.model_validate(resp.json()) 159 160 def audit(self, competition_id: str, challenge_id: str) -> AuthoredAuditReport: 161 """Run the challenge audit over this upload. Read ``outcome`` 162 before the counts — ``timed_out`` and ``failed`` are the audit 163 declining to answer, never a clean bill.""" 164 resp = self._http.request( 165 "GET", 166 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/audit", 167 ) 168 _raise_for_status(resp) 169 return AuthoredAuditReport.model_validate(resp.json()) 170 171 def flow(self, competition_id: str, challenge_id: str) -> AuthoredFlow: 172 """Where one of this event's uploads is in its lifecycle, and what the 173 caller can do next as the event's reviewer or organiser (#2147).""" 174 resp = self._http.request( 175 "GET", 176 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/flow", 177 ) 178 _raise_for_status(resp) 179 return AuthoredFlow.model_validate(resp.json()) 180 181 def precheck_runs(self, competition_id: str, challenge_id: str) -> PrecheckRunsInfo: 182 """The AI pre-check's managed-eval runs of one of this event's uploads' 183 pending versions, for its reviewers and organisers (#3189).""" 184 resp = self._http.request( 185 "GET", 186 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 187 "/precheck-runs", 188 ) 189 _raise_for_status(resp) 190 return PrecheckRunsInfo.model_validate(resp.json()) 191 192 def precheck_run_artifact(self, competition_id: str, challenge_id: str, run_id: str) -> bytes: 193 """What one pre-check run's agent left behind, as ``.tar.gz`` bytes, 194 for this event's reviewers and organisers — never its author (#3260).""" 195 resp = self._http.request( 196 "GET", 197 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 198 f"/precheck-runs/{run_id}/artifact", 199 ) 200 _raise_for_status(resp) 201 return bytes(resp.content) 202 203 def history(self, competition_id: str, challenge_id: str) -> AuthoredHistory: 204 """One of this event's uploads' review history, oldest first (#3093).""" 205 resp = self._http.request( 206 "GET", 207 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/history", 208 ) 209 _raise_for_status(resp) 210 return AuthoredHistory.model_validate(resp.json()) 211 212 def smoke_logs( 213 self, competition_id: str, challenge_id: str 214 ) -> builtins.list[AuthoredSmokeLogSummary]: 215 """This event's upload's smoke runs, newest first (#2024).""" 216 resp = self._http.request( 217 "GET", 218 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-logs", 219 ) 220 _raise_for_status(resp) 221 return [AuthoredSmokeLogSummary.model_validate(row) for row in resp.json()] 222 223 def versions( 224 self, competition_id: str, challenge_id: str 225 ) -> builtins.list[AdminAuthoredVersionRow]: 226 """Every stored version of one of this event's uploads (#2037).""" 227 resp = self._http.request( 228 "GET", 229 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/versions", 230 ) 231 _raise_for_status(resp) 232 return [AdminAuthoredVersionRow.model_validate(row) for row in resp.json()] 233 234 def version_diff( 235 self, competition_id: str, challenge_id: str, *, from_version: str, to_version: str 236 ) -> AuthoredVersionDiff: 237 """Two versions of one of this event's uploads compared (#2037).""" 238 resp = self._http.request( 239 "GET", 240 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 241 "/versions/diff", 242 params={"from_version": from_version, "to_version": to_version}, 243 ) 244 _raise_for_status(resp) 245 return AuthoredVersionDiff.model_validate(resp.json()) 246 247 def version_messages( 248 self, competition_id: str, challenge_id: str, version_id: str 249 ) -> builtins.list[AuthoredVersionMessage]: 250 """One version's thread, for this event's reviewers (#2037).""" 251 resp = self._http.request( 252 "GET", 253 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 254 f"/versions/{version_id}/messages", 255 ) 256 _raise_for_status(resp) 257 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()] 258 259 def post_version_message( 260 self, 261 competition_id: str, 262 challenge_id: str, 263 version_id: str, 264 body: str, 265 *, 266 as_review_group: bool = True, 267 ) -> builtins.list[AuthoredVersionMessage]: 268 """Write to an upload's authors as this event's reviewer (#2037).""" 269 resp = self._http.request( 270 "POST", 271 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 272 f"/versions/{version_id}/messages", 273 json={"body": body, "as_review_group": as_review_group}, 274 ) 275 _raise_for_status(resp) 276 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()] 277 278 def version_archive(self, competition_id: str, challenge_id: str, version_id: str) -> bytes: 279 """One version of one of this event's uploads, as ``.tar.gz`` bytes.""" 280 resp = self._http.request( 281 "GET", 282 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 283 f"/versions/{version_id}/archive", 284 ) 285 _raise_for_status(resp) 286 return bytes(resp.content) 287 288 def smoke_log(self, competition_id: str, challenge_id: str, log_id: str) -> AuthoredSmokeLog: 289 """One smoke run of one of this event's uploads (#2024).""" 290 resp = self._http.request( 291 "GET", 292 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 293 f"/smoke-logs/{log_id}", 294 ) 295 _raise_for_status(resp) 296 return AuthoredSmokeLog.model_validate(resp.json()) 297 298 def smoke_live(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 299 """The smoke run of one of this event's uploads that is in flight (#2145).""" 300 resp = self._http.request( 301 "GET", 302 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-live", 303 ) 304 _raise_for_status(resp) 305 return AuthoredSmokeLive.model_validate(resp.json()) 306 307 def cancel_smoke(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 308 """Take back a waiting smoke run of one of this event's uploads (#2145).""" 309 resp = self._http.request( 310 "POST", 311 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 312 "/smoke-live/cancel", 313 ) 314 _raise_for_status(resp) 315 return AuthoredSmokeLive.model_validate(resp.json()) 316 317 def smoke_run_lines( 318 self, 319 competition_id: str, 320 challenge_id: str, 321 run_id: str, 322 key: str, 323 *, 324 before: int = 0, 325 after: int = 0, 326 limit: int = 200, 327 ) -> AuthoredSmokeStepLines: 328 """A window of one step's full output of one of this event's uploads (#2145).""" 329 resp = self._http.request( 330 "GET", 331 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 332 f"/smoke-runs/{run_id}/lines", 333 params={"step": key, "before": before, "after": after, "limit": limit}, 334 ) 335 _raise_for_status(resp) 336 return AuthoredSmokeStepLines.model_validate(resp.json()) 337 338 def model_review(self, competition_id: str, challenge_id: str) -> AdminModelReviewInfo: 339 """The model's reading of one of this event's uploads (#1965 D) — 340 `admin.authored_challenges.model_review`'s answer, for its reviewers.""" 341 resp = self._http.request( 342 "GET", 343 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 344 ) 345 _raise_for_status(resp) 346 return AdminModelReviewInfo.model_validate(resp.json()) 347 348 def assistant_review(self, competition_id: str, challenge_id: str) -> AdminProbeInfo: 349 """The AI pre-check of one of this event's uploads (#2830, #3053) — 350 every model's run, the numbers and the white-box findings, for its 351 reviewers and organisers.""" 352 resp = self._http.request( 353 "GET", 354 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 355 "/assistant-review", 356 ) 357 _raise_for_status(resp) 358 return AdminProbeInfo.model_validate(resp.json()) 359 360 def rerun_assistant_review(self, competition_id: str, challenge_id: str) -> AdminTaskInfo: 361 """Pre-check the version under review again (#3053): the event's 362 organisers only, on an isolated node, counted against the version's 363 cap (``409 authoring_probe_budget_spent`` once it is spent).""" 364 resp = self._http.request( 365 "POST", 366 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 367 "/assistant-review", 368 ) 369 _raise_for_status(resp) 370 return AdminTaskInfo.model_validate(resp.json()) 371 372 def run_model_review( 373 self, competition_id: str, challenge_id: str, *, revision_id: str = "" 374 ) -> AdminTaskInfo: 375 """Ask the model to read one of this event's uploads — advice.""" 376 params = {"revision_id": revision_id} if revision_id else None 377 resp = self._http.request( 378 "POST", 379 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 380 params=params, 381 ) 382 _raise_for_status(resp) 383 return AdminTaskInfo.model_validate(resp.json()) 384 385 def override_model_review( 386 self, competition_id: str, challenge_id: str, review_id: str, *, note: str 387 ) -> AdminModelReview: 388 """Say the text a review flagged is not an injection (``note`` is 389 the audit's *why*); a version it rejected is decided again.""" 390 resp = self._http.request( 391 "POST", 392 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 393 f"/model-review/{review_id}/override", 394 json={"note": note}, 395 ) 396 _raise_for_status(resp) 397 return AdminModelReview.model_validate(resp.json()) 398 399 def malice_review(self, competition_id: str, challenge_id: str) -> AdminMaliceInfo: 400 """The malice check of one of this event's uploads (#3211), for its 401 reviewers. Never calls a model.""" 402 resp = self._http.request( 403 "GET", 404 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 405 "/malice-review", 406 ) 407 _raise_for_status(resp) 408 return AdminMaliceInfo.model_validate(resp.json()) 409 410 def override_malice_review( 411 self, 412 competition_id: str, 413 challenge_id: str, 414 review_id: str, 415 *, 416 decision: Literal["clear", "confirm"], 417 note: str, 418 ) -> AdminMaliceStanding: 419 """Rule on a malice hold of one of this event's uploads: ``clear`` 420 or ``confirm``, with the audit's *why*.""" 421 resp = self._http.request( 422 "POST", 423 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 424 f"/malice-review/{review_id}/override", 425 json={"decision": decision, "note": note}, 426 ) 427 _raise_for_status(resp) 428 return AdminMaliceStanding.model_validate(resp.json()) 429 430 def read_file( 431 self, competition_id: str, challenge_id: str, path: str 432 ) -> AuthoredChallengeFileBody: 433 """One file's contents — attacker-authored text written to be 434 run. ``binary`` and ``truncated`` say what you are not seeing.""" 435 resp = self._http.request( 436 "POST", 437 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/file", 438 json={"path": path}, 439 ) 440 _raise_for_status(resp) 441 return AuthoredChallengeFileBody.model_validate(resp.json()) 442 443 def review( 444 self, 445 competition_id: str, 446 challenge_id: str, 447 *, 448 status: ReviewStatus, 449 note: str = "", 450 ) -> AdminAuthoredChallengeRow: 451 """Record a content verdict. ``rejected`` requires a note, and 452 ``pending`` withdraws a verdict. Publishing stays with a 453 platform admin, who reviews what the tree runs.""" 454 resp = self._http.request( 455 "POST", 456 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/review", 457 json={"status": status, "note": note}, 458 ) 459 _raise_for_status(resp) 460 return AdminAuthoredChallengeRow.model_validate(resp.json()) 461 462 def release_to_catalog( 463 self, competition_id: str, challenge_id: str 464 ) -> AdminAuthoredChallengeRow: 465 """Put an upload this competition curates in the public catalog 466 (#1989), from that competition — **platform admins only** (owner 467 decision on #2756): an organiser is refused 403. Refused (409, with reasons) for an 468 upload the competition does not curate, and for a tree an automatic 469 review released that no person has reviewed; confirming what such a 470 tree runs stays with a platform admin 471 (``admin.authored_challenges.release_to_catalog``).""" 472 resp = self._http.request( 473 "POST", 474 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 475 "/catalog-release", 476 ) 477 _raise_for_status(resp) 478 return AdminAuthoredChallengeRow.model_validate(resp.json()) 479 480 def points_at_stake( 481 self, competition_id: str, challenge_id: str, action: AuthoredFlowAction 482 ) -> AuthoredPointsAtStake: 483 """The organiser's read of what a removal would decide about the 484 points already scored on an upload this competition holds (#2991).""" 485 resp = self._http.request( 486 "GET", 487 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/points", 488 params={"action": action}, 489 ) 490 _raise_for_status(resp) 491 return AuthoredPointsAtStake.model_validate(resp.json()) 492 493 def unpublish( 494 self, 495 competition_id: str, 496 challenge_id: str, 497 *, 498 reason: str, 499 scores: AuthoredScoresChoice | None = None, 500 ) -> AdminAuthoredChallengeRow: 501 """Take down an upload this competition holds, as one of its full 502 organisers (#1965 H) — with a reason, recorded on the upload's 503 publication history. There is no automatic take-down: a failing 504 re-check or players' reports alert the organisers, and this is what 505 they press. It leaves **every** competition that uses it, not only 506 this one (#2740 D7). Its solves keep their points unless ``scores`` says 507 otherwise; its authoring credit is void while it stays down. Refused (409, with reasons) for an upload 508 the competition does not hold; ``422`` for a blank reason. 509 ``scores`` (#2991): ``removed`` takes the points already scored on it 510 off the board; ``kept`` and ``None`` keep them.""" 511 body: dict[str, str] = {"reason": reason} 512 if scores: 513 body["scores"] = scores 514 resp = self._http.request( 515 "POST", 516 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/unpublish", 517 json=body, 518 ) 519 _raise_for_status(resp) 520 return AdminAuthoredChallengeRow.model_validate(resp.json()) 521 522 def republish( 523 self, competition_id: str, challenge_id: str, *, reason: str = "" 524 ) -> AdminAuthoredChallengeRow: 525 """Restore an upload that was taken down (#1965 H) — the take-down 526 overruled, recorded beside it. Only a take-down is reversible here: 527 publishing a tree the first time stays a platform admin's act. 528 Refused (409, with reasons) when the upload's latest record is not a 529 take-down, or by any publish rule.""" 530 resp = self._http.request( 531 "POST", 532 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/republish", 533 json={"reason": reason}, 534 ) 535 _raise_for_status(resp) 536 return AdminAuthoredChallengeRow.model_validate(resp.json()) 537 538 def revisions( 539 self, competition_id: str, challenge_id: str 540 ) -> builtins.list[AdminAuthoredRevisionRow]: 541 """Every revision of one of this event's uploads, newest first.""" 542 resp = self._http.request( 543 "GET", 544 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/revisions", 545 ) 546 _raise_for_status(resp) 547 return [AdminAuthoredRevisionRow.model_validate(row) for row in resp.json()] 548 549 def revision_diff( 550 self, competition_id: str, challenge_id: str, revision_id: str 551 ) -> AuthoredRevisionDiff: 552 """A proposed revision against the live tree (plan P3.1).""" 553 resp = self._http.request( 554 "GET", 555 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 556 f"/revisions/{revision_id}/diff", 557 ) 558 _raise_for_status(resp) 559 return AuthoredRevisionDiff.model_validate(resp.json()) 560 561 def read_revision_file( 562 self, competition_id: str, challenge_id: str, revision_id: str, path: str 563 ) -> AuthoredChallengeFileBody: 564 """One file of a revision, whole.""" 565 resp = self._http.request( 566 "POST", 567 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 568 f"/revisions/{revision_id}/file", 569 json={"path": path}, 570 ) 571 _raise_for_status(resp) 572 return AuthoredChallengeFileBody.model_validate(resp.json()) 573 574 def review_revision( 575 self, 576 competition_id: str, 577 challenge_id: str, 578 revision_id: str, 579 *, 580 status: ReviewStatus, 581 note: str = "", 582 ) -> AdminAuthoredRevisionRow: 583 """A content verdict on a revision of one of this event's uploads. 584 Applying it stays with a platform admin.""" 585 resp = self._http.request( 586 "POST", 587 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 588 f"/revisions/{revision_id}/review", 589 json={"status": status, "note": note}, 590 ) 591 _raise_for_status(resp) 592 return AdminAuthoredRevisionRow.model_validate(resp.json())
Read and rule on one competition's uploads.
⚠️ The competition is the grant, not a filter: another event's upload answers 404 here, and there is no parameter that widens the listing.
110 def list( 111 self, 112 competition_id: str, 113 *, 114 review_status: ReviewStatus | None = None, 115 smoke: AuthoredSmokeFilter | None = None, 116 revision: ReviewStatus | None = None, 117 order: AuthoredQueueOrder | None = None, 118 destination: AuthoredDestination | None = None, 119 offset: int = 0, 120 limit: int = 50, 121 ) -> PagedList[AdminAuthoredChallengeRow]: 122 """This event's uploads, oldest first. ``smoke``, ``revision``, 123 ``destination`` and ``order`` work as on the platform queue — except that each 124 uploader's ``author_reputation``, and so the ``reputation`` order, 125 counts only the uploads this reviewer may attribute to them.""" 126 params: dict[str, object] = {"offset": offset, "limit": limit} 127 if review_status is not None: 128 params["review_status"] = review_status 129 if smoke is not None: 130 params["smoke"] = smoke 131 if revision is not None: 132 params["revision"] = revision 133 if order is not None: 134 params["order"] = order 135 if destination is not None: 136 params["destination"] = destination 137 resp = self._http.request( 138 "GET", f"/admin/competitions/{competition_id}/authored-challenges", params=params 139 ) 140 _raise_for_status(resp) 141 return _extract_items(resp.json(), AdminAuthoredChallengeRow)
This event's uploads, oldest first. smoke, revision,
destination and order work as on the platform queue — except that each
uploader's author_reputation, and so the reputation order,
counts only the uploads this reviewer may attribute to them.
143 def queue_counts(self, competition_id: str) -> AuthoredQueueCounts: 144 """This event's per-tab counts and longest wait (#2037 B1).""" 145 resp = self._http.request( 146 "GET", f"/admin/competitions/{competition_id}/authored-challenges/queue-counts" 147 ) 148 _raise_for_status(resp) 149 return AuthoredQueueCounts.model_validate(resp.json())
This event's per-tab counts and longest wait (#2037 B1).
151 def get(self, competition_id: str, challenge_id: str) -> AdminAuthoredChallengeDetail: 152 """One upload, with the paths, sizes and execution surface of 153 what is in it.""" 154 resp = self._http.request( 155 "GET", f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 156 ) 157 _raise_for_status(resp) 158 return AdminAuthoredChallengeDetail.model_validate(resp.json())
One upload, with the paths, sizes and execution surface of what is in it.
160 def audit(self, competition_id: str, challenge_id: str) -> AuthoredAuditReport: 161 """Run the challenge audit over this upload. Read ``outcome`` 162 before the counts — ``timed_out`` and ``failed`` are the audit 163 declining to answer, never a clean bill.""" 164 resp = self._http.request( 165 "GET", 166 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/audit", 167 ) 168 _raise_for_status(resp) 169 return AuthoredAuditReport.model_validate(resp.json())
Run the challenge audit over this upload. Read outcome
before the counts — timed_out and failed are the audit
declining to answer, never a clean bill.
171 def flow(self, competition_id: str, challenge_id: str) -> AuthoredFlow: 172 """Where one of this event's uploads is in its lifecycle, and what the 173 caller can do next as the event's reviewer or organiser (#2147).""" 174 resp = self._http.request( 175 "GET", 176 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/flow", 177 ) 178 _raise_for_status(resp) 179 return AuthoredFlow.model_validate(resp.json())
Where one of this event's uploads is in its lifecycle, and what the caller can do next as the event's reviewer or organiser (#2147).
181 def precheck_runs(self, competition_id: str, challenge_id: str) -> PrecheckRunsInfo: 182 """The AI pre-check's managed-eval runs of one of this event's uploads' 183 pending versions, for its reviewers and organisers (#3189).""" 184 resp = self._http.request( 185 "GET", 186 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 187 "/precheck-runs", 188 ) 189 _raise_for_status(resp) 190 return PrecheckRunsInfo.model_validate(resp.json())
The AI pre-check's managed-eval runs of one of this event's uploads' pending versions, for its reviewers and organisers (#3189).
192 def precheck_run_artifact(self, competition_id: str, challenge_id: str, run_id: str) -> bytes: 193 """What one pre-check run's agent left behind, as ``.tar.gz`` bytes, 194 for this event's reviewers and organisers — never its author (#3260).""" 195 resp = self._http.request( 196 "GET", 197 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 198 f"/precheck-runs/{run_id}/artifact", 199 ) 200 _raise_for_status(resp) 201 return bytes(resp.content)
What one pre-check run's agent left behind, as .tar.gz bytes,
for this event's reviewers and organisers — never its author (#3260).
203 def history(self, competition_id: str, challenge_id: str) -> AuthoredHistory: 204 """One of this event's uploads' review history, oldest first (#3093).""" 205 resp = self._http.request( 206 "GET", 207 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/history", 208 ) 209 _raise_for_status(resp) 210 return AuthoredHistory.model_validate(resp.json())
One of this event's uploads' review history, oldest first (#3093).
212 def smoke_logs( 213 self, competition_id: str, challenge_id: str 214 ) -> builtins.list[AuthoredSmokeLogSummary]: 215 """This event's upload's smoke runs, newest first (#2024).""" 216 resp = self._http.request( 217 "GET", 218 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-logs", 219 ) 220 _raise_for_status(resp) 221 return [AuthoredSmokeLogSummary.model_validate(row) for row in resp.json()]
This event's upload's smoke runs, newest first (#2024).
223 def versions( 224 self, competition_id: str, challenge_id: str 225 ) -> builtins.list[AdminAuthoredVersionRow]: 226 """Every stored version of one of this event's uploads (#2037).""" 227 resp = self._http.request( 228 "GET", 229 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/versions", 230 ) 231 _raise_for_status(resp) 232 return [AdminAuthoredVersionRow.model_validate(row) for row in resp.json()]
Every stored version of one of this event's uploads (#2037).
234 def version_diff( 235 self, competition_id: str, challenge_id: str, *, from_version: str, to_version: str 236 ) -> AuthoredVersionDiff: 237 """Two versions of one of this event's uploads compared (#2037).""" 238 resp = self._http.request( 239 "GET", 240 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 241 "/versions/diff", 242 params={"from_version": from_version, "to_version": to_version}, 243 ) 244 _raise_for_status(resp) 245 return AuthoredVersionDiff.model_validate(resp.json())
Two versions of one of this event's uploads compared (#2037).
247 def version_messages( 248 self, competition_id: str, challenge_id: str, version_id: str 249 ) -> builtins.list[AuthoredVersionMessage]: 250 """One version's thread, for this event's reviewers (#2037).""" 251 resp = self._http.request( 252 "GET", 253 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 254 f"/versions/{version_id}/messages", 255 ) 256 _raise_for_status(resp) 257 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()]
One version's thread, for this event's reviewers (#2037).
259 def post_version_message( 260 self, 261 competition_id: str, 262 challenge_id: str, 263 version_id: str, 264 body: str, 265 *, 266 as_review_group: bool = True, 267 ) -> builtins.list[AuthoredVersionMessage]: 268 """Write to an upload's authors as this event's reviewer (#2037).""" 269 resp = self._http.request( 270 "POST", 271 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 272 f"/versions/{version_id}/messages", 273 json={"body": body, "as_review_group": as_review_group}, 274 ) 275 _raise_for_status(resp) 276 return [AuthoredVersionMessage.model_validate(row) for row in resp.json()]
Write to an upload's authors as this event's reviewer (#2037).
278 def version_archive(self, competition_id: str, challenge_id: str, version_id: str) -> bytes: 279 """One version of one of this event's uploads, as ``.tar.gz`` bytes.""" 280 resp = self._http.request( 281 "GET", 282 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 283 f"/versions/{version_id}/archive", 284 ) 285 _raise_for_status(resp) 286 return bytes(resp.content)
One version of one of this event's uploads, as .tar.gz bytes.
288 def smoke_log(self, competition_id: str, challenge_id: str, log_id: str) -> AuthoredSmokeLog: 289 """One smoke run of one of this event's uploads (#2024).""" 290 resp = self._http.request( 291 "GET", 292 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 293 f"/smoke-logs/{log_id}", 294 ) 295 _raise_for_status(resp) 296 return AuthoredSmokeLog.model_validate(resp.json())
One smoke run of one of this event's uploads (#2024).
298 def smoke_live(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 299 """The smoke run of one of this event's uploads that is in flight (#2145).""" 300 resp = self._http.request( 301 "GET", 302 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/smoke-live", 303 ) 304 _raise_for_status(resp) 305 return AuthoredSmokeLive.model_validate(resp.json())
The smoke run of one of this event's uploads that is in flight (#2145).
307 def cancel_smoke(self, competition_id: str, challenge_id: str) -> AuthoredSmokeLive: 308 """Take back a waiting smoke run of one of this event's uploads (#2145).""" 309 resp = self._http.request( 310 "POST", 311 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 312 "/smoke-live/cancel", 313 ) 314 _raise_for_status(resp) 315 return AuthoredSmokeLive.model_validate(resp.json())
Take back a waiting smoke run of one of this event's uploads (#2145).
317 def smoke_run_lines( 318 self, 319 competition_id: str, 320 challenge_id: str, 321 run_id: str, 322 key: str, 323 *, 324 before: int = 0, 325 after: int = 0, 326 limit: int = 200, 327 ) -> AuthoredSmokeStepLines: 328 """A window of one step's full output of one of this event's uploads (#2145).""" 329 resp = self._http.request( 330 "GET", 331 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 332 f"/smoke-runs/{run_id}/lines", 333 params={"step": key, "before": before, "after": after, "limit": limit}, 334 ) 335 _raise_for_status(resp) 336 return AuthoredSmokeStepLines.model_validate(resp.json())
A window of one step's full output of one of this event's uploads (#2145).
338 def model_review(self, competition_id: str, challenge_id: str) -> AdminModelReviewInfo: 339 """The model's reading of one of this event's uploads (#1965 D) — 340 `admin.authored_challenges.model_review`'s answer, for its reviewers.""" 341 resp = self._http.request( 342 "GET", 343 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 344 ) 345 _raise_for_status(resp) 346 return AdminModelReviewInfo.model_validate(resp.json())
The model's reading of one of this event's uploads (#1965 D) —
admin.authored_challenges.model_review's answer, for its reviewers.
348 def assistant_review(self, competition_id: str, challenge_id: str) -> AdminProbeInfo: 349 """The AI pre-check of one of this event's uploads (#2830, #3053) — 350 every model's run, the numbers and the white-box findings, for its 351 reviewers and organisers.""" 352 resp = self._http.request( 353 "GET", 354 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 355 "/assistant-review", 356 ) 357 _raise_for_status(resp) 358 return AdminProbeInfo.model_validate(resp.json())
The AI pre-check of one of this event's uploads (#2830, #3053) — every model's run, the numbers and the white-box findings, for its reviewers and organisers.
360 def rerun_assistant_review(self, competition_id: str, challenge_id: str) -> AdminTaskInfo: 361 """Pre-check the version under review again (#3053): the event's 362 organisers only, on an isolated node, counted against the version's 363 cap (``409 authoring_probe_budget_spent`` once it is spent).""" 364 resp = self._http.request( 365 "POST", 366 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 367 "/assistant-review", 368 ) 369 _raise_for_status(resp) 370 return AdminTaskInfo.model_validate(resp.json())
Pre-check the version under review again (#3053): the event's
organisers only, on an isolated node, counted against the version's
cap (409 authoring_probe_budget_spent once it is spent).
372 def run_model_review( 373 self, competition_id: str, challenge_id: str, *, revision_id: str = "" 374 ) -> AdminTaskInfo: 375 """Ask the model to read one of this event's uploads — advice.""" 376 params = {"revision_id": revision_id} if revision_id else None 377 resp = self._http.request( 378 "POST", 379 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/model-review", 380 params=params, 381 ) 382 _raise_for_status(resp) 383 return AdminTaskInfo.model_validate(resp.json())
Ask the model to read one of this event's uploads — advice.
385 def override_model_review( 386 self, competition_id: str, challenge_id: str, review_id: str, *, note: str 387 ) -> AdminModelReview: 388 """Say the text a review flagged is not an injection (``note`` is 389 the audit's *why*); a version it rejected is decided again.""" 390 resp = self._http.request( 391 "POST", 392 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 393 f"/model-review/{review_id}/override", 394 json={"note": note}, 395 ) 396 _raise_for_status(resp) 397 return AdminModelReview.model_validate(resp.json())
Say the text a review flagged is not an injection (note is
the audit's why); a version it rejected is decided again.
399 def malice_review(self, competition_id: str, challenge_id: str) -> AdminMaliceInfo: 400 """The malice check of one of this event's uploads (#3211), for its 401 reviewers. Never calls a model.""" 402 resp = self._http.request( 403 "GET", 404 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 405 "/malice-review", 406 ) 407 _raise_for_status(resp) 408 return AdminMaliceInfo.model_validate(resp.json())
The malice check of one of this event's uploads (#3211), for its reviewers. Never calls a model.
410 def override_malice_review( 411 self, 412 competition_id: str, 413 challenge_id: str, 414 review_id: str, 415 *, 416 decision: Literal["clear", "confirm"], 417 note: str, 418 ) -> AdminMaliceStanding: 419 """Rule on a malice hold of one of this event's uploads: ``clear`` 420 or ``confirm``, with the audit's *why*.""" 421 resp = self._http.request( 422 "POST", 423 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 424 f"/malice-review/{review_id}/override", 425 json={"decision": decision, "note": note}, 426 ) 427 _raise_for_status(resp) 428 return AdminMaliceStanding.model_validate(resp.json())
Rule on a malice hold of one of this event's uploads: clear
or confirm, with the audit's why.
430 def read_file( 431 self, competition_id: str, challenge_id: str, path: str 432 ) -> AuthoredChallengeFileBody: 433 """One file's contents — attacker-authored text written to be 434 run. ``binary`` and ``truncated`` say what you are not seeing.""" 435 resp = self._http.request( 436 "POST", 437 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/file", 438 json={"path": path}, 439 ) 440 _raise_for_status(resp) 441 return AuthoredChallengeFileBody.model_validate(resp.json())
One file's contents — attacker-authored text written to be
run. binary and truncated say what you are not seeing.
443 def review( 444 self, 445 competition_id: str, 446 challenge_id: str, 447 *, 448 status: ReviewStatus, 449 note: str = "", 450 ) -> AdminAuthoredChallengeRow: 451 """Record a content verdict. ``rejected`` requires a note, and 452 ``pending`` withdraws a verdict. Publishing stays with a 453 platform admin, who reviews what the tree runs.""" 454 resp = self._http.request( 455 "POST", 456 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/review", 457 json={"status": status, "note": note}, 458 ) 459 _raise_for_status(resp) 460 return AdminAuthoredChallengeRow.model_validate(resp.json())
Record a content verdict. rejected requires a note, and
pending withdraws a verdict. Publishing stays with a
platform admin, who reviews what the tree runs.
462 def release_to_catalog( 463 self, competition_id: str, challenge_id: str 464 ) -> AdminAuthoredChallengeRow: 465 """Put an upload this competition curates in the public catalog 466 (#1989), from that competition — **platform admins only** (owner 467 decision on #2756): an organiser is refused 403. Refused (409, with reasons) for an 468 upload the competition does not curate, and for a tree an automatic 469 review released that no person has reviewed; confirming what such a 470 tree runs stays with a platform admin 471 (``admin.authored_challenges.release_to_catalog``).""" 472 resp = self._http.request( 473 "POST", 474 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 475 "/catalog-release", 476 ) 477 _raise_for_status(resp) 478 return AdminAuthoredChallengeRow.model_validate(resp.json())
Put an upload this competition curates in the public catalog
(#1989), from that competition — platform admins only (owner
decision on #2756): an organiser is refused 403. Refused (409, with reasons) for an
upload the competition does not curate, and for a tree an automatic
review released that no person has reviewed; confirming what such a
tree runs stays with a platform admin
(admin.authored_challenges.release_to_catalog).
480 def points_at_stake( 481 self, competition_id: str, challenge_id: str, action: AuthoredFlowAction 482 ) -> AuthoredPointsAtStake: 483 """The organiser's read of what a removal would decide about the 484 points already scored on an upload this competition holds (#2991).""" 485 resp = self._http.request( 486 "GET", 487 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/points", 488 params={"action": action}, 489 ) 490 _raise_for_status(resp) 491 return AuthoredPointsAtStake.model_validate(resp.json())
The organiser's read of what a removal would decide about the points already scored on an upload this competition holds (#2991).
493 def unpublish( 494 self, 495 competition_id: str, 496 challenge_id: str, 497 *, 498 reason: str, 499 scores: AuthoredScoresChoice | None = None, 500 ) -> AdminAuthoredChallengeRow: 501 """Take down an upload this competition holds, as one of its full 502 organisers (#1965 H) — with a reason, recorded on the upload's 503 publication history. There is no automatic take-down: a failing 504 re-check or players' reports alert the organisers, and this is what 505 they press. It leaves **every** competition that uses it, not only 506 this one (#2740 D7). Its solves keep their points unless ``scores`` says 507 otherwise; its authoring credit is void while it stays down. Refused (409, with reasons) for an upload 508 the competition does not hold; ``422`` for a blank reason. 509 ``scores`` (#2991): ``removed`` takes the points already scored on it 510 off the board; ``kept`` and ``None`` keep them.""" 511 body: dict[str, str] = {"reason": reason} 512 if scores: 513 body["scores"] = scores 514 resp = self._http.request( 515 "POST", 516 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/unpublish", 517 json=body, 518 ) 519 _raise_for_status(resp) 520 return AdminAuthoredChallengeRow.model_validate(resp.json())
Take down an upload this competition holds, as one of its full
organisers (#1965 H) — with a reason, recorded on the upload's
publication history. There is no automatic take-down: a failing
re-check or players' reports alert the organisers, and this is what
they press. It leaves every competition that uses it, not only
this one (#2740 D7). Its solves keep their points unless scores says
otherwise; its authoring credit is void while it stays down. Refused (409, with reasons) for an upload
the competition does not hold; 422 for a blank reason.
scores (#2991): removed takes the points already scored on it
off the board; kept and None keep them.
522 def republish( 523 self, competition_id: str, challenge_id: str, *, reason: str = "" 524 ) -> AdminAuthoredChallengeRow: 525 """Restore an upload that was taken down (#1965 H) — the take-down 526 overruled, recorded beside it. Only a take-down is reversible here: 527 publishing a tree the first time stays a platform admin's act. 528 Refused (409, with reasons) when the upload's latest record is not a 529 take-down, or by any publish rule.""" 530 resp = self._http.request( 531 "POST", 532 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/republish", 533 json={"reason": reason}, 534 ) 535 _raise_for_status(resp) 536 return AdminAuthoredChallengeRow.model_validate(resp.json())
Restore an upload that was taken down (#1965 H) — the take-down overruled, recorded beside it. Only a take-down is reversible here: publishing a tree the first time stays a platform admin's act. Refused (409, with reasons) when the upload's latest record is not a take-down, or by any publish rule.
538 def revisions( 539 self, competition_id: str, challenge_id: str 540 ) -> builtins.list[AdminAuthoredRevisionRow]: 541 """Every revision of one of this event's uploads, newest first.""" 542 resp = self._http.request( 543 "GET", 544 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}/revisions", 545 ) 546 _raise_for_status(resp) 547 return [AdminAuthoredRevisionRow.model_validate(row) for row in resp.json()]
Every revision of one of this event's uploads, newest first.
549 def revision_diff( 550 self, competition_id: str, challenge_id: str, revision_id: str 551 ) -> AuthoredRevisionDiff: 552 """A proposed revision against the live tree (plan P3.1).""" 553 resp = self._http.request( 554 "GET", 555 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 556 f"/revisions/{revision_id}/diff", 557 ) 558 _raise_for_status(resp) 559 return AuthoredRevisionDiff.model_validate(resp.json())
A proposed revision against the live tree (plan P3.1).
561 def read_revision_file( 562 self, competition_id: str, challenge_id: str, revision_id: str, path: str 563 ) -> AuthoredChallengeFileBody: 564 """One file of a revision, whole.""" 565 resp = self._http.request( 566 "POST", 567 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 568 f"/revisions/{revision_id}/file", 569 json={"path": path}, 570 ) 571 _raise_for_status(resp) 572 return AuthoredChallengeFileBody.model_validate(resp.json())
One file of a revision, whole.
574 def review_revision( 575 self, 576 competition_id: str, 577 challenge_id: str, 578 revision_id: str, 579 *, 580 status: ReviewStatus, 581 note: str = "", 582 ) -> AdminAuthoredRevisionRow: 583 """A content verdict on a revision of one of this event's uploads. 584 Applying it stays with a platform admin.""" 585 resp = self._http.request( 586 "POST", 587 f"/admin/competitions/{competition_id}/authored-challenges/{challenge_id}" 588 f"/revisions/{revision_id}/review", 589 json={"status": status, "note": note}, 590 ) 591 _raise_for_status(resp) 592 return AdminAuthoredRevisionRow.model_validate(resp.json())
A content verdict on a revision of one of this event's uploads. Applying it stays with a platform admin.