Jarvis2345 commited on
Commit
850b3bb
Β·
verified Β·
1 Parent(s): 26466a0

deploy(S4): Blender headless pipeline + WebAR client + backend fixes

Browse files
backend/routes/mobile_bridge_routes.py CHANGED
@@ -257,9 +257,24 @@ async def command(request: Request) -> dict[str, Any]:
257
  "pc_count": delivered,
258
  }
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  try:
261
  from modules.max_autonomy import execute_task
262
- # No PC connected: run through the safety-gated coordinator in
 
263
  # record+sync mode β€” logged, risk-assessed and pushed to the AR HUD,
264
  # without faking hardware control.
265
  result = execute_task(
 
257
  "pc_count": delivered,
258
  }
259
 
260
+ # PC offline but the command was accepted onto the replay queue: say so
261
+ # plainly. "Queued" and "recorded" are different promises β€” one will actually
262
+ # run, the other never will β€” and the user has to be able to tell them apart.
263
+ if outcome.get("queued"):
264
+ depth = int(outcome.get("queue_depth") or 0)
265
+ return {
266
+ "ok": True,
267
+ "spoken": ["Your PC is offline. I'll run this the moment it comes "
268
+ f"back online.{f' ({depth} waiting.)' if depth > 1 else ''}"],
269
+ "status": "queued",
270
+ "action": "pc_queue",
271
+ "queue_depth": depth,
272
+ }
273
+
274
  try:
275
  from modules.max_autonomy import execute_task
276
+ # No PC connected AND not queueable (e.g. an unlock, which must never be
277
+ # replayed later): run through the safety-gated coordinator in
278
  # record+sync mode β€” logged, risk-assessed and pushed to the AR HUD,
279
  # without faking hardware control.
280
  result = execute_task(
backend/services/usb_monitor.py CHANGED
@@ -1,9 +1,27 @@
1
  import asyncio
2
  import logging
3
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  if os.environ.get("CLOUD_ENV", "false").lower() != "true":
5
- import wmi
6
- import pythoncom
 
 
 
 
7
  else:
8
  wmi = None
9
  pythoncom = None
 
1
  import asyncio
2
  import logging
3
  import os
4
+ # wmi/pythoncom are OPTIONAL, and the import must never be fatal.
5
+ #
6
+ # This was a bare `import wmi` on the non-cloud path. wmi is not installed in
7
+ # this repo's venv, so importing backend.services.usb_monitor raised
8
+ # ModuleNotFoundError β€” and usb_vault.py imports get_db_path FROM this module,
9
+ # so the entire secrets vault became unimportable on the very PC it protects.
10
+ # Every caller wrapped that in `except Exception: return ""`, so the vault did
11
+ # not fail loudly; it just answered "no such secret" forever. That is how
12
+ # pc_relay_client's vault token lookup came back empty and how the vault ended up
13
+ # holding nothing at all.
14
+ #
15
+ # WMI is used only to map drive letters inside the monitor loop, which already
16
+ # handles wmi being None. A missing USB-enumeration dependency must not take the
17
+ # credential store down with it.
18
  if os.environ.get("CLOUD_ENV", "false").lower() != "true":
19
+ try:
20
+ import wmi
21
+ import pythoncom
22
+ except Exception: # pragma: no cover - depends on host packages
23
+ wmi = None
24
+ pythoncom = None
25
  else:
26
  wmi = None
27
  pythoncom = None
backend/ws/agent_ws.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
  import json
3
  import logging
4
  import os
 
5
  from fastapi import WebSocket
6
  import uuid
7
 
@@ -62,6 +63,12 @@ class ConnectionManager:
62
  # desktop then refused outright (locked session) or failed to run. The
63
  # phone showed success for work that never happened.
64
  self.pending_results: dict[str, asyncio.Future] = {}
 
 
 
 
 
 
65
  from backend.agent.react_agent import Tool
66
  self.tools = []
67
  for name, func in TOOL_REGISTRY.items():
@@ -132,7 +139,10 @@ class ConnectionManager:
132
  },
