Release v0.51.330 — Release KT (#3716 api docstring backfill, partial) (#3841)
Some checks failed
Release & Docker / release (push) Has been cancelled

Backfill docstrings for api/oauth.py + api/kanban_bridge.py (51 functions, verified accurate to current behavior). Conflicted files dropped for follow-up. Docstring-only, no behavior change. Full suite 8275, CI 11/11. Co-authored-by: camr <camr@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-08 12:22:22 -07:00
committed by GitHub
parent a0e5b9042f
commit cf07c0a02d
3 changed files with 56 additions and 0 deletions

View File

@@ -3,6 +3,11 @@
## [Unreleased]
## [v0.51.330] — 2026-06-08 — Release KT (api docstring backfill)
### Changed
- **Internal: backfilled docstrings for 51 previously-undocumented functions** in `api/oauth.py` and `api/kanban_bridge.py` (developer-facing documentation only; no behavior change). (#3716, @camr)
## [v0.51.329] — 2026-06-08 — Release KS (session-list + startup latency)
### Fixed

View File

@@ -25,6 +25,7 @@ _TASK_PREFIX = "/api/kanban/tasks/"
def _kb():
"""Lazily import hermes_cli.kanban_db to avoid circular imports at module load."""
from hermes_cli import kanban_db as kb
return kb
@@ -78,12 +79,14 @@ def _normalise_board_or_raise(raw):
def _conn(board=None):
"""Initialize the kanban DB for the given board slug and return a context-managed sqlite connection."""
kb = _kb()
kb.init_db(board=board)
return kb.connect(board=board)
def _obj_dict(value):
"""Coerce a dataclass or arbitrary object to a plain dict; returns None unchanged."""
if value is None:
return None
if is_dataclass(value):
@@ -94,6 +97,7 @@ def _obj_dict(value):
def _task_dict(task):
"""Convert a task to a JSON-serialisable dict, annotating it with computed age_seconds and progress fields."""
data = _obj_dict(task)
if not data:
return data
@@ -108,6 +112,7 @@ def _task_dict(task):
def _latest_event_id(conn) -> int:
"""Return the highest event id in task_events, falling back to 0 when the table is empty."""
try:
row = conn.execute("SELECT COALESCE(MAX(id), 0) AS latest FROM task_events").fetchone()
return int(row["latest"] or 0)
@@ -116,6 +121,7 @@ def _latest_event_id(conn) -> int:
def _bool_query(parsed, name: str, default: bool = False) -> bool:
"""Extract a boolean query param, treating 1/true/yes/on (case-insensitive) as True."""
raw = (parse_qs(parsed.query or "").get(name) or [None])[0]
if raw is None:
return default
@@ -123,11 +129,13 @@ def _bool_query(parsed, name: str, default: bool = False) -> bool:
def _str_query(parsed, name: str):
"""Extract a string query param, returning None when the param is absent or blank."""
raw = (parse_qs(parsed.query or "").get(name) or [None])[0]
return str(raw).strip() or None if raw is not None else None
def _int_query(parsed, name: str, default=None, *, minimum=None, maximum=None):
"""Extract an integer query param, clamped to [minimum, maximum] when those bounds are provided."""
raw = _str_query(parsed, name)
if raw is None:
return default
@@ -143,6 +151,7 @@ def _int_query(parsed, name: str, default=None, *, minimum=None, maximum=None):
def _task_link_counts(conn, tasks):
"""Return a dict mapping each task id to its {parents, children} dependency link counts."""
counts = {task.id: {"parents": 0, "children": 0} for task in tasks}
try:
rows = conn.execute("SELECT parent_id, child_id FROM task_links").fetchall()
@@ -155,6 +164,7 @@ def _task_link_counts(conn, tasks):
def _comment_counts(conn):
"""Return a dict mapping each task id to its total comment count across the board."""
try:
rows = conn.execute(
"SELECT task_id, COUNT(*) AS n FROM task_comments GROUP BY task_id"
@@ -165,6 +175,7 @@ def _comment_counts(conn):
def _board_payload(parsed):
"""Build the full board JSON payload: kanban columns with tasks, filter state, and latest_event_id."""
board = _resolve_board(parsed)
kb = _kb()
tenant = _str_query(parsed, "tenant")
@@ -230,6 +241,7 @@ def _board_payload(parsed):
def _validate_status(status: str) -> str:
"""Validate a status string against BOARD_COLUMNS, raising ValueError for unrecognised values."""
value = str(status or "").strip().lower()
allowed = set(BOARD_COLUMNS) | {"archived"}
if value not in allowed:
@@ -304,6 +316,7 @@ def _set_status_direct(conn, task_id: str, new_status: str) -> bool:
def _create_task_payload(body: dict, *, board=None):
"""Create a new task from a parsed request body and return the task dict in a read_only envelope."""
title = str(body.get("title") or "").strip()
if not title:
raise ValueError("title is required")
@@ -336,6 +349,7 @@ def _create_task_payload(body: dict, *, board=None):
def _patch_task(conn, task_id: str, body: dict):
"""Apply a partial update to a task, routing status transitions through structured verbs (complete, block, archive)."""
kb = _kb()
task = kb.get_task(conn, task_id)
if not task:
@@ -425,6 +439,7 @@ def _patch_task(conn, task_id: str, body: dict):
def _patch_task_payload(task_id: str, body: dict, *, board=None):
"""Validate task_id, open a connection, and delegate field-level updates to _patch_task."""
task_id = str(task_id or "").strip()
if not task_id:
raise ValueError("task_id is required")
@@ -435,6 +450,7 @@ def _patch_task_payload(task_id: str, body: dict, *, board=None):
def _comment_payload(task_id: str, body: dict, *, board=None):
"""Add a comment to a task and return the new comment_id in a read_only envelope."""
task_id = str(task_id or "").strip()
comment_body = str(body.get("body") or "").strip()
if not task_id:
@@ -450,6 +466,7 @@ def _comment_payload(task_id: str, body: dict, *, board=None):
def _link_tasks_payload(body: dict, *, unlink: bool = False, board=None):
"""Create or delete a parent-child dependency link between two tasks."""
parent_id = str(body.get("parent_id") or "").strip()
child_id = str(body.get("child_id") or "").strip()
if not parent_id or not child_id:
@@ -467,6 +484,7 @@ def _link_tasks_payload(body: dict, *, unlink: bool = False, board=None):
return {"ok": True, "parent_id": parent_id, "child_id": child_id, "read_only": False}
def _links_for(conn, task_id: str) -> dict:
"""Return {parents: [...], children: [...]} dependency id lists for a task."""
kb = _kb()
return {
"parents": kb.parent_ids(conn, task_id),
@@ -475,6 +493,7 @@ def _links_for(conn, task_id: str) -> dict:
def _task_detail_payload(task_id: str, *, board=None):
"""Return the full task detail: task dict, comments, events, dependency links, and run history."""
kb = _kb()
with _conn(board=board) as conn:
task = kb.get_task(conn, task_id)
@@ -491,6 +510,7 @@ def _task_detail_payload(task_id: str, *, board=None):
def _events_payload(parsed):
"""Return paginated task events from the board's event log, starting after the ?since= cursor."""
board = _resolve_board(parsed)
since = _int_query(parsed, "since", 0, minimum=0)
limit = _int_query(parsed, "limit", 200, minimum=1, maximum=200)
@@ -523,6 +543,7 @@ def _events_payload(parsed):
def _config_payload(*, board=None):
"""Return kanban configuration: column names, known assignees, and lane/display settings from hermes_cli.config."""
kb = _kb()
try:
with _conn(board=board) as conn:
@@ -551,6 +572,7 @@ def _config_payload(*, board=None):
def _stats_payload(*, board=None):
"""Return per-status and per-assignee task counts for the board."""
kb = _kb()
with _conn(board=board) as conn:
if hasattr(kb, "board_stats"):
@@ -569,6 +591,7 @@ def _stats_payload(*, board=None):
def _assignees_payload(*, board=None):
"""Return the list of known assignees derived from task history."""
kb = _kb()
with _conn(board=board) as conn:
try:
@@ -582,6 +605,7 @@ def _assignees_payload(*, board=None):
def _task_log_payload(parsed, task_id: str):
"""Return the raw worker log content and on-disk metadata for a task's dispatcher run."""
board = _resolve_board(parsed)
kb = _kb()
tail = _int_query(parsed, "tail", None, minimum=1, maximum=2_000_000)
@@ -607,6 +631,7 @@ def _task_log_payload(parsed, task_id: str):
def _bulk_tasks_payload(body: dict, *, board=None):
"""Apply a common mutation (archive/status/assignee/priority) to multiple task ids in a single transaction."""
ids = [str(i).strip() for i in (body.get("ids") or []) if str(i).strip()]
if not ids:
raise ValueError("ids is required")
@@ -644,6 +669,7 @@ def _bulk_tasks_payload(body: dict, *, board=None):
def _dispatch_payload(parsed):
"""Trigger a single-pass kanban dispatcher run and return the dispatch result."""
board = _resolve_board(parsed)
kb = _kb()
dry_run = _bool_query(parsed, "dry_run", False)
@@ -661,6 +687,7 @@ def _dispatch_payload(parsed):
def _task_action_payload(task_id: str, body: dict, action: str, *, board=None):
"""Execute a named action (block or unblock) on a task and return the updated task dict."""
kb = _kb()
task_id = str(task_id or "").strip()
if not task_id:

View File

@@ -83,6 +83,7 @@ def resolve_runtime_provider_with_anthropic_env_lock(resolver, *args, **kwargs):
def _normalize_onboarding_oauth_provider(provider: str) -> str:
"""Normalize Anthropic aliases (claude, claude-code) to 'anthropic'; defaults to 'openai-codex' when blank."""
provider = str(provider or "").strip().lower()
if provider in _ANTHROPIC_PROVIDER_ALIASES:
return "anthropic"
@@ -90,6 +91,7 @@ def _normalize_onboarding_oauth_provider(provider: str) -> str:
def _get_active_hermes_home() -> Path:
"""Return the active Hermes profile home directory, falling back to ~/.hermes when profile resolution fails."""
try:
from api.profiles import get_active_hermes_home
@@ -156,6 +158,7 @@ def _write_auth_json(data: dict[str, Any], auth_path: Path | None = None) -> Pat
def _now_iso() -> str:
"""Return the current UTC time as an ISO-8601 string ending in Z."""
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
@@ -230,6 +233,7 @@ def _persist_codex_credentials(hermes_home: Path, token_data: dict[str, Any]) ->
# Backward-compatible wrapper used by older code/tests.
def _save_codex_credentials(token_data):
"""Backward-compatible wrapper: persist Codex OAuth tokens to the active-profile auth.json."""
return _persist_codex_credentials(_get_active_hermes_home(), token_data)
@@ -338,6 +342,7 @@ def _link_anthropic_credentials(hermes_home: Path) -> None:
def _anthropic_public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Build the browser-safe start payload for an Anthropic credential-linking flow, omitting server-side secrets."""
payload: dict[str, Any] = {
"ok": True,
"provider": "anthropic",
@@ -357,6 +362,7 @@ def _anthropic_public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[
def _anthropic_public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Build the browser-safe status payload for an Anthropic flow, replacing internal error with a safe string."""
payload: dict[str, Any] = {
"ok": True,
"provider": "anthropic",
@@ -369,6 +375,7 @@ def _anthropic_public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict
def _spawn_anthropic_credential_worker(flow_id: str) -> None:
"""Launch a daemon thread that polls for Claude Code credentials and transitions the Anthropic flow to success."""
worker = threading.Thread(
target=_run_anthropic_credential_worker, args=(flow_id,), daemon=True,
)
@@ -462,6 +469,7 @@ def _remove_anthropic_link_marker(hermes_home: Path) -> None:
# ── Codex protocol ──────────────────────────────────────────────────────────
def _json_request(url: str, payload: dict[str, Any], *, form: bool = False) -> dict[str, Any]:
"""POST a JSON or form-encoded payload to url and return the parsed JSON response."""
if form:
data = urllib.parse.urlencode(payload).encode("utf-8")
content_type = "application/x-www-form-urlencoded"
@@ -479,10 +487,12 @@ def _json_request(url: str, payload: dict[str, Any], *, form: bool = False) -> d
def _request_codex_user_code() -> dict[str, Any]:
"""Request a new device-auth user code and device_auth_id from the Codex endpoint."""
return _json_request(CODEX_USER_CODE_URL, {"client_id": CODEX_CLIENT_ID})
def _poll_codex_authorization(device_auth_id: str, user_code: str) -> dict[str, Any] | None:
"""Poll the Codex device token endpoint; returns None on 403/404 (not yet authorized) or raises otherwise."""
try:
return _json_request(
CODEX_DEVICE_TOKEN_URL,
@@ -495,6 +505,7 @@ def _poll_codex_authorization(device_auth_id: str, user_code: str) -> dict[str,
def _exchange_codex_authorization(authorization_code: str, code_verifier: str) -> dict[str, Any]:
"""Exchange a Codex authorization code and PKCE verifier for access/refresh tokens."""
return _json_request(
CODEX_TOKEN_URL,
{
@@ -509,6 +520,7 @@ def _exchange_codex_authorization(authorization_code: str, code_verifier: str) -
def _codex_public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Build the browser-safe start payload for a Codex device-code flow, including user_code and verification_uri."""
return {
"ok": True,
"provider": "openai-codex",
@@ -522,6 +534,7 @@ def _codex_public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[str,
def _codex_public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Build the browser-safe status payload for a Codex flow, capping error strings at 200 characters."""
payload = {
"ok": True,
"provider": "openai-codex",
@@ -534,6 +547,7 @@ def _codex_public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict[str
def _public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Dispatch to the provider-specific start payload builder based on flow['provider']."""
provider = flow.get("provider", "openai-codex")
if provider == "anthropic":
return _anthropic_public_start_payload(flow_id, flow)
@@ -541,6 +555,7 @@ def _public_start_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
def _public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]:
"""Dispatch to the provider-specific status payload builder based on flow['provider']."""
provider = flow.get("provider", "openai-codex")
if provider == "anthropic":
return _anthropic_public_status_payload(flow_id, flow)
@@ -548,6 +563,7 @@ def _public_status_payload(flow_id: str, flow: dict[str, Any]) -> dict[str, Any]
def _drop_sensitive_flow_fields(flow: dict[str, Any]) -> None:
"""Remove device codes, authorization codes, and token material from a flow dict in place."""
for key in (
"device_auth_id",
"authorization_code",
@@ -560,6 +576,7 @@ def _drop_sensitive_flow_fields(flow: dict[str, Any]) -> None:
def _cleanup_oauth_flows(now: float | None = None) -> None:
"""Expire pending flows past their deadline and purge terminal flows older than 300 seconds from memory."""
now = now or time.time()
cutoff = now - 300
with _OAUTH_FLOWS_LOCK:
@@ -573,11 +590,13 @@ def _cleanup_oauth_flows(now: float | None = None) -> None:
def _spawn_codex_oauth_worker(flow_id: str) -> None:
"""Launch a daemon thread that drives the Codex device-code polling and token exchange loop."""
worker = threading.Thread(target=_run_codex_oauth_worker, args=(flow_id,), daemon=True)
worker.start()
def _set_flow_status(flow_id: str, status: str, **fields: Any) -> None:
"""Update a flow's status under the lock, then strip sensitive fields on terminal transitions."""
with _OAUTH_FLOWS_LOCK:
flow = _OAUTH_FLOWS.get(flow_id)
if not flow:
@@ -590,6 +609,7 @@ def _set_flow_status(flow_id: str, status: str, **fields: Any) -> None:
def _run_codex_oauth_worker(flow_id: str) -> None:
"""Drive the Codex device-code polling loop until the user authorizes, the flow cancels, or it expires."""
while True:
with _OAUTH_FLOWS_LOCK:
flow = dict(_OAUTH_FLOWS.get(flow_id) or {})
@@ -726,6 +746,7 @@ def start_onboarding_oauth_flow(body: dict[str, Any] | None) -> dict[str, Any]:
def poll_onboarding_oauth_flow(flow_id: str) -> dict[str, Any]:
"""Return the current browser-safe status for an in-flight OAuth flow, expiring it if past its deadline."""
_cleanup_oauth_flows()
fid = str(flow_id or "").strip()
if not fid:
@@ -742,6 +763,7 @@ def poll_onboarding_oauth_flow(flow_id: str) -> dict[str, Any]:
def cancel_onboarding_oauth_flow(body: dict[str, Any] | None) -> dict[str, Any]:
"""Cancel a pending OAuth flow by flow_id and return the final status payload."""
fid = str((body or {}).get("flow_id") or "").strip()
if not fid:
raise ValueError("flow_id is required")
@@ -763,8 +785,10 @@ def cancel_onboarding_oauth_flow(body: dict[str, Any] | None) -> dict[str, Any]:
# Backward-compatible names from the abandoned spike. They intentionally do not
# expose provider device secrets to callers anymore.
def start_codex_device_code():
"""Backward-compatible shim: start a Codex device-code flow via start_onboarding_oauth_flow."""
return start_onboarding_oauth_flow({"provider": "openai-codex"})
def poll_codex_token(device_code, interval=5):
"""Backward-compatible stub that always yields an error directing callers to the /api/onboarding/oauth/poll endpoint."""
yield {"status": "error", "error": "Use /api/onboarding/oauth/poll with flow_id"}