feat: per-site SHIFT path with video search, fix agent token injection, accept cpu_temp

- deploy_agent.sh: agent v3.3-shift — use dashboard-configured SHIFT path for
  file fetch/search (SR/SHIFT sites have no TAKELEAP folder), date fast-path;
  fix installer discarding the injected agent token (sentinel was being
  rewritten by the server's placeholder replace, agents ended up tokenless)
- central_api_prototype.py: replace only the token assignment when serving the
  installer; add cpu_temp to TelemetryPayload (agents already send it, it was
  silently dropped); new endpoints set-shift-path, request-search,
  search-results; heartbeat response now carries shift_path + pending search
- commands.json: restore last_reboot/dummy, add update_diag diagnostic
- CLAUDE.md: document deployment layout, procedures, and gotchas
This commit is contained in:
2026-07-16 17:31:33 +05:30
parent b9aeedc5ef
commit c929176cfa
4 changed files with 323 additions and 9 deletions

View File

@@ -325,6 +325,7 @@ class TelemetryPayload(BaseModel):
memory_free_gb: float
disks: List[Dict[str, Any]]
gpus: List[Dict[str, Any]]
cpu_temp: Optional[float] = None
agent_version: Optional[str] = None
speed_upload_mbps: Optional[float] = None
speed_download_mbps: Optional[float] = None
@@ -335,6 +336,12 @@ class CommandResultPayload(BaseModel):
stdout: str
stderr: str
class SearchResultsPayload(BaseModel):
query: str
results: List[Dict[str, Any]]
searched_path: Optional[str] = None
error: Optional[str] = None
class LoginPayload(BaseModel):
username: str
password: str
@@ -735,6 +742,57 @@ async def request_file(client_id: str, filename: str, current_user: str = Depend
append_to_logs("file_requested", client_id, {"filename": safe_name})
return {"status": "success", "message": f"File '{safe_name}' requested from {client_id}"}
@app.post("/api/set-shift-path")
async def set_shift_path(client_id: str, shift_path: str = "", current_user: str = Depends(get_current_user)):
"""
Store where this node keeps its SHIFT folder (varies per site:
/mnt/<disk-uuid>/SR/SHIFT or /mnt/<disk-uuid>/TAKELEAP/SHIFT).
Delivered to the agent in every heartbeat response.
"""
path = shift_path.strip()
result = clients_collection.update_one({"_id": client_id}, {"$set": {"shift_path": path}})
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Unknown client_id")
append_to_logs("shift_path_set", client_id, {"shift_path": path})
return {"status": "success", "shift_path": path}
@app.post("/api/request-search")
async def request_search(client_id: str, query: str, current_user: str = Depends(get_current_user)):
"""
Queue a filename search in the node's SHIFT folder. The agent picks the
query up on its next heartbeat and posts matches to /api/search-results.
"""
q = query.strip()
if not q:
raise HTTPException(status_code=400, detail="Empty search query")
now_str = datetime.now().isoformat()
result = clients_collection.update_one(
{"_id": client_id},
{"$set": {"pending_search": q,
"file_search": {"query": q, "status": "searching", "results": [], "requested_at": now_str}}}
)
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Unknown client_id")
append_to_logs("search_requested", client_id, {"query": q})
return {"status": "success"}
@app.post("/api/search-results")
async def receive_search_results(client_id: str, payload: SearchResultsPayload, token_valid: bool = Depends(require_agent_token)):
now_str = datetime.now().isoformat()
clients_collection.update_one(
{"_id": client_id},
{"$set": {"file_search": {
"query": payload.query,
"status": "error" if payload.error else "done",
"results": payload.results[:200],
"searched_path": payload.searched_path,
"error": payload.error,
"completed_at": now_str,
}}}
)
append_to_logs("search_results", client_id, {"query": payload.query, "count": len(payload.results), "error": payload.error})
return {"status": "success"}
@app.get("/api/get-file-request")
async def get_file_request(client_id: str, token_valid: bool = Depends(require_agent_token)):
if not client_id or not client_id.strip():
@@ -907,7 +965,14 @@ async def get_agent_installer(x_agent_token: str = Header(None), token: Optional
# the live agent token so a freshly downloaded installer bakes it into the agent.
with open(path, "rb") as f:
content = f.read().replace(b"\r\n", b"\n")
content = content.replace(b"__AGENT_TOKEN__", AGENT_TOKEN.encode("utf-8"))
# Replace only the assignment, not the "was I injected?" sentinel comparison
# a few lines below it — a blanket replace turns that check into an
# always-true self-comparison and the installer discards the injected token.
content = content.replace(
b'AGENT_TOKEN="__AGENT_TOKEN__"',
b'AGENT_TOKEN="' + AGENT_TOKEN.encode("utf-8") + b'"',
1,
)
return Response(
content=content,
media_type="text/x-shellscript",
@@ -949,20 +1014,26 @@ async def receive_heartbeat(client_id: str, payload: TelemetryPayload,
)
doc = clients_collection.find_one_and_update(
{"_id": client_id},
{"$set": {"pending_command": "none", "pending_file_request": "none"}},
{"$set": {"pending_command": "none", "pending_file_request": "none", "pending_search": "none"}},
return_document=False
)
cmd = "none"
fname = "none"
search = "none"
shift_path = None
if doc:
cmd = doc.get("pending_command", "none")
fname = doc.get("pending_file_request", "none")
search = doc.get("pending_search", "none") or "none"
shift_path = doc.get("shift_path") or None
if cmd != "none":
append_to_logs("command_polled", client_id, {"command": cmd})
return {
"status": "success",
"command": cmd,
"filename": fname
"filename": fname,
"search": search,
"shift_path": shift_path
}