133
  })
134
  if not delivered:
135
- return {"delivered": 0, "ok": False, "spoken": [], "timed_out": False}
 
 
 
136
  try:
137
  result = await asyncio.wait_for(future, timeout=timeout)
138
  except asyncio.TimeoutError:
@@ -151,6 +161,80 @@ class ConnectionManager:
151
  finally:
152
  self.pending_results.pop(command_id, None)
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  async def broadcast(self, message: dict):
155
  for connection in self.active_connections:
156
  try:
@@ -175,13 +259,20 @@ class ConnectionManager:
175
  "PC relay registered (%d PC connection(s) now available)",
176
  len(self.pc_connections),
177
  )
 
178
  try:
179
  await websocket.send_json({
180
  "event": "backend:pc_registered",
181
- "payload": {"ok": True},
 
182
  })
183
- except Exception:
184
- pass
 
 
 
 
 
185
  return
186
 
187
  # ── WHAT HAPPENED? ──────────────────────────────────────────────────────
 
2
  import json
3
  import logging
4
  import os
5
+ import time
6
  from fastapi import WebSocket
7
  import uuid
8
 
 
63
  # desktop then refused outright (locked session) or failed to run. The
64
  # phone showed success for work that never happened.
65
  self.pending_results: dict[str, asyncio.Future] = {}
66
+ # Commands accepted while NO PC was connected, replayed in order the
67
+ # moment one registers. Without this the honest answer to "open notepad"
68
+ # with the desktop asleep was "nothing was listening" and the command was
69
+ # simply dropped β€” the user had to remember it and send it again once the
70
+ # PC woke up.
71
+ self.pending_commands: list[dict] = []
72
  from backend.agent.react_agent import Tool
73
  self.tools = []
74
  for name, func in TOOL_REGISTRY.items():
 
139
  },
140
  })
141
  if not delivered:
142
+ queued = self.queue_for_offline_pc(cmd, device_name)
143
+ return {"delivered": 0, "ok": False, "spoken": [],
144
+ "timed_out": False, "queued": queued,
145
+ "queue_depth": len(self.pending_commands)}
146
  try:
147
  result = await asyncio.wait_for(future, timeout=timeout)
148
  except asyncio.TimeoutError:
 
161
  finally:
162
  self.pending_results.pop(command_id, None)
163
 
