Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/routers/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def is_public_ip(ip):
@router.get("/presence")
def health_presence():
"""Human presence detection — always returns present for headless operation."""
return {"present": False, "detection": "disabled"}
return {"present": False, "evidence": [], "detection": "disabled"}

@router.get("/invariants")
def health_invariants():
Expand Down Expand Up @@ -495,7 +495,8 @@ def health_tools():
"gui_path": str(wininspect.gui_exe()) if wininspect.gui_exe().is_file() else None,
}
missing = [name for name, info in details.items() if not info["present"]]
return {"ok": len(missing) == 0, "missing": missing, "tools": details}
all_ok = len(missing) == 0
return {"ok": all_ok, "all_ok": all_ok, "missing": missing, "tools": details}


@router.get("/storage")
Expand Down
23 changes: 23 additions & 0 deletions api/routers/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ async def lifecycle_status():
},
"user_dir": os.getenv("WINEBOT_USER_DIR"),
"processes": processes,
# Contract-required fields (override health status with lifecycle status)
"status": "pending" if _shutdown_in_progress else "idle",
"pending_action": "shutdown" if _shutdown_in_progress else None,
"remaining_seconds": max(0, int(_shutdown_started_at + float(config.WINEBOT_SHUTDOWN_GUARD_TTL_SECONDS) - time.time())) if _shutdown_in_progress else None,
}
emit_operation_timing(
session_dir,
Expand Down Expand Up @@ -475,6 +479,10 @@ async def lifecycle_shutdown(
power_off: bool = False,
):
"""Gracefully stop components and terminate the container process."""
if delay < 0:
raise HTTPException(status_code=422, detail="delay must be non-negative")
if delay > 300:
raise HTTPException(status_code=422, detail="delay must not exceed 300 seconds")
global _shutdown_in_progress, _shutdown_mode, _shutdown_started_at
op_started = time.perf_counter()
session_dir = read_session_dir()
Expand Down Expand Up @@ -567,6 +575,9 @@ async def lifecycle_shutdown(
shutdown_payload: dict[str, Any] = {
"status": "shutting_down",
"delay_seconds": delay,
"cancel_before": int(time.time() + delay),
"cancel_command": "POST /lifecycle/cancel",
"human_present": False,
"results": results
}
emit_operation_timing(
Expand All @@ -585,6 +596,18 @@ async def lifecycle_shutdown(
return shutdown_payload


@router.post("/lifecycle/cancel")
async def lifecycle_cancel():
"""Cancel a pending shutdown. Returns cancelled or no_pending."""
global _shutdown_in_progress, _shutdown_mode, _shutdown_started_at
if not _shutdown_in_progress:
return {"status": "no_pending"}
_shutdown_in_progress = False
_shutdown_mode = ""
_shutdown_started_at = 0.0
return {"status": "cancelled"}


@router.post("/lifecycle/reset_workspace")
async def reset_workspace():
"""Force Wine desktop to be maximized and undecorated."""
Expand Down
2 changes: 1 addition & 1 deletion api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async def verify_token_logic(request: Request, api_key: str = Security(api_key_h
if expected_token:
provided_token = (api_key or "").strip()
if not provided_token or not hmac.compare_digest(provided_token, expected_token):
raise HTTPException(status_code=403, detail="Invalid or missing API Token")
raise HTTPException(status_code=401, detail="Invalid or missing API Token")
return api_key


Expand Down
2 changes: 1 addition & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_health_check(
def test_health_check_unauthorized(mock_run):
with patch.dict(os.environ, {"API_TOKEN": "test-token", "WINEBOT_RECORD": "1"}):
response = client.get("/health", headers={"X-API-Key": "wrong"})
assert response.status_code == 403
assert response.status_code == 401


@patch(
Expand Down
Loading