164
+ # Bounded on purpose. A PC that has been off for a week must not wake up and
165
+ # execute a hundred stale instructions in a burst β€” that is how "convenient"
166
+ # becomes "destructive". Oldest is dropped past the cap; anything older than
167
+ # the TTL is discarded rather than run.
168
+ QUEUE_MAX = 25
169
+ QUEUE_TTL_SECONDS = 12 * 3600
170
+
171
+ def queue_for_offline_pc(self, cmd: str, device_name: str = "") -> bool:
172
+ cmd = (cmd or "").strip()
173
+ if not cmd:
174
+ return False
175
+ # Unlock commands carry a password and are time-sensitive; replaying one
176
+ # later against a PC that may already be unlocked is pointless and would
177
+ # persist a credential in memory for hours. Never queue them.
178
+ if cmd.lower().startswith(("unlock", "activate")):
179
+ return False
180
+ self._expire_queue()
181
+ self.pending_commands.append({
182
+ "cmd": cmd,
183
+ "device_name": device_name,
184
+ "queued_at": time.time(),
185
+ })
186
+ if len(self.pending_commands) > self.QUEUE_MAX:
187
+ dropped = self.pending_commands.pop(0)
188
+ logging.warning("Offline command queue full; dropped %r",
189
+ dropped.get("cmd"))
190
+ logging.info("Queued %r for an offline PC (depth=%d)", cmd,
191
+ len(self.pending_commands))
192
+ return True
193
+
194
+ def _expire_queue(self) -> None:
195
+ cutoff = time.time() - self.QUEUE_TTL_SECONDS
196
+ before = len(self.pending_commands)
197
+ self.pending_commands = [
198
+ item for item in self.pending_commands
199
+ if float(item.get("queued_at") or 0) >= cutoff
200
+ ]
201
+ if len(self.pending_commands) != before:
202
+ logging.info("Expired %d stale queued command(s)",
203
+ before - len(self.pending_commands))
204
+
205
+ async def flush_pending_commands(self, websocket: WebSocket) -> int:
206
+ """Replay queued commands to a PC that just came online, in order."""
207
+ self._expire_queue()
208
+ if not self.pending_commands:
209
+ return 0
210
+ queued, self.pending_commands = self.pending_commands, []
211
+ sent = 0
212
+ for item in queued:
213
+ try:
214
+ await websocket.send_json({
215
+ "event": "system:execute",
216
+ "payload": {
217
+ "cmd": item["cmd"],
218
+ "source": "guardian_queued",
219
+ "device_name": item.get("device_name", ""),
220
+ "queued_at": item.get("queued_at"),
221
+ # No command_id: the HTTP caller that queued this is long
222
+ # gone, so there is no future to resolve. The PC still
223
+ # logs its own result.
224
+ },
225
+ })
226
+ sent += 1
227
+ except Exception as exc:
228
+ # Put back what we could not deliver, preserving order, so a
229
+ # half-failed flush does not silently eat the rest.
230
+ logging.warning("Flush failed after %d command(s): %s", sent, exc)
231
+ self.pending_commands = queued[sent:] + self.pending_commands
232
+ break
233
+ if sent:
234
+ logging.info("Replayed %d queued command(s) to the PC that came online",
235
+ sent)
236
+ return sent
237
+
238
  async def broadcast(self, message: dict):
239
  for connection in self.active_connections:
240
  try:
 
259
  "PC relay registered (%d PC connection(s) now available)",
260
  len(self.pc_connections),
261
  )
262
+ replayed = 0
263
  try:
264
  await websocket.send_json({
265
  "event": "backend:pc_registered",
266
+ "payload": {"ok": True,
267
+ "queued": len(self.pending_commands)},
268
  })
269
+ # The PC is back: run whatever was sent while it was away.
270
+ replayed = await self.flush_pending_commands(websocket)
271
+ except Exception as exc:
272
+ logging.warning("PC registration follow-up failed: %s", exc)
273
+ if replayed:
274
+ logging.info("PC came online and picked up %d queued command(s)",
275
+ replayed)
276
  return
277
 
278
  # ── WHAT HAPPENED? ──────────────────────────────────────────────────────
dist/app-release.apk CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3ad8ae06d51f018c394e07acd4492bd6d3ec89137891812596d50286595ecd3b
3
  size 116538395
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f6ff4329c1d9ab621312b87542baa2a90f5020db9131cb1601b5058ab50853d
3
  size 116538395
modules/activation_state.py CHANGED
@@ -9,6 +9,14 @@ from pathlib import Path
9
  ROOT = Path(__file__).resolve().parents[1]
10
  UNLOCK_JSON = ROOT / "data" / "session_unlocked.json"
11
 
 
 
 
 
 
 
 
 
12
 
13
  def _boot_id() -> int:
14
  try:
 
9
  ROOT = Path(__file__).resolve().parents[1]
10
  UNLOCK_JSON = ROOT / "data" / "session_unlocked.json"
11
 
12
+ # One wording for the refusal, shared by every caller that has to explain it β€”
13
+ # core.commands (voice/GUI) and phone.command_runner (phone and cloud relay) β€”
14
+ # so a remote user is never told something different from a local one.
15
+ LOCKED_MESSAGE = (
16
+ "This PC is locked. Activate JARVIS on the desktop with your "
17
+ "activation key, then send the command again."
18
+ )
19
+
20
 
21
  def _boot_id() -> int:
22
  